From 7594924d5edf853a68a3b4d8931d6adb8f6f5cfc Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 24 Sep 2022 14:57:44 +0200 Subject: [PATCH 001/531] bump version to 2.0 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index b7f0f9612..1257c2361 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "COBREXA" uuid = "babc4406-5200-4a30-9033-bf5ae714c842" authors = ["The developers of COBREXA.jl"] -version = "1.4.0" +version = "2.0.0" [deps] Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" From b5625434f8745d9b71843ae8e735f6bffb9a745d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 27 Jun 2022 12:13:47 +0200 Subject: [PATCH 002/531] remove lots of lb/ub shortcuts from API --- src/analysis/modifications/generic.jl | 6 +-- src/analysis/modifications/knockout.jl | 2 +- src/base/solver.jl | 8 ++-- src/base/types/Reaction.jl | 31 +++++++------ src/base/types/StandardModel.jl | 8 ++-- src/base/utils/StandardModel.jl | 4 +- src/io/show/Reaction.jl | 8 ++-- src/reconstruction/CoreModel.jl | 4 +- src/reconstruction/StandardModel.jl | 6 +-- src/reconstruction/community.jl | 8 ++-- .../gapfill_minimum_reactions.jl | 4 +- test/analysis/fba_with_crowding.jl | 2 +- test/analysis/flux_balance_analysis.jl | 4 +- test/analysis/flux_variability_analysis.jl | 4 +- test/analysis/gecko.jl | 2 +- test/analysis/knockouts.jl | 4 +- test/analysis/moment.jl | 2 +- .../parsimonious_flux_balance_analysis.jl | 2 +- test/base/types/Reaction.jl | 10 ++--- test/base/types/StandardModel.jl | 4 +- test/base/utils/StandardModel.jl | 4 +- test/reconstruction/CoreModel.jl | 2 +- test/reconstruction/Reaction.jl | 8 ++-- test/reconstruction/StandardModel.jl | 44 +++++++++---------- test/reconstruction/add_reactions.jl | 12 ++--- test/reconstruction/community.jl | 16 +++---- 26 files changed, 107 insertions(+), 102 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index bcf1c0b89..2a56c9cb6 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -15,13 +15,13 @@ constrain_objective_value(tolerance) = """ $(TYPEDSIGNATURES) -Change the lower and upper bounds (`lb` and `ub` respectively) of reaction `id` if supplied. +Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) of reaction `id` if supplied. """ -change_constraint(id::String; lb = nothing, ub = nothing) = +change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin ind = first(indexin([id], reactions(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!(ind, opt_model, lb = lb, ub = ub) + set_optmodel_bound!(ind, opt_model, lower = lower_bound, upper = upper_bound) end """ diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index b55715e61..d0e0f57aa 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -38,7 +38,7 @@ function _do_knockout(model::MetabolicModel, opt_model, gene_ids::Vector{String} rga = reaction_gene_association(model, rxn_id) if !isnothing(rga) && all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) - set_optmodel_bound!(rxn_num, opt_model, ub = 0, lb = 0) + set_optmodel_bound!(rxn_num, opt_model, lower = 0, upper = 0) end end end diff --git a/src/base/solver.jl b/src/base/solver.jl index 7d112e7d8..25fb9ed72 100644 --- a/src/base/solver.jl +++ b/src/base/solver.jl @@ -73,11 +73,11 @@ not be changed. function set_optmodel_bound!( vidx, opt_model; - lb::Maybe{Real} = nothing, - ub::Maybe{Real} = nothing, + lower::Maybe{Real} = nothing, + upper::Maybe{Real} = nothing, ) - isnothing(lb) || set_normalized_rhs(opt_model[:lbs][vidx], -lb) - isnothing(ub) || set_normalized_rhs(opt_model[:ubs][vidx], ub) + isnothing(lower) || set_normalized_rhs(opt_model[:lbs][vidx], -lower) + isnothing(upper) || set_normalized_rhs(opt_model[:ubs][vidx], upper) end """ diff --git a/src/base/types/Reaction.jl b/src/base/types/Reaction.jl index b87e1c45c..1ec28ee29 100644 --- a/src/base/types/Reaction.jl +++ b/src/base/types/Reaction.jl @@ -10,8 +10,8 @@ mutable struct Reaction id::String name::Maybe{String} metabolites::Dict{String,Float64} - lb::Float64 - ub::Float64 + lower_bound::Float64 + upper_bound::Float64 grr::Maybe{GeneAssociation} subsystem::Maybe{String} notes::Notes @@ -30,8 +30,8 @@ function Reaction( id = ""; name = nothing, metabolites = Dict{String,Float64}(), - lb = -_constants.default_reaction_bound, - ub = _constants.default_reaction_bound, + lower_bound = -_constants.default_reaction_bound, + upper_bound = _constants.default_reaction_bound, grr = nothing, subsystem = nothing, notes = Notes(), @@ -43,8 +43,8 @@ function Reaction( id, name, mets, - lb, - ub, + lower_bound, + upper_bound, grr, subsystem, notes, @@ -70,16 +70,21 @@ function Reaction( default_bound = _constants.default_reaction_bound, ) if dir == :forward - lb = 0.0 - ub = default_bound + lower_bound = 0.0 + upper_bound = default_bound elseif dir == :reverse - lb = -default_bound - ub = 0.0 + lower_bound = -default_bound + upper_bound = 0.0 elseif dir == :bidirectional - lb = -default_bound - ub = default_bound + lower_bound = -default_bound + upper_bound = default_bound else throw(DomainError(dir, "unsupported direction")) end - Reaction(id; metabolites = metabolites, lb = lb, ub = ub) + Reaction( + id; + metabolites = metabolites, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) end diff --git a/src/base/types/StandardModel.jl b/src/base/types/StandardModel.jl index 211a1e088..45be53177 100644 --- a/src/base/types/StandardModel.jl +++ b/src/base/types/StandardModel.jl @@ -144,7 +144,7 @@ $(TYPEDSIGNATURES) Return the lower bounds for all reactions in `model` in sparse format. """ lower_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].lb for rxn in reactions(model)]) + sparse([model.reactions[rxn].lower_bound for rxn in reactions(model)]) """ $(TYPEDSIGNATURES) @@ -153,7 +153,7 @@ Return the upper bounds for all reactions in `model` in sparse format. Order matches that of the reaction ids returned in `reactions()`. """ upper_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].ub for rxn in reactions(model)]) + sparse([model.reactions[rxn].upper_bound for rxn in reactions(model)]) """ $(TYPEDSIGNATURES) @@ -360,8 +360,8 @@ function Base.convert(::Type{StandardModel}, model::MetabolicModel) rid; name = reaction_name(model, rid), metabolites = rmets, - lb = lbs[i], - ub = ubs[i], + lower_bound = lbs[i], + upper_bound = ubs[i], grr = reaction_gene_association(model, rid), objective_coefficient = ocs[i], notes = reaction_notes(model, rid), diff --git a/src/base/utils/StandardModel.jl b/src/base/utils/StandardModel.jl index 3da400441..05e95504c 100644 --- a/src/base/utils/StandardModel.jl +++ b/src/base/utils/StandardModel.jl @@ -19,8 +19,8 @@ Shallow copy of a [`Reaction`](@ref) Base.copy(r::Reaction) = Reaction( r.id; metabolites = r.metabolites, - lb = r.lb, - ub = r.ub, + lower_bound = r.lower_bound, + upper_bound = r.upper_bound, grr = r.grr, subsystem = r.subsystem, notes = r.notes, diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl index b5e340615..752b9acbe 100644 --- a/src/io/show/Reaction.jl +++ b/src/io/show/Reaction.jl @@ -15,11 +15,11 @@ function _pretty_substances(ss::Vector{String})::String end function Base.show(io::IO, ::MIME"text/plain", r::Reaction) - if r.ub > 0.0 && r.lb < 0.0 + if r.upper_bound > 0.0 && r.lower_bound < 0.0 arrow = " ↔ " - elseif r.ub <= 0.0 && r.lb < 0.0 + elseif r.upper_bound <= 0.0 && r.lower_bound < 0.0 arrow = " ← " - elseif r.ub > 0.0 && r.lb >= 0.0 + elseif r.upper_bound > 0.0 && r.lower_bound >= 0.0 arrow = " → " else arrow = " →|← " # blocked reaction @@ -42,7 +42,7 @@ function Base.show(io::IO, ::MIME"text/plain", r::Reaction) "Reaction.$(string(fname)): ", _maybemap(x -> _unparse_grr(String, x), r.grr), ) - elseif fname in (:lb, :ub, :objective_coefficient) + elseif fname in (:lower_bound, :upper_bound, :objective_coefficient) _pretty_print_keyvals( io, "Reaction.$(string(fname)): ", diff --git a/src/reconstruction/CoreModel.jl b/src/reconstruction/CoreModel.jl index e791cce3e..905ef670a 100644 --- a/src/reconstruction/CoreModel.jl +++ b/src/reconstruction/CoreModel.jl @@ -19,8 +19,8 @@ function add_reactions!(model::CoreModel, rxns::Vector{Reaction}) push!(V, v) end push!(model.rxns, rxn.id) - lbs[j] = rxn.lb - ubs[j] = rxn.ub + lbs[j] = rxn.lower_bound + ubs[j] = rxn.upper_bound cs[j] = rxn.objective_coefficient end Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) diff --git a/src/reconstruction/StandardModel.jl b/src/reconstruction/StandardModel.jl index 89c77f8eb..e6c5c150b 100644 --- a/src/reconstruction/StandardModel.jl +++ b/src/reconstruction/StandardModel.jl @@ -97,12 +97,12 @@ macro add_reactions!(model::Symbol, ex::Expr) push!(all_reactions.args, :(r.id = $id)) if length(args) == 3 lb = args[3] - push!(all_reactions.args, :(r.lb = $lb)) + push!(all_reactions.args, :(r.lower_bound = $lb)) elseif length(args) == 4 lb = args[3] ub = args[4] - push!(all_reactions.args, :(r.lb = $lb)) - push!(all_reactions.args, :(r.ub = $ub)) + push!(all_reactions.args, :(r.lower_bound = $lb)) + push!(all_reactions.args, :(r.upper_bound = $ub)) end push!(all_reactions.args, :(add_reaction!($model, r))) end diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index a2ca155a5..492566847 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -52,8 +52,8 @@ function add_community_objective!( rdict = Dict(k => -float(v) for (k, v) in objective_mets_weights) rxn = Reaction(objective_id) rxn.metabolites = rdict - rxn.lb = 0.0 - rxn.ub = _constants.default_reaction_bound + rxn.lower_bound = 0.0 + rxn.upper_bound = _constants.default_reaction_bound rxn.objective_coefficient = 1.0 community.reactions[rxn.id] = rxn @@ -347,8 +347,8 @@ function join_with_exchanges( for (rid, mid) in exchange_rxn_mets community.reactions[rid] = Reaction(rid) community.reactions[rid].metabolites = Dict{String,Float64}(mid => -1.0) - community.reactions[rid].lb = 0.0 - community.reactions[rid].ub = 0.0 + community.reactions[rid].lower_bound = 0.0 + community.reactions[rid].upper_bound = 0.0 community.metabolites[mid] = Metabolite(mid) community.metabolites[mid].id = mid end diff --git a/src/reconstruction/gapfill_minimum_reactions.jl b/src/reconstruction/gapfill_minimum_reactions.jl index e3828e8fb..c853aeedc 100644 --- a/src/reconstruction/gapfill_minimum_reactions.jl +++ b/src/reconstruction/gapfill_minimum_reactions.jl @@ -203,8 +203,8 @@ function _universal_stoichiometry(urxns::Vector{Reaction}, mids::Vector{String}) length(urxns), ), ), - lbs = [rxn.lb for rxn in urxns], - ubs = [rxn.ub for rxn in urxns], + lbs = [rxn.lower_bound for rxn in urxns], + ubs = [rxn.upper_bound for rxn in urxns], new_mids = new_mids, ) end diff --git a/test/analysis/fba_with_crowding.jl b/test/analysis/fba_with_crowding.jl index 7ecb187b4..3650852ed 100644 --- a/test/analysis/fba_with_crowding.jl +++ b/test/analysis/fba_with_crowding.jl @@ -11,7 +11,7 @@ modifications = [ change_optimizer_attribute("IPM_IterationsLimit", 1000), add_crowding_constraints(rid_weight), - change_constraint("EX_glc__D_e"; lb = -6), + change_constraint("EX_glc__D_e"; lower_bound = -6), ], ) diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index 51215f321..fd4dd226b 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -39,7 +39,7 @@ end Tulip.Optimizer; modifications = [ change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), change_sense(MAX_SENSE), change_optimizer_attribute("IPM_IterationsLimit", 110), ], @@ -71,7 +71,7 @@ end @test_throws DomainError flux_balance_analysis_dict( model, Tulip.Optimizer; - modifications = [change_constraint("gbbrsh"; lb = -12, ub = -12)], + modifications = [change_constraint("gbbrsh"; lower_bound = -12, upper_bound = -12)], ) @test_throws DomainError flux_balance_analysis_dict( model, diff --git a/test/analysis/flux_variability_analysis.jl b/test/analysis/flux_variability_analysis.jl index 8d384ce5b..6dce4dd15 100644 --- a/test/analysis/flux_variability_analysis.jl +++ b/test/analysis/flux_variability_analysis.jl @@ -85,8 +85,8 @@ end bounds = objective_bounds(0.99), modifications = [ change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("EX_o2_e"; lb = 0.0, ub = 0.0), + change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), + change_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), ], ) diff --git a/test/analysis/gecko.jl b/test/analysis/gecko.jl index eab3a7083..7711f3e83 100644 --- a/test/analysis/gecko.jl +++ b/test/analysis/gecko.jl @@ -58,7 +58,7 @@ Tulip.Optimizer; modifications = [ change_objective(genes(gm); weights = [], sense = COBREXA.MIN_SENSE), - change_constraint("BIOMASS_Ecoli_core_w_GAM", lb = growth_lb), + change_constraint("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb), change_optimizer_attribute("IPM_IterationsLimit", 1000), ], ) diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index 6d59ecc8c..3ac0cf7e4 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -104,7 +104,7 @@ end Tulip.Optimizer; modifications = [ change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), change_sense(MAX_SENSE), change_optimizer_attribute("IPM_IterationsLimit", 110), knockout(["b0978", "b0734"]), # knockouts out cytbd @@ -121,7 +121,7 @@ end Tulip.Optimizer; modifications = [ change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lb = -12, ub = -12), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), change_sense(MAX_SENSE), change_optimizer_attribute("IPM_IterationsLimit", 110), knockout("b2779"), # knockouts out enolase diff --git a/test/analysis/moment.jl b/test/analysis/moment.jl index d8a0264b2..991197619 100644 --- a/test/analysis/moment.jl +++ b/test/analysis/moment.jl @@ -9,7 +9,7 @@ Tulip.Optimizer; modifications = [ add_moment_constraints(ksas, protein_mass_fraction;), - change_constraint("EX_glc__D_e", lb = -1000), + change_constraint("EX_glc__D_e", lower_bound = -1000), ], ) diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index a0775e51f..0db2d0935 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -5,7 +5,7 @@ model, Tulip.Optimizer; modifications = [ - change_constraint("EX_m1(e)", lb = -10.0), + change_constraint("EX_m1(e)", lower_bound = -10.0), change_optimizer_attribute("IPM_IterationsLimit", 500), ], qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], diff --git a/test/base/types/Reaction.jl b/test/base/types/Reaction.jl index 3fa5a1c07..d4644da9b 100644 --- a/test/base/types/Reaction.jl +++ b/test/base/types/Reaction.jl @@ -21,8 +21,8 @@ r1 = Reaction() r1.id = "r1" r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lb = -100.0 - r1.ub = 100.0 + r1.lower_bound = -100.0 + r1.upper_bound = 100.0 r1.grr = [["g1", "g2"], ["g3"]] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) @@ -77,14 +77,14 @@ @test occursin("...", sprint(show, MIME("text/plain"), rlongrev)) r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) - @test r2.lb == -1000.0 && r2.ub == 0.0 + @test r2.lower_bound == -1000.0 && r2.upper_bound == 0.0 r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) - @test r3.lb == 0.0 && r3.ub == 1000.0 + @test r3.lower_bound == 0.0 && r3.upper_bound == 1000.0 r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - @test r4.lb == -1000.0 && r4.ub == 1000.0 + @test r4.lower_bound == -1000.0 && r4.upper_bound == 1000.0 rd = OrderedDict(r.id => r for r in [r1, r2, r3, r4]) @test issetequal(["r1", "r4"], ambiguously_identified_items(annotation_index(rd))) diff --git a/test/base/types/StandardModel.jl b/test/base/types/StandardModel.jl index ef836eec7..4bdf28165 100644 --- a/test/base/types/StandardModel.jl +++ b/test/base/types/StandardModel.jl @@ -20,8 +20,8 @@ r1 = Reaction() r1.id = "r1" r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lb = -100.0 - r1.ub = 100.0 + r1.lower_bound = -100.0 + r1.upper_bound = 100.0 r1.grr = [["g1", "g2"], ["g3"]] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) diff --git a/test/base/utils/StandardModel.jl b/test/base/utils/StandardModel.jl index 6d80bc7a3..f5b1333c5 100644 --- a/test/base/utils/StandardModel.jl +++ b/test/base/utils/StandardModel.jl @@ -15,10 +15,10 @@ glucose_index = first(indexin(["EX_glc__D_e"], reactions(model))) o2_index = first(indexin(["EX_o2_e"], reactions(model))) atpm_index = first(indexin(["ATPM"], reactions(model))) - set_optmodel_bound!(glucose_index, cbm; ub = -1.0, lb = -1.0) + set_optmodel_bound!(glucose_index, cbm; upper = -1.0, lower = -1.0) @test normalized_rhs(ubs[glucose_index]) == -1.0 @test normalized_rhs(lbs[glucose_index]) == 1.0 - set_optmodel_bound!(o2_index, cbm; ub = 1.0, lb = 1.0) + set_optmodel_bound!(o2_index, cbm; upper = 1.0, lower = 1.0) @test normalized_rhs(ubs[o2_index]) == 1.0 @test normalized_rhs(lbs[o2_index]) == -1.0 end diff --git a/test/reconstruction/CoreModel.jl b/test/reconstruction/CoreModel.jl index 1a72a826e..c8a58ae9e 100644 --- a/test/reconstruction/CoreModel.jl +++ b/test/reconstruction/CoreModel.jl @@ -236,7 +236,7 @@ end rxn1 = Reaction("nr1"; metabolites = Dict("m1[c]" => -1, "m3[c]" => 1)) rxn2 = Reaction("nr2"; metabolites = Dict("m1[c]" => -1, "m2[c]" => 1)) rxn3 = Reaction("nr3"; metabolites = Dict("m2[c]" => -1, "m3[c]" => 1)) - rxn3.lb = 10 + rxn3.lower_bound = 10 add_reaction!(toymodel, rxn1) @test toymodel.S[1, 8] == -1 diff --git a/test/reconstruction/Reaction.jl b/test/reconstruction/Reaction.jl index 7d193fd21..b57b468fe 100644 --- a/test/reconstruction/Reaction.jl +++ b/test/reconstruction/Reaction.jl @@ -10,13 +10,13 @@ h_p = model.metabolites["h_p"] rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb == 0.0 && rxn.ub > 0.0 + @test rxn.lower_bound == 0.0 && rxn.upper_bound > 0.0 rxn = 1.0 * nadh + 4.0 * h_c + q8 ← 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb < 0.0 && rxn.ub == 0.0 + @test rxn.lower_bound < 0.0 && rxn.upper_bound == 0.0 rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 ↔ q8h2 + nad + 3.0 * h_p - @test rxn.lb < 0.0 && rxn.ub > 0.0 + @test rxn.lower_bound < 0.0 && rxn.upper_bound > 0.0 rxn = 1.0 * nadh → nothing @test length(rxn.metabolites) == 1 @@ -32,5 +32,5 @@ @test ("q8h2_c" in [x for x in keys(rxn.metabolites)]) rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lb == 0.0 && rxn.ub > 0.0 + @test rxn.lower_bound == 0.0 && rxn.upper_bound > 0.0 end diff --git a/test/reconstruction/StandardModel.jl b/test/reconstruction/StandardModel.jl index 24c602381..97ab4a3df 100644 --- a/test/reconstruction/StandardModel.jl +++ b/test/reconstruction/StandardModel.jl @@ -34,42 +34,42 @@ # change bound tests - in place change_bound!(model, "r2"; lower = -10, upper = 10) - @test model.reactions["r2"].lb == -10 - @test model.reactions["r2"].ub == 10 + @test model.reactions["r2"].lower_bound == -10 + @test model.reactions["r2"].upper_bound == 10 change_bound!(model, "r2"; lower = -100) - @test model.reactions["r2"].lb == -100 - @test model.reactions["r2"].ub == 10 + @test model.reactions["r2"].lower_bound == -100 + @test model.reactions["r2"].upper_bound == 10 change_bound!(model, "r2"; upper = 111) - @test model.reactions["r2"].lb == -100 - @test model.reactions["r2"].ub == 111 + @test model.reactions["r2"].lower_bound == -100 + @test model.reactions["r2"].upper_bound == 111 change_bounds!(model, ["r1", "r2"]; lower = [-110, -220], upper = [110.0, 220.0]) - @test model.reactions["r1"].lb == -110 - @test model.reactions["r1"].ub == 110 - @test model.reactions["r2"].lb == -220 - @test model.reactions["r2"].ub == 220 + @test model.reactions["r1"].lower_bound == -110 + @test model.reactions["r1"].upper_bound == 110 + @test model.reactions["r2"].lower_bound == -220 + @test model.reactions["r2"].upper_bound == 220 # change bound - new model new_model = change_bound(model, "r2"; lower = -10, upper = 10) - @test new_model.reactions["r2"].lb == -10 - @test new_model.reactions["r2"].ub == 10 + @test new_model.reactions["r2"].lower_bound == -10 + @test new_model.reactions["r2"].upper_bound == 10 new_model = change_bound(model, "r2"; lower = -10) - @test new_model.reactions["r2"].lb == -10 - @test new_model.reactions["r2"].ub == 220 + @test new_model.reactions["r2"].lower_bound == -10 + @test new_model.reactions["r2"].upper_bound == 220 new_model = change_bounds(model, ["r1", "r2"]; lower = [-10, -20], upper = [10.0, 20.0]) - @test new_model.reactions["r1"].lb == -10 - @test new_model.reactions["r1"].ub == 10 - @test new_model.reactions["r2"].lb == -20 - @test new_model.reactions["r2"].ub == 20 + @test new_model.reactions["r1"].lower_bound == -10 + @test new_model.reactions["r1"].upper_bound == 10 + @test new_model.reactions["r2"].lower_bound == -20 + @test new_model.reactions["r2"].upper_bound == 20 new_model = change_bounds(model, ["r1", "r2"]; lower = [-10, nothing], upper = [nothing, 20.0]) - @test new_model.reactions["r1"].lb == -10 - @test new_model.reactions["r1"].ub == 110 - @test new_model.reactions["r2"].lb == -220 - @test new_model.reactions["r2"].ub == 20 + @test new_model.reactions["r1"].lower_bound == -10 + @test new_model.reactions["r1"].upper_bound == 110 + @test new_model.reactions["r2"].lower_bound == -220 + @test new_model.reactions["r2"].upper_bound == 20 ### reactions add_reactions!(model, [r3, r4]) diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl index 823432f37..53ad43d89 100644 --- a/test/reconstruction/add_reactions.jl +++ b/test/reconstruction/add_reactions.jl @@ -12,14 +12,14 @@ end rxn = mod.reactions["v1"] - @test rxn.lb == -1000.0 - @test rxn.ub == 1000.0 + @test rxn.lower_bound == -1000.0 + @test rxn.upper_bound == 1000.0 rxn = mod.reactions["v2"] - @test rxn.lb == -500 - @test rxn.ub == 1000.0 + @test rxn.lower_bound == -500 + @test rxn.upper_bound == 1000.0 rxn = mod.reactions["v3"] - @test rxn.lb == -500 - @test rxn.ub == 500 + @test rxn.lower_bound == -500 + @test rxn.upper_bound == 500 end diff --git a/test/reconstruction/community.jl b/test/reconstruction/community.jl index fc532fb10..fc0875799 100644 --- a/test/reconstruction/community.jl +++ b/test/reconstruction/community.jl @@ -20,9 +20,9 @@ # test if environmental exchanges have been added properly @test c1.S[13, 15] == c1.S[14, 16] == -1 # test of bounds set properly - lb, ub = bounds(c1) - @test all(lb[1:14] .== -ub[1:14] .== -1000) - @test all(lb[15:16] .== -ub[15:16] .== 0.0) + lower_bound, upper_bound = bounds(c1) + @test all(lower_bound[1:14] .== -upper_bound[1:14] .== -1000) + @test all(lower_bound[15:16] .== -upper_bound[15:16] .== 0.0) add_community_objective!( c1, @@ -202,9 +202,9 @@ end @test c1.reactions["species_2_EX_m3(e)"].metabolites["species_2_m3[e]"] == 1 # test of bounds set properly - lb, ub = bounds(c1) # this only works because the insertion order is preserved (they get added last) - @test all(lb[1:14] .== -ub[1:14] .== -1000) - @test all(lb[15:16] .== -ub[15:16] .== 0.0) + lower_bound, upper_bound = bounds(c1) # this only works because the insertion order is preserved (they get added last) + @test all(lower_bound[1:14] .== -upper_bound[1:14] .== -1000) + @test all(lower_bound[15:16] .== -upper_bound[15:16] .== 0.0) add_community_objective!( c1, @@ -243,8 +243,8 @@ end ) for rid in keys(exchange_rxn_mets) - c.reactions[rid].lb = m1.reactions[rid].lb - c.reactions[rid].ub = m1.reactions[rid].ub + c.reactions[rid].lower_bound = m1.reactions[rid].lower_bound + c.reactions[rid].upper_bound = m1.reactions[rid].upper_bound end @test c.reactions["species_1_BIOMASS_Ecoli_core_w_GAM"].metabolites["species_1_BIOMASS_Ecoli_core_w_GAM"] == From 8d31ff39b7cfe672c88c3ee400cd35faeb9a52ca Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 21 Jul 2022 11:23:05 +0200 Subject: [PATCH 003/531] fixes --- src/analysis/flux_variability_analysis.jl | 4 ++-- src/analysis/minimize_metabolic_adjustment.jl | 2 +- src/reconstruction/StandardModel.jl | 4 ++-- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index b540ec598..53a4ad7b5 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -144,8 +144,8 @@ mins, maxs = flux_variability_analysis_dict( bounds = objective_bounds(0.99), modifications = [ change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("EX_o2_e"; lb = 0, ub = 0), + change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), + change_constraint("EX_o2_e"; lower_bound = 0, upper_bound = 0), ], ) ``` diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index e51e0ea0d..386300fd2 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -27,7 +27,7 @@ optmodel = minimize_metabolic_adjustment( model, flux_ref, Gurobi.Optimizer; - modifications = [change_constraint("PFL"; lb=0, ub=0)], # find flux of mutant that is closest to the wild type (reference) model + modifications = [change_constraint("PFL"; lower_bound=0, upper_bound=0)], # find flux of mutant that is closest to the wild type (reference) model ) value.(solution[:x]) # extract the flux from the optimizer ``` diff --git a/src/reconstruction/StandardModel.jl b/src/reconstruction/StandardModel.jl index e6c5c150b..7f0a75e52 100644 --- a/src/reconstruction/StandardModel.jl +++ b/src/reconstruction/StandardModel.jl @@ -155,8 +155,8 @@ remove_gene!(model::StandardModel, gid::String; knockout_reactions::Bool = false @_change_bounds_fn StandardModel String inplace begin - isnothing(lower) || (model.reactions[rxn_id].lb = lower) - isnothing(upper) || (model.reactions[rxn_id].ub = upper) + isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) + isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) nothing end From c8efa5ad9530e0eeb1a32672c93535dba75d538c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 24 Sep 2022 15:10:59 +0200 Subject: [PATCH 004/531] test building docs in `next` branch --- .gitlab-ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 04e7eae05..a45a8b9ab 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -29,6 +29,7 @@ variables: .global_trigger_build_doc: &global_trigger_build_doc rules: - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "master" && $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME == "develop" + - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "develop" && $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME == "next" - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" when: never - if: $CI_COMMIT_BRANCH == "develop" From aa7285c6e1ecccdb4947d98f91fbd8cbf117d61c Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 26 Sep 2022 09:01:39 +0200 Subject: [PATCH 005/531] generalize the module export thing in a macro --- src/COBREXA.jl | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 9e75888f9..505b91ead 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -78,12 +78,19 @@ _inc_all.( ), ) -# export everything that isn't prefixed with _ (inspired by JuMP.jl, thanks!) -for sym in names(@__MODULE__, all = true) - if sym in [Symbol(@__MODULE__), :eval, :include] || startswith(string(sym), ['_', '#']) - continue +# export everything from the local namespace that seems exportable +# (inspired by JuMP.jl, thanks!) +macro _export_locals() + quote + for sym in names(@__MODULE__, all = true) + sym in [Symbol(@__MODULE__), :eval, :include] && continue + startswith(string(sym), ['_', '#']) && continue + sym == :Internal && continue + @eval export $(Expr(:$, :sym)) + end end - @eval export $sym end +@_export_locals + end # module From 67e265962bb7761bb9de9630d22e6221d7a6e914 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 09:06:45 +0200 Subject: [PATCH 006/531] it's always one ontology --- src/COBREXA.jl | 2 +- src/base/{ontologies => ontology}/SBOTerms.jl | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/base/{ontologies => ontology}/SBOTerms.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 505b91ead..5bdff2f7a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -64,7 +64,7 @@ _inc_all.( joinpath("base", "macros"), joinpath("base", "types"), joinpath("base", "types", "wrappers"), - joinpath("base", "ontologies"), + joinpath("base", "ontology"), "base", "io", joinpath("io", "show"), diff --git a/src/base/ontologies/SBOTerms.jl b/src/base/ontology/SBOTerms.jl similarity index 100% rename from src/base/ontologies/SBOTerms.jl rename to src/base/ontology/SBOTerms.jl From 505fd99dd7deaa80a4460fe2b910311bff0bc889 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 14:35:19 +0200 Subject: [PATCH 007/531] squash `src/base/` to `src/` --- src/COBREXA.jl | 14 +++++++------- src/{base/logging => }/log.jl | 0 src/{base => }/macros/change_bounds.jl | 0 src/{base => }/macros/is_xxx_reaction.jl | 0 src/{base => }/macros/model_wrapper.jl | 0 src/{base => }/macros/remove_item.jl | 0 src/{base => }/macros/serialized.jl | 0 src/{base => }/ontology/SBOTerms.jl | 0 src/{base => }/types/CoreModel.jl | 0 src/{base => }/types/CoreModelCoupled.jl | 0 src/{base => }/types/FluxSummary.jl | 0 src/{base => }/types/FluxVariabilitySummary.jl | 0 src/{base => }/types/Gene.jl | 0 src/{base => }/types/HDF5Model.jl | 0 src/{base => }/types/Isozyme.jl | 0 src/{base => }/types/JSONModel.jl | 0 src/{base => }/types/MATModel.jl | 0 src/{base => }/types/MetabolicModel.jl | 0 src/{base => }/types/Metabolite.jl | 0 src/{base => }/types/ModelWrapper.jl | 0 src/{base => }/types/Reaction.jl | 0 src/{base => }/types/ReactionStatus.jl | 0 src/{base => }/types/SBMLModel.jl | 0 src/{base => }/types/Serialized.jl | 0 src/{base => }/types/StandardModel.jl | 0 src/{base => }/types/abstract/Maybe.jl | 0 src/{base => }/types/abstract/MetabolicModel.jl | 0 src/{base => }/types/wrappers/GeckoModel.jl | 0 src/{base => }/types/wrappers/SMomentModel.jl | 0 src/{base => }/utils/Annotation.jl | 0 src/{base => }/utils/CoreModel.jl | 0 src/{base => }/utils/HDF5Model.jl | 0 src/{base => }/utils/Reaction.jl | 0 src/{base => }/utils/Serialized.jl | 0 src/{base => }/utils/StandardModel.jl | 0 src/{base => }/utils/bounds.jl | 0 src/{base => }/utils/chemical_formulas.jl | 0 src/{base => }/utils/enzymes.jl | 0 src/{base => }/utils/fluxes.jl | 0 src/{base => }/utils/gecko.jl | 0 src/{base => }/utils/gene_associations.jl | 0 src/{base => }/utils/guesskey.jl | 0 src/{base => }/utils/looks_like.jl | 0 src/{base => }/utils/smoment.jl | 0 44 files changed, 7 insertions(+), 7 deletions(-) rename src/{base/logging => }/log.jl (100%) rename src/{base => }/macros/change_bounds.jl (100%) rename src/{base => }/macros/is_xxx_reaction.jl (100%) rename src/{base => }/macros/model_wrapper.jl (100%) rename src/{base => }/macros/remove_item.jl (100%) rename src/{base => }/macros/serialized.jl (100%) rename src/{base => }/ontology/SBOTerms.jl (100%) rename src/{base => }/types/CoreModel.jl (100%) rename src/{base => }/types/CoreModelCoupled.jl (100%) rename src/{base => }/types/FluxSummary.jl (100%) rename src/{base => }/types/FluxVariabilitySummary.jl (100%) rename src/{base => }/types/Gene.jl (100%) rename src/{base => }/types/HDF5Model.jl (100%) rename src/{base => }/types/Isozyme.jl (100%) rename src/{base => }/types/JSONModel.jl (100%) rename src/{base => }/types/MATModel.jl (100%) rename src/{base => }/types/MetabolicModel.jl (100%) rename src/{base => }/types/Metabolite.jl (100%) rename src/{base => }/types/ModelWrapper.jl (100%) rename src/{base => }/types/Reaction.jl (100%) rename src/{base => }/types/ReactionStatus.jl (100%) rename src/{base => }/types/SBMLModel.jl (100%) rename src/{base => }/types/Serialized.jl (100%) rename src/{base => }/types/StandardModel.jl (100%) rename src/{base => }/types/abstract/Maybe.jl (100%) rename src/{base => }/types/abstract/MetabolicModel.jl (100%) rename src/{base => }/types/wrappers/GeckoModel.jl (100%) rename src/{base => }/types/wrappers/SMomentModel.jl (100%) rename src/{base => }/utils/Annotation.jl (100%) rename src/{base => }/utils/CoreModel.jl (100%) rename src/{base => }/utils/HDF5Model.jl (100%) rename src/{base => }/utils/Reaction.jl (100%) rename src/{base => }/utils/Serialized.jl (100%) rename src/{base => }/utils/StandardModel.jl (100%) rename src/{base => }/utils/bounds.jl (100%) rename src/{base => }/utils/chemical_formulas.jl (100%) rename src/{base => }/utils/enzymes.jl (100%) rename src/{base => }/utils/fluxes.jl (100%) rename src/{base => }/utils/gecko.jl (100%) rename src/{base => }/utils/gene_associations.jl (100%) rename src/{base => }/utils/guesskey.jl (100%) rename src/{base => }/utils/looks_like.jl (100%) rename src/{base => }/utils/smoment.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 5bdff2f7a..c2d0b4dff 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -59,12 +59,12 @@ _inc_all.( joinpath.( @__DIR__, [ - joinpath("base", "types", "abstract"), - joinpath("base", "logging"), - joinpath("base", "macros"), - joinpath("base", "types"), - joinpath("base", "types", "wrappers"), - joinpath("base", "ontology"), + joinpath("types", "abstract"), + joinpath("logging"), + joinpath("macros"), + joinpath("types"), + joinpath("types", "wrappers"), + joinpath("ontology"), "base", "io", joinpath("io", "show"), @@ -73,7 +73,7 @@ _inc_all.( "analysis", joinpath("analysis", "modifications"), joinpath("analysis", "sampling"), - joinpath("base", "utils"), + joinpath("utils"), ], ), ) diff --git a/src/base/logging/log.jl b/src/log.jl similarity index 100% rename from src/base/logging/log.jl rename to src/log.jl diff --git a/src/base/macros/change_bounds.jl b/src/macros/change_bounds.jl similarity index 100% rename from src/base/macros/change_bounds.jl rename to src/macros/change_bounds.jl diff --git a/src/base/macros/is_xxx_reaction.jl b/src/macros/is_xxx_reaction.jl similarity index 100% rename from src/base/macros/is_xxx_reaction.jl rename to src/macros/is_xxx_reaction.jl diff --git a/src/base/macros/model_wrapper.jl b/src/macros/model_wrapper.jl similarity index 100% rename from src/base/macros/model_wrapper.jl rename to src/macros/model_wrapper.jl diff --git a/src/base/macros/remove_item.jl b/src/macros/remove_item.jl similarity index 100% rename from src/base/macros/remove_item.jl rename to src/macros/remove_item.jl diff --git a/src/base/macros/serialized.jl b/src/macros/serialized.jl similarity index 100% rename from src/base/macros/serialized.jl rename to src/macros/serialized.jl diff --git a/src/base/ontology/SBOTerms.jl b/src/ontology/SBOTerms.jl similarity index 100% rename from src/base/ontology/SBOTerms.jl rename to src/ontology/SBOTerms.jl diff --git a/src/base/types/CoreModel.jl b/src/types/CoreModel.jl similarity index 100% rename from src/base/types/CoreModel.jl rename to src/types/CoreModel.jl diff --git a/src/base/types/CoreModelCoupled.jl b/src/types/CoreModelCoupled.jl similarity index 100% rename from src/base/types/CoreModelCoupled.jl rename to src/types/CoreModelCoupled.jl diff --git a/src/base/types/FluxSummary.jl b/src/types/FluxSummary.jl similarity index 100% rename from src/base/types/FluxSummary.jl rename to src/types/FluxSummary.jl diff --git a/src/base/types/FluxVariabilitySummary.jl b/src/types/FluxVariabilitySummary.jl similarity index 100% rename from src/base/types/FluxVariabilitySummary.jl rename to src/types/FluxVariabilitySummary.jl diff --git a/src/base/types/Gene.jl b/src/types/Gene.jl similarity index 100% rename from src/base/types/Gene.jl rename to src/types/Gene.jl diff --git a/src/base/types/HDF5Model.jl b/src/types/HDF5Model.jl similarity index 100% rename from src/base/types/HDF5Model.jl rename to src/types/HDF5Model.jl diff --git a/src/base/types/Isozyme.jl b/src/types/Isozyme.jl similarity index 100% rename from src/base/types/Isozyme.jl rename to src/types/Isozyme.jl diff --git a/src/base/types/JSONModel.jl b/src/types/JSONModel.jl similarity index 100% rename from src/base/types/JSONModel.jl rename to src/types/JSONModel.jl diff --git a/src/base/types/MATModel.jl b/src/types/MATModel.jl similarity index 100% rename from src/base/types/MATModel.jl rename to src/types/MATModel.jl diff --git a/src/base/types/MetabolicModel.jl b/src/types/MetabolicModel.jl similarity index 100% rename from src/base/types/MetabolicModel.jl rename to src/types/MetabolicModel.jl diff --git a/src/base/types/Metabolite.jl b/src/types/Metabolite.jl similarity index 100% rename from src/base/types/Metabolite.jl rename to src/types/Metabolite.jl diff --git a/src/base/types/ModelWrapper.jl b/src/types/ModelWrapper.jl similarity index 100% rename from src/base/types/ModelWrapper.jl rename to src/types/ModelWrapper.jl diff --git a/src/base/types/Reaction.jl b/src/types/Reaction.jl similarity index 100% rename from src/base/types/Reaction.jl rename to src/types/Reaction.jl diff --git a/src/base/types/ReactionStatus.jl b/src/types/ReactionStatus.jl similarity index 100% rename from src/base/types/ReactionStatus.jl rename to src/types/ReactionStatus.jl diff --git a/src/base/types/SBMLModel.jl b/src/types/SBMLModel.jl similarity index 100% rename from src/base/types/SBMLModel.jl rename to src/types/SBMLModel.jl diff --git a/src/base/types/Serialized.jl b/src/types/Serialized.jl similarity index 100% rename from src/base/types/Serialized.jl rename to src/types/Serialized.jl diff --git a/src/base/types/StandardModel.jl b/src/types/StandardModel.jl similarity index 100% rename from src/base/types/StandardModel.jl rename to src/types/StandardModel.jl diff --git a/src/base/types/abstract/Maybe.jl b/src/types/abstract/Maybe.jl similarity index 100% rename from src/base/types/abstract/Maybe.jl rename to src/types/abstract/Maybe.jl diff --git a/src/base/types/abstract/MetabolicModel.jl b/src/types/abstract/MetabolicModel.jl similarity index 100% rename from src/base/types/abstract/MetabolicModel.jl rename to src/types/abstract/MetabolicModel.jl diff --git a/src/base/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl similarity index 100% rename from src/base/types/wrappers/GeckoModel.jl rename to src/types/wrappers/GeckoModel.jl diff --git a/src/base/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl similarity index 100% rename from src/base/types/wrappers/SMomentModel.jl rename to src/types/wrappers/SMomentModel.jl diff --git a/src/base/utils/Annotation.jl b/src/utils/Annotation.jl similarity index 100% rename from src/base/utils/Annotation.jl rename to src/utils/Annotation.jl diff --git a/src/base/utils/CoreModel.jl b/src/utils/CoreModel.jl similarity index 100% rename from src/base/utils/CoreModel.jl rename to src/utils/CoreModel.jl diff --git a/src/base/utils/HDF5Model.jl b/src/utils/HDF5Model.jl similarity index 100% rename from src/base/utils/HDF5Model.jl rename to src/utils/HDF5Model.jl diff --git a/src/base/utils/Reaction.jl b/src/utils/Reaction.jl similarity index 100% rename from src/base/utils/Reaction.jl rename to src/utils/Reaction.jl diff --git a/src/base/utils/Serialized.jl b/src/utils/Serialized.jl similarity index 100% rename from src/base/utils/Serialized.jl rename to src/utils/Serialized.jl diff --git a/src/base/utils/StandardModel.jl b/src/utils/StandardModel.jl similarity index 100% rename from src/base/utils/StandardModel.jl rename to src/utils/StandardModel.jl diff --git a/src/base/utils/bounds.jl b/src/utils/bounds.jl similarity index 100% rename from src/base/utils/bounds.jl rename to src/utils/bounds.jl diff --git a/src/base/utils/chemical_formulas.jl b/src/utils/chemical_formulas.jl similarity index 100% rename from src/base/utils/chemical_formulas.jl rename to src/utils/chemical_formulas.jl diff --git a/src/base/utils/enzymes.jl b/src/utils/enzymes.jl similarity index 100% rename from src/base/utils/enzymes.jl rename to src/utils/enzymes.jl diff --git a/src/base/utils/fluxes.jl b/src/utils/fluxes.jl similarity index 100% rename from src/base/utils/fluxes.jl rename to src/utils/fluxes.jl diff --git a/src/base/utils/gecko.jl b/src/utils/gecko.jl similarity index 100% rename from src/base/utils/gecko.jl rename to src/utils/gecko.jl diff --git a/src/base/utils/gene_associations.jl b/src/utils/gene_associations.jl similarity index 100% rename from src/base/utils/gene_associations.jl rename to src/utils/gene_associations.jl diff --git a/src/base/utils/guesskey.jl b/src/utils/guesskey.jl similarity index 100% rename from src/base/utils/guesskey.jl rename to src/utils/guesskey.jl diff --git a/src/base/utils/looks_like.jl b/src/utils/looks_like.jl similarity index 100% rename from src/base/utils/looks_like.jl rename to src/utils/looks_like.jl diff --git a/src/base/utils/smoment.jl b/src/utils/smoment.jl similarity index 100% rename from src/base/utils/smoment.jl rename to src/utils/smoment.jl From 5c2633589a1ff0fb4ad9668d7d6c00fec1775eba Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 15:15:23 +0200 Subject: [PATCH 008/531] start reorganizing stuff into modules (types + io) --- src/COBREXA.jl | 65 ++++++++++++++----- .../parsimonious_flux_balance_analysis.jl | 2 +- src/io.jl | 11 ++++ src/io/json.jl | 2 +- src/io/mat.jl | 4 +- src/io/sbml.jl | 2 +- src/log.jl | 42 ++++++++---- src/macros.jl | 9 +++ src/macros/model_wrapper.jl | 10 +-- src/reconstruction/StandardModel.jl | 4 +- src/types.jl | 17 +++++ src/types/CoreModelCoupled.jl | 4 +- src/types/ModelWrapper.jl | 8 +-- src/types/SBMLModel.jl | 4 +- src/utils/gene_associations.jl | 2 +- 15 files changed, 136 insertions(+), 50 deletions(-) create mode 100644 src/io.jl create mode 100644 src/macros.jl create mode 100644 src/types.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index c2d0b4dff..741368cf3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -39,21 +39,62 @@ using Serialization using SparseArrays using StableRNGs using Statistics -using DocStringExtensions import Base: findfirst, getindex, show import Pkg import SBML # conflict with Reaction struct name +# versioning tools const _PKG_ROOT_DIR = normpath(joinpath(@__DIR__, "..")) include_dependency(joinpath(_PKG_ROOT_DIR, "Project.toml")) const COBREXA_VERSION = VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) -# autoloading -const _inc(path...) = include(joinpath(path...)) -const _inc_all(dir) = _inc.(joinpath.(dir, filter(fn -> endswith(fn, ".jl"), readdir(dir)))) +module ModuleTools +macro inc(path...) + esc(:(include(joinpath(@__DIR__, $(joinpath(String.(path)...) * ".jl"))))) +end + +macro inc_dir(path...) + dir = joinpath(@__DIR__, String.(path)...) + files = filter(endswith(".jl"), readdir(dir; join = true)) + esc(Expr(:block, (:(include($f)) for f in files)...)) +end + +macro dse() + :(using DocStringExtensions) +end + +# export everything from the local namespace that seems exportable +# (inspired by JuMP.jl, thanks!) +macro export_locals() + quote + for sym in names(@__MODULE__; all = true, imported = true) + sym in [Symbol(@__MODULE__), :eval, :include] && continue + startswith(string(sym), ['_', '#']) && continue + sym == :Internal && continue + @eval export $(Expr(:$, :sym)) + end + end +end + +@export_locals +end + +# start loading the individual modules +module Internal +using ..ModuleTools +@inc macros +end + +using .ModuleTools + +@inc log +@inc types +@inc io + +#= TODOs here _inc_all.( joinpath.( @@ -77,20 +118,8 @@ _inc_all.( ], ), ) +=# -# export everything from the local namespace that seems exportable -# (inspired by JuMP.jl, thanks!) -macro _export_locals() - quote - for sym in names(@__MODULE__, all = true) - sym in [Symbol(@__MODULE__), :eval, :include] && continue - startswith(string(sym), ['_', '#']) && continue - sym == :Internal && continue - @eval export $(Expr(:$, :sym)) - end - end -end - -@_export_locals +@export_locals end # module diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 8d06f4d45..f2a9c9975 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -73,7 +73,7 @@ function parsimonious_flux_balance_analysis( for rb in relax_bounds lb, ub = objective_bounds(rb)(Z) - @_models_log @info "pFBA step relaxed to [$lb,$ub]" + @models_log @info "pFBA step relaxed to [$lb,$ub]" @constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) optimize!(opt_model) diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 000000000..d351691af --- /dev/null +++ b/src/io.jl @@ -0,0 +1,11 @@ +module IO +using ..ModuleTools +@dse + +using ..Types +using ..Log: @io_log + +@inc_dir io + +@export_locals +end diff --git a/src/io/json.jl b/src/io/json.jl index 918267a5c..51e6a03fb 100644 --- a/src/io/json.jl +++ b/src/io/json.jl @@ -18,7 +18,7 @@ function save_json_model(model::MetabolicModel, file_name::String) m = typeof(model) == JSONModel ? model : begin - @_io_log @warn "Automatically converting $(typeof(model)) to JSONModel for saving, information may be lost." + @io_log @warn "Automatically converting $(typeof(model)) to JSONModel for saving, information may be lost." convert(JSONModel, model) end diff --git a/src/io/mat.jl b/src/io/mat.jl index 84b205868..723975b1c 100644 --- a/src/io/mat.jl +++ b/src/io/mat.jl @@ -7,7 +7,7 @@ model. """ function load_mat_model(file_name::String)::MATModel model_pair = first(matread(file_name)) - @_io_log @info "Loading MAT: taking a model with ID $(model_pair.first)" + @io_log @info "Loading MAT: taking a model with ID $(model_pair.first)" return MATModel(model_pair.second) end @@ -26,7 +26,7 @@ function save_mat_model(model::MetabolicModel, file_path::String; model_name = " m = typeof(model) == MATModel ? model : begin - @_io_log @warn "Automatically converting $(typeof(model)) to MATModel for saving, information may be lost." + @io_log @warn "Automatically converting $(typeof(model)) to MATModel for saving, information may be lost." convert(MATModel, model) end matwrite(file_path, Dict(model_name => m.mat)) diff --git a/src/io/sbml.jl b/src/io/sbml.jl index 574a60300..09085e59a 100644 --- a/src/io/sbml.jl +++ b/src/io/sbml.jl @@ -17,7 +17,7 @@ function save_sbml_model(model::MetabolicModel, file_name::String) m = typeof(model) == SBMLModel ? model : begin - @_io_log @warn "Automatically converting $(typeof(model)) to SBMLModel for saving, information may be lost." + @io_log @warn "Automatically converting $(typeof(model)) to SBMLModel for saving, information may be lost." convert(SBMLModel, model) end diff --git a/src/log.jl b/src/log.jl index 866846988..480ac0cbb 100644 --- a/src/log.jl +++ b/src/log.jl @@ -1,22 +1,29 @@ +module Log +using ..ModuleTools +@dse + +module Internal +using ..ModuleTools +@dse """ $(TYPEDSIGNATURES) This creates a group of functions that allow masking out topic-related logging actions. A call that goes as follows: - @_make_logging_tag XYZ + @make_logging_tag XYZ creates the following tools: -- global variable `_XYZ_log_enabled` defaulted to false +- global variable `XYZ_log_enabled` defaulted to false - function `log_XYZ` that can be called to turn the logging on/off -- a masking macro `@_XYZ_log` that can be prepended to commands that should +- a masking macro `@XYZ_log` that can be prepended to commands that should only happen if the logging of tag XYZ is enabled. The masking macro is then used as follows: - @_XYZ_log @info "This is the extra verbose information you wanted!" a b c + @XYZ_log @info "This is the extra verbose information you wanted!" a b c The user can direct logging with these: @@ -27,10 +34,10 @@ The user can direct logging with these: log_XYZ() is enabled -- it is used to create a friendly documentation for the logging switch. In this case it could say `"X, Y and Z-related messages"`. """ -macro _make_logging_tag(sym::Symbol, doc::String) - enable_flag = Symbol(:_, sym, :_log_enabled) +macro make_logging_tag(sym::Symbol, doc::String) + enable_flag = Symbol(sym, :_log_enabled) enable_fun = Symbol(:log_, sym) - log_macro = Symbol(:_, sym, :_log) + log_macro = Symbol(sym, :_log) # esc() is necessary here because the internal macro processing would # otherwise bind the variables incorrectly. esc(:( @@ -47,12 +54,25 @@ macro _make_logging_tag(sym::Symbol, doc::String) end macro $log_macro(x) - $enable_flag ? x : :nothing + $enable_flag ? x : nothing end end )) end -@_make_logging_tag models "model-related messages" -@_make_logging_tag io "messages and warnings from model input/output" -@_make_logging_tag perf "performance-related tracing information" +@make_logging_tag models "model-related messages" +@make_logging_tag io "messages and warnings from model input/output" +@make_logging_tag perf "performance-related tracing information" + +@export_locals +end #Internal + +using .Internal + +#TODO can this be exported automatically? +export log_models +export log_io +export log_perf + +@export_locals +end diff --git a/src/macros.jl b/src/macros.jl new file mode 100644 index 000000000..33808d15b --- /dev/null +++ b/src/macros.jl @@ -0,0 +1,9 @@ + +module Macros +using ..ModuleTools +@dse + +@inc_dir macros + +@export_locals +end diff --git a/src/macros/model_wrapper.jl b/src/macros/model_wrapper.jl index 66de84db6..ca082345c 100644 --- a/src/macros/model_wrapper.jl +++ b/src/macros/model_wrapper.jl @@ -2,8 +2,8 @@ """ $(TYPEDSIGNATURES) -A helper backend for [`@_inherit_model_methods`](@ref) and -[`@_inherit_model_methods_fn`](@ref). +A helper backend for [`@inherit_model_methods`](@ref) and +[`@inherit_model_methods_fn`](@ref). """ function _inherit_model_methods_impl( source, @@ -43,7 +43,7 @@ $(TYPEDSIGNATURES) Generates trivial accessor functions listed in `fns` for a model that is wrapped in type `mtype` as field `member`. """ -macro _inherit_model_methods(mtype::Symbol, arglist, member::Symbol, fwdlist, fns...) +macro inherit_model_methods(mtype::Symbol, arglist, member::Symbol, fwdlist, fns...) _inherit_model_methods_impl( __source__, mtype, @@ -57,10 +57,10 @@ end """ $(TYPEDSIGNATURES) -A more generic version of [`@_inherit_model_methods`](@ref) that accesses the +A more generic version of [`@inherit_model_methods`](@ref) that accesses the "inner" model using an accessor function name. """ -macro _inherit_model_methods_fn(mtype::Symbol, arglist, accessor, fwdlist, fns...) +macro inherit_model_methods_fn(mtype::Symbol, arglist, accessor, fwdlist, fns...) _inherit_model_methods_impl( __source__, mtype, diff --git a/src/reconstruction/StandardModel.jl b/src/reconstruction/StandardModel.jl index 7f0a75e52..5e2988fea 100644 --- a/src/reconstruction/StandardModel.jl +++ b/src/reconstruction/StandardModel.jl @@ -182,7 +182,7 @@ end @_remove_fn reaction StandardModel String inplace begin if !(reaction_id in reactions(model)) - @_models_log @info "Reaction $reaction_id not found in model." + @models_log @info "Reaction $reaction_id not found in model." else delete!(model.reactions, reaction_id) end @@ -211,7 +211,7 @@ end @_remove_fn metabolite StandardModel String inplace plural begin !all(in.(metabolite_ids, Ref(metabolites(model)))) && - @_models_log @info "Some metabolites not found in model." + @models_log @info "Some metabolites not found in model." remove_reactions!( model, [ diff --git a/src/types.jl b/src/types.jl new file mode 100644 index 000000000..97f586877 --- /dev/null +++ b/src/types.jl @@ -0,0 +1,17 @@ + +module Types +using ..ModuleTools +@dse + +using ..Internal.Macros +using ..Log: @io_log + +using SparseArrays, OrderedCollections +using HDF5, SBML, JSON, MAT, Serialization #for the storable types + +@inc_dir types abstract +@inc_dir types +@inc_dir types wrappers + +@export_locals +end diff --git a/src/types/CoreModelCoupled.jl b/src/types/CoreModelCoupled.jl index 048ffa730..cae3d86ae 100644 --- a/src/types/CoreModelCoupled.jl +++ b/src/types/CoreModelCoupled.jl @@ -99,5 +99,5 @@ CoreModelCoupled(lm::CoreModel, C::MatType, cl::VecType, cu::VecType) = CoreCoupling(lm, sparse(C), collect(cl), collect(cu)) # these are special for CoreModel-ish models -@_inherit_model_methods CoreModelCoupled () lm () reaction_gene_association_vec -@_inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) reaction_stoichiometry +@inherit_model_methods CoreModelCoupled () lm () reaction_gene_association_vec +@inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) reaction_stoichiometry diff --git a/src/types/ModelWrapper.jl b/src/types/ModelWrapper.jl index e1988f1ae..c887b5b5c 100644 --- a/src/types/ModelWrapper.jl +++ b/src/types/ModelWrapper.jl @@ -14,10 +14,10 @@ end # The list of inherited functions must be synced with the methods available for [`MetabolicModel`](@ref). # -@_inherit_model_methods_fn ModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! +@inherit_model_methods_fn ModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! -@_inherit_model_methods_fn ModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes +@inherit_model_methods_fn ModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes -@_inherit_model_methods_fn ModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes +@inherit_model_methods_fn ModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes -@_inherit_model_methods_fn ModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes +@inherit_model_methods_fn ModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes diff --git a/src/types/SBMLModel.jl b/src/types/SBMLModel.jl index 66fda9916..0fd37f6d6 100644 --- a/src/types/SBMLModel.jl +++ b/src/types/SBMLModel.jl @@ -129,10 +129,10 @@ function _sbml_export_annotation(annotation)::Maybe{String} if isnothing(annotation) || isempty(annotation) nothing elseif length(annotation) != 1 || first(annotation).first != "" - @_io_log @warn "Possible data loss: multiple annotations converted to text for SBML" annotation + @io_log @warn "Possible data loss: multiple annotations converted to text for SBML" annotation join(["$k: $v" for (k, v) in annotation], "\n") else - @_io_log @warn "Possible data loss: trying to represent annotation in SBML is unlikely to work " annotation + @io_log @warn "Possible data loss: trying to represent annotation in SBML is unlikely to work " annotation first(annotation).second end end diff --git a/src/utils/gene_associations.jl b/src/utils/gene_associations.jl index 109317900..156752de8 100644 --- a/src/utils/gene_associations.jl +++ b/src/utils/gene_associations.jl @@ -9,7 +9,7 @@ function _parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation parse_ref(x) = typeof(x) == SBML.GPARef ? [x.gene_product] : begin - @_models_log @warn "Could not parse a part of gene association, ignoring: $x" + @models_log @warn "Could not parse a part of gene association, ignoring: $x" String[] end parse_and(x) = From f5c23e04b877c25cce9581e05cd63d7b22859e47 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 15:18:28 +0200 Subject: [PATCH 009/531] fix namespacing of log functions --- src/io.jl | 2 +- src/log.jl | 2 +- src/types.jl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/io.jl b/src/io.jl index d351691af..ea9d799dc 100644 --- a/src/io.jl +++ b/src/io.jl @@ -3,7 +3,7 @@ using ..ModuleTools @dse using ..Types -using ..Log: @io_log +using ..Log.Internal: @io_log @inc_dir io diff --git a/src/log.jl b/src/log.jl index 480ac0cbb..125b93b2d 100644 --- a/src/log.jl +++ b/src/log.jl @@ -67,7 +67,7 @@ end @export_locals end #Internal -using .Internal +import .Internal: log_models, log_io, log_perf #TODO can this be exported automatically? export log_models diff --git a/src/types.jl b/src/types.jl index 97f586877..d908035f2 100644 --- a/src/types.jl +++ b/src/types.jl @@ -4,7 +4,7 @@ using ..ModuleTools @dse using ..Internal.Macros -using ..Log: @io_log +using ..Log.Internal: @io_log using SparseArrays, OrderedCollections using HDF5, SBML, JSON, MAT, Serialization #for the storable types From 4a270f085eca55cdc2e0dac70f7985e5e41a7e28 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 15:18:54 +0200 Subject: [PATCH 010/531] fix IO name conflict (and all other name conflict at once, for good) --- src/COBREXA.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 741368cf3..226dba2dd 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -120,6 +120,4 @@ _inc_all.( ) =# -@export_locals - end # module From 6dcb6f15acf9475d45d26f54d4742c1aa77a6663 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 26 Sep 2022 15:21:17 +0200 Subject: [PATCH 011/531] have less caps --- src/COBREXA.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 226dba2dd..1b00c27ee 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -1,6 +1,6 @@ """ ``` -\\\\\\\\\\ // // | COBREXA.jl v$(COBREXA.COBREXA_VERSION) +\\\\\\\\\\ // // | COBREXA.jl v$(COBREXA.version) \\\\ \\\\// // | \\\\ \\/ // | COnstraint-Based Reconstruction \\\\ // | and EXascale Analysis in Julia @@ -48,7 +48,7 @@ import SBML # conflict with Reaction struct name const _PKG_ROOT_DIR = normpath(joinpath(@__DIR__, "..")) include_dependency(joinpath(_PKG_ROOT_DIR, "Project.toml")) -const COBREXA_VERSION = +const version = VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) module ModuleTools From dddceb5bc3b0cb3e76c0e656426ac4212caa8953 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 14:46:22 +0200 Subject: [PATCH 012/531] separate out accessors into their own module --- src/COBREXA.jl | 4 ++ src/types.jl | 30 +++++++-- src/types/{ => accessors}/MetabolicModel.jl | 0 src/types/{ => accessors}/ModelWrapper.jl | 2 +- src/types/{ => models}/CoreModel.jl | 22 +++--- src/types/{ => models}/CoreModelCoupled.jl | 11 +-- src/types/{ => models}/HDF5Model.jl | 18 ++--- src/types/{ => models}/JSONModel.jl | 49 ++++++++------ src/types/{ => models}/MATModel.jl | 74 ++++++++++----------- src/types/{ => models}/SBMLModel.jl | 37 ++++++----- src/types/{ => models}/Serialized.jl | 4 +- src/types/{ => models}/StandardModel.jl | 54 ++++++++------- src/types/wrappers/GeckoModel.jl | 31 ++++----- src/types/wrappers/SMomentModel.jl | 22 +++--- 14 files changed, 198 insertions(+), 160 deletions(-) rename src/types/{ => accessors}/MetabolicModel.jl (100%) rename src/types/{ => accessors}/ModelWrapper.jl (95%) rename src/types/{ => models}/CoreModel.jl (83%) rename src/types/{ => models}/CoreModelCoupled.jl (87%) rename src/types/{ => models}/HDF5Model.jl (75%) rename src/types/{ => models}/JSONModel.jl (83%) rename src/types/{ => models}/MATModel.jl (84%) rename src/types/{ => models}/SBMLModel.jl (82%) rename src/types/{ => models}/Serialized.jl (88%) rename src/types/{ => models}/StandardModel.jl (81%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 1b00c27ee..b9018cfd3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -66,6 +66,10 @@ macro dse() :(using DocStringExtensions) end +macro inject(mod::Symbol, code) + esc(:(Base.eval($mod, $(Expr(:quote, code))))) +end + # export everything from the local namespace that seems exportable # (inspired by JuMP.jl, thanks!) macro export_locals() diff --git a/src/types.jl b/src/types.jl index d908035f2..5c7a2dce6 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,17 +1,33 @@ +# make the module for types and load basic abstract types module Types using ..ModuleTools @dse - -using ..Internal.Macros -using ..Log.Internal: @io_log - using SparseArrays, OrderedCollections using HDF5, SBML, JSON, MAT, Serialization #for the storable types - @inc_dir types abstract -@inc_dir types -@inc_dir types wrappers +@export_locals +end +# the specialized module for accessors +module Accessors +using ..ModuleTools +@dse +using ..Types +using ..Internal.Macros +@inc_dir types accessors @export_locals end + +# the modules depend on each other so we have to inject the stuff like this +@inject Types begin + using ..Accessors + + using ..Internal.Macros + using ..Log.Internal: @io_log + @inc_dir types + @inc_dir types models + @inc_dir types wrappers + + @export_locals +end diff --git a/src/types/MetabolicModel.jl b/src/types/accessors/MetabolicModel.jl similarity index 100% rename from src/types/MetabolicModel.jl rename to src/types/accessors/MetabolicModel.jl diff --git a/src/types/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl similarity index 95% rename from src/types/ModelWrapper.jl rename to src/types/accessors/ModelWrapper.jl index c887b5b5c..29ca1b24c 100644 --- a/src/types/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -2,7 +2,7 @@ """ $(TYPEDSIGNATURES) -A simple helper to pick the single w +A simple helper to pick a single wrapped model """ function unwrap_model(a::ModelWrapper) _missing_impl_error(unwrap_model, (a,)) diff --git a/src/types/CoreModel.jl b/src/types/models/CoreModel.jl similarity index 83% rename from src/types/CoreModel.jl rename to src/types/models/CoreModel.jl index 9133fda6a..ffef731f6 100644 --- a/src/types/CoreModel.jl +++ b/src/types/models/CoreModel.jl @@ -52,42 +52,42 @@ $(TYPEDSIGNATURES) Get the reactions in a `CoreModel`. """ -reactions(a::CoreModel)::Vector{String} = a.rxns +Accessors.reactions(a::CoreModel)::Vector{String} = a.rxns """ $(TYPEDSIGNATURES) Metabolites in a `CoreModel`. """ -metabolites(a::CoreModel)::Vector{String} = a.mets +Accessors.metabolites(a::CoreModel)::Vector{String} = a.mets """ $(TYPEDSIGNATURES) `CoreModel` stoichiometry matrix. """ -stoichiometry(a::CoreModel)::SparseMat = a.S +Accessors.stoichiometry(a::CoreModel)::SparseMat = a.S """ $(TYPEDSIGNATURES) `CoreModel` flux bounds. """ -bounds(a::CoreModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) +Accessors.bounds(a::CoreModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) """ $(TYPEDSIGNATURES) `CoreModel` target flux balance. """ -balance(a::CoreModel)::SparseVec = a.b +Accessors.balance(a::CoreModel)::SparseVec = a.b """ $(TYPEDSIGNATURES) `CoreModel` objective vector. """ -objective(a::CoreModel)::SparseVec = a.c +Accessors.objective(a::CoreModel)::SparseVec = a.c """ $(TYPEDSIGNATURES) @@ -96,7 +96,7 @@ Collect all genes contained in the [`CoreModel`](@ref). The call is expensive for large models, because the vector is not stored and instead gets rebuilt each time this function is called. """ -function genes(a::CoreModel)::Vector{String} +function Accessors.genes(a::CoreModel)::Vector{String} res = Set{String}() for grr in a.grrs isnothing(grr) && continue @@ -113,7 +113,7 @@ end $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. -""" +Accessors.""" reaction_stoichiometry(m::CoreModel, rid::String)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) @@ -122,7 +122,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction at index `ridx`. """ -reaction_stoichiometry(m::CoreModel, ridx)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::CoreModel, ridx)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) """ @@ -139,7 +139,7 @@ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction index. """ -reaction_gene_association(model::CoreModel, ridx::Int)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association(model::CoreModel, ridx::Int)::Maybe{GeneAssociation} = model.grrs[ridx] """ @@ -147,7 +147,7 @@ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction ID. """ -reaction_gene_association(model::CoreModel, rid::String)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association(model::CoreModel, rid::String)::Maybe{GeneAssociation} = model.grrs[first(indexin([rid], model.rxns))] """ diff --git a/src/types/CoreModelCoupled.jl b/src/types/models/CoreModelCoupled.jl similarity index 87% rename from src/types/CoreModelCoupled.jl rename to src/types/models/CoreModelCoupled.jl index cae3d86ae..5806d9b05 100644 --- a/src/types/CoreModelCoupled.jl +++ b/src/types/models/CoreModelCoupled.jl @@ -37,28 +37,29 @@ $(TYPEDSIGNATURES) Get the internal [`CoreModel`](@ref) out of [`CoreCoupling`](@ref). """ -unwrap_model(a::CoreCoupling) = a.lm +Accessors.unwrap_model(a::CoreCoupling) = a.lm """ $(TYPEDSIGNATURES) Coupling constraint matrix for a `CoreCoupling`. """ -coupling(a::CoreCoupling)::SparseMat = vcat(coupling(a.lm), a.C) +Accessors.coupling(a::CoreCoupling)::SparseMat = vcat(coupling(a.lm), a.C) """ $(TYPEDSIGNATURES) The number of coupling constraints in a `CoreCoupling`. """ -n_coupling_constraints(a::CoreCoupling)::Int = n_coupling_constraints(a.lm) + size(a.C, 1) +Accessors.n_coupling_constraints(a::CoreCoupling)::Int = + n_coupling_constraints(a.lm) + size(a.C, 1) """ $(TYPEDSIGNATURES) Coupling bounds for a `CoreCoupling`. """ -coupling_bounds(a::CoreCoupling)::Tuple{Vector{Float64},Vector{Float64}} = +Accessors.coupling_bounds(a::CoreCoupling)::Tuple{Vector{Float64},Vector{Float64}} = vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) """ @@ -100,4 +101,4 @@ CoreModelCoupled(lm::CoreModel, C::MatType, cl::VecType, cu::VecType) = # these are special for CoreModel-ish models @inherit_model_methods CoreModelCoupled () lm () reaction_gene_association_vec -@inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) reaction_stoichiometry +@inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) Accessors.reaction_stoichiometry diff --git a/src/types/HDF5Model.jl b/src/types/models/HDF5Model.jl similarity index 75% rename from src/types/HDF5Model.jl rename to src/types/models/HDF5Model.jl index c6b710733..df611d22d 100644 --- a/src/types/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -26,50 +26,50 @@ mutable struct HDF5Model <: MetabolicModel HDF5Model(filename::String) = new(nothing, filename) end -function precache!(model::HDF5Model)::Nothing +function Accessors.precache!(model::HDF5Model)::Nothing if isnothing(model.h5) model.h5 = h5open(model.filename, "r") end nothing end -function n_reactions(model::HDF5Model)::Int +function Accessors.n_reactions(model::HDF5Model)::Int precache!(model) length(model.h5["reactions"]) end -function reactions(model::HDF5Model)::Vector{String} +function Accessors.reactions(model::HDF5Model)::Vector{String} precache!(model) # TODO is there any reasonable method to mmap strings from HDF5? read(model.h5["reactions"]) end -function n_metabolites(model::HDF5Model)::Int +function Accessors.n_metabolites(model::HDF5Model)::Int precache!(model) length(model.h5["metabolites"]) end -function metabolites(model::HDF5Model)::Vector{String} +function Accessors.metabolites(model::HDF5Model)::Vector{String} precache!(model) read(model.h5["metabolites"]) end -function stoichiometry(model::HDF5Model)::SparseMat +function Accessors.stoichiometry(model::HDF5Model)::SparseMat precache!(model) _h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end -function bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} +function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} precache!(model) (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) end -function balance(model::HDF5Model)::SparseVec +function Accessors.balance(model::HDF5Model)::SparseVec precache!(model) _h5_read_sparse(SparseVec, model.h5["balance"]) end -function objective(model::HDF5Model)::SparseVec +function Accessors.objective(model::HDF5Model)::SparseVec precache!(model) _h5_read_sparse(SparseVec, model.h5["objective"]) end diff --git a/src/types/JSONModel.jl b/src/types/models/JSONModel.jl similarity index 83% rename from src/types/JSONModel.jl rename to src/types/models/JSONModel.jl index 03ab30a5d..047482967 100644 --- a/src/types/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -71,26 +71,33 @@ end _parse_notes(x)::Notes = _parse_annotations(x) +Accessors.n_reactions(model::JSONModel) = length(model.rxns) +Accessors.n_metabolites(model::JSONModel) = length(model.mets) +Accessors.n_genes(model::JSONModel) = length(model.genes) + """ $(TYPEDSIGNATURES) Extract reaction names (stored as `.id`) from JSON model. """ -reactions(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] +Accessors.reactions(model::JSONModel) = + [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] """ $(TYPEDSIGNATURES) Extract metabolite names (stored as `.id`) from JSON model. """ -metabolites(model::JSONModel) = [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] +Accessors.metabolites(model::JSONModel) = + [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] """ $(TYPEDSIGNATURES) Extract gene names from a JSON model. """ -genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] +Accessors.genes(model::JSONModel) = + [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] """ $(TYPEDSIGNATURES) @@ -98,7 +105,7 @@ $(TYPEDSIGNATURES) Get the stoichiometry. Assuming the information is stored in reaction object under key `.metabolites`. """ -function stoichiometry(model::JSONModel) +function Accessors.stoichiometry(model::JSONModel) rxn_ids = reactions(model) met_ids = metabolites(model) @@ -140,7 +147,7 @@ $(TYPEDSIGNATURES) Get the bounds for reactions, assuming the information is stored in `.lower_bound` and `.upper_bound`. """ -bounds(model::JSONModel) = ( +Accessors.bounds(model::JSONModel) = ( [get(rxn, "lower_bound", -_constants.default_reaction_bound) for rxn in model.rxns], [get(rxn, "upper_bound", _constants.default_reaction_bound) for rxn in model.rxns], ) @@ -150,7 +157,7 @@ $(TYPEDSIGNATURES) Collect `.objective_coefficient` keys from model reactions. """ -objective(model::JSONModel) = +Accessors.objective(model::JSONModel) = sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in model.rxns]) """ @@ -158,7 +165,7 @@ $(TYPEDSIGNATURES) Parses the `.gene_reaction_rule` from reactions. """ -reaction_gene_association(model::JSONModel, rid::String) = _maybemap( +Accessors.reaction_gene_association(model::JSONModel, rid::String) = _maybemap( _parse_grr, get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), ) @@ -168,7 +175,7 @@ $(TYPEDSIGNATURES) Parses the `.subsystem` out from reactions. """ -reaction_subsystem(model::JSONModel, rid::String) = +Accessors.reaction_subsystem(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "subsystem", nothing) """ @@ -176,7 +183,7 @@ $(TYPEDSIGNATURES) Parse and return the metabolite `.formula` """ -metabolite_formula(model::JSONModel, mid::String) = +Accessors.metabolite_formula(model::JSONModel, mid::String) = _maybemap(_parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) """ @@ -184,7 +191,7 @@ $(TYPEDSIGNATURES) Return the metabolite `.charge` """ -metabolite_charge(model::JSONModel, mid::String) = +Accessors.metabolite_charge(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "charge", 0) """ @@ -192,7 +199,7 @@ $(TYPEDSIGNATURES) Return the metabolite `.compartment` """ -metabolite_compartment(model::JSONModel, mid::String) = +Accessors.metabolite_compartment(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "compartment", nothing) """ @@ -200,7 +207,7 @@ $(TYPEDSIGNATURES) Gene annotations from the [`JSONModel`](@ref). """ -gene_annotations(model::JSONModel, gid::String)::Annotations = _maybemap( +Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = _maybemap( _parse_annotations, get(model.genes[model.gene_index[gid]], "annotation", nothing), ) @@ -210,7 +217,7 @@ $(TYPEDSIGNATURES) Gene notes from the [`JSONModel`](@ref). """ -gene_notes(model::JSONModel, gid::String)::Notes = +Accessors.gene_notes(model::JSONModel, gid::String)::Notes = _maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) """ @@ -218,7 +225,7 @@ $(TYPEDSIGNATURES) Reaction annotations from the [`JSONModel`](@ref). """ -reaction_annotations(model::JSONModel, rid::String)::Annotations = _maybemap( +Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = _maybemap( _parse_annotations, get(model.rxns[model.rxn_index[rid]], "annotation", nothing), ) @@ -228,7 +235,7 @@ $(TYPEDSIGNATURES) Reaction notes from the [`JSONModel`](@ref). """ -reaction_notes(model::JSONModel, rid::String)::Notes = +Accessors.reaction_notes(model::JSONModel, rid::String)::Notes = _maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) """ @@ -236,7 +243,7 @@ $(TYPEDSIGNATURES) Metabolite annotations from the [`JSONModel`](@ref). """ -metabolite_annotations(model::JSONModel, mid::String)::Annotations = _maybemap( +Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = _maybemap( _parse_annotations, get(model.mets[model.met_index[mid]], "annotation", nothing), ) @@ -246,7 +253,7 @@ $(TYPEDSIGNATURES) Metabolite notes from the [`JSONModel`](@ref). """ -metabolite_notes(model::JSONModel, mid::String)::Notes = +Accessors.metabolite_notes(model::JSONModel, mid::String)::Notes = _maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) """ @@ -254,7 +261,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. """ -reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = model.rxns[model.rxn_index[rid]]["metabolites"] """ @@ -262,7 +269,7 @@ $(TYPEDSIGNATURES) Return the name of reaction with ID `rid`. """ -reaction_name(model::JSONModel, rid::String) = +Accessors.reaction_name(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "name", nothing) """ @@ -270,7 +277,7 @@ $(TYPEDSIGNATURES) Return the name of metabolite with ID `mid`. """ -metabolite_name(model::JSONModel, mid::String) = +Accessors.metabolite_name(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "name", nothing) """ @@ -278,7 +285,7 @@ $(TYPEDSIGNATURES) Return the name of gene with ID `gid`. """ -gene_name(model::JSONModel, gid::String) = +Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) """ diff --git a/src/types/MATModel.jl b/src/types/models/MATModel.jl similarity index 84% rename from src/types/MATModel.jl rename to src/types/models/MATModel.jl index 2d1a147e5..97b43f166 100644 --- a/src/types/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -10,15 +10,15 @@ struct MATModel <: MetabolicModel mat::Dict{String,Any} end -n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) -n_reactions(m::MATModel)::Int = size(m.mat["S"], 2) +Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) +Accessors.n_reactions(m::MATModel)::Int = size(m.mat["S"], 2) """ $(TYPEDSIGNATURES) Extracts reaction names from `rxns` key in the MAT file. """ -function reactions(m::MATModel)::Vector{String} +function Accessors.reactions(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") reshape(m.mat["rxns"], n_reactions(m)) else @@ -40,7 +40,7 @@ $(TYPEDSIGNATURES) Extracts metabolite names from `mets` key in the MAT file. """ -function metabolites(m::MATModel)::Vector{String} +function Accessors.metabolites(m::MATModel)::Vector{String} nm = n_metabolites(m) if haskey(m.mat, "mets") reshape(m.mat["mets"], length(m.mat["mets"]))[begin:nm] @@ -54,14 +54,14 @@ $(TYPEDSIGNATURES) Extract the stoichiometry matrix, stored under key `S`. """ -stoichiometry(m::MATModel) = sparse(m.mat["S"]) +Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) """ $(TYPEDSIGNATURES) Extracts bounds from the MAT file, saved under `lb` and `ub`. """ -bounds(m::MATModel) = ( +Accessors.bounds(m::MATModel) = ( reshape(get(m.mat, "lb", fill(-Inf, n_reactions(m), 1)), n_reactions(m)), reshape(get(m.mat, "ub", fill(Inf, n_reactions(m), 1)), n_reactions(m)), ) @@ -71,7 +71,7 @@ $(TYPEDSIGNATURES) Extracts balance from the MAT model, defaulting to zeroes if not present. """ -function balance(m::MATModel) +function Accessors.balance(m::MATModel) b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) if _mat_has_squashed_coupling(m.mat) b = b[1:n_metabolites(m), :] @@ -84,7 +84,7 @@ $(TYPEDSIGNATURES) Extracts the objective from the MAT model (defaults to zeroes). """ -objective(m::MATModel) = +Accessors.objective(m::MATModel) = sparse(reshape(get(m.mat, "c", zeros(n_reactions(m), 1)), n_reactions(m))) """ @@ -92,7 +92,7 @@ $(TYPEDSIGNATURES) Extract coupling matrix stored, in `C` key. """ -coupling(m::MATModel) = +Accessors.coupling(m::MATModel) = _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_reactions(m)+1:end, :]) : sparse(get(m.mat, "C", zeros(0, n_reactions(m)))) @@ -101,7 +101,7 @@ $(TYPEDSIGNATURES) Extracts the coupling constraints. Currently, there are several accepted ways to store these in MATLAB models; this takes the constraints from vectors `cl` and `cu`. """ -function coupling_bounds(m::MATModel) +function Accessors.coupling_bounds(m::MATModel) nc = n_coupling_constraints(m) if _mat_has_squashed_coupling(m.mat) ( @@ -121,7 +121,7 @@ $(TYPEDSIGNATURES) Extracts the possible gene list from `genes` key. """ -function genes(m::MATModel) +function Accessors.genes(m::MATModel) x = get(m.mat, "genes", []) reshape(x, length(x)) end @@ -131,7 +131,7 @@ $(TYPEDSIGNATURES) Extracts the associations from `grRules` key, if present. """ -function reaction_gene_association(m::MATModel, rid::String) +function Accessors.reaction_gene_association(m::MATModel, rid::String) if haskey(m.mat, "grRules") grr = m.mat["grRules"][findfirst(==(rid), reactions(m))] typeof(grr) == String ? _parse_grr(grr) : nothing @@ -145,7 +145,7 @@ $(TYPEDSIGNATURES) Extract metabolite formula from key `metFormula` or `metFormulas`. """ -metabolite_formula(m::MATModel, mid::String) = _maybemap( +Accessors.metabolite_formula(m::MATModel, mid::String) = _maybemap( x -> _parse_formula(x[findfirst(==(mid), metabolites(m))]), gets(m.mat, nothing, _constants.keynames.metformulas), ) @@ -155,7 +155,7 @@ $(TYPEDSIGNATURES) Extract metabolite charge from `metCharge` or `metCharges`. """ -function metabolite_charge(m::MATModel, mid::String) +function Accessors.metabolite_charge(m::MATModel, mid::String) met_charge = _maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, _constants.keynames.metcharges), @@ -168,7 +168,7 @@ $(TYPEDSIGNATURES) Extract metabolite compartment from `metCompartment` or `metCompartments`. """ -metabolite_compartment(m::MATModel, mid::String) = _maybemap( +Accessors.metabolite_compartment(m::MATModel, mid::String) = _maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, _constants.keynames.metcompartments), ) @@ -178,7 +178,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. """ -function reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} +function Accessors.reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} ridx = first(indexin([rid], m.mat["rxns"])) reaction_stoichiometry(m, ridx) end @@ -188,11 +188,31 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction at index `ridx`. """ -function reaction_stoichiometry(m::MATModel, ridx)::Dict{String,Float64} +function Accessors.reaction_stoichiometry(m::MATModel, ridx)::Dict{String,Float64} met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) Dict(m.mat["mets"][met_ind] => m.mat["S"][met_ind, ridx] for met_ind in met_inds) end +""" +$(TYPEDSIGNATURES) + +Extract reaction name from `rxnNames`. +""" +Accessors.reaction_name(m::MATModel, rid::String) = _maybemap( + x -> x[findfirst(==(rid), reactions(m))], + gets(m.mat, nothing, _constants.keynames.rxnnames), +) + +""" +$(TYPEDSIGNATURES) + +Extract metabolite name from `metNames`. +""" +Accessors.metabolite_name(m::MATModel, mid::String) = _maybemap( + x -> x[findfirst(==(mid), metabolites(m))], + gets(m.mat, nothing, _constants.keynames.metnames), +) + # NOTE: There's no useful standard on how and where to store notes and # annotations in MATLAB models. We therefore leave it very open for the users, # who can easily support any annotation scheme using a custom wrapper. @@ -249,23 +269,3 @@ function Base.convert(::Type{MATModel}, m::MetabolicModel) ), ) end - -""" -$(TYPEDSIGNATURES) - -Extract reaction name from `rxnNames`. -""" -reaction_name(m::MATModel, rid::String) = _maybemap( - x -> x[findfirst(==(rid), reactions(m))], - gets(m.mat, nothing, _constants.keynames.rxnnames), -) - -""" -$(TYPEDSIGNATURES) - -Extract metabolite name from `metNames`. -""" -metabolite_name(m::MATModel, mid::String) = _maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metnames), -) diff --git a/src/types/SBMLModel.jl b/src/types/models/SBMLModel.jl similarity index 82% rename from src/types/SBMLModel.jl rename to src/types/models/SBMLModel.jl index 0fd37f6d6..cb6970376 100644 --- a/src/types/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -16,35 +16,37 @@ $(TYPEDSIGNATURES) Get reactions from a [`SBMLModel`](@ref). """ -reactions(model::SBMLModel)::Vector{String} = [k for k in keys(model.sbml.reactions)] +Accessors.reactions(model::SBMLModel)::Vector{String} = + [k for k in keys(model.sbml.reactions)] """ $(TYPEDSIGNATURES) Get metabolites from a [`SBMLModel`](@ref). """ -metabolites(model::SBMLModel)::Vector{String} = [k for k in keys(model.sbml.species)] +Accessors.metabolites(model::SBMLModel)::Vector{String} = + [k for k in keys(model.sbml.species)] """ $(TYPEDSIGNATURES) Efficient counting of reactions in [`SBMLModel`](@ref). """ -n_reactions(model::SBMLModel)::Int = length(model.sbml.reactions) +Accessors.n_reactions(model::SBMLModel)::Int = length(model.sbml.reactions) """ $(TYPEDSIGNATURES) Efficient counting of metabolites in [`SBMLModel`](@ref). """ -n_metabolites(model::SBMLModel)::Int = length(model.sbml.species) +Accessors.n_metabolites(model::SBMLModel)::Int = length(model.sbml.species) """ $(TYPEDSIGNATURES) Recreate the stoichiometry matrix from the [`SBMLModel`](@ref). """ -function stoichiometry(model::SBMLModel)::SparseMat +function Accessors.stoichiometry(model::SBMLModel)::SparseMat _, _, S = SBML.stoichiometry_matrix(model.sbml) return S end @@ -55,7 +57,7 @@ $(TYPEDSIGNATURES) Get the lower and upper flux bounds of model [`SBMLModel`](@ref). Throws `DomainError` in case if the SBML contains mismatching units. """ -function bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} +function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} lbu, ubu = SBML.flux_bounds(model.sbml) unit = lbu[1][2] @@ -78,35 +80,36 @@ $(TYPEDSIGNATURES) Balance vector of a [`SBMLModel`](@ref). This is always zero. """ -balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) +Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) """ $(TYPEDSIGNATURES) Objective of the [`SBMLModel`](@ref). """ -objective(model::SBMLModel)::SparseVec = SBML.flux_objective(model.sbml) +Accessors.objective(model::SBMLModel)::SparseVec = SBML.flux_objective(model.sbml) """ $(TYPEDSIGNATURES) Get genes of a [`SBMLModel`](@ref). """ -genes(model::SBMLModel)::Vector{String} = [k for k in keys(model.sbml.gene_products)] +Accessors.genes(model::SBMLModel)::Vector{String} = + [k for k in keys(model.sbml.gene_products)] """ $(TYPEDSIGNATURES) Get number of genes in [`SBMLModel`](@ref). """ -n_genes(model::SBMLModel)::Int = length(model.sbml.gene_products) +Accessors.n_genes(model::SBMLModel)::Int = length(model.sbml.gene_products) """ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`SBMLModel`](@ref). """ -reaction_gene_association(model::SBMLModel, rid::String)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association(model::SBMLModel, rid::String)::Maybe{GeneAssociation} = _maybemap(_parse_grr, model.sbml.reactions[rid].gene_product_association) """ @@ -114,7 +117,7 @@ $(TYPEDSIGNATURES) Get [`MetaboliteFormula`](@ref) from a chosen metabolite from [`SBMLModel`](@ref). """ -metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = +Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = _maybemap(_parse_formula, model.sbml.species[mid].formula) """ @@ -122,7 +125,7 @@ $(TYPEDSIGNATURES) Get charge of a chosen metabolite from [`SBMLModel`](@ref). """ -metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = +Accessors.metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = model.sbml.species[mid].charge function _sbml_export_annotation(annotation)::Maybe{String} @@ -144,7 +147,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. """ -function reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} +function Accessors.reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} s = Dict{String,Float64}() default1(x) = isnothing(x) ? 1 : x for sr in m.sbml.reactions[rid].reactants @@ -161,21 +164,21 @@ $(TYPEDSIGNATURES) Return the name of reaction with ID `rid`. """ -reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name +Accessors.reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name """ $(TYPEDSIGNATURES) Return the name of metabolite with ID `mid`. """ -metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name +Accessors.metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name """ $(TYPEDSIGNATURES) Return the name of gene with ID `gid`. """ -gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name +Accessors.gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name """ $(TYPEDSIGNATURES) diff --git a/src/types/Serialized.jl b/src/types/models/Serialized.jl similarity index 88% rename from src/types/Serialized.jl rename to src/types/models/Serialized.jl index aef1c887c..b307c3d88 100644 --- a/src/types/Serialized.jl +++ b/src/types/models/Serialized.jl @@ -23,7 +23,7 @@ $(TYPEDSIGNATURES) Unwrap the serialized model (precaching it transparently). """ -function unwrap_model(m::Serialized) +function Accessors.unwrap_model(m::Serialized) precache!(m) m.m end @@ -33,7 +33,7 @@ $(TYPEDSIGNATURES) Load the `Serialized` model from disk in case it's not alreadly loaded. """ -function precache!(model::Serialized)::Nothing +function Accessors.precache!(model::Serialized)::Nothing if isnothing(model.m) model.m = deserialize(model.filename) end diff --git a/src/types/StandardModel.jl b/src/types/models/StandardModel.jl similarity index 81% rename from src/types/StandardModel.jl rename to src/types/models/StandardModel.jl index 45be53177..5d8254d6b 100644 --- a/src/types/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -57,14 +57,14 @@ Return a vector of reaction id strings contained in `model`. The order of reaction ids returned here matches the order used to construct the stoichiometric matrix. """ -reactions(model::StandardModel)::StringVecType = collect(keys(model.reactions)) +Accessors.reactions(model::StandardModel)::StringVecType = collect(keys(model.reactions)) """ $(TYPEDSIGNATURES) Return the number of reactions contained in `model`. """ -n_reactions(model::StandardModel)::Int = length(model.reactions) +Accessors.n_reactions(model::StandardModel)::Int = length(model.reactions) """ @@ -74,35 +74,36 @@ Return a vector of metabolite id strings contained in `model`. The order of metabolite strings returned here matches the order used to construct the stoichiometric matrix. """ -metabolites(model::StandardModel)::StringVecType = collect(keys(model.metabolites)) +Accessors.metabolites(model::StandardModel)::StringVecType = + collect(keys(model.metabolites)) """ $(TYPEDSIGNATURES) Return the number of metabolites in `model`. """ -n_metabolites(model::StandardModel)::Int = length(model.metabolites) +Accessors.n_metabolites(model::StandardModel)::Int = length(model.metabolites) """ $(TYPEDSIGNATURES) Return a vector of gene id strings in `model`. """ -genes(model::StandardModel)::StringVecType = collect(keys(model.genes)) +Accessors.genes(model::StandardModel)::StringVecType = collect(keys(model.genes)) """ $(TYPEDSIGNATURES) Return the number of genes in `model`. """ -n_genes(model::StandardModel)::Int = length(model.genes) +Accessors.n_genes(model::StandardModel)::Int = length(model.genes) """ $(TYPEDSIGNATURES) Return the stoichiometric matrix associated with `model` in sparse format. """ -function stoichiometry(model::StandardModel)::SparseMat +function Accessors.stoichiometry(model::StandardModel)::SparseMat n_entries = 0 for (_, r) in model.reactions for _ in r.metabolites @@ -161,7 +162,7 @@ $(TYPEDSIGNATURES) Return the lower and upper bounds, respectively, for reactions in `model`. Order matches that of the reaction ids returned in `reactions()`. """ -bounds(model::StandardModel)::Tuple{Vector{Float64},Vector{Float64}} = +Accessors.bounds(model::StandardModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) """ @@ -170,14 +171,14 @@ $(TYPEDSIGNATURES) Return the balance of the linear problem, i.e. b in Sv = 0 where S is the stoichiometric matrix and v is the flux vector. """ -balance(model::StandardModel)::SparseVec = spzeros(length(model.metabolites)) +Accessors.balance(model::StandardModel)::SparseVec = spzeros(length(model.metabolites)) """ $(TYPEDSIGNATURES) Return sparse objective vector for `model`. """ -objective(model::StandardModel)::SparseVec = +Accessors.objective(model::StandardModel)::SparseVec = sparse([model.reactions[rid].objective_coefficient for rid in keys(model.reactions)]) """ @@ -186,8 +187,10 @@ $(TYPEDSIGNATURES) Return the gene reaction rule in string format for reaction with `id` in `model`. Return `nothing` if not available. """ -reaction_gene_association(model::StandardModel, id::String)::Maybe{GeneAssociation} = - _maybemap(identity, model.reactions[id].grr) +Accessors.reaction_gene_association( + model::StandardModel, + id::String, +)::Maybe{GeneAssociation} = _maybemap(identity, model.reactions[id].grr) """ $(TYPEDSIGNATURES) @@ -195,7 +198,7 @@ $(TYPEDSIGNATURES) Return the formula of reaction `id` in `model`. Return `nothing` if not present. """ -metabolite_formula(model::StandardModel, id::String)::Maybe{MetaboliteFormula} = +Accessors.metabolite_formula(model::StandardModel, id::String)::Maybe{MetaboliteFormula} = _maybemap(_parse_formula, model.metabolites[id].formula) """ @@ -204,7 +207,7 @@ $(TYPEDSIGNATURES) Return the charge associated with metabolite `id` in `model`. Return nothing if not present. """ -metabolite_charge(model::StandardModel, id::String)::Maybe{Int} = +Accessors.metabolite_charge(model::StandardModel, id::String)::Maybe{Int} = model.metabolites[id].charge """ @@ -213,7 +216,7 @@ $(TYPEDSIGNATURES) Return compartment associated with metabolite `id` in `model`. Return `nothing` if not present. """ -metabolite_compartment(model::StandardModel, id::String)::Maybe{String} = +Accessors.metabolite_compartment(model::StandardModel, id::String)::Maybe{String} = model.metabolites[id].compartment """ @@ -222,7 +225,7 @@ $(TYPEDSIGNATURES) Return the subsystem associated with reaction `id` in `model`. Return `nothing` if not present. """ -reaction_subsystem(model::StandardModel, id::String)::Maybe{String} = +Accessors.reaction_subsystem(model::StandardModel, id::String)::Maybe{String} = model.reactions[id].subsystem """ @@ -231,7 +234,7 @@ $(TYPEDSIGNATURES) Return the notes associated with metabolite `id` in `model`. Return an empty Dict if not present. """ -metabolite_notes(model::StandardModel, id::String)::Maybe{Notes} = +Accessors.metabolite_notes(model::StandardModel, id::String)::Maybe{Notes} = model.metabolites[id].notes """ @@ -240,7 +243,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with metabolite `id` in `model`. Return an empty Dict if not present. """ -metabolite_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.metabolite_annotations(model::StandardModel, id::String)::Maybe{Annotations} = model.metabolites[id].annotations """ @@ -249,7 +252,7 @@ $(TYPEDSIGNATURES) Return the notes associated with gene `id` in `model`. Return an empty Dict if not present. """ -gene_notes(model::StandardModel, id::String)::Maybe{Notes} = model.genes[id].notes +Accessors.gene_notes(model::StandardModel, id::String)::Maybe{Notes} = model.genes[id].notes """ $(TYPEDSIGNATURES) @@ -257,7 +260,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with gene `id` in `model`. Return an empty Dict if not present. """ -gene_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.gene_annotations(model::StandardModel, id::String)::Maybe{Annotations} = model.genes[id].annotations """ @@ -266,7 +269,8 @@ $(TYPEDSIGNATURES) Return the notes associated with reaction `id` in `model`. Return an empty Dict if not present. """ -reaction_notes(model::StandardModel, id::String)::Maybe{Notes} = model.reactions[id].notes +Accessors.reaction_notes(model::StandardModel, id::String)::Maybe{Notes} = + model.reactions[id].notes """ $(TYPEDSIGNATURES) @@ -274,7 +278,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with reaction `id` in `model`. Return an empty Dict if not present. """ -reaction_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.reaction_annotations(model::StandardModel, id::String)::Maybe{Annotations} = model.reactions[id].annotations """ @@ -290,21 +294,21 @@ $(TYPEDSIGNATURES) Return the name of reaction with ID `id`. """ -reaction_name(m::StandardModel, rid::String) = m.reactions[rid].name +Accessors.reaction_name(m::StandardModel, rid::String) = m.reactions[rid].name """ $(TYPEDSIGNATURES) Return the name of metabolite with ID `id`. """ -metabolite_name(m::StandardModel, mid::String) = m.metabolites[mid].name +Accessors.metabolite_name(m::StandardModel, mid::String) = m.metabolites[mid].name """ $(TYPEDSIGNATURES) Return the name of gene with ID `id`. """ -gene_name(m::StandardModel, gid::String) = m.genes[gid].name +Accessors.gene_name(m::StandardModel, gid::String) = m.genes[gid].name """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index dace278d7..bf9902935 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -84,7 +84,7 @@ struct GeckoModel <: ModelWrapper inner::MetabolicModel end -unwrap_model(model::GeckoModel) = model.inner +Accessors.unwrap_model(model::GeckoModel) = model.inner """ $(TYPEDSIGNATURES) @@ -93,7 +93,7 @@ Return a stoichiometry of the [`GeckoModel`](@ref). The enzymatic reactions are split into unidirectional forward and reverse ones, each of which may have multiple variants per isozyme. """ -function stoichiometry(model::GeckoModel) +function Accessors.stoichiometry(model::GeckoModel) irrevS = stoichiometry(model.inner) * COBREXA._gecko_reaction_column_reactions(model) enzS = COBREXA._gecko_gene_product_coupling(model) [ @@ -110,7 +110,7 @@ respect to the internal variables, i.e. [`reactions(model)`](@ref), which are the unidirectional reactions and the genes involved in enzymatic reactions that have kinetic data. """ -objective(model::GeckoModel) = model.objective +Accessors.objective(model::GeckoModel) = model.objective """ $(TYPEDSIGNATURES) @@ -119,7 +119,7 @@ Returns the internal reactions in a [`GeckoModel`](@ref) (these may be split to forward- and reverse-only parts with different isozyme indexes; reactions IDs are mangled accordingly with suffixes). """ -function reactions(model::GeckoModel) +function Accessors.reactions(model::GeckoModel) inner_reactions = reactions(model.inner) mangled_reactions = [ _gecko_reaction_name( @@ -137,14 +137,14 @@ $(TYPEDSIGNATURES) Returns the number of all irreversible reactions in `model` as well as the number of gene products that take part in enzymatic reactions. """ -n_reactions(model::GeckoModel) = length(model.columns) + n_genes(model) +Accessors.n_reactions(model::GeckoModel) = length(model.columns) + n_genes(model) """ $(TYPEDSIGNATURES) Return variable bounds for [`GeckoModel`](@ref). """ -function bounds(model::GeckoModel) +function Accessors.bounds(model::GeckoModel) lbs = [ [col.lb for col in model.columns] [lb for (_, (lb, _)) in model.coupling_row_gene_product] @@ -162,7 +162,7 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original fluxes in the wrapped model. """ -function reaction_flux(model::GeckoModel) +function Accessors.reaction_flux(model::GeckoModel) rxnmat = _gecko_reaction_column_reactions(model)' * reaction_flux(model.inner) [ rxnmat @@ -177,7 +177,7 @@ Return the coupling of [`GeckoModel`](@ref). That combines the coupling of the wrapped model, coupling for split (arm) reactions, and the coupling for the total enzyme capacity. """ -function coupling(model::GeckoModel) +function Accessors.coupling(model::GeckoModel) innerC = coupling(model.inner) * _gecko_reaction_column_reactions(model) rxnC = _gecko_reaction_coupling(model) enzcap = _gecko_mass_group_coupling(model) @@ -194,7 +194,7 @@ $(TYPEDSIGNATURES) Count the coupling constraints in [`GeckoModel`](@ref) (refer to [`coupling`](@ref) for details). """ -n_coupling_constraints(model::GeckoModel) = +Accessors.n_coupling_constraints(model::GeckoModel) = n_coupling_constraints(model.inner) + length(model.coupling_row_reaction) + length(model.coupling_row_mass_group) @@ -205,7 +205,7 @@ $(TYPEDSIGNATURES) The coupling bounds for [`GeckoModel`](@ref) (refer to [`coupling`](@ref) for details). """ -function coupling_bounds(model::GeckoModel) +function Accessors.coupling_bounds(model::GeckoModel) (iclb, icub) = coupling_bounds(model.inner) (ilb, iub) = bounds(model.inner) return ( @@ -228,7 +228,7 @@ $(TYPEDSIGNATURES) Return the balance of the reactions in the inner model, concatenated with a vector of zeros representing the enzyme balance of a [`GeckoModel`](@ref). """ -balance(model::GeckoModel) = +Accessors.balance(model::GeckoModel) = [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] """ @@ -236,14 +236,14 @@ $(TYPEDSIGNATURES) Return the number of genes that have enzymatic constraints associated with them. """ -n_genes(model::GeckoModel) = length(model.coupling_row_gene_product) +Accessors.n_genes(model::GeckoModel) = length(model.coupling_row_gene_product) """ $(TYPEDSIGNATURES) Return the gene ids of genes that have enzymatic constraints associated with them. """ -genes(model::GeckoModel) = +Accessors.genes(model::GeckoModel) = genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] """ @@ -251,11 +251,12 @@ $(TYPEDSIGNATURES) Return the ids of all metabolites, both real and pseudo, for a [`GeckoModel`](@ref). """ -metabolites(model::GeckoModel) = [metabolites(model.inner); genes(model) .* "#gecko"] +Accessors.metabolites(model::GeckoModel) = + [metabolites(model.inner); genes(model) .* "#gecko"] """ $(TYPEDSIGNATURES) Return the number of metabolites, both real and pseudo, for a [`GeckoModel`](@ref). """ -n_metabolites(model::GeckoModel) = n_metabolites(model.inner) + n_genes(model) +Accessors.n_metabolites(model::GeckoModel) = n_metabolites(model.inner) + n_genes(model) diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 98f8041f7..8e3629efc 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -61,7 +61,7 @@ struct SMomentModel <: ModelWrapper inner::MetabolicModel end -unwrap_model(model::SMomentModel) = model.inner +Accessors.unwrap_model(model::SMomentModel) = model.inner """ $(TYPEDSIGNATURES) @@ -69,7 +69,7 @@ $(TYPEDSIGNATURES) Return a stoichiometry of the [`SMomentModel`](@ref). The enzymatic reactions are split into unidirectional forward and reverse ones. """ -stoichiometry(model::SMomentModel) = +Accessors.stoichiometry(model::SMomentModel) = stoichiometry(model.inner) * _smoment_column_reactions(model) """ @@ -77,7 +77,8 @@ $(TYPEDSIGNATURES) Reconstruct an objective of the [`SMomentModel`](@ref). """ -objective(model::SMomentModel) = _smoment_column_reactions(model)' * objective(model.inner) +Accessors.objective(model::SMomentModel) = + _smoment_column_reactions(model)' * objective(model.inner) """ $(TYPEDSIGNATURES) @@ -86,7 +87,7 @@ Returns the internal reactions in a [`SMomentModel`](@ref) (these may be split to forward- and reverse-only parts; reactions IDs are mangled accordingly with suffixes). """ -reactions(model::SMomentModel) = +Accessors.reactions(model::SMomentModel) = let inner_reactions = reactions(model.inner) [ _smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for @@ -99,14 +100,14 @@ $(TYPEDSIGNATURES) The number of reactions (including split ones) in [`SMomentModel`](@ref). """ -n_reactions(model::SMomentModel) = length(model.columns) +Accessors.n_reactions(model::SMomentModel) = length(model.columns) """ $(TYPEDSIGNATURES) Return the variable bounds for [`SMomentModel`](@ref). """ -bounds(model::SMomentModel) = +Accessors.bounds(model::SMomentModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) """ @@ -115,7 +116,7 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`SMomentModel`](@ref) to the original fluxes in the wrapped model. """ -reaction_flux(model::SMomentModel) = +Accessors.reaction_flux(model::SMomentModel) = _smoment_column_reactions(model)' * reaction_flux(model.inner) """ @@ -125,7 +126,7 @@ Return the coupling of [`SMomentModel`](@ref). That combines the coupling of the wrapped model, coupling for split reactions, and the coupling for the total enzyme capacity. """ -coupling(model::SMomentModel) = vcat( +Accessors.coupling(model::SMomentModel) = vcat( coupling(model.inner) * _smoment_column_reactions(model), [col.capacity_required for col in model.columns]', ) @@ -136,7 +137,8 @@ $(TYPEDSIGNATURES) Count the coupling constraints in [`SMomentModel`](@ref) (refer to [`coupling`](@ref) for details). """ -n_coupling_constraints(model::SMomentModel) = n_coupling_constraints(model.inner) + 1 +Accessors.n_coupling_constraints(model::SMomentModel) = + n_coupling_constraints(model.inner) + 1 """ $(TYPEDSIGNATURES) @@ -144,7 +146,7 @@ $(TYPEDSIGNATURES) The coupling bounds for [`SMomentModel`](@ref) (refer to [`coupling`](@ref) for details). """ -coupling_bounds(model::SMomentModel) = +Accessors.coupling_bounds(model::SMomentModel) = let (iclb, icub) = coupling_bounds(model.inner) (vcat(iclb, [0.0]), vcat(icub, [model.total_enzyme_capacity])) end From d9a4bcf2ee4327080f3d516540a3d86690ca5be1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 15:23:34 +0200 Subject: [PATCH 013/531] add more internals to modules (this version can actually load modules) --- src/COBREXA.jl | 73 ++++++++----------- src/analysis/gecko.jl | 2 +- src/analysis/max_min_driving_force.jl | 4 +- src/analysis/modifications/loopless.jl | 4 +- src/analysis/sampling/affine_hit_and_run.jl | 6 +- src/analysis/smoment.jl | 4 +- src/io.jl | 5 ++ src/io/show/FluxSummary.jl | 2 +- src/io/show/FluxVariabilitySummary.jl | 2 +- src/io/show/Gene.jl | 2 +- src/io/show/MetabolicModel.jl | 4 +- src/io/show/Metabolite.jl | 2 +- src/io/show/Reaction.jl | 2 +- src/io/show/Serialized.jl | 2 +- src/{base => misc}/constants.jl | 2 +- src/{utils => misc}/guesskey.jl | 2 +- src/{base => misc}/identifiers.jl | 2 +- src/{ => misc}/ontology/SBOTerms.jl | 8 +- src/reconstruction/community.jl | 6 +- .../gapfill_minimum_reactions.jl | 2 +- src/{base => }/solver.jl | 0 src/types.jl | 1 + src/types/FluxSummary.jl | 8 +- src/types/FluxVariabilitySummary.jl | 4 +- src/types/Reaction.jl | 6 +- src/types/models/JSONModel.jl | 16 ++-- src/types/models/MATModel.jl | 10 +-- src/utils/looks_like.jl | 10 +-- 28 files changed, 93 insertions(+), 98 deletions(-) rename src/{base => misc}/constants.jl (98%) rename src/{utils => misc}/guesskey.jl (95%) rename src/{base => misc}/identifiers.jl (97%) rename src/{ => misc}/ontology/SBOTerms.jl (93%) rename src/{base => }/solver.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index b9018cfd3..ab72fca90 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -25,24 +25,7 @@ A complete overview of the functionality can be found in the documentation. """ module COBREXA -using Distributed -using DistributedData -using HDF5 -using JSON -using JuMP -using LinearAlgebra -using MAT -using MacroTools -using OrderedCollections -using Random -using Serialization -using SparseArrays -using StableRNGs -using Statistics - -import Base: findfirst, getindex, show import Pkg -import SBML # conflict with Reaction struct name # versioning tools const _PKG_ROOT_DIR = normpath(joinpath(@__DIR__, "..")) @@ -86,42 +69,46 @@ end @export_locals end -# start loading the individual modules +# load various internal helpers module Internal using ..ModuleTools +@dse + @inc macros +@inc_dir misc ontology +@inc_dir misc + +@export_locals end +# start loading individual user-facing modules using .ModuleTools @inc log @inc types @inc io -#= TODOs here - -_inc_all.( - joinpath.( - @__DIR__, - [ - joinpath("types", "abstract"), - joinpath("logging"), - joinpath("macros"), - joinpath("types"), - joinpath("types", "wrappers"), - joinpath("ontology"), - "base", - "io", - joinpath("io", "show"), - "reconstruction", - joinpath("reconstruction", "modifications"), - "analysis", - joinpath("analysis", "modifications"), - joinpath("analysis", "sampling"), - joinpath("utils"), - ], - ), -) -=# +# TODO: this needs to be assimilated to actual modules +module Rest +using ..ModuleTools +@dse + +using ..Internal +using ..Types +using ..Accessors +using ..IO + +using JuMP + +@inc solver + +#joinpath("io", "show"), +#"reconstruction", +#joinpath("reconstruction", "modifications"), +#"analysis", +#joinpath("analysis", "modifications"), +#joinpath("analysis", "sampling"), +#joinpath("utils"), +end end # module diff --git a/src/analysis/gecko.jl b/src/analysis/gecko.jl index b4b5dcab7..9436ee7e6 100644 --- a/src/analysis/gecko.jl +++ b/src/analysis/gecko.jl @@ -88,7 +88,7 @@ function make_gecko_model( # all isozymes in this direction for (iidx, isozyme) in enumerate(isozymes) kcat = kcatf(isozyme) - if ub > 0 && kcat > _constants.tolerance + if ub > 0 && kcat > constants.tolerance # prepare the coupling with gene product molar gene_product_coupling = collect( begin diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl index 8bb20f4eb..5961f58f1 100644 --- a/src/analysis/max_min_driving_force.jl +++ b/src/analysis/max_min_driving_force.jl @@ -60,8 +60,8 @@ function max_min_driving_force( }(), concentration_lb = 1e-9, concentration_ub = 100e-3, - T = _constants.T, - R = _constants.R, + T = constants.T, + R = constants.R, small_flux_tol = 1e-6, modifications = [], ignore_reaction_ids = [], diff --git a/src/analysis/modifications/loopless.jl b/src/analysis/modifications/loopless.jl index b7578ac74..99e895b1e 100644 --- a/src/analysis/modifications/loopless.jl +++ b/src/analysis/modifications/loopless.jl @@ -22,8 +22,8 @@ of thermodynamically infeasible loops in steady-state metabolic models.", Biophy journal, 2011`. """ add_loopless_constraints(; - max_flux_bound = _constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = _constants.loopless_strict_inequality_tolerance, + max_flux_bound = constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = constants.loopless_strict_inequality_tolerance, ) = (model, opt_model) -> begin diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index b369b0be6..06dceb32e 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -87,8 +87,8 @@ function _affine_hit_and_run_chain(warmup, lbs, ubs, C, cl, cu, iters, seed) dl = lb - pos du = ub - pos lower, upper = - dir < -_constants.tolerance ? (du, dl) ./ dir : - dir > _constants.tolerance ? (dl, du) ./ dir : (-Inf, Inf) + dir < -constants.tolerance ? (du, dl) ./ dir : + dir > constants.tolerance ? (dl, du) ./ dir : (-Inf, Inf) return (max(range[1], lower), min(range[2], upper)) end @@ -103,7 +103,7 @@ function _affine_hit_and_run_chain(warmup, lbs, ubs, C, cl, cu, iters, seed) for i = 1:n_points - mix = rand(rng, n_points) .+ _constants.tolerance + mix = rand(rng, n_points) .+ constants.tolerance dir = points * (mix ./ sum(mix)) - points[:, i] # iteratively collect the maximum and minimum possible multiple diff --git a/src/analysis/smoment.jl b/src/analysis/smoment.jl index 179d41597..50b258c1f 100644 --- a/src/analysis/smoment.jl +++ b/src/analysis/smoment.jl @@ -47,7 +47,7 @@ function make_smoment_model( mw = sum(gpmm_(gid) * ps for (gid, ps) in isozyme.gene_product_count) - if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_reverse > _constants.tolerance + if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_reverse > constants.tolerance # reaction can run in reverse push!( columns, @@ -55,7 +55,7 @@ function make_smoment_model( ) end - if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > _constants.tolerance + if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > constants.tolerance # reaction can run forward push!( columns, diff --git a/src/io.jl b/src/io.jl index ea9d799dc..ec9a3ed83 100644 --- a/src/io.jl +++ b/src/io.jl @@ -3,9 +3,14 @@ using ..ModuleTools @dse using ..Types +using ..Accessors +using ..Internal: constants using ..Log.Internal: @io_log +using JSON, MAT, SBML, HDF5 + @inc_dir io +@inc_dir io show @export_locals end diff --git a/src/io/show/FluxSummary.jl b/src/io/show/FluxSummary.jl index 259036045..5aafb6cfb 100644 --- a/src/io/show/FluxSummary.jl +++ b/src/io/show/FluxSummary.jl @@ -4,7 +4,7 @@ end _pad_spaces(str::String, maxlen::Int) = _pad_spaces(length(str), maxlen) -function Base.show(io::IO, ::MIME"text/plain", flux_res::FluxSummary) +function Base.show(io::Base.IO, ::MIME"text/plain", flux_res::FluxSummary) longest_biomass_len = maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) longest_import_len = maximum(length(k) for k in keys(flux_res.import_fluxes); init = 0) diff --git a/src/io/show/FluxVariabilitySummary.jl b/src/io/show/FluxVariabilitySummary.jl index ba07fda5e..4862ea776 100644 --- a/src/io/show/FluxVariabilitySummary.jl +++ b/src/io/show/FluxVariabilitySummary.jl @@ -1,4 +1,4 @@ -function Base.show(io::IO, ::MIME"text/plain", flux_res::FluxVariabilitySummary) +function Base.show(io::Base.IO, ::MIME"text/plain", flux_res::FluxVariabilitySummary) longest_biomass_len = maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) diff --git a/src/io/show/Gene.jl b/src/io/show/Gene.jl index 8d030b238..90d6e9066 100644 --- a/src/io/show/Gene.jl +++ b/src/io/show/Gene.jl @@ -1,4 +1,4 @@ -function Base.show(io::IO, ::MIME"text/plain", g::Gene) +function Base.show(io::Base.IO, ::MIME"text/plain", g::Gene) for fname in fieldnames(Gene) _pretty_print_keyvals(io, "Gene.$(string(fname)): ", getfield(g, fname)) end diff --git a/src/io/show/MetabolicModel.jl b/src/io/show/MetabolicModel.jl index b600208be..b851e08ba 100644 --- a/src/io/show/MetabolicModel.jl +++ b/src/io/show/MetabolicModel.jl @@ -3,9 +3,9 @@ $(TYPEDSIGNATURES) Pretty printing of everything metabolic-modelish. """ -function Base.show(io::IO, ::MIME"text/plain", m::MetabolicModel) +function Base.show(io::Base.IO, ::MIME"text/plain", m::MetabolicModel) _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m))") - if n_reactions(m) <= _constants.default_stoich_show_size + if n_reactions(m) <= constants.default_stoich_show_size println(io, stoichiometry(m)) else # too big to display nicely println(io, "S = [...]") diff --git a/src/io/show/Metabolite.jl b/src/io/show/Metabolite.jl index 9f116fd21..ed31a0368 100644 --- a/src/io/show/Metabolite.jl +++ b/src/io/show/Metabolite.jl @@ -1,4 +1,4 @@ -function Base.show(io::IO, ::MIME"text/plain", m::Metabolite) +function Base.show(io::Base.IO, ::MIME"text/plain", m::Metabolite) for fname in fieldnames(Metabolite) if fname == :charge c = isnothing(getfield(m, fname)) ? nothing : string(getfield(m, fname)) diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl index 752b9acbe..426f0c96e 100644 --- a/src/io/show/Reaction.jl +++ b/src/io/show/Reaction.jl @@ -14,7 +14,7 @@ function _pretty_substances(ss::Vector{String})::String end end -function Base.show(io::IO, ::MIME"text/plain", r::Reaction) +function Base.show(io::Base.IO, ::MIME"text/plain", r::Reaction) if r.upper_bound > 0.0 && r.lower_bound < 0.0 arrow = " ↔ " elseif r.upper_bound <= 0.0 && r.lower_bound < 0.0 diff --git a/src/io/show/Serialized.jl b/src/io/show/Serialized.jl index 598e2ceff..cf83665cc 100644 --- a/src/io/show/Serialized.jl +++ b/src/io/show/Serialized.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Show the [`Serialized`](@ref) model without unnecessarily loading it. """ -function Base.show(io::IO, ::MIME"text/plain", m::Serialized{M}) where {M} +function Base.show(io::Base.IO, ::MIME"text/plain", m::Serialized{M}) where {M} print( io, "Serialized{$M} saved in \"$(m.filename)\" ($(isnothing(m.m) ? "not loaded" : "loaded"))", diff --git a/src/base/constants.jl b/src/misc/constants.jl similarity index 98% rename from src/base/constants.jl rename to src/misc/constants.jl index b789b71af..fdf7aaf4d 100644 --- a/src/base/constants.jl +++ b/src/misc/constants.jl @@ -3,7 +3,7 @@ A named tuple that contains the magic values that are used globally for whatever purposes. """ -const _constants = ( +const constants = ( default_stoich_show_size = 50_000, default_reaction_bound = 1e3, tolerance = 1e-6, diff --git a/src/utils/guesskey.jl b/src/misc/guesskey.jl similarity index 95% rename from src/utils/guesskey.jl rename to src/misc/guesskey.jl index 183e82656..7194526ad 100644 --- a/src/utils/guesskey.jl +++ b/src/misc/guesskey.jl @@ -7,7 +7,7 @@ standardized field names, so we need to try a few possibilities and guess the best one. The keys used to look for valid field names should be ideally specified as constants in `src/base/constants.jl`. """ -function _guesskey(avail, possibilities) +function guesskey(avail, possibilities) x = intersect(possibilities, avail) if isempty(x) diff --git a/src/base/identifiers.jl b/src/misc/identifiers.jl similarity index 97% rename from src/base/identifiers.jl rename to src/misc/identifiers.jl index 92b147a93..10ae9aeb6 100644 --- a/src/base/identifiers.jl +++ b/src/misc/identifiers.jl @@ -4,7 +4,7 @@ genes, etc. If an subject has a matching annotation, then it is assumed that it is part of the associated class of objects. """ module Identifiers -using ..COBREXA.SBOTerms +using ..SBOTerms const EXCHANGE_REACTIONS = [SBOTerms.EXCHANGE_REACTION] diff --git a/src/ontology/SBOTerms.jl b/src/misc/ontology/SBOTerms.jl similarity index 93% rename from src/ontology/SBOTerms.jl rename to src/misc/ontology/SBOTerms.jl index 6a3e2a4f3..c41d1cead 100644 --- a/src/ontology/SBOTerms.jl +++ b/src/misc/ontology/SBOTerms.jl @@ -3,12 +3,13 @@ This module contains SBO terms recognized by COBREXA. For the full ontology, see https://github.com/EBI-BioModels/SBO/blob/master/SBO_OBO.obo. If an SBO term appears here it *may* be used in a function; if an SBO term does -not appear here, then it is *not* used in a COBREXA function. +not appear here, then it is *not* used in any COBREXA function. -These terms are used in `Identifiers.jl` which groups them as appropriate for -use in functions that classify reactions, metabolites, etc. +These terms are used in module `Identifiers` which groups them as appropriate +for use in functions that classify reactions, metabolites, etc. """ module SBOTerms + const FLUX_BALANCE_FRAMEWORK = "SBO:0000624" const RESOURCE_BALANCE_FRAMEWORK = "SBO:0000692" const CONSTRAINT_BASED_FRAMEWORK = "SBO:0000693" @@ -81,4 +82,5 @@ const ACTIVE_TRANSPORT = "SBO:0000657" const PASSIVE_TRANSPORT = "SBO:0000658" const SUBSYSTEM = "SBO:0000633" + end diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index 492566847..7ffc59060 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -28,7 +28,7 @@ function add_community_objective!( # extend model by one reaction community.S = hcat(community.S, objcol) community.xl = [community.xl; 0.0] - community.xu = [community.xu; _constants.default_reaction_bound] + community.xu = [community.xu; constants.default_reaction_bound] community.rxns = [community.rxns; objective_id] community.c = spzeros(size(community.S, 2)) community.c[end] = 1.0 @@ -53,7 +53,7 @@ function add_community_objective!( rxn = Reaction(objective_id) rxn.metabolites = rdict rxn.lower_bound = 0.0 - rxn.upper_bound = _constants.default_reaction_bound + rxn.upper_bound = constants.default_reaction_bound rxn.objective_coefficient = 1.0 community.reactions[rxn.id] = rxn @@ -92,7 +92,7 @@ function update_community_objective!( community.c = spzeros(size(community.S, 2)) community.c[objective_column_index] = 1.0 community.xl[objective_column_index] = 0.0 - community.xu[objective_column_index] = _constants.default_reaction_bound + community.xu[objective_column_index] = constants.default_reaction_bound return nothing # stop annoying return value end diff --git a/src/reconstruction/gapfill_minimum_reactions.jl b/src/reconstruction/gapfill_minimum_reactions.jl index c853aeedc..6034a13d4 100644 --- a/src/reconstruction/gapfill_minimum_reactions.jl +++ b/src/reconstruction/gapfill_minimum_reactions.jl @@ -46,7 +46,7 @@ function gapfill_minimum_reactions( model::MetabolicModel, universal_reactions::Vector{Reaction}, optimizer; - objective_bounds = (_constants.tolerance, _constants.default_reaction_bound), + objective_bounds = (constants.tolerance, constants.default_reaction_bound), maximum_new_reactions = length(universal_reactions), weights = fill(1.0, length(universal_reactions)), modifications = [], diff --git a/src/base/solver.jl b/src/solver.jl similarity index 100% rename from src/base/solver.jl rename to src/solver.jl diff --git a/src/types.jl b/src/types.jl index 5c7a2dce6..d0cc11c0a 100644 --- a/src/types.jl +++ b/src/types.jl @@ -3,6 +3,7 @@ module Types using ..ModuleTools @dse +using ..Internal using SparseArrays, OrderedCollections using HDF5, SBML, JSON, MAT, Serialization #for the storable types @inc_dir types abstract diff --git a/src/types/FluxSummary.jl b/src/types/FluxSummary.jl index 13f136225..f3dfffc1d 100644 --- a/src/types/FluxSummary.jl +++ b/src/types/FluxSummary.jl @@ -60,11 +60,11 @@ Export: function flux_summary( flux_result::Maybe{Dict{String,Float64}}; exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, + exchange_prefixes = constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, exclude_biomass = false, - small_flux_bound = 1.0 / _constants.default_reaction_bound^2, - large_flux_bound = _constants.default_reaction_bound, + small_flux_bound = 1.0 / constants.default_reaction_bound^2, + large_flux_bound = constants.default_reaction_bound, keep_unbounded = false, ) isnothing(flux_result) && return FluxSummary() diff --git a/src/types/FluxVariabilitySummary.jl b/src/types/FluxVariabilitySummary.jl index c1b451096..b977b65f9 100644 --- a/src/types/FluxVariabilitySummary.jl +++ b/src/types/FluxVariabilitySummary.jl @@ -56,8 +56,8 @@ Exchange function flux_variability_summary( flux_result::Tuple{Dict{String,Dict{String,Float64}},Dict{String,Dict{String,Float64}}}; exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, + exchange_prefixes = constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, exclude_biomass = false, ) isnothing(flux_result) && return FluxVariabilitySummary() diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 1ec28ee29..79400bbdc 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -30,8 +30,8 @@ function Reaction( id = ""; name = nothing, metabolites = Dict{String,Float64}(), - lower_bound = -_constants.default_reaction_bound, - upper_bound = _constants.default_reaction_bound, + lower_bound = -constants.default_reaction_bound, + upper_bound = constants.default_reaction_bound, grr = nothing, subsystem = nothing, notes = Notes(), @@ -67,7 +67,7 @@ function Reaction( id::String, metabolites, dir = :bidirectional; - default_bound = _constants.default_reaction_bound, + default_bound = constants.default_reaction_bound, ) if dir == :forward lower_bound = 0.0 diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 047482967..5004fc11f 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -39,15 +39,15 @@ _json_met_name(m, i) = string(get(m, "id", "met$i")) _json_gene_name(g, i) = string(get(g, "id", "gene$i")) JSONModel(json::Dict{String,Any}) = begin - rkey = _guesskey(keys(json), _constants.keynames.rxns) + rkey = guesskey(keys(json), constants.keynames.rxns) isnothing(rkey) && throw(DomainError(keys(json), "JSON model has no reaction keys")) rs = json[rkey] - mkey = _guesskey(keys(json), _constants.keynames.mets) + mkey = guesskey(keys(json), constants.keynames.mets) ms = json[mkey] isnothing(mkey) && throw(DomainError(keys(json), "JSON model has no metabolite keys")) - gkey = _guesskey(keys(json), _constants.keynames.genes) + gkey = guesskey(keys(json), constants.keynames.genes) gs = isnothing(gkey) ? [] : json[gkey] JSONModel( @@ -148,8 +148,8 @@ Get the bounds for reactions, assuming the information is stored in `.lower_bound` and `.upper_bound`. """ Accessors.bounds(model::JSONModel) = ( - [get(rxn, "lower_bound", -_constants.default_reaction_bound) for rxn in model.rxns], - [get(rxn, "upper_bound", _constants.default_reaction_bound) for rxn in model.rxns], + [get(rxn, "lower_bound", -constants.default_reaction_bound) for rxn in model.rxns], + [get(rxn, "upper_bound", constants.default_reaction_bound) for rxn in model.rxns], ) """ @@ -308,7 +308,7 @@ function Base.convert(::Type{JSONModel}, mm::MetabolicModel) json = Dict{String,Any}() json["id"] = "model" # default - json[first(_constants.keynames.genes)] = [ + json[first(constants.keynames.genes)] = [ Dict([ "id" => gid, "name" => gene_name(mm, gid), @@ -317,7 +317,7 @@ function Base.convert(::Type{JSONModel}, mm::MetabolicModel) ],) for gid in gene_ids ] - json[first(_constants.keynames.mets)] = [ + json[first(constants.keynames.mets)] = [ Dict([ "id" => mid, "name" => metabolite_name(mm, mid), @@ -329,7 +329,7 @@ function Base.convert(::Type{JSONModel}, mm::MetabolicModel) ]) for mid in met_ids ] - json[first(_constants.keynames.rxns)] = [ + json[first(constants.keynames.rxns)] = [ begin res = Dict{String,Any}() res["id"] = rid diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 97b43f166..53352425a 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -147,7 +147,7 @@ Extract metabolite formula from key `metFormula` or `metFormulas`. """ Accessors.metabolite_formula(m::MATModel, mid::String) = _maybemap( x -> _parse_formula(x[findfirst(==(mid), metabolites(m))]), - gets(m.mat, nothing, _constants.keynames.metformulas), + gets(m.mat, nothing, constants.keynames.metformulas), ) """ @@ -158,7 +158,7 @@ Extract metabolite charge from `metCharge` or `metCharges`. function Accessors.metabolite_charge(m::MATModel, mid::String) met_charge = _maybemap( x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metcharges), + gets(m.mat, nothing, constants.keynames.metcharges), ) isnan(met_charge) ? 0 : met_charge end @@ -170,7 +170,7 @@ Extract metabolite compartment from `metCompartment` or `metCompartments`. """ Accessors.metabolite_compartment(m::MATModel, mid::String) = _maybemap( x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metcompartments), + gets(m.mat, nothing, constants.keynames.metcompartments), ) """ @@ -200,7 +200,7 @@ Extract reaction name from `rxnNames`. """ Accessors.reaction_name(m::MATModel, rid::String) = _maybemap( x -> x[findfirst(==(rid), reactions(m))], - gets(m.mat, nothing, _constants.keynames.rxnnames), + gets(m.mat, nothing, constants.keynames.rxnnames), ) """ @@ -210,7 +210,7 @@ Extract metabolite name from `metNames`. """ Accessors.metabolite_name(m::MATModel, mid::String) = _maybemap( x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, _constants.keynames.metnames), + gets(m.mat, nothing, constants.keynames.metnames), ) # NOTE: There's no useful standard on how and where to store notes and diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index 69f2d93d9..70d7f702e 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -29,8 +29,8 @@ filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(mod function looks_like_exchange_reaction( rxn_id::String; exclude_biomass = false, - biomass_strings = _constants.biomass_strings, - exchange_prefixes = _constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, + exchange_prefixes = constants.exchange_prefixes, )::Bool any(startswith(rxn_id, x) for x in exchange_prefixes) && !(exclude_biomass && any(occursin(x, rxn_id) for x in biomass_strings)) @@ -77,8 +77,8 @@ findall(looks_like_biomass_reaction, reactions(model)) # returns indices function looks_like_biomass_reaction( rxn_id::String; exclude_exchanges = false, - exchange_prefixes = _constants.exchange_prefixes, - biomass_strings = _constants.biomass_strings, + exchange_prefixes = constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, )::Bool any(occursin(x, rxn_id) for x in biomass_strings) && !(exclude_exchanges && any(startswith(rxn_id, x) for x in exchange_prefixes)) @@ -117,7 +117,7 @@ findall(looks_like_extracellular_metabolite, metabolites(model)) # returns indic """ function looks_like_extracellular_metabolite( met_id::String; - extracellular_suffixes = _constants.extracellular_suffixes, + extracellular_suffixes = constants.extracellular_suffixes, )::Bool any(endswith(met_id, x) for x in extracellular_suffixes) end From d72366f1bd52dda58baf51c28ac68c463f011ca5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 15:28:17 +0200 Subject: [PATCH 014/531] make the main file shorter --- src/COBREXA.jl | 51 +++++-------------------------------------------- src/internal.jl | 10 ++++++++++ src/modules.jl | 34 +++++++++++++++++++++++++++++++++ 3 files changed, 49 insertions(+), 46 deletions(-) create mode 100644 src/internal.jl create mode 100644 src/modules.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index ab72fca90..66d8818ec 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -34,55 +34,14 @@ include_dependency(joinpath(_PKG_ROOT_DIR, "Project.toml")) const version = VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) -module ModuleTools -macro inc(path...) - esc(:(include(joinpath(@__DIR__, $(joinpath(String.(path)...) * ".jl"))))) -end - -macro inc_dir(path...) - dir = joinpath(@__DIR__, String.(path)...) - files = filter(endswith(".jl"), readdir(dir; join = true)) - esc(Expr(:block, (:(include($f)) for f in files)...)) -end - -macro dse() - :(using DocStringExtensions) -end - -macro inject(mod::Symbol, code) - esc(:(Base.eval($mod, $(Expr(:quote, code))))) -end - -# export everything from the local namespace that seems exportable -# (inspired by JuMP.jl, thanks!) -macro export_locals() - quote - for sym in names(@__MODULE__; all = true, imported = true) - sym in [Symbol(@__MODULE__), :eval, :include] && continue - startswith(string(sym), ['_', '#']) && continue - sym == :Internal && continue - @eval export $(Expr(:$, :sym)) - end - end -end - -@export_locals -end - -# load various internal helpers -module Internal -using ..ModuleTools -@dse - -@inc macros -@inc_dir misc ontology -@inc_dir misc +# bootstrap the module machinery +include("modules.jl") +using .ModuleTools -@export_locals -end +# load various internal helpers first +@inc internal # start loading individual user-facing modules -using .ModuleTools @inc log @inc types diff --git a/src/internal.jl b/src/internal.jl new file mode 100644 index 000000000..fca730b38 --- /dev/null +++ b/src/internal.jl @@ -0,0 +1,10 @@ +module Internal +using ..ModuleTools +@dse + +@inc macros +@inc_dir misc ontology +@inc_dir misc + +@export_locals +end diff --git a/src/modules.jl b/src/modules.jl new file mode 100644 index 000000000..75808ce05 --- /dev/null +++ b/src/modules.jl @@ -0,0 +1,34 @@ +module ModuleTools +macro inc(path...) + esc(:(include(joinpath(@__DIR__, $(joinpath(String.(path)...) * ".jl"))))) +end + +macro inc_dir(path...) + dir = joinpath(@__DIR__, String.(path)...) + files = filter(endswith(".jl"), readdir(dir; join = true)) + esc(Expr(:block, (:(include($f)) for f in files)...)) +end + +macro dse() + :(using DocStringExtensions) +end + +macro inject(mod::Symbol, code) + esc(:(Base.eval($mod, $(Expr(:quote, code))))) +end + +# export everything from the local namespace that seems exportable +# (inspired by JuMP.jl, thanks!) +macro export_locals() + quote + for sym in names(@__MODULE__; all = true, imported = true) + sym in [Symbol(@__MODULE__), :eval, :include] && continue + startswith(string(sym), ['_', '#']) && continue + sym == :Internal && continue + @eval export $(Expr(:$, :sym)) + end + end +end + +@export_locals +end From 190a102158f31fac84c3c930baa9809a9ff51218 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 17:43:59 +0200 Subject: [PATCH 015/531] organize Solver and Reconstruction modules --- src/COBREXA.jl | 13 +++------ .../gapfill_minimum_reactions.jl | 0 src/macros/change_bounds.jl | 26 +++++++++--------- src/macros/remove_item.jl | 14 +++++----- src/macros/serialized.jl | 18 +++++++------ src/modules.jl | 2 +- src/reconstruction.jl | 27 +++++++++++++++++++ src/solver.jl | 11 ++++++++ src/types.jl | 3 ++- src/{utils => types/misc}/CoreModel.jl | 0 src/{utils => types/misc}/StandardModel.jl | 0 11 files changed, 77 insertions(+), 37 deletions(-) rename src/{ => analysis}/reconstruction/gapfill_minimum_reactions.jl (100%) create mode 100644 src/reconstruction.jl rename src/{utils => types/misc}/CoreModel.jl (100%) rename src/{utils => types/misc}/StandardModel.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 66d8818ec..f8a69cb17 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -40,12 +40,14 @@ using .ModuleTools # load various internal helpers first @inc internal +@inc log # start loading individual user-facing modules - -@inc log @inc types + @inc io +@inc solver +@inc reconstruction # TODO: this needs to be assimilated to actual modules module Rest @@ -57,13 +59,6 @@ using ..Types using ..Accessors using ..IO -using JuMP - -@inc solver - -#joinpath("io", "show"), -#"reconstruction", -#joinpath("reconstruction", "modifications"), #"analysis", #joinpath("analysis", "modifications"), #joinpath("analysis", "sampling"), diff --git a/src/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl similarity index 100% rename from src/reconstruction/gapfill_minimum_reactions.jl rename to src/analysis/reconstruction/gapfill_minimum_reactions.jl diff --git a/src/macros/change_bounds.jl b/src/macros/change_bounds.jl index 83a479479..5073f2f48 100644 --- a/src/macros/change_bounds.jl +++ b/src/macros/change_bounds.jl @@ -47,18 +47,20 @@ macro _change_bounds_fn(model_type, idx_type, args...) ``` """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::$model_type, - $idx_var::$idx_type; - lower = $missing_default, - upper = $missing_default, - ) = $body + esc( + Expr( + :macrocall, + Symbol("@doc"), + __source__, + docstring, + :( + $fname( + model::$model_type, + $idx_var::$idx_type; + lower = $missing_default, + upper = $missing_default, + ) = $body + ), ), ) end diff --git a/src/macros/remove_item.jl b/src/macros/remove_item.jl index 0caf50c55..0f000a1a0 100644 --- a/src/macros/remove_item.jl +++ b/src/macros/remove_item.jl @@ -31,11 +31,13 @@ macro _remove_fn(objname, model_type, idx_type, args...) $(inplace ? "in-place" : "and return the modified model"). """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :($fname(model::$model_type, $idx_var::$idx_type) = $body), + esc( + Expr( + :macrocall, + Symbol("@doc"), + __source__, + docstring, + :($fname(model::$model_type, $idx_var::$idx_type) = $body), + ), ) end diff --git a/src/macros/serialized.jl b/src/macros/serialized.jl index 0883e5a91..8b9aac0bf 100644 --- a/src/macros/serialized.jl +++ b/src/macros/serialized.jl @@ -12,14 +12,16 @@ macro _serialized_change_unwrap(fn::Symbol) Calls [`$fn`](@ref) of the internal serialized model type. Returns the modified un-serialized model. """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fn(model::Serialized, args...; kwargs...) = - $fn(unwrap_serialized(model), args...; kwargs...) + esc( + Expr( + :macrocall, + Symbol("@doc"), + __source__, + docstring, + :( + $fn(model::Serialized, args...; kwargs...) = + $fn(unwrap_serialized(model), args...; kwargs...) + ), ), ) end diff --git a/src/modules.jl b/src/modules.jl index 75808ce05..752fe63e1 100644 --- a/src/modules.jl +++ b/src/modules.jl @@ -13,7 +13,7 @@ macro dse() :(using DocStringExtensions) end -macro inject(mod::Symbol, code) +macro inject(mod, code) esc(:(Base.eval($mod, $(Expr(:quote, code))))) end diff --git a/src/reconstruction.jl b/src/reconstruction.jl new file mode 100644 index 000000000..768686c64 --- /dev/null +++ b/src/reconstruction.jl @@ -0,0 +1,27 @@ + +module Reconstruction +using ..ModuleTools +@dse + +using ..Internal.Macros +using ..Log.Internal +using ..Types +using ..Accessors + +@inc_dir reconstruction + +module Modifications +using ..ModuleTools +@dse +end + +@export_locals +end + +# this needs to import from Reconstruction +@inject Reconstruction.Modifications begin + using ...Reconstruction + @inc_dir reconstruction modifications + + @export_locals +end diff --git a/src/solver.jl b/src/solver.jl index 25fb9ed72..d05102b49 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -1,4 +1,12 @@ +module Solver +using ..ModuleTools +@dse + +using ..Types +using ..Accessors +using JuMP + """ $(TYPEDSIGNATURES) @@ -131,3 +139,6 @@ flux_balance_analysis(model, ...) |> flux_dict(model) ``` """ flux_dict(model::MetabolicModel) = opt_model -> flux_dict(model, opt_model) + +@export_locals +end diff --git a/src/types.jl b/src/types.jl index d0cc11c0a..ec0a1a920 100644 --- a/src/types.jl +++ b/src/types.jl @@ -23,12 +23,13 @@ end # the modules depend on each other so we have to inject the stuff like this @inject Types begin using ..Accessors - using ..Internal.Macros using ..Log.Internal: @io_log + @inc_dir types @inc_dir types models @inc_dir types wrappers + @inc_dir types misc @export_locals end diff --git a/src/utils/CoreModel.jl b/src/types/misc/CoreModel.jl similarity index 100% rename from src/utils/CoreModel.jl rename to src/types/misc/CoreModel.jl diff --git a/src/utils/StandardModel.jl b/src/types/misc/StandardModel.jl similarity index 100% rename from src/utils/StandardModel.jl rename to src/types/misc/StandardModel.jl From 938063ba64b93650fe7a80697e9776dc3eabc5c8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 18:03:00 +0200 Subject: [PATCH 016/531] sort out some things from `utils/` --- src/types.jl | 2 +- .../misc}/chemical_formulas.jl | 0 src/{utils => types/misc}/gecko.jl | 0 .../misc}/gene_associations.jl | 0 src/types/misc/smoment.jl | 23 +++++++++++++++++++ src/utils/smoment.jl | 23 ------------------- 6 files changed, 24 insertions(+), 24 deletions(-) rename src/{utils => types/misc}/chemical_formulas.jl (100%) rename src/{utils => types/misc}/gecko.jl (100%) rename src/{utils => types/misc}/gene_associations.jl (100%) create mode 100644 src/types/misc/smoment.jl diff --git a/src/types.jl b/src/types.jl index ec0a1a920..a1df961bc 100644 --- a/src/types.jl +++ b/src/types.jl @@ -24,7 +24,7 @@ end @inject Types begin using ..Accessors using ..Internal.Macros - using ..Log.Internal: @io_log + using ..Log.Internal: @io_log, @models_log @inc_dir types @inc_dir types models diff --git a/src/utils/chemical_formulas.jl b/src/types/misc/chemical_formulas.jl similarity index 100% rename from src/utils/chemical_formulas.jl rename to src/types/misc/chemical_formulas.jl diff --git a/src/utils/gecko.jl b/src/types/misc/gecko.jl similarity index 100% rename from src/utils/gecko.jl rename to src/types/misc/gecko.jl diff --git a/src/utils/gene_associations.jl b/src/types/misc/gene_associations.jl similarity index 100% rename from src/utils/gene_associations.jl rename to src/types/misc/gene_associations.jl diff --git a/src/types/misc/smoment.jl b/src/types/misc/smoment.jl new file mode 100644 index 000000000..04c2fed4a --- /dev/null +++ b/src/types/misc/smoment.jl @@ -0,0 +1,23 @@ + +""" +$(TYPEDSIGNATURES) + +Internal helper for systematically naming reactions in [`SMomentModel`](@ref). +""" +_smoment_reaction_name(original_name::String, direction::Int) = + direction == 0 ? original_name : + direction > 0 ? "$original_name#forward" : "$original_name#reverse" + +""" +$(TYPEDSIGNATURES) + +Retrieve a utility mapping between reactions and split reactions; rows +correspond to "original" reactions, columns correspond to "split" reactions. +""" +_smoment_column_reactions(model::SMomentModel) = sparse( + [col.reaction_idx for col in model.columns], + 1:length(model.columns), + [col.direction >= 0 ? 1 : -1 for col in model.columns], + n_reactions(model.inner), + length(model.columns), +) diff --git a/src/utils/smoment.jl b/src/utils/smoment.jl index 4bbf4783c..37b8ea23d 100644 --- a/src/utils/smoment.jl +++ b/src/utils/smoment.jl @@ -2,29 +2,6 @@ """ $(TYPEDSIGNATURES) -Internal helper for systematically naming reactions in [`SMomentModel`](@ref). -""" -_smoment_reaction_name(original_name::String, direction::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward" : "$original_name#reverse" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -_smoment_column_reactions(model::SMomentModel) = sparse( - [col.reaction_idx for col in model.columns], - 1:length(model.columns), - [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_reactions(model.inner), - length(model.columns), -) - -""" -$(TYPEDSIGNATURES) - Compute a "score" for picking the most viable isozyme for [`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of the isozyme. This is used because sMOMENT algorithm can not handle multiple From d9184f34a2c1a562a7706cb2951124d7cca0ffce Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 18:18:07 +0200 Subject: [PATCH 017/531] make analysis and utils import well --- src/COBREXA.jl | 18 ++---------------- src/analysis.jl | 35 +++++++++++++++++++++++++++++++++++ src/macros/is_xxx_reaction.jl | 24 +++++++++++++----------- src/utils.jl | 19 +++++++++++++++++++ 4 files changed, 69 insertions(+), 27 deletions(-) create mode 100644 src/analysis.jl create mode 100644 src/utils.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index f8a69cb17..8765b8bd5 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -47,22 +47,8 @@ using .ModuleTools @inc io @inc solver +@inc analysis @inc reconstruction - -# TODO: this needs to be assimilated to actual modules -module Rest -using ..ModuleTools -@dse - -using ..Internal -using ..Types -using ..Accessors -using ..IO - -#"analysis", -#joinpath("analysis", "modifications"), -#joinpath("analysis", "sampling"), -#joinpath("utils"), -end +@inc utils end # module diff --git a/src/analysis.jl b/src/analysis.jl new file mode 100644 index 000000000..7d38ddfe7 --- /dev/null +++ b/src/analysis.jl @@ -0,0 +1,35 @@ + +module Analysis +using ..ModuleTools +@dse + +using ..Types +using ..Accessors +using ..Log.Internal: @models_log + +using Distributed, DistributedData +using JuMP + +@inc_dir analysis +@inc_dir analysis sampling + +module Modifications +using ..ModuleTools +@dse +end + +@export_locals +end + +# this needs to import from Analysis +@inject Analysis.Modifications begin + using ...Analysis + using ...Types + using ...Accessors + + using JuMP + + @inc_dir analysis modifications + + @export_locals +end diff --git a/src/macros/is_xxx_reaction.jl b/src/macros/is_xxx_reaction.jl index b8601df86..f63ec514b 100644 --- a/src/macros/is_xxx_reaction.jl +++ b/src/macros/is_xxx_reaction.jl @@ -33,17 +33,19 @@ macro _is_reaction_fn(anno_id, identifiers) the reaction annotations, use the keys in `annotation_keys` to look for entries. Returns false if no hits or if no keys are found. """ - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::MetabolicModel, - reaction_id::String; - annotation_keys = ["sbo", "SBO"], - ) = $body + esc( + Expr( + :macrocall, + Symbol("@doc"), + __source__, + docstring, + :( + $fname( + model::MetabolicModel, + reaction_id::String; + annotation_keys = ["sbo", "SBO"], + ) = $body + ), ), ) end diff --git a/src/utils.jl b/src/utils.jl new file mode 100644 index 000000000..85b5d3666 --- /dev/null +++ b/src/utils.jl @@ -0,0 +1,19 @@ + +# TODO: This is now for stuff that didn't really fit anywhere else. It might be +# much much more useful to actually sort out the utils into individual +# namespaces. +module Utils +using ..ModuleTools +@dse + +using ..Types +using ..Internal.Identifiers +using ..Internal.Macros + +using SparseArrays, OrderedCollections +using HDF5 + +@inc_dir utils + +@export_locals +end From 95d10a585b08b27b5a8e57ac1fcd2f17cdc8860d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 27 Sep 2022 23:30:37 +0200 Subject: [PATCH 018/531] many small fixes --- src/analysis.jl | 10 ++- src/io.jl | 1 + src/reconstruction.jl | 7 +- src/types.jl | 2 + src/types/FluxSummary.jl | 108 ------------------------------ src/types/models/CoreModel.jl | 2 +- src/types/models/StandardModel.jl | 2 +- src/utils.jl | 5 ++ src/utils/flux_summary.jl | 108 ++++++++++++++++++++++++++++++ test/base/log.jl | 13 ++++ test/base/logging/log.jl | 13 ---- test/base/utils/Serialized.jl | 2 +- test/runtests.jl | 17 ++++- 13 files changed, 160 insertions(+), 130 deletions(-) create mode 100644 src/utils/flux_summary.jl create mode 100644 test/base/log.jl delete mode 100644 test/base/logging/log.jl diff --git a/src/analysis.jl b/src/analysis.jl index 7d38ddfe7..b22b9cfee 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -3,15 +3,18 @@ module Analysis using ..ModuleTools @dse -using ..Types using ..Accessors using ..Log.Internal: @models_log +using ..Solver +using ..Types using Distributed, DistributedData using JuMP +using StableRNGs, Random @inc_dir analysis @inc_dir analysis sampling +@inc_dir analysis reconstruction module Modifications using ..ModuleTools @@ -23,11 +26,14 @@ end # this needs to import from Analysis @inject Analysis.Modifications begin + using ...Accessors using ...Analysis + using ...Solver using ...Types - using ...Accessors + using ...Internal: constants using JuMP + using SparseArrays @inc_dir analysis modifications diff --git a/src/io.jl b/src/io.jl index ec9a3ed83..5ea207790 100644 --- a/src/io.jl +++ b/src/io.jl @@ -3,6 +3,7 @@ using ..ModuleTools @dse using ..Types +using ..Types: _maybemap using ..Accessors using ..Internal: constants using ..Log.Internal: @io_log diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 768686c64..5bba14188 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -3,10 +3,15 @@ module Reconstruction using ..ModuleTools @dse +using ..Accessors +using ..Analysis +using ..Internal: constants using ..Internal.Macros using ..Log.Internal using ..Types -using ..Accessors + +using SparseArrays, OrderedCollections +using MacroTools @inc_dir reconstruction diff --git a/src/types.jl b/src/types.jl index a1df961bc..0e90deb89 100644 --- a/src/types.jl +++ b/src/types.jl @@ -16,6 +16,8 @@ using ..ModuleTools @dse using ..Types using ..Internal.Macros +using SparseArrays + @inc_dir types accessors @export_locals end diff --git a/src/types/FluxSummary.jl b/src/types/FluxSummary.jl index f3dfffc1d..e2cddf37c 100644 --- a/src/types/FluxSummary.jl +++ b/src/types/FluxSummary.jl @@ -27,111 +27,3 @@ function FluxSummary() OrderedDict{String,Float64}(), ) end - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of fluxes into small, useful representation of the most -important information contained. Useful for pretty-printing and quickly -exploring the results. Internally this function uses -[`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments -passed to these functions. Use this if your model has non-standard ids for -reactions. Fluxes smaller than `small_flux_bound` are not stored, while fluxes -larger than `large_flux_bound` are only stored if `keep_unbounded` is `true`. - -# Example -``` -julia> sol = flux_dict(flux_balance_analysis(model, Tulip.Optimizer)) -julia> fr = flux_summary(sol) -Biomass: - BIOMASS_Ecoli_core_w_GAM: 0.8739 -Import: - EX_o2_e: -21.7995 - EX_glc__D_e: -10.0 - EX_nh4_e: -4.7653 - EX_pi_e: -3.2149 -Export: - EX_h_e: 17.5309 - EX_co2_e: 22.8098 - EX_h2o_e: 29.1758 -``` -""" -function flux_summary( - flux_result::Maybe{Dict{String,Float64}}; - exclude_exchanges = false, - exchange_prefixes = constants.exchange_prefixes, - biomass_strings = constants.biomass_strings, - exclude_biomass = false, - small_flux_bound = 1.0 / constants.default_reaction_bound^2, - large_flux_bound = constants.default_reaction_bound, - keep_unbounded = false, -) - isnothing(flux_result) && return FluxSummary() - - rxn_ids = collect(keys(flux_result)) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - ex_fluxes = [flux_result[k] for k in ex_rxns] - bmass_fluxes = [flux_result[k] for k in bmasses] - - idx_srt_fluxes = sortperm(ex_fluxes) - import_fluxes = [ - idx for - idx in idx_srt_fluxes if -large_flux_bound < ex_fluxes[idx] <= -small_flux_bound - ] - export_fluxes = [ - idx for - idx in idx_srt_fluxes if small_flux_bound < ex_fluxes[idx] <= large_flux_bound - ] - - if keep_unbounded - lower_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] <= -large_flux_bound] - upper_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] >= large_flux_bound] - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip( - [ex_rxns[lower_unbounded]; ex_rxns[upper_unbounded]], - [ex_fluxes[lower_unbounded]; ex_fluxes[upper_unbounded]], - ) - ), - ) - else - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict{String,Float64}(), - ) - end -end diff --git a/src/types/models/CoreModel.jl b/src/types/models/CoreModel.jl index ffef731f6..05ae63ade 100644 --- a/src/types/models/CoreModel.jl +++ b/src/types/models/CoreModel.jl @@ -114,7 +114,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. Accessors.""" -reaction_stoichiometry(m::CoreModel, rid::String)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::CoreModel, rid::String)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) """ diff --git a/src/types/models/StandardModel.jl b/src/types/models/StandardModel.jl index 5d8254d6b..d2a9c6e3c 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -286,7 +286,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. """ -reaction_stoichiometry(m::StandardModel, rid::String)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::StandardModel, rid::String)::Dict{String,Float64} = m.reactions[rid].metabolites """ diff --git a/src/utils.jl b/src/utils.jl index 85b5d3666..df2b89eda 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -7,13 +7,18 @@ using ..ModuleTools @dse using ..Types +using ..Accessors using ..Internal.Identifiers +using ..Internal: constants using ..Internal.Macros using SparseArrays, OrderedCollections using HDF5 +using Serialization @inc_dir utils @export_locals end + +@inject Analysis.Modifications using ...Utils: is_boundary diff --git a/src/utils/flux_summary.jl b/src/utils/flux_summary.jl new file mode 100644 index 000000000..a726edc33 --- /dev/null +++ b/src/utils/flux_summary.jl @@ -0,0 +1,108 @@ + +""" +$(TYPEDSIGNATURES) + +Summarize a dictionary of fluxes into small, useful representation of the most +important information contained. Useful for pretty-printing and quickly +exploring the results. Internally this function uses +[`looks_like_biomass_reaction`](@ref) and +[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments +passed to these functions. Use this if your model has non-standard ids for +reactions. Fluxes smaller than `small_flux_bound` are not stored, while fluxes +larger than `large_flux_bound` are only stored if `keep_unbounded` is `true`. + +# Example +``` +julia> sol = flux_dict(flux_balance_analysis(model, Tulip.Optimizer)) +julia> fr = flux_summary(sol) +Biomass: + BIOMASS_Ecoli_core_w_GAM: 0.8739 +Import: + EX_o2_e: -21.7995 + EX_glc__D_e: -10.0 + EX_nh4_e: -4.7653 + EX_pi_e: -3.2149 +Export: + EX_h_e: 17.5309 + EX_co2_e: 22.8098 + EX_h2o_e: 29.1758 +``` +""" +function flux_summary( + flux_result::Maybe{Dict{String,Float64}}; + exclude_exchanges = false, + exchange_prefixes = constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, + exclude_biomass = false, + small_flux_bound = 1.0 / constants.default_reaction_bound^2, + large_flux_bound = constants.default_reaction_bound, + keep_unbounded = false, +) + isnothing(flux_result) && return FluxSummary() + + rxn_ids = collect(keys(flux_result)) + ex_rxns = filter( + x -> looks_like_exchange_reaction( + x, + exclude_biomass = exclude_biomass, + biomass_strings = biomass_strings, + exchange_prefixes = exchange_prefixes, + ), + rxn_ids, + ) + bmasses = filter( + x -> looks_like_biomass_reaction( + x; + exclude_exchanges = exclude_exchanges, + exchange_prefixes = exchange_prefixes, + biomass_strings = biomass_strings, + ), + rxn_ids, + ) + + ex_fluxes = [flux_result[k] for k in ex_rxns] + bmass_fluxes = [flux_result[k] for k in bmasses] + + idx_srt_fluxes = sortperm(ex_fluxes) + import_fluxes = [ + idx for + idx in idx_srt_fluxes if -large_flux_bound < ex_fluxes[idx] <= -small_flux_bound + ] + export_fluxes = [ + idx for + idx in idx_srt_fluxes if small_flux_bound < ex_fluxes[idx] <= large_flux_bound + ] + + if keep_unbounded + lower_unbounded = + [idx for idx in idx_srt_fluxes if ex_fluxes[idx] <= -large_flux_bound] + upper_unbounded = + [idx for idx in idx_srt_fluxes if ex_fluxes[idx] >= large_flux_bound] + return FluxSummary( + OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), + OrderedDict( + k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) + ), + OrderedDict( + k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) + ), + OrderedDict( + k => v for (k, v) in zip( + [ex_rxns[lower_unbounded]; ex_rxns[upper_unbounded]], + [ex_fluxes[lower_unbounded]; ex_fluxes[upper_unbounded]], + ) + ), + ) + else + return FluxSummary( + OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), + OrderedDict( + k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) + ), + OrderedDict( + k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) + ), + OrderedDict{String,Float64}(), + ) + end +end diff --git a/test/base/log.jl b/test/base/log.jl new file mode 100644 index 000000000..702adff1e --- /dev/null +++ b/test/base/log.jl @@ -0,0 +1,13 @@ +COBREXA.Log.Internal.@make_logging_tag TEST "testing stuff" + +log_TEST() +@testset "Logging on" begin + @test TEST_log_enabled + @test_logs (:warn, "qweasdzxc") @TEST_log @warn "qweasdzxc" +end + +log_TEST(false) +@testset "Logging off" begin + @test !TEST_log_enabled + @test_logs @TEST_log @warn "all okay!" +end diff --git a/test/base/logging/log.jl b/test/base/logging/log.jl deleted file mode 100644 index a41342d6a..000000000 --- a/test/base/logging/log.jl +++ /dev/null @@ -1,13 +0,0 @@ -COBREXA.@_make_logging_tag TEST "testing stuff" - -log_TEST() -@testset "Logging on" begin - @test _TEST_log_enabled - @test_logs (:warn, "qweasdzxc") @_TEST_log @warn "qweasdzxc" -end - -log_TEST(false) -@testset "Logging off" begin - @test !_TEST_log_enabled - @test_logs @_TEST_log @warn "all okay!" -end diff --git a/test/base/utils/Serialized.jl b/test/base/utils/Serialized.jl index ef015383c..5e95d1424 100644 --- a/test/base/utils/Serialized.jl +++ b/test/base/utils/Serialized.jl @@ -12,7 +12,7 @@ @test isequal(m, sm.m) # the data is kept okay @test sm2.m == nothing # nothing is cached here - @test isequal(m, COBREXA.Serialization.deserialize(tmpfile("toy2.serialized"))) # it was written as-is + @test isequal(m, deserialize(tmpfile("toy2.serialized"))) # it was written as-is @test issetequal( reactions(convert(StandardModel, sm)), reactions(convert(StandardModel, sm2)), diff --git a/test/runtests.jl b/test/runtests.jl index e19df54f1..6a783ab14 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,23 +1,34 @@ using COBREXA, Test +using COBREXA.Types, + COBREXA.Accessors, + COBREXA.Analysis, + COBREXA.Analysis.Modifications, + COBREXA.Reconstruction, + COBREXA.Reconstruction.Modifications, + COBREXA.Utils, + COBREXA.IO, + COBREXA.Solver + using Aqua +using Clarabel using Distributed using Downloads +using GLPK # for MILPs using JSON using JuMP using LinearAlgebra using MAT using OrderedCollections -using Clarabel +using Serialization using SHA using SparseArrays using Statistics using Tulip -using GLPK # for MILPs # tolerance for comparing analysis results (should be a bit bigger than the # error tolerance in computations) -TEST_TOLERANCE = 10 * COBREXA._constants.tolerance +TEST_TOLERANCE = 10 * COBREXA.Internal.constants.tolerance QP_TEST_TOLERANCE = 1e-2 # for Clarabel print_timing(fn, t) = @info "$(fn) done in $(round(t; digits = 2))s" From 7b443f971726b54788475c9453c2f90d7e63f4b3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 28 Sep 2022 10:35:55 +0200 Subject: [PATCH 019/531] fixup hopefully all simple namespacing issues --- src/analysis.jl | 13 +++++++++--- src/analysis/gecko.jl | 12 +++++------ src/analysis/smoment.jl | 8 ++++---- src/io.jl | 20 +++++++++++++++++- src/io/h5.jl | 6 +++--- src/io/misc/h5.jl | 32 +++++++++++++++++++++++++++++ src/io/show/Reaction.jl | 2 +- src/types.jl | 23 +++++++++++++++++++-- src/types/abstract/Maybe.jl | 20 +----------------- src/types/misc/chemical_formulas.jl | 4 ++-- src/types/misc/gecko.jl | 14 ++++++------- src/types/misc/gene_associations.jl | 16 +++++++-------- src/types/misc/maybe.jl | 18 ++++++++++++++++ src/types/misc/smoment.jl | 4 ++-- src/types/models/HDF5Model.jl | 6 +++--- src/types/models/JSONModel.jl | 22 ++++++++++---------- src/types/models/MATModel.jl | 31 +++++++++++++--------------- src/types/models/SBMLModel.jl | 12 +++++------ src/types/models/StandardModel.jl | 6 +++--- src/types/wrappers/GeckoModel.jl | 22 ++++++++++---------- src/types/wrappers/SMomentModel.jl | 14 ++++++------- src/utils/HDF5Model.jl | 32 ----------------------------- test/runtests.jl | 2 +- 23 files changed, 190 insertions(+), 149 deletions(-) create mode 100644 src/io/misc/h5.jl create mode 100644 src/types/misc/maybe.jl delete mode 100644 src/utils/HDF5Model.jl diff --git a/src/analysis.jl b/src/analysis.jl index b22b9cfee..600891016 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -4,13 +4,19 @@ using ..ModuleTools @dse using ..Accessors +using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types +using ..Types: _GeckoReactionColumn, _SMomentColumn -using Distributed, DistributedData +using Distributed +using DistributedData using JuMP -using StableRNGs, Random +using LinearAlgebra +using Random +using SparseArrays +using StableRNGs @inc_dir analysis @inc_dir analysis sampling @@ -28,11 +34,12 @@ end @inject Analysis.Modifications begin using ...Accessors using ...Analysis + using ...Internal: constants using ...Solver using ...Types - using ...Internal: constants using JuMP + using LinearAlgebra using SparseArrays @inc_dir analysis modifications diff --git a/src/analysis/gecko.jl b/src/analysis/gecko.jl index 9436ee7e6..11f9d831c 100644 --- a/src/analysis/gecko.jl +++ b/src/analysis/gecko.jl @@ -48,7 +48,7 @@ function make_gecko_model( (grp -> gene_product_mass_group_bound[grp]) # ...it would be nicer to have an overload for this, but kwargs can't be used for dispatch - columns = Vector{_gecko_reaction_column}() + columns = Vector{_GeckoReactionColumn}() coupling_row_reaction = Int[] coupling_row_gene_product = Int[] @@ -62,7 +62,7 @@ function make_gecko_model( for i = 1:n_reactions(model) isozymes = ris_(rids[i]) if isempty(isozymes) - push!(columns, _gecko_reaction_column(i, 0, 0, 0, lbs[i], ubs[i], [])) + push!(columns, _GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) continue end @@ -108,7 +108,7 @@ function make_gecko_model( # make a new column push!( columns, - _gecko_reaction_column( + _GeckoReactionColumn( i, iidx, dir, @@ -133,16 +133,16 @@ function make_gecko_model( mg_gid_lookup[mg] = [gid] end end - coupling_row_mass_group = Vector{_gecko_capacity}() + coupling_row_mass_group = Vector{_GeckoCapacity}() for (grp, gs) in mg_gid_lookup idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] mms = gpmm_.(gs) - push!(coupling_row_mass_group, _gecko_capacity(grp, idxs, mms, gmgb_(grp))) + push!(coupling_row_mass_group, _GeckoCapacity(grp, idxs, mms, gmgb_(grp))) end GeckoModel( [ - _gecko_reaction_column_reactions(columns, model)' * objective(model) + _GeckoReactionColumn_reactions(columns, model)' * objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/analysis/smoment.jl b/src/analysis/smoment.jl index 50b258c1f..0ce6f3ae1 100644 --- a/src/analysis/smoment.jl +++ b/src/analysis/smoment.jl @@ -32,7 +32,7 @@ function make_smoment_model( gene_product_molar_mass isa Function ? gene_product_molar_mass : (gid -> gene_product_molar_mass[gid]) - columns = Vector{_smoment_column}() + columns = Vector{_SMomentColumn}() (lbs, ubs) = bounds(model) rids = reactions(model) @@ -41,7 +41,7 @@ function make_smoment_model( isozyme = ris_(rids[i]) if isnothing(isozyme) # non-enzymatic reaction (or a totally ignored one) - push!(columns, _smoment_column(i, 0, lbs[i], ubs[i], 0)) + push!(columns, _SMomentColumn(i, 0, lbs[i], ubs[i], 0)) continue end @@ -51,7 +51,7 @@ function make_smoment_model( # reaction can run in reverse push!( columns, - _smoment_column(i, -1, max(-ubs[i], 0), -lbs[i], mw / isozyme.kcat_reverse), + _SMomentColumn(i, -1, max(-ubs[i], 0), -lbs[i], mw / isozyme.kcat_reverse), ) end @@ -59,7 +59,7 @@ function make_smoment_model( # reaction can run forward push!( columns, - _smoment_column(i, 1, max(lbs[i], 0), ubs[i], mw / isozyme.kcat_forward), + _SMomentColumn(i, 1, max(lbs[i], 0), ubs[i], mw / isozyme.kcat_forward), ) end end diff --git a/src/io.jl b/src/io.jl index 5ea207790..eddd399ea 100644 --- a/src/io.jl +++ b/src/io.jl @@ -3,7 +3,7 @@ using ..ModuleTools @dse using ..Types -using ..Types: _maybemap +using ..Types.Internal: maybemap, unparse_grr using ..Accessors using ..Internal: constants using ..Log.Internal: @io_log @@ -13,5 +13,23 @@ using JSON, MAT, SBML, HDF5 @inc_dir io @inc_dir io show +module Internal +using ..ModuleTools +@dse +end + @export_locals end + +@inject IO.Internal begin + using ..Types + using HDF5 + using SparseArrays + + @inc_dir io misc + + @export_locals +end + +@inject IO using .Internal +@inject Types using ..IO.Internal: h5_read_sparse diff --git a/src/io/h5.jl b/src/io/h5.jl index 5d7785305..5fe0642d4 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -28,9 +28,9 @@ function save_h5_model(model::MetabolicModel, file_name::String)::HDF5Model h5open(file_name, "w") do f write(f, "metabolites", mets[metp]) write(f, "reactions", rxns[rxnp]) - _h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) - _h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) - _h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) + h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) + h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) + h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) let (lbs, ubs) = bounds(model) write(f, "lower_bounds", lbs[rxnp]) write(f, "upper_bounds", ubs[rxnp]) diff --git a/src/io/misc/h5.jl b/src/io/misc/h5.jl new file mode 100644 index 000000000..b24aefb4e --- /dev/null +++ b/src/io/misc/h5.jl @@ -0,0 +1,32 @@ + +h5_mmap_nonempty(x) = length(x) > 0 ? HDF5.readmmap(x) : HDF5.read(x) + +function h5_write_sparse(g::HDF5.Group, v::SparseVector) + write(g, "n", v.n) + write(g, "nzind", v.nzind) + write(g, "nzval", v.nzval) +end + +function h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseVector} + n = read(g["n"]) + nzind = h5_mmap_nonempty(g["nzind"]) + nzval = h5_mmap_nonempty(g["nzval"]) + SparseVector{eltype(nzval),eltype(nzind)}(n, nzind, nzval) +end + +function h5_write_sparse(g::HDF5.Group, m::SparseMatrixCSC) + write(g, "m", m.m) + write(g, "n", m.n) + write(g, "colptr", m.colptr) + write(g, "rowval", m.rowval) + write(g, "nzval", m.nzval) +end + +function h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseMatrixCSC} + m = read(g["m"]) + n = read(g["n"]) + colptr = h5_mmap_nonempty(g["colptr"]) + rowval = h5_mmap_nonempty(g["rowval"]) + nzval = h5_mmap_nonempty(g["nzval"]) + SparseMatrixCSC{eltype(nzval),eltype(colptr)}(m, n, colptr, rowval, nzval) +end diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl index 426f0c96e..49d174a13 100644 --- a/src/io/show/Reaction.jl +++ b/src/io/show/Reaction.jl @@ -40,7 +40,7 @@ function Base.show(io::Base.IO, ::MIME"text/plain", r::Reaction) _pretty_print_keyvals( io, "Reaction.$(string(fname)): ", - _maybemap(x -> _unparse_grr(String, x), r.grr), + maybemap(x -> unparse_grr(String, x), r.grr), ) elseif fname in (:lower_bound, :upper_bound, :objective_coefficient) _pretty_print_keyvals( diff --git a/src/types.jl b/src/types.jl index 0e90deb89..eec52a56f 100644 --- a/src/types.jl +++ b/src/types.jl @@ -18,6 +18,11 @@ using ..Types using ..Internal.Macros using SparseArrays +module Internal +using ..ModuleTools +@dse +end + @inc_dir types accessors @export_locals end @@ -26,12 +31,26 @@ end @inject Types begin using ..Accessors using ..Internal.Macros - using ..Log.Internal: @io_log, @models_log + using ..Log.Internal: @io_log @inc_dir types @inc_dir types models @inc_dir types wrappers - @inc_dir types misc @export_locals end + +@inject Types.Internal begin + using ..Types + using ..Accessors + using ..Log.Internal: @models_log + + using SBML + + @inc_dir types misc + @export_locals +end + +@inject Types begin + using .Internal +end diff --git a/src/types/abstract/Maybe.jl b/src/types/abstract/Maybe.jl index 7b5df0a39..8aef5b515 100644 --- a/src/types/abstract/Maybe.jl +++ b/src/types/abstract/Maybe.jl @@ -2,24 +2,6 @@ """ Maybe{T} = Union{Nothing, T} -A nice name for "nullable" type. +A nice name for a "nullable" type. """ const Maybe{T} = Union{Nothing,T} - -""" -$(TYPEDSIGNATURES) - -Fold the `Maybe{T}` down to `T` by defaulting. -""" -function _default(d::T, x::Maybe{T})::T where {T} - isnothing(x) ? d : x -end - -""" -$(TYPEDSIGNATURES) - -Apply a function to `x` only if it is not `nothing`. -""" -function _maybemap(f, x::Maybe)::Maybe - isnothing(x) ? nothing : f(x) -end diff --git a/src/types/misc/chemical_formulas.jl b/src/types/misc/chemical_formulas.jl index d4c081619..f4682fc24 100644 --- a/src/types/misc/chemical_formulas.jl +++ b/src/types/misc/chemical_formulas.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) Parse a formula in format `C2H6O` into a [`MetaboliteFormula`](@ref), which is basically a dictionary of atom counts in the molecule. """ -function _parse_formula(f::String)::MetaboliteFormula +function parse_formula(f::String)::MetaboliteFormula res = Dict{String,Int}() pattern = @r_str "([A-Z][a-z]*)([1-9][0-9]*)?" @@ -21,6 +21,6 @@ $(TYPEDSIGNATURES) Format [`MetaboliteFormula`](@ref) to `String`. """ -function _unparse_formula(f::MetaboliteFormula)::String +function unparse_formula(f::MetaboliteFormula)::String return join(["$elem$n" for (elem, n) in f]) end diff --git a/src/types/misc/gecko.jl b/src/types/misc/gecko.jl index 219f45b6a..865bb03c5 100644 --- a/src/types/misc/gecko.jl +++ b/src/types/misc/gecko.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Internal helper for systematically naming reactions in [`GeckoModel`](@ref). """ -_gecko_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = +gecko_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = direction == 0 ? original_name : direction > 0 ? "$original_name#forward#$isozyme_idx" : "$original_name#reverse#$isozyme_idx" @@ -15,15 +15,15 @@ $(TYPEDSIGNATURES) Retrieve a utility mapping between reactions and split reactions; rows correspond to "original" reactions, columns correspond to "split" reactions. """ -_gecko_reaction_column_reactions(model::GeckoModel) = - _gecko_reaction_column_reactions(model.columns, model.inner) +_GeckoReactionColumn_reactions(model::GeckoModel) = + _GeckoReactionColumn_reactions(model.columns, model.inner) """ $(TYPEDSIGNATURES) Helper method that doesn't require the whole [`GeckoModel`](@ref). """ -_gecko_reaction_column_reactions(columns, inner) = sparse( +_GeckoReactionColumn_reactions(columns, inner) = sparse( [col.reaction_idx for col in columns], 1:length(columns), [col.direction >= 0 ? 1 : -1 for col in columns], @@ -37,7 +37,7 @@ $(TYPEDSIGNATURES) Compute the part of the coupling for [`GeckoModel`](@ref) that limits the "arm" reactions (which group the individual split unidirectional reactions). """ -_gecko_reaction_coupling(model::GeckoModel) = +gecko_reaction_coupling(model::GeckoModel) = let tmp = [ (col.reaction_coupling_row, i, col.direction) for (i, col) = enumerate(model.columns) if col.reaction_coupling_row != 0 @@ -57,7 +57,7 @@ $(TYPEDSIGNATURES) Compute the part of the coupling for GeckoModel that limits the amount of each kind of protein available. """ -_gecko_gene_product_coupling(model::GeckoModel) = +gecko_gene_product_coupling(model::GeckoModel) = let tmp = [ (row, i, val) for (i, col) in enumerate(model.columns) for @@ -78,7 +78,7 @@ $(TYPEDSIGNATURES) Compute the part of the coupling for [`GeckoModel`](@ref) that limits the total mass of each group of gene products. """ -function _gecko_mass_group_coupling(model::GeckoModel) +function gecko_mass_group_coupling(model::GeckoModel) tmp = [ # mm = molar mass, mg = mass group, i = row idx, j = col idx (i, j, mm) for (i, mg) in enumerate(model.coupling_row_mass_group) for (j, mm) in zip(mg.gene_product_idxs, mg.gene_product_molar_masses) diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 156752de8..45003366d 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) Parse `SBML.GeneProductAssociation` structure to the simpler GeneAssociation. The input must be (implicitly) in a positive DNF. """ -function _parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation +function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation parse_ref(x) = typeof(x) == SBML.GPARef ? [x.gene_product] : begin @@ -24,7 +24,7 @@ $(TYPEDSIGNATURES) Convert a GeneAssociation to the corresponding `SBML.jl` structure. """ -function _unparse_grr( +function unparse_grr( ::Type{SBML.GeneProductAssociation}, x::GeneAssociation, )::SBML.GeneProductAssociation @@ -40,21 +40,21 @@ and `&&`. # Example ``` -julia> _parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") +julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") 2-element Array{Array{String,1},1}: ["YIL010W", "YLR043C"] ["YIL010W", "YGR209C"] ``` """ -_parse_grr(s::String)::Maybe{GeneAssociation} = _maybemap(_parse_grr, _parse_grr_to_sbml(s)) +parse_grr(s::String)::Maybe{GeneAssociation} = maybemap(parse_grr, parse_grr_to_sbml(s)) """ $(TYPEDSIGNATURES) Internal helper for parsing the string GRRs into SBML data structures. More -general than [`_parse_grr`](@ref). +general than [`parse_grr`](@ref). """ -function _parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} +function parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} s = str toks = String[] m = Nothing @@ -123,11 +123,11 @@ string. # Example ``` -julia> _unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) +julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) "(YIL010W and YLR043C) or (YIL010W and YGR209C)" ``` """ -function _unparse_grr(::Type{String}, grr::GeneAssociation)::String +function unparse_grr(::Type{String}, grr::GeneAssociation)::String grr_strings = String[] for gr in grr push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") diff --git a/src/types/misc/maybe.jl b/src/types/misc/maybe.jl new file mode 100644 index 000000000..ddb35b6eb --- /dev/null +++ b/src/types/misc/maybe.jl @@ -0,0 +1,18 @@ + +""" +$(TYPEDSIGNATURES) + +Fold the `Maybe{T}` down to `T` by defaulting. +""" +function default(d::T, x::Maybe{T})::T where {T} + isnothing(x) ? d : x +end + +""" +$(TYPEDSIGNATURES) + +Apply a function to `x` only if it is not `nothing`. +""" +function maybemap(f, x::Maybe)::Maybe + isnothing(x) ? nothing : f(x) +end diff --git a/src/types/misc/smoment.jl b/src/types/misc/smoment.jl index 04c2fed4a..2627e3e46 100644 --- a/src/types/misc/smoment.jl +++ b/src/types/misc/smoment.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Internal helper for systematically naming reactions in [`SMomentModel`](@ref). """ -_smoment_reaction_name(original_name::String, direction::Int) = +smoment_reaction_name(original_name::String, direction::Int) = direction == 0 ? original_name : direction > 0 ? "$original_name#forward" : "$original_name#reverse" @@ -14,7 +14,7 @@ $(TYPEDSIGNATURES) Retrieve a utility mapping between reactions and split reactions; rows correspond to "original" reactions, columns correspond to "split" reactions. """ -_smoment_column_reactions(model::SMomentModel) = sparse( +smoment_column_reactions(model::SMomentModel) = sparse( [col.reaction_idx for col in model.columns], 1:length(model.columns), [col.direction >= 0 ? 1 : -1 for col in model.columns], diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index df611d22d..07debdc95 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -56,7 +56,7 @@ end function Accessors.stoichiometry(model::HDF5Model)::SparseMat precache!(model) - _h5_read_sparse(SparseMat, model.h5["stoichiometry"]) + h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} @@ -66,10 +66,10 @@ end function Accessors.balance(model::HDF5Model)::SparseVec precache!(model) - _h5_read_sparse(SparseVec, model.h5["balance"]) + h5_read_sparse(SparseVec, model.h5["balance"]) end function Accessors.objective(model::HDF5Model)::SparseVec precache!(model) - _h5_read_sparse(SparseVec, model.h5["objective"]) + h5_read_sparse(SparseVec, model.h5["objective"]) end diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 5004fc11f..696add761 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -165,8 +165,8 @@ $(TYPEDSIGNATURES) Parses the `.gene_reaction_rule` from reactions. """ -Accessors.reaction_gene_association(model::JSONModel, rid::String) = _maybemap( - _parse_grr, +Accessors.reaction_gene_association(model::JSONModel, rid::String) = maybemap( + parse_grr, get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), ) @@ -184,7 +184,7 @@ $(TYPEDSIGNATURES) Parse and return the metabolite `.formula` """ Accessors.metabolite_formula(model::JSONModel, mid::String) = - _maybemap(_parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) + maybemap(parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) """ $(TYPEDSIGNATURES) @@ -207,7 +207,7 @@ $(TYPEDSIGNATURES) Gene annotations from the [`JSONModel`](@ref). """ -Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = _maybemap( +Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = maybemap( _parse_annotations, get(model.genes[model.gene_index[gid]], "annotation", nothing), ) @@ -218,14 +218,14 @@ $(TYPEDSIGNATURES) Gene notes from the [`JSONModel`](@ref). """ Accessors.gene_notes(model::JSONModel, gid::String)::Notes = - _maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) + maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) """ $(TYPEDSIGNATURES) Reaction annotations from the [`JSONModel`](@ref). """ -Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = _maybemap( +Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = maybemap( _parse_annotations, get(model.rxns[model.rxn_index[rid]], "annotation", nothing), ) @@ -236,14 +236,14 @@ $(TYPEDSIGNATURES) Reaction notes from the [`JSONModel`](@ref). """ Accessors.reaction_notes(model::JSONModel, rid::String)::Notes = - _maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) + maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) """ $(TYPEDSIGNATURES) Metabolite annotations from the [`JSONModel`](@ref). """ -Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = _maybemap( +Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = maybemap( _parse_annotations, get(model.mets[model.met_index[mid]], "annotation", nothing), ) @@ -254,7 +254,7 @@ $(TYPEDSIGNATURES) Metabolite notes from the [`JSONModel`](@ref). """ Accessors.metabolite_notes(model::JSONModel, mid::String)::Notes = - _maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) + maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) """ $(TYPEDSIGNATURES) @@ -321,7 +321,7 @@ function Base.convert(::Type{JSONModel}, mm::MetabolicModel) Dict([ "id" => mid, "name" => metabolite_name(mm, mid), - "formula" => _maybemap(_unparse_formula, metabolite_formula(mm, mid)), + "formula" => maybemap(unparse_formula, metabolite_formula(mm, mid)), "charge" => metabolite_charge(mm, mid), "compartment" => metabolite_compartment(mm, mid), "annotation" => metabolite_annotations(mm, mid), @@ -340,7 +340,7 @@ function Base.convert(::Type{JSONModel}, mm::MetabolicModel) grr = reaction_gene_association(mm, rid) if !isnothing(grr) - res["gene_reaction_rule"] = _unparse_grr(String, grr) + res["gene_reaction_rule"] = unparse_grr(String, grr) end res["lower_bound"] = lbs[ri] diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 53352425a..6a914448b 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -134,7 +134,7 @@ Extracts the associations from `grRules` key, if present. function Accessors.reaction_gene_association(m::MATModel, rid::String) if haskey(m.mat, "grRules") grr = m.mat["grRules"][findfirst(==(rid), reactions(m))] - typeof(grr) == String ? _parse_grr(grr) : nothing + typeof(grr) == String ? parse_grr(grr) : nothing else nothing end @@ -145,8 +145,8 @@ $(TYPEDSIGNATURES) Extract metabolite formula from key `metFormula` or `metFormulas`. """ -Accessors.metabolite_formula(m::MATModel, mid::String) = _maybemap( - x -> _parse_formula(x[findfirst(==(mid), metabolites(m))]), +Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( + x -> parse_formula(x[findfirst(==(mid), metabolites(m))]), gets(m.mat, nothing, constants.keynames.metformulas), ) @@ -156,7 +156,7 @@ $(TYPEDSIGNATURES) Extract metabolite charge from `metCharge` or `metCharges`. """ function Accessors.metabolite_charge(m::MATModel, mid::String) - met_charge = _maybemap( + met_charge = maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metcharges), ) @@ -168,7 +168,7 @@ $(TYPEDSIGNATURES) Extract metabolite compartment from `metCompartment` or `metCompartments`. """ -Accessors.metabolite_compartment(m::MATModel, mid::String) = _maybemap( +Accessors.metabolite_compartment(m::MATModel, mid::String) = maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metcompartments), ) @@ -198,7 +198,7 @@ $(TYPEDSIGNATURES) Extract reaction name from `rxnNames`. """ -Accessors.reaction_name(m::MATModel, rid::String) = _maybemap( +Accessors.reaction_name(m::MATModel, rid::String) = maybemap( x -> x[findfirst(==(rid), reactions(m))], gets(m.mat, nothing, constants.keynames.rxnnames), ) @@ -208,7 +208,7 @@ $(TYPEDSIGNATURES) Extract metabolite name from `metNames`. """ -Accessors.metabolite_name(m::MATModel, mid::String) = _maybemap( +Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metnames), ) @@ -248,24 +248,21 @@ function Base.convert(::Type{MATModel}, m::MetabolicModel) "cu" => Vector(cu), "genes" => genes(m), "grRules" => - _default.( + default.( "", - _maybemap.( - x -> _unparse_grr(String, x), + maybemap.( + x -> unparse_grr(String, x), reaction_gene_association.(Ref(m), reactions(m)), ), ), "metFormulas" => - _default.( + default.( "", - _maybemap.( - _unparse_formula, - metabolite_formula.(Ref(m), metabolites(m)), - ), + maybemap.(unparse_formula, metabolite_formula.(Ref(m), metabolites(m))), ), - "metCharges" => _default.(0, metabolite_charge.(Ref(m), metabolites(m))), + "metCharges" => default.(0, metabolite_charge.(Ref(m), metabolites(m))), "metCompartments" => - _default.("", metabolite_compartment.(Ref(m), metabolites(m))), + default.("", metabolite_compartment.(Ref(m), metabolites(m))), ), ) end diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index cb6970376..44c09700a 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -110,7 +110,7 @@ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`SBMLModel`](@ref). """ Accessors.reaction_gene_association(model::SBMLModel, rid::String)::Maybe{GeneAssociation} = - _maybemap(_parse_grr, model.sbml.reactions[rid].gene_product_association) + maybemap(parse_grr, model.sbml.reactions[rid].gene_product_association) """ $(TYPEDSIGNATURES) @@ -118,7 +118,7 @@ $(TYPEDSIGNATURES) Get [`MetaboliteFormula`](@ref) from a chosen metabolite from [`SBMLModel`](@ref). """ Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = - _maybemap(_parse_formula, model.sbml.species[mid].formula) + maybemap(parse_formula, model.sbml.species[mid].formula) """ $(TYPEDSIGNATURES) @@ -194,7 +194,7 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) rxns = reactions(mm) stoi = stoichiometry(mm) (lbs, ubs) = bounds(mm) - comps = _default.("compartment", metabolite_compartment.(Ref(mm), mets)) + comps = default.("compartment", metabolite_compartment.(Ref(mm), mets)) compss = Set(comps) return SBMLModel( @@ -205,7 +205,7 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) species = Dict( mid => SBML.Species( name = metabolite_name(mm, mid), - compartment = _default("compartment", comps[mi]), + compartment = default("compartment", comps[mi]), formula = metabolite_formula(mm, mid), charge = metabolite_charge(mm, mid), constant = false, @@ -240,8 +240,8 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) ), lower_bound = "LOWER_BOUND", upper_bound = "UPPER_BOUND", - gene_product_association = _maybemap( - x -> _unparse_grr(SBML.GeneProductAssociation, x), + gene_product_association = maybemap( + x -> unparse_grr(SBML.GeneProductAssociation, x), reaction_gene_association(mm, rid), ), reversible = true, diff --git a/src/types/models/StandardModel.jl b/src/types/models/StandardModel.jl index d2a9c6e3c..6376e5ee1 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -190,7 +190,7 @@ Return `nothing` if not available. Accessors.reaction_gene_association( model::StandardModel, id::String, -)::Maybe{GeneAssociation} = _maybemap(identity, model.reactions[id].grr) +)::Maybe{GeneAssociation} = maybemap(identity, model.reactions[id].grr) """ $(TYPEDSIGNATURES) @@ -199,7 +199,7 @@ Return the formula of reaction `id` in `model`. Return `nothing` if not present. """ Accessors.metabolite_formula(model::StandardModel, id::String)::Maybe{MetaboliteFormula} = - _maybemap(_parse_formula, model.metabolites[id].formula) + maybemap(parse_formula, model.metabolites[id].formula) """ $(TYPEDSIGNATURES) @@ -345,7 +345,7 @@ function Base.convert(::Type{StandardModel}, model::MetabolicModel) mid; name = metabolite_name(model, mid), charge = metabolite_charge(model, mid), - formula = _maybemap(_unparse_formula, metabolite_formula(model, mid)), + formula = maybemap(unparse_formula, metabolite_formula(model, mid)), compartment = metabolite_compartment(model, mid), notes = metabolite_notes(model, mid), annotations = metabolite_annotations(model, mid), diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index bf9902935..1407eac61 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -6,7 +6,7 @@ A helper type for describing the contents of [`GeckoModel`](@ref)s. # Fields $(TYPEDFIELDS) """ -struct _gecko_reaction_column +struct _GeckoReactionColumn reaction_idx::Int isozyme_idx::Int direction::Int @@ -25,7 +25,7 @@ the grouping type, e.g. metabolic or membrane groups etc. # Fields $(TYPEDFIELDS) """ -struct _gecko_capacity +struct _GeckoCapacity group_id::String gene_product_idxs::Vector{Int} gene_product_molar_masses::Vector{Float64} @@ -76,10 +76,10 @@ $(TYPEDFIELDS) """ struct GeckoModel <: ModelWrapper objective::SparseVec - columns::Vector{_gecko_reaction_column} + columns::Vector{_GeckoReactionColumn} coupling_row_reaction::Vector{Int} coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} - coupling_row_mass_group::Vector{_gecko_capacity} + coupling_row_mass_group::Vector{_GeckoCapacity} inner::MetabolicModel end @@ -94,8 +94,8 @@ split into unidirectional forward and reverse ones, each of which may have multiple variants per isozyme. """ function Accessors.stoichiometry(model::GeckoModel) - irrevS = stoichiometry(model.inner) * COBREXA._gecko_reaction_column_reactions(model) - enzS = COBREXA._gecko_gene_product_coupling(model) + irrevS = stoichiometry(model.inner) * _GeckoReactionColumn_reactions(model) + enzS = gecko_gene_product_coupling(model) [ irrevS spzeros(size(irrevS, 1), size(enzS, 1)) -enzS I(size(enzS, 1)) @@ -122,7 +122,7 @@ IDs are mangled accordingly with suffixes). function Accessors.reactions(model::GeckoModel) inner_reactions = reactions(model.inner) mangled_reactions = [ - _gecko_reaction_name( + gecko_reaction_name( inner_reactions[col.reaction_idx], col.direction, col.isozyme_idx, @@ -163,7 +163,7 @@ Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original fluxes in the wrapped model. """ function Accessors.reaction_flux(model::GeckoModel) - rxnmat = _gecko_reaction_column_reactions(model)' * reaction_flux(model.inner) + rxnmat = _GeckoReactionColumn_reactions(model)' * reaction_flux(model.inner) [ rxnmat spzeros(n_genes(model), size(rxnmat, 2)) @@ -178,9 +178,9 @@ wrapped model, coupling for split (arm) reactions, and the coupling for the tota enzyme capacity. """ function Accessors.coupling(model::GeckoModel) - innerC = coupling(model.inner) * _gecko_reaction_column_reactions(model) - rxnC = _gecko_reaction_coupling(model) - enzcap = _gecko_mass_group_coupling(model) + innerC = coupling(model.inner) * _GeckoReactionColumn_reactions(model) + rxnC = gecko_reaction_coupling(model) + enzcap = gecko_mass_group_coupling(model) [ innerC spzeros(size(innerC, 1), n_genes(model)) rxnC spzeros(size(rxnC, 1), n_genes(model)) diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 8e3629efc..305e6dcd9 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -7,7 +7,7 @@ A helper type that describes the contents of [`SMomentModel`](@ref)s. # Fields $(TYPEDFIELDS) """ -struct _smoment_column +struct _SMomentColumn reaction_idx::Int # number of the corresponding reaction in the inner model direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) @@ -55,7 +55,7 @@ are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). $(TYPEDFIELDS) """ struct SMomentModel <: ModelWrapper - columns::Vector{_smoment_column} + columns::Vector{_SMomentColumn} total_enzyme_capacity::Float64 inner::MetabolicModel @@ -70,7 +70,7 @@ Return a stoichiometry of the [`SMomentModel`](@ref). The enzymatic reactions are split into unidirectional forward and reverse ones. """ Accessors.stoichiometry(model::SMomentModel) = - stoichiometry(model.inner) * _smoment_column_reactions(model) + stoichiometry(model.inner) * smoment_column_reactions(model) """ $(TYPEDSIGNATURES) @@ -78,7 +78,7 @@ $(TYPEDSIGNATURES) Reconstruct an objective of the [`SMomentModel`](@ref). """ Accessors.objective(model::SMomentModel) = - _smoment_column_reactions(model)' * objective(model.inner) + smoment_column_reactions(model)' * objective(model.inner) """ $(TYPEDSIGNATURES) @@ -90,7 +90,7 @@ suffixes). Accessors.reactions(model::SMomentModel) = let inner_reactions = reactions(model.inner) [ - _smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for + smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for col in model.columns ] end @@ -117,7 +117,7 @@ Get the mapping of the reaction rates in [`SMomentModel`](@ref) to the original fluxes in the wrapped model. """ Accessors.reaction_flux(model::SMomentModel) = - _smoment_column_reactions(model)' * reaction_flux(model.inner) + smoment_column_reactions(model)' * reaction_flux(model.inner) """ $(TYPEDSIGNATURES) @@ -127,7 +127,7 @@ the wrapped model, coupling for split reactions, and the coupling for the total enzyme capacity. """ Accessors.coupling(model::SMomentModel) = vcat( - coupling(model.inner) * _smoment_column_reactions(model), + coupling(model.inner) * smoment_column_reactions(model), [col.capacity_required for col in model.columns]', ) diff --git a/src/utils/HDF5Model.jl b/src/utils/HDF5Model.jl deleted file mode 100644 index 97934ef22..000000000 --- a/src/utils/HDF5Model.jl +++ /dev/null @@ -1,32 +0,0 @@ - -_h5_mmap_nonempty(x) = length(x) > 0 ? HDF5.readmmap(x) : HDF5.read(x) - -function _h5_write_sparse(g::HDF5.Group, v::SparseVector) - write(g, "n", v.n) - write(g, "nzind", v.nzind) - write(g, "nzval", v.nzval) -end - -function _h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseVector} - n = read(g["n"]) - nzind = _h5_mmap_nonempty(g["nzind"]) - nzval = _h5_mmap_nonempty(g["nzval"]) - SparseVector{eltype(nzval),eltype(nzind)}(n, nzind, nzval) -end - -function _h5_write_sparse(g::HDF5.Group, m::SparseMatrixCSC) - write(g, "m", m.m) - write(g, "n", m.n) - write(g, "colptr", m.colptr) - write(g, "rowval", m.rowval) - write(g, "nzval", m.nzval) -end - -function _h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseMatrixCSC} - m = read(g["m"]) - n = read(g["n"]) - colptr = _h5_mmap_nonempty(g["colptr"]) - rowval = _h5_mmap_nonempty(g["rowval"]) - nzval = _h5_mmap_nonempty(g["nzval"]) - SparseMatrixCSC{eltype(nzval),eltype(colptr)}(m, n, colptr, rowval, nzval) -end diff --git a/test/runtests.jl b/test/runtests.jl index 6a783ab14..a4b6dc391 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -72,7 +72,7 @@ run_test_file("data_downloaded.jl") @testset "COBREXA test suite" begin run_test_dir(joinpath("base", "types", "abstract"), "Abstract types") run_test_dir(joinpath("base", "types"), "Base model types") - run_test_dir(joinpath("base", "logging"), "Logging") + run_test_file("base", "log.jl") run_test_dir("base", "Base functionality") run_test_dir(joinpath("base", "utils"), "Utilities") run_test_dir("io", "I/O functions") From 17a4238f62ce63c9453d0531c6c635653d43f5f6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 28 Sep 2022 11:12:07 +0200 Subject: [PATCH 020/531] more namespacing fixes (some analyses work now) --- src/analysis/gecko.jl | 12 ++-- src/analysis/sampling/warmup_variability.jl | 6 +- src/analysis/screening.jl | 10 +-- src/analysis/smoment.jl | 20 ++++-- src/types.jl | 1 + src/types/FluxVariabilitySummary.jl | 76 --------------------- src/utils.jl | 1 + src/utils/flux_summary.jl | 76 +++++++++++++++++++++ 8 files changed, 105 insertions(+), 97 deletions(-) diff --git a/src/analysis/gecko.jl b/src/analysis/gecko.jl index 11f9d831c..befdea6d1 100644 --- a/src/analysis/gecko.jl +++ b/src/analysis/gecko.jl @@ -48,7 +48,7 @@ function make_gecko_model( (grp -> gene_product_mass_group_bound[grp]) # ...it would be nicer to have an overload for this, but kwargs can't be used for dispatch - columns = Vector{_GeckoReactionColumn}() + columns = Vector{Types._GeckoReactionColumn}() coupling_row_reaction = Int[] coupling_row_gene_product = Int[] @@ -62,7 +62,7 @@ function make_gecko_model( for i = 1:n_reactions(model) isozymes = ris_(rids[i]) if isempty(isozymes) - push!(columns, _GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) + push!(columns, Types._GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) continue end @@ -108,7 +108,7 @@ function make_gecko_model( # make a new column push!( columns, - _GeckoReactionColumn( + Types._GeckoReactionColumn( i, iidx, dir, @@ -133,16 +133,16 @@ function make_gecko_model( mg_gid_lookup[mg] = [gid] end end - coupling_row_mass_group = Vector{_GeckoCapacity}() + coupling_row_mass_group = Vector{Types._GeckoCapacity}() for (grp, gs) in mg_gid_lookup idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] mms = gpmm_.(gs) - push!(coupling_row_mass_group, _GeckoCapacity(grp, idxs, mms, gmgb_(grp))) + push!(coupling_row_mass_group, Types._GeckoCapacity(grp, idxs, mms, gmgb_(grp))) end GeckoModel( [ - _GeckoReactionColumn_reactions(columns, model)' * objective(model) + Types._GeckoReactionColumn_reactions(columns, model)' * objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl index 8764bbeb2..03b4e0fb7 100644 --- a/src/analysis/sampling/warmup_variability.jl +++ b/src/analysis/sampling/warmup_variability.jl @@ -58,7 +58,7 @@ function warmup_from_variability( save_model = :( begin local model = $model - local optmodel = $COBREXA.make_optimization_model(model, $optimizer) + local optmodel = $make_optimization_model(model, $optimizer) for mod in $modifications mod(model, optmodel) end @@ -70,10 +70,10 @@ function warmup_from_variability( fluxes = hcat( dpmap( - rid -> :($COBREXA._maximize_warmup_reaction( + rid -> :($_maximize_warmup_reaction( cobrexa_sampling_warmup_optmodel, $rid, - om -> $COBREXA.JuMP.value.(om[:x]), + om -> $JuMP.value.(om[:x]), )), CachingPool(workers), vcat(-min_reactions, max_reactions), diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl index a4dd13af4..2887bb9f2 100644 --- a/src/analysis/screening.jl +++ b/src/analysis/screening.jl @@ -119,7 +119,7 @@ function _screen_impl( asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_model, Ref(model))) asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_analysis_fn, Ref(analysis))) - asyncmap(fetch, get_from.(workers, Ref(:(precache!(cobrexa_screen_variants_model))))) + asyncmap(fetch, get_from.(workers, Ref(:($(precache!)(cobrexa_screen_variants_model))))) res = pmap( (vars, args)::Tuple -> screen_variant( @@ -253,13 +253,7 @@ function _screen_optmodel_modifications_impl( save_at.( workers, :cobrexa_screen_optmodel_modifications_data, - Ref( - :($COBREXA._screen_optmodel_prepare( - $model, - $optimizer, - $common_modifications, - )), - ), + Ref(:($_screen_optmodel_prepare($model, $optimizer, $common_modifications))), ), ) asyncmap( diff --git a/src/analysis/smoment.jl b/src/analysis/smoment.jl index 0ce6f3ae1..32c06c56d 100644 --- a/src/analysis/smoment.jl +++ b/src/analysis/smoment.jl @@ -32,7 +32,7 @@ function make_smoment_model( gene_product_molar_mass isa Function ? gene_product_molar_mass : (gid -> gene_product_molar_mass[gid]) - columns = Vector{_SMomentColumn}() + columns = Vector{Types._SMomentColumn}() (lbs, ubs) = bounds(model) rids = reactions(model) @@ -41,7 +41,7 @@ function make_smoment_model( isozyme = ris_(rids[i]) if isnothing(isozyme) # non-enzymatic reaction (or a totally ignored one) - push!(columns, _SMomentColumn(i, 0, lbs[i], ubs[i], 0)) + push!(columns, Types._SMomentColumn(i, 0, lbs[i], ubs[i], 0)) continue end @@ -51,7 +51,13 @@ function make_smoment_model( # reaction can run in reverse push!( columns, - _SMomentColumn(i, -1, max(-ubs[i], 0), -lbs[i], mw / isozyme.kcat_reverse), + Types._SMomentColumn( + i, + -1, + max(-ubs[i], 0), + -lbs[i], + mw / isozyme.kcat_reverse, + ), ) end @@ -59,7 +65,13 @@ function make_smoment_model( # reaction can run forward push!( columns, - _SMomentColumn(i, 1, max(lbs[i], 0), ubs[i], mw / isozyme.kcat_forward), + Types._SMomentColumn( + i, + 1, + max(lbs[i], 0), + ubs[i], + mw / isozyme.kcat_forward, + ), ) end end diff --git a/src/types.jl b/src/types.jl index eec52a56f..7cf12d878 100644 --- a/src/types.jl +++ b/src/types.jl @@ -46,6 +46,7 @@ end using ..Log.Internal: @models_log using SBML + using SparseArrays @inc_dir types misc @export_locals diff --git a/src/types/FluxVariabilitySummary.jl b/src/types/FluxVariabilitySummary.jl index b977b65f9..a86c7b983 100644 --- a/src/types/FluxVariabilitySummary.jl +++ b/src/types/FluxVariabilitySummary.jl @@ -22,79 +22,3 @@ function FluxVariabilitySummary() Dict{String,Vector{Maybe{Float64}}}(), ) end - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of flux dictionaries obtained eg. from -[`flux_variability_analysis_dict`](@ref). The simplified summary representation -is useful for pretty-printing and easily showing the most important results. - -Internally this function uses [`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments are -passed to these functions. Use this if your model has an uncommon naming of -reactions. - -# Example -``` -julia> sol = flux_variability_analysis_dict(model, Gurobi.Optimizer; bounds = objective_bounds(0.99)) -julia> flux_res = flux_variability_summary(sol) -Biomass Lower bound Upper bound - BIOMASS_Ecoli_core_w_GAM: 0.8652 0.8652 -Exchange - EX_h2o_e: 28.34 28.34 - EX_co2_e: 22.0377 22.0377 - EX_o2_e: -22.1815 -22.1815 - EX_h_e: 17.3556 17.3556 - EX_glc__D_e: -10.0 -10.0 - EX_nh4_e: -4.8448 -4.8448 - EX_pi_e: -3.2149 -3.2149 - EX_for_e: 0.0 0.0 - ... ... ... -``` -""" -function flux_variability_summary( - flux_result::Tuple{Dict{String,Dict{String,Float64}},Dict{String,Dict{String,Float64}}}; - exclude_exchanges = false, - exchange_prefixes = constants.exchange_prefixes, - biomass_strings = constants.biomass_strings, - exclude_biomass = false, -) - isnothing(flux_result) && return FluxVariabilitySummary() - - rxn_ids = keys(flux_result[1]) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - biomass_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in bmasses - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - biomass_fluxes[rxn_id] = [lb, ub] - end - - ex_rxn_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in ex_rxns - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - ex_rxn_fluxes[rxn_id] = [lb, ub] - end - - return FluxVariabilitySummary(biomass_fluxes, ex_rxn_fluxes) -end diff --git a/src/utils.jl b/src/utils.jl index df2b89eda..b3484fa2d 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -21,4 +21,5 @@ using Serialization @export_locals end +@inject Analysis using ...Utils: objective_bounds @inject Analysis.Modifications using ...Utils: is_boundary diff --git a/src/utils/flux_summary.jl b/src/utils/flux_summary.jl index a726edc33..c52f1fa78 100644 --- a/src/utils/flux_summary.jl +++ b/src/utils/flux_summary.jl @@ -106,3 +106,79 @@ function flux_summary( ) end end + +""" +$(TYPEDSIGNATURES) + +Summarize a dictionary of flux dictionaries obtained eg. from +[`flux_variability_analysis_dict`](@ref). The simplified summary representation +is useful for pretty-printing and easily showing the most important results. + +Internally this function uses [`looks_like_biomass_reaction`](@ref) and +[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments are +passed to these functions. Use this if your model has an uncommon naming of +reactions. + +# Example +``` +julia> sol = flux_variability_analysis_dict(model, Gurobi.Optimizer; bounds = objective_bounds(0.99)) +julia> flux_res = flux_variability_summary(sol) +Biomass Lower bound Upper bound + BIOMASS_Ecoli_core_w_GAM: 0.8652 0.8652 +Exchange + EX_h2o_e: 28.34 28.34 + EX_co2_e: 22.0377 22.0377 + EX_o2_e: -22.1815 -22.1815 + EX_h_e: 17.3556 17.3556 + EX_glc__D_e: -10.0 -10.0 + EX_nh4_e: -4.8448 -4.8448 + EX_pi_e: -3.2149 -3.2149 + EX_for_e: 0.0 0.0 + ... ... ... +``` +""" +function flux_variability_summary( + flux_result::Tuple{Dict{String,Dict{String,Float64}},Dict{String,Dict{String,Float64}}}; + exclude_exchanges = false, + exchange_prefixes = constants.exchange_prefixes, + biomass_strings = constants.biomass_strings, + exclude_biomass = false, +) + isnothing(flux_result) && return FluxVariabilitySummary() + + rxn_ids = keys(flux_result[1]) + ex_rxns = filter( + x -> looks_like_exchange_reaction( + x, + exclude_biomass = exclude_biomass, + biomass_strings = biomass_strings, + exchange_prefixes = exchange_prefixes, + ), + rxn_ids, + ) + bmasses = filter( + x -> looks_like_biomass_reaction( + x; + exclude_exchanges = exclude_exchanges, + exchange_prefixes = exchange_prefixes, + biomass_strings = biomass_strings, + ), + rxn_ids, + ) + + biomass_fluxes = Dict{String,Vector{Maybe{Float64}}}() + for rxn_id in bmasses + lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] + ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] + biomass_fluxes[rxn_id] = [lb, ub] + end + + ex_rxn_fluxes = Dict{String,Vector{Maybe{Float64}}}() + for rxn_id in ex_rxns + lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] + ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] + ex_rxn_fluxes[rxn_id] = [lb, ub] + end + + return FluxVariabilitySummary(biomass_fluxes, ex_rxn_fluxes) +end From e87550477e2ba539fbe572f8690f894eb19fa23f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 28 Sep 2022 12:51:55 +0200 Subject: [PATCH 021/531] rest of the fixes --- src/analysis/gecko.jl | 2 +- src/types.jl | 13 +++++++++++-- src/types/misc/gecko.jl | 6 +++--- src/types/wrappers/GeckoModel.jl | 6 +++--- src/utils.jl | 6 +++++- test/analysis/gecko.jl | 2 +- test/analysis/screening.jl | 4 ++-- test/runtests.jl | 2 +- 8 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/analysis/gecko.jl b/src/analysis/gecko.jl index befdea6d1..f663f8333 100644 --- a/src/analysis/gecko.jl +++ b/src/analysis/gecko.jl @@ -142,7 +142,7 @@ function make_gecko_model( GeckoModel( [ - Types._GeckoReactionColumn_reactions(columns, model)' * objective(model) + Types.gecko_column_reactions(columns, model)' * objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/types.jl b/src/types.jl index 7cf12d878..f2b26e275 100644 --- a/src/types.jl +++ b/src/types.jl @@ -4,8 +4,16 @@ module Types using ..ModuleTools @dse using ..Internal -using SparseArrays, OrderedCollections -using HDF5, SBML, JSON, MAT, Serialization #for the storable types + +using HDF5 +using JSON +using LinearAlgebra +using MAT +using OrderedCollections +using SBML +using Serialization +using SparseArrays + @inc_dir types abstract @export_locals end @@ -16,6 +24,7 @@ using ..ModuleTools @dse using ..Types using ..Internal.Macros + using SparseArrays module Internal diff --git a/src/types/misc/gecko.jl b/src/types/misc/gecko.jl index 865bb03c5..58f5daaf1 100644 --- a/src/types/misc/gecko.jl +++ b/src/types/misc/gecko.jl @@ -15,15 +15,15 @@ $(TYPEDSIGNATURES) Retrieve a utility mapping between reactions and split reactions; rows correspond to "original" reactions, columns correspond to "split" reactions. """ -_GeckoReactionColumn_reactions(model::GeckoModel) = - _GeckoReactionColumn_reactions(model.columns, model.inner) +gecko_column_reactions(model::GeckoModel) = + gecko_column_reactions(model.columns, model.inner) """ $(TYPEDSIGNATURES) Helper method that doesn't require the whole [`GeckoModel`](@ref). """ -_GeckoReactionColumn_reactions(columns, inner) = sparse( +gecko_column_reactions(columns, inner) = sparse( [col.reaction_idx for col in columns], 1:length(columns), [col.direction >= 0 ? 1 : -1 for col in columns], diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index 1407eac61..8507680e1 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -94,7 +94,7 @@ split into unidirectional forward and reverse ones, each of which may have multiple variants per isozyme. """ function Accessors.stoichiometry(model::GeckoModel) - irrevS = stoichiometry(model.inner) * _GeckoReactionColumn_reactions(model) + irrevS = stoichiometry(model.inner) * gecko_column_reactions(model) enzS = gecko_gene_product_coupling(model) [ irrevS spzeros(size(irrevS, 1), size(enzS, 1)) @@ -163,7 +163,7 @@ Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original fluxes in the wrapped model. """ function Accessors.reaction_flux(model::GeckoModel) - rxnmat = _GeckoReactionColumn_reactions(model)' * reaction_flux(model.inner) + rxnmat = gecko_column_reactions(model)' * reaction_flux(model.inner) [ rxnmat spzeros(n_genes(model), size(rxnmat, 2)) @@ -178,7 +178,7 @@ wrapped model, coupling for split (arm) reactions, and the coupling for the tota enzyme capacity. """ function Accessors.coupling(model::GeckoModel) - innerC = coupling(model.inner) * _GeckoReactionColumn_reactions(model) + innerC = coupling(model.inner) * gecko_column_reactions(model) rxnC = gecko_reaction_coupling(model) enzcap = gecko_mass_group_coupling(model) [ diff --git a/src/utils.jl b/src/utils.jl index b3484fa2d..2688b6d16 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -11,10 +11,14 @@ using ..Accessors using ..Internal.Identifiers using ..Internal: constants using ..Internal.Macros +using ..Solver -using SparseArrays, OrderedCollections using HDF5 +using JuMP +using LinearAlgebra +using OrderedCollections using Serialization +using SparseArrays @inc_dir utils diff --git a/test/analysis/gecko.jl b/test/analysis/gecko.jl index 7711f3e83..686e64f4f 100644 --- a/test/analysis/gecko.jl +++ b/test/analysis/gecko.jl @@ -57,7 +57,7 @@ gm, Tulip.Optimizer; modifications = [ - change_objective(genes(gm); weights = [], sense = COBREXA.MIN_SENSE), + change_objective(genes(gm); weights = [], sense = MIN_SENSE), change_constraint("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb), change_optimizer_attribute("IPM_IterationsLimit", 1000), ], diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl index d710f4fe8..692d6bd86 100644 --- a/test/analysis/screening.jl +++ b/test/analysis/screening.jl @@ -37,7 +37,7 @@ @test screen_variants( m, [[quad_rxn(i)] for i = 1:3], - m -> flux_balance_analysis_vec(m, Tulip.Optimizer); + m -> Analysis.flux_balance_analysis_vec(m, Tulip.Optimizer); workers = W, ) == [ [250.0, -250.0, -1000.0, 250.0, 1000.0, 250.0, 250.0], @@ -48,7 +48,7 @@ # test solver modifications @test screen( m; - analysis = (m, sense) -> flux_balance_analysis_vec( + analysis = (m, sense) -> Analysis.flux_balance_analysis_vec( m, Tulip.Optimizer; modifications = [change_sense(sense)], diff --git a/test/runtests.jl b/test/runtests.jl index a4b6dc391..db6a824a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -49,7 +49,7 @@ end # set up the workers for Distributed, so that the tests that require more # workers do not unnecessarily load the stuff multiple times W = addprocs(2) -t = @elapsed @everywhere using COBREXA, Tulip, JuMP +t = @elapsed @everywhere using COBREXA, COBREXA.Analysis, Tulip, JuMP print_timing("import of packages", t) t = @elapsed @everywhere begin model = Model(Tulip.Optimizer) From 3bbfdc216ffb6ed5567945b895ba32e504c4730c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 28 Sep 2022 12:54:14 +0200 Subject: [PATCH 022/531] squash `/base/` in tests types.jl wow. --- test/base/types.jl | 4 ---- test/{base => }/log.jl | 0 test/runtests.jl | 9 ++++----- test/{base => }/types/CoreModel.jl | 0 test/{base => }/types/CoreModelCoupled.jl | 0 test/{base => }/types/FluxSummary.jl | 0 test/{base => }/types/FluxVariabilitySummary.jl | 0 test/{base => }/types/Gene.jl | 0 test/{base => }/types/JSONModel.jl | 0 test/{base => }/types/MATModel.jl | 0 test/{base => }/types/Metabolite.jl | 0 test/{base => }/types/Reaction.jl | 0 test/{base => }/types/SBMLModel.jl | 0 test/{base => }/types/StandardModel.jl | 0 test/{base => }/types/abstract/MetabolicModel.jl | 0 test/{base => }/utils/CoreModel.jl | 0 test/{base => }/utils/Serialized.jl | 0 test/{base => }/utils/StandardModel.jl | 0 test/{base => }/utils/fluxes.jl | 0 test/{base => }/utils/looks_like.jl | 0 test/{base => }/utils/reaction.jl | 0 21 files changed, 4 insertions(+), 9 deletions(-) delete mode 100644 test/base/types.jl rename test/{base => }/log.jl (100%) rename test/{base => }/types/CoreModel.jl (100%) rename test/{base => }/types/CoreModelCoupled.jl (100%) rename test/{base => }/types/FluxSummary.jl (100%) rename test/{base => }/types/FluxVariabilitySummary.jl (100%) rename test/{base => }/types/Gene.jl (100%) rename test/{base => }/types/JSONModel.jl (100%) rename test/{base => }/types/MATModel.jl (100%) rename test/{base => }/types/Metabolite.jl (100%) rename test/{base => }/types/Reaction.jl (100%) rename test/{base => }/types/SBMLModel.jl (100%) rename test/{base => }/types/StandardModel.jl (100%) rename test/{base => }/types/abstract/MetabolicModel.jl (100%) rename test/{base => }/utils/CoreModel.jl (100%) rename test/{base => }/utils/Serialized.jl (100%) rename test/{base => }/utils/StandardModel.jl (100%) rename test/{base => }/utils/fluxes.jl (100%) rename test/{base => }/utils/looks_like.jl (100%) rename test/{base => }/utils/reaction.jl (100%) diff --git a/test/base/types.jl b/test/base/types.jl deleted file mode 100644 index 09f3b16bc..000000000 --- a/test/base/types.jl +++ /dev/null @@ -1,4 +0,0 @@ -@testset "CoreModel type" begin - cp = test_LP() - @test cp isa CoreModel -end diff --git a/test/base/log.jl b/test/log.jl similarity index 100% rename from test/base/log.jl rename to test/log.jl diff --git a/test/runtests.jl b/test/runtests.jl index db6a824a6..467d3325a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -70,11 +70,10 @@ run_test_file("data_downloaded.jl") # import base files @testset "COBREXA test suite" begin - run_test_dir(joinpath("base", "types", "abstract"), "Abstract types") - run_test_dir(joinpath("base", "types"), "Base model types") - run_test_file("base", "log.jl") - run_test_dir("base", "Base functionality") - run_test_dir(joinpath("base", "utils"), "Utilities") + run_test_dir(joinpath("types", "abstract"), "Abstract types") + run_test_dir("types", "Model types and wrappers") + run_test_file("log.jl") + run_test_dir("utils", "Utilities") run_test_dir("io", "I/O functions") run_test_dir("reconstruction") run_test_dir("analysis") diff --git a/test/base/types/CoreModel.jl b/test/types/CoreModel.jl similarity index 100% rename from test/base/types/CoreModel.jl rename to test/types/CoreModel.jl diff --git a/test/base/types/CoreModelCoupled.jl b/test/types/CoreModelCoupled.jl similarity index 100% rename from test/base/types/CoreModelCoupled.jl rename to test/types/CoreModelCoupled.jl diff --git a/test/base/types/FluxSummary.jl b/test/types/FluxSummary.jl similarity index 100% rename from test/base/types/FluxSummary.jl rename to test/types/FluxSummary.jl diff --git a/test/base/types/FluxVariabilitySummary.jl b/test/types/FluxVariabilitySummary.jl similarity index 100% rename from test/base/types/FluxVariabilitySummary.jl rename to test/types/FluxVariabilitySummary.jl diff --git a/test/base/types/Gene.jl b/test/types/Gene.jl similarity index 100% rename from test/base/types/Gene.jl rename to test/types/Gene.jl diff --git a/test/base/types/JSONModel.jl b/test/types/JSONModel.jl similarity index 100% rename from test/base/types/JSONModel.jl rename to test/types/JSONModel.jl diff --git a/test/base/types/MATModel.jl b/test/types/MATModel.jl similarity index 100% rename from test/base/types/MATModel.jl rename to test/types/MATModel.jl diff --git a/test/base/types/Metabolite.jl b/test/types/Metabolite.jl similarity index 100% rename from test/base/types/Metabolite.jl rename to test/types/Metabolite.jl diff --git a/test/base/types/Reaction.jl b/test/types/Reaction.jl similarity index 100% rename from test/base/types/Reaction.jl rename to test/types/Reaction.jl diff --git a/test/base/types/SBMLModel.jl b/test/types/SBMLModel.jl similarity index 100% rename from test/base/types/SBMLModel.jl rename to test/types/SBMLModel.jl diff --git a/test/base/types/StandardModel.jl b/test/types/StandardModel.jl similarity index 100% rename from test/base/types/StandardModel.jl rename to test/types/StandardModel.jl diff --git a/test/base/types/abstract/MetabolicModel.jl b/test/types/abstract/MetabolicModel.jl similarity index 100% rename from test/base/types/abstract/MetabolicModel.jl rename to test/types/abstract/MetabolicModel.jl diff --git a/test/base/utils/CoreModel.jl b/test/utils/CoreModel.jl similarity index 100% rename from test/base/utils/CoreModel.jl rename to test/utils/CoreModel.jl diff --git a/test/base/utils/Serialized.jl b/test/utils/Serialized.jl similarity index 100% rename from test/base/utils/Serialized.jl rename to test/utils/Serialized.jl diff --git a/test/base/utils/StandardModel.jl b/test/utils/StandardModel.jl similarity index 100% rename from test/base/utils/StandardModel.jl rename to test/utils/StandardModel.jl diff --git a/test/base/utils/fluxes.jl b/test/utils/fluxes.jl similarity index 100% rename from test/base/utils/fluxes.jl rename to test/utils/fluxes.jl diff --git a/test/base/utils/looks_like.jl b/test/utils/looks_like.jl similarity index 100% rename from test/base/utils/looks_like.jl rename to test/utils/looks_like.jl diff --git a/test/base/utils/reaction.jl b/test/utils/reaction.jl similarity index 100% rename from test/base/utils/reaction.jl rename to test/utils/reaction.jl From 94980af4409674d01b51cf75b6135514c3d56b1d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 29 Sep 2022 11:01:54 +0200 Subject: [PATCH 023/531] internalize missing_impl_error --- src/types.jl | 7 +++++++ src/types/accessors/MetabolicModel.jl | 14 ++++++-------- src/types/accessors/ModelWrapper.jl | 2 +- src/types/accessors/misc/missing_impl.jl | 2 ++ 4 files changed, 16 insertions(+), 9 deletions(-) create mode 100644 src/types/accessors/misc/missing_impl.jl diff --git a/src/types.jl b/src/types.jl index f2b26e275..982fceeec 100644 --- a/src/types.jl +++ b/src/types.jl @@ -30,8 +30,15 @@ using SparseArrays module Internal using ..ModuleTools @dse +# TODO: Note to self: we might be a bit more systematic here -- these are +# "pre-includes" (might go into bits/), contrasting to "post-includes" (which +# may stay in misc/) +@inc_dir types accessors misc +@export_locals end +using .Internal + @inc_dir types accessors @export_locals end diff --git a/src/types/accessors/MetabolicModel.jl b/src/types/accessors/MetabolicModel.jl index 7474b1e61..a8d37b9a4 100644 --- a/src/types/accessors/MetabolicModel.jl +++ b/src/types/accessors/MetabolicModel.jl @@ -7,8 +7,6 @@ # automatically derived methods for [`ModelWrapper`](@ref). # -_missing_impl_error(m, a) = throw(MethodError(m, a)) - """ $(TYPEDSIGNATURES) @@ -22,7 +20,7 @@ supplies of enzymatic and genetic material and virtual cell volume, etc. To simplify the view of the model contents use [`reaction_flux`](@ref). """ function reactions(a::MetabolicModel)::Vector{String} - _missing_impl_error(reactions, (a,)) + missing_impl_error(reactions, (a,)) end """ @@ -35,7 +33,7 @@ As with [`reactions`](@ref)s, some metabolites in models may be virtual, representing purely technical equality constraints. """ function metabolites(a::MetabolicModel)::Vector{String} - _missing_impl_error(metabolites, (a,)) + missing_impl_error(metabolites, (a,)) end """ @@ -68,7 +66,7 @@ model `m` is defined as satisfying the equations: - `(lbs, ubs) == bounds(m) """ function stoichiometry(a::MetabolicModel)::SparseMat - _missing_impl_error(stoichiometry, (a,)) + missing_impl_error(stoichiometry, (a,)) end """ @@ -77,7 +75,7 @@ $(TYPEDSIGNATURES) Get the lower and upper solution bounds of a model. """ function bounds(a::MetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - _missing_impl_error(bounds, (a,)) + missing_impl_error(bounds, (a,)) end """ @@ -97,7 +95,7 @@ Get the objective vector of the model. Analysis functions, such as where `x` is a feasible solution of the model. """ function objective(a::MetabolicModel)::SparseVec - _missing_impl_error(objective, (a,)) + missing_impl_error(objective, (a,)) end """ @@ -131,7 +129,7 @@ an identity matrix. function reaction_flux(a::MetabolicModel)::SparseMat nr = n_reactions(a) nf = n_fluxes(a) - nr == nf || _missing_impl_error(reaction_flux, (a,)) + nr == nf || missing_impl_error(reaction_flux, (a,)) spdiagm(fill(1, nr)) end diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 29ca1b24c..2cc07a01c 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) A simple helper to pick a single wrapped model """ function unwrap_model(a::ModelWrapper) - _missing_impl_error(unwrap_model, (a,)) + missing_impl_error(unwrap_model, (a,)) end # diff --git a/src/types/accessors/misc/missing_impl.jl b/src/types/accessors/misc/missing_impl.jl new file mode 100644 index 000000000..e9dbbd4df --- /dev/null +++ b/src/types/accessors/misc/missing_impl.jl @@ -0,0 +1,2 @@ + +missing_impl_error(m, a) = throw(MethodError(m, a)) From 22c8630143035ab099e1eedfa5790d7cf993e135 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 29 Sep 2022 11:15:21 +0200 Subject: [PATCH 024/531] clean up the naming --- src/COBREXA.jl | 2 +- src/{modules.jl => moduletools.jl} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/{modules.jl => moduletools.jl} (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 8765b8bd5..09ea06382 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -35,7 +35,7 @@ const version = VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) # bootstrap the module machinery -include("modules.jl") +include("moduletools.jl") using .ModuleTools # load various internal helpers first diff --git a/src/modules.jl b/src/moduletools.jl similarity index 100% rename from src/modules.jl rename to src/moduletools.jl From 6a66953a7cc474e9e79d5bcc7962e8a64046d2db Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 13 Oct 2022 16:31:23 +0200 Subject: [PATCH 025/531] analysis module doc (test) --- src/analysis.jl | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/analysis.jl b/src/analysis.jl index 600891016..5c442421f 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,4 +1,18 @@ +""" +Contains the analysis functions of COBREXA.jl. Typically, these take a +[`MetabolicModel`](@ref), convert it to the solver represenation and run +various optimization tasks on top of it, such as finding an optimum (e.g. in +[`flux_balance_analysis`](@ref)) or multiple optima (e.g., +[`flux_variability_analysis`](@ref)). + +Functions [`screen`](@ref) and [`screen_optmodel_modifications`](@ref) are +special meta-analyses that apply another specified analysis to multiple +systematically generated versions of the same input model. + +# Exports +$(EXPORTS) +""" module Analysis using ..ModuleTools @dse From 84ed40d1bb11d8c62ed3a2a1533e9f05675c13e0 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 10:05:32 +0200 Subject: [PATCH 026/531] add module docs --- src/analysis.jl | 13 +++++++++++++ src/internal.jl | 8 ++++++++ src/io.jl | 16 ++++++++++++++++ src/log.jl | 16 ++++++++++++++++ src/macros.jl | 8 ++++++++ src/misc/identifiers.jl | 13 +++++++++++-- src/misc/ontology/SBOTerms.jl | 20 ++++++++++++++------ src/moduletools.jl | 9 +++++++++ src/reconstruction.jl | 18 ++++++++++++++++++ src/solver.jl | 9 +++++++++ src/types.jl | 26 +++++++++++++++++++++++++- src/utils.jl | 14 +++++++++++--- 12 files changed, 158 insertions(+), 12 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index 5c442421f..4627b0c23 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -1,5 +1,7 @@ """ + module Analysis + Contains the analysis functions of COBREXA.jl. Typically, these take a [`MetabolicModel`](@ref), convert it to the solver represenation and run various optimization tasks on top of it, such as finding an optimum (e.g. in @@ -36,7 +38,18 @@ using StableRNGs @inc_dir analysis sampling @inc_dir analysis reconstruction +""" + module Modifications + +Functions that implement well-defined modifications of the optimization model +before solving. These can be used as `modifications` parameter in e.g. +[`flux_balance_analysis`](@ref) and other analysis functions. + +# Exports +$(EXPORTS) +""" module Modifications +# TODO move this into Solver using ..ModuleTools @dse end diff --git a/src/internal.jl b/src/internal.jl index fca730b38..80795c4c0 100644 --- a/src/internal.jl +++ b/src/internal.jl @@ -1,3 +1,11 @@ +""" + module Internal + +Internal COBREXA.jl functionality, mostly constants and macros. + +# Exports +$(EXPORTS) +""" module Internal using ..ModuleTools @dse diff --git a/src/io.jl b/src/io.jl index eddd399ea..b5300175d 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,3 +1,11 @@ +""" + module IO + +Reading and writing the models from/to storage. + +# Exports +$(EXPORTS) +""" module IO using ..ModuleTools @dse @@ -13,6 +21,14 @@ using JSON, MAT, SBML, HDF5 @inc_dir io @inc_dir io show +""" + module Internal + +Internal IO helpers. + +# Exports +$(EXPORTS) +""" module Internal using ..ModuleTools @dse diff --git a/src/log.jl b/src/log.jl index 125b93b2d..2365c7fb4 100644 --- a/src/log.jl +++ b/src/log.jl @@ -1,8 +1,24 @@ +""" + module Log + +Logging helpers for COBREXA. + +# Exports +$(EXPORTS) +""" module Log using ..ModuleTools @dse +""" + module Internal + +Internal helpers for logging. + +# Exports +$(EXPORTS) +""" module Internal using ..ModuleTools @dse diff --git a/src/macros.jl b/src/macros.jl index 33808d15b..ab20cae23 100644 --- a/src/macros.jl +++ b/src/macros.jl @@ -1,4 +1,12 @@ +""" + module Macros + +Internal COBREXA macros. + +# Exports +$(EXPORTS) +""" module Macros using ..ModuleTools @dse diff --git a/src/misc/identifiers.jl b/src/misc/identifiers.jl index 10ae9aeb6..3da59b299 100644 --- a/src/misc/identifiers.jl +++ b/src/misc/identifiers.jl @@ -1,9 +1,18 @@ """ -This module uses annotation identifiers to classify reactions, metabolites, + module Identifiers + +This module exports interpretation of terms to classify reactions, metabolites, genes, etc. If an subject has a matching annotation, then it is assumed that it -is part of the associated class of objects. +is part of the associated class of objects. Where possible, this is done using +the terms in module [`SBOTerms`](@ref). + +# Exports +$(EXPORTS) """ module Identifiers +using ..ModuleTools +@dse + using ..SBOTerms const EXCHANGE_REACTIONS = [SBOTerms.EXCHANGE_REACTION] diff --git a/src/misc/ontology/SBOTerms.jl b/src/misc/ontology/SBOTerms.jl index c41d1cead..f89c14a99 100644 --- a/src/misc/ontology/SBOTerms.jl +++ b/src/misc/ontology/SBOTerms.jl @@ -1,14 +1,22 @@ """ -This module contains SBO terms recognized by COBREXA. For the full ontology, see -https://github.com/EBI-BioModels/SBO/blob/master/SBO_OBO.obo. + module SBOTerms -If an SBO term appears here it *may* be used in a function; if an SBO term does -not appear here, then it is *not* used in any COBREXA function. +Several selected SBO terms that are recognized by COBREXA. For the full +ontology, see https://github.com/EBI-BioModels/SBO/blob/master/SBO_OBO.obo. -These terms are used in module `Identifiers` which groups them as appropriate -for use in functions that classify reactions, metabolites, etc. +If an SBO term appears here, it *may* be recognized in a function; if an SBO +term does not appear here, then it is *not* used in any COBREXA function. + +Mostly, the SBO terms are now used in module `Identifiers`, where they get +grouped semantically to allow other functions to classify reactions (such as +exchanges vs. biomass vs. normal reactions), metabolites, compartments, etc. + +# Exports +$(EXPORTS) """ module SBOTerms +using ..ModuleTools +@dse const FLUX_BALANCE_FRAMEWORK = "SBO:0000624" const RESOURCE_BALANCE_FRAMEWORK = "SBO:0000692" diff --git a/src/moduletools.jl b/src/moduletools.jl index 752fe63e1..aad5ab758 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -1,3 +1,11 @@ +""" + module ModuleTools + +Internal helpers for simplifying the work with COBREXA submodules. + +# Exports +$(EXPORTS) +""" module ModuleTools macro inc(path...) esc(:(include(joinpath(@__DIR__, $(joinpath(String.(path)...) * ".jl"))))) @@ -12,6 +20,7 @@ end macro dse() :(using DocStringExtensions) end +@dse macro inject(mod, code) esc(:(Base.eval($mod, $(Expr(:quote, code))))) diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 5bba14188..3c07f1fd6 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -1,4 +1,13 @@ +""" + module Reconstruction + +Reconstruction functionality of COBREXA; mainly functions that construct or +modify the model contents. + +# Exports +$(EXPORTS) +""" module Reconstruction using ..ModuleTools @dse @@ -15,6 +24,15 @@ using MacroTools @inc_dir reconstruction +""" + module Modifications + +Functions that create model variants, typically for efficient use in +[`screen`](@ref) and similar functions. + +# Exports +$(EXPORTS) +""" module Modifications using ..ModuleTools @dse diff --git a/src/solver.jl b/src/solver.jl index d05102b49..aee1a8919 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -1,4 +1,13 @@ +""" + module Solver + +Interface of COBREXA to JuMP solvers; mainly recreation of the +`MetabolicModel`s into JuMP optimization models. + +# Exports +$(EXPORTS) +""" module Solver using ..ModuleTools @dse diff --git a/src/types.jl b/src/types.jl index 982fceeec..56e14474a 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,5 +1,13 @@ # make the module for types and load basic abstract types +""" + module Types + +Module with all types, mainly the model types and various typed model contents. + +# Exports +$(EXPORTS) +""" module Types using ..ModuleTools @dse @@ -18,7 +26,15 @@ using SparseArrays @export_locals end -# the specialized module for accessors +""" + module Accessors + +Functions that gather data from various model types in a standardized form. +Overload these if you want COBREXA to work with your own module types. + +# Exports +$(EXPORTS) +""" module Accessors using ..ModuleTools @dse @@ -27,6 +43,14 @@ using ..Internal.Macros using SparseArrays +""" + module Internal + +Internal helpers for types. + +# Exports +$(EXPORTS) +""" module Internal using ..ModuleTools @dse diff --git a/src/utils.jl b/src/utils.jl index 2688b6d16..74a27f589 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -1,7 +1,15 @@ -# TODO: This is now for stuff that didn't really fit anywhere else. It might be -# much much more useful to actually sort out the utils into individual -# namespaces. +# TODO: It might be much much more useful to actually sort out the utils into +# individual namespaces. +""" + module Utils + +Mostly contains various COBREXA functions and helpers, including internal ones. + +# Exports + +$(EXPORTS) +""" module Utils using ..ModuleTools @dse From 9289b908b1eeb3ed99d454df60619f154367f2f4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 11:39:30 +0200 Subject: [PATCH 027/531] remove unused functions (these should be replaced by local model knowledge) --- src/types/models/CoreModel.jl | 8 -------- src/types/models/CoreModelCoupled.jl | 1 - src/types/models/StandardModel.jl | 17 ----------------- test/types/CoreModelCoupled.jl | 11 ----------- 4 files changed, 37 deletions(-) diff --git a/src/types/models/CoreModel.jl b/src/types/models/CoreModel.jl index 05ae63ade..e2686eee1 100644 --- a/src/types/models/CoreModel.jl +++ b/src/types/models/CoreModel.jl @@ -128,14 +128,6 @@ Accessors.reaction_stoichiometry(m::CoreModel, ridx)::Dict{String,Float64} = """ $(TYPEDSIGNATURES) -Retrieve a vector of all gene associations in a [`CoreModel`](@ref), in the -same order as `reactions(model)`. -""" -reaction_gene_association_vec(model::CoreModel)::Vector{Maybe{GeneAssociation}} = model.grrs - -""" -$(TYPEDSIGNATURES) - Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction index. """ diff --git a/src/types/models/CoreModelCoupled.jl b/src/types/models/CoreModelCoupled.jl index 5806d9b05..c07357a76 100644 --- a/src/types/models/CoreModelCoupled.jl +++ b/src/types/models/CoreModelCoupled.jl @@ -100,5 +100,4 @@ CoreModelCoupled(lm::CoreModel, C::MatType, cl::VecType, cu::VecType) = CoreCoupling(lm, sparse(C), collect(cl), collect(cu)) # these are special for CoreModel-ish models -@inherit_model_methods CoreModelCoupled () lm () reaction_gene_association_vec @inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) Accessors.reaction_stoichiometry diff --git a/src/types/models/StandardModel.jl b/src/types/models/StandardModel.jl index 6376e5ee1..8a96c5907 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -142,23 +142,6 @@ end """ $(TYPEDSIGNATURES) -Return the lower bounds for all reactions in `model` in sparse format. -""" -lower_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].lower_bound for rxn in reactions(model)]) - -""" -$(TYPEDSIGNATURES) - -Return the upper bounds for all reactions in `model` in sparse format. -Order matches that of the reaction ids returned in `reactions()`. -""" -upper_bounds(model::StandardModel)::Vector{Float64} = - sparse([model.reactions[rxn].upper_bound for rxn in reactions(model)]) - -""" -$(TYPEDSIGNATURES) - Return the lower and upper bounds, respectively, for reactions in `model`. Order matches that of the reaction ids returned in `reactions()`. """ diff --git a/test/types/CoreModelCoupled.jl b/test/types/CoreModelCoupled.jl index d64ba1ae9..5258b0f5d 100644 --- a/test/types/CoreModelCoupled.jl +++ b/test/types/CoreModelCoupled.jl @@ -17,15 +17,4 @@ end @test reaction_gene_association(sm, reactions(sm)[1]) == reaction_gene_association(cm, reactions(sm)[1]) - - @test reaction_gene_association_vec(cm)[1:3] == [ - [["b3916"], ["b1723"]], - [ - ["b0902", "b0903", "b2579"], - ["b0902", "b0903"], - ["b0902", "b3114"], - ["b3951", "b3952"], - ], - [["b4025"]], - ] end From 94436ef66a9aa721a943b9e7a18ee250d0306e56 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 13:22:15 +0200 Subject: [PATCH 028/531] fix and recreate StdModel lower_ and upper_bounds --- .../{ => modifications}/enzymes.jl | 0 src/types/misc/StandardModel.jl | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) rename src/reconstruction/{ => modifications}/enzymes.jl (100%) diff --git a/src/reconstruction/enzymes.jl b/src/reconstruction/modifications/enzymes.jl similarity index 100% rename from src/reconstruction/enzymes.jl rename to src/reconstruction/modifications/enzymes.jl diff --git a/src/types/misc/StandardModel.jl b/src/types/misc/StandardModel.jl index 05e95504c..f85aa0ded 100644 --- a/src/types/misc/StandardModel.jl +++ b/src/types/misc/StandardModel.jl @@ -48,3 +48,21 @@ $(TYPEDSIGNATURES) Shallow copy of a [`Gene`](@ref) """ Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) + +""" +$(TYPEDSIGNATURES) + +Return the lower bounds for all reactions in `model`. +Order matches that of the reaction ids returned in `reactions()`. +""" +lower_bounds(model::StandardModel)::Vector{Float64} = + [model.reactions[rxn].lower_bound for rxn in reactions(model)] + +""" +$(TYPEDSIGNATURES) + +Return the upper bounds for all reactions in `model`. +Order matches that of the reaction ids returned in `reactions()`. +""" +upper_bounds(model::StandardModel)::Vector{Float64} = + [model.reactions[rxn].upper_bound for rxn in reactions(model)] From ff6438291f79daf74f199f4387c92923cbf0a179 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 13:23:15 +0200 Subject: [PATCH 029/531] doc cleaning --- src/types/misc/StandardModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/misc/StandardModel.jl b/src/types/misc/StandardModel.jl index f85aa0ded..04d3dc196 100644 --- a/src/types/misc/StandardModel.jl +++ b/src/types/misc/StandardModel.jl @@ -53,7 +53,7 @@ Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) $(TYPEDSIGNATURES) Return the lower bounds for all reactions in `model`. -Order matches that of the reaction ids returned in `reactions()`. +Order matches that of the reaction IDs returned by [`reactions`](@ref). """ lower_bounds(model::StandardModel)::Vector{Float64} = [model.reactions[rxn].lower_bound for rxn in reactions(model)] @@ -62,7 +62,7 @@ lower_bounds(model::StandardModel)::Vector{Float64} = $(TYPEDSIGNATURES) Return the upper bounds for all reactions in `model`. -Order matches that of the reaction ids returned in `reactions()`. +Order matches that of the reaction IDs returned in [`reactions`](@ref). """ upper_bounds(model::StandardModel)::Vector{Float64} = [model.reactions[rxn].upper_bound for rxn in reactions(model)] From 644888f5fc748d3b47cacbbb1c06f98962501464 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 13:53:07 +0200 Subject: [PATCH 030/531] some final reordering --- src/reconstruction.jl | 3 +-- src/{analysis => reconstruction}/gecko.jl | 0 src/{analysis => reconstruction}/smoment.jl | 0 test/{analysis => reconstruction}/gecko.jl | 0 test/{analysis => reconstruction}/smoment.jl | 0 5 files changed, 1 insertion(+), 2 deletions(-) rename src/{analysis => reconstruction}/gecko.jl (100%) rename src/{analysis => reconstruction}/smoment.jl (100%) rename test/{analysis => reconstruction}/gecko.jl (100%) rename test/{analysis => reconstruction}/smoment.jl (100%) diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 3c07f1fd6..55bbdc44c 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -13,7 +13,6 @@ using ..ModuleTools @dse using ..Accessors -using ..Analysis using ..Internal: constants using ..Internal.Macros using ..Log.Internal @@ -43,7 +42,7 @@ end # this needs to import from Reconstruction @inject Reconstruction.Modifications begin - using ...Reconstruction + using ..Reconstruction @inc_dir reconstruction modifications @export_locals diff --git a/src/analysis/gecko.jl b/src/reconstruction/gecko.jl similarity index 100% rename from src/analysis/gecko.jl rename to src/reconstruction/gecko.jl diff --git a/src/analysis/smoment.jl b/src/reconstruction/smoment.jl similarity index 100% rename from src/analysis/smoment.jl rename to src/reconstruction/smoment.jl diff --git a/test/analysis/gecko.jl b/test/reconstruction/gecko.jl similarity index 100% rename from test/analysis/gecko.jl rename to test/reconstruction/gecko.jl diff --git a/test/analysis/smoment.jl b/test/reconstruction/smoment.jl similarity index 100% rename from test/analysis/smoment.jl rename to test/reconstruction/smoment.jl From 6f7d125ee9d04cea9796011c97ffd8d6c538ef5c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 14:07:36 +0200 Subject: [PATCH 031/531] remove doctests (all documentation is tested by being built in in notebooks anyway) --- .gitlab-ci.yml | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index a45a8b9ab..16bdac78d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -257,31 +257,6 @@ docker-release: <<: *global_dind <<: *global_trigger_release_containers -# -# DOCUMENTATION TESTS -# -# In pull requests, triggered after the tests succeed to avoid unnecessary -# double failure. In normal branch testing, these get triggered with normal -# tests (the error should be visible ASAP). We avoid a separate stage to keep -# the pipeline parallelizable. -# - -.global_doctests: &global_doctests - image: $CI_REGISTRY/r3/docker/julia-custom - script: - - julia --project=@. -e 'import Pkg; Pkg.instantiate();' - - julia --project=@. --color=yes test/doctests.jl - -doc-tests-pr:julia1.7: - stage: documentation - <<: *global_doctests - <<: *global_trigger_pull_request - -doc-tests:julia1.7: - stage: test - <<: *global_doctests - <<: *global_trigger_full_tests - # # DOCUMENTATION # From 8736edcb62bbcb25126db4d8eaae8f4af2bab71f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 14:10:30 +0200 Subject: [PATCH 032/531] remove doctests leftover --- test/doctests.jl | 14 -------------- 1 file changed, 14 deletions(-) delete mode 100644 test/doctests.jl diff --git a/test/doctests.jl b/test/doctests.jl deleted file mode 100644 index 50da9f2d5..000000000 --- a/test/doctests.jl +++ /dev/null @@ -1,14 +0,0 @@ -using Test, Documenter, COBREXA - -ex = quote - using COBREXA, Tulip - include(joinpath(dirname(pathof(COBREXA)), "..", "test", "data_static.jl")) - model = test_LP() -end - -# set module-level metadata -DocMeta.setdocmeta!(COBREXA, :DocTestSetup, ex; recursive = true) - -@testset "Documentation tests" begin - doctest(COBREXA; manual = true) -end From 4abb74876d8d81746b17fe6d2e2d14df5c19c6c6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 21 Oct 2022 14:37:33 +0200 Subject: [PATCH 033/531] iolog fixup --- src/types/models/SBMLModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index effb8a3bf..2f79098a7 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -169,7 +169,7 @@ end function _sbml_export_sbo(annotations::Annotations)::Maybe{String} haskey(annotations, "sbo") || return nothing if length(annotations["sbo"]) != 1 - @_io_log @error "Data loss: SBO term is not unique for SBML export" annotations["sbo"] + @io_log @error "Data loss: SBO term is not unique for SBML export" annotations["sbo"] return end return annotations["sbo"][1] @@ -180,7 +180,7 @@ function _sbml_import_notes(notes::Maybe{String})::Notes end function _sbml_export_notes(notes::Notes)::Maybe{String} - isempty(notes) || @_io_log @error "Data loss: notes not exported to SBML" notes + isempty(notes) || @io_log @error "Data loss: notes not exported to SBML" notes nothing end From 98bf813a0a14ae678e50f165c31728ffb4995e64 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 24 Oct 2022 09:27:16 +0200 Subject: [PATCH 034/531] fix merge fails --- src/types/models/MATModel.jl | 2 +- src/types/models/SBMLModel.jl | 15 +++++++-------- src/types/models/StandardModel.jl | 2 +- 3 files changed, 9 insertions(+), 10 deletions(-) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index db45711b2..20e22faeb 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -160,7 +160,7 @@ function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metcharges), ) - _maybemap(Int, isnan(met_charge) ? nothing : met_charge) + maybemap(Int, isnan(met_charge) ? nothing : met_charge) end """ diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 2f79098a7..337c3806e 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -125,7 +125,7 @@ $(TYPEDSIGNATURES) Get the compartment of a chosen metabolite from [`SBMLModel`](@ref). """ -metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment +Accessors.metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment """ $(TYPEDSIGNATURES) @@ -227,7 +227,7 @@ $(TYPEDSIGNATURES) Return the annotations of reaction with ID `rid`. """ -reaction_annotations(model::SBMLModel, rid::String) = +Accessors.reaction_annotations(model::SBMLModel, rid::String) = _sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms) """ @@ -235,7 +235,7 @@ $(TYPEDSIGNATURES) Return the annotations of metabolite with ID `mid`. """ -metabolite_annotations(model::SBMLModel, mid::String) = +Accessors.metabolite_annotations(model::SBMLModel, mid::String) = _sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms) """ @@ -243,7 +243,7 @@ $(TYPEDSIGNATURES) Return the annotations of gene with ID `gid`. """ -gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( +Accessors.gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( model.sbml.gene_products[gid].sbo, model.sbml.gene_products[gid].cv_terms, ) @@ -253,7 +253,7 @@ $(TYPEDSIGNATURES) Return the notes about reaction with ID `rid`. """ -reaction_notes(model::SBMLModel, rid::String) = +Accessors.reaction_notes(model::SBMLModel, rid::String) = _sbml_import_notes(model.sbml.reactions[rid].notes) """ @@ -261,7 +261,7 @@ $(TYPEDSIGNATURES) Return the notes about metabolite with ID `mid`. """ -metabolite_notes(model::SBMLModel, mid::String) = +Accessors.metabolite_notes(model::SBMLModel, mid::String) = _sbml_import_notes(model.sbml.species[mid].notes) """ @@ -269,7 +269,7 @@ $(TYPEDSIGNATURES) Return the notes about gene with ID `gid`. """ -gene_notes(model::SBMLModel, gid::String) = +Accessors.gene_notes(model::SBMLModel, gid::String) = _sbml_import_notes(model.sbml.gene_products[gid].notes) """ @@ -302,7 +302,6 @@ function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) metid(mid) => SBML.Species( name = metabolite_name(mm, mid), compartment = default("compartment", comps[mi]), - formula = metabolite_formula(mm, mid), formula = maybemap(unparse_formula, metabolite_formula(mm, mid)), charge = metabolite_charge(mm, mid), constant = false, diff --git a/src/types/models/StandardModel.jl b/src/types/models/StandardModel.jl index 8a96c5907..05f0915e8 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -235,7 +235,7 @@ $(TYPEDSIGNATURES) Return the notes associated with gene `id` in `model`. Return an empty Dict if not present. """ -Accessors.gene_notes(model::StandardModel, id::String)::Maybe{Notes} = model.genes[id].notes +Accessors.gene_notes(model::StandardModel, gid::String) = model.genes[gid].notes """ $(TYPEDSIGNATURES) From c2035c33725391c230c782b6845ed6a627b97c9d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 24 Oct 2022 10:51:35 +0200 Subject: [PATCH 035/531] format --- src/types/models/SBMLModel.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 337c3806e..71b7ce802 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -125,7 +125,8 @@ $(TYPEDSIGNATURES) Get the compartment of a chosen metabolite from [`SBMLModel`](@ref). """ -Accessors.metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment +Accessors.metabolite_compartment(model::SBMLModel, mid::String) = + model.sbml.species[mid].compartment """ $(TYPEDSIGNATURES) From a933cd5a5cd1a9560a150b3823291aa16d1bc469 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 25 Oct 2022 13:01:57 +0200 Subject: [PATCH 036/531] delete moment algorithm --- src/analysis/modifications/moment.jl | 120 --------------------------- test/analysis/moment.jl | 21 ----- 2 files changed, 141 deletions(-) delete mode 100644 src/analysis/modifications/moment.jl delete mode 100644 test/analysis/moment.jl diff --git a/src/analysis/modifications/moment.jl b/src/analysis/modifications/moment.jl deleted file mode 100644 index 31b2e56f6..000000000 --- a/src/analysis/modifications/moment.jl +++ /dev/null @@ -1,120 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A modification that adds enzyme capacity constraints to the problem using a _modified_ -version of the MOMENT algorithm. Requires specific activities, `ksas` [mmol product/g -enzyme/h], for each reaction. Proteins are identified by their associated gene IDs. Adds a -variable vector `y` to the problem corresponding to the protein concentration [g enzyme/gDW -cell] of each gene product in the order of `genes(model)`. The total protein concentration -[g protein/gDW cell] is constrained to be less than or equal to the `protein_mass_fraction`. -Reaction flux constraints are changed to the MOMENT constraints (see below) for all -reactions that have a gene reaction rule, otherwise the flux bounds are left unaltered. - -See Adadi, Roi, et al. "Prediction of microbial growth rate versus biomass yield by a -metabolic network with kinetic parameters." PLoS computational biology (2012) for more -details of the original algorithm. - -Here, a streamlined version of the algorithm is implemented to ensure that the correct units -are used. Specifically, this implementation uses specific activities instead of `kcats`. -Thus, for a reaction that can only proceed forward and is catalyzed by protein `a`, the flux -`x[i]` is bounded by `x[i] <= ksas[i] * y[a]`. If isozymes `a` or `b` catalyse the -reaction, then `x[i] <= ksas[i] * (y[a] + y[b])`. If a reaction is catalyzed by subunits `a` -and `b` then `x[i] <= ksas[i] * min(y[a], y[b])`. These rules are applied recursively in the -model like in the original algorithm. The enzyme capacity constraint is then implemented by -`sum(y) ≤ protein_mass_fraction`. The major benefit of using `ksas` instead of `kcats` is -that active site number and unit issues are prevented. - -# Example -``` -flux_balance_analysis( - ..., - modifications = [ add_moment_constraints(my_kcats, 0.6) ], -) -``` -""" -add_moment_constraints(kcats::Dict{String,Float64}, protein_mass_fraction::Float64) = - (model, opt_model) -> begin - @warn( - "DEPRECATION WARNING: 'add_moment_constraints' will be removed in future versions of COBREXA.jl in favor of a GECKO-based formulation" - ) - - lbs, ubs = get_optmodel_bounds(opt_model) # to assign directions - # get grrs and ignore empty blocks: TODO: fix importing to avoid this ugly conditional see #462 - grrs = Dict( - rid => reaction_gene_association(model, rid) for - rid in reactions(model) if !( - reaction_gene_association(model, rid) == [[""]] || - isnothing(reaction_gene_association(model, rid)) - ) - ) - - # add protein variables - y = @variable(opt_model, y[1:n_genes(model)] >= 0) - - # add variables to deal with enzyme subunits - num_temp = sum(length(values(grr)) for grr in values(grrs)) # number "AND" blocks in grrs - t = @variable(opt_model, [1:num_temp]) # anonymous variable - @constraint(opt_model, t .>= 0) - - #= - Note, not all of t needs to be created, only those with OR GRR rules, however this - adds a lot of complexity to the code - re-implement this method if efficiency becomes - an issue. - =# - - # add capacity constraint - @constraint(opt_model, sum(y) <= protein_mass_fraction) - - k = 1 # counter - kstart = 1 - kend = 1 - x = opt_model[:x] - for (ridx, rid) in enumerate(reactions(model)) - - isnothing(get(grrs, rid, nothing)) && continue # only apply MOMENT constraints to reactions with a valid GRR - grrs[rid] == [[""]] && continue # TODO: remove once #462 is implemented - - # delete original constraints - delete(opt_model, opt_model[:lbs][ridx]) - delete(opt_model, opt_model[:ubs][ridx]) - - #= - For multi-subunit enzymes, the flux is bounded by the minimum concentration of - any subunit in the enzyme. If multiple isozymes with subunits exist, then the - sum of these minima bound the enzyme flux. E.g., suppose that you have a GRR - [[y, z], [w, u, v]], then x <= sum(min(y, z), min(w, u, v)). - - It is possible to reformulate x <= min(y, z) to preserve convexity: - x <= t && t <= y && t <= z where t is a subunit variable. - - The remainder of the code implements these ideas. - =# - - # build up the subunit variables - kstart = k - for grr in grrs[rid] - for gid in grr - gidx = first(indexin([gid], genes(model))) - @constraint(opt_model, t[k] <= y[gidx]) - end - k += 1 - end - kend = k - 1 - - # total enzyme concentration is sum of minimums - isozymes = @expression(opt_model, kcats[rid] * sum(t[kstart:kend])) - - if lbs[ridx] >= 0 && ubs[ridx] > 0 # forward only - @constraint(opt_model, x[ridx] <= isozymes) - @constraint(opt_model, 0 <= x[ridx]) - elseif lbs[ridx] < 0 && ubs[ridx] <= 0 # reverse only - @constraint(opt_model, -isozymes <= x[ridx]) - @constraint(opt_model, x[ridx] <= 0) - elseif lbs[ridx] == ubs[ridx] == 0 # set to zero - @constraint(opt_model, x[ridx] == 0) - else # reversible - @constraint(opt_model, x[ridx] <= isozymes) - @constraint(opt_model, -isozymes <= x[ridx]) - end - end - end diff --git a/test/analysis/moment.jl b/test/analysis/moment.jl deleted file mode 100644 index 991197619..000000000 --- a/test/analysis/moment.jl +++ /dev/null @@ -1,21 +0,0 @@ -@testset "Moment algorithm" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) - - ksas = Dict(rid => 1000.0 for rid in reactions(model)) - protein_mass_fraction = 0.56 - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - add_moment_constraints(ksas, protein_mass_fraction;), - change_constraint("EX_glc__D_e", lower_bound = -1000), - ], - ) - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.6623459899423948, - atol = TEST_TOLERANCE, - ) -end From d1d43c0ff02e81a7a7a36ba52f887870db671c5d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 25 Oct 2022 13:16:46 +0200 Subject: [PATCH 037/531] rename MetabolicModel to AbstractMetabolicModel --- docs/src/concepts/2_modifications.md | 6 +- docs/src/concepts/3_custom_models.md | 6 +- docs/src/concepts/4_wrappers.md | 10 +-- docs/src/examples/03b_accessors.jl | 4 +- src/analysis.jl | 2 +- src/analysis/envelopes.jl | 8 +-- src/analysis/flux_balance_analysis.jl | 6 +- src/analysis/flux_variability_analysis.jl | 12 ++-- src/analysis/max_min_driving_force.jl | 4 +- src/analysis/minimize_metabolic_adjustment.jl | 6 +- src/analysis/modifications/knockout.jl | 4 +- .../parsimonious_flux_balance_analysis.jl | 6 +- .../gapfill_minimum_reactions.jl | 2 +- src/analysis/sampling/affine_hit_and_run.jl | 2 +- src/analysis/sampling/warmup_variability.jl | 4 +- src/analysis/screening.jl | 6 +- src/io/h5.jl | 2 +- src/io/io.jl | 6 +- src/io/json.jl | 2 +- src/io/mat.jl | 2 +- src/io/sbml.jl | 2 +- src/io/show/MetabolicModel.jl | 2 +- src/macros/is_xxx_reaction.jl | 4 +- src/reconstruction/community.jl | 10 +-- src/reconstruction/gecko.jl | 2 +- src/reconstruction/smoment.jl | 2 +- src/solver.jl | 12 ++-- ...olicModel.jl => AbstractMetabolicModel.jl} | 12 ++-- src/types/accessors/MetabolicModel.jl | 66 +++++++++---------- src/types/accessors/ModelWrapper.jl | 2 +- src/types/models/CoreModel.jl | 4 +- src/types/models/CoreModelCoupled.jl | 6 +- src/types/models/HDF5Model.jl | 2 +- src/types/models/JSONModel.jl | 6 +- src/types/models/MATModel.jl | 4 +- src/types/models/SBMLModel.jl | 4 +- src/types/models/Serialized.jl | 4 +- src/types/models/StandardModel.jl | 8 +-- src/types/wrappers/GeckoModel.jl | 2 +- src/types/wrappers/SMomentModel.jl | 4 +- src/utils/Reaction.jl | 6 +- src/utils/Serialized.jl | 2 +- src/utils/fluxes.jl | 4 +- src/utils/looks_like.jl | 12 ++-- test/types/abstract/MetabolicModel.jl | 2 +- 45 files changed, 142 insertions(+), 142 deletions(-) rename src/types/abstract/{MetabolicModel.jl => AbstractMetabolicModel.jl} (83%) diff --git a/docs/src/concepts/2_modifications.md b/docs/src/concepts/2_modifications.md index 5517ca975..2ded14a34 100644 --- a/docs/src/concepts/2_modifications.md +++ b/docs/src/concepts/2_modifications.md @@ -2,7 +2,7 @@ # Writing custom optimizer modifications Functions such as [`flux_balance_analysis`](@ref) internally create a JuMP -model out of the [`MetabolicModel`](@ref), and run the optimizer on that. To be +model out of the [`AbstractMetabolicModel`](@ref), and run the optimizer on that. To be able to make some modifications on the JuMP model before the optimizer is started, most of the functions accept a `modifications` argument, where one can list callbacks that do the changes to the prepared optimization model. @@ -24,8 +24,8 @@ changes by carelessly adding the modifications. Here, we show how to construct the modifications. Their semantics is similar to the [variant-generating functions](1_screen.md), which receive a model (of type -[`MetabolicModel`](@ref)), and are expected to create another (modified) model. -Contrary to that, modifications receive both the [`MetabolicModel`](@ref) and a +[`AbstractMetabolicModel`](@ref)), and are expected to create another (modified) model. +Contrary to that, modifications receive both the [`AbstractMetabolicModel`](@ref) and a JuMP model structure, and are expected to cause a side effect on the latter. A trivial modification that does not do anything can thus be written as: diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index 957b35552..99ba6de9c 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -15,8 +15,8 @@ separate concerns: work properly on any data structure? To solve the first concern, COBREXA.jl specifies a set of generic accessors -that work over the abstract type [`MetabolicModel`](@ref). To use your data -structure in a model, you just make it a subtype of [`MetabolicModel`](@ref) +that work over the abstract type [`AbstractMetabolicModel`](@ref). To use your data +structure in a model, you just make it a subtype of [`AbstractMetabolicModel`](@ref) and overload the required accessors. The accessors are functions that extract some relevant information, such as [`stoichiometry`](@ref) and [`bounds`](@ref), returning a fixed simple data type that can be further used @@ -41,7 +41,7 @@ The whole model can thus be specified with a single integer N that represents the length of the reaction cycle: ```julia -struct CircularModel <: MetabolicModel +struct CircularModel <: AbstractMetabolicModel size::Int end ``` diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index fcbd3f8b5..9d0c0d462 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -8,7 +8,7 @@ small layers that add or change the functionality of a given base models. Types [`Serialized`](@ref), [`CoreCoupling`](@ref), [`SMomentModel`](@ref), and [`GeckoModel`](@ref) all work in this manner -- add some extra functionality to the "base". Technically, they are all subtypes of the abstract type -[`ModelWrapper`](@ref), which itself is a subtype of [`MetabolicModel`](@ref) +[`ModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) and can thus be used in all standard analysis functions. Similarly, the model wraps can be stacked -- it is easy to e.g. serialize a [`GeckoModel`](@ref), or to add coupling to an existing [`SMomentModel`](@ref). @@ -28,14 +28,14 @@ and fast -- we just discard the wrapper. Creating a model wrapper structure is simple -- by declaring it a subtype of [`ModelWrapper`](@ref) and implementing a single function [`unwrap_model`](@ref), we get default implementations of all accessors that -should work for any [`MetabolicModel`](@ref). +should work for any [`AbstractMetabolicModel`](@ref). As a technical example, we may make a minimal model wrapper that does not do anything: ```julia struct IdentityWrap <: ModelWrapper - mdl::MetabolicModel + mdl::AbstractMetabolicModel end COBREXA.unwrap_model(x::IdentityWrap) = x.mdl @@ -62,7 +62,7 @@ of certain organism in a model. ```julia struct RateChangedModel <: ModelWrapper factor::Float64 - mdl::MetabolicModel + mdl::AbstractMetabolicModel end ``` @@ -94,7 +94,7 @@ of the models. struct LeakyModel <: ModelWrapper leaking_metabolites::Vector{String} leak_rate::Float64 - mdl::MetabolicModel + mdl::AbstractMetabolicModel end ``` diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl index 125afc9ee..4c58aae67 100644 --- a/docs/src/examples/03b_accessors.jl +++ b/docs/src/examples/03b_accessors.jl @@ -42,8 +42,8 @@ reactions(std) using InteractiveUtils accessors = [ - x.name for x in methodswith(MetabolicModel, COBREXA) if - endswith(String(x.file), "MetabolicModel.jl") + x.name for x in methodswith(AbstractMetabolicModel, COBREXA) if + endswith(String(x.file), "AbstractMetabolicModel.jl") ] println.(accessors); diff --git a/src/analysis.jl b/src/analysis.jl index 4627b0c23..7a249c5c4 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -3,7 +3,7 @@ module Analysis Contains the analysis functions of COBREXA.jl. Typically, these take a -[`MetabolicModel`](@ref), convert it to the solver represenation and run +[`AbstractMetabolicModel`](@ref), convert it to the solver represenation and run various optimization tasks on top of it, such as finding an optimum (e.g. in [`flux_balance_analysis`](@ref)) or multiple optima (e.g., [`flux_variability_analysis`](@ref)). diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl index ddf22bc09..57be74164 100644 --- a/src/analysis/envelopes.jl +++ b/src/analysis/envelopes.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) Version of [`envelope_lattice`](@ref) that works on string reaction IDs instead of integer indexes. """ -envelope_lattice(model::MetabolicModel, rids::Vector{String}; kwargs...) = +envelope_lattice(model::AbstractMetabolicModel, rids::Vector{String}; kwargs...) = envelope_lattice(model, Vector{Int}(indexin(rids, reactions(model))); kwargs...) """ @@ -16,7 +16,7 @@ model. Arguments `samples`, `ranges`, and `reaction_samples` may be optionally specified to customize the lattice creation process. """ envelope_lattice( - model::MetabolicModel, + model::AbstractMetabolicModel, ridxs::Vector{Int}; samples = 10, ranges = collect(zip(bounds(model)...))[ridxs], @@ -32,7 +32,7 @@ $(TYPEDSIGNATURES) Version of [`objective_envelope`](@ref) that works on string reaction IDs instead of integer indexes. """ -objective_envelope(model::MetabolicModel, rids::Vector{String}, args...; kwargs...) = +objective_envelope(model::AbstractMetabolicModel, rids::Vector{String}, args...; kwargs...) = objective_envelope( model, Vector{Int}(indexin(rids, reactions(model))), @@ -93,7 +93,7 @@ julia> envelope.values # the computed flux objective values for each reaction ``` """ objective_envelope( - model::MetabolicModel, + model::AbstractMetabolicModel, ridxs::Vector{Int}, optimizer; modifications = [], diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 495f4545f..bfe69afb7 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -10,7 +10,7 @@ This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ flux_balance_analysis_vec( - model::MetabolicModel, + model::AbstractMetabolicModel, args...; kwargs..., )::Maybe{Vector{Float64}} = @@ -26,7 +26,7 @@ This function is kept for backwards compatibility, use [`flux_dict`](@ref) instead. """ flux_balance_analysis_dict( - model::MetabolicModel, + model::AbstractMetabolicModel, args...; kwargs..., )::Maybe{Dict{String,Float64}} = @@ -71,7 +71,7 @@ function flux_balance_analysis( model::M, optimizer; modifications = [], -) where {M<:MetabolicModel} +) where {M<:AbstractMetabolicModel} opt_model = make_optimization_model(model, optimizer) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index 53a4ad7b5..aee0a7a61 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -46,7 +46,7 @@ flux_variability_analysis(model, [1, 2, 3, 42], GLPK.optimizer) ``` """ function flux_variability_analysis( - model::MetabolicModel, + model::AbstractMetabolicModel, fluxes::SparseMat, optimizer; modifications = [], @@ -104,7 +104,7 @@ $(TYPEDSIGNATURES) An overload of [`flux_variability_analysis`](@ref) that explores the fluxes specified by integer indexes """ function flux_variability_analysis( - model::MetabolicModel, + model::AbstractMetabolicModel, flux_indexes::Vector{Int}, optimizer; kwargs..., @@ -127,7 +127,7 @@ $(TYPEDSIGNATURES) A simpler version of [`flux_variability_analysis`](@ref) that maximizes and minimizes all declared fluxes in the model. Arguments are forwarded. """ -flux_variability_analysis(model::MetabolicModel, optimizer; kwargs...) = +flux_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = flux_variability_analysis(model, reaction_flux(model), optimizer; kwargs...) """ @@ -150,7 +150,7 @@ mins, maxs = flux_variability_analysis_dict( ) ``` """ -function flux_variability_analysis_dict(model::MetabolicModel, optimizer; kwargs...) +function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer; kwargs...) vs = flux_variability_analysis( model, optimizer; @@ -184,7 +184,7 @@ fluxes. This may be useful for models where the sets of reactions and fluxes differ. """ function reaction_variability_analysis( - model::MetabolicModel, + model::AbstractMetabolicModel, reaction_indexes::Vector{Int}, optimizer; kwargs..., @@ -212,7 +212,7 @@ $(TYPEDSIGNATURES) Shortcut for [`reaction_variability_analysis`](@ref) that examines all reactions. """ -reaction_variability_analysis(model::MetabolicModel, optimizer; kwargs...) = +reaction_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = reaction_variability_analysis( model, collect(1:n_reactions(model)), diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl index 5961f58f1..b84a0b777 100644 --- a/src/analysis/max_min_driving_force.jl +++ b/src/analysis/max_min_driving_force.jl @@ -47,7 +47,7 @@ optimization problems. `T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. """ function max_min_driving_force( - model::MetabolicModel, + model::AbstractMetabolicModel, reaction_standard_gibbs_free_energies::Dict{String,Float64}, optimizer; flux_solution::Dict{String,Float64} = Dict{String,Float64}(), @@ -189,7 +189,7 @@ For the metabolite rows, the first column is the maximum concentration, and the is the minimum concentration subject to the constraints above. """ function max_min_driving_force_variability( - model::MetabolicModel, + model::AbstractMetabolicModel, reaction_standard_gibbs_free_energies::Dict{String,Float64}, optimizer; workers = [myid()], diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 386300fd2..4f042a2d8 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -33,7 +33,7 @@ value.(solution[:x]) # extract the flux from the optimizer ``` """ minimize_metabolic_adjustment_analysis( - model::MetabolicModel, + model::AbstractMetabolicModel, flux_ref::Union{Dict{String,Float64},Vector{Float64}}, optimizer; modifications = [], @@ -85,7 +85,7 @@ same order as the reactions in `model`. Arguments are forwarded to This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ -minimize_metabolic_adjustment_analysis_vec(model::MetabolicModel, args...; kwargs...) = +minimize_metabolic_adjustment_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = flux_vector(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) """ @@ -98,5 +98,5 @@ internally. This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ -minimize_metabolic_adjustment_analysis_dict(model::MetabolicModel, args...; kwargs...) = +minimize_metabolic_adjustment_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = flux_dict(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index d0e0f57aa..743c33660 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -29,11 +29,11 @@ knockout(gene_id::String) = knockout([gene_id]) """ $(TYPEDSIGNATURES) -Internal helper for knockouts on generic MetabolicModels. This can be +Internal helper for knockouts on generic AbstractMetabolicModels. This can be overloaded so that the knockouts may work differently (more efficiently) with other models. """ -function _do_knockout(model::MetabolicModel, opt_model, gene_ids::Vector{String}) +function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) for (rxn_num, rxn_id) in enumerate(reactions(model)) rga = reaction_gene_association(model, rxn_id) if !isnothing(rga) && diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index f2a9c9975..8f8ab04f4 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -48,7 +48,7 @@ value.(solution[:x]) # extract the flux from the optimizer ``` """ function parsimonious_flux_balance_analysis( - model::MetabolicModel, + model::AbstractMetabolicModel, optimizer; modifications = [], qp_modifications = [], @@ -97,7 +97,7 @@ internally. This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ -parsimonious_flux_balance_analysis_vec(model::MetabolicModel, args...; kwargs...) = +parsimonious_flux_balance_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = flux_vector(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) """ @@ -110,5 +110,5 @@ forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. This function is kept for backwards compatibility, use [`flux_dict`](@ref) instead. """ -parsimonious_flux_balance_analysis_dict(model::MetabolicModel, args...; kwargs...) = +parsimonious_flux_balance_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = flux_dict(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl index 6034a13d4..a91b76e1d 100644 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ b/src/analysis/reconstruction/gapfill_minimum_reactions.jl @@ -43,7 +43,7 @@ on the reaction count may serve as a heuristic that helps the solver not to waste too much time solving impractically complex subproblems. """ function gapfill_minimum_reactions( - model::MetabolicModel, + model::AbstractMetabolicModel, universal_reactions::Vector{Reaction}, optimizer; objective_bounds = (constants.tolerance, constants.default_reaction_bound), diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index 06dceb32e..41776fb24 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -31,7 +31,7 @@ fluxes = reaction_flux(model)' * samples ``` """ function affine_hit_and_run( - m::MetabolicModel, + m::AbstractMetabolicModel, warmup_points::Matrix{Float64}; sample_iters = 100 .* (1:5), workers = [myid()], diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl index 03b4e0fb7..db41e1aae 100644 --- a/src/analysis/sampling/warmup_variability.jl +++ b/src/analysis/sampling/warmup_variability.jl @@ -6,7 +6,7 @@ minimizing and maximizing reactions. Can not return more than 2 times the number of reactions in the model. """ function warmup_from_variability( - model::MetabolicModel, + model::AbstractMetabolicModel, optimizer, n_points::Int, seed = rand(Int); @@ -46,7 +46,7 @@ single column in the result. actual sampling functions, such as [`affine_hit_and_run`](@ref). """ function warmup_from_variability( - model::MetabolicModel, + model::AbstractMetabolicModel, optimizer, min_reactions::AbstractVector{Int} = 1:n_reactions(model), max_reactions::AbstractVector{Int} = 1:n_reactions(model); diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl index 2887bb9f2..5a008eb79 100644 --- a/src/analysis/screening.jl +++ b/src/analysis/screening.jl @@ -110,7 +110,7 @@ $(TYPEDSIGNATURES) The actual implementation of [`screen`](@ref). """ function _screen_impl( - model::MetabolicModel; + model::AbstractMetabolicModel; variants::Array{V,N}, analysis, args::Array{A,N}, @@ -147,7 +147,7 @@ functions in `variant` to the `model` (in order from "first" to Can be used to test model variants locally. """ -function screen_variant(model::MetabolicModel, variant::Vector, analysis, args = ()) +function screen_variant(model::AbstractMetabolicModel, variant::Vector, analysis, args = ()) for fn in variant model = fn(model) end @@ -239,7 +239,7 @@ $(TYPEDSIGNATURES) The actual implementation of [`screen_optmodel_modifications`](@ref). """ function _screen_optmodel_modifications_impl( - model::MetabolicModel, + model::AbstractMetabolicModel, optimizer; common_modifications::VF = [], modifications::Array{V,N}, diff --git a/src/io/h5.jl b/src/io/h5.jl index 5fe0642d4..4136f269b 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -20,7 +20,7 @@ contents of the saved file. Because all HDF5-based models need to be backed by disk storage, writing the data to disk (using this function) is the only way to make new HDF5 models. """ -function save_h5_model(model::MetabolicModel, file_name::String)::HDF5Model +function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Model rxns = reactions(model) rxnp = sortperm(rxns) mets = metabolites(model) diff --git a/src/io/io.jl b/src/io/io.jl index 4c8b268ba..2f5388c51 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -12,7 +12,7 @@ Currently, these model types are supported: - MATLAB models (`*.mat`, loaded with [`load_mat_model`](@ref)) - HDF5 models (`*.h5`, loaded with [`load_h5_model`](@ref)) """ -function load_model(file_name::String)::MetabolicModel +function load_model(file_name::String)::AbstractMetabolicModel if endswith(file_name, ".json") return load_json_model(file_name) elseif endswith(file_name, ".xml") @@ -37,7 +37,7 @@ converted to `type`. load_model(CoreModel, "mySBMLModel.xml") """ -function load_model(type::Type{T}, file_name::String)::T where {T<:MetabolicModel} +function load_model(type::Type{T}, file_name::String)::T where {T<:AbstractMetabolicModel} convert(type, load_model(file_name)) end @@ -54,7 +54,7 @@ Currently, these model types are supported: - MATLAB models (`*.mat`, saved with [`save_mat_model`](@ref)) - HDF5 models (`*.h5`, saved with [`save_h5_model`](@ref)) """ -function save_model(model::MetabolicModel, file_name::String) +function save_model(model::AbstractMetabolicModel, file_name::String) if endswith(file_name, ".json") return save_json_model(model, file_name) elseif endswith(file_name, ".xml") diff --git a/src/io/json.jl b/src/io/json.jl index 51e6a03fb..55269ad6d 100644 --- a/src/io/json.jl +++ b/src/io/json.jl @@ -14,7 +14,7 @@ Save a [`JSONModel`](@ref) in `model` to a JSON file `file_name`. In case the `model` is not `JSONModel`, it will be converted automatically. """ -function save_json_model(model::MetabolicModel, file_name::String) +function save_json_model(model::AbstractMetabolicModel, file_name::String) m = typeof(model) == JSONModel ? model : begin diff --git a/src/io/mat.jl b/src/io/mat.jl index 723975b1c..6f9b51d6b 100644 --- a/src/io/mat.jl +++ b/src/io/mat.jl @@ -22,7 +22,7 @@ In case the `model` is not `MATModel`, it will be converted automatically. `model_name` is the identifier name for the whole model written to the MATLAB file; defaults to just "model". """ -function save_mat_model(model::MetabolicModel, file_path::String; model_name = "model") +function save_mat_model(model::AbstractMetabolicModel, file_path::String; model_name = "model") m = typeof(model) == MATModel ? model : begin diff --git a/src/io/sbml.jl b/src/io/sbml.jl index 09085e59a..e61792b9e 100644 --- a/src/io/sbml.jl +++ b/src/io/sbml.jl @@ -13,7 +13,7 @@ $(TYPEDSIGNATURES) Write a given SBML model to `file_name`. """ -function save_sbml_model(model::MetabolicModel, file_name::String) +function save_sbml_model(model::AbstractMetabolicModel, file_name::String) m = typeof(model) == SBMLModel ? model : begin diff --git a/src/io/show/MetabolicModel.jl b/src/io/show/MetabolicModel.jl index b851e08ba..155aeb1cd 100644 --- a/src/io/show/MetabolicModel.jl +++ b/src/io/show/MetabolicModel.jl @@ -3,7 +3,7 @@ $(TYPEDSIGNATURES) Pretty printing of everything metabolic-modelish. """ -function Base.show(io::Base.IO, ::MIME"text/plain", m::MetabolicModel) +function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m))") if n_reactions(m) <= constants.default_stoich_show_size println(io, stoichiometry(m)) diff --git a/src/macros/is_xxx_reaction.jl b/src/macros/is_xxx_reaction.jl index f63ec514b..f5ec5118b 100644 --- a/src/macros/is_xxx_reaction.jl +++ b/src/macros/is_xxx_reaction.jl @@ -23,7 +23,7 @@ macro _is_reaction_fn(anno_id, identifiers) docstring = """ $fname( - model::MetabolicModel, + model::AbstractMetabolicModel, reaction_id::String; annotation_keys = ["sbo", "SBO"], ) @@ -41,7 +41,7 @@ macro _is_reaction_fn(anno_id, identifiers) docstring, :( $fname( - model::MetabolicModel, + model::AbstractMetabolicModel, reaction_id::String; annotation_keys = ["sbo", "SBO"], ) = $body diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index 7ffc59060..6de61158c 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -182,7 +182,7 @@ function join_with_exchanges( exchange_rxn_mets::Dict{String,String}; biomass_ids = String[], model_names = String[], -) where {M<:MetabolicModel} +) where {M<:AbstractMetabolicModel} exchange_rxn_ids = keys(exchange_rxn_mets) exchange_met_ids = values(exchange_rxn_mets) @@ -312,7 +312,7 @@ function join_with_exchanges( exchange_rxn_mets::Dict{String,String}; biomass_ids = [], model_names = [], -)::StandardModel where {M<:MetabolicModel} +)::StandardModel where {M<:AbstractMetabolicModel} community = StandardModel() rxns = OrderedDict{String,Reaction}() @@ -381,7 +381,7 @@ community = add_model_with_exchanges(community, """ function add_model_with_exchanges( community::CoreModel, - model::MetabolicModel, + model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", biomass_id = nothing, @@ -473,7 +473,7 @@ The `StandardModel` variant of [`add_model_with_exchanges`](@ref), but is in-pla """ function add_model_with_exchanges!( community::StandardModel, - model::MetabolicModel, + model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", biomass_id = nothing, @@ -525,7 +525,7 @@ The `StandardModel` variant of [`add_model_with_exchanges`](@ref). Makes a deepc """ function add_model_with_exchanges( community::StandardModel, - model::MetabolicModel, + model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", biomass_id = nothing, diff --git a/src/reconstruction/gecko.jl b/src/reconstruction/gecko.jl index f663f8333..4a49409f5 100644 --- a/src/reconstruction/gecko.jl +++ b/src/reconstruction/gecko.jl @@ -24,7 +24,7 @@ Alternatively, all function arguments may be also supplied as dictionaries that provide the same data lookup. """ function make_gecko_model( - model::MetabolicModel; + model::AbstractMetabolicModel; reaction_isozymes::Union{Function,Dict{String,Vector{Isozyme}}}, gene_product_bounds::Union{Function,Dict{String,Tuple{Float64,Float64}}}, gene_product_molar_mass::Union{Function,Dict{String,Float64}}, diff --git a/src/reconstruction/smoment.jl b/src/reconstruction/smoment.jl index 32c06c56d..88b4299ad 100644 --- a/src/reconstruction/smoment.jl +++ b/src/reconstruction/smoment.jl @@ -20,7 +20,7 @@ Alternatively, all function arguments also accept dictionaries that are used to provide the same data lookup. """ function make_smoment_model( - model::MetabolicModel; + model::AbstractMetabolicModel; reaction_isozyme::Union{Function,Dict{String,Isozyme}}, gene_product_molar_mass::Union{Function,Dict{String,Float64}}, total_enzyme_capacity::Float64, diff --git a/src/solver.jl b/src/solver.jl index aee1a8919..7a529cb11 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -3,7 +3,7 @@ module Solver Interface of COBREXA to JuMP solvers; mainly recreation of the -`MetabolicModel`s into JuMP optimization models. +`AbstractMetabolicModel`s into JuMP optimization models. # Exports $(EXPORTS) @@ -19,12 +19,12 @@ using JuMP """ $(TYPEDSIGNATURES) -Convert `MetabolicModel`s to a JuMP model, place objectives and the equality +Convert `AbstractMetabolicModel`s to a JuMP model, place objectives and the equality constraint. Here coupling means inequality constraints coupling multiple variables together. """ -function make_optimization_model(model::MetabolicModel, optimizer; sense = MAX_SENSE) +function make_optimization_model(model::AbstractMetabolicModel, optimizer; sense = MAX_SENSE) precache!(model) @@ -120,7 +120,7 @@ Returns a vector of fluxes of the model, if solved. flux_vector(flux_balance_analysis(model, ...)) ``` """ -flux_vector(model::MetabolicModel, opt_model)::Maybe{Vector{Float64}} = +flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = is_solved(opt_model) ? reaction_flux(model)' * value.(opt_model[:x]) : nothing """ @@ -133,7 +133,7 @@ Returns the fluxes of the model as a reaction-keyed dictionary, if solved. flux_dict(model, flux_balance_analysis(model, ...)) ``` """ -flux_dict(model::MetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = +flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = is_solved(opt_model) ? Dict(fluxes(model) .=> reaction_flux(model)' * value.(opt_model[:x])) : nothing @@ -147,7 +147,7 @@ A pipeable variant of `flux_dict`. flux_balance_analysis(model, ...) |> flux_dict(model) ``` """ -flux_dict(model::MetabolicModel) = opt_model -> flux_dict(model, opt_model) +flux_dict(model::AbstractMetabolicModel) = opt_model -> flux_dict(model, opt_model) @export_locals end diff --git a/src/types/abstract/MetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl similarity index 83% rename from src/types/abstract/MetabolicModel.jl rename to src/types/abstract/AbstractMetabolicModel.jl index 8b1fc4b77..1f28e7cd7 100644 --- a/src/types/abstract/MetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -1,25 +1,25 @@ """ - abstract type MetabolicModel end + abstract type AbstractMetabolicModel end A helper supertype of everything usable as a linear-like model for COBREXA functions. -If you want your model type to work with COBREXA, add the `MetabolicModel` as +If you want your model type to work with COBREXA, add the `AbstractMetabolicModel` as its supertype, and implement the accessor functions. Accessors [`reactions`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), [`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not mandatory and default to safe "empty" values. """ -abstract type MetabolicModel end +abstract type AbstractMetabolicModel end """ - abstract type ModelWrapper <: MetabolicModel end + abstract type ModelWrapper <: AbstractMetabolicModel end A helper supertype of all "wrapper" types that contain precisely one other -[`MetabolicModel`](@ref). +[`AbstractMetabolicModel`](@ref). """ -abstract type ModelWrapper <: MetabolicModel end +abstract type ModelWrapper <: AbstractMetabolicModel end const SparseMat = SparseMatrixCSC{Float64,Int} const SparseVec = SparseVector{Float64,Int} diff --git a/src/types/accessors/MetabolicModel.jl b/src/types/accessors/MetabolicModel.jl index a8d37b9a4..bd08fa09c 100644 --- a/src/types/accessors/MetabolicModel.jl +++ b/src/types/accessors/MetabolicModel.jl @@ -3,7 +3,7 @@ # IMPORTANT # # This file provides a list of "officially supported" accessors that should -# work with all subtypes of [`MetabolicModel`](@ref). Keep this synced with the +# work with all subtypes of [`AbstractMetabolicModel`](@ref). Keep this synced with the # automatically derived methods for [`ModelWrapper`](@ref). # @@ -19,7 +19,7 @@ modeling, such as metabolite exchanges, separate forward and reverse reactions, supplies of enzymatic and genetic material and virtual cell volume, etc. To simplify the view of the model contents use [`reaction_flux`](@ref). """ -function reactions(a::MetabolicModel)::Vector{String} +function reactions(a::AbstractMetabolicModel)::Vector{String} missing_impl_error(reactions, (a,)) end @@ -32,7 +32,7 @@ corresponds to the rows in [`stoichiometry`](@ref) matrix. As with [`reactions`](@ref)s, some metabolites in models may be virtual, representing purely technical equality constraints. """ -function metabolites(a::MetabolicModel)::Vector{String} +function metabolites(a::AbstractMetabolicModel)::Vector{String} missing_impl_error(metabolites, (a,)) end @@ -41,7 +41,7 @@ $(TYPEDSIGNATURES) Get the number of reactions in a model. """ -function n_reactions(a::MetabolicModel)::Int +function n_reactions(a::AbstractMetabolicModel)::Int length(reactions(a)) end @@ -50,7 +50,7 @@ $(TYPEDSIGNATURES) Get the number of metabolites in a model. """ -function n_metabolites(a::MetabolicModel)::Int +function n_metabolites(a::AbstractMetabolicModel)::Int length(metabolites(a)) end @@ -65,7 +65,7 @@ model `m` is defined as satisfying the equations: - `y .<= ubs` - `(lbs, ubs) == bounds(m) """ -function stoichiometry(a::MetabolicModel)::SparseMat +function stoichiometry(a::AbstractMetabolicModel)::SparseMat missing_impl_error(stoichiometry, (a,)) end @@ -74,7 +74,7 @@ $(TYPEDSIGNATURES) Get the lower and upper solution bounds of a model. """ -function bounds(a::MetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} +function bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} missing_impl_error(bounds, (a,)) end @@ -83,7 +83,7 @@ $(TYPEDSIGNATURES) Get the sparse balance vector of a model. """ -function balance(a::MetabolicModel)::SparseVec +function balance(a::AbstractMetabolicModel)::SparseVec return spzeros(n_metabolites(a)) end @@ -94,7 +94,7 @@ Get the objective vector of the model. Analysis functions, such as [`flux_balance_analysis`](@ref), are supposed to maximize `dot(objective, x)` where `x` is a feasible solution of the model. """ -function objective(a::MetabolicModel)::SparseVec +function objective(a::AbstractMetabolicModel)::SparseVec missing_impl_error(objective, (a,)) end @@ -110,11 +110,11 @@ flux is decomposed into individual reactions. By default (and in most models), fluxes and reactions perfectly correspond. """ -function fluxes(a::MetabolicModel)::Vector{String} +function fluxes(a::AbstractMetabolicModel)::Vector{String} reactions(a) end -function n_fluxes(a::MetabolicModel)::Int +function n_fluxes(a::AbstractMetabolicModel)::Int n_reactions(a) end @@ -126,7 +126,7 @@ linear system to the fluxes (see [`fluxes`](@ref) for rationale). Returns a sparse matrix of size `(n_reactions(a), n_fluxes(a))`. For most models, this is an identity matrix. """ -function reaction_flux(a::MetabolicModel)::SparseMat +function reaction_flux(a::AbstractMetabolicModel)::SparseMat nr = n_reactions(a) nf = n_fluxes(a) nr == nf || missing_impl_error(reaction_flux, (a,)) @@ -139,7 +139,7 @@ $(TYPEDSIGNATURES) Get a matrix of coupling constraint definitions of a model. By default, there is no coupling in the models. """ -function coupling(a::MetabolicModel)::SparseMat +function coupling(a::AbstractMetabolicModel)::SparseMat return spzeros(0, n_reactions(a)) end @@ -148,7 +148,7 @@ $(TYPEDSIGNATURES) Get the number of coupling constraints in a model. """ -function n_coupling_constraints(a::MetabolicModel)::Int +function n_coupling_constraints(a::AbstractMetabolicModel)::Int size(coupling(a), 1) end @@ -158,7 +158,7 @@ $(TYPEDSIGNATURES) Get the lower and upper bounds for each coupling bound in a model, as specified by `coupling`. By default, the model does not have any coupling bounds. """ -function coupling_bounds(a::MetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} +function coupling_bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} return (spzeros(0), spzeros(0)) end @@ -171,7 +171,7 @@ no genes. In SBML, these are usually called "gene products" but we write `genes` for simplicity. """ -function genes(a::MetabolicModel)::Vector{String} +function genes(a::AbstractMetabolicModel)::Vector{String} return [] end @@ -182,7 +182,7 @@ Return the number of genes in the model (as returned by [`genes`](@ref)). If you just need the number of the genes, this may be much more efficient than calling [`genes`](@ref) and measuring the array. """ -function n_genes(a::MetabolicModel)::Int +function n_genes(a::AbstractMetabolicModel)::Int return length(genes(a)) end @@ -196,7 +196,7 @@ For simplicity, `nothing` may be returned, meaning that the reaction always takes place. (in DNF, that would be equivalent to returning `[[]]`.) """ function reaction_gene_association( - a::MetabolicModel, + a::AbstractMetabolicModel, reaction_id::String, )::Maybe{GeneAssociation} return nothing @@ -208,7 +208,7 @@ $(TYPEDSIGNATURES) Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not, return `nothing`. """ -function reaction_subsystem(model::MetabolicModel, reaction_id::String)::Maybe{String} +function reaction_subsystem(model::AbstractMetabolicModel, reaction_id::String)::Maybe{String} return nothing end @@ -218,7 +218,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid` in the model. The dictionary maps the metabolite IDs to their stoichiometric coefficients. """ -function reaction_stoichiometry(m::MetabolicModel, rid::String)::Dict{String,Float64} +function reaction_stoichiometry(m::AbstractMetabolicModel, rid::String)::Dict{String,Float64} mets = metabolites(m) Dict( mets[k] => v for @@ -233,7 +233,7 @@ Return the formula of metabolite `metabolite_id` in `model`. Return `nothing` in case the formula is not known or irrelevant. """ function metabolite_formula( - model::MetabolicModel, + model::AbstractMetabolicModel, metabolite_id::String, )::Maybe{MetaboliteFormula} return nothing @@ -245,7 +245,7 @@ $(TYPEDSIGNATURES) Return the charge associated with metabolite `metabolite_id` in `model`. Returns `nothing` if charge not present. """ -function metabolite_charge(model::MetabolicModel, metabolite_id::String)::Maybe{Int} +function metabolite_charge(model::AbstractMetabolicModel, metabolite_id::String)::Maybe{Int} return nothing end @@ -255,7 +255,7 @@ $(TYPEDSIGNATURES) Return the compartment of metabolite `metabolite_id` in `model` if it is assigned. If not, return `nothing`. """ -function metabolite_compartment(model::MetabolicModel, metabolite_id::String)::Maybe{String} +function metabolite_compartment(model::AbstractMetabolicModel, metabolite_id::String)::Maybe{String} return nothing end @@ -266,7 +266,7 @@ Return standardized names that may help identifying the reaction. The dictionary assigns vectors of possible identifiers to identifier system names, e.g. `"Reactome" => ["reactomeID123"]`. """ -function reaction_annotations(a::MetabolicModel, reaction_id::String)::Annotations +function reaction_annotations(a::AbstractMetabolicModel, reaction_id::String)::Annotations return Dict() end @@ -277,7 +277,7 @@ Return standardized names that may help to reliably identify the metabolite. The dictionary assigns vectors of possible identifiers to identifier system names, e.g. `"ChEMBL" => ["123"]` or `"PubChem" => ["CID123", "CID654645645"]`. """ -function metabolite_annotations(a::MetabolicModel, metabolite_id::String)::Annotations +function metabolite_annotations(a::AbstractMetabolicModel, metabolite_id::String)::Annotations return Dict() end @@ -288,7 +288,7 @@ Return standardized names that identify the corresponding gene or product. The dictionary assigns vectors of possible identifiers to identifier system names, e.g. `"PDB" => ["PROT01"]`. """ -function gene_annotations(a::MetabolicModel, gene_id::String)::Annotations +function gene_annotations(a::AbstractMetabolicModel, gene_id::String)::Annotations return Dict() end @@ -297,7 +297,7 @@ $(TYPEDSIGNATURES) Return the notes associated with reaction `reaction_id` in `model`. """ -function reaction_notes(model::MetabolicModel, reaction_id::String)::Notes +function reaction_notes(model::AbstractMetabolicModel, reaction_id::String)::Notes return Dict() end @@ -306,7 +306,7 @@ $(TYPEDSIGNATURES) Return the notes associated with metabolite `reaction_id` in `model`. """ -function metabolite_notes(model::MetabolicModel, metabolite_id::String)::Notes +function metabolite_notes(model::AbstractMetabolicModel, metabolite_id::String)::Notes return Dict() end @@ -315,7 +315,7 @@ $(TYPEDSIGNATURES) Return the notes associated with the gene `gene_id` in `model`. """ -function gene_notes(model::MetabolicModel, gene_id::String)::Notes +function gene_notes(model::AbstractMetabolicModel, gene_id::String)::Notes return Dict() end @@ -324,21 +324,21 @@ $(TYPEDSIGNATURES) Return the name of reaction with ID `rid`. """ -reaction_name(model::MetabolicModel, rid::String) = nothing +reaction_name(model::AbstractMetabolicModel, rid::String) = nothing """ $(TYPEDSIGNATURES) Return the name of metabolite with ID `mid`. """ -metabolite_name(model::MetabolicModel, mid::String) = nothing +metabolite_name(model::AbstractMetabolicModel, mid::String) = nothing """ $(TYPEDSIGNATURES) Return the name of gene with ID `gid`. """ -gene_name(model::MetabolicModel, gid::String) = nothing +gene_name(model::AbstractMetabolicModel, gid::String) = nothing """ $(TYPEDSIGNATURES) @@ -354,6 +354,6 @@ are done in a good hope that the performance will be improved. By default, it should be safe to do nothing. """ -function precache!(a::MetabolicModel)::Nothing +function precache!(a::AbstractMetabolicModel)::Nothing nothing end diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 2cc07a01c..49e99535e 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -11,7 +11,7 @@ end # # IMPORTANT # -# The list of inherited functions must be synced with the methods available for [`MetabolicModel`](@ref). +# The list of inherited functions must be synced with the methods available for [`AbstractMetabolicModel`](@ref). # @inherit_model_methods_fn ModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! diff --git a/src/types/models/CoreModel.jl b/src/types/models/CoreModel.jl index e2686eee1..cd28793d0 100644 --- a/src/types/models/CoreModel.jl +++ b/src/types/models/CoreModel.jl @@ -13,7 +13,7 @@ s.t. S x = b # Fields $(TYPEDFIELDS) """ -mutable struct CoreModel <: MetabolicModel +mutable struct CoreModel <: AbstractMetabolicModel S::SparseMat b::SparseVec c::SparseVec @@ -147,7 +147,7 @@ $(TYPEDSIGNATURES) Make a `CoreModel` out of any compatible model type. """ -function Base.convert(::Type{CoreModel}, m::M) where {M<:MetabolicModel} +function Base.convert(::Type{CoreModel}, m::M) where {M<:AbstractMetabolicModel} if typeof(m) == CoreModel return m end diff --git a/src/types/models/CoreModelCoupled.jl b/src/types/models/CoreModelCoupled.jl index c07357a76..a33db6da5 100644 --- a/src/types/models/CoreModelCoupled.jl +++ b/src/types/models/CoreModelCoupled.jl @@ -11,7 +11,7 @@ flux `x` feasible in this model must satisfy: # Fields $(TYPEDFIELDS) """ -mutable struct CoreCoupling{M} <: ModelWrapper where {M<:MetabolicModel} +mutable struct CoreCoupling{M} <: ModelWrapper where {M<:AbstractMetabolicModel} lm::M C::SparseMat cl::Vector{Float64} @@ -22,7 +22,7 @@ mutable struct CoreCoupling{M} <: ModelWrapper where {M<:MetabolicModel} C::MatType, cl::VecType, cu::VecType, - ) where {M<:MetabolicModel} + ) where {M<:AbstractMetabolicModel} length(cu) == length(cl) || throw(DimensionMismatch("`cl` and `cu` need to have the same size")) size(C) == (length(cu), n_reactions(lm)) || @@ -69,7 +69,7 @@ Make a `CoreCoupling` out of any compatible model type. """ function Base.convert( ::Type{CoreCoupling{M}}, - mm::MetabolicModel; + mm::AbstractMetabolicModel; clone_coupling = true, ) where {M} if mm isa CoreCoupling{M} diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index 07debdc95..cf92ddc79 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -19,7 +19,7 @@ somewhere. # Fields $(TYPEDFIELDS) """ -mutable struct HDF5Model <: MetabolicModel +mutable struct HDF5Model <: AbstractMetabolicModel h5::Maybe{HDF5.File} filename::String diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 696add761..23b9572b6 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -24,7 +24,7 @@ reactions(model) # see the list of reactions # Fields $(TYPEDFIELDS) """ -struct JSONModel <: MetabolicModel +struct JSONModel <: AbstractMetabolicModel json::Dict{String,Any} rxn_index::Dict{String,Int} rxns::Vector{Any} @@ -291,9 +291,9 @@ Accessors.gene_name(model::JSONModel, gid::String) = """ $(TYPEDSIGNATURES) -Convert any [`MetabolicModel`](@ref) to [`JSONModel`](@ref). +Convert any [`AbstractMetabolicModel`](@ref) to [`JSONModel`](@ref). """ -function Base.convert(::Type{JSONModel}, mm::MetabolicModel) +function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) if typeof(mm) == JSONModel return mm end diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 20e22faeb..393e909c5 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -6,7 +6,7 @@ Wrapper around the models loaded in dictionaries from the MATLAB representation. # Fields $(TYPEDFIELDS) """ -struct MATModel <: MetabolicModel +struct MATModel <: AbstractMetabolicModel mat::Dict{String,Any} end @@ -225,7 +225,7 @@ $(TYPEDSIGNATURES) Convert any metabolic model to `MATModel`. """ -function Base.convert(::Type{MATModel}, m::MetabolicModel) +function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) if typeof(m) == MATModel return m end diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 71b7ce802..142a4a1d7 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -7,7 +7,7 @@ SBML to any other model format. # Fields $(TYPEDFIELDS) """ -struct SBMLModel <: MetabolicModel +struct SBMLModel <: AbstractMetabolicModel sbml::SBML.Model end @@ -278,7 +278,7 @@ $(TYPEDSIGNATURES) Convert any metabolic model to [`SBMLModel`](@ref). """ -function Base.convert(::Type{SBMLModel}, mm::MetabolicModel) +function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) if typeof(mm) == SBMLModel return mm end diff --git a/src/types/models/Serialized.jl b/src/types/models/Serialized.jl index b307c3d88..e5031fc0d 100644 --- a/src/types/models/Serialized.jl +++ b/src/types/models/Serialized.jl @@ -9,12 +9,12 @@ internal model will be loaded on-demand by using any accessor, or by calling # Fields $(TYPEDFIELDS) """ -mutable struct Serialized{M} <: ModelWrapper where {M<:MetabolicModel} +mutable struct Serialized{M} <: ModelWrapper where {M<:AbstractMetabolicModel} m::Maybe{M} filename::String Serialized{T}(filename::String) where {T} = new{T}(nothing, filename) - Serialized(model::T, filename::String) where {T<:MetabolicModel} = + Serialized(model::T, filename::String) where {T<:AbstractMetabolicModel} = new{T}(model, filename) end diff --git a/src/types/models/StandardModel.jl b/src/types/models/StandardModel.jl index 05f0915e8..73babf1e3 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/StandardModel.jl @@ -35,7 +35,7 @@ keys(model.reactions) # Fields $(TYPEDFIELDS) """ -mutable struct StandardModel <: MetabolicModel +mutable struct StandardModel <: AbstractMetabolicModel id::String reactions::OrderedDict{String,Reaction} metabolites::OrderedDict{String,Metabolite} @@ -49,7 +49,7 @@ mutable struct StandardModel <: MetabolicModel ) = new(id, reactions, metabolites, genes) end -# MetabolicModel interface follows +# AbstractMetabolicModel interface follows """ $(TYPEDSIGNATURES) @@ -296,11 +296,11 @@ Accessors.gene_name(m::StandardModel, gid::String) = m.genes[gid].name """ $(TYPEDSIGNATURES) -Convert any `MetabolicModel` into a `StandardModel`. +Convert any `AbstractMetabolicModel` into a `StandardModel`. Note, some data loss may occur since only the generic interface is used during the conversion process. """ -function Base.convert(::Type{StandardModel}, model::MetabolicModel) +function Base.convert(::Type{StandardModel}, model::AbstractMetabolicModel) if typeof(model) == StandardModel return model end diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index 8507680e1..09769b115 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -81,7 +81,7 @@ struct GeckoModel <: ModelWrapper coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} coupling_row_mass_group::Vector{_GeckoCapacity} - inner::MetabolicModel + inner::AbstractMetabolicModel end Accessors.unwrap_model(model::GeckoModel) = model.inner diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 305e6dcd9..545fe9d61 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -36,7 +36,7 @@ The model is constructed as follows: maximum. The `SMomentModel` structure contains a worked-out representation of the -optimization problem atop a wrapped [`MetabolicModel`](@ref), in particular the +optimization problem atop a wrapped [`AbstractMetabolicModel`](@ref), in particular the separation of certain reactions into unidirectional forward and reverse parts, an "enzyme capacity" required for each reaction, and the value of the maximum capacity constraint. Original coupling in the inner model is retained. @@ -58,7 +58,7 @@ struct SMomentModel <: ModelWrapper columns::Vector{_SMomentColumn} total_enzyme_capacity::Float64 - inner::MetabolicModel + inner::AbstractMetabolicModel end Accessors.unwrap_model(model::SMomentModel) = model.inner diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl index 011b732b9..62d145b93 100644 --- a/src/utils/Reaction.jl +++ b/src/utils/Reaction.jl @@ -49,7 +49,7 @@ function is_boundary(rxn_dict::Dict{String,Float64})::Bool length(keys(rxn_dict)) == 1 end -is_boundary(model::MetabolicModel, rxn_id::String) = +is_boundary(model::AbstractMetabolicModel, rxn_id::String) = is_boundary(reaction_stoichiometry(model, rxn_id)) is_boundary(rxn::Reaction) = is_boundary(rxn.metabolites) @@ -65,7 +65,7 @@ dictionary, a reaction string id or a `Reaction` as an argument for `rxn`. See also: [`reaction_mass_balanced`](@ref) """ -function reaction_atom_balance(model::MetabolicModel, reaction_dict::Dict{String,Float64}) +function reaction_atom_balance(model::AbstractMetabolicModel, reaction_dict::Dict{String,Float64}) atom_balances = Dict{String,Float64}() for (met, stoich_rxn) in reaction_dict adict = metabolite_formula(model, met) @@ -79,7 +79,7 @@ function reaction_atom_balance(model::MetabolicModel, reaction_dict::Dict{String return atom_balances end -function reaction_atom_balance(model::MetabolicModel, rxn_id::String) +function reaction_atom_balance(model::AbstractMetabolicModel, rxn_id::String) reaction_atom_balance(model, reaction_stoichiometry(model, rxn_id)) end diff --git a/src/utils/Serialized.jl b/src/utils/Serialized.jl index 577d101c4..80a7c2c0a 100644 --- a/src/utils/Serialized.jl +++ b/src/utils/Serialized.jl @@ -16,7 +16,7 @@ the `Serialization` package directly. function serialize_model( model::MM, filename::String, -)::Serialized{MM} where {MM<:MetabolicModel} +)::Serialized{MM} where {MM<:AbstractMetabolicModel} open(f -> serialize(f, model), filename, "w") Serialized{MM}(filename) end diff --git a/src/utils/fluxes.jl b/src/utils/fluxes.jl index cf23a3f9d..fd4651196 100644 --- a/src/utils/fluxes.jl +++ b/src/utils/fluxes.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Return two dictionaries of metabolite `id`s mapped to reactions that consume or produce them, given the flux distribution supplied in `flux_dict`. """ -function metabolite_fluxes(model::MetabolicModel, flux_dict::Dict{String,Float64}) +function metabolite_fluxes(model::AbstractMetabolicModel, flux_dict::Dict{String,Float64}) S = stoichiometry(model) rids = reactions(model) mids = metabolites(model) @@ -51,7 +51,7 @@ delete!(fluxes, "BIOMASS_Ecoli_core_w_GAM") atom_fluxes(model, fluxes)["C"] ``` """ -function atom_fluxes(model::MetabolicModel, reaction_fluxes::Dict{String,Float64}) +function atom_fluxes(model::AbstractMetabolicModel, reaction_fluxes::Dict{String,Float64}) rids = reactions(model) atom_flux = Dict{String,Float64}() for (ridx, rid) in enumerate(rids) diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index 70d7f702e..fbfe8abf5 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -42,7 +42,7 @@ $(TYPEDSIGNATURES) Shortcut for finding exchange reaction indexes in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ -find_exchange_reactions(m::MetabolicModel; kwargs...) = +find_exchange_reactions(m::AbstractMetabolicModel; kwargs...) = findall(id -> looks_like_exchange_reaction(id; kwargs...), reactions(m)) """ @@ -51,7 +51,7 @@ $(TYPEDSIGNATURES) Shortcut for finding exchange reaction identifiers in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ -find_exchange_reaction_ids(m::MetabolicModel; kwargs...) = +find_exchange_reaction_ids(m::AbstractMetabolicModel; kwargs...) = filter(id -> looks_like_exchange_reaction(id, kwargs...), reactions(m)) """ @@ -90,7 +90,7 @@ $(TYPEDSIGNATURES) Shortcut for finding biomass reaction indexes in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ -find_biomass_reactions(m::MetabolicModel; kwargs...) = +find_biomass_reactions(m::AbstractMetabolicModel; kwargs...) = findall(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) """ @@ -99,7 +99,7 @@ $(TYPEDSIGNATURES) Shortcut for finding biomass reaction identifiers in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ -find_biomass_reaction_ids(m::MetabolicModel; kwargs...) = +find_biomass_reaction_ids(m::AbstractMetabolicModel; kwargs...) = filter(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) """ @@ -128,7 +128,7 @@ $(TYPEDSIGNATURES) Shortcut for finding extracellular metabolite indexes in a model; arguments are forwarded to [`looks_like_extracellular_metabolite`](@ref). """ -find_extracellular_metabolites(m::MetabolicModel; kwargs...) = +find_extracellular_metabolites(m::AbstractMetabolicModel; kwargs...) = findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) """ @@ -137,7 +137,7 @@ $(TYPEDSIGNATURES) Shortcut for finding extracellular metabolite identifiers in a model; arguments are forwarded to [`looks_like_extracellular_metabolite`](@ref). """ -find_extracellular_metabolite_ids(m::MetabolicModel; kwargs...) = +find_extracellular_metabolite_ids(m::AbstractMetabolicModel; kwargs...) = findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) @_is_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS diff --git a/test/types/abstract/MetabolicModel.jl b/test/types/abstract/MetabolicModel.jl index 59f2ea088..29abf7cba 100644 --- a/test/types/abstract/MetabolicModel.jl +++ b/test/types/abstract/MetabolicModel.jl @@ -1,5 +1,5 @@ -struct FakeModel <: MetabolicModel +struct FakeModel <: AbstractMetabolicModel dummy::Int end From bdc61337c4922c3d611f483263b407fddb45bf1c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 25 Oct 2022 13:27:05 +0200 Subject: [PATCH 038/531] also rename ModelWrapper --- docs/src/concepts/4_wrappers.md | 10 +++++----- src/reconstruction/CoreModelCoupled.jl | 2 +- src/types/abstract/AbstractMetabolicModel.jl | 4 ++-- src/types/accessors/MetabolicModel.jl | 2 +- src/types/accessors/ModelWrapper.jl | 10 +++++----- src/types/models/CoreModelCoupled.jl | 2 +- src/types/models/Serialized.jl | 2 +- src/types/wrappers/GeckoModel.jl | 2 +- src/types/wrappers/SMomentModel.jl | 2 +- 9 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index 9d0c0d462..d5657c850 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -8,7 +8,7 @@ small layers that add or change the functionality of a given base models. Types [`Serialized`](@ref), [`CoreCoupling`](@ref), [`SMomentModel`](@ref), and [`GeckoModel`](@ref) all work in this manner -- add some extra functionality to the "base". Technically, they are all subtypes of the abstract type -[`ModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) +[`AbstractModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) and can thus be used in all standard analysis functions. Similarly, the model wraps can be stacked -- it is easy to e.g. serialize a [`GeckoModel`](@ref), or to add coupling to an existing [`SMomentModel`](@ref). @@ -26,7 +26,7 @@ and fast -- we just discard the wrapper. ## Writing a model wrapper Creating a model wrapper structure is simple -- by declaring it a subtype of -[`ModelWrapper`](@ref) and implementing a single function +[`AbstractModelWrapper`](@ref) and implementing a single function [`unwrap_model`](@ref), we get default implementations of all accessors that should work for any [`AbstractMetabolicModel`](@ref). @@ -34,7 +34,7 @@ As a technical example, we may make a minimal model wrapper that does not do anything: ```julia -struct IdentityWrap <: ModelWrapper +struct IdentityWrap <: AbstractModelWrapper mdl::AbstractMetabolicModel end @@ -60,7 +60,7 @@ a constant factor. This can be used to e.g. simulate higher or lower abundance of certain organism in a model. ```julia -struct RateChangedModel <: ModelWrapper +struct RateChangedModel <: AbstractModelWrapper factor::Float64 mdl::AbstractMetabolicModel end @@ -91,7 +91,7 @@ not quite realistic, but may be useful to validate the mathematical robustness of the models. ```julia -struct LeakyModel <: ModelWrapper +struct LeakyModel <: AbstractModelWrapper leaking_metabolites::Vector{String} leak_rate::Float64 mdl::AbstractMetabolicModel diff --git a/src/reconstruction/CoreModelCoupled.jl b/src/reconstruction/CoreModelCoupled.jl index 147cfab89..ec5b26e7a 100644 --- a/src/reconstruction/CoreModelCoupled.jl +++ b/src/reconstruction/CoreModelCoupled.jl @@ -254,7 +254,7 @@ function change_coupling_bounds!( nothing end -# TODO see if some of these can be derived from ModelWrapper +# TODO see if some of these can be derived from AbstractModelWrapper @_change_bounds_fn CoreCoupling Int inplace begin change_bound!(model.lm, rxn_idx, lower = lower, upper = upper) end diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index 1f28e7cd7..8dfe81506 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -14,12 +14,12 @@ mandatory and default to safe "empty" values. abstract type AbstractMetabolicModel end """ - abstract type ModelWrapper <: AbstractMetabolicModel end + abstract type AbstractModelWrapper <: AbstractMetabolicModel end A helper supertype of all "wrapper" types that contain precisely one other [`AbstractMetabolicModel`](@ref). """ -abstract type ModelWrapper <: AbstractMetabolicModel end +abstract type AbstractModelWrapper <: AbstractMetabolicModel end const SparseMat = SparseMatrixCSC{Float64,Int} const SparseVec = SparseVector{Float64,Int} diff --git a/src/types/accessors/MetabolicModel.jl b/src/types/accessors/MetabolicModel.jl index bd08fa09c..6ee2e7f5f 100644 --- a/src/types/accessors/MetabolicModel.jl +++ b/src/types/accessors/MetabolicModel.jl @@ -4,7 +4,7 @@ # # This file provides a list of "officially supported" accessors that should # work with all subtypes of [`AbstractMetabolicModel`](@ref). Keep this synced with the -# automatically derived methods for [`ModelWrapper`](@ref). +# automatically derived methods for [`AbstractModelWrapper`](@ref). # """ diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 49e99535e..e059b0efc 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) A simple helper to pick a single wrapped model """ -function unwrap_model(a::ModelWrapper) +function unwrap_model(a::AbstractModelWrapper) missing_impl_error(unwrap_model, (a,)) end @@ -14,10 +14,10 @@ end # The list of inherited functions must be synced with the methods available for [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn ModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! -@inherit_model_methods_fn ModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes +@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes -@inherit_model_methods_fn ModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes +@inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes -@inherit_model_methods_fn ModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes +@inherit_model_methods_fn AbstractModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes diff --git a/src/types/models/CoreModelCoupled.jl b/src/types/models/CoreModelCoupled.jl index a33db6da5..550c8ade5 100644 --- a/src/types/models/CoreModelCoupled.jl +++ b/src/types/models/CoreModelCoupled.jl @@ -11,7 +11,7 @@ flux `x` feasible in this model must satisfy: # Fields $(TYPEDFIELDS) """ -mutable struct CoreCoupling{M} <: ModelWrapper where {M<:AbstractMetabolicModel} +mutable struct CoreCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} lm::M C::SparseMat cl::Vector{Float64} diff --git a/src/types/models/Serialized.jl b/src/types/models/Serialized.jl index e5031fc0d..0eb9d0bcc 100644 --- a/src/types/models/Serialized.jl +++ b/src/types/models/Serialized.jl @@ -9,7 +9,7 @@ internal model will be loaded on-demand by using any accessor, or by calling # Fields $(TYPEDFIELDS) """ -mutable struct Serialized{M} <: ModelWrapper where {M<:AbstractMetabolicModel} +mutable struct Serialized{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} m::Maybe{M} filename::String diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index 09769b115..ca427500e 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -74,7 +74,7 @@ The related constraints are implemented using [`coupling`](@ref) and # Fields $(TYPEDFIELDS) """ -struct GeckoModel <: ModelWrapper +struct GeckoModel <: AbstractModelWrapper objective::SparseVec columns::Vector{_GeckoReactionColumn} coupling_row_reaction::Vector{Int} diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 545fe9d61..a14be4245 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -54,7 +54,7 @@ are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). # Fields $(TYPEDFIELDS) """ -struct SMomentModel <: ModelWrapper +struct SMomentModel <: AbstractModelWrapper columns::Vector{_SMomentColumn} total_enzyme_capacity::Float64 From dabdf4c89666cab5846a428f6e01af787a5fae86 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 25 Oct 2022 13:32:47 +0200 Subject: [PATCH 039/531] formatting --- src/analysis/envelopes.jl | 18 ++++++++++------- src/analysis/minimize_metabolic_adjustment.jl | 14 +++++++++---- src/io/mat.jl | 6 +++++- src/solver.jl | 6 +++++- src/types/accessors/MetabolicModel.jl | 20 +++++++++++++++---- src/utils/Reaction.jl | 5 ++++- 6 files changed, 51 insertions(+), 18 deletions(-) diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl index 57be74164..11f5eff10 100644 --- a/src/analysis/envelopes.jl +++ b/src/analysis/envelopes.jl @@ -32,13 +32,17 @@ $(TYPEDSIGNATURES) Version of [`objective_envelope`](@ref) that works on string reaction IDs instead of integer indexes. """ -objective_envelope(model::AbstractMetabolicModel, rids::Vector{String}, args...; kwargs...) = - objective_envelope( - model, - Vector{Int}(indexin(rids, reactions(model))), - args...; - kwargs..., - ) +objective_envelope( + model::AbstractMetabolicModel, + rids::Vector{String}, + args...; + kwargs..., +) = objective_envelope( + model, + Vector{Int}(indexin(rids, reactions(model))), + args...; + kwargs..., +) """ $(TYPEDSIGNATURES) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 4f042a2d8..ad33a4cd7 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -85,8 +85,11 @@ same order as the reactions in `model`. Arguments are forwarded to This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ -minimize_metabolic_adjustment_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = - flux_vector(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) +minimize_metabolic_adjustment_analysis_vec( + model::AbstractMetabolicModel, + args...; + kwargs..., +) = flux_vector(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -98,5 +101,8 @@ internally. This function is kept for backwards compatibility, use [`flux_vector`](@ref) instead. """ -minimize_metabolic_adjustment_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = - flux_dict(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) +minimize_metabolic_adjustment_analysis_dict( + model::AbstractMetabolicModel, + args...; + kwargs..., +) = flux_dict(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) diff --git a/src/io/mat.jl b/src/io/mat.jl index 6f9b51d6b..eeb4d82c9 100644 --- a/src/io/mat.jl +++ b/src/io/mat.jl @@ -22,7 +22,11 @@ In case the `model` is not `MATModel`, it will be converted automatically. `model_name` is the identifier name for the whole model written to the MATLAB file; defaults to just "model". """ -function save_mat_model(model::AbstractMetabolicModel, file_path::String; model_name = "model") +function save_mat_model( + model::AbstractMetabolicModel, + file_path::String; + model_name = "model", +) m = typeof(model) == MATModel ? model : begin diff --git a/src/solver.jl b/src/solver.jl index 7a529cb11..1b7383a68 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -24,7 +24,11 @@ constraint. Here coupling means inequality constraints coupling multiple variables together. """ -function make_optimization_model(model::AbstractMetabolicModel, optimizer; sense = MAX_SENSE) +function make_optimization_model( + model::AbstractMetabolicModel, + optimizer; + sense = MAX_SENSE, +) precache!(model) diff --git a/src/types/accessors/MetabolicModel.jl b/src/types/accessors/MetabolicModel.jl index 6ee2e7f5f..75df6273c 100644 --- a/src/types/accessors/MetabolicModel.jl +++ b/src/types/accessors/MetabolicModel.jl @@ -208,7 +208,10 @@ $(TYPEDSIGNATURES) Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not, return `nothing`. """ -function reaction_subsystem(model::AbstractMetabolicModel, reaction_id::String)::Maybe{String} +function reaction_subsystem( + model::AbstractMetabolicModel, + reaction_id::String, +)::Maybe{String} return nothing end @@ -218,7 +221,10 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid` in the model. The dictionary maps the metabolite IDs to their stoichiometric coefficients. """ -function reaction_stoichiometry(m::AbstractMetabolicModel, rid::String)::Dict{String,Float64} +function reaction_stoichiometry( + m::AbstractMetabolicModel, + rid::String, +)::Dict{String,Float64} mets = metabolites(m) Dict( mets[k] => v for @@ -255,7 +261,10 @@ $(TYPEDSIGNATURES) Return the compartment of metabolite `metabolite_id` in `model` if it is assigned. If not, return `nothing`. """ -function metabolite_compartment(model::AbstractMetabolicModel, metabolite_id::String)::Maybe{String} +function metabolite_compartment( + model::AbstractMetabolicModel, + metabolite_id::String, +)::Maybe{String} return nothing end @@ -277,7 +286,10 @@ Return standardized names that may help to reliably identify the metabolite. The dictionary assigns vectors of possible identifiers to identifier system names, e.g. `"ChEMBL" => ["123"]` or `"PubChem" => ["CID123", "CID654645645"]`. """ -function metabolite_annotations(a::AbstractMetabolicModel, metabolite_id::String)::Annotations +function metabolite_annotations( + a::AbstractMetabolicModel, + metabolite_id::String, +)::Annotations return Dict() end diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl index 62d145b93..d58481619 100644 --- a/src/utils/Reaction.jl +++ b/src/utils/Reaction.jl @@ -65,7 +65,10 @@ dictionary, a reaction string id or a `Reaction` as an argument for `rxn`. See also: [`reaction_mass_balanced`](@ref) """ -function reaction_atom_balance(model::AbstractMetabolicModel, reaction_dict::Dict{String,Float64}) +function reaction_atom_balance( + model::AbstractMetabolicModel, + reaction_dict::Dict{String,Float64}, +) atom_balances = Dict{String,Float64}() for (met, stoich_rxn) in reaction_dict adict = metabolite_formula(model, met) From c5c0c7336c9380d8aa90507f4892cb6261cc3b8c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 25 Oct 2022 13:53:57 +0200 Subject: [PATCH 040/531] megarename! StandardModel -> ObjectModel CoreModel -> MatrixModel CoreCoupling -> MatrixCoupling CodeModelCoupled -> MatrixModelWithCoupling (to be further deprecated) + file moves --- docs/src/concepts/1_screen.md | 6 +- docs/src/concepts/3_custom_models.md | 4 +- docs/src/concepts/4_wrappers.md | 2 +- docs/src/examples/02_convert_save.jl | 18 +-- docs/src/examples/03_exploring.jl | 16 +-- docs/src/examples/03b_accessors.jl | 6 +- docs/src/examples/04_core_model.jl | 22 ++-- docs/src/examples/04_standardmodel.jl | 46 ++++---- .../04b_standardmodel_construction.jl | 8 +- docs/src/examples/07_restricting_reactions.jl | 2 +- docs/src/examples/11_growth.jl | 2 +- docs/src/examples/13_moma.jl | 2 +- docs/src/quickstart.md | 2 +- docs/src/quickstart.md.template | 2 +- src/analysis/screening.jl | 4 +- src/io/io.jl | 2 +- ...olicModel.jl => AbstractMetabolicModel.jl} | 0 ...{CoreModelCoupled.jl => MatrixCoupling.jl} | 98 ++++++++--------- .../{CoreModel.jl => MatrixModel.jl} | 80 +++++++------- .../{StandardModel.jl => ObjectModel.jl} | 44 ++++---- src/reconstruction/community.jl | 46 ++++---- src/types/Reaction.jl | 2 +- ...olicModel.jl => AbstractMetabolicModel.jl} | 0 .../misc/{CoreModel.jl => MatrixModel.jl} | 10 +- .../misc/{StandardModel.jl => ObjectModel.jl} | 8 +- src/types/models/CoreModelCoupled.jl | 103 ------------------ src/types/models/MatrixCoupling.jl | 103 ++++++++++++++++++ .../models/{CoreModel.jl => MatrixModel.jl} | 52 ++++----- .../{StandardModel.jl => ObjectModel.jl} | 72 ++++++------ src/utils/Reaction.jl | 10 +- test/analysis/flux_balance_analysis.jl | 14 +-- test/analysis/flux_variability_analysis.jl | 6 +- test/analysis/knockouts.jl | 6 +- .../parsimonious_flux_balance_analysis.jl | 2 +- test/analysis/sampling/affine_hit_and_run.jl | 2 +- test/analysis/sampling/warmup_variability.jl | 2 +- test/analysis/screening.jl | 2 +- test/data_static.jl | 14 +-- test/io/h5.jl | 2 +- test/io/io.jl | 2 +- test/io/json.jl | 8 +- test/io/mat.jl | 10 +- test/io/sbml.jl | 8 +- ...{CoreModelCoupled.jl => MatrixCoupling.jl} | 18 +-- .../{CoreModel.jl => MatrixModel.jl} | 4 +- .../{StandardModel.jl => ObjectModel.jl} | 2 +- test/reconstruction/Reaction.jl | 2 +- test/reconstruction/add_reactions.jl | 2 +- test/reconstruction/community.jl | 40 +++---- .../gapfill_minimum_reactions.jl | 2 +- test/reconstruction/gecko.jl | 4 +- test/reconstruction/knockouts.jl | 4 +- test/reconstruction/smoment.jl | 2 +- test/types/Gene.jl | 2 +- test/types/JSONModel.jl | 2 +- test/types/MATModel.jl | 2 +- ...{CoreModelCoupled.jl => MatrixCoupling.jl} | 12 +- test/types/{CoreModel.jl => MatrixModel.jl} | 12 +- .../{StandardModel.jl => ObjectModel.jl} | 10 +- test/types/SBMLModel.jl | 2 +- ...olicModel.jl => AbstractMetabolicModel.jl} | 0 test/utils/{CoreModel.jl => MatrixModel.jl} | 2 +- .../{StandardModel.jl => ObjectModel.jl} | 4 +- test/utils/Serialized.jl | 12 +- test/utils/fluxes.jl | 2 +- test/utils/looks_like.jl | 12 +- test/utils/reaction.jl | 2 +- 67 files changed, 503 insertions(+), 503 deletions(-) rename src/io/show/{MetabolicModel.jl => AbstractMetabolicModel.jl} (100%) rename src/reconstruction/{CoreModelCoupled.jl => MatrixCoupling.jl} (72%) rename src/reconstruction/{CoreModel.jl => MatrixModel.jl} (81%) rename src/reconstruction/{StandardModel.jl => ObjectModel.jl} (79%) rename src/types/accessors/{MetabolicModel.jl => AbstractMetabolicModel.jl} (100%) rename src/types/misc/{CoreModel.jl => MatrixModel.jl} (52%) rename src/types/misc/{StandardModel.jl => ObjectModel.jl} (87%) delete mode 100644 src/types/models/CoreModelCoupled.jl create mode 100644 src/types/models/MatrixCoupling.jl rename src/types/models/{CoreModel.jl => MatrixModel.jl} (62%) rename src/types/models/{StandardModel.jl => ObjectModel.jl} (76%) rename test/reconstruction/{CoreModelCoupled.jl => MatrixCoupling.jl} (92%) rename test/reconstruction/{CoreModel.jl => MatrixModel.jl} (98%) rename test/reconstruction/{StandardModel.jl => ObjectModel.jl} (99%) rename test/types/{CoreModelCoupled.jl => MatrixCoupling.jl} (51%) rename test/types/{CoreModel.jl => MatrixModel.jl} (55%) rename test/types/{StandardModel.jl => ObjectModel.jl} (94%) rename test/types/abstract/{MetabolicModel.jl => AbstractMetabolicModel.jl} (100%) rename test/utils/{CoreModel.jl => MatrixModel.jl} (90%) rename test/utils/{StandardModel.jl => ObjectModel.jl} (88%) diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md index e3023b582..60bdc182c 100644 --- a/docs/src/concepts/1_screen.md +++ b/docs/src/concepts/1_screen.md @@ -14,7 +14,7 @@ of [`screen`](@ref) that is called [`screen_variants`](@ref), which works as follows: ```julia -m = load_model(StandardModel, "e_coli_core.json") +m = load_model(ObjectModel, "e_coli_core.json") screen_variants( m, # the model for screening @@ -162,7 +162,7 @@ model where this change is easy to perform (generally, not all variants may be feasible on all model types). ```julia -with_disabled_oxygen_transport = (model::StandardModel) -> begin +with_disabled_oxygen_transport = (model::ObjectModel) -> begin # make "as shallow as possible" copy of the `model`. # Utilizing `deepcopy` is also possible, but inefficient. @@ -180,7 +180,7 @@ Finally, the whole definition may be parameterized as a normal function. The following variant removes any user-selected reaction: ```julia -with_disabled_reaction(reaction_id) = (model::StandardModel) -> begin +with_disabled_reaction(reaction_id) = (model::ObjectModel) -> begin new_model = copy(model) new_model.reactions = copy(model.reactions) delete!(new_model.reactions, reaction_id) # use the parameter from the specification diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index 99ba6de9c..418e788d3 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -84,8 +84,8 @@ function COBREXA.stoichiometry(m::CircularModel) end ``` -You may check that the result now works just as with [`CoreModel`](@ref) and -[`StandardModel`](@ref): +You may check that the result now works just as with [`MatrixModel`](@ref) and +[`ObjectModel`](@ref): ```julia julia> m = CircularModel(5) diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index d5657c850..02d10c69b 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -5,7 +5,7 @@ To simplify doing (and undoing) simple modifications to the existing model structure, COBREXA.jl supports a class of model _wrappers_, which are basically small layers that add or change the functionality of a given base models. -Types [`Serialized`](@ref), [`CoreCoupling`](@ref), [`SMomentModel`](@ref), and +Types [`Serialized`](@ref), [`MatrixCoupling`](@ref), [`SMomentModel`](@ref), and [`GeckoModel`](@ref) all work in this manner -- add some extra functionality to the "base". Technically, they are all subtypes of the abstract type [`AbstractModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) diff --git a/docs/src/examples/02_convert_save.jl b/docs/src/examples/02_convert_save.jl index 85fced321..564a38acd 100644 --- a/docs/src/examples/02_convert_save.jl +++ b/docs/src/examples/02_convert_save.jl @@ -29,12 +29,12 @@ save_model(sbml_model, "converted_e_coli.mat") # package `Serialization`. # # This way, you can use `serialize` to save any model format (even the -# complicated [`StandardModel`](@ref), which does not have a "native" file format +# complicated [`ObjectModel`](@ref), which does not have a "native" file format # representation): using Serialization -sm = convert(StandardModel, sbml_model) +sm = convert(ObjectModel, sbml_model) open(f -> serialize(f, sm), "myModel.stdmodel", "w") @@ -59,23 +59,23 @@ t = @elapsed deserialize("myModel.stdmodel") # ## Converting and saving a modified model # To modify the models easily, it is useful to convert them to a format that -# simplifies this modification. You may use e.g. [`CoreModel`](@ref) that +# simplifies this modification. You may use e.g. [`MatrixModel`](@ref) that # exposes the usual matrix-and-vectors structure of models as used in MATLAB -# COBRA implementations, and [`StandardModel`](@ref) that contains structures, +# COBRA implementations, and [`ObjectModel`](@ref) that contains structures, # lists and dictionaries of model contents, as typical in Python COBRA -# implementations. The object-oriented nature of [`StandardModel`](@ref) is +# implementations. The object-oriented nature of [`ObjectModel`](@ref) is # better for making small modifications that utilize known identifiers of model # contents. # -# Conversion of any model to [`StandardModel`](@ref) can be performed using the +# Conversion of any model to [`ObjectModel`](@ref) can be performed using the # standard Julia `convert`: -sm = convert(StandardModel, sbml_model) +sm = convert(ObjectModel, sbml_model) # The conversion can be also achieved right away when loading the model, using # an extra parameter of [`load_model`](@ref): -sm = load_model(StandardModel, "e_coli_core.json") +sm = load_model(ObjectModel, "e_coli_core.json") # As an example, we change an upper bound on one of the reactions: @@ -87,5 +87,5 @@ sm.reactions["PFK"].ub = 10.0 save_model(sm, "modified_e_coli.json") save_model(sm, "modified_e_coli.mat") -# More information about [`StandardModel`](@ref) internals is available [in a +# More information about [`ObjectModel`](@ref) internals is available [in a # separate example](04_standardmodel.md). diff --git a/docs/src/examples/03_exploring.jl b/docs/src/examples/03_exploring.jl index d8659084a..75a8bbd0e 100644 --- a/docs/src/examples/03_exploring.jl +++ b/docs/src/examples/03_exploring.jl @@ -5,25 +5,25 @@ # and [`SBMLModel`](@ref)), and ones that are more easily accessible for users # and mimic the usual workflows in COBRA methodology: # -# - [`StandardModel`](@ref), which contains and object-oriented representation +# - [`ObjectModel`](@ref), which contains and object-oriented representation # of model internals, built out of [`Reaction`](@ref), [`Metabolite`](@ref) # and [`Gene`](@ref) structures, in a way similar to e.g. # [COBRApy](https://github.com/opencobra/cobrapy/) -# - [`CoreModel`](@ref), which contains array-oriented representation of the +# - [`MatrixModel`](@ref), which contains array-oriented representation of the # model structures, such as stoichiometry matrix and the bounds vector, in a # way similar to e.g. [COBRA # toolbox](https://github.com/opencobra/cobratoolbox) -# The fields in [`StandardModel`](@ref) structure can be discovered using `fieldnames` as follows: +# The fields in [`ObjectModel`](@ref) structure can be discovered using `fieldnames` as follows: using COBREXA -fieldnames(StandardModel) +fieldnames(ObjectModel) !isfile("e_coli_core.json") && download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); -sm = load_model(StandardModel, "e_coli_core.json") +sm = load_model(ObjectModel, "e_coli_core.json") typeof(sm.reactions) fieldnames(Reaction) @@ -40,11 +40,11 @@ sm.reactions["TALA"].subsystem # sm.reactions["TALA"].ub #upper rate bound -# The same applies to [`CoreModel`](@ref): +# The same applies to [`MatrixModel`](@ref): -fieldnames(CoreModel) +fieldnames(MatrixModel) # -cm = load_model(CoreModel, "e_coli_core.json") +cm = load_model(MatrixModel, "e_coli_core.json") # cm.S # diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl index 4c58aae67..1af182175 100644 --- a/docs/src/examples/03b_accessors.jl +++ b/docs/src/examples/03b_accessors.jl @@ -7,8 +7,8 @@ # functions can work on any data. # For example, you can check the reactions and metabolites contained in any -# model type ([`SBMLModel`](@ref), [`JSONModel`](@ref), [`CoreModel`](@ref), -# [`StandardModel`](@ref), and any other) using the same accessor: +# model type ([`SBMLModel`](@ref), [`JSONModel`](@ref), [`MatrixModel`](@ref), +# [`ObjectModel`](@ref), and any other) using the same accessor: using COBREXA @@ -18,7 +18,7 @@ using COBREXA js = load_model("e_coli_core.json") reactions(js) # -std = convert(CoreModel, js) +std = convert(MatrixModel, js) reactions(std) # All accessors allow systematic access to information about reactions, diff --git a/docs/src/examples/04_core_model.jl b/docs/src/examples/04_core_model.jl index 84ec6f4a7..2c2610051 100644 --- a/docs/src/examples/04_core_model.jl +++ b/docs/src/examples/04_core_model.jl @@ -1,21 +1,21 @@ -# # `CoreModel` usage +# # `MatrixModel` usage #md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) #md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) -# In this tutorial we will introduce `COBREXA`'s `CoreModel` and -# `CoreModelCoupled`. We will use *E. coli*'s toy model to start with. +# In this tutorial we will introduce `COBREXA`'s `MatrixModel` and +# `MatrixModelWithCoupling`. We will use *E. coli*'s toy model to start with. !isfile("e_coli_core.xml") && download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") using COBREXA -# ## Loading a `CoreModel` +# ## Loading a `MatrixModel` -model = load_model(CoreModel, "e_coli_core.xml") # we specifically want to load a CoreModel from the model file +model = load_model(MatrixModel, "e_coli_core.xml") # we specifically want to load a MatrixModel from the model file -# ## Basic analysis on `CoreModel` +# ## Basic analysis on `MatrixModel` # As before, for optimization based analysis we need to load an optimizer. Here we # will use [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl) to optimize the linear @@ -34,20 +34,20 @@ dict_sol = flux_balance_analysis_dict( ], ) -# ## Structure of `CoreModel` +# ## Structure of `MatrixModel` -# `CoreModel` is optimized for analysis of models that utilizes the matrix, +# `MatrixModel` is optimized for analysis of models that utilizes the matrix, # linearly-algebraic "view" of the models. It stores data in a sparse format # wherever possible. # # The structure contains fields that contain the expectable model elements: -fieldnames(CoreModel) +fieldnames(MatrixModel) # model.S # Contrary to the usual implementations, the model representation does not # contain reaction coupling boudns; these can be added to any model by wrapping -# it with [`CoreCoupling`](@ref). You may also use the prepared -# [`CoreModelCoupled`](@ref) to get a version of [`CoreModel`](@ref) with this +# it with [`MatrixCoupling`](@ref). You may also use the prepared +# [`MatrixModelWithCoupling`](@ref) to get a version of [`MatrixModel`](@ref) with this # coupling. diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl index 13fda3b2a..ceb40b533 100644 --- a/docs/src/examples/04_standardmodel.jl +++ b/docs/src/examples/04_standardmodel.jl @@ -1,9 +1,9 @@ -# # Basic usage of `StandardModel` +# # Basic usage of `ObjectModel` #md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) #md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) -# In this tutorial we will use `COBREXA`'s `StandardModel` and functions that +# In this tutorial we will use `COBREXA`'s `ObjectModel` and functions that # specifically operate on it. As usual we will use the toy model of *E. coli* # for demonstration. @@ -12,26 +12,26 @@ using COBREXA -# ## Loading a model in the StandardModel format +# ## Loading a model in the ObjectModel format -model = load_model(StandardModel, "e_coli_core.json") # we specifically want to load a StandardModel from the model file +model = load_model(ObjectModel, "e_coli_core.json") # we specifically want to load a ObjectModel from the model file -#md # !!! note "Note: Loading `StandardModel`s implicitly uses `convert`" -#md # When using `load_model(StandardModel, file_location)` the model at +#md # !!! note "Note: Loading `ObjectModel`s implicitly uses `convert`" +#md # When using `load_model(ObjectModel, file_location)` the model at #md # `file_location` is first loaded into its inferred format and is then -#md # converted to a `StandardModel` using the generic accessor interface. +#md # converted to a `ObjectModel` using the generic accessor interface. #md # Thus, data loss may occur. Always check your model to ensure that #md # nothing important has been lost. -#nb # When using `load_model(StandardModel, file_location)` the model at +#nb # When using `load_model(ObjectModel, file_location)` the model at #nb # `file_location` is first loaded into its inferred format and is then -#nb # converted to a `StandardModel` using the generic accessor interface. +#nb # converted to a `ObjectModel` using the generic accessor interface. #nb # Thus, data loss may occur. Always check your model to ensure that #nb # nothing important has been lost. -# ## Internals of `StandardModel` +# ## Internals of `ObjectModel` -# A benefit of `StandardModel` is that it supports a richer internal +# A benefit of `ObjectModel` is that it supports a richer internal # infrastructure that can be used to manipulate internal model attributes in a # systematic way. Specifically, the genes, reactions, and metabolites with of a # model each have a type. This is particularly useful when modifying or even @@ -39,22 +39,22 @@ model = load_model(StandardModel, "e_coli_core.json") # we specifically want to # ## `Gene`s, `Reaction`s, and `Metabolite`s -# `StandardModel` is composed of ordered dictionaries of `Gene`s, `Metabolite`s +# `ObjectModel` is composed of ordered dictionaries of `Gene`s, `Metabolite`s # and `Reaction`s. Ordered dictionaries are used because the order of the # reactions and metabolites are important for constructing a stoichiometric # matrix since the rows and columns should correspond to the order of the metabolites # and reactions returned by calling the accessors `metabolites` and `reactions`. -# Each `StandardModel` is composed of the following fields: +# Each `ObjectModel` is composed of the following fields: -fieldnames(StandardModel) # fields of a StandardModel +fieldnames(ObjectModel) # fields of a ObjectModel -# The `:genes` field of a `StandardModel` contains an ordered dictionary of gene ids mapped to `Gene`s. +# The `:genes` field of a `ObjectModel` contains an ordered dictionary of gene ids mapped to `Gene`s. model.genes # the keys of this dictionary are the same as genes(model) # The `Gene` type is a struct that can be used to store information about genes -# in a `StandardModel`. Each `Gene` is composed of the following fields: +# in a `ObjectModel`. Each `Gene` is composed of the following fields: fieldnames(Gene) @@ -82,12 +82,12 @@ model.metabolites[random_metabolite_id] random_reaction_id = reactions(model)[rand(1:n_reactions(model))] model.reactions[random_reaction_id] -# `StandardModel` can be used to build your own metabolic model or modify an -# existing one. One of the main use cases for `StandardModel` is that it can be +# `ObjectModel` can be used to build your own metabolic model or modify an +# existing one. One of the main use cases for `ObjectModel` is that it can be # used to merge multiple models or parts of multiple models together. Since the -# internals are uniform inside each `StandardModel`, attributes of other model +# internals are uniform inside each `ObjectModel`, attributes of other model # types are squashed into the required format (using the generic accessors). -# This ensures that the internals of all `StandardModel`s are the same - +# This ensures that the internals of all `ObjectModel`s are the same - # allowing easy systematic evaluation. #md # !!! warning "Warning: Combining models with different namespaces is tricky" @@ -97,7 +97,7 @@ model.reactions[random_reaction_id] #md # manually addressed to prevent duplicates, e.g. reactions, #md # from being added. -# ## Checking the internals of `StandardModel`s: `annotation_index` +# ## Checking the internals of `ObjectModel`s: `annotation_index` # Often when models are automatically reconstructed duplicate genes, reactions # or metabolites end up in a model. `COBREXA` exports `annotation_index` to @@ -113,7 +113,7 @@ rxn_annotations["ec-code"] # The `annotation_index` function can also be used on `Reaction`s and # `Gene`s in the same way. -# ## Checking the internals of `StandardModel`s: `check_duplicate_reaction` +# ## Checking the internals of `ObjectModel`s: `check_duplicate_reaction` # Another useful function is `check_duplicate_reaction`, which checks for # reactions that have duplicate (or similar) reaction equations. @@ -125,7 +125,7 @@ pgm_duplicate # check_duplicate_reaction(pgm_duplicate, model.reactions; only_metabolites = false) # can also just check if only the metabolites are the same but different stoichiometry is used -# ## Checking the internals of `StandardModel`s: `reaction_mass_balanced` +# ## Checking the internals of `ObjectModel`s: `reaction_mass_balanced` # Finally, [`reaction_mass_balanced`](@ref) can be used to check if a reaction is mass # balanced based on the formulas of the reaction equation. diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 37d60b900..43f4a7477 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -1,16 +1,16 @@ # # Model construction and modification # `COBREXA` can load models stored in `.mat`, `.json`, and `.xml` formats; and convert -# these into `StandardModel`s. However, it is also possible to construct models +# these into `ObjectModel`s. However, it is also possible to construct models # from scratch, and modify existing models. This will be demonstrated # here. using COBREXA -# In `COBREXA`, model construction is primarily supported through `StandardModel`s. -# To begin, create an empty `StandardModel`. +# In `COBREXA`, model construction is primarily supported through `ObjectModel`s. +# To begin, create an empty `ObjectModel`. -model = StandardModel("FirstModel") # assign model id = "FirstModel" +model = ObjectModel("FirstModel") # assign model id = "FirstModel" # Next, genes, metabolites and reactions need to be added to the model. diff --git a/docs/src/examples/07_restricting_reactions.jl b/docs/src/examples/07_restricting_reactions.jl index 5a9261491..222db3d8b 100644 --- a/docs/src/examples/07_restricting_reactions.jl +++ b/docs/src/examples/07_restricting_reactions.jl @@ -10,7 +10,7 @@ using COBREXA, GLPK -model = load_model(StandardModel, "e_coli_core.json") +model = load_model(ObjectModel, "e_coli_core.json") # ## Disabling a reaction diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl index 35be46d46..dcfe71200 100644 --- a/docs/src/examples/11_growth.jl +++ b/docs/src/examples/11_growth.jl @@ -13,7 +13,7 @@ using COBREXA, GLPK !isfile("e_coli_core.xml") && download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") -model = load_model(StandardModel, "e_coli_core.xml") +model = load_model(ObjectModel, "e_coli_core.xml") # ## What nutrients does my model need to grow? diff --git a/docs/src/examples/13_moma.jl b/docs/src/examples/13_moma.jl index 200ccd07d..b69a28a16 100644 --- a/docs/src/examples/13_moma.jl +++ b/docs/src/examples/13_moma.jl @@ -17,7 +17,7 @@ using COBREXA -model = load_model(StandardModel, "e_coli_core.xml") +model = load_model(ObjectModel, "e_coli_core.xml") # MOMA analysis requires solution of a quadratic model, we will thus use Clarabel as the main optimizer. diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md index 11c9709fa..8dfb99c4a 100644 --- a/docs/src/quickstart.md +++ b/docs/src/quickstart.md @@ -67,7 +67,7 @@ perform if some reactions were disabled independently: ```julia # convert to a model type that is efficient to modify -m = convert(StandardModel, model) +m = convert(ObjectModel, model) # find the model objective value if oxygen or carbon dioxide transports are disabled screen(m, # the base model diff --git a/docs/src/quickstart.md.template b/docs/src/quickstart.md.template index 1d7cae079..16670aacc 100644 --- a/docs/src/quickstart.md.template +++ b/docs/src/quickstart.md.template @@ -11,7 +11,7 @@ perform if some reactions were disabled independently: ```julia # convert to a model type that is efficient to modify -m = convert(StandardModel, model) +m = convert(ObjectModel, model) # find the model objective value if oxygen or carbon dioxide transports are disabled screen(m, # the base model diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl index 5a008eb79..043b54ab5 100644 --- a/src/analysis/screening.jl +++ b/src/analysis/screening.jl @@ -77,14 +77,14 @@ Note: this function is a thin argument-handling wrapper around # Example ``` function reverse_reaction(i::Int) - (model::CoreModel) -> begin + (model::MatrixModel) -> begin mod = copy(model) mod.S[:,i] .*= -1 # this is unrealistic but sufficient for demonstration mod end end -m = load_model(CoreModel, "e_coli_core.xml") +m = load_model(MatrixModel, "e_coli_core.xml") screen(m, variants = [ diff --git a/src/io/io.jl b/src/io/io.jl index 2f5388c51..883f214d8 100644 --- a/src/io/io.jl +++ b/src/io/io.jl @@ -35,7 +35,7 @@ converted to `type`. # Example: - load_model(CoreModel, "mySBMLModel.xml") + load_model(MatrixModel, "mySBMLModel.xml") """ function load_model(type::Type{T}, file_name::String)::T where {T<:AbstractMetabolicModel} convert(type, load_model(file_name)) diff --git a/src/io/show/MetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl similarity index 100% rename from src/io/show/MetabolicModel.jl rename to src/io/show/AbstractMetabolicModel.jl diff --git a/src/reconstruction/CoreModelCoupled.jl b/src/reconstruction/MatrixCoupling.jl similarity index 72% rename from src/reconstruction/CoreModelCoupled.jl rename to src/reconstruction/MatrixCoupling.jl index ec5b26e7a..a75289e0b 100644 --- a/src/reconstruction/CoreModelCoupled.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -1,11 +1,11 @@ """ $(TYPEDSIGNATURES) -Add reaction(s) to a `CoreModelCoupled` model `m`. +Add reaction(s) to a `MatrixModelWithCoupling` model `m`. """ function add_reactions( - m::CoreModelCoupled, + m::MatrixModelWithCoupling, s::V1, b::V2, c::AbstractFloat, @@ -14,7 +14,7 @@ function add_reactions( check_consistency = false, ) where {V1<:VecType,V2<:VecType} new_lm = add_reactions(m.lm, s, b, c, xl, xu, check_consistency = check_consistency) - return CoreModelCoupled( + return MatrixModelWithCoupling( new_lm, hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), m.cl, @@ -27,7 +27,7 @@ $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModelCoupled, + m::MatrixModelWithCoupling, s::V1, b::V2, c::AbstractFloat, @@ -48,7 +48,7 @@ function add_reactions( mets, check_consistency = check_consistency, ) - return CoreModelCoupled( + return MatrixModelWithCoupling( new_lm, hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), m.cl, @@ -60,7 +60,7 @@ end $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModelCoupled, + m::MatrixModelWithCoupling, Sp::M, b::V, c::V, @@ -69,7 +69,7 @@ function add_reactions( check_consistency = false, ) where {M<:MatType,V<:VecType} new_lm = add_reactions(m.lm, Sp, b, c, xl, xu, check_consistency = check_consistency) - return CoreModelCoupled( + return MatrixModelWithCoupling( new_lm, hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), m.cl, @@ -82,9 +82,9 @@ $(TYPEDSIGNATURES) Add all reactions from `m2` to `m1`. """ -function add_reactions(m1::CoreModelCoupled, m2::CoreModel; check_consistency = false) +function add_reactions(m1::MatrixModelWithCoupling, m2::MatrixModel; check_consistency = false) new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) - return CoreModelCoupled( + return MatrixModelWithCoupling( new_lm, hcat(m1.C, spzeros(size(m1.C, 1), n_reactions(new_lm) - n_reactions(m1.lm))), m1.cl, @@ -96,7 +96,7 @@ end $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModelCoupled, + m::MatrixModelWithCoupling, Sp::M, b::V, c::V, @@ -117,7 +117,7 @@ function add_reactions( mets, check_consistency = check_consistency, ) - return CoreModelCoupled( + return MatrixModelWithCoupling( new_lm, hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), m.cl, @@ -128,12 +128,12 @@ end """ $(TYPEDSIGNATURES) -Add constraints of the following form to CoreCoupling and return the modified +Add constraints of the following form to MatrixCoupling and return the modified model. The arguments are same as for in-place [`add_coupling_constraints!`](@ref). """ -function add_coupling_constraints(m::CoreCoupling, args...) +function add_coupling_constraints(m::MatrixCoupling, args...) new_lp = deepcopy(m) add_coupling_constraints!(new_lp, args...) return new_lp @@ -142,10 +142,10 @@ end """ $(TYPEDSIGNATURES) -Add coupling constraints to a plain [`CoreModel`](@ref) (returns a -[`CoreModelCoupled`](@ref)). +Add coupling constraints to a plain [`MatrixModel`](@ref) (returns a +[`MatrixModelWithCoupling`](@ref)). """ -add_coupling_constraints(m::CoreModel, args...) = CoreModelCoupled(m, args...) +add_coupling_constraints(m::MatrixModel, args...) = MatrixModelWithCoupling(m, args...) """ $(TYPEDSIGNATURES) @@ -153,7 +153,7 @@ $(TYPEDSIGNATURES) Overload for adding a single coupling constraint. """ function add_coupling_constraints!( - m::CoreCoupling, + m::MatrixCoupling, c::VecType, cl::AbstractFloat, cu::AbstractFloat, @@ -170,7 +170,7 @@ In-place add a single coupling constraint in form ``` """ function add_coupling_constraints!( - m::CoreCoupling, + m::MatrixCoupling, C::MatType, cl::V, cu::V, @@ -194,7 +194,7 @@ Remove coupling constraints from the linear model, and return the modified model. Arguments are the same as for in-place version [`remove_coupling_constraints!`](@ref). """ -function remove_coupling_constraints(m::CoreCoupling, args...) +function remove_coupling_constraints(m::MatrixCoupling, args...) new_model = deepcopy(m) remove_coupling_constraints!(new_model, args...) return new_model @@ -203,19 +203,19 @@ end """ $(TYPEDSIGNATURES) -Removes a single coupling constraints from a [`CoreCoupling`](@ref) in-place. +Removes a single coupling constraints from a [`MatrixCoupling`](@ref) in-place. """ -remove_coupling_constraints!(m::CoreCoupling, constraint::Int) = +remove_coupling_constraints!(m::MatrixCoupling, constraint::Int) = remove_coupling_constraints!(m, [constraint]) """ $(TYPEDSIGNATURES) -Removes a set of coupling constraints from a [`CoreCoupling`](@ref) +Removes a set of coupling constraints from a [`MatrixCoupling`](@ref) in-place. """ -function remove_coupling_constraints!(m::CoreCoupling, constraints::Vector{Int}) +function remove_coupling_constraints!(m::MatrixCoupling, constraints::Vector{Int}) to_be_kept = filter(!in(constraints), 1:n_coupling_constraints(m)) m.C = m.C[to_be_kept, :] m.cl = m.cl[to_be_kept] @@ -230,7 +230,7 @@ Change the lower and/or upper bounds (`cl` and `cu`) for the given list of coupling constraints. """ function change_coupling_bounds!( - model::CoreCoupling, + model::MatrixCoupling, constraints::Vector{Int}; cl::V = Float64[], cu::V = Float64[], @@ -255,118 +255,118 @@ function change_coupling_bounds!( end # TODO see if some of these can be derived from AbstractModelWrapper -@_change_bounds_fn CoreCoupling Int inplace begin +@_change_bounds_fn MatrixCoupling Int inplace begin change_bound!(model.lm, rxn_idx, lower = lower, upper = upper) end -@_change_bounds_fn CoreCoupling Int inplace plural begin +@_change_bounds_fn MatrixCoupling Int inplace plural begin change_bounds!(model.lm, rxn_idxs, lower = lower, upper = upper) end -@_change_bounds_fn CoreCoupling String inplace begin +@_change_bounds_fn MatrixCoupling String inplace begin change_bound!(model.lm, rxn_id, lower = lower, upper = upper) end -@_change_bounds_fn CoreCoupling String inplace plural begin +@_change_bounds_fn MatrixCoupling String inplace plural begin change_bounds!(model.lm, rxn_ids, lower = lower, upper = upper) end -@_change_bounds_fn CoreCoupling Int begin +@_change_bounds_fn MatrixCoupling Int begin n = copy(model) n.lm = change_bound(model.lm, rxn_idx, lower = lower, upper = upper) n end -@_change_bounds_fn CoreCoupling Int plural begin +@_change_bounds_fn MatrixCoupling Int plural begin n = copy(model) n.lm = change_bounds(model.lm, rxn_idxs, lower = lower, upper = upper) n end -@_change_bounds_fn CoreCoupling String begin +@_change_bounds_fn MatrixCoupling String begin n = copy(model) n.lm = change_bound(model.lm, rxn_id, lower = lower, upper = upper) n end -@_change_bounds_fn CoreCoupling String plural begin +@_change_bounds_fn MatrixCoupling String plural begin n = copy(model) n.lm = change_bounds(model.lm, rxn_ids, lower = lower, upper = upper) n end -@_remove_fn reaction CoreCoupling Int inplace begin +@_remove_fn reaction MatrixCoupling Int inplace begin remove_reactions!(model, [reaction_idx]) end -@_remove_fn reaction CoreCoupling Int inplace plural begin +@_remove_fn reaction MatrixCoupling Int inplace plural begin orig_rxns = reactions(model.lm) remove_reactions!(model.lm, reaction_idxs) model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] nothing end -@_remove_fn reaction CoreCoupling Int begin +@_remove_fn reaction MatrixCoupling Int begin remove_reactions(model, [reaction_idx]) end -@_remove_fn reaction CoreCoupling Int plural begin +@_remove_fn reaction MatrixCoupling Int plural begin n = copy(model) n.lm = remove_reactions(n.lm, reaction_idxs) n.C = n.C[:, in.(reactions(model.lm), Ref(Set(reactions(n.lm))))] return n end -@_remove_fn reaction CoreCoupling String inplace begin +@_remove_fn reaction MatrixCoupling String inplace begin remove_reactions!(model, [reaction_id]) end -@_remove_fn reaction CoreCoupling String inplace plural begin +@_remove_fn reaction MatrixCoupling String inplace plural begin remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) end -@_remove_fn reaction CoreCoupling String begin +@_remove_fn reaction MatrixCoupling String begin remove_reactions(model, [reaction_id]) end -@_remove_fn reaction CoreCoupling String plural begin +@_remove_fn reaction MatrixCoupling String plural begin remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) end -@_remove_fn metabolite CoreCoupling Int inplace begin +@_remove_fn metabolite MatrixCoupling Int inplace begin remove_metabolites!(model, [metabolite_idx]) end -@_remove_fn metabolite CoreCoupling Int plural inplace begin +@_remove_fn metabolite MatrixCoupling Int plural inplace begin orig_rxns = reactions(model.lm) model.lm = remove_metabolites(model.lm, metabolite_idxs) model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] nothing end -@_remove_fn metabolite CoreCoupling Int begin +@_remove_fn metabolite MatrixCoupling Int begin remove_metabolites(model, [metabolite_idx]) end -@_remove_fn metabolite CoreCoupling Int plural begin +@_remove_fn metabolite MatrixCoupling Int plural begin n = copy(model) n.lm = remove_metabolites(n.lm, metabolite_idxs) return n end -@_remove_fn metabolite CoreCoupling String inplace begin +@_remove_fn metabolite MatrixCoupling String inplace begin remove_metabolites!(model, [metabolite_id]) end -@_remove_fn metabolite CoreCoupling String inplace plural begin +@_remove_fn metabolite MatrixCoupling String inplace plural begin remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) end -@_remove_fn metabolite CoreCoupling String begin +@_remove_fn metabolite MatrixCoupling String begin remove_metabolites(model, [metabolite_id]) end -@_remove_fn metabolite CoreCoupling String plural begin +@_remove_fn metabolite MatrixCoupling String plural begin remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) end @@ -375,6 +375,6 @@ $(TYPEDSIGNATURES) Forwards arguments to [`change_objective!`](@ref) of the internal model. """ -function change_objective!(model::CoreCoupling, args...; kwargs...) +function change_objective!(model::MatrixCoupling, args...; kwargs...) change_objective!(model.lm, args...; kwargs...) end diff --git a/src/reconstruction/CoreModel.jl b/src/reconstruction/MatrixModel.jl similarity index 81% rename from src/reconstruction/CoreModel.jl rename to src/reconstruction/MatrixModel.jl index 905ef670a..1df789ea3 100644 --- a/src/reconstruction/CoreModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Add `rxns` to `model` efficiently. The model must already contain the metabolites used by `rxns` in the model. """ -function add_reactions!(model::CoreModel, rxns::Vector{Reaction}) +function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) I = Int64[] # rows J = Int64[] # cols V = Float64[] # values @@ -37,15 +37,15 @@ $(TYPEDSIGNATURES) Add `rxn` to `model`. The model must already contain the metabolites used by `rxn` in the model. """ -add_reaction!(model::CoreModel, rxn::Reaction) = add_reactions!(model, [rxn]) +add_reaction!(model::MatrixModel, rxn::Reaction) = add_reactions!(model, [rxn]) """ $(TYPEDSIGNATURES) -Add reaction(s) to a `CoreModel` model `m`. +Add reaction(s) to a `MatrixModel` model `m`. """ function add_reactions( - m::CoreModel, + m::MatrixModel, s::VecType, b::VecType, c::AbstractFloat, @@ -68,7 +68,7 @@ end $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModel, + m::MatrixModel, s::VecType, b::VecType, c::AbstractFloat, @@ -95,7 +95,7 @@ end $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModel, + m::MatrixModel, Sp::MatType, b::VecType, c::VecType, @@ -123,7 +123,7 @@ $(TYPEDSIGNATURES) Add all reactions from `m2` to `m1`. """ -function add_reactions(m1::CoreModel, m2::CoreModel; check_consistency = false) +function add_reactions(m1::MatrixModel, m2::MatrixModel; check_consistency = false) return add_reactions( m1, m2.S, @@ -141,7 +141,7 @@ end $(TYPEDSIGNATURES) """ function add_reactions( - m::CoreModel, + m::MatrixModel, Sp::MatType, b::VecType, c::VecType, @@ -199,7 +199,7 @@ function add_reactions( newxl = vcat(m.xl, xl[new_reactions]) newxu = vcat(m.xu, xu[new_reactions]) new_rxns = vcat(m.rxns, rxns[new_reactions]) - new_lp = CoreModel(new_s, newb, newc, newxl, newxu, new_rxns, new_mets) + new_lp = MatrixModel(new_s, newb, newc, newxl, newxu, new_rxns, new_mets) if check_consistency return (new_lp, new_reactions, new_metabolites) @@ -216,7 +216,7 @@ Check the consistency of given reactions with existing reactions in `m`. TODO: work in progress, doesn't return consistency status. """ function verify_consistency( - m::CoreModel, + m::MatrixModel, Sp::M, b::V, c::V, @@ -255,23 +255,23 @@ function verify_consistency( return (new_reactions, new_metabolites) end -@_change_bounds_fn CoreModel Int inplace begin +@_change_bounds_fn MatrixModel Int inplace begin isnothing(lower) || (model.xl[rxn_idx] = lower) isnothing(upper) || (model.xu[rxn_idx] = upper) nothing end -@_change_bounds_fn CoreModel Int inplace plural begin +@_change_bounds_fn MatrixModel Int inplace plural begin for (i, l, u) in zip(rxn_idxs, lower, upper) change_bound!(model, i, lower = l, upper = u) end end -@_change_bounds_fn CoreModel Int begin +@_change_bounds_fn MatrixModel Int begin change_bounds(model, [rxn_idx], lower = [lower], upper = [upper]) end -@_change_bounds_fn CoreModel Int plural begin +@_change_bounds_fn MatrixModel Int plural begin n = copy(model) n.xl = copy(n.xl) n.xu = copy(n.xu) @@ -279,11 +279,11 @@ end n end -@_change_bounds_fn CoreModel String inplace begin +@_change_bounds_fn MatrixModel String inplace begin change_bounds!(model, [rxn_id], lower = [lower], upper = [upper]) end -@_change_bounds_fn CoreModel String inplace plural begin +@_change_bounds_fn MatrixModel String inplace plural begin change_bounds!( model, Vector{Int}(indexin(rxn_ids, reactions(model))), @@ -292,11 +292,11 @@ end ) end -@_change_bounds_fn CoreModel String begin +@_change_bounds_fn MatrixModel String begin change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) end -@_change_bounds_fn CoreModel String plural begin +@_change_bounds_fn MatrixModel String plural begin change_bounds( model, Int.(indexin(rxn_ids, reactions(model))), @@ -305,11 +305,11 @@ end ) end -@_remove_fn reaction CoreModel Int inplace begin +@_remove_fn reaction MatrixModel Int inplace begin remove_reactions!(model, [reaction_idx]) end -@_remove_fn reaction CoreModel Int inplace plural begin +@_remove_fn reaction MatrixModel Int inplace plural begin mask = .!in.(1:n_reactions(model), Ref(reaction_idxs)) model.S = model.S[:, mask] model.c = model.c[mask] @@ -319,37 +319,37 @@ end nothing end -@_remove_fn reaction CoreModel Int begin +@_remove_fn reaction MatrixModel Int begin remove_reactions(model, [reaction_idx]) end -@_remove_fn reaction CoreModel Int plural begin +@_remove_fn reaction MatrixModel Int plural begin n = copy(model) remove_reactions!(n, reaction_idxs) return n end -@_remove_fn reaction CoreModel String inplace begin +@_remove_fn reaction MatrixModel String inplace begin remove_reactions!(model, [reaction_id]) end -@_remove_fn reaction CoreModel String inplace plural begin +@_remove_fn reaction MatrixModel String inplace plural begin remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) end -@_remove_fn reaction CoreModel String begin +@_remove_fn reaction MatrixModel String begin remove_reactions(model, [reaction_id]) end -@_remove_fn reaction CoreModel String plural begin +@_remove_fn reaction MatrixModel String plural begin remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) end -@_remove_fn metabolite CoreModel Int inplace begin +@_remove_fn metabolite MatrixModel Int inplace begin remove_metabolites!(model, [metabolite_idx]) end -@_remove_fn metabolite CoreModel Int plural inplace begin +@_remove_fn metabolite MatrixModel Int plural inplace begin remove_reactions!( model, [ @@ -364,29 +364,29 @@ end nothing end -@_remove_fn metabolite CoreModel Int begin +@_remove_fn metabolite MatrixModel Int begin remove_metabolites(model, [metabolite_idx]) end -@_remove_fn metabolite CoreModel Int plural begin +@_remove_fn metabolite MatrixModel Int plural begin n = deepcopy(model) #everything gets changed anyway remove_metabolites!(n, metabolite_idxs) return n end -@_remove_fn metabolite CoreModel String inplace begin +@_remove_fn metabolite MatrixModel String inplace begin remove_metabolites!(model, [metabolite_id]) end -@_remove_fn metabolite CoreModel String inplace plural begin +@_remove_fn metabolite MatrixModel String inplace plural begin remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) end -@_remove_fn metabolite CoreModel String begin +@_remove_fn metabolite MatrixModel String begin remove_metabolites(model, [metabolite_id]) end -@_remove_fn metabolite CoreModel String plural begin +@_remove_fn metabolite MatrixModel String plural begin remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) end @@ -397,7 +397,7 @@ Change the objective to reactions at given indexes, optionally specifying their `weights` in the same order. By default, all set weights are 1. """ function change_objective!( - model::CoreModel, + model::MatrixModel, rxn_idxs::Vector{Int}; weights = ones(length(rxn_idxs)), ) @@ -409,10 +409,10 @@ end """ $(TYPEDSIGNATURES) -Change objective function of a CoreModel to a single `1` at reaction index +Change objective function of a MatrixModel to a single `1` at reaction index `rxn_idx`. """ -change_objective!(model::CoreModel, rxn_idx::Int) = change_objective!(model, [rxn_idx]) +change_objective!(model::MatrixModel, rxn_idx::Int) = change_objective!(model, [rxn_idx]) """ $(TYPEDSIGNATURES) @@ -421,7 +421,7 @@ Change objective of given reaction IDs, optionally specifying objective `weights` in the same order as `rxn_ids`. By default, all set weights are 1. """ function change_objective!( - model::CoreModel, + model::MatrixModel, rxn_ids::Vector{String}; weights = ones(length(rxn_ids)), ) @@ -434,7 +434,7 @@ end """ $(TYPEDSIGNATURES) -Change objective function of a CoreModel to a single `1` at the given reaction +Change objective function of a MatrixModel to a single `1` at the given reaction ID. """ -change_objective!(model::CoreModel, rxn_id::String) = change_objective!(model, [rxn_id]) +change_objective!(model::MatrixModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/StandardModel.jl b/src/reconstruction/ObjectModel.jl similarity index 79% rename from src/reconstruction/StandardModel.jl rename to src/reconstruction/ObjectModel.jl index 5e2988fea..ab383e2dd 100644 --- a/src/reconstruction/StandardModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -3,7 +3,7 @@ $(TYPEDSIGNATURES) Add `rxns` to `model` based on reaction `id`. """ -function add_reactions!(model::StandardModel, rxns::Vector{Reaction}) +function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) for rxn in rxns model.reactions[rxn.id] = rxn end @@ -15,14 +15,14 @@ $(TYPEDSIGNATURES) Add `rxn` to `model` based on reaction `id`. """ -add_reaction!(model::StandardModel, rxn::Reaction) = add_reactions!(model, [rxn]) +add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) """ $(TYPEDSIGNATURES) Add `mets` to `model` based on metabolite `id`. """ -function add_metabolites!(model::StandardModel, mets::Vector{Metabolite}) +function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) for met in mets model.metabolites[met.id] = met end @@ -34,14 +34,14 @@ $(TYPEDSIGNATURES) Add `met` to `model` based on metabolite `id`. """ -add_metabolite!(model::StandardModel, met::Metabolite) = add_metabolites!(model, [met]) +add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met]) """ $(TYPEDSIGNATURES) Add `genes` to `model` based on gene `id`. """ -function add_genes!(model::StandardModel, genes::Vector{Gene}) +function add_genes!(model::ObjectModel, genes::Vector{Gene}) for gene in genes model.genes[gene.id] = gene end @@ -53,7 +53,7 @@ $(TYPEDSIGNATURES) Add `gene` to `model` based on gene `id`. """ -add_gene!(model::StandardModel, gene::Gene) = add_genes!(model, [gene]) +add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) """ $(TYPEDSIGNATURES) @@ -121,7 +121,7 @@ remove_genes!(model, ["g1", "g2"]) ``` """ function remove_genes!( - model::StandardModel, + model::ObjectModel, gids::Vector{String}; knockout_reactions::Bool = false, ) @@ -150,27 +150,27 @@ constrain reactions that require the genes to function to carry zero flux. remove_gene!(model, "g1") ``` """ -remove_gene!(model::StandardModel, gid::String; knockout_reactions::Bool = false) = +remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) -@_change_bounds_fn StandardModel String inplace begin +@_change_bounds_fn ObjectModel String inplace begin isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) nothing end -@_change_bounds_fn StandardModel String inplace plural begin +@_change_bounds_fn ObjectModel String inplace plural begin for (i, l, u) in zip(rxn_ids, lower, upper) change_bound!(model, i, lower = l, upper = u) end end -@_change_bounds_fn StandardModel String begin +@_change_bounds_fn ObjectModel String begin change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) end -@_change_bounds_fn StandardModel String plural begin +@_change_bounds_fn ObjectModel String plural begin n = copy(model) n.reactions = copy(model.reactions) for i in rxn_ids @@ -180,7 +180,7 @@ end return n end -@_remove_fn reaction StandardModel String inplace begin +@_remove_fn reaction ObjectModel String inplace begin if !(reaction_id in reactions(model)) @models_log @info "Reaction $reaction_id not found in model." else @@ -189,27 +189,27 @@ end nothing end -@_remove_fn reaction StandardModel String inplace plural begin +@_remove_fn reaction ObjectModel String inplace plural begin remove_reaction!.(Ref(model), reaction_ids) nothing end -@_remove_fn reaction StandardModel String begin +@_remove_fn reaction ObjectModel String begin remove_reactions(model, [reaction_id]) end -@_remove_fn reaction StandardModel String plural begin +@_remove_fn reaction ObjectModel String plural begin n = copy(model) n.reactions = copy(model.reactions) remove_reactions!(n, reaction_ids) return n end -@_remove_fn metabolite StandardModel String inplace begin +@_remove_fn metabolite ObjectModel String inplace begin remove_metabolites!(model, [metabolite_id]) end -@_remove_fn metabolite StandardModel String inplace plural begin +@_remove_fn metabolite ObjectModel String inplace plural begin !all(in.(metabolite_ids, Ref(metabolites(model)))) && @models_log @info "Some metabolites not found in model." remove_reactions!( @@ -223,11 +223,11 @@ end nothing end -@_remove_fn metabolite StandardModel String begin +@_remove_fn metabolite ObjectModel String begin remove_metabolites(model, [metabolite_id]) end -@_remove_fn metabolite StandardModel String plural begin +@_remove_fn metabolite ObjectModel String plural begin n = copy(model) n.reactions = copy(model.reactions) n.metabolites = copy(model.metabolites) @@ -242,7 +242,7 @@ Change the objective for `model` to reaction(s) with `rxn_ids`, optionally speci assume equal weights. If no objective exists in model, sets objective. """ function change_objective!( - model::StandardModel, + model::ObjectModel, rxn_ids::Vector{String}; weights = ones(length(rxn_ids)), ) @@ -257,4 +257,4 @@ function change_objective!( end end -change_objective!(model::StandardModel, rxn_id::String) = change_objective!(model, [rxn_id]) +change_objective!(model::ObjectModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index 6de61158c..4a0cef828 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -14,7 +14,7 @@ add_community_objective!(model, Dict("met1"=>1.0, "met2"=>2.0)) See also: [`update_community_objective!`](@ref) """ function add_community_objective!( - community::CoreModel, + community::MatrixModel, objective_mets_weights::Dict{String,Float64}; objective_id = "community_biomass", ) @@ -39,10 +39,10 @@ end """ $(TYPEDSIGNATURES) -Variant of [`add_community_objective!`] that takes a `StandardModel` community model as input. +Variant of [`add_community_objective!`] that takes a `ObjectModel` community model as input. """ function add_community_objective!( - community::StandardModel, + community::ObjectModel, objective_mets_weights::Dict{String,Float64}; objective_id = "community_biomass", ) @@ -77,7 +77,7 @@ update_community_objective!(model, "community_biomass", Dict("met1"=>1.0, "met2" See also: [`add_community_objective!`](@ref) """ function update_community_objective!( - community::CoreModel, + community::MatrixModel, objective_id::String, objective_mets_weights::Dict{String,Float64}, ) @@ -100,10 +100,10 @@ end """ $(TYPEDSIGNATURES) -Variant of [`update_community_objective!`] that takes a `StandardModel` community model as input. +Variant of [`update_community_objective!`] that takes a `ObjectModel` community model as input. """ function update_community_objective!( - community::StandardModel, + community::ObjectModel, objective_id::String, objective_mets_weights::Dict{String,Float64}, ) @@ -116,7 +116,7 @@ end """ $(TYPEDSIGNATURES) -Return a `CoreModel` representing the community model of `models` joined through their +Return a `MatrixModel` representing the community model of `models` joined through their exchange reactions and metabolites in the dictionary `exchange_rxn_mets`, which maps exchange reactions to their associated metabolite. These exchange reactions and metabolites link model metabolites to environmental metabolites and reactions. Optionally specify @@ -160,7 +160,7 @@ this is unclear. # Example ``` m1 = load_model(core_model_path) -m2 = load_model(CoreModel, core_model_path) +m2 = load_model(MatrixModel, core_model_path) # need to list ALL the exchanges that will form part of the entire model exchange_rxn_mets = Dict(k => first(keys(reaction_stoichiometry(m1, ex_rxn))) @@ -169,7 +169,7 @@ exchange_rxn_mets = Dict(k => first(keys(reaction_stoichiometry(m1, ex_rxn))) biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] community = join_with_exchanges( - CoreModel, + MatrixModel, [m1, m2], exchange_rxn_mets; biomass_ids = biomass_ids, @@ -177,7 +177,7 @@ community = join_with_exchanges( ``` """ function join_with_exchanges( - ::Type{CoreModel}, + ::Type{MatrixModel}, models::Vector{M}, exchange_rxn_mets::Dict{String,String}; biomass_ids = String[], @@ -298,23 +298,23 @@ function join_with_exchanges( end end - return CoreModel(S, spzeros(size(S, 1)), spzeros(size(S, 2)), lbs, ubs, rxns, mets) + return MatrixModel(S, spzeros(size(S, 1)), spzeros(size(S, 2)), lbs, ubs, rxns, mets) end """ $(TYPEDSIGNATURES) -A variant of [`join_with_exchanges`](@ref) that returns a `StandardModel`. +A variant of [`join_with_exchanges`](@ref) that returns a `ObjectModel`. """ function join_with_exchanges( - ::Type{StandardModel}, + ::Type{ObjectModel}, models::Vector{M}, exchange_rxn_mets::Dict{String,String}; biomass_ids = [], model_names = [], -)::StandardModel where {M<:AbstractMetabolicModel} +)::ObjectModel where {M<:AbstractMetabolicModel} - community = StandardModel() + community = ObjectModel() rxns = OrderedDict{String,Reaction}() mets = OrderedDict{String,Metabolite}() genes = OrderedDict{String,Gene}() @@ -368,7 +368,7 @@ unit coefficient. The exchange reactions and metabolites in `exchange_rxn_mets` exist in `community`. Always returns a new community model because it is more efficient than resizing all the matrices. -No in-place variant for `CoreModel`s exists yet. +No in-place variant for `MatrixModel`s exists yet. # Example ``` @@ -380,7 +380,7 @@ community = add_model_with_exchanges(community, ``` """ function add_model_with_exchanges( - community::CoreModel, + community::MatrixModel, model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", @@ -463,22 +463,22 @@ function add_model_with_exchanges( I, V = findnz(objective(community)) c = sparsevec(I, V, n_reactions_total) - return CoreModel(S, b, c, lbs, ubs, rxns, mets) + return MatrixModel(S, b, c, lbs, ubs, rxns, mets) end """ $(TYPEDSIGNATURES) -The `StandardModel` variant of [`add_model_with_exchanges`](@ref), but is in-place. +The `ObjectModel` variant of [`add_model_with_exchanges`](@ref), but is in-place. """ function add_model_with_exchanges!( - community::StandardModel, + community::ObjectModel, model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", biomass_id = nothing, ) - stdm = model isa StandardModel ? deepcopy(model) : convert(StandardModel, model) + stdm = model isa ObjectModel ? deepcopy(model) : convert(ObjectModel, model) model_name = model_name * "_" for met in values(stdm.metabolites) @@ -520,11 +520,11 @@ end """ $(TYPEDSIGNATURES) -The `StandardModel` variant of [`add_model_with_exchanges`](@ref). Makes a deepcopy of +The `ObjectModel` variant of [`add_model_with_exchanges`](@ref). Makes a deepcopy of `community` and calls the inplace variant of this function on that copy. """ function add_model_with_exchanges( - community::StandardModel, + community::ObjectModel, model::AbstractMetabolicModel, exchange_rxn_mets::Dict{String,String}; model_name = "unknown_species", diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 79400bbdc..5fb975e0a 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -1,7 +1,7 @@ """ $(TYPEDEF) -A structure for representing a single reaction in a [`StandardModel`](@ref). +A structure for representing a single reaction in a [`ObjectModel`](@ref). # Fields $(TYPEDFIELDS) diff --git a/src/types/accessors/MetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl similarity index 100% rename from src/types/accessors/MetabolicModel.jl rename to src/types/accessors/AbstractMetabolicModel.jl diff --git a/src/types/misc/CoreModel.jl b/src/types/misc/MatrixModel.jl similarity index 52% rename from src/types/misc/CoreModel.jl rename to src/types/misc/MatrixModel.jl index 94476477b..7216e0242 100644 --- a/src/types/misc/CoreModel.jl +++ b/src/types/misc/MatrixModel.jl @@ -1,4 +1,4 @@ -Base.isequal(model1::CoreModel, model2::CoreModel) = +Base.isequal(model1::MatrixModel, model2::MatrixModel) = isequal(model1.S, model2.S) && isequal(model1.b, model2.b) && isequal(model1.c, model2.c) && @@ -7,13 +7,13 @@ Base.isequal(model1::CoreModel, model2::CoreModel) = isequal(model1.rxns, model2.rxns) && isequal(model1.mets, model2.mets) -Base.copy(model::CoreModel) = - CoreModel(model.S, model.b, model.c, model.xl, model.xu, model.rxns, model.mets) +Base.copy(model::MatrixModel) = + MatrixModel(model.S, model.b, model.c, model.xl, model.xu, model.rxns, model.mets) -Base.isequal(model1::CoreModelCoupled, model2::CoreModelCoupled) = +Base.isequal(model1::MatrixModelWithCoupling, model2::MatrixModelWithCoupling) = isequal(model1.lm, model2.lm) && isequal(model1.C, model2.C) && isequal(model1.cl, model2.cl) && isequal(model1.cu, model2.cu) -Base.copy(model::CoreModelCoupled) = CoreModelCoupled(model.lm, model.C, model.cl, model.cu) +Base.copy(model::MatrixModelWithCoupling) = MatrixModelWithCoupling(model.lm, model.C, model.cl, model.cu) diff --git a/src/types/misc/StandardModel.jl b/src/types/misc/ObjectModel.jl similarity index 87% rename from src/types/misc/StandardModel.jl rename to src/types/misc/ObjectModel.jl index 04d3dc196..d0f11231b 100644 --- a/src/types/misc/StandardModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -2,9 +2,9 @@ """ $(TYPEDSIGNATURES) -Shallow copy of a [`StandardModel`](@ref) +Shallow copy of a [`ObjectModel`](@ref) """ -Base.copy(m::StandardModel) = StandardModel( +Base.copy(m::ObjectModel) = ObjectModel( m.id, reactions = m.reactions, metabolites = m.metabolites, @@ -55,7 +55,7 @@ $(TYPEDSIGNATURES) Return the lower bounds for all reactions in `model`. Order matches that of the reaction IDs returned by [`reactions`](@ref). """ -lower_bounds(model::StandardModel)::Vector{Float64} = +lower_bounds(model::ObjectModel)::Vector{Float64} = [model.reactions[rxn].lower_bound for rxn in reactions(model)] """ @@ -64,5 +64,5 @@ $(TYPEDSIGNATURES) Return the upper bounds for all reactions in `model`. Order matches that of the reaction IDs returned in [`reactions`](@ref). """ -upper_bounds(model::StandardModel)::Vector{Float64} = +upper_bounds(model::ObjectModel)::Vector{Float64} = [model.reactions[rxn].upper_bound for rxn in reactions(model)] diff --git a/src/types/models/CoreModelCoupled.jl b/src/types/models/CoreModelCoupled.jl deleted file mode 100644 index 550c8ade5..000000000 --- a/src/types/models/CoreModelCoupled.jl +++ /dev/null @@ -1,103 +0,0 @@ - -""" -$(TYPEDEF) - -A matrix-based wrap that adds reaction coupling matrix to the inner model. A -flux `x` feasible in this model must satisfy: -``` - cₗ ≤ C x ≤ cᵤ -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct CoreCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} - lm::M - C::SparseMat - cl::Vector{Float64} - cu::Vector{Float64} - - function CoreCoupling( - lm::M, - C::MatType, - cl::VecType, - cu::VecType, - ) where {M<:AbstractMetabolicModel} - length(cu) == length(cl) || - throw(DimensionMismatch("`cl` and `cu` need to have the same size")) - size(C) == (length(cu), n_reactions(lm)) || - throw(DimensionMismatch("wrong dimensions of `C`")) - - new{M}(lm, sparse(C), collect(cl), collect(cu)) - end -end - -""" -$(TYPEDSIGNATURES) - -Get the internal [`CoreModel`](@ref) out of [`CoreCoupling`](@ref). -""" -Accessors.unwrap_model(a::CoreCoupling) = a.lm - -""" -$(TYPEDSIGNATURES) - -Coupling constraint matrix for a `CoreCoupling`. -""" -Accessors.coupling(a::CoreCoupling)::SparseMat = vcat(coupling(a.lm), a.C) - -""" -$(TYPEDSIGNATURES) - -The number of coupling constraints in a `CoreCoupling`. -""" -Accessors.n_coupling_constraints(a::CoreCoupling)::Int = - n_coupling_constraints(a.lm) + size(a.C, 1) - -""" -$(TYPEDSIGNATURES) - -Coupling bounds for a `CoreCoupling`. -""" -Accessors.coupling_bounds(a::CoreCoupling)::Tuple{Vector{Float64},Vector{Float64}} = - vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) - -""" -$(TYPEDSIGNATURES) - -Make a `CoreCoupling` out of any compatible model type. -""" -function Base.convert( - ::Type{CoreCoupling{M}}, - mm::AbstractMetabolicModel; - clone_coupling = true, -) where {M} - if mm isa CoreCoupling{M} - mm - elseif mm isa CoreCoupling - CoreCoupling(convert(M, mm.lm), mm.C, mm.cl, mm.cu) - elseif clone_coupling - (cl, cu) = coupling_bounds(mm) - CoreCoupling(convert(M, mm), coupling(mm), cl, cu) - else - CoreCoupling(convert(M, mm), spzeros(0, n_reactions(mm)), spzeros(0), spzeros(0)) - end -end - -""" - const CoreModelCoupled = CoreCoupling{CoreModel} - -A matrix-based linear model with additional coupling constraints in the form: -``` - cₗ ≤ C x ≤ cᵤ -``` - -Internally, the model is implemented using [`CoreCoupling`](@ref) that contains a single [`CoreModel`](@ref). -""" -const CoreModelCoupled = CoreCoupling{CoreModel} - -CoreModelCoupled(lm::CoreModel, C::MatType, cl::VecType, cu::VecType) = - CoreCoupling(lm, sparse(C), collect(cl), collect(cu)) - -# these are special for CoreModel-ish models -@inherit_model_methods CoreModelCoupled (ridx::Int,) lm (ridx,) Accessors.reaction_stoichiometry diff --git a/src/types/models/MatrixCoupling.jl b/src/types/models/MatrixCoupling.jl new file mode 100644 index 000000000..786c0e54f --- /dev/null +++ b/src/types/models/MatrixCoupling.jl @@ -0,0 +1,103 @@ + +""" +$(TYPEDEF) + +A matrix-based wrap that adds reaction coupling matrix to the inner model. A +flux `x` feasible in this model must satisfy: +``` + cₗ ≤ C x ≤ cᵤ +``` + +# Fields +$(TYPEDFIELDS) +""" +mutable struct MatrixCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} + lm::M + C::SparseMat + cl::Vector{Float64} + cu::Vector{Float64} + + function MatrixCoupling( + lm::M, + C::MatType, + cl::VecType, + cu::VecType, + ) where {M<:AbstractMetabolicModel} + length(cu) == length(cl) || + throw(DimensionMismatch("`cl` and `cu` need to have the same size")) + size(C) == (length(cu), n_reactions(lm)) || + throw(DimensionMismatch("wrong dimensions of `C`")) + + new{M}(lm, sparse(C), collect(cl), collect(cu)) + end +end + +""" +$(TYPEDSIGNATURES) + +Get the internal [`MatrixModel`](@ref) out of [`MatrixCoupling`](@ref). +""" +Accessors.unwrap_model(a::MatrixCoupling) = a.lm + +""" +$(TYPEDSIGNATURES) + +Coupling constraint matrix for a `MatrixCoupling`. +""" +Accessors.coupling(a::MatrixCoupling)::SparseMat = vcat(coupling(a.lm), a.C) + +""" +$(TYPEDSIGNATURES) + +The number of coupling constraints in a `MatrixCoupling`. +""" +Accessors.n_coupling_constraints(a::MatrixCoupling)::Int = + n_coupling_constraints(a.lm) + size(a.C, 1) + +""" +$(TYPEDSIGNATURES) + +Coupling bounds for a `MatrixCoupling`. +""" +Accessors.coupling_bounds(a::MatrixCoupling)::Tuple{Vector{Float64},Vector{Float64}} = + vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) + +""" +$(TYPEDSIGNATURES) + +Make a `MatrixCoupling` out of any compatible model type. +""" +function Base.convert( + ::Type{MatrixCoupling{M}}, + mm::AbstractMetabolicModel; + clone_coupling = true, +) where {M} + if mm isa MatrixCoupling{M} + mm + elseif mm isa MatrixCoupling + MatrixCoupling(convert(M, mm.lm), mm.C, mm.cl, mm.cu) + elseif clone_coupling + (cl, cu) = coupling_bounds(mm) + MatrixCoupling(convert(M, mm), coupling(mm), cl, cu) + else + MatrixCoupling(convert(M, mm), spzeros(0, n_reactions(mm)), spzeros(0), spzeros(0)) + end +end + +""" + const MatrixModelWithCoupling = MatrixCoupling{MatrixModel} + +A matrix-based linear model with additional coupling constraints in the form: +``` + cₗ ≤ C x ≤ cᵤ +``` + +Internally, the model is implemented using [`MatrixCoupling`](@ref) that contains a single [`MatrixModel`](@ref). +""" +const MatrixModelWithCoupling = MatrixCoupling{MatrixModel} + +MatrixModelWithCoupling(lm::MatrixModel, C::MatType, cl::VecType, cu::VecType) = + MatrixCoupling(lm, sparse(C), collect(cl), collect(cu)) + +# these are special for MatrixModel-ish models +@inherit_model_methods MatrixModelWithCoupling (ridx::Int,) lm (ridx,) Accessors.reaction_stoichiometry diff --git a/src/types/models/CoreModel.jl b/src/types/models/MatrixModel.jl similarity index 62% rename from src/types/models/CoreModel.jl rename to src/types/models/MatrixModel.jl index cd28793d0..817ebf050 100644 --- a/src/types/models/CoreModel.jl +++ b/src/types/models/MatrixModel.jl @@ -13,7 +13,7 @@ s.t. S x = b # Fields $(TYPEDFIELDS) """ -mutable struct CoreModel <: AbstractMetabolicModel +mutable struct MatrixModel <: AbstractMetabolicModel S::SparseMat b::SparseVec c::SparseVec @@ -23,7 +23,7 @@ mutable struct CoreModel <: AbstractMetabolicModel mets::Vector{String} grrs::Vector{Maybe{GeneAssociation}} - function CoreModel( + function MatrixModel( S::MatType, b::VecType, c::VecType, @@ -50,53 +50,53 @@ end """ $(TYPEDSIGNATURES) -Get the reactions in a `CoreModel`. +Get the reactions in a `MatrixModel`. """ -Accessors.reactions(a::CoreModel)::Vector{String} = a.rxns +Accessors.reactions(a::MatrixModel)::Vector{String} = a.rxns """ $(TYPEDSIGNATURES) -Metabolites in a `CoreModel`. +Metabolites in a `MatrixModel`. """ -Accessors.metabolites(a::CoreModel)::Vector{String} = a.mets +Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets """ $(TYPEDSIGNATURES) -`CoreModel` stoichiometry matrix. +`MatrixModel` stoichiometry matrix. """ -Accessors.stoichiometry(a::CoreModel)::SparseMat = a.S +Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S """ $(TYPEDSIGNATURES) -`CoreModel` flux bounds. +`MatrixModel` flux bounds. """ -Accessors.bounds(a::CoreModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) +Accessors.bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) """ $(TYPEDSIGNATURES) -`CoreModel` target flux balance. +`MatrixModel` target flux balance. """ -Accessors.balance(a::CoreModel)::SparseVec = a.b +Accessors.balance(a::MatrixModel)::SparseVec = a.b """ $(TYPEDSIGNATURES) -`CoreModel` objective vector. +`MatrixModel` objective vector. """ -Accessors.objective(a::CoreModel)::SparseVec = a.c +Accessors.objective(a::MatrixModel)::SparseVec = a.c """ $(TYPEDSIGNATURES) -Collect all genes contained in the [`CoreModel`](@ref). The call is expensive +Collect all genes contained in the [`MatrixModel`](@ref). The call is expensive for large models, because the vector is not stored and instead gets rebuilt each time this function is called. """ -function Accessors.genes(a::CoreModel)::Vector{String} +function Accessors.genes(a::MatrixModel)::Vector{String} res = Set{String}() for grr in a.grrs isnothing(grr) && continue @@ -114,7 +114,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. Accessors.""" -Accessors.reaction_stoichiometry(m::CoreModel, rid::String)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) """ @@ -122,38 +122,38 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction at index `ridx`. """ -Accessors.reaction_stoichiometry(m::CoreModel, ridx)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::MatrixModel, ridx)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction +Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction index. """ -Accessors.reaction_gene_association(model::CoreModel, ridx::Int)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association(model::MatrixModel, ridx::Int)::Maybe{GeneAssociation} = model.grrs[ridx] """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociation`](@ref) from [`CoreModel`](@ref) by reaction ID. +Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID. """ -Accessors.reaction_gene_association(model::CoreModel, rid::String)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association(model::MatrixModel, rid::String)::Maybe{GeneAssociation} = model.grrs[first(indexin([rid], model.rxns))] """ $(TYPEDSIGNATURES) -Make a `CoreModel` out of any compatible model type. +Make a `MatrixModel` out of any compatible model type. """ -function Base.convert(::Type{CoreModel}, m::M) where {M<:AbstractMetabolicModel} - if typeof(m) == CoreModel +function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicModel} + if typeof(m) == MatrixModel return m end (xl, xu) = bounds(m) - CoreModel( + MatrixModel( stoichiometry(m), balance(m), objective(m), diff --git a/src/types/models/StandardModel.jl b/src/types/models/ObjectModel.jl similarity index 76% rename from src/types/models/StandardModel.jl rename to src/types/models/ObjectModel.jl index 73babf1e3..1c63bc9bd 100644 --- a/src/types/models/StandardModel.jl +++ b/src/types/models/ObjectModel.jl @@ -1,15 +1,15 @@ """ $(TYPEDEF) -`StandardModel` is used to store a constraint based metabolic model with +`ObjectModel` is used to store a constraint based metabolic model with meta-information. Meta-information is defined as annotation details, which include gene-reaction-rules, formulas, etc. This model type seeks to keep as much meta-information as possible, as opposed -to `CoreModel` and `CoreModelCoupled`, which keep the bare neccessities only. +to `MatrixModel` and `MatrixModelWithCoupling`, which keep the bare neccessities only. When merging models and keeping meta-information is important, use this as the model type. If meta-information is not important, use the more efficient core -model types. See [`CoreModel`](@ref) and [`CoreModelCoupled`](@ref) for +model types. See [`MatrixModel`](@ref) and [`MatrixModelWithCoupling`](@ref) for comparison. In this model, reactions, metabolites, and genes are stored in ordered @@ -18,7 +18,7 @@ dictionaries indexed by each struct's `id` field. For example, `"rxn1_id"`. This makes adding and removing reactions efficient. Note that the stoichiometric matrix (or any other core data, e.g. flux bounds) -is not stored directly as in `CoreModel`. When this model type is used in +is not stored directly as in `MatrixModel`. When this model type is used in analysis functions, these core data structures are built from scratch each time an analysis function is called. This can cause performance issues if you run many small analysis functions sequentially. Consider using the core model @@ -28,20 +28,20 @@ See also: [`Reaction`](@ref), [`Metabolite`](@ref), [`Gene`](@ref) # Example ``` -model = load_model(StandardModel, "my_model.json") +model = load_model(ObjectModel, "my_model.json") keys(model.reactions) ``` # Fields $(TYPEDFIELDS) """ -mutable struct StandardModel <: AbstractMetabolicModel +mutable struct ObjectModel <: AbstractMetabolicModel id::String reactions::OrderedDict{String,Reaction} metabolites::OrderedDict{String,Metabolite} genes::OrderedDict{String,Gene} - StandardModel( + ObjectModel( id = ""; reactions = OrderedDict{String,Reaction}(), metabolites = OrderedDict{String,Metabolite}(), @@ -57,14 +57,14 @@ Return a vector of reaction id strings contained in `model`. The order of reaction ids returned here matches the order used to construct the stoichiometric matrix. """ -Accessors.reactions(model::StandardModel)::StringVecType = collect(keys(model.reactions)) +Accessors.reactions(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) """ $(TYPEDSIGNATURES) Return the number of reactions contained in `model`. """ -Accessors.n_reactions(model::StandardModel)::Int = length(model.reactions) +Accessors.n_reactions(model::ObjectModel)::Int = length(model.reactions) """ @@ -74,7 +74,7 @@ Return a vector of metabolite id strings contained in `model`. The order of metabolite strings returned here matches the order used to construct the stoichiometric matrix. """ -Accessors.metabolites(model::StandardModel)::StringVecType = +Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) """ @@ -82,28 +82,28 @@ $(TYPEDSIGNATURES) Return the number of metabolites in `model`. """ -Accessors.n_metabolites(model::StandardModel)::Int = length(model.metabolites) +Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) """ $(TYPEDSIGNATURES) Return a vector of gene id strings in `model`. """ -Accessors.genes(model::StandardModel)::StringVecType = collect(keys(model.genes)) +Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) """ $(TYPEDSIGNATURES) Return the number of genes in `model`. """ -Accessors.n_genes(model::StandardModel)::Int = length(model.genes) +Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) """ $(TYPEDSIGNATURES) Return the stoichiometric matrix associated with `model` in sparse format. """ -function Accessors.stoichiometry(model::StandardModel)::SparseMat +function Accessors.stoichiometry(model::ObjectModel)::SparseMat n_entries = 0 for (_, r) in model.reactions for _ in r.metabolites @@ -145,7 +145,7 @@ $(TYPEDSIGNATURES) Return the lower and upper bounds, respectively, for reactions in `model`. Order matches that of the reaction ids returned in `reactions()`. """ -Accessors.bounds(model::StandardModel)::Tuple{Vector{Float64},Vector{Float64}} = +Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) """ @@ -154,14 +154,14 @@ $(TYPEDSIGNATURES) Return the balance of the linear problem, i.e. b in Sv = 0 where S is the stoichiometric matrix and v is the flux vector. """ -Accessors.balance(model::StandardModel)::SparseVec = spzeros(length(model.metabolites)) +Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) """ $(TYPEDSIGNATURES) Return sparse objective vector for `model`. """ -Accessors.objective(model::StandardModel)::SparseVec = +Accessors.objective(model::ObjectModel)::SparseVec = sparse([model.reactions[rid].objective_coefficient for rid in keys(model.reactions)]) """ @@ -171,7 +171,7 @@ Return the gene reaction rule in string format for reaction with `id` in `model` Return `nothing` if not available. """ Accessors.reaction_gene_association( - model::StandardModel, + model::ObjectModel, id::String, )::Maybe{GeneAssociation} = maybemap(identity, model.reactions[id].grr) @@ -181,7 +181,7 @@ $(TYPEDSIGNATURES) Return the formula of reaction `id` in `model`. Return `nothing` if not present. """ -Accessors.metabolite_formula(model::StandardModel, id::String)::Maybe{MetaboliteFormula} = +Accessors.metabolite_formula(model::ObjectModel, id::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.metabolites[id].formula) """ @@ -190,7 +190,7 @@ $(TYPEDSIGNATURES) Return the charge associated with metabolite `id` in `model`. Return nothing if not present. """ -Accessors.metabolite_charge(model::StandardModel, id::String)::Maybe{Int} = +Accessors.metabolite_charge(model::ObjectModel, id::String)::Maybe{Int} = model.metabolites[id].charge """ @@ -199,7 +199,7 @@ $(TYPEDSIGNATURES) Return compartment associated with metabolite `id` in `model`. Return `nothing` if not present. """ -Accessors.metabolite_compartment(model::StandardModel, id::String)::Maybe{String} = +Accessors.metabolite_compartment(model::ObjectModel, id::String)::Maybe{String} = model.metabolites[id].compartment """ @@ -208,7 +208,7 @@ $(TYPEDSIGNATURES) Return the subsystem associated with reaction `id` in `model`. Return `nothing` if not present. """ -Accessors.reaction_subsystem(model::StandardModel, id::String)::Maybe{String} = +Accessors.reaction_subsystem(model::ObjectModel, id::String)::Maybe{String} = model.reactions[id].subsystem """ @@ -217,7 +217,7 @@ $(TYPEDSIGNATURES) Return the notes associated with metabolite `id` in `model`. Return an empty Dict if not present. """ -Accessors.metabolite_notes(model::StandardModel, id::String)::Maybe{Notes} = +Accessors.metabolite_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.metabolites[id].notes """ @@ -226,7 +226,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with metabolite `id` in `model`. Return an empty Dict if not present. """ -Accessors.metabolite_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.metabolite_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.metabolites[id].annotations """ @@ -235,7 +235,7 @@ $(TYPEDSIGNATURES) Return the notes associated with gene `id` in `model`. Return an empty Dict if not present. """ -Accessors.gene_notes(model::StandardModel, gid::String) = model.genes[gid].notes +Accessors.gene_notes(model::ObjectModel, gid::String) = model.genes[gid].notes """ $(TYPEDSIGNATURES) @@ -243,7 +243,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with gene `id` in `model`. Return an empty Dict if not present. """ -Accessors.gene_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.gene_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.genes[id].annotations """ @@ -252,7 +252,7 @@ $(TYPEDSIGNATURES) Return the notes associated with reaction `id` in `model`. Return an empty Dict if not present. """ -Accessors.reaction_notes(model::StandardModel, id::String)::Maybe{Notes} = +Accessors.reaction_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.reactions[id].notes """ @@ -261,7 +261,7 @@ $(TYPEDSIGNATURES) Return the annotation associated with reaction `id` in `model`. Return an empty Dict if not present. """ -Accessors.reaction_annotations(model::StandardModel, id::String)::Maybe{Annotations} = +Accessors.reaction_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.reactions[id].annotations """ @@ -269,7 +269,7 @@ $(TYPEDSIGNATURES) Return the stoichiometry of reaction with ID `rid`. """ -Accessors.reaction_stoichiometry(m::StandardModel, rid::String)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::ObjectModel, rid::String)::Dict{String,Float64} = m.reactions[rid].metabolites """ @@ -277,31 +277,31 @@ $(TYPEDSIGNATURES) Return the name of reaction with ID `id`. """ -Accessors.reaction_name(m::StandardModel, rid::String) = m.reactions[rid].name +Accessors.reaction_name(m::ObjectModel, rid::String) = m.reactions[rid].name """ $(TYPEDSIGNATURES) Return the name of metabolite with ID `id`. """ -Accessors.metabolite_name(m::StandardModel, mid::String) = m.metabolites[mid].name +Accessors.metabolite_name(m::ObjectModel, mid::String) = m.metabolites[mid].name """ $(TYPEDSIGNATURES) Return the name of gene with ID `id`. """ -Accessors.gene_name(m::StandardModel, gid::String) = m.genes[gid].name +Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name """ $(TYPEDSIGNATURES) -Convert any `AbstractMetabolicModel` into a `StandardModel`. +Convert any `AbstractMetabolicModel` into a `ObjectModel`. Note, some data loss may occur since only the generic interface is used during the conversion process. """ -function Base.convert(::Type{StandardModel}, model::AbstractMetabolicModel) - if typeof(model) == StandardModel +function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) + if typeof(model) == ObjectModel return model end @@ -357,7 +357,7 @@ function Base.convert(::Type{StandardModel}, model::AbstractMetabolicModel) ) end - return StandardModel( + return ObjectModel( id; reactions = modelreactions, metabolites = modelmetabolites, diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl index d58481619..4a59b2b5e 100644 --- a/src/utils/Reaction.jl +++ b/src/utils/Reaction.jl @@ -54,7 +54,7 @@ is_boundary(model::AbstractMetabolicModel, rxn_id::String) = is_boundary(rxn::Reaction) = is_boundary(rxn.metabolites) -is_boundary(model::StandardModel, rxn::Reaction) = is_boundary(rxn) # for consistency with functions below +is_boundary(model::ObjectModel, rxn::Reaction) = is_boundary(rxn) # for consistency with functions below """ $(TYPEDSIGNATURES) @@ -86,7 +86,7 @@ function reaction_atom_balance(model::AbstractMetabolicModel, rxn_id::String) reaction_atom_balance(model, reaction_stoichiometry(model, rxn_id)) end -reaction_atom_balance(model::StandardModel, rxn::Reaction) = +reaction_atom_balance(model::ObjectModel, rxn::Reaction) = reaction_atom_balance(model, rxn.id) """ @@ -98,13 +98,13 @@ and the associated balance of atoms for convenience (useful if not balanced). Ca See also: [`check_duplicate_reaction`](@ref), [`reaction_atom_balance`](@ref) """ -reaction_mass_balanced(model::StandardModel, rxn_id::String) = +reaction_mass_balanced(model::ObjectModel, rxn_id::String) = all(values(reaction_atom_balance(model, rxn_id)) .== 0) -reaction_mass_balanced(model::StandardModel, rxn::Reaction) = +reaction_mass_balanced(model::ObjectModel, rxn::Reaction) = reaction_mass_balanced(model, rxn.id) -reaction_mass_balanced(model::StandardModel, reaction_dict::Dict{String,Float64}) = +reaction_mass_balanced(model::ObjectModel, reaction_dict::Dict{String,Float64}) = all(values(reaction_atom_balance(model, reaction_dict)) .== 0) """ diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index fd4dd226b..07b472f58 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -1,4 +1,4 @@ -@testset "Flux balance analysis with CoreModel" begin +@testset "Flux balance analysis with MatrixModel" begin cp = test_simpleLP() lp = flux_balance_analysis(cp, Tulip.Optimizer) @test termination_status(lp) == MOI.OPTIMAL @@ -13,7 +13,7 @@ @test sol ≈ [-1.0, 2.0] # test with a more biologically meaningfull model - cp = load_model(CoreModel, model_paths["iJR904.mat"]) + cp = load_model(MatrixModel, model_paths["iJR904.mat"]) expected_optimum = 0.9219480950504393 lp = flux_balance_analysis(cp, Tulip.Optimizer) @@ -30,9 +30,9 @@ @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) end -@testset "Flux balance analysis with StandardModel" begin +@testset "Flux balance analysis with ObjectModel" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) sol = flux_balance_analysis_dict( model, @@ -85,9 +85,9 @@ end ) end -@testset "Flux balance analysis with CoreModelCoupled" begin +@testset "Flux balance analysis with MatrixModelWithCoupling" begin - model = load_model(CoreModel, model_paths["e_coli_core.json"]) + model = load_model(MatrixModel, model_paths["e_coli_core.json"]) # assume coupling constraints of the form: # -γ ≤ vᵢ/μ ≤ γ @@ -112,7 +112,7 @@ end cub = spzeros(2 * nr) cub[nr+1:end] .= 1000 - cmodel = CoreModelCoupled(model, C, clb, cub) # construct + cmodel = MatrixModelWithCoupling(model, C, clb, cub) # construct dc = flux_balance_analysis_dict(cmodel, Tulip.Optimizer) @test isapprox(dc["BIOMASS_Ecoli_core_w_GAM"], 0.665585699298256, atol = TEST_TOLERANCE) diff --git a/test/analysis/flux_variability_analysis.jl b/test/analysis/flux_variability_analysis.jl index 6dce4dd15..c9835d116 100644 --- a/test/analysis/flux_variability_analysis.jl +++ b/test/analysis/flux_variability_analysis.jl @@ -18,7 +18,7 @@ @test isapprox(fluxes, [2 2], atol = TEST_TOLERANCE) # a special testcase for slightly sub-optimal FVA (gamma<1) - cp = CoreModel( + cp = MatrixModel( [-1.0 -1.0 -1.0], [0.0], [1.0, 0.0, 0.0], @@ -77,8 +77,8 @@ end ) end -@testset "Flux variability analysis with StandardModel" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) +@testset "Flux variability analysis with ObjectModel" begin + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) mins, maxs = flux_variability_analysis_dict( model, Tulip.Optimizer; diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index 3ac0cf7e4..33612b69b 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -1,5 +1,5 @@ @testset "single_knockout" begin - m = StandardModel() + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) @@ -47,7 +47,7 @@ end @testset "multiple_knockouts" begin - m = StandardModel() + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) @@ -95,7 +95,7 @@ end @testset "Knockouts on realistic models" begin for model in [ - load_model(StandardModel, model_paths["e_coli_core.json"]), #test on standardModel + load_model(ObjectModel, model_paths["e_coli_core.json"]), #test on standardModel load_model(model_paths["e_coli_core.json"]), #then on JSONModel with the same contents ] diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 0db2d0935..511008572 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -1,4 +1,4 @@ -@testset "Parsimonious flux balance analysis with StandardModel" begin +@testset "Parsimonious flux balance analysis with ObjectModel" begin model = test_toyModel() d = parsimonious_flux_balance_analysis_dict( diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl index 03841eb7f..8159e0883 100644 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ b/test/analysis/sampling/affine_hit_and_run.jl @@ -2,7 +2,7 @@ model = load_model(model_paths["e_coli_core.json"]) - cm = CoreCoupling(model, zeros(1, n_reactions(model)), [17.0], [19.0]) + cm = MatrixCoupling(model, zeros(1, n_reactions(model)), [17.0], [19.0]) pfk, tala = indexin(["PFK", "TALA"], reactions(cm)) cm.C[:, [pfk, tala]] .= 1.0 diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl index c4287ecca..507eda1d1 100644 --- a/test/analysis/sampling/warmup_variability.jl +++ b/test/analysis/sampling/warmup_variability.jl @@ -1,5 +1,5 @@ @testset "Warm up point generation" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) rid = "EX_glc__D_e" pts = warmup_from_variability( diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl index 692d6bd86..accdee175 100644 --- a/test/analysis/screening.jl +++ b/test/analysis/screening.jl @@ -27,7 +27,7 @@ @test screen(m, analysis = (a, b) -> b, args = [(1,), (2,)]) == [1, 2] # test modifying some reactions - quad_rxn(i) = (m::CoreModel) -> begin + quad_rxn(i) = (m::MatrixModel) -> begin mm = copy(m) mm.S = copy(m.S) mm.S[:, i] .^= 2 diff --git a/test/data_static.jl b/test/data_static.jl index a80d46d16..f06a71d5b 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -1,4 +1,4 @@ -test_LP() = CoreModel( +test_LP() = MatrixModel( zeros(4, 3), zeros(4), ones(3), @@ -8,7 +8,7 @@ test_LP() = CoreModel( ["m$x" for x = 1:4], ) -test_simpleLP() = CoreModel( +test_simpleLP() = MatrixModel( [ 1.0 1.0 -1.0 1.0 @@ -21,7 +21,7 @@ test_simpleLP() = CoreModel( ["m$x" for x = 1:2], ) -test_simpleLP2() = CoreModel( +test_simpleLP2() = MatrixModel( zeros(2, 2), [0.0, 0.0], [-0.25, 1.0], @@ -31,7 +31,7 @@ test_simpleLP2() = CoreModel( ["m$x" for x = 1:2], ) -test_sparseLP() = CoreModel( +test_sparseLP() = MatrixModel( sprand(4000, 3000, 0.5), sprand(4000, 0.5), sprand(3000, 0.5), @@ -41,8 +41,8 @@ test_sparseLP() = CoreModel( ["m$x" for x = 1:4000], ) -test_coupledLP() = CoreModelCoupled( - CoreModel( +test_coupledLP() = MatrixModelWithCoupling( + MatrixModel( sprand(4000, 3000, 0.5), sprand(4000, 0.5), sprand(3000, 0.5), @@ -56,7 +56,7 @@ test_coupledLP() = CoreModelCoupled( sprand(2000, 0.5), ) -test_toyModel() = CoreModel( +test_toyModel() = MatrixModel( [ -1.0 1.0 0.0 0.0 0.0 0.0 0.0 -2.0 0.0 1.0 0.0 0.0 0.0 0.0 diff --git a/test/io/h5.jl b/test/io/h5.jl index a27e3fab0..72fda108d 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -1,6 +1,6 @@ @testset "HDF5 model SBML model" begin - model = load_model(CoreModel, model_paths["e_coli_core.xml"]) + model = load_model(MatrixModel, model_paths["e_coli_core.xml"]) fn = "ecoli_test.h5" h5m = save_model(model, fn) @test h5m isa HDF5Model diff --git a/test/io/io.jl b/test/io/io.jl index 4eb037f0b..e7d4649ba 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -18,7 +18,7 @@ Set("r_" .* lowercase.(reactions(jsonmodel))) # specifically test parsing of gene-reaction associations in Recon - reconmodel = load_model(StandardModel, model_paths["Recon3D.json"]) + reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) @test n_reactions(reconmodel) == 10600 recon_grrs = [r.grr for (i, r) in reconmodel.reactions if !isnothing(r.grr)] @test length(recon_grrs) == 5938 diff --git a/test/io/json.jl b/test/io/json.jl index 1a7922266..5c297a5cc 100644 --- a/test/io/json.jl +++ b/test/io/json.jl @@ -1,7 +1,7 @@ -@testset "Test conversion from JSONModel to StandardModel" begin +@testset "Test conversion from JSONModel to ObjectModel" begin jsonmodel = load_model(model_paths["e_coli_core.json"]) - stdmodel = convert(StandardModel, jsonmodel) + stdmodel = convert(ObjectModel, jsonmodel) # test if same reaction ids @test issetequal(reactions(jsonmodel), reactions(stdmodel)) @@ -16,9 +16,9 @@ end @testset "Save JSON model" begin - model = load_model(CoreModel, model_paths["e_coli_core.json"]) + model = load_model(MatrixModel, model_paths["e_coli_core.json"]) testpath = tmpfile("modeltest.json") save_model(model, testpath) - wrote = convert(CoreModel, load_json_model(testpath)) + wrote = convert(MatrixModel, load_json_model(testpath)) @test isequal(model, wrote) end diff --git a/test/io/mat.jl b/test/io/mat.jl index 0c82717da..351eafbca 100644 --- a/test/io/mat.jl +++ b/test/io/mat.jl @@ -1,15 +1,15 @@ @testset "Import MAT model" begin - cp = load_model(CoreModel, model_paths["iJR904.mat"]) - @test cp isa CoreModel + cp = load_model(MatrixModel, model_paths["iJR904.mat"]) + @test cp isa MatrixModel @test size(cp.S) == (761, 1075) end @testset "Save MAT model" begin - loaded = load_model(CoreModel, model_paths["iJR904.mat"]) + loaded = load_model(MatrixModel, model_paths["iJR904.mat"]) testpath = tmpfile("iJR904-clone.mat") save_model(loaded, testpath) - wrote = load_model(CoreModel, testpath) - @test wrote isa CoreModel + wrote = load_model(MatrixModel, testpath) + @test wrote isa MatrixModel @test isequal(wrote, loaded) end diff --git a/test/io/sbml.jl b/test/io/sbml.jl index 4266cb1eb..9ff8ecdca 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -1,7 +1,7 @@ @testset "SBML import and conversion" begin sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - m = convert(CoreModel, sbmlm) + m = convert(MatrixModel, sbmlm) @test size(stoichiometry(sbmlm)) == (92, 95) @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_reactions(sbmlm)) @@ -13,14 +13,14 @@ @test metabolites(m)[1:3] == ["M_succoa_c", "M_ac_c", "M_fru_b"] @test reactions(m)[1:3] == ["R_EX_fum_e", "R_ACONTb", "R_GLNS"] - cm = convert(CoreModelCoupled, sbmlm) + cm = convert(MatrixModelWithCoupling, sbmlm) @test n_coupling_constraints(cm) == 0 end @testset "Save SBML model" begin - model = load_model(CoreModel, model_paths["e_coli_core.xml"]) + model = load_model(MatrixModel, model_paths["e_coli_core.xml"]) testpath = tmpfile("modeltest.xml") save_model(convert(SBMLModel, model), testpath) - wrote = convert(CoreModel, load_sbml_model(testpath)) + wrote = convert(MatrixModel, load_sbml_model(testpath)) @test isequal(model, wrote) end diff --git a/test/reconstruction/CoreModelCoupled.jl b/test/reconstruction/MatrixCoupling.jl similarity index 92% rename from test/reconstruction/CoreModelCoupled.jl rename to test/reconstruction/MatrixCoupling.jl index 2305e25fd..690254d6f 100644 --- a/test/reconstruction/CoreModelCoupled.jl +++ b/test/reconstruction/MatrixCoupling.jl @@ -1,7 +1,7 @@ @testset "Coupling constraints" begin - cp = convert(CoreModelCoupled, test_LP()) + cp = convert(MatrixModelWithCoupling, test_LP()) @test size(cp.lm.S) == (4, 3) - @test size(stoichiometry(convert(CoreModel, cp))) == (4, 3) + @test size(stoichiometry(convert(MatrixModel, cp))) == (4, 3) new_cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) @test n_coupling_constraints(cp) + 1 == n_coupling_constraints(new_cp) @@ -45,11 +45,11 @@ end @testset "Add reactions" begin - cp = convert(CoreModelCoupled, test_LP()) + cp = convert(MatrixModelWithCoupling, test_LP()) cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) new_cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0) - @test new_cp isa CoreModelCoupled + @test new_cp isa MatrixModelWithCoupling @test cp.C == new_cp.C[:, 1:end-1] @test cp.cl == new_cp.cl @test cp.cu == new_cp.cu @@ -100,7 +100,7 @@ end @test cp.cl == new_cp.cl @test cp.cu == new_cp.cu - cm = CoreModel( + cm = MatrixModel( 2.0 * ones(4, 10), 3 .* ones(4), 2 .* ones(10), @@ -116,11 +116,11 @@ end end @testset "Remove reactions" begin - cp = convert(CoreModelCoupled, test_LP()) + cp = convert(MatrixModelWithCoupling, test_LP()) cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_reactions(cp)), -1.0, 1.0) new_cp = remove_reactions(cp, [3, 2]) - @test new_cp isa CoreModelCoupled + @test new_cp isa MatrixModelWithCoupling @test new_cp.C[:] == cp.C[:, 1] @test new_cp.cl == cp.cl @test new_cp.cu == cp.cu @@ -148,8 +148,8 @@ end end @testset "Change bounds" begin - cp = convert(CoreModelCoupled, test_LP()) - @test cp isa CoreModelCoupled + cp = convert(MatrixModelWithCoupling, test_LP()) + @test cp isa MatrixModelWithCoupling change_bound!(cp, 1, lower = -10, upper = 10) @test cp.lm.xl[1] == -10 diff --git a/test/reconstruction/CoreModel.jl b/test/reconstruction/MatrixModel.jl similarity index 98% rename from test/reconstruction/CoreModel.jl rename to test/reconstruction/MatrixModel.jl index c8a58ae9e..2fb3d512f 100644 --- a/test/reconstruction/CoreModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -192,7 +192,7 @@ end cp = remove_reactions(cp, ["r2"]) @test size(cp.S) == (4, 1) - lp = CoreModel( + lp = MatrixModel( [1.0 1 1 0; 1 1 1 0; 1 1 1 0; 0 0 0 1], collect(1.0:4), collect(1.0:4), @@ -213,7 +213,7 @@ end end @testset "Remove metabolites" begin - model = load_model(CoreModel, model_paths["e_coli_core.json"]) + model = load_model(MatrixModel, model_paths["e_coli_core.json"]) m1 = remove_metabolites(model, ["glc__D_e", "for_c"]) m2 = remove_metabolite(model, "glc__D_e") diff --git a/test/reconstruction/StandardModel.jl b/test/reconstruction/ObjectModel.jl similarity index 99% rename from test/reconstruction/StandardModel.jl rename to test/reconstruction/ObjectModel.jl index 97ab4a3df..fc757c219 100644 --- a/test/reconstruction/StandardModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -26,7 +26,7 @@ rxns = [r1, r2] - model = StandardModel() + model = ObjectModel() model.id = "model" model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) diff --git a/test/reconstruction/Reaction.jl b/test/reconstruction/Reaction.jl index b57b468fe..7b08f1279 100644 --- a/test/reconstruction/Reaction.jl +++ b/test/reconstruction/Reaction.jl @@ -1,5 +1,5 @@ @testset "Construction overloading" begin - model = load_model(StandardModel, model_paths["iJO1366.json"]) + model = load_model(ObjectModel, model_paths["iJO1366.json"]) rxn_original = model.reactions["NADH16pp"] nadh = model.metabolites["nadh_c"] diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl index 53ad43d89..23996d165 100644 --- a/test/reconstruction/add_reactions.jl +++ b/test/reconstruction/add_reactions.jl @@ -1,5 +1,5 @@ @testset "@add_reactions! helper" begin - mod = StandardModel() + mod = ObjectModel() A = Metabolite("A") B = Metabolite("B") C = Metabolite("C") diff --git a/test/reconstruction/community.jl b/test/reconstruction/community.jl index fc0875799..e038b9601 100644 --- a/test/reconstruction/community.jl +++ b/test/reconstruction/community.jl @@ -1,9 +1,9 @@ -@testset "CoreModel: Detailed community stoichiometrix matrix check" begin +@testset "MatrixModel: Detailed community stoichiometrix matrix check" begin m1 = test_toyModel() m2 = test_toyModel() ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - c1 = join_with_exchanges(CoreModel, [m1, m2], ex_rxn_mets) + c1 = join_with_exchanges(MatrixModel, [m1, m2], ex_rxn_mets) # test of stoichs are the same @test all(c1.S[1:6, 1:7] .== c1.S[7:12, 8:14]) @@ -32,7 +32,7 @@ @test c1.S[12, end] == -1.0 c2 = join_with_exchanges( - CoreModel, + MatrixModel, [m1, m2], ex_rxn_mets; biomass_ids = ["biomass1", "biomass1"], @@ -53,9 +53,9 @@ @test c2.S[16, end] == -0.9 end -@testset "CoreModel: Small model join" begin +@testset "MatrixModel: Small model join" begin m1 = load_model(model_paths["e_coli_core.json"]) - m2 = load_model(CoreModel, model_paths["e_coli_core.json"]) + m2 = load_model(MatrixModel, model_paths["e_coli_core.json"]) exchange_rxn_mets = Dict( ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for @@ -65,7 +65,7 @@ end biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] community = join_with_exchanges( - CoreModel, + MatrixModel, [m1, m2], exchange_rxn_mets; biomass_ids = biomass_ids, @@ -88,9 +88,9 @@ end @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) end -@testset "CoreModel: Heterogenous model join" begin - m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) - m2 = load_model(CoreModel, model_paths["iJO1366.mat"]) +@testset "MatrixModel: Heterogenous model join" begin + m1 = load_model(MatrixModel, model_paths["e_coli_core.json"]) + m2 = load_model(MatrixModel, model_paths["iJO1366.mat"]) exchange_rxn_mets = Dict( ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for @@ -100,7 +100,7 @@ end biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ec_iJO1366_core_53p95M"] community = join_with_exchanges( - CoreModel, + MatrixModel, [m1, m2], exchange_rxn_mets; biomass_ids = biomass_ids, @@ -138,8 +138,8 @@ end @test isapprox(d["community_biomass"], 0.8739215069675402, atol = TEST_TOLERANCE) end -@testset "CoreModel: Community model modifications" begin - m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) +@testset "MatrixModel: Community model modifications" begin + m1 = load_model(MatrixModel, model_paths["e_coli_core.json"]) exchange_rxn_mets = Dict( ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for @@ -149,14 +149,14 @@ end biomass_ids = ["BIOMASS_Ecoli_core_w_GAM"] community = - join_with_exchanges(CoreModel, [m1], exchange_rxn_mets; biomass_ids = biomass_ids) + join_with_exchanges(MatrixModel, [m1], exchange_rxn_mets; biomass_ids = biomass_ids) env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) m1_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m1)) community.xl[env_ex_inds] .= m1.xl[m1_ex_inds] community.xu[env_ex_inds] .= m1.xu[m1_ex_inds] - m2 = load_model(CoreModel, model_paths["e_coli_core.json"]) + m2 = load_model(MatrixModel, model_paths["e_coli_core.json"]) community = add_model_with_exchanges( community, @@ -179,12 +179,12 @@ end @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) end -@testset "StandardModel: Detailed community stoichiometrix matrix check" begin +@testset "ObjectModel: Detailed community stoichiometrix matrix check" begin m1 = test_toyModel() m2 = test_toyModel() ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - c1 = join_with_exchanges(StandardModel, [m1, m2], ex_rxn_mets) + c1 = join_with_exchanges(ObjectModel, [m1, m2], ex_rxn_mets) @test size(stoichiometry(c1)) == (14, 16) # test if each models exchange reactions have been added to the environmental exchange properly @@ -214,7 +214,7 @@ end @test c1.reactions["community_biomass"].metabolites["species_1_biomass[c]"] == -1 c2 = join_with_exchanges( - StandardModel, + ObjectModel, [m1, m2], ex_rxn_mets; biomass_ids = ["biomass1", "biomass1"], @@ -225,8 +225,8 @@ end @test isempty(c2.reactions["community_biomass"].metabolites) end -@testset "StandardModel: coarse community models checks" begin - m1 = load_model(StandardModel, model_paths["e_coli_core.json"]) +@testset "ObjectModel: coarse community models checks" begin + m1 = load_model(ObjectModel, model_paths["e_coli_core.json"]) exchange_rxn_mets = Dict( ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for @@ -236,7 +236,7 @@ end biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] c = join_with_exchanges( - StandardModel, + ObjectModel, [m1, m1], exchange_rxn_mets; biomass_ids = biomass_ids, diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 884b31063..b8631beb1 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -2,7 +2,7 @@ #= Implement the small model that should be gapfilled. =# - model = StandardModel("partial model") + model = ObjectModel("partial model") (m1, m2, m3, m4, m5, m6, m7, m8) = Metabolite.("m$i" for i = 1:8) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 686e64f4f..127a54427 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -1,5 +1,5 @@ @testset "GECKO" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) get_reaction_isozymes = rid -> @@ -72,7 +72,7 @@ end original GECKO paper. This model is nice to troubleshoot with, because the stoich matrix is small. =# - m = StandardModel("gecko") + m = ObjectModel("gecko") m1 = Metabolite("m1") m2 = Metabolite("m2") m3 = Metabolite("m3") diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl index e7d5d4388..e005d008e 100644 --- a/test/reconstruction/knockouts.jl +++ b/test/reconstruction/knockouts.jl @@ -6,7 +6,7 @@ is available, but inside a group all of the genes need to be available """ @testset "knockout_single_gene" begin - m = StandardModel() + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) @@ -40,7 +40,7 @@ is available, but inside a group all of the genes need to be available end @testset "knockout_multiple_genes" begin - m = StandardModel() + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) diff --git a/test/reconstruction/smoment.jl b/test/reconstruction/smoment.jl index cba241217..8520cc315 100644 --- a/test/reconstruction/smoment.jl +++ b/test/reconstruction/smoment.jl @@ -1,5 +1,5 @@ @testset "SMOMENT" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) diff --git a/test/types/Gene.jl b/test/types/Gene.jl index 92c9e2fa0..12cf99fa7 100644 --- a/test/types/Gene.jl +++ b/test/types/Gene.jl @@ -20,7 +20,7 @@ g3.annotations = Dict("sboterm" => ["sbo3"], "ncbigene" => ["ads"]) g4 = Gene("g4") g4.annotations = Dict("sboterm" => ["sbo4"], "ncbigene" => ["ads22", "asd22s"]) - gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in StandardModel + gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in ObjectModel idx = annotation_index(gdict) @test length(idx["ncbigene"]["ads"]) > 1 diff --git a/test/types/JSONModel.jl b/test/types/JSONModel.jl index 3c63a6895..3cdf11fe8 100644 --- a/test/types/JSONModel.jl +++ b/test/types/JSONModel.jl @@ -2,7 +2,7 @@ json_model = model_paths["iJO1366.json"] jm = load_json_model(json_model) - sm = convert(StandardModel, jm) + sm = convert(ObjectModel, jm) jm2 = convert(JSONModel, sm) @test Set(reactions(jm)) == Set(reactions(sm)) diff --git a/test/types/MATModel.jl b/test/types/MATModel.jl index 10103d8d1..e575e8a0a 100644 --- a/test/types/MATModel.jl +++ b/test/types/MATModel.jl @@ -3,7 +3,7 @@ filename = model_paths["iJO1366.mat"] mm = load_mat_model(filename) - sm = convert(StandardModel, mm) + sm = convert(ObjectModel, mm) mm2 = convert(MATModel, sm) @test Set(reactions(mm)) == Set(reactions(sm)) diff --git a/test/types/CoreModelCoupled.jl b/test/types/MatrixCoupling.jl similarity index 51% rename from test/types/CoreModelCoupled.jl rename to test/types/MatrixCoupling.jl index 5258b0f5d..2c2ad3de5 100644 --- a/test/types/CoreModelCoupled.jl +++ b/test/types/MatrixCoupling.jl @@ -1,16 +1,16 @@ -@testset "CoreModelCoupled generic interface" begin - model = load_model(CoreModelCoupled, model_paths["e_coli_core.mat"]) +@testset "MatrixModelWithCoupling generic interface" begin + model = load_model(MatrixModelWithCoupling, model_paths["e_coli_core.mat"]) @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) end -@testset "Conversion from and to StandardModel" begin - cm = load_model(CoreModelCoupled, model_paths["e_coli_core.mat"]) +@testset "Conversion from and to ObjectModel" begin + cm = load_model(MatrixModelWithCoupling, model_paths["e_coli_core.mat"]) - sm = convert(StandardModel, cm) - cm2 = convert(CoreModelCoupled, sm) + sm = convert(ObjectModel, cm) + cm2 = convert(MatrixModelWithCoupling, sm) @test Set(reactions(cm)) == Set(reactions(sm)) @test Set(reactions(cm)) == Set(reactions(cm2)) diff --git a/test/types/CoreModel.jl b/test/types/MatrixModel.jl similarity index 55% rename from test/types/CoreModel.jl rename to test/types/MatrixModel.jl index 61e9ba96a..1be562143 100644 --- a/test/types/CoreModel.jl +++ b/test/types/MatrixModel.jl @@ -1,15 +1,15 @@ -@testset "CoreModel generic interface" begin - model = load_model(CoreModel, model_paths["e_coli_core.mat"]) +@testset "MatrixModel generic interface" begin + model = load_model(MatrixModel, model_paths["e_coli_core.mat"]) @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) end -@testset "Conversion from and to StandardModel" begin - cm = load_model(CoreModel, model_paths["e_coli_core.mat"]) +@testset "Conversion from and to ObjectModel" begin + cm = load_model(MatrixModel, model_paths["e_coli_core.mat"]) - sm = convert(StandardModel, cm) - cm2 = convert(CoreModel, sm) + sm = convert(ObjectModel, cm) + cm2 = convert(MatrixModel, sm) @test Set(reactions(cm)) == Set(reactions(sm)) @test Set(reactions(cm)) == Set(reactions(cm2)) diff --git a/test/types/StandardModel.jl b/test/types/ObjectModel.jl similarity index 94% rename from test/types/StandardModel.jl rename to test/types/ObjectModel.jl index 4bdf28165..7c8f6ce3b 100644 --- a/test/types/StandardModel.jl +++ b/test/types/ObjectModel.jl @@ -1,4 +1,4 @@ -@testset "StandardModel generic interface" begin +@testset "ObjectModel generic interface" begin # create a small model m1 = Metabolite("m1") m1.formula = "C2H3" @@ -37,13 +37,13 @@ gs = [g1, g2, g3] rxns = [r1, r2, r3, r4] - model = StandardModel() + model = ObjectModel() model.id = "model" model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) model.genes = OrderedDict(g.id => g for g in gs) - @test contains(sprint(show, MIME("text/plain"), model), "StandardModel") + @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") @test "r1" in reactions(model) @test "m4" in metabolites(model) @@ -116,11 +116,11 @@ @test reaction_stoichiometry(model, "r1") == Dict("m1" => -1.0, "m2" => 1.0) # To do: test convert - same_model = convert(StandardModel, model) + same_model = convert(ObjectModel, model) @test same_model == model jsonmodel = convert(JSONModel, model) - stdmodel = convert(StandardModel, jsonmodel) + stdmodel = convert(ObjectModel, jsonmodel) @test issetequal(reactions(jsonmodel), reactions(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) diff --git a/test/types/SBMLModel.jl b/test/types/SBMLModel.jl index 554ff60e7..3753f484b 100644 --- a/test/types/SBMLModel.jl +++ b/test/types/SBMLModel.jl @@ -1,7 +1,7 @@ @testset "Conversion from and to SBML model" begin sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - sm = convert(StandardModel, sbmlm) + sm = convert(ObjectModel, sbmlm) sbmlm2 = convert(SBMLModel, sm) @test Set(reactions(sbmlm)) == Set(reactions(sbmlm2)) diff --git a/test/types/abstract/MetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl similarity index 100% rename from test/types/abstract/MetabolicModel.jl rename to test/types/abstract/AbstractMetabolicModel.jl diff --git a/test/utils/CoreModel.jl b/test/utils/MatrixModel.jl similarity index 90% rename from test/utils/CoreModel.jl rename to test/utils/MatrixModel.jl index 91b4b9c66..64c3121c5 100644 --- a/test/utils/CoreModel.jl +++ b/test/utils/MatrixModel.jl @@ -1,4 +1,4 @@ -@testset "CoreModel utilities" begin +@testset "MatrixModel utilities" begin cp = test_LP() @test n_reactions(cp) == 3 @test n_metabolites(cp) == 4 diff --git a/test/utils/StandardModel.jl b/test/utils/ObjectModel.jl similarity index 88% rename from test/utils/StandardModel.jl rename to test/utils/ObjectModel.jl index f5b1333c5..5ce5dd2a0 100644 --- a/test/utils/StandardModel.jl +++ b/test/utils/ObjectModel.jl @@ -1,5 +1,5 @@ -@testset "StandardModel utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) +@testset "ObjectModel utilities" begin + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # FBA fluxes = flux_balance_analysis_dict( diff --git a/test/utils/Serialized.jl b/test/utils/Serialized.jl index 5e95d1424..646c29a18 100644 --- a/test/utils/Serialized.jl +++ b/test/utils/Serialized.jl @@ -5,8 +5,8 @@ sm = serialize_model(m, tmpfile("toy1.serialized")) sm2 = serialize_model(sm, tmpfile("toy2.serialized")) - @test typeof(sm) == Serialized{CoreModel} # expected type - @test typeof(sm2) == Serialized{CoreModel} # no multi-layer serialization + @test typeof(sm) == Serialized{MatrixModel} # expected type + @test typeof(sm2) == Serialized{MatrixModel} # no multi-layer serialization precache!(sm) @@ -14,12 +14,12 @@ @test sm2.m == nothing # nothing is cached here @test isequal(m, deserialize(tmpfile("toy2.serialized"))) # it was written as-is @test issetequal( - reactions(convert(StandardModel, sm)), - reactions(convert(StandardModel, sm2)), + reactions(convert(ObjectModel, sm)), + reactions(convert(ObjectModel, sm2)), ) sm.m = nothing @test issetequal( - metabolites(convert(CoreModelCoupled, sm)), - metabolites(convert(CoreModelCoupled, sm2)), + metabolites(convert(MatrixModelWithCoupling, sm)), + metabolites(convert(MatrixModelWithCoupling, sm2)), ) end diff --git a/test/utils/fluxes.jl b/test/utils/fluxes.jl index 5dbc3129b..ad3906138 100644 --- a/test/utils/fluxes.jl +++ b/test/utils/fluxes.jl @@ -1,5 +1,5 @@ @testset "Flux utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) fluxes = flux_balance_analysis_dict( model, diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index 6b88dccb5..9c813ef6b 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -1,11 +1,11 @@ -@testset "Looks like in CoreModel, detailed test" begin +@testset "Looks like in MatrixModel, detailed test" begin cp = test_LP() @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) cp = test_simpleLP() @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) - cp = CoreModel( + cp = MatrixModel( [-1.0 -1 -2; 0 -1 0; 0 0 0], zeros(3), ones(3), @@ -16,7 +16,7 @@ ) @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1"] - cp = CoreModel( + cp = MatrixModel( [-1.0 0 0; 0 0 -1; 0 -1 0], zeros(3), ones(3), @@ -66,19 +66,19 @@ end @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - model = convert(StandardModel, model) + model = convert(ObjectModel, model) @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - model = convert(CoreModelCoupled, model) + model = convert(MatrixModelWithCoupling, model) @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 end @testset "Ontology usage in is_xxx_reaction" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # macro generated, so only test positive and negative case @test !is_biomass_reaction(model, "PFL") diff --git a/test/utils/reaction.jl b/test/utils/reaction.jl index 8d68f307e..149f1ea43 100644 --- a/test/utils/reaction.jl +++ b/test/utils/reaction.jl @@ -1,5 +1,5 @@ @testset "Reaction utilities" begin - model = load_model(StandardModel, model_paths["e_coli_core.json"]) + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # FBA fluxes = flux_balance_analysis_dict( From 39d4d95e8e54ab1b3da6934baa421c57ab492bc8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 25 Oct 2022 14:36:20 +0200 Subject: [PATCH 041/531] more formatting --- src/reconstruction/MatrixCoupling.jl | 6 +++++- src/types/misc/MatrixModel.jl | 3 ++- src/types/misc/ObjectModel.jl | 8 ++------ src/types/models/MatrixModel.jl | 6 ++++-- src/types/models/ObjectModel.jl | 3 +-- 5 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index a75289e0b..2131ef19e 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -82,7 +82,11 @@ $(TYPEDSIGNATURES) Add all reactions from `m2` to `m1`. """ -function add_reactions(m1::MatrixModelWithCoupling, m2::MatrixModel; check_consistency = false) +function add_reactions( + m1::MatrixModelWithCoupling, + m2::MatrixModel; + check_consistency = false, +) new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, diff --git a/src/types/misc/MatrixModel.jl b/src/types/misc/MatrixModel.jl index 7216e0242..3c854f917 100644 --- a/src/types/misc/MatrixModel.jl +++ b/src/types/misc/MatrixModel.jl @@ -16,4 +16,5 @@ Base.isequal(model1::MatrixModelWithCoupling, model2::MatrixModelWithCoupling) = isequal(model1.cl, model2.cl) && isequal(model1.cu, model2.cu) -Base.copy(model::MatrixModelWithCoupling) = MatrixModelWithCoupling(model.lm, model.C, model.cl, model.cu) +Base.copy(model::MatrixModelWithCoupling) = + MatrixModelWithCoupling(model.lm, model.C, model.cl, model.cu) diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index d0f11231b..1a5e79998 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -4,12 +4,8 @@ $(TYPEDSIGNATURES) Shallow copy of a [`ObjectModel`](@ref) """ -Base.copy(m::ObjectModel) = ObjectModel( - m.id, - reactions = m.reactions, - metabolites = m.metabolites, - genes = m.genes, -) +Base.copy(m::ObjectModel) = + ObjectModel(m.id, reactions = m.reactions, metabolites = m.metabolites, genes = m.genes) """ $(TYPEDSIGNATURES) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 817ebf050..3f7c00348 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -139,8 +139,10 @@ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID. """ -Accessors.reaction_gene_association(model::MatrixModel, rid::String)::Maybe{GeneAssociation} = - model.grrs[first(indexin([rid], model.rxns))] +Accessors.reaction_gene_association( + model::MatrixModel, + rid::String, +)::Maybe{GeneAssociation} = model.grrs[first(indexin([rid], model.rxns))] """ $(TYPEDSIGNATURES) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 1c63bc9bd..dddf8d93d 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -74,8 +74,7 @@ Return a vector of metabolite id strings contained in `model`. The order of metabolite strings returned here matches the order used to construct the stoichiometric matrix. """ -Accessors.metabolites(model::ObjectModel)::StringVecType = - collect(keys(model.metabolites)) +Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) """ $(TYPEDSIGNATURES) From 3fde62e29ab5b40a968e033836bbfca29091e42f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 25 Oct 2022 14:42:07 +0200 Subject: [PATCH 042/531] fix model/wrapper ordering --- src/types/{models => wrappers}/MatrixCoupling.jl | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/types/{models => wrappers}/MatrixCoupling.jl (100%) diff --git a/src/types/models/MatrixCoupling.jl b/src/types/wrappers/MatrixCoupling.jl similarity index 100% rename from src/types/models/MatrixCoupling.jl rename to src/types/wrappers/MatrixCoupling.jl From 93220ab1276444eabd7fa30e962d42a675b084ee Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 2 Nov 2022 12:19:52 +0100 Subject: [PATCH 043/531] JuliaFormatter: improve formatting of modules --- .JuliaFormatter.toml | 2 + src/analysis.jl | 4 +- src/io.jl | 4 +- src/log.jl | 116 +++++++++++++++++++++--------------------- src/reconstruction.jl | 4 +- src/types.jl | 14 ++--- 6 files changed, 73 insertions(+), 71 deletions(-) create mode 100644 .JuliaFormatter.toml diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml new file mode 100644 index 000000000..c67ebb9c1 --- /dev/null +++ b/.JuliaFormatter.toml @@ -0,0 +1,2 @@ +indent_submodule = true +margin = 92 diff --git a/src/analysis.jl b/src/analysis.jl index 7a249c5c4..451cf9af2 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -50,8 +50,8 @@ $(EXPORTS) """ module Modifications # TODO move this into Solver -using ..ModuleTools -@dse + using ..ModuleTools + @dse end @export_locals diff --git a/src/io.jl b/src/io.jl index b5300175d..53e5cb5ad 100644 --- a/src/io.jl +++ b/src/io.jl @@ -30,8 +30,8 @@ Internal IO helpers. $(EXPORTS) """ module Internal -using ..ModuleTools -@dse + using ..ModuleTools + @dse end @export_locals diff --git a/src/log.jl b/src/log.jl index 2365c7fb4..85a801ca3 100644 --- a/src/log.jl +++ b/src/log.jl @@ -20,67 +20,67 @@ Internal helpers for logging. $(EXPORTS) """ module Internal -using ..ModuleTools -@dse -""" -$(TYPEDSIGNATURES) - -This creates a group of functions that allow masking out topic-related logging -actions. A call that goes as follows: - - @make_logging_tag XYZ - -creates the following tools: - -- global variable `XYZ_log_enabled` defaulted to false -- function `log_XYZ` that can be called to turn the logging on/off -- a masking macro `@XYZ_log` that can be prepended to commands that should - only happen if the logging of tag XYZ is enabled. - -The masking macro is then used as follows: - - @XYZ_log @info "This is the extra verbose information you wanted!" a b c - -The user can direct logging with these: - - log_XYZ() - log_XYZ(false) - -`doc` should be a name of the stuff that is being printed if the corresponding -log_XYZ() is enabled -- it is used to create a friendly documentation for the -logging switch. In this case it could say `"X, Y and Z-related messages"`. -""" -macro make_logging_tag(sym::Symbol, doc::String) - enable_flag = Symbol(sym, :_log_enabled) - enable_fun = Symbol(:log_, sym) - log_macro = Symbol(sym, :_log) - # esc() is necessary here because the internal macro processing would - # otherwise bind the variables incorrectly. - esc(:( - begin - $enable_flag = false - - """ - $(string($enable_fun))(enable::Bool=true) - - Enable (default) or disable (by passing `false`) output of $($doc). - """ - $enable_fun(x::Bool = true) = begin - global $enable_flag = x + using ..ModuleTools + @dse + """ + $(TYPEDSIGNATURES) + + This creates a group of functions that allow masking out topic-related logging + actions. A call that goes as follows: + + @make_logging_tag XYZ + + creates the following tools: + + - global variable `XYZ_log_enabled` defaulted to false + - function `log_XYZ` that can be called to turn the logging on/off + - a masking macro `@XYZ_log` that can be prepended to commands that should + only happen if the logging of tag XYZ is enabled. + + The masking macro is then used as follows: + + @XYZ_log @info "This is the extra verbose information you wanted!" a b c + + The user can direct logging with these: + + log_XYZ() + log_XYZ(false) + + `doc` should be a name of the stuff that is being printed if the corresponding + log_XYZ() is enabled -- it is used to create a friendly documentation for the + logging switch. In this case it could say `"X, Y and Z-related messages"`. + """ + macro make_logging_tag(sym::Symbol, doc::String) + enable_flag = Symbol(sym, :_log_enabled) + enable_fun = Symbol(:log_, sym) + log_macro = Symbol(sym, :_log) + # esc() is necessary here because the internal macro processing would + # otherwise bind the variables incorrectly. + esc(:( + begin + $enable_flag = false + + """ + $(string($enable_fun))(enable::Bool=true) + + Enable (default) or disable (by passing `false`) output of $($doc). + """ + $enable_fun(x::Bool = true) = begin + global $enable_flag = x + end + + macro $log_macro(x) + $enable_flag ? x : nothing + end end + )) + end - macro $log_macro(x) - $enable_flag ? x : nothing - end - end - )) -end + @make_logging_tag models "model-related messages" + @make_logging_tag io "messages and warnings from model input/output" + @make_logging_tag perf "performance-related tracing information" -@make_logging_tag models "model-related messages" -@make_logging_tag io "messages and warnings from model input/output" -@make_logging_tag perf "performance-related tracing information" - -@export_locals + @export_locals end #Internal import .Internal: log_models, log_io, log_perf diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 55bbdc44c..b6a763d1d 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -33,8 +33,8 @@ Functions that create model variants, typically for efficient use in $(EXPORTS) """ module Modifications -using ..ModuleTools -@dse + using ..ModuleTools + @dse end @export_locals diff --git a/src/types.jl b/src/types.jl index 56e14474a..e53077459 100644 --- a/src/types.jl +++ b/src/types.jl @@ -52,13 +52,13 @@ Internal helpers for types. $(EXPORTS) """ module Internal -using ..ModuleTools -@dse -# TODO: Note to self: we might be a bit more systematic here -- these are -# "pre-includes" (might go into bits/), contrasting to "post-includes" (which -# may stay in misc/) -@inc_dir types accessors misc -@export_locals + using ..ModuleTools + @dse + # TODO: Note to self: we might be a bit more systematic here -- these are + # "pre-includes" (might go into bits/), contrasting to "post-includes" (which + # may stay in misc/) + @inc_dir types accessors misc + @export_locals end using .Internal From 9b377e5f9367ddaa15bedc7f3f8b570b08b0572e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 2 Nov 2022 12:45:13 +0100 Subject: [PATCH 044/531] de-default the format width --- .JuliaFormatter.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/.JuliaFormatter.toml b/.JuliaFormatter.toml index c67ebb9c1..fd43e5e9c 100644 --- a/.JuliaFormatter.toml +++ b/.JuliaFormatter.toml @@ -1,2 +1 @@ indent_submodule = true -margin = 92 From b50a0ad76953df193c7a5538e1a06551f52cd60e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 27 Oct 2022 16:52:54 +0200 Subject: [PATCH 045/531] Update OM model --- src/reconstruction/MatrixModel.jl | 3 +- src/reconstruction/ObjectModel.jl | 13 ++-- src/reconstruction/community.jl | 16 ++--- src/types/Gene.jl | 17 +++-- src/types/GeneAssociations.jl | 32 +++++++++ src/types/Isozyme.jl | 13 ---- src/types/Metabolite.jl | 29 ++++---- src/types/Reaction.jl | 57 +++++---------- src/types/abstract/AbstractMetabolicModel.jl | 10 --- src/types/misc/ObjectModel.jl | 5 +- src/types/misc/gene_associations.jl | 12 ++-- src/types/models/MatrixModel.jl | 16 +++-- src/types/models/ObjectModel.jl | 72 +++++++++---------- src/types/models/SBMLModel.jl | 7 +- test/io/io.jl | 2 +- test/reconstruction/ObjectModel.jl | 11 ++- test/reconstruction/add_reactions.jl | 8 +-- .../gapfill_minimum_reactions.jl | 6 +- test/reconstruction/gecko.jl | 2 +- test/reconstruction/knockouts.jl | 20 +++--- test/types/Gene.jl | 8 +-- test/types/Metabolite.jl | 8 +-- test/types/ObjectModel.jl | 28 ++++---- test/types/Reaction.jl | 38 +++++----- 24 files changed, 202 insertions(+), 231 deletions(-) create mode 100644 src/types/GeneAssociations.jl delete mode 100644 src/types/Isozyme.jl diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 1df789ea3..0672d0fbf 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -21,11 +21,10 @@ function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) push!(model.rxns, rxn.id) lbs[j] = rxn.lower_bound ubs[j] = rxn.upper_bound - cs[j] = rxn.objective_coefficient end Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) model.S = [model.S Sadd] - model.c = dropzeros([model.c; cs]) + model.c = spzeros(size(model.S, 2)) # does not add an objective model.xu = ubs model.xl = lbs return nothing diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index ab383e2dd..94567e563 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -128,8 +128,8 @@ function remove_genes!( if knockout_reactions rm_reactions = String[] for (rid, r) in model.reactions - if !isnothing(r.grr) && - all(any(in.(gids, Ref(conjunction))) for conjunction in r.grr) + if !isnothing(r.gene_associations) && + all(any(in.(gids, Ref(conjunction))) for conjunction in reaction_gene_association(model, rid)) push!(rm_reactions, rid) end end @@ -248,13 +248,8 @@ function change_objective!( ) all(!haskey(model.reactions, rid) for rid in rxn_ids) && throw(DomainError(rxn_ids, "Some reaction ids were not found in model.")) - - for (_, rxn) in model.reactions # reset to zero - rxn.objective_coefficient = 0.0 - end - for (k, rid) in enumerate(rxn_ids) - model.reactions[rid].objective_coefficient = weights[k] - end + model.objective = Dict(rxn_ids .=> weights) + nothing end change_objective!(model::ObjectModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index 4a0cef828..f73c8025c 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -50,12 +50,14 @@ function add_community_objective!( throw(ArgumentError("Some objective metabolite(s) not found.")) rdict = Dict(k => -float(v) for (k, v) in objective_mets_weights) - rxn = Reaction(objective_id) + rxn = Reaction(id=objective_id) rxn.metabolites = rdict rxn.lower_bound = 0.0 rxn.upper_bound = constants.default_reaction_bound - rxn.objective_coefficient = 1.0 community.reactions[rxn.id] = rxn + + community.objective = Dict(objective_id => 1.0) + return nothing # stop annoying return value end @@ -314,7 +316,7 @@ function join_with_exchanges( model_names = [], )::ObjectModel where {M<:AbstractMetabolicModel} - community = ObjectModel() + community = ObjectModel(id="communitymodel") rxns = OrderedDict{String,Reaction}() mets = OrderedDict{String,Metabolite}() genes = OrderedDict{String,Gene}() @@ -345,11 +347,11 @@ function join_with_exchanges( # Add environmental exchange reactions and metabolites.TODO: add annotation details for (rid, mid) in exchange_rxn_mets - community.reactions[rid] = Reaction(rid) + community.reactions[rid] = Reaction(id=rid) community.reactions[rid].metabolites = Dict{String,Float64}(mid => -1.0) community.reactions[rid].lower_bound = 0.0 community.reactions[rid].upper_bound = 0.0 - community.metabolites[mid] = Metabolite(mid) + community.metabolites[mid] = Metabolite(id=mid) community.metabolites[mid].id = mid end @@ -496,14 +498,12 @@ function add_model_with_exchanges!( # add biomass metabolite if applicable if rxn.id == biomass_id rxn.metabolites[model_name*rxn.id] = 1.0 # produces one biomass - community.metabolites[model_name*rxn.id] = Metabolite(model_name * rxn.id) + community.metabolites[model_name*rxn.id] = Metabolite(id = model_name * rxn.id) end # add environmental connection if rxn.id in keys(exchange_rxn_mets) rxn.metabolites[exchange_rxn_mets[rxn.id]] = -1.0 end - # reset objective - rxn.objective_coefficient = 0.0 rxn.id = model_name * rxn.id # add to community model community.reactions[rxn.id] = rxn diff --git a/src/types/Gene.jl b/src/types/Gene.jl index 909c60411..bc978f106 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -3,18 +3,21 @@ $(TYPEDEF) # Fields $(TYPEDFIELDS) + +Default constructor requires the `id` keyword to be assigned. """ -mutable struct Gene +Base.@kwdef mutable struct Gene id::String - name::Maybe{String} - notes::Notes - annotations::Annotations + name::Maybe{String} = nothing + sequence::Maybe{String} = nothing + notes::Notes = Notes() + annotations::Annotations = Annotations() end """ $(TYPEDSIGNATURES) -A convenient constructor for a `Gene`. +A convenience constructor for [`Gene`](@ref) taking as first argument the id +of the gene. All other kwargs are forwarded to the type constructor. """ -Gene(id = ""; name = nothing, notes = Notes(), annotations = Annotations()) = - Gene(String(id), name, notes, annotations) +Gene(id; kwargs...) = Gene(;id, kwargs...) \ No newline at end of file diff --git a/src/types/GeneAssociations.jl b/src/types/GeneAssociations.jl new file mode 100644 index 000000000..2d6639c98 --- /dev/null +++ b/src/types/GeneAssociations.jl @@ -0,0 +1,32 @@ +""" +$(TYPEDEF) + +Information about isozyme composition and annotations justifying the +stoichiometry. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Isozyme + stoichiometry::Dict{String,Float64} + annotation::Annotations = Annotations() +end + +""" +$(TYPEDSIGNATURES) + +A convenience constructor for [`Isozyme`](@ref) that takes a string gene +reaction rule and converts it into the appropriate format. Assumes the +stoichiometry for each subunit is 1. +""" +Isozyme(gids::Vector{String}) = Isozyme(stoichiometry=Dict(gid => 1.0 for gid in gids)) + +""" + const GeneAssociations + +An association of genes to a reaction. Each [`Isozyme`](@ref) represents a +distinct enzyme that can catalyze a certain reaction. All the gene products in +an isozyme are required for the enzyme to function. Multiple [`Isozyme`](@ref)s +can catalyze the same reaction, but function independently. +""" +const GeneAssociations = Vector{Isozyme} \ No newline at end of file diff --git a/src/types/Isozyme.jl b/src/types/Isozyme.jl deleted file mode 100644 index b7dc6cb9b..000000000 --- a/src/types/Isozyme.jl +++ /dev/null @@ -1,13 +0,0 @@ -""" -$(TYPEDEF) - -Information about isozyme composition and activity. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Isozyme - gene_product_count::Dict{String,Int} - kcat_forward::Float64 - kcat_reverse::Float64 -end diff --git a/src/types/Metabolite.jl b/src/types/Metabolite.jl index 79bb0330a..b793ff74a 100644 --- a/src/types/Metabolite.jl +++ b/src/types/Metabolite.jl @@ -3,28 +3,23 @@ $(TYPEDEF) # Fields $(TYPEDFIELDS) + +Default constructor requires the `id` keyword to be assigned. """ -mutable struct Metabolite +Base.@kwdef mutable struct Metabolite id::String - name::Maybe{String} - formula::Maybe{String} - charge::Maybe{Int} - compartment::Maybe{String} - notes::Notes - annotations::Annotations + name::Maybe{String} = nothing + formula::Maybe{String} = nothing + charge::Maybe{Int} = nothing + compartment::Maybe{String} = nothing + notes::Notes = Notes() + annotations::Annotations = Annotations() end """ $(TYPEDSIGNATURES) -A constructor for `Metabolite`s. +A convenience constructor for [`Metabolite`](@ref) taking as first argument the id +of the metabolite. All other kwargs are forwarded to the type constructor. """ -Metabolite( - id = ""; - name = nothing, - formula = nothing, - charge = nothing, - compartment = nothing, - notes = Notes(), - annotations = Annotations(), -) = Metabolite(String(id), name, formula, charge, compartment, notes, annotations) +Metabolite(id; kwargs...) = Metabolite(;id, kwargs...) \ No newline at end of file diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 5fb975e0a..65c793b31 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -5,53 +5,28 @@ A structure for representing a single reaction in a [`ObjectModel`](@ref). # Fields $(TYPEDFIELDS) + +Default constructor requires the `id` keyword to be assigned. """ -mutable struct Reaction +Base.@kwdef mutable struct Reaction id::String - name::Maybe{String} - metabolites::Dict{String,Float64} - lower_bound::Float64 - upper_bound::Float64 - grr::Maybe{GeneAssociation} - subsystem::Maybe{String} - notes::Notes - annotations::Annotations - objective_coefficient::Float64 + name::Maybe{String} = nothing + metabolites::Dict{String,Float64} = Dict{String,Float64}() + lower_bound::Float64 = -constants.default_reaction_bound + upper_bound::Float64 = constants.default_reaction_bound + gene_associations::Maybe{GeneAssociations} = nothing + subsystem::Maybe{String} = nothing + notes::Notes = Notes() + annotations::Annotations = Annotations() end """ $(TYPEDSIGNATURES) -A constructor for Reaction that only takes a reaction `id` and -assigns default/uninformative values to all the fields that are not -explicitely assigned. +A convenience constructor for [`Reaction`](@ref) taking as first argument the id +of the reaction. All other kwargs are forwarded to the type constructor. """ -function Reaction( - id = ""; - name = nothing, - metabolites = Dict{String,Float64}(), - lower_bound = -constants.default_reaction_bound, - upper_bound = constants.default_reaction_bound, - grr = nothing, - subsystem = nothing, - notes = Notes(), - annotations = Annotations(), - objective_coefficient = 0.0, -) - mets = Dict(k => float(v) for (k, v) in metabolites) - return Reaction( - id, - name, - mets, - lower_bound, - upper_bound, - grr, - subsystem, - notes, - annotations, - objective_coefficient, - ) -end +Reaction(id; kwargs...) = Reaction(; id, kwargs...) """ $(TYPEDSIGNATURES) @@ -81,8 +56,8 @@ function Reaction( else throw(DomainError(dir, "unsupported direction")) end - Reaction( - id; + Reaction(; + id, metabolites = metabolites, lower_bound = lower_bound, upper_bound = upper_bound, diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index 8dfe81506..2dc2132f9 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -27,16 +27,6 @@ const MatType = AbstractMatrix{Float64} const VecType = AbstractVector{Float64} const StringVecType = AbstractVector{String} -""" - GeneAssociation = Vector{Vector{String}} - -An association to genes, represented as a logical formula in a positive -disjunctive normal form (DNF). (The 2nd-level vectors of strings are connected -by "and" to form conjunctions, and the 1st-level vectors of these conjunctions -are connected by "or" to form the DNF.) -""" -const GeneAssociation = Vector{Vector{String}} - """ MetaboliteFormula = Dict{String,Int} diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index 1a5e79998..5d96e21fb 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) Shallow copy of a [`ObjectModel`](@ref) """ Base.copy(m::ObjectModel) = - ObjectModel(m.id, reactions = m.reactions, metabolites = m.metabolites, genes = m.genes) + ObjectModel(id = m.id, reactions = m.reactions, metabolites = m.metabolites, genes = m.genes) """ $(TYPEDSIGNATURES) @@ -17,11 +17,10 @@ Base.copy(r::Reaction) = Reaction( metabolites = r.metabolites, lower_bound = r.lower_bound, upper_bound = r.upper_bound, - grr = r.grr, + gene_associations = r.gene_associations, subsystem = r.subsystem, notes = r.notes, annotations = r.annotations, - objective_coefficient = r.objective_coefficient, ) """ diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 45003366d..bc71dbe9d 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -2,10 +2,10 @@ """ $(TYPEDSIGNATURES) -Parse `SBML.GeneProductAssociation` structure to the simpler GeneAssociation. +Parse `SBML.GeneProductAssociation` structure to the simpler [`GeneAssociations`](@ref). The input must be (implicitly) in a positive DNF. """ -function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation +function parse_grr(gpa::SBML.GeneProductAssociation) parse_ref(x) = typeof(x) == SBML.GPARef ? [x.gene_product] : begin @@ -26,7 +26,7 @@ Convert a GeneAssociation to the corresponding `SBML.jl` structure. """ function unparse_grr( ::Type{SBML.GeneProductAssociation}, - x::GeneAssociation, + x::Vector{Vector{String}}, )::SBML.GeneProductAssociation SBML.GPAOr([SBML.GPAAnd([SBML.GPARef(j) for j in i]) for i in x]) end @@ -46,7 +46,7 @@ julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") ["YIL010W", "YGR209C"] ``` """ -parse_grr(s::String)::Maybe{GeneAssociation} = maybemap(parse_grr, parse_grr_to_sbml(s)) +parse_grr(s::String) = maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{Vector{Vector{String}}} """ $(TYPEDSIGNATURES) @@ -118,7 +118,7 @@ end """ $(TYPEDSIGNATURES) -Converts a nested string gene reaction array back into a gene reaction rule +Converts a nested string gene reaction array back into a gene reaction rule string. # Example @@ -127,7 +127,7 @@ julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) "(YIL010W and YLR043C) or (YIL010W and YGR209C)" ``` """ -function unparse_grr(::Type{String}, grr::GeneAssociation)::String +function unparse_grr(::Type{String}, grr::Vector{Vector{String}})::String grr_strings = String[] for gr in grr push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 3f7c00348..9d82e2124 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -21,7 +21,7 @@ mutable struct MatrixModel <: AbstractMetabolicModel xu::Vector{Float64} rxns::Vector{String} mets::Vector{String} - grrs::Vector{Maybe{GeneAssociation}} + grrs::Vector{Maybe{Vector{Vector{String}}}} function MatrixModel( S::MatType, @@ -31,7 +31,7 @@ mutable struct MatrixModel <: AbstractMetabolicModel xu::VecType, rxns::StringVecType, mets::StringVecType, - grrs::Vector{Maybe{GeneAssociation}} = Vector{Maybe{GeneAssociation}}( + grrs::Vector{Maybe{Vector{Vector{String}}}} = Vector{Maybe{Vector{Vector{String}}}}( nothing, length(rxns), ), @@ -128,11 +128,13 @@ Accessors.reaction_stoichiometry(m::MatrixModel, ridx)::Dict{String,Float64} = """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction +Retrieve the gene reaction associations from [`MatrixModel`](@ref) by reaction index. """ -Accessors.reaction_gene_association(model::MatrixModel, ridx::Int)::Maybe{GeneAssociation} = - model.grrs[ridx] +Accessors.reaction_gene_association( + model::MatrixModel, + ridx::Int, +)::Maybe{Vector{Vector{String}}} = model.grrs[ridx] """ $(TYPEDSIGNATURES) @@ -142,7 +144,7 @@ Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID Accessors.reaction_gene_association( model::MatrixModel, rid::String, -)::Maybe{GeneAssociation} = model.grrs[first(indexin([rid], model.rxns))] +)::Maybe{Vector{Vector{String}}} = model.grrs[first(indexin([rid], model.rxns))] """ $(TYPEDSIGNATURES) @@ -163,7 +165,7 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode xu, reactions(m), metabolites(m), - Vector{Maybe{GeneAssociation}}([ + Vector{Maybe{Vector{Vector{String}}}}([ reaction_gene_association(m, id) for id in reactions(m) ]), ) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index dddf8d93d..bf378007d 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -6,11 +6,9 @@ meta-information. Meta-information is defined as annotation details, which include gene-reaction-rules, formulas, etc. This model type seeks to keep as much meta-information as possible, as opposed -to `MatrixModel` and `MatrixModelWithCoupling`, which keep the bare neccessities only. -When merging models and keeping meta-information is important, use this as the -model type. If meta-information is not important, use the more efficient core -model types. See [`MatrixModel`](@ref) and [`MatrixModelWithCoupling`](@ref) for -comparison. +to `MatrixModel` and `MatrixModelWithCoupling`, which keep the bare neccessities +only. When merging models and keeping meta-information is important, use this as +the model type. In this model, reactions, metabolites, and genes are stored in ordered dictionaries indexed by each struct's `id` field. For example, @@ -18,11 +16,10 @@ dictionaries indexed by each struct's `id` field. For example, `"rxn1_id"`. This makes adding and removing reactions efficient. Note that the stoichiometric matrix (or any other core data, e.g. flux bounds) -is not stored directly as in `MatrixModel`. When this model type is used in -analysis functions, these core data structures are built from scratch each time -an analysis function is called. This can cause performance issues if you run -many small analysis functions sequentially. Consider using the core model -types if performance is critical. +is not stored directly as in [`MatrixModel`](@ref). When this model type is used +in analysis functions, these core data structures are built from scratch each +time an analysis function is called. This can cause performance issues if you +run many small analysis functions sequentially. See also: [`Reaction`](@ref), [`Metabolite`](@ref), [`Gene`](@ref) @@ -35,18 +32,12 @@ keys(model.reactions) # Fields $(TYPEDFIELDS) """ -mutable struct ObjectModel <: AbstractMetabolicModel +Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel id::String - reactions::OrderedDict{String,Reaction} - metabolites::OrderedDict{String,Metabolite} - genes::OrderedDict{String,Gene} - - ObjectModel( - id = ""; - reactions = OrderedDict{String,Reaction}(), - metabolites = OrderedDict{String,Metabolite}(), - genes = OrderedDict{String,Gene}(), - ) = new(id, reactions, metabolites, genes) + reactions::OrderedDict{String,Reaction} = OrderedDict{String,Reaction}() + metabolites::OrderedDict{String,Metabolite} = OrderedDict{String,Metabolite}() + genes::OrderedDict{String,Gene} = OrderedDict{String,Gene}() + objective::Dict{String,Float64} = Dict{String,Float64}() end # AbstractMetabolicModel interface follows @@ -161,7 +152,7 @@ $(TYPEDSIGNATURES) Return sparse objective vector for `model`. """ Accessors.objective(model::ObjectModel)::SparseVec = - sparse([model.reactions[rid].objective_coefficient for rid in keys(model.reactions)]) + sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) """ $(TYPEDSIGNATURES) @@ -169,10 +160,13 @@ $(TYPEDSIGNATURES) Return the gene reaction rule in string format for reaction with `id` in `model`. Return `nothing` if not available. """ -Accessors.reaction_gene_association( +function Accessors.reaction_gene_association( model::ObjectModel, id::String, -)::Maybe{GeneAssociation} = maybemap(identity, model.reactions[id].grr) +) + isnothing(model.reactions[id].gene_associations) && return nothing + [collect(keys(rga.stoichiometry)) for rga in model.reactions[id].gene_associations] +end """ $(TYPEDSIGNATURES) @@ -295,9 +289,9 @@ Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name """ $(TYPEDSIGNATURES) -Convert any `AbstractMetabolicModel` into a `ObjectModel`. -Note, some data loss may occur since only the generic interface is used during -the conversion process. +Convert any `AbstractMetabolicModel` into a `ObjectModel`. Note, some data loss +may occur since only the generic interface is used during the conversion +process. Additionally, assume the stoichiometry for each gene association is 1. """ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) if typeof(model) == ObjectModel @@ -314,8 +308,8 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) rxnids = reactions(model) for gid in gids - modelgenes[gid] = Gene( - gid; + modelgenes[gid] = Gene(; + id=gid, name = gene_name(model, gid), notes = gene_notes(model, gid), annotations = gene_annotations(model, gid), @@ -323,8 +317,8 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) end for mid in metids - modelmetabolites[mid] = Metabolite( - mid; + modelmetabolites[mid] = Metabolite(; + id= mid, name = metabolite_name(model, mid), charge = metabolite_charge(model, mid), formula = maybemap(unparse_formula, metabolite_formula(model, mid)), @@ -336,30 +330,32 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) S = stoichiometry(model) lbs, ubs = bounds(model) - ocs = objective(model) + obj_idxs, obj_vals = findnz(objective(model)) + modelobjective = Dict(k => v for (k, v) in zip(reactions(model)[obj_idxs], obj_vals)) for (i, rid) in enumerate(rxnids) rmets = Dict{String,Float64}() for (j, stoich) in zip(findnz(S[:, i])...) rmets[metids[j]] = stoich end - modelreactions[rid] = Reaction( - rid; + rgas = reaction_gene_association(model, rid) + modelreactions[rid] = Reaction(; + id = rid, name = reaction_name(model, rid), metabolites = rmets, lower_bound = lbs[i], upper_bound = ubs[i], - grr = reaction_gene_association(model, rid), - objective_coefficient = ocs[i], + gene_associations = isnothing(rgas) ? nothing : [Isozyme(; stoichiometry = Dict(k => 1.0 for k in rga) ) for rga in rgas], notes = reaction_notes(model, rid), annotations = reaction_annotations(model, rid), subsystem = reaction_subsystem(model, rid), ) end - return ObjectModel( - id; + return ObjectModel(; + id, reactions = modelreactions, metabolites = modelmetabolites, genes = modelgenes, + objective = modelobjective, ) end diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 142a4a1d7..c56932d94 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -107,9 +107,12 @@ Accessors.n_genes(model::SBMLModel)::Int = length(model.sbml.gene_products) """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociation`](@ref) from [`SBMLModel`](@ref). +Retrieve the reaction gene associations from [`SBMLModel`](@ref). """ -Accessors.reaction_gene_association(model::SBMLModel, rid::String)::Maybe{GeneAssociation} = +Accessors.reaction_gene_association( + model::SBMLModel, + rid::String, +)::Maybe{Vector{Vector{String}}} = maybemap(parse_grr, model.sbml.reactions[rid].gene_product_association) """ diff --git a/test/io/io.jl b/test/io/io.jl index e7d4649ba..d69c51f00 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -20,7 +20,7 @@ # specifically test parsing of gene-reaction associations in Recon reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) @test n_reactions(reconmodel) == 10600 - recon_grrs = [r.grr for (i, r) in reconmodel.reactions if !isnothing(r.grr)] + recon_grrs = [r.gene_associations for (i, r) in reconmodel.reactions if !isnothing(r.gene_associations)] @test length(recon_grrs) == 5938 @test sum(length.(recon_grrs)) == 13903 end diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index fc757c219..6b8db53e6 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -12,25 +12,24 @@ g2 = Gene("g2") g3 = Gene("g3") g4 = Gene("g4") - genes = [g1, g2, g3, g4] + gene_vec = [g1, g2, g3, g4] g5 = Gene("g5") g6 = Gene("g6") g7 = Gene("g7") r1 = Reaction("r1", Dict(m1.id => -1.0, m2.id => 1.0), :forward) r2 = Reaction("r2", Dict(m2.id => -2.0, m3.id => 1.0), :bidirectional) - r2.grr = [["g2"], ["g1", "g3"]] + r2.gene_associations = [Isozyme(x) for x in [["g2"], ["g1", "g3"]]] r3 = Reaction("r3", Dict(m1.id => -1.0, m4.id => 2.0), :reverse) r4 = Reaction("r4", Dict(m1.id => -5.0, m4.id => 2.0), :reverse) r5 = Reaction("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0), :reverse) rxns = [r1, r2] - model = ObjectModel() - model.id = "model" + model = ObjectModel(id = "model") model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) - model.genes = OrderedDict(g.id => g for g in genes) + model.genes = OrderedDict(g.id => g for g in gene_vec) # change bound tests - in place change_bound!(model, "r2"; lower = -10, upper = 10) @@ -112,5 +111,5 @@ ### objective change_objective!(model, "r2") - @test model.reactions["r2"].objective_coefficient == 1.0 + @test model.objective["r2"] == 1.0 end diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl index 23996d165..70bcb41ce 100644 --- a/test/reconstruction/add_reactions.jl +++ b/test/reconstruction/add_reactions.jl @@ -1,8 +1,8 @@ @testset "@add_reactions! helper" begin - mod = ObjectModel() - A = Metabolite("A") - B = Metabolite("B") - C = Metabolite("C") + mod = ObjectModel(id="testmodel") + A = Metabolite(id="A") + B = Metabolite(id="B") + C = Metabolite(id="C") add_metabolites!(mod, [A, B, C]) @add_reactions! mod begin diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index b8631beb1..366963d33 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -2,9 +2,9 @@ #= Implement the small model that should be gapfilled. =# - model = ObjectModel("partial model") + model = ObjectModel(id="partial model") - (m1, m2, m3, m4, m5, m6, m7, m8) = Metabolite.("m$i" for i = 1:8) + (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id="m$i") for i = 1:8] @add_reactions! model begin "r1", nothing → m1, 0, 1 @@ -21,7 +21,7 @@ "r12", m3 → m5, -10, 10 end - model.reactions["r11"].objective_coefficient = 1.0 + model.objective = Dict("r11" => 1) add_metabolites!(model, [m1, m2, m3, m4, m5, m7, m8]) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 127a54427..3a9afb0a7 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -1,7 +1,7 @@ @testset "GECKO" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - get_reaction_isozymes = + get_reaction_isozyme_kcats = rid -> haskey(ecoli_core_reaction_kcats, rid) ? collect( diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl index e005d008e..d4cf388ac 100644 --- a/test/reconstruction/knockouts.jl +++ b/test/reconstruction/knockouts.jl @@ -6,29 +6,29 @@ is available, but inside a group all of the genes need to be available """ @testset "knockout_single_gene" begin - m = ObjectModel() + m = ObjectModel(id="testmodel") add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) add_gene!(m, Gene("g2")) add_reaction!( m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"]]), + Reaction("v1"; metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1"]]]), ) add_reaction!( m, - Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1", "g2"]]), + Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1", "g2"]]]), ) add_reaction!( m, - Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g2"]]), + Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1"], ["g2"]]]), ) add_reaction!( m, Reaction( "v4", metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g2"]], + gene_associations = [Isozyme(x) for x in [["g1", "g2"], ["g2"]]], ), ) @@ -40,23 +40,23 @@ is available, but inside a group all of the genes need to be available end @testset "knockout_multiple_genes" begin - m = ObjectModel() + m = ObjectModel(id="testmodel") add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) + add_metabolite!(m, Metabolite(id="B")) add_gene!(m, Gene("g1")) add_gene!(m, Gene("g2")) add_gene!(m, Gene("g3")) add_reaction!( m, Reaction( - "v1", + "v1"; metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1"], ["g2"], ["g3"]], + gene_associations = [Isozyme(x) for x in [["g1"], ["g2"], ["g3"]]], ), ) add_reaction!( m, - Reaction("v2", metabolites = Dict("A" => 1.0, "B" => -1.0), grr = [["g1"], ["g3"]]), + Reaction("v2"; metabolites = Dict("A" => 1.0, "B" => -1.0), gene_associations = [Isozyme(x) for x in [["g1"], ["g3"]]]), ) remove_genes!(m, ["g1", "g3"], knockout_reactions = true) diff --git a/test/types/Gene.jl b/test/types/Gene.jl index 12cf99fa7..bf7f1fc2a 100644 --- a/test/types/Gene.jl +++ b/test/types/Gene.jl @@ -1,5 +1,5 @@ @testset "Gene: construction, printing, utils" begin - g = Gene() + g = Gene(id="testgene") # test defaults @test isempty(g.notes) @@ -14,11 +14,11 @@ @test all(contains.(sprint(show, MIME("text/plain"), g), ["gene1", "blah", "asds"])) # Test duplicate annotation finder - g2 = Gene("gene2") + g2 = Gene(id="gene2") g2.annotations = Dict("sboterm" => ["sbo2"], "ncbigene" => ["fff", "ggg"]) - g3 = Gene("g3") + g3 = Gene(id="g3") g3.annotations = Dict("sboterm" => ["sbo3"], "ncbigene" => ["ads"]) - g4 = Gene("g4") + g4 = Gene(id="g4") g4.annotations = Dict("sboterm" => ["sbo4"], "ncbigene" => ["ads22", "asd22s"]) gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in ObjectModel diff --git a/test/types/Metabolite.jl b/test/types/Metabolite.jl index 189504517..ee9615531 100644 --- a/test/types/Metabolite.jl +++ b/test/types/Metabolite.jl @@ -1,5 +1,5 @@ @testset "Metabolite" begin - m1 = Metabolite() + m1 = Metabolite(id="testmetabolite") m1.id = "met1" m1.formula = "C6H12O6N" m1.charge = 1 @@ -14,15 +14,15 @@ ), ) - m2 = Metabolite("met2") + m2 = Metabolite(id="met2") m2.formula = "C6H12O6N" - m3 = Metabolite("met3") + m3 = Metabolite(id="met3") m3.formula = "X" m3.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ad2s", "asds"]) - m4 = Metabolite("met4") + m4 = Metabolite(id="met4") m4.formula = "X" m4.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["adxxx2s", "asdxxxs"]) diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 7c8f6ce3b..a67a28542 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -1,33 +1,31 @@ @testset "ObjectModel generic interface" begin # create a small model - m1 = Metabolite("m1") + m1 = Metabolite(id="m1") m1.formula = "C2H3" m1.compartment = "cytosol" - m2 = Metabolite("m2") + m2 = Metabolite(id="m2") m2.formula = "H3C2" - m3 = Metabolite("m3") + m3 = Metabolite(id="m3") m3.charge = -1 - m4 = Metabolite("m4") + m4 = Metabolite(id="m4") m4.notes = Dict("confidence" => ["iffy"]) m4.annotations = Dict("sbo" => ["blah"]) - g1 = Gene("g1") - g2 = Gene("g2") + g1 = Gene(id="g1") + g2 = Gene(id="g2") g2.notes = Dict("confidence" => ["iffy"]) g2.annotations = Dict("sbo" => ["blah"]) - g3 = Gene("g3") + g3 = Gene(id="g3") - r1 = Reaction() - r1.id = "r1" + r1 = Reaction(id = "r1") r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) r1.lower_bound = -100.0 r1.upper_bound = 100.0 - r1.grr = [["g1", "g2"], ["g3"]] + r1.gene_associations = [Isozyme(stoichiometry=Dict("g1"=>1, "g2"=>1)) , Isozyme(stoichiometry = Dict("g3"=>1))] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r1.objective_coefficient = 1.0 - + r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) @@ -37,11 +35,11 @@ gs = [g1, g2, g3] rxns = [r1, r2, r3, r4] - model = ObjectModel() - model.id = "model" + model = ObjectModel(id = "model") model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) model.genes = OrderedDict(g.id => g for g in gs) + model.objective = Dict("r1" => 1.0) @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") @@ -83,7 +81,7 @@ obj_test[1] = 1.0 @test objective(model) == obj_test - @test [["g1", "g2"], ["g3"]] == reaction_gene_association(model, "r1") + @test all(occursin.(["g1", "g2", "g3"], Ref(COBREXA.Internal.unparse_grr(String, reaction_gene_association(model, "r1"))))) @test isnothing(reaction_gene_association(model, "r2")) @test metabolite_formula(model, "m2")["C"] == 2 diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index d4644da9b..5b11f055f 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -1,38 +1,36 @@ @testset "Reaction" begin - m1 = Metabolite("m1") + m1 = Metabolite(id="m1") m1.formula = "C2H3" - m2 = Metabolite("m2") + m2 = Metabolite(id="m2") m2.formula = "H3C2" - m3 = Metabolite("m3") - m4 = Metabolite("m4") - m5 = Metabolite("m5") - m6 = Metabolite("m6") - m7 = Metabolite("m7") - m8 = Metabolite("m8") - m9 = Metabolite("m9") - m10 = Metabolite("m10") - m11 = Metabolite("m11") - m12 = Metabolite("m12") + m3 = Metabolite(id="m3") + m4 = Metabolite(id="m4") + m5 = Metabolite(id="m5") + m6 = Metabolite(id="m6") + m7 = Metabolite(id="m7") + m8 = Metabolite(id="m8") + m9 = Metabolite(id="m9") + m10 = Metabolite(id="m10") + m11 = Metabolite(id="m11") + m12 = Metabolite(id="m12") - g1 = Gene("g1") - g2 = Gene("g2") - g3 = Gene("g3") + g1 = Gene(id="g1") + g2 = Gene(id="g2") + g3 = Gene(id="g3") - r1 = Reaction() - r1.id = "r1" + r1 = Reaction(id = "r1") r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) r1.lower_bound = -100.0 r1.upper_bound = 100.0 - r1.grr = [["g1", "g2"], ["g3"]] + r1.gene_associations = [Isozyme(stoichiometry=Dict("g1"=>1, "g2"=>1)) , Isozyme(stoichiometry = Dict("g3"=>1))] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r1.objective_coefficient = 1.0 @test all( contains.( sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "g1 and g2", "glycolysis", "blah", "biocyc"], + ["r1", "100.0", "glycolysis", "blah", "biocyc", "Isozyme[Isozyme(Dict(\"g2\" => 1.0, \"g1\" => 1.0)"], ), ) From 4c36ff961a045b40ee90ad6b480be75940141f20 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 3 Nov 2022 16:08:48 +0100 Subject: [PATCH 046/531] consolidate enzyme constrained models --- .../{enzymes.jl => enzyme_constrained.jl} | 32 +++++++++++++++++++ src/utils/smoment.jl | 32 ------------------- 2 files changed, 32 insertions(+), 32 deletions(-) rename src/utils/{enzymes.jl => enzyme_constrained.jl} (65%) delete mode 100644 src/utils/smoment.jl diff --git a/src/utils/enzymes.jl b/src/utils/enzyme_constrained.jl similarity index 65% rename from src/utils/enzymes.jl rename to src/utils/enzyme_constrained.jl index cf86b71b7..e78a115be 100644 --- a/src/utils/enzymes.jl +++ b/src/utils/enzyme_constrained.jl @@ -54,3 +54,35 @@ $(TYPEDSIGNATURES) A pipe-able variant of [`gene_product_mass`](@ref). """ gene_product_mass(model::SMomentModel) = x -> gene_product_mass(model, x) + +""" +$(TYPEDSIGNATURES) + +Compute a "score" for picking the most viable isozyme for +[`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of +the isozyme. This is used because sMOMENT algorithm can not handle multiple +isozymes for one reaction. +""" +smoment_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = + max(isozyme.kcat_forward, isozyme.kcat_reverse) / sum( + count * gene_product_molar_mass(gene) for + (gene, count) in isozyme.gene_product_count + ) + +""" +$(TYPEDSIGNATURES) + +A piping- and argmax-friendly overload of [`smoment_isozyme_speed`](@ref). + +# Example +``` +gene_mass_function = gid -> 1.234 + +best_isozyme_for_smoment = argmax( + smoment_isozyme_speed(gene_mass_function), + my_isozyme_vector, +) +``` +""" +smoment_isozyme_speed(gene_product_molar_mass::Function) = + isozyme -> smoment_isozyme_speed(isozyme, gene_product_molar_mass) diff --git a/src/utils/smoment.jl b/src/utils/smoment.jl deleted file mode 100644 index 37b8ea23d..000000000 --- a/src/utils/smoment.jl +++ /dev/null @@ -1,32 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Compute a "score" for picking the most viable isozyme for -[`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of -the isozyme. This is used because sMOMENT algorithm can not handle multiple -isozymes for one reaction. -""" -smoment_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = - max(isozyme.kcat_forward, isozyme.kcat_reverse) / sum( - count * gene_product_molar_mass(gene) for - (gene, count) in isozyme.gene_product_count - ) - -""" -$(TYPEDSIGNATURES) - -A piping- and argmax-friendly overload of [`smoment_isozyme_speed`](@ref). - -# Example -``` -gene_mass_function = gid -> 1.234 - -best_isozyme_for_smoment = argmax( - smoment_isozyme_speed(gene_mass_function), - my_isozyme_vector, -) -``` -""" -smoment_isozyme_speed(gene_product_molar_mass::Function) = - isozyme -> smoment_isozyme_speed(isozyme, gene_product_molar_mass) From 79a76f3fc7a3a804571a9fe78924c87fee0ae518 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 3 Nov 2022 17:57:11 +0100 Subject: [PATCH 047/531] Implement reviews and polish --- docs/src/examples/03_exploring.jl | 2 +- .../04b_standardmodel_construction.jl | 4 +- src/io/h5.jl | 2 +- src/io/show/Reaction.jl | 4 +- src/reconstruction/MatrixModel.jl | 3 +- src/reconstruction/ObjectModel.jl | 6 ++- src/reconstruction/community.jl | 13 +++-- src/reconstruction/gecko.jl | 12 ++--- src/reconstruction/smoment.jl | 6 +-- src/types/Gene.jl | 2 +- src/types/GeneAssociations.jl | 9 ++-- src/types/Metabolite.jl | 2 +- src/types/Reaction.jl | 4 +- src/types/abstract/AbstractMetabolicModel.jl | 13 +++++ src/types/accessors/AbstractMetabolicModel.jl | 2 +- src/types/misc/ObjectModel.jl | 9 +++- src/types/misc/gene_associations.jl | 9 ++-- src/types/models/MatrixModel.jl | 10 ++-- src/types/models/ObjectModel.jl | 34 +++++++----- src/types/models/SBMLModel.jl | 2 +- src/utils/enzyme_constrained.jl | 10 ++-- test/analysis/knockouts.jl | 34 ++++++++---- test/io/io.jl | 5 +- test/reconstruction/add_reactions.jl | 8 +-- .../gapfill_minimum_reactions.jl | 4 +- test/reconstruction/gecko.jl | 53 +++++++++++-------- test/reconstruction/knockouts.jl | 30 ++++++++--- test/reconstruction/smoment.jl | 6 ++- test/types/Gene.jl | 8 +-- test/types/Metabolite.jl | 8 +-- test/types/ObjectModel.jl | 33 ++++++++---- test/types/Reaction.jl | 44 +++++++++------ 32 files changed, 245 insertions(+), 146 deletions(-) diff --git a/docs/src/examples/03_exploring.jl b/docs/src/examples/03_exploring.jl index 75a8bbd0e..1b547051e 100644 --- a/docs/src/examples/03_exploring.jl +++ b/docs/src/examples/03_exploring.jl @@ -34,7 +34,7 @@ fieldnames(Reaction) sm.reactions["TALA"].name # -sm.reactions["TALA"].grr #gene-reaction relationship +sm.reactions["TALA"].gene_associations #gene-reaction relationship # sm.reactions["TALA"].subsystem # diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 43f4a7477..7011df3d1 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -38,7 +38,7 @@ add_metabolites!(model, metabolite_list) r_m1 = Reaction("EX_m1", Dict("m1" => -1.0), :bidirectional) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) r1 = Reaction("r1", Dict("m1" => -1.0, "m2" => 1.0), :forward) -r1.grr = [["g1", "g2"], ["g3"]] # add some gene reaction rules +r1.gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])] # add some gene reaction rules r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :reverse) r3 = Reaction("r3", Dict("m2" => -1.0, "m3" => 1.0), :bidirectional) @@ -56,7 +56,7 @@ m4 = metabolite_list[4] "r5", m4 → m2 end -model.reactions["r4"].grr = [["g5"], ["g6", "g7"], ["g8"]] +model.reactions["r4"].gene_associations = [Isozyme(x) for x in [["g5"], ["g6", "g7"], ["g8"]]] #md # !!! note "Note: Writing unicode arrows" #md # The reaction arrows can be easily written by using the `LaTeX` diff --git a/src/io/h5.jl b/src/io/h5.jl index 4136f269b..f27e28782 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -36,7 +36,7 @@ function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Mo write(f, "upper_bounds", ubs[rxnp]) end end - # TODO: genes, grrs, compartments. Perhaps chemistry and others? + # TODO: genes, gene_associations, compartments. Perhaps chemistry and others? HDF5Model(file_name) end diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl index 49d174a13..90ac06999 100644 --- a/src/io/show/Reaction.jl +++ b/src/io/show/Reaction.jl @@ -36,11 +36,11 @@ function Base.show(io::Base.IO, ::MIME"text/plain", r::Reaction) "Reaction.$(string(fname)): ", _pretty_substances(substrates) * arrow * _pretty_substances(products), ) - elseif fname == :grr + elseif fname == :gene_associations _pretty_print_keyvals( io, "Reaction.$(string(fname)): ", - maybemap(x -> unparse_grr(String, x), r.grr), + maybemap(x -> unparse_grr(String, x), r.gene_associations), ) elseif fname in (:lower_bound, :upper_bound, :objective_coefficient) _pretty_print_keyvals( diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 0672d0fbf..38b9a3731 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -8,7 +8,6 @@ function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) I = Int64[] # rows J = Int64[] # cols V = Float64[] # values - cs = zeros(length(rxns)) lbs = zeros(length(rxns)) ubs = zeros(length(rxns)) for (j, rxn) in enumerate(rxns) @@ -24,7 +23,7 @@ function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) end Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) model.S = [model.S Sadd] - model.c = spzeros(size(model.S, 2)) # does not add an objective + model.c = dropzeros([model.c; zeros(length(rxns))]) # does not add an objective info from rxns model.xu = ubs model.xl = lbs return nothing diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 94567e563..510ef9c86 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -128,8 +128,10 @@ function remove_genes!( if knockout_reactions rm_reactions = String[] for (rid, r) in model.reactions - if !isnothing(r.gene_associations) && - all(any(in.(gids, Ref(conjunction))) for conjunction in reaction_gene_association(model, rid)) + if !isnothing(r.gene_associations) && all( + any(in.(gids, Ref(conjunction))) for + conjunction in reaction_gene_association(model, rid) + ) push!(rm_reactions, rid) end end diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl index f73c8025c..a7f31c216 100644 --- a/src/reconstruction/community.jl +++ b/src/reconstruction/community.jl @@ -50,12 +50,12 @@ function add_community_objective!( throw(ArgumentError("Some objective metabolite(s) not found.")) rdict = Dict(k => -float(v) for (k, v) in objective_mets_weights) - rxn = Reaction(id=objective_id) + rxn = Reaction(objective_id) rxn.metabolites = rdict rxn.lower_bound = 0.0 rxn.upper_bound = constants.default_reaction_bound community.reactions[rxn.id] = rxn - + community.objective = Dict(objective_id => 1.0) @@ -316,7 +316,7 @@ function join_with_exchanges( model_names = [], )::ObjectModel where {M<:AbstractMetabolicModel} - community = ObjectModel(id="communitymodel") + community = ObjectModel(id = "communitymodel") rxns = OrderedDict{String,Reaction}() mets = OrderedDict{String,Metabolite}() genes = OrderedDict{String,Gene}() @@ -347,12 +347,11 @@ function join_with_exchanges( # Add environmental exchange reactions and metabolites.TODO: add annotation details for (rid, mid) in exchange_rxn_mets - community.reactions[rid] = Reaction(id=rid) + community.reactions[rid] = Reaction(rid) community.reactions[rid].metabolites = Dict{String,Float64}(mid => -1.0) community.reactions[rid].lower_bound = 0.0 community.reactions[rid].upper_bound = 0.0 - community.metabolites[mid] = Metabolite(id=mid) - community.metabolites[mid].id = mid + community.metabolites[mid] = Metabolite(mid) end return community @@ -498,7 +497,7 @@ function add_model_with_exchanges!( # add biomass metabolite if applicable if rxn.id == biomass_id rxn.metabolites[model_name*rxn.id] = 1.0 # produces one biomass - community.metabolites[model_name*rxn.id] = Metabolite(id = model_name * rxn.id) + community.metabolites[model_name*rxn.id] = Metabolite(model_name * rxn.id) end # add environmental connection if rxn.id in keys(exchange_rxn_mets) diff --git a/src/reconstruction/gecko.jl b/src/reconstruction/gecko.jl index 4a49409f5..c22c40f9e 100644 --- a/src/reconstruction/gecko.jl +++ b/src/reconstruction/gecko.jl @@ -7,7 +7,7 @@ GECKO algorithm (see [`GeckoModel`](@ref) documentation for details). # Arguments - `reaction_isozymes` is a function that returns a vector of [`Isozyme`](@ref)s - for each reaction, or empty vector if the reaction is not enzymatic. + for each reaction, or `nothing` if the reaction is not enzymatic. - `gene_product_bounds` is a function that returns lower and upper bound for concentration for a given gene product (specified by the same string gene ID as in `reaction_isozymes`), as `Tuple{Float64,Float64}`. @@ -33,7 +33,7 @@ function make_gecko_model( ) ris_ = reaction_isozymes isa Function ? reaction_isozymes : - (rid -> get(reaction_isozymes, rid, [])) + (rid -> get(reaction_isozymes, rid, nothing)) gpb_ = gene_product_bounds isa Function ? gene_product_bounds : (gid -> gene_product_bounds[gid]) @@ -61,15 +61,15 @@ function make_gecko_model( for i = 1:n_reactions(model) isozymes = ris_(rids[i]) - if isempty(isozymes) + if isnothing(isozymes) push!(columns, Types._GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) continue end # loop over both directions for all isozymes for (lb, ub, kcatf, dir) in [ - (-ubs[i], -lbs[i], i -> i.kcat_reverse, -1), - (lbs[i], ubs[i], i -> i.kcat_forward, 1), + (-ubs[i], -lbs[i], x -> x.kcat_backward, -1), + (lbs[i], ubs[i], x -> x.kcat_forward, 1), ] # The coefficients in the coupling matrix will be categorized in # separate rows for negative and positive reactions. Surprisingly, @@ -101,7 +101,7 @@ function make_gecko_model( length(coupling_row_gene_product) end (row_idx, stoich / kcat) - end for (gene, stoich) in isozyme.gene_product_count if + end for (gene, stoich) in isozyme.stoichiometry if haskey(gene_name_lookup, gene) ) diff --git a/src/reconstruction/smoment.jl b/src/reconstruction/smoment.jl index 88b4299ad..c59b9a817 100644 --- a/src/reconstruction/smoment.jl +++ b/src/reconstruction/smoment.jl @@ -45,9 +45,9 @@ function make_smoment_model( continue end - mw = sum(gpmm_(gid) * ps for (gid, ps) in isozyme.gene_product_count) + mw = sum(gpmm_(gid) * ps for (gid, ps) in isozyme.stoichiometry) - if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_reverse > constants.tolerance + if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance # reaction can run in reverse push!( columns, @@ -56,7 +56,7 @@ function make_smoment_model( -1, max(-ubs[i], 0), -lbs[i], - mw / isozyme.kcat_reverse, + mw / isozyme.kcat_backward, ), ) end diff --git a/src/types/Gene.jl b/src/types/Gene.jl index bc978f106..04c7a9a29 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -20,4 +20,4 @@ $(TYPEDSIGNATURES) A convenience constructor for [`Gene`](@ref) taking as first argument the id of the gene. All other kwargs are forwarded to the type constructor. """ -Gene(id; kwargs...) = Gene(;id, kwargs...) \ No newline at end of file +Gene(id; kwargs...) = Gene(; id, kwargs...) diff --git a/src/types/GeneAssociations.jl b/src/types/GeneAssociations.jl index 2d6639c98..3e88c9ef8 100644 --- a/src/types/GeneAssociations.jl +++ b/src/types/GeneAssociations.jl @@ -2,7 +2,7 @@ $(TYPEDEF) Information about isozyme composition and annotations justifying the -stoichiometry. +stoichiometry or turnover numbers. # Fields $(TYPEDFIELDS) @@ -10,6 +10,8 @@ $(TYPEDFIELDS) Base.@kwdef mutable struct Isozyme stoichiometry::Dict{String,Float64} annotation::Annotations = Annotations() + kcat_forward::Maybe{Float64} = nothing + kcat_backward::Maybe{Float64} = nothing end """ @@ -19,7 +21,8 @@ A convenience constructor for [`Isozyme`](@ref) that takes a string gene reaction rule and converts it into the appropriate format. Assumes the stoichiometry for each subunit is 1. """ -Isozyme(gids::Vector{String}) = Isozyme(stoichiometry=Dict(gid => 1.0 for gid in gids)) +Isozyme(gids::Vector{String}; kwargs...) = + Isozyme(; stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) """ const GeneAssociations @@ -29,4 +32,4 @@ distinct enzyme that can catalyze a certain reaction. All the gene products in an isozyme are required for the enzyme to function. Multiple [`Isozyme`](@ref)s can catalyze the same reaction, but function independently. """ -const GeneAssociations = Vector{Isozyme} \ No newline at end of file +const GeneAssociations = Vector{Isozyme} diff --git a/src/types/Metabolite.jl b/src/types/Metabolite.jl index b793ff74a..445be768a 100644 --- a/src/types/Metabolite.jl +++ b/src/types/Metabolite.jl @@ -22,4 +22,4 @@ $(TYPEDSIGNATURES) A convenience constructor for [`Metabolite`](@ref) taking as first argument the id of the metabolite. All other kwargs are forwarded to the type constructor. """ -Metabolite(id; kwargs...) = Metabolite(;id, kwargs...) \ No newline at end of file +Metabolite(id; kwargs...) = Metabolite(; id, kwargs...) diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 65c793b31..ad1ecfac7 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -56,8 +56,8 @@ function Reaction( else throw(DomainError(dir, "unsupported direction")) end - Reaction(; - id, + Reaction( + id; metabolites = metabolites, lower_bound = lower_bound, upper_bound = upper_bound, diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index 2dc2132f9..bd30a3a7a 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -54,3 +54,16 @@ Free-form notes about something (e.g. a [`Gene`](@ref)), categorized by "topic". """ const Notes = Dict{String,Vector{String}} + +""" + GeneAssociationsDNF = Vector{Vector{String}} + +Disjunctive normal form of simple gene associations. For example, `[[A, B], +[B]]` represents two isozymes where the first requires both genes `A` and `B`, +while the second isozyme only requires gene `C`. + +This string representation is typically used to represent gene reaction rules, +but does not contain any subunit stoichiometry of kinetic information of the +isozymes. See [`Isozyme`}(@ref) for a more complete structure. +""" +const GeneAssociationsDNF = Vector{Vector{String}} \ No newline at end of file diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 75df6273c..33195b795 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -198,7 +198,7 @@ takes place. (in DNF, that would be equivalent to returning `[[]]`.) function reaction_gene_association( a::AbstractMetabolicModel, reaction_id::String, -)::Maybe{GeneAssociation} +)::Maybe{GeneAssociationsDNF} return nothing end diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index 5d96e21fb..b062d2d01 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -4,8 +4,13 @@ $(TYPEDSIGNATURES) Shallow copy of a [`ObjectModel`](@ref) """ -Base.copy(m::ObjectModel) = - ObjectModel(id = m.id, reactions = m.reactions, metabolites = m.metabolites, genes = m.genes) +Base.copy(m::ObjectModel) = ObjectModel( + id = m.id, + reactions = m.reactions, + metabolites = m.metabolites, + genes = m.genes, + objective = m.objective, +) """ $(TYPEDSIGNATURES) diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index bc71dbe9d..5d8f37967 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -5,7 +5,7 @@ $(TYPEDSIGNATURES) Parse `SBML.GeneProductAssociation` structure to the simpler [`GeneAssociations`](@ref). The input must be (implicitly) in a positive DNF. """ -function parse_grr(gpa::SBML.GeneProductAssociation) +function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociationsDNF parse_ref(x) = typeof(x) == SBML.GPARef ? [x.gene_product] : begin @@ -26,7 +26,7 @@ Convert a GeneAssociation to the corresponding `SBML.jl` structure. """ function unparse_grr( ::Type{SBML.GeneProductAssociation}, - x::Vector{Vector{String}}, + x::GeneAssociationsDNF, )::SBML.GeneProductAssociation SBML.GPAOr([SBML.GPAAnd([SBML.GPARef(j) for j in i]) for i in x]) end @@ -46,7 +46,8 @@ julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") ["YIL010W", "YGR209C"] ``` """ -parse_grr(s::String) = maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{Vector{Vector{String}}} +parse_grr(s::String) = + maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{GeneAssociationsDNF} """ $(TYPEDSIGNATURES) @@ -127,7 +128,7 @@ julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) "(YIL010W and YLR043C) or (YIL010W and YGR209C)" ``` """ -function unparse_grr(::Type{String}, grr::Vector{Vector{String}})::String +function unparse_grr(::Type{String}, grr::GeneAssociationsDNF)::String grr_strings = String[] for gr in grr push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 9d82e2124..b9ace4987 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -21,7 +21,7 @@ mutable struct MatrixModel <: AbstractMetabolicModel xu::Vector{Float64} rxns::Vector{String} mets::Vector{String} - grrs::Vector{Maybe{Vector{Vector{String}}}} + grrs::Vector{Maybe{GeneAssociationsDNF}} function MatrixModel( S::MatType, @@ -31,7 +31,7 @@ mutable struct MatrixModel <: AbstractMetabolicModel xu::VecType, rxns::StringVecType, mets::StringVecType, - grrs::Vector{Maybe{Vector{Vector{String}}}} = Vector{Maybe{Vector{Vector{String}}}}( + grrs::Vector{Maybe{GeneAssociationsDNF}} = Vector{Maybe{GeneAssociationsDNF}}( nothing, length(rxns), ), @@ -134,7 +134,7 @@ index. Accessors.reaction_gene_association( model::MatrixModel, ridx::Int, -)::Maybe{Vector{Vector{String}}} = model.grrs[ridx] +)::Maybe{GeneAssociationsDNF} = model.grrs[ridx] """ $(TYPEDSIGNATURES) @@ -144,7 +144,7 @@ Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID Accessors.reaction_gene_association( model::MatrixModel, rid::String, -)::Maybe{Vector{Vector{String}}} = model.grrs[first(indexin([rid], model.rxns))] +)::Maybe{GeneAssociationsDNF} = model.grrs[first(indexin([rid], model.rxns))] """ $(TYPEDSIGNATURES) @@ -165,7 +165,7 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode xu, reactions(m), metabolites(m), - Vector{Maybe{Vector{Vector{String}}}}([ + Vector{Maybe{GeneAssociationsDNF}}([ reaction_gene_association(m, id) for id in reactions(m) ]), ) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index bf378007d..ebda64f1d 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -33,10 +33,19 @@ keys(model.reactions) $(TYPEDFIELDS) """ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel + "Name of the model" id::String + + "Ordered dictionary of reactions" reactions::OrderedDict{String,Reaction} = OrderedDict{String,Reaction}() + + "Ordered dictionary of metabolites" metabolites::OrderedDict{String,Metabolite} = OrderedDict{String,Metabolite}() + + "Ordered dictionary of genes" genes::OrderedDict{String,Gene} = OrderedDict{String,Gene}() + + "Model objective" objective::Dict{String,Float64} = Dict{String,Float64}() end @@ -57,7 +66,6 @@ Return the number of reactions contained in `model`. """ Accessors.n_reactions(model::ObjectModel)::Int = length(model.reactions) - """ $(TYPEDSIGNATURES) @@ -160,10 +168,7 @@ $(TYPEDSIGNATURES) Return the gene reaction rule in string format for reaction with `id` in `model`. Return `nothing` if not available. """ -function Accessors.reaction_gene_association( - model::ObjectModel, - id::String, -) +function Accessors.reaction_gene_association(model::ObjectModel, id::String)::Maybe{GeneAssociationsDNF} isnothing(model.reactions[id].gene_associations) && return nothing [collect(keys(rga.stoichiometry)) for rga in model.reactions[id].gene_associations] end @@ -308,8 +313,8 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) rxnids = reactions(model) for gid in gids - modelgenes[gid] = Gene(; - id=gid, + modelgenes[gid] = Gene( + gid; name = gene_name(model, gid), notes = gene_notes(model, gid), annotations = gene_annotations(model, gid), @@ -317,8 +322,8 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) end for mid in metids - modelmetabolites[mid] = Metabolite(; - id= mid, + modelmetabolites[mid] = Metabolite( + mid; name = metabolite_name(model, mid), charge = metabolite_charge(model, mid), formula = maybemap(unparse_formula, metabolite_formula(model, mid)), @@ -330,7 +335,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) S = stoichiometry(model) lbs, ubs = bounds(model) - obj_idxs, obj_vals = findnz(objective(model)) + obj_idxs, obj_vals = findnz(objective(model)) modelobjective = Dict(k => v for (k, v) in zip(reactions(model)[obj_idxs], obj_vals)) for (i, rid) in enumerate(rxnids) rmets = Dict{String,Float64}() @@ -338,13 +343,16 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) rmets[metids[j]] = stoich end rgas = reaction_gene_association(model, rid) - modelreactions[rid] = Reaction(; - id = rid, + modelreactions[rid] = Reaction( + rid; name = reaction_name(model, rid), metabolites = rmets, lower_bound = lbs[i], upper_bound = ubs[i], - gene_associations = isnothing(rgas) ? nothing : [Isozyme(; stoichiometry = Dict(k => 1.0 for k in rga) ) for rga in rgas], + gene_associations = isnothing(rgas) ? nothing : + [ + Isozyme(; stoichiometry = Dict(k => 1.0 for k in rga)) for rga in rgas + ], notes = reaction_notes(model, rid), annotations = reaction_annotations(model, rid), subsystem = reaction_subsystem(model, rid), diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index c56932d94..c9e156d72 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -112,7 +112,7 @@ Retrieve the reaction gene associations from [`SBMLModel`](@ref). Accessors.reaction_gene_association( model::SBMLModel, rid::String, -)::Maybe{Vector{Vector{String}}} = +)::Maybe{GeneAssociationsDNF} = maybemap(parse_grr, model.sbml.reactions[rid].gene_product_association) """ diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index e78a115be..a3fc8e0e1 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -62,12 +62,14 @@ Compute a "score" for picking the most viable isozyme for [`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of the isozyme. This is used because sMOMENT algorithm can not handle multiple isozymes for one reaction. + +# Note +This function does not take the direction of the reaction into account, i.e. the +maximum forward or reverse turnover number is used internally. """ smoment_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = - max(isozyme.kcat_forward, isozyme.kcat_reverse) / sum( - count * gene_product_molar_mass(gene) for - (gene, count) in isozyme.gene_product_count - ) + max(isozyme.kcat_forward, isozyme.kcat_backward) / + sum(count * gene_product_molar_mass(gene) for (gene, count) in isozyme.stoichiometry) """ $(TYPEDSIGNATURES) diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index 33612b69b..c8154ed11 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -1,5 +1,5 @@ @testset "single_knockout" begin - m = ObjectModel() + m = ObjectModel(id = "testmodel") add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) @@ -7,22 +7,34 @@ add_gene!(m, Gene("g2")) add_reaction!( m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"]]), + Reaction( + "v1", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(["g1"])], + ), ) add_reaction!( m, - Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1", "g2"]]), + Reaction( + "v2", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(["g1", "g2"])], + ), ) add_reaction!( m, - Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g2"]]), + Reaction( + "v3", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(["g1"]), Isozyme(["g2"])], + ), ) add_reaction!( m, Reaction( "v4", metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g2"]], + gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g2"])], ), ) @@ -47,7 +59,7 @@ end @testset "multiple_knockouts" begin - m = ObjectModel() + m = ObjectModel(id = "testmodel") add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) @@ -55,14 +67,18 @@ end add_gene!(m, Gene("g3")) add_reaction!( m, - Reaction("v1", metabolites = Dict("A" => -1.0, "B" => 1.0), grr = [["g1"], ["g3"]]), + Reaction( + "v1", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(["g1"]), Isozyme(["g3"])], + ), ) add_reaction!( m, Reaction( "v2", metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1", "g2"], ["g3"]], + gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])], ), ) add_reaction!( @@ -70,7 +86,7 @@ end Reaction( "v3", metabolites = Dict("A" => -1.0, "B" => 1.0), - grr = [["g1"], ["g2"], ["g3"]], + gene_associations = [Isozyme(x) for x in [["g1"], ["g2"], ["g3"]]], ), ) diff --git a/test/io/io.jl b/test/io/io.jl index d69c51f00..ce44a7216 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -20,7 +20,10 @@ # specifically test parsing of gene-reaction associations in Recon reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) @test n_reactions(reconmodel) == 10600 - recon_grrs = [r.gene_associations for (i, r) in reconmodel.reactions if !isnothing(r.gene_associations)] + recon_grrs = [ + r.gene_associations for + (i, r) in reconmodel.reactions if !isnothing(r.gene_associations) + ] @test length(recon_grrs) == 5938 @test sum(length.(recon_grrs)) == 13903 end diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl index 70bcb41ce..f5d7b24b2 100644 --- a/test/reconstruction/add_reactions.jl +++ b/test/reconstruction/add_reactions.jl @@ -1,8 +1,8 @@ @testset "@add_reactions! helper" begin - mod = ObjectModel(id="testmodel") - A = Metabolite(id="A") - B = Metabolite(id="B") - C = Metabolite(id="C") + mod = ObjectModel(id = "testmodel") + A = Metabolite(id = "A") + B = Metabolite(id = "B") + C = Metabolite(id = "C") add_metabolites!(mod, [A, B, C]) @add_reactions! mod begin diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 366963d33..09f0bb1fe 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -2,9 +2,9 @@ #= Implement the small model that should be gapfilled. =# - model = ObjectModel(id="partial model") + model = ObjectModel(id = "partial model") - (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id="m$i") for i = 1:8] + (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id = "m$i") for i = 1:8] @add_reactions! model begin "r1", nothing → m1, 0, 1 diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 3a9afb0a7..b689edadc 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -1,29 +1,29 @@ @testset "GECKO" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - get_reaction_isozyme_kcats = + get_reaction_isozymes = rid -> haskey(ecoli_core_reaction_kcats, rid) ? collect( Isozyme( - Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - ecoli_core_reaction_kcats[rid][i]..., + stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) - ) : Isozyme[] + ) : nothing get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) total_gene_product_mass = 100.0 - bounded_model = - model |> with_changed_bounds( + gm = + model |> + with_changed_bounds( ["EX_glc__D_e", "GLCpts"]; lower = [-1000.0, -1.0], upper = [nothing, 12.0], - ) - - gm = - bounded_model |> with_gecko( + ) |> + with_gecko( reaction_isozymes = get_reaction_isozymes, gene_product_bounds = g -> g == "b2779" ? (0.01, 0.06) : (0.0, 1.0), gene_product_molar_mass = get_gene_product_mass, @@ -72,7 +72,7 @@ end original GECKO paper. This model is nice to troubleshoot with, because the stoich matrix is small. =# - m = ObjectModel("gecko") + m = ObjectModel(id = "gecko") m1 = Metabolite("m1") m2 = Metabolite("m2") m3 = Metabolite("m3") @@ -89,21 +89,25 @@ end gs = [Gene("g$i") for i = 1:5] - m.reactions["r2"].grr = [["g5"]] - m.reactions["r3"].grr = [["g1"]] - m.reactions["r4"].grr = [["g1"], ["g2"]] - m.reactions["r5"].grr = [["g3", "g4"]] - m.reactions["r6"].objective_coefficient = 1.0 + m.reactions["r2"].gene_associations = [Isozyme(["g5"])] + m.reactions["r3"].gene_associations = + [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] + m.reactions["r4"].gene_associations = [ + Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0), + Isozyme(["g2"]; kcat_forward = 3.0, kcat_backward = 3.0), + ] + m.reactions["r5"].gene_associations = [ + Isozyme(; + stoichiometry = Dict("g3" => 1, "g4" => 2), + kcat_forward = 70.0, + kcat_backward = 70.0, + ), + ] + m.objective = Dict("r6" => 1.0) add_genes!(m, gs) add_metabolites!(m, [m1, m2, m3, m4]) - reaction_isozymes = Dict( - "r3" => [Isozyme(Dict("g1" => 1), 1.0, 1.0)], - "r4" => - [Isozyme(Dict("g1" => 1), 2.0, 2.0), Isozyme(Dict("g2" => 1), 3.0, 3.0)], - "r5" => [Isozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)], - ) gene_product_bounds = Dict( "g1" => (0.0, 10.0), "g2" => (0.0, 10.0), @@ -117,7 +121,10 @@ end gm = make_gecko_model( m; - reaction_isozymes, + reaction_isozymes = Dict( + rid => r.gene_associations for (rid, r) in m.reactions if + !isnothing(reaction_gene_association(m, rid)) && rid in ["r3", "r4", "r5"] + ), gene_product_bounds, gene_product_molar_mass, gene_product_mass_group_bound, diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl index d4cf388ac..4f9176fb1 100644 --- a/test/reconstruction/knockouts.jl +++ b/test/reconstruction/knockouts.jl @@ -6,22 +6,34 @@ is available, but inside a group all of the genes need to be available """ @testset "knockout_single_gene" begin - m = ObjectModel(id="testmodel") + m = ObjectModel(id = "testmodel") add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) add_gene!(m, Gene("g2")) add_reaction!( m, - Reaction("v1"; metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1"]]]), + Reaction( + "v1"; + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(x) for x in [["g1"]]], + ), ) add_reaction!( m, - Reaction("v2", metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1", "g2"]]]), + Reaction( + "v2", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(x) for x in [["g1", "g2"]]], + ), ) add_reaction!( m, - Reaction("v3", metabolites = Dict("A" => -1.0, "B" => 1.0), gene_associations = [Isozyme(x) for x in [["g1"], ["g2"]]]), + Reaction( + "v3", + metabolites = Dict("A" => -1.0, "B" => 1.0), + gene_associations = [Isozyme(x) for x in [["g1"], ["g2"]]], + ), ) add_reaction!( m, @@ -40,9 +52,9 @@ is available, but inside a group all of the genes need to be available end @testset "knockout_multiple_genes" begin - m = ObjectModel(id="testmodel") + m = ObjectModel(id = "testmodel") add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite(id="B")) + add_metabolite!(m, Metabolite(id = "B")) add_gene!(m, Gene("g1")) add_gene!(m, Gene("g2")) add_gene!(m, Gene("g3")) @@ -56,7 +68,11 @@ end ) add_reaction!( m, - Reaction("v2"; metabolites = Dict("A" => 1.0, "B" => -1.0), gene_associations = [Isozyme(x) for x in [["g1"], ["g3"]]]), + Reaction( + "v2"; + metabolites = Dict("A" => 1.0, "B" => -1.0), + gene_associations = [Isozyme(x) for x in [["g1"], ["g3"]]], + ), ) remove_genes!(m, ["g1", "g3"], knockout_reactions = true) diff --git a/test/reconstruction/smoment.jl b/test/reconstruction/smoment.jl index 8520cc315..809b5aab5 100644 --- a/test/reconstruction/smoment.jl +++ b/test/reconstruction/smoment.jl @@ -9,8 +9,9 @@ argmax( smoment_isozyme_speed(get_gene_product_mass), Isozyme( - Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - ecoli_core_reaction_kcats[rid][i]..., + stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) ) : nothing @@ -26,6 +27,7 @@ gene_product_molar_mass = get_gene_product_mass, total_enzyme_capacity = 100.0, ) + objective(smoment_model) rxn_fluxes = flux_balance_analysis_dict( smoment_model, diff --git a/test/types/Gene.jl b/test/types/Gene.jl index bf7f1fc2a..f681d13e3 100644 --- a/test/types/Gene.jl +++ b/test/types/Gene.jl @@ -1,5 +1,5 @@ @testset "Gene: construction, printing, utils" begin - g = Gene(id="testgene") + g = Gene("testgene") # test defaults @test isempty(g.notes) @@ -14,11 +14,11 @@ @test all(contains.(sprint(show, MIME("text/plain"), g), ["gene1", "blah", "asds"])) # Test duplicate annotation finder - g2 = Gene(id="gene2") + g2 = Gene("gene2") g2.annotations = Dict("sboterm" => ["sbo2"], "ncbigene" => ["fff", "ggg"]) - g3 = Gene(id="g3") + g3 = Gene("g3") g3.annotations = Dict("sboterm" => ["sbo3"], "ncbigene" => ["ads"]) - g4 = Gene(id="g4") + g4 = Gene("g4") g4.annotations = Dict("sboterm" => ["sbo4"], "ncbigene" => ["ads22", "asd22s"]) gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in ObjectModel diff --git a/test/types/Metabolite.jl b/test/types/Metabolite.jl index ee9615531..66d04cbd0 100644 --- a/test/types/Metabolite.jl +++ b/test/types/Metabolite.jl @@ -1,5 +1,5 @@ @testset "Metabolite" begin - m1 = Metabolite(id="testmetabolite") + m1 = Metabolite("testmetabolite") m1.id = "met1" m1.formula = "C6H12O6N" m1.charge = 1 @@ -14,15 +14,15 @@ ), ) - m2 = Metabolite(id="met2") + m2 = Metabolite("met2") m2.formula = "C6H12O6N" - m3 = Metabolite(id="met3") + m3 = Metabolite("met3") m3.formula = "X" m3.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ad2s", "asds"]) - m4 = Metabolite(id="met4") + m4 = Metabolite("met4") m4.formula = "X" m4.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["adxxx2s", "asdxxxs"]) diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index a67a28542..c8bfdf984 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -1,31 +1,34 @@ @testset "ObjectModel generic interface" begin # create a small model - m1 = Metabolite(id="m1") + m1 = Metabolite("m1") m1.formula = "C2H3" m1.compartment = "cytosol" - m2 = Metabolite(id="m2") + m2 = Metabolite("m2") m2.formula = "H3C2" - m3 = Metabolite(id="m3") + m3 = Metabolite("m3") m3.charge = -1 - m4 = Metabolite(id="m4") + m4 = Metabolite("m4") m4.notes = Dict("confidence" => ["iffy"]) m4.annotations = Dict("sbo" => ["blah"]) - g1 = Gene(id="g1") - g2 = Gene(id="g2") + g1 = Gene("g1") + g2 = Gene("g2") g2.notes = Dict("confidence" => ["iffy"]) g2.annotations = Dict("sbo" => ["blah"]) - g3 = Gene(id="g3") + g3 = Gene("g3") r1 = Reaction(id = "r1") r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) r1.lower_bound = -100.0 r1.upper_bound = 100.0 - r1.gene_associations = [Isozyme(stoichiometry=Dict("g1"=>1, "g2"=>1)) , Isozyme(stoichiometry = Dict("g3"=>1))] + r1.gene_associations = [ + Isozyme(stoichiometry = Dict("g1" => 1, "g2" => 1)), + Isozyme(stoichiometry = Dict("g3" => 1)), + ] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - + r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) @@ -81,7 +84,17 @@ obj_test[1] = 1.0 @test objective(model) == obj_test - @test all(occursin.(["g1", "g2", "g3"], Ref(COBREXA.Internal.unparse_grr(String, reaction_gene_association(model, "r1"))))) + @test all( + occursin.( + ["g1", "g2", "g3"], + Ref( + COBREXA.Internal.unparse_grr( + String, + reaction_gene_association(model, "r1"), + ), + ), + ), + ) @test isnothing(reaction_gene_association(model, "r2")) @test metabolite_formula(model, "m2")["C"] == 2 diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 5b11f055f..557ba0311 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -1,28 +1,31 @@ @testset "Reaction" begin - m1 = Metabolite(id="m1") + m1 = Metabolite("m1") m1.formula = "C2H3" - m2 = Metabolite(id="m2") + m2 = Metabolite("m2") m2.formula = "H3C2" - m3 = Metabolite(id="m3") - m4 = Metabolite(id="m4") - m5 = Metabolite(id="m5") - m6 = Metabolite(id="m6") - m7 = Metabolite(id="m7") - m8 = Metabolite(id="m8") - m9 = Metabolite(id="m9") - m10 = Metabolite(id="m10") - m11 = Metabolite(id="m11") - m12 = Metabolite(id="m12") + m3 = Metabolite("m3") + m4 = Metabolite("m4") + m5 = Metabolite("m5") + m6 = Metabolite("m6") + m7 = Metabolite("m7") + m8 = Metabolite("m8") + m9 = Metabolite("m9") + m10 = Metabolite("m10") + m11 = Metabolite("m11") + m12 = Metabolite("m12") - g1 = Gene(id="g1") - g2 = Gene(id="g2") - g3 = Gene(id="g3") + g1 = Gene("g1") + g2 = Gene("g2") + g3 = Gene("g3") r1 = Reaction(id = "r1") r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) r1.lower_bound = -100.0 r1.upper_bound = 100.0 - r1.gene_associations = [Isozyme(stoichiometry=Dict("g1"=>1, "g2"=>1)) , Isozyme(stoichiometry = Dict("g3"=>1))] + r1.gene_associations = [ + Isozyme(stoichiometry = Dict("g1" => 1, "g2" => 1)), + Isozyme(stoichiometry = Dict("g3" => 1)), + ] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) @@ -30,7 +33,14 @@ @test all( contains.( sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "glycolysis", "blah", "biocyc", "Isozyme[Isozyme(Dict(\"g2\" => 1.0, \"g1\" => 1.0)"], + [ + "r1", + "100.0", + "glycolysis", + "blah", + "biocyc", + "Isozyme[Isozyme(Dict(\"g2\" => 1.0, \"g1\" => 1.0)", + ], ), ) From 2981c67a8b96229c6e12172d9e30cd46f9dad5ad Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 3 Nov 2022 21:15:23 +0100 Subject: [PATCH 048/531] Improve tests for reaction show --- .../src/examples/04b_standardmodel_construction.jl | 3 ++- src/types/abstract/AbstractMetabolicModel.jl | 2 +- src/types/misc/gene_associations.jl | 14 +++++++++++--- src/types/models/ObjectModel.jl | 9 ++++++--- test/types/Reaction.jl | 2 +- 5 files changed, 21 insertions(+), 9 deletions(-) diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 7011df3d1..c4ecd4312 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -56,7 +56,8 @@ m4 = metabolite_list[4] "r5", m4 → m2 end -model.reactions["r4"].gene_associations = [Isozyme(x) for x in [["g5"], ["g6", "g7"], ["g8"]]] +model.reactions["r4"].gene_associations = + [Isozyme(x) for x in [["g5"], ["g6", "g7"], ["g8"]]] #md # !!! note "Note: Writing unicode arrows" #md # The reaction arrows can be easily written by using the `LaTeX` diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index bd30a3a7a..5e627b443 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -66,4 +66,4 @@ This string representation is typically used to represent gene reaction rules, but does not contain any subunit stoichiometry of kinetic information of the isozymes. See [`Isozyme`}(@ref) for a more complete structure. """ -const GeneAssociationsDNF = Vector{Vector{String}} \ No newline at end of file +const GeneAssociationsDNF = Vector{Vector{String}} diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 5d8f37967..38e6d4777 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -46,8 +46,7 @@ julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") ["YIL010W", "YGR209C"] ``` """ -parse_grr(s::String) = - maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{GeneAssociationsDNF} +parse_grr(s::String) = maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{GeneAssociationsDNF} """ $(TYPEDSIGNATURES) @@ -128,7 +127,16 @@ julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) "(YIL010W and YLR043C) or (YIL010W and YGR209C)" ``` """ -function unparse_grr(::Type{String}, grr::GeneAssociationsDNF)::String +unparse_grr(::Type{String}, grr::GeneAssociationsDNF)::String = unparse_grr_from_dnf(grr) +unparse_grr(::Type{String}, isozymes::Vector{Isozyme})::String = + unparse_grr_from_dnf([collect(keys(iso.stoichiometry)) for iso in isozymes]) + +""" +$(TYPEDSIGNATURES) + +Internal helper for parsing the GRRs into their human readable versions. +""" +function unparse_grr_from_dnf(grr::GeneAssociationsDNF) grr_strings = String[] for gr in grr push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index ebda64f1d..51e83f540 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -41,10 +41,10 @@ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel "Ordered dictionary of metabolites" metabolites::OrderedDict{String,Metabolite} = OrderedDict{String,Metabolite}() - + "Ordered dictionary of genes" genes::OrderedDict{String,Gene} = OrderedDict{String,Gene}() - + "Model objective" objective::Dict{String,Float64} = Dict{String,Float64}() end @@ -168,7 +168,10 @@ $(TYPEDSIGNATURES) Return the gene reaction rule in string format for reaction with `id` in `model`. Return `nothing` if not available. """ -function Accessors.reaction_gene_association(model::ObjectModel, id::String)::Maybe{GeneAssociationsDNF} +function Accessors.reaction_gene_association( + model::ObjectModel, + id::String, +)::Maybe{GeneAssociationsDNF} isnothing(model.reactions[id].gene_associations) && return nothing [collect(keys(rga.stoichiometry)) for rga in model.reactions[id].gene_associations] end diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 557ba0311..715022e6f 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -39,7 +39,7 @@ "glycolysis", "blah", "biocyc", - "Isozyme[Isozyme(Dict(\"g2\" => 1.0, \"g1\" => 1.0)", + "(g2 and g1) or (g3)", ], ), ) From 2a89cc66938da37e772533eddd51a76d9e379a25 Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 3 Nov 2022 20:56:43 +0000 Subject: [PATCH 049/531] automatic formatting triggered by @stelmo on PR #689 --- test/types/Reaction.jl | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 715022e6f..c90223fa4 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -33,14 +33,7 @@ @test all( contains.( sprint(show, MIME("text/plain"), r1), - [ - "r1", - "100.0", - "glycolysis", - "blah", - "biocyc", - "(g2 and g1) or (g3)", - ], + ["r1", "100.0", "glycolysis", "blah", "biocyc", "(g2 and g1) or (g3)"], ), ) From 3cbff225ee2321c5eb46aff7e8b40322f6a25952 Mon Sep 17 00:00:00 2001 From: htpusa Date: Mon, 21 Mar 2022 18:49:55 +0200 Subject: [PATCH 050/531] rebase --- src/types/models/CommunityModel.jl | 80 ++++++++++++++++++++++++++++++ test/types/CommunityModel.jl | 16 ++++++ 2 files changed, 96 insertions(+) create mode 100644 src/types/models/CommunityModel.jl create mode 100644 test/types/CommunityModel.jl diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl new file mode 100644 index 000000000..dd0f70d8a --- /dev/null +++ b/src/types/models/CommunityModel.jl @@ -0,0 +1,80 @@ +""" + mutable struct CommunityModel + +`CommunityModel` is used to store a so-called compartmentalised community model +where a number of individual models are joined together via a joined +compartment, which in turn connects the whole model to the "outside". +Additionally, the model may include a community level biomass (pseudo)reaction. + +The actual model is stored as a regular `MetabolicModel` that functions just +like a "normal" model would. In addition, `exchange_rxn_mets` stores the +community level exchanges connecting the individual models together +(see [`join_with_exchanges`](@ref)), `biomass_rxn` is the id of the +community biomass reaction (if it exists), and `model_names` lists the string +identifiers of the individual models. It is expected that `model_names` +corresponds to the prefixes used to identify which reaction belongs to which +model. + +See also: [`join_with_exchanges`](@ref) + +# Fields +``` +metabolicModel :: MetabolicModel +exchange_rxn_mets :: Dict{String,String} +biomass_rxn :: Maybe{String} +model_names :: Array{String} +``` + +# Example +``` + +``` +""" +mutable struct CommunityModel <: MetabolicModel + metabolicModel::Any + exchange_rxn_mets::Dict{String,String} + biomass_rxn::Maybe{String} + model_names::Array{String} + + CommunityModel( + metabolicModel = StandardModel(); + exchange_rxn_mets = Dict{String,String}(), + biomass_rxn = nothing, + model_names = String[], + ) = new(metabolicModel, exchange_rxn_mets, biomass_rxn, model_names) +end + +""" + reactions(cm::CommunityModel) + +Get the reactions from the `MetabolicModel` in a `CommunityModel`. +""" +reactions(cm::CommunityModel) = reactions(cm.metabolicModel) + +""" + metabolites(cm::CommunityModel) + +Get the metabolites from the `MetabolicModel` in a `CommunityModel`. +""" +metabolites(cm::CommunityModel) = metabolites(cm.metabolicModel) + +""" + stoichiometry(cm::CommunityModel) + +Get the stoichiometry of the underlying `MetabolicModel`. +""" +stoichiometry(cm::CommunityModel) = stoichiometry(cm.metabolicModel) + +""" + bounds(cm::CommunityModel) + +Get the flux bounds of the underlying `MetabolicModel`. +""" +bounds(cm::CommunityModel) = bounds(cm.metabolicModel) + +""" + objective(cm::CommunityModel) + +Get the objective vector of the underlying `MetabolicModel`. +""" +objective(cm::CommunityModel) = objective(cm.metabolicModel) diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl new file mode 100644 index 000000000..4f7650da4 --- /dev/null +++ b/test/types/CommunityModel.jl @@ -0,0 +1,16 @@ +@testset "Construction" begin + cm = CommunityModel() + @test isa(cm, CommunityModel) + cm = CommunityModel(test_toyModel()) + @test isa(cm, CommunityModel) +end + +@testset "Basic getters" begin + cm = CommunityModel(test_toyModel()) + @test reactions(cm) == reactions(test_toyModel()) + @test metabolites(cm) == metabolites(test_toyModel()) + @test stoichiometry(cm) == stoichiometry(test_toyModel()) + cm = CommunityModel(test_LP()) + @test bounds(cm) == bounds(test_LP()) + @test objective(cm) == objective(test_LP()) +end From 2649bb22ee329c792e1c54cfe4303302bb61bed6 Mon Sep 17 00:00:00 2001 From: htpusa Date: Tue, 22 Mar 2022 14:08:22 +0200 Subject: [PATCH 051/531] type --- src/types/models/CommunityModel.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index dd0f70d8a..1b6816637 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -30,18 +30,19 @@ model_names :: Array{String} ``` """ -mutable struct CommunityModel <: MetabolicModel - metabolicModel::Any +mutable struct CommunityModel{M} <: MetabolicModel where {M<:MetabolicModel} + metabolicModel::M exchange_rxn_mets::Dict{String,String} biomass_rxn::Maybe{String} model_names::Array{String} CommunityModel( - metabolicModel = StandardModel(); + metabolicModel::M = StandardModel(); exchange_rxn_mets = Dict{String,String}(), biomass_rxn = nothing, model_names = String[], - ) = new(metabolicModel, exchange_rxn_mets, biomass_rxn, model_names) + ) where {M<:MetabolicModel} = + new{M}(metabolicModel, exchange_rxn_mets, biomass_rxn, model_names) end """ From c512eec93b603a1371bd60ed5c0e649767884c0f Mon Sep 17 00:00:00 2001 From: htpusa Date: Tue, 22 Mar 2022 14:16:05 +0200 Subject: [PATCH 052/531] use macro magic for basic getters --- src/types/models/CommunityModel.jl | 35 +----------------------------- 1 file changed, 1 insertion(+), 34 deletions(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 1b6816637..8fe170013 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -45,37 +45,4 @@ mutable struct CommunityModel{M} <: MetabolicModel where {M<:MetabolicModel} new{M}(metabolicModel, exchange_rxn_mets, biomass_rxn, model_names) end -""" - reactions(cm::CommunityModel) - -Get the reactions from the `MetabolicModel` in a `CommunityModel`. -""" -reactions(cm::CommunityModel) = reactions(cm.metabolicModel) - -""" - metabolites(cm::CommunityModel) - -Get the metabolites from the `MetabolicModel` in a `CommunityModel`. -""" -metabolites(cm::CommunityModel) = metabolites(cm.metabolicModel) - -""" - stoichiometry(cm::CommunityModel) - -Get the stoichiometry of the underlying `MetabolicModel`. -""" -stoichiometry(cm::CommunityModel) = stoichiometry(cm.metabolicModel) - -""" - bounds(cm::CommunityModel) - -Get the flux bounds of the underlying `MetabolicModel`. -""" -bounds(cm::CommunityModel) = bounds(cm.metabolicModel) - -""" - objective(cm::CommunityModel) - -Get the objective vector of the underlying `MetabolicModel`. -""" -objective(cm::CommunityModel) = objective(cm.metabolicModel) +@_inherit_model_methods CommunityModel () metabolicModel () reactions metabolites stoichiometry bounds balance objective From 8f01af1b6bf78c5c3f477d08df8b86c5703733cf Mon Sep 17 00:00:00 2001 From: htpusa Date: Tue, 22 Mar 2022 14:45:57 +0200 Subject: [PATCH 053/531] add example --- src/types/models/CommunityModel.jl | 20 ++++++++++++++++++- test/types/CommunityModel.jl | 32 ++++++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 8fe170013..e90378b4b 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -27,7 +27,25 @@ model_names :: Array{String} # Example ``` - +m1 = load_model("e_coli_core.json") +m2 = deepcopy(m1) +exchange_rxn_mets = Dict( + ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for + ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) +) +biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] +community_model = CommunityModel( + join_with_exchanges( + CoreModel, + [m1, m2], + exchange_rxn_mets; + biomass_ids = biomass_ids, + model_names = ["m1", "m2"], + ), + exchange_rxn_mets = exchange_rxn_mets, + biomass_rxn = "community_biomass", + model_names = ["m1", "m2"] +) ``` """ mutable struct CommunityModel{M} <: MetabolicModel where {M<:MetabolicModel} diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 4f7650da4..e0c9c0a7d 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -14,3 +14,35 @@ end @test bounds(cm) == bounds(test_LP()) @test objective(cm) == objective(test_LP()) end + +@testset "Actual use case" begin + m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) + m2 = deepcopy(m1) + exchange_rxn_mets = Dict( + ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for + ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) + ) + biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] + community = join_with_exchanges( + CoreModel, + [m1, m2], + exchange_rxn_mets; + biomass_ids = biomass_ids, + model_names = ["m1", "m2"], + ) + env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) + m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) + community.xl[env_ex_inds] .= m2.xl[m2_ex_inds] + community.xu[env_ex_inds] .= m2.xu[m2_ex_inds] + biomass_ids = + Dict("m1_BIOMASS_Ecoli_core_w_GAM" => 1.0, "m2_BIOMASS_Ecoli_core_w_GAM" => 1.0) + update_community_objective!(community, "community_biomass", biomass_ids) + cm = CommunityModel( + community, + exchange_rxn_mets = exchange_rxn_mets, + biomass_rxn = "community_biomass", + model_names = ["m1", "m2"], + ) + d = flux_balance_analysis_dict(cm, Tulip.Optimizer) + @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) +end From 3ce45168f5412ffe762bbfeb6a38eccb756041bd Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 7 Nov 2022 18:18:10 +0100 Subject: [PATCH 054/531] restructure community model --- src/io/show/BalancedGrowthCommunityModel.jl | 28 + src/reconstruction/ObjectModel.jl | 32 ++ src/reconstruction/community.jl | 541 ------------------ src/reconstruction/modifications/generic.jl | 10 + .../misc/BalancedGrowthCommunityModel.jl | 34 ++ .../models/BalancedGrowthCommunityModel.jl | 258 +++++++++ src/types/models/CommunityModel.jl | 66 --- test/reconstruction/community.jl | 263 --------- test/types/BalancedGrowthCommunityModel.jl | 333 +++++++++++ test/types/CommunityModel.jl | 48 -- test/types/temp_comm.jl | 86 +++ 11 files changed, 781 insertions(+), 918 deletions(-) create mode 100644 src/io/show/BalancedGrowthCommunityModel.jl delete mode 100644 src/reconstruction/community.jl create mode 100644 src/types/misc/BalancedGrowthCommunityModel.jl create mode 100644 src/types/models/BalancedGrowthCommunityModel.jl delete mode 100644 src/types/models/CommunityModel.jl delete mode 100644 test/reconstruction/community.jl create mode 100644 test/types/BalancedGrowthCommunityModel.jl delete mode 100644 test/types/CommunityModel.jl create mode 100644 test/types/temp_comm.jl diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl new file mode 100644 index 000000000..089a2ca2c --- /dev/null +++ b/src/io/show/BalancedGrowthCommunityModel.jl @@ -0,0 +1,28 @@ +function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) + println(io, "A $(typeof(cm.model)) CommunityMember with:") + println(io, " id $(cm.id),") + println(io, " abundance $(cm.abundance),") + println( + io, + " underlying model of $(n_reactions(cm.model)) reactions and $(n_metabolites(cm.model)) metabolites,", + ) + println(io, " biomass metabolite $(cm.biomass_metabolite_id),") + println( + io, + " and $(length(cm.exchange_reaction_ids)) exchange reactions that will be connected to the environment.", + ) +end + +function Base.show(io::Base.IO, ::MIME"text/plain", cm::BalancedGrowthCommunityModel) + println(io, "A balanced growth community model with:") + println(io, " $(length(cm.members)) underlying models,") + println(io, " objective $(cm.objective_id),") + if isempty(cm.env_met_flux_bounds) + println(io, " and no constraints on environmental metabolite fluxes.") + else + println(io, " and constraints on the following environmental metabolite fluxes:") + for (k, v) in cm.env_met_flux_bounds + println(io, " $(first(v)) ≤ $(k) ≤ $(last(v))") + end + end +end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 510ef9c86..67e3001c2 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -255,3 +255,35 @@ function change_objective!( end change_objective!(model::ObjectModel, rxn_id::String) = change_objective!(model, [rxn_id]) + +""" +$(TYPEDSIGNATURES) + +Add a biomass metabolite called `biomass_metabolite_id` with stoichiometry 1 to +the biomass reaction, called `biomass_rxn_id` in `model`. +""" +function add_biomass_metabolite!( + model::ObjectModel, + biomass_rxn_id::String; + biomass_metabolite_id = "biomass", +) + model.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 + add_metabolite!(model, Metabolite(biomass_metabolite_id)) +end + +function add_biomass_metabolite( + model::ObjectModel, + biomass_rxn_id::String; + biomass_metabolite_id = "biomass", +) + m = copy(model) + m.metabolites = copy(m.metabolites) + m.metabolites[biomass_metabolite_id] = Metabolite(biomass_metabolite_id) + + m.reactions = copy(model.reactions) + m.reactions[biomass_rxn_id].metabolites = + copy(model.reactions[biomass_rxn_id].metabolites) + m.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 + + m +end diff --git a/src/reconstruction/community.jl b/src/reconstruction/community.jl deleted file mode 100644 index a7f31c216..000000000 --- a/src/reconstruction/community.jl +++ /dev/null @@ -1,541 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add an objective column to the `community` model with optional id `objective_id`. Supply a -dictionary mapping the string names of the objective metabolites to their weights in -`objective_mets_weights`. Note, the weights are negated inside the function so that positive -weights are seen as reagents/substrates, NOT products in the reaction equation. - -# Example -``` -add_community_objective!(model, Dict("met1"=>1.0, "met2"=>2.0)) -``` - -See also: [`update_community_objective!`](@ref) -""" -function add_community_objective!( - community::MatrixModel, - objective_mets_weights::Dict{String,Float64}; - objective_id = "community_biomass", -) - obj_inds = indexin(keys(objective_mets_weights), metabolites(community)) - nothing in obj_inds && throw(ArgumentError("Some objective metabolite(s) not found.")) - - nr, _ = size(community.S) - objcol = spzeros(nr) - objcol[obj_inds] .= -collect(values(objective_mets_weights)) - - # extend model by one reaction - community.S = hcat(community.S, objcol) - community.xl = [community.xl; 0.0] - community.xu = [community.xu; constants.default_reaction_bound] - community.rxns = [community.rxns; objective_id] - community.c = spzeros(size(community.S, 2)) - community.c[end] = 1.0 - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_community_objective!`] that takes a `ObjectModel` community model as input. -""" -function add_community_objective!( - community::ObjectModel, - objective_mets_weights::Dict{String,Float64}; - objective_id = "community_biomass", -) - nothing in indexin(keys(objective_mets_weights), metabolites(community)) && - throw(ArgumentError("Some objective metabolite(s) not found.")) - - rdict = Dict(k => -float(v) for (k, v) in objective_mets_weights) - rxn = Reaction(objective_id) - rxn.metabolites = rdict - rxn.lower_bound = 0.0 - rxn.upper_bound = constants.default_reaction_bound - community.reactions[rxn.id] = rxn - - community.objective = Dict(objective_id => 1.0) - - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Update the weights for the objective column with id `objective_id` in `community` using -`objective_mets_weights`, which maps metabolite ids to weights. The current weights are -reset to 0 before being updated to the supplied weights. Note, the weights are negated -inside the function so that the objective metabolites are seen as reagents/substrates, NOT -products in the reaction equation. - -# Example -``` -update_community_objective!(model, "community_biomass", Dict("met1"=>1.0, "met2"=>2.0)) -``` - -See also: [`add_community_objective!`](@ref) -""" -function update_community_objective!( - community::MatrixModel, - objective_id::String, - objective_mets_weights::Dict{String,Float64}, -) - obj_inds = indexin(keys(objective_mets_weights), metabolites(community)) - nothing in obj_inds && throw(ArgumentError("Some objective metabolite(s) not found.")) - - objective_column_index = first(indexin([objective_id], reactions(community))) - community.S[:, objective_column_index] .= 0.0 # reset - community.S[obj_inds, objective_column_index] .= - -collect(values(objective_mets_weights)) - dropzeros!(community.S) - community.c = spzeros(size(community.S, 2)) - community.c[objective_column_index] = 1.0 - community.xl[objective_column_index] = 0.0 - community.xu[objective_column_index] = constants.default_reaction_bound - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`update_community_objective!`] that takes a `ObjectModel` community model as input. -""" -function update_community_objective!( - community::ObjectModel, - objective_id::String, - objective_mets_weights::Dict{String,Float64}, -) - delete!(community.reactions, objective_id) - add_community_objective!(community, objective_mets_weights; objective_id = objective_id) - - return nothing # stop annoying return value -end - -""" -$(TYPEDSIGNATURES) - -Return a `MatrixModel` representing the community model of `models` joined through their -exchange reactions and metabolites in the dictionary `exchange_rxn_mets`, which maps -exchange reactions to their associated metabolite. These exchange reactions and metabolites -link model metabolites to environmental metabolites and reactions. Optionally specify -`model_names` to append a specific name to each reaction and metabolite of an organism for -easier reference (default is `species_i` for each model index i in `models`). Note, the -bounds of the environmental variables are all set to zero. Thus, to run a simulation you -need to constrain them appropriately. All the other bounds are inherited from the models -used to construct the community model. - -If `biomass_ids` is supplied, then a community model is returned that has an extra reaction -added to the end of the stoichiometric matrix (last column) that can be assigned as the -objective reaction. It also creates biomass "metabolites" that can be used in this objective -reaction. In the returned mode, these biomass metabolites are produced by the reaction -corresponding to `biomass_ids` in each model respectively. Note, this reaction is -unspecified, further action needs to be taken to specify it, e.g. assign weights to the last -column of the stoichiometric matrix in the rows corresponding to the biomass metabolites. - -To further clarify how this `join` works. Suppose you have 2 organisms with stoichiometric -matrices S₁ and S₂ and you want to link them with `exchange_rxn_mets = Dict(er₁ => em₁, er₂ -=> em₂, er₃ => em₃, ...)`. Then a new community stoichiometric matrix is constructed that -looks like this: -``` - _ er₁ er₂ er₃ ... b_ - |S₁ | - | S₂ | - em₁| | -S = em₂| | - em₃| | - ...| | - bm₁| | - bm₂|_ _| - -``` -The exchange reactions in each model get linked to environmental metabolites, `emᵢ`, and -these get linked to environmental exchanges, `erᵢ`. These `erᵢ` behave like normal single -organism exchange reactions. When `biomass_ids` are supplied, each model's biomass reaction -produces a pseudo-metabolite (`bmᵢ`). These can be weighted in column `b`, called the -`community_biomass` reaction in the community model, if desired. Refer to the tutorial if -this is unclear. - -# Example -``` -m1 = load_model(core_model_path) -m2 = load_model(MatrixModel, core_model_path) - -# need to list ALL the exchanges that will form part of the entire model -exchange_rxn_mets = Dict(k => first(keys(reaction_stoichiometry(m1, ex_rxn))) - for filter(looks_like_exchange_reaction, reactions(m1))) - -biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - -community = join_with_exchanges( - MatrixModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, -) -``` -""" -function join_with_exchanges( - ::Type{MatrixModel}, - models::Vector{M}, - exchange_rxn_mets::Dict{String,String}; - biomass_ids = String[], - model_names = String[], -) where {M<:AbstractMetabolicModel} - - exchange_rxn_ids = keys(exchange_rxn_mets) - exchange_met_ids = values(exchange_rxn_mets) - add_biomass_objective = isempty(biomass_ids) ? false : true - - # get offsets to construct community S - reaction_lengths = [n_reactions(model) for model in models] - metabolite_lengths = [n_metabolites(model) for model in models] - reaction_offset = [0; cumsum(reaction_lengths[1:end-1])] - metabolite_offset = [0; cumsum(metabolite_lengths[1:end-1])] - - # get each model's S matrix (needed for the size calculations) - stoichs = [stoichiometry(model) for model in models] - nnzs = [findnz(stoich) for stoich in stoichs] # nonzero indices per model - - # size calculations - column_add = add_biomass_objective ? 1 : 0 # objective rxn - row_add = add_biomass_objective ? length(models) : 0 # biomass as metabolites - nnz_add = add_biomass_objective ? (1 + length(models)) : 0 - nnz_total = - sum(length(first(nnz)) for nnz in nnzs) + - length(models) * length(exchange_rxn_ids) + - length(exchange_met_ids) + - nnz_add - n_reactions_metabolic = sum(reaction_lengths) - n_reactions_total = n_reactions_metabolic + length(exchange_rxn_ids) + column_add - n_metabolites_metabolic = sum(metabolite_lengths) - n_metabolites_total = n_metabolites_metabolic + length(exchange_met_ids) + row_add - - # Create empty storage vectors - lbs = spzeros(n_reactions_total) - ubs = spzeros(n_reactions_total) - rxns = Array{String,1}(undef, n_reactions_total) - mets = Array{String,1}(undef, n_metabolites_total) - I = Array{Int,1}(undef, nnz_total) - J = Array{Int,1}(undef, nnz_total) - V = Array{Float64,1}(undef, nnz_total) - - # build metabolic components, block diagonals - kstart = 1 - for i = 1:length(models) - kend = kstart + length(nnzs[i][3]) - 1 - rng = kstart:kend - I[rng] .= nnzs[i][1] .+ metabolite_offset[i] - J[rng] .= nnzs[i][2] .+ reaction_offset[i] - V[rng] .= nnzs[i][3] - kstart += length(nnzs[i][3]) - end - - # build environmental - exchange links - for i = 1:length(models) - exchange_rxn_inds = indexin(exchange_rxn_ids, reactions(models[i])) - exchange_met_inds = indexin(exchange_met_ids, metabolites(models[i])) - for (n, (ex_rxn, ex_met)) in enumerate(zip(exchange_rxn_inds, exchange_met_inds)) # each exchange rxn has one exchange met - isnothing(ex_rxn) && continue - isnothing(ex_met) && continue - # connect environmental metabolite with exchange metabolite - I[kstart] = n_metabolites_metabolic + n - J[kstart] = ex_rxn + reaction_offset[i] - V[kstart] = -stoichs[i][ex_met, ex_rxn] # ex is normally negative, make positive - kstart += 1 - end - end - - # # build diagonal environmental exchanges - for i = 1:length(exchange_rxn_ids) - I[kstart] = n_metabolites_metabolic + i - J[kstart] = n_reactions_metabolic + i - V[kstart] = -1.0 - kstart += 1 - end - - if add_biomass_objective - n_before_biomass_row = n_metabolites_metabolic + length(exchange_met_ids) - for i = 1:length(models) - biomass_ind = first(indexin([biomass_ids[i]], reactions(models[i]))) - I[kstart] = i + n_before_biomass_row - J[kstart] = biomass_ind + reaction_offset[i] - V[kstart] = 1.0 - kstart += 1 - end - end - - S = sparse( - I[1:kstart-1], - J[1:kstart-1], - V[1:kstart-1], - n_metabolites_total, - n_reactions_total, - ) # could be that some microbes don't have all the exchanges, hence kstart-1 - - _reaction_offsets = cumsum(reaction_lengths) - _metabolite_offsets = cumsum(metabolite_lengths) - for i = 1:length(models) - species = isempty(model_names) ? "species_$(i)_" : model_names[i] * "_" - tlbs, tubs = bounds(models[i]) - lbs[reaction_offset[i]+1:_reaction_offsets[i]] .= tlbs - ubs[reaction_offset[i]+1:_reaction_offsets[i]] .= tubs - rxns[reaction_offset[i]+1:_reaction_offsets[i]] = species .* reactions(models[i]) - mets[metabolite_offset[i]+1:_metabolite_offsets[i]] = - species .* metabolites(models[i]) - end - mets[_metabolite_offsets[end]+1:_metabolite_offsets[end]+length(exchange_met_ids)] .= - exchange_met_ids - rxns[_reaction_offsets[end]+1:_reaction_offsets[end]+length(exchange_rxn_ids)] .= - exchange_rxn_ids - - if add_biomass_objective - rxns[end] = "community_biomass" - for i = 1:length(models) - species = isempty(model_names) ? "species_$(i)_" : model_names[i] * "_" - mets[end-length(biomass_ids)+i] = species .* biomass_ids[i] - end - end - - return MatrixModel(S, spzeros(size(S, 1)), spzeros(size(S, 2)), lbs, ubs, rxns, mets) -end - -""" -$(TYPEDSIGNATURES) - -A variant of [`join_with_exchanges`](@ref) that returns a `ObjectModel`. -""" -function join_with_exchanges( - ::Type{ObjectModel}, - models::Vector{M}, - exchange_rxn_mets::Dict{String,String}; - biomass_ids = [], - model_names = [], -)::ObjectModel where {M<:AbstractMetabolicModel} - - community = ObjectModel(id = "communitymodel") - rxns = OrderedDict{String,Reaction}() - mets = OrderedDict{String,Metabolite}() - genes = OrderedDict{String,Gene}() - sizehint!(rxns, sum(n_reactions(m) for m in models) + length(keys(exchange_rxn_mets))) - sizehint!( - mets, - sum(n_metabolites(m) for m in models) + length(values(exchange_rxn_mets)), - ) - sizehint!(genes, sum(n_genes(m) for m in models)) - - for (i, model) in enumerate(models) - species = isempty(model_names) ? "species_$(i)" : model_names[i] # underscore gets added in add_model_with_exchanges! - biomass_id = isempty(biomass_ids) ? nothing : biomass_ids[i] - add_model_with_exchanges!( - community, - model, - exchange_rxn_mets; - model_name = species, - biomass_id = biomass_id, - ) - end - - if !isempty(biomass_ids) - bm = Dict{String,Float64}() # unassigned - community.reactions["community_biomass"] = - Reaction("community_biomass", bm, :forward) - end - - # Add environmental exchange reactions and metabolites.TODO: add annotation details - for (rid, mid) in exchange_rxn_mets - community.reactions[rid] = Reaction(rid) - community.reactions[rid].metabolites = Dict{String,Float64}(mid => -1.0) - community.reactions[rid].lower_bound = 0.0 - community.reactions[rid].upper_bound = 0.0 - community.metabolites[mid] = Metabolite(mid) - end - - return community -end - -""" -$(TYPEDSIGNATURES) - -Add `model` to `community`, which is a pre-existing community model with exchange reactions -and metabolites in the dictionary `exchange_rxn_mets`. The `model_name` is appended to each -reaction and metabolite, see [`join_with_exchanges`](@ref). If `biomass_id` is specified -then a biomass metabolite for `model` is also added to the resulting model. The column -corresponding to the `biomass_id` reaction then produces this new biomass metabolite with -unit coefficient. The exchange reactions and metabolites in `exchange_rxn_mets` must already -exist in `community`. Always returns a new community model because it is more efficient than -resizing all the matrices. - -No in-place variant for `MatrixModel`s exists yet. - -# Example -``` -community = add_model_with_exchanges(community, - model, - exchange_rxn_mets; - model_name="species_2", - biomass_id="BIOMASS_Ecoli_core_w_GAM") -``` -""" -function add_model_with_exchanges( - community::MatrixModel, - model::AbstractMetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - - exchange_rxn_ids = keys(exchange_rxn_mets) - exchange_met_ids = values(exchange_rxn_mets) - exchange_met_community_inds = indexin(exchange_met_ids, metabolites(community)) - exchange_rxn_community_inds = indexin(exchange_rxn_ids, reactions(community)) - if any(isnothing.(exchange_met_community_inds)) || - any(isnothing.(exchange_rxn_community_inds)) - throw( - DomainError( - "exchange metabolite/reaction not found.", - "Exchange metabolites/reactions must already be contained in the community model.", - ), - ) - end - - n_cmodel_rows, n_cmodel_cols = size(stoichiometry(community)) - n_model_rows, n_model_cols = size(stoichiometry(model)) - # A note on the variable names here.Suppose M is some sparse matrix, then I - # = row indices, J = column indices and V = values at the associated - # indices. So I[a] = i, J[a]=j and then M[i,j] = V[a] - Iadd, Jadd, Vadd = findnz(stoichiometry(model)) - - # shift to fit into community - Iadd .+= n_cmodel_rows - Jadd .+= n_cmodel_cols - - # when adding a single model not that many reactions, push! okay? - exchange_rxn_model_idxs = indexin(exchange_rxn_ids, reactions(model)) - for i = 1:length(exchange_rxn_ids) - isnothing(exchange_rxn_model_idxs[i]) && continue - push!(Iadd, exchange_met_community_inds[i]) # already present ex met in community model - push!(Jadd, n_cmodel_cols + exchange_rxn_model_idxs[i]) # new column hence the offset - push!(Vadd, 1.0) - end - - biomass_met = 0.0 - if biomass_id != "" # add biomass metabolite - biomass_rxn = first(indexin([biomass_id], reactions(model))) - push!(Iadd, n_model_rows + n_cmodel_rows + 1) - push!(Jadd, biomass_rxn + n_cmodel_cols) - push!(Vadd, 1.0) - biomass_met = 1 - end - - n_metabolites_total = n_model_rows + n_cmodel_rows + biomass_met - n_reactions_total = n_cmodel_cols + n_model_cols - - I, J, V = findnz(stoichiometry(community)) - I = [I; Iadd] - J = [J; Jadd] - V = [V; Vadd] - S = sparse(I, J, V, n_metabolites_total, n_reactions_total) - - # A note on the variables here. The bounds are vectors of upper and lower - # bounds for each reaction. So lbs = [lb_1, lb_2, lb_i, ...], ubs = [ub_1, - # ub_2, ub_i, ...] for reaction i. See the bounds function for more - # information - lbsadd, ubsadd = bounds(model) - lbs, ubs = bounds(community) - lbs = [lbs; lbsadd] - ubs = [ubs; ubsadd] - - rxnsadd = "$(model_name)_" .* reactions(model) - if !isnothing(biomass_id) - metsadd = ["$(model_name)_" .* metabolites(model); "$(model_name)_" * biomass_id] - else - metsadd = "$(model_name)_" .* metabolites(model) - end - rxns = [reactions(community); rxnsadd] - mets = [metabolites(community); metsadd] - - # adds to the original community data, could possibly reset? - I, V = findnz(balance(community)) - b = sparsevec(I, V, n_metabolites_total) - I, V = findnz(objective(community)) - c = sparsevec(I, V, n_reactions_total) - - return MatrixModel(S, b, c, lbs, ubs, rxns, mets) -end - -""" -$(TYPEDSIGNATURES) - -The `ObjectModel` variant of [`add_model_with_exchanges`](@ref), but is in-place. -""" -function add_model_with_exchanges!( - community::ObjectModel, - model::AbstractMetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - stdm = model isa ObjectModel ? deepcopy(model) : convert(ObjectModel, model) - model_name = model_name * "_" - - for met in values(stdm.metabolites) - met.id = model_name * met.id - community.metabolites[met.id] = met - end - - for rxn in values(stdm.reactions) - # rename reaction string - rxn.metabolites = Dict(model_name * k => v for (k, v) in rxn.metabolites) - # change direction of exchange - if rxn.id in keys(exchange_rxn_mets) - rxn.metabolites[model_name*exchange_rxn_mets[rxn.id]] = 1.0 # exchanges should be negative originally. TODO: test if they are? - end - # add biomass metabolite if applicable - if rxn.id == biomass_id - rxn.metabolites[model_name*rxn.id] = 1.0 # produces one biomass - community.metabolites[model_name*rxn.id] = Metabolite(model_name * rxn.id) - end - # add environmental connection - if rxn.id in keys(exchange_rxn_mets) - rxn.metabolites[exchange_rxn_mets[rxn.id]] = -1.0 - end - rxn.id = model_name * rxn.id - # add to community model - community.reactions[rxn.id] = rxn - end - - for gene in values(stdm.genes) - gene.id = model_name * gene.id - community.genes[gene.id] = gene - end - - return nothing -end - -""" -$(TYPEDSIGNATURES) - -The `ObjectModel` variant of [`add_model_with_exchanges`](@ref). Makes a deepcopy of -`community` and calls the inplace variant of this function on that copy. -""" -function add_model_with_exchanges( - community::ObjectModel, - model::AbstractMetabolicModel, - exchange_rxn_mets::Dict{String,String}; - model_name = "unknown_species", - biomass_id = nothing, -) - new_comm = deepcopy(community) - add_model_with_exchanges!( - new_comm, - model, - exchange_rxn_mets; - model_name = model_name, - biomass_id = biomass_id, - ) - return new_comm -end diff --git a/src/reconstruction/modifications/generic.jl b/src/reconstruction/modifications/generic.jl index 383366500..d065b709c 100644 --- a/src/reconstruction/modifications/generic.jl +++ b/src/reconstruction/modifications/generic.jl @@ -54,3 +54,13 @@ Plural version of [`with_removed_reaction`](@ref), calls [`remove_reactions`](@ref) internally. """ with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) + + +""" +$(TYPEDSIGNATURES) + +Species a model variant that adds a biomass metabolite to the biomass reaction. +Forwards arguments to [`add_biomass_metabolite`](@ref). +""" +with_added_biomass_metabolite(args...; kwargs...) = + m -> add_biomass_metabolite(m, args...; kwargs...) diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/BalancedGrowthCommunityModel.jl new file mode 100644 index 000000000..20a37bf8a --- /dev/null +++ b/src/types/misc/BalancedGrowthCommunityModel.jl @@ -0,0 +1,34 @@ +""" +$(TYPEDSIGNATURES) + +A helper function to get the exchange metabolites in the order of the listed +exchange reactions of a [`CommunityMember`](@ref). +""" +get_exchange_mets(m::CommunityMember) = + [first(keys(reaction_stoichiometry(m.model, r))) for r in m.exchange_reaction_ids] + +""" +$(TYPEDSIGNATURES) + +A helper function to get the unique environmental metabolites. +""" +get_env_mets(cm::BalancedGrowthCommunityModel) = + unique(hcat([get_exchange_mets(m) for m in cm.members]...)) + +""" +$(TYPEDSIGNATURES) + +A helper function that creates an exchange/environmental variable linking matrix +for community member `m` with `abundance`. +""" +function env_ex_matrix(m::CommunityMember, env_mets) + mat = spzeros(length(env_mets), size(stoichiometry(m.model), 2)) + for (env_met_idx, env_met) in enumerate(env_mets) + !(env_met in metabolites(m.model)) && continue + rex = first(indexin([env_met], get_exchange_mets(m))) + isnothing(rex) && continue + ex_ridx = first(indexin([m.exchange_reaction_ids[rex]], reactions(m.model))) + mat[env_met_idx, ex_ridx] = 1.0 + end + return mat +end diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl new file mode 100644 index 000000000..e3e0a0fc7 --- /dev/null +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -0,0 +1,258 @@ +""" +$(TYPEDEF) + +A standardized structure used to package models that can easily be combined into +a [`BalancedGrowthCommunityModel`](@ref). + +# Fields +$(TYPEDFIELDS) + +# Assumptions +1. Exchange reactions, *all* of which are idenitified in `exchange_reaction_ids`, + have the form: `A[external] ⟷ ∅` where `A` is a metabolite. No other + exchanges are allowed. +2. The biomass reaction of a model produces a biomass metabolite called + `biomass_metabolite_id` with stoichiometry 1. +3. There is only one biomass reaction in the model. +""" +Base.@kwdef mutable struct CommunityMember + "Name of model appended to intracellular reactions and metabolites." + id::String + "Abundance of organism." + abundance::Float64 + "Underlying model." + model::AbstractMetabolicModel + "List of all exchange reactions in model." + exchange_reaction_ids::Vector{String} + "ID of biomass metabolite." + biomass_metabolite_id::String +end + +""" +$(TYPEDEF) + +# Fields +$(TYPEDFIELDS) + +# Assumptions +1. Each exchanged metabolite in the underlying models, identified through the + exchange reactions, will get an environmental variable. +2. It is assumed that the same namespace is used to identify unique exchanged + metabolites. +3. The objective created by this model is the equal growth rate/balanced growth + objective. In short, all biomass metabolites are produced at the same rate. +4. The flux units are `mmol X/gDW_total/h` for some metabolite `X`. +""" +Base.@kwdef mutable struct BalancedGrowthCommunityModel <: AbstractMetabolicModel + "Models making up the community." + members::Vector{CommunityMember} + "Name of the objective" + objective_id::String = "equal_growth_rates_biomass_function" + "Bounds enforced on the environmental exchanged metabolites." + env_met_flux_bounds::Dict{String,Tuple{Float64,Float64}} = + Dict{String,Tuple{Float64,Float64}}() +end + +""" +$(TYPEDSIGNATURES) + +Return the reactions in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). +All reactions have the `id` of each respective underlying +[`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. +Consequently, exchange reactions of the original model will look like +`species1#EX_...`. All exchange environmental reactions have `EX_` as a prefix +followed by the environmental metabolite id. +""" +function Accessors.reactions(cm::BalancedGrowthCommunityModel) + rxns = [m.id * "#" * rid for m in cm.members for rid in reactions(m.model)] + env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] + return [rxns; env_exs; cm.objective_id] +end + +""" +$(TYPEDSIGNATURES) + +Return the number of reactions in `cm`, which is a +[`BalancedGrowthCommunityModel`](@ref). +""" +function Accessors.n_reactions(cm::BalancedGrowthCommunityModel) + num_model_reactions = sum(n_reactions(m.model) for m in cm.members) + # assume each env metabolite gets an env exchange + num_env_metabolites = length(get_env_mets(cm)) + return num_model_reactions + num_env_metabolites + 1 # add 1 for the community biomass +end + +""" +$(TYPEDSIGNATURES) + +Return the metabolites in `cm`, which is a +[`BalancedGrowthCommunityModel`](@ref). All metabolites have the `id` of each +respective underlying [`CommunityMember`](@ref) appended as a prefix with the +delimiter `#`. The environmental metabolites have no prefix. +""" +function Accessors.metabolites(cm::BalancedGrowthCommunityModel) + mets = [m.id * "#" * mid for m in cm.members for mid in metabolites(m.model)] + + return [mets; "ENV_" .* get_env_mets(cm)] +end + +""" +$(TYPEDSIGNATURES) + +Return the number of metabolites in `cm`, which is a +[`BalancedGrowthCommunityModel`](@ref). +""" +function Accessors.n_metabolites(cm::BalancedGrowthCommunityModel) + num_model_reactions = sum(n_metabolites(m.model) for m in cm.members) + # assume each env metabolite gets an env exchange + num_env_metabolites = length(get_env_mets(cm)) + return num_model_reactions + num_env_metabolites +end + +""" +$(TYPEDSIGNATURES) + +Return the genes in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). All +genes have the `id` of the respective underlying [`CommunityMember`](@ref) +appended as a prefix with the delimiter `#`. +""" +Accessors.genes(cm::BalancedGrowthCommunityModel) = + [m.id * "#" * gid for m in cm.members for gid in genes(m.model)] + +""" +$(TYPEDSIGNATURES) + +Return the number of metabolites in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). +""" +Accessors.n_genes(cm::BalancedGrowthCommunityModel) = + sum(n_genes(m.model) for m in cm.members) + +""" +$(TYPEDSIGNATURES) + +Return the overall stoichiometric matrix for a [`BalancedGrowthCommunityModel`](@ref), built +from the underlying models. +""" +function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) + env_mets = get_env_mets(cm) + + model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) + + zero_rxns = spzeros(size(model_S, 1), length(env_mets)) + obj_rxn = spzeros(size(model_S, 1)) + obj_rxn[indexin( + [m.id * "#" * m.biomass_metabolite_id for m in cm.members], + metabolites(cm), + )] .= [-m.abundance for m in cm.members] # fix units of biomass + + env_met_rows = spzeros(length(env_mets), size(model_S, 2)) + env_met_rows = hcat([env_ex_matrix(m, env_mets) for m in cm.members]...) + zero_objs = spzeros(length(env_mets)) + + return [ + model_S zero_rxns obj_rxn + env_met_rows -I zero_objs + ] +end + +""" +$(TYPEDSIGNATURES) + +Returns the simple variable bounds on `cm`. Assumes the objective can only go +forward at maximum rate `constants.default_reaction_bound`. Note, each bound +from the underlying community members is multiplied by the abundance of that +member. +""" +function Accessors.bounds(cm::BalancedGrowthCommunityModel) + models_lbs = vcat([first(bounds(m.model)) .* m.abundance for m in cm.members]...) + models_ubs = vcat([last(bounds(m.model)) .* m.abundance for m in cm.members]...) + env_lbs = [ + first(get(cm.env_met_flux_bounds, met_id, -constants.default_reaction_bound)) + for met_id in get_env_mets(cm) + ] + env_ubs = [ + last(get(cm.env_met_flux_bounds, met_id, constants.default_reaction_bound)) for + met_id in get_env_mets(cm) + ] + return ( + [models_lbs; env_lbs; 0], + [models_ubs; env_ubs; constants.default_reaction_bound], + ) +end + +""" +$(TYPEDSIGNATURES) + +Returns the objective of `cm`. This objective is assumed to be the equal growth +rate/balanced growth objective. Consequently, the relation `community_growth * +abundance_species_i = growth_species_i` should hold. +""" +function Accessors.objective(cm::BalancedGrowthCommunityModel) + vec = spzeros(n_reactions(cm)) + vec[end] = 1.0 + return vec +end + +""" +$(TYPEDSIGNATURES) + +Coupling constraint matrix for a [`BalancedGrowthCommunityModel`](@ref). +""" +function Accessors.coupling(cm::BalancedGrowthCommunityModel) + coups = blockdiag([coupling(m.model) for m in cm.members]...) + n = n_reactions(cm) + return [coups spzeros(size(coups, 1), n - size(coups, 2))] +end + +""" +$(TYPEDSIGNATURES) + +The number of coupling constraints in a [`BalancedGrowthCommunityModel`](@ref). +""" +Accessors.n_coupling_constraints(cm::BalancedGrowthCommunityModel) = + sum(n_coupling_constraints(m.model) for m in cm.members) + +""" +$(TYPEDSIGNATURES) + +Coupling bounds for a [`BalancedGrowthCommunityModel`](@ref). Note, each bound +from the underlying community members is multiplied by the abundance of that +member. +""" +function Accessors.coupling_bounds(cm::BalancedGrowthCommunityModel) + lbs = vcat([first(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) + ubs = vcat([last(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) + return (lbs, ubs) +end + +""" +$(TYPEDSIGNATURES) + +Returns a matrix, which when multipled by the solution of a constraints based +problem, yields the semantically meaningful fluxes that correspond to +[`fluxes`](@ref). +""" +function Accessors.reaction_flux(cm::BalancedGrowthCommunityModel) + rfs = blockdiag([reaction_flux(m.model) for m in cm.members]...) + nr = length(get_env_mets(cm)) + 1 # env ex + obj + blockdiag(rfs, spdiagm(fill(1, nr))) +end + +""" +$(TYPEDSIGNATURES) + +Returns the semantically meaningful reactions of the model. +""" +Accessors.fluxes(cm::BalancedGrowthCommunityModel) = [ + vcat([fluxes(m.model) for m in cm.members]...) + ["EX_" * env_met for env_met in get_env_mets(cm)] + cm.objective_id +] + +""" +$(TYPEDSIGNATURES) + +Return the semantically meaningful reactions of the model. +""" +Accessors.n_fluxes(cm::BalancedGrowthCommunityModel) = + sum(n_fluxes(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl deleted file mode 100644 index e90378b4b..000000000 --- a/src/types/models/CommunityModel.jl +++ /dev/null @@ -1,66 +0,0 @@ -""" - mutable struct CommunityModel - -`CommunityModel` is used to store a so-called compartmentalised community model -where a number of individual models are joined together via a joined -compartment, which in turn connects the whole model to the "outside". -Additionally, the model may include a community level biomass (pseudo)reaction. - -The actual model is stored as a regular `MetabolicModel` that functions just -like a "normal" model would. In addition, `exchange_rxn_mets` stores the -community level exchanges connecting the individual models together -(see [`join_with_exchanges`](@ref)), `biomass_rxn` is the id of the -community biomass reaction (if it exists), and `model_names` lists the string -identifiers of the individual models. It is expected that `model_names` -corresponds to the prefixes used to identify which reaction belongs to which -model. - -See also: [`join_with_exchanges`](@ref) - -# Fields -``` -metabolicModel :: MetabolicModel -exchange_rxn_mets :: Dict{String,String} -biomass_rxn :: Maybe{String} -model_names :: Array{String} -``` - -# Example -``` -m1 = load_model("e_coli_core.json") -m2 = deepcopy(m1) -exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) -) -biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] -community_model = CommunityModel( - join_with_exchanges( - CoreModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - model_names = ["m1", "m2"], - ), - exchange_rxn_mets = exchange_rxn_mets, - biomass_rxn = "community_biomass", - model_names = ["m1", "m2"] -) -``` -""" -mutable struct CommunityModel{M} <: MetabolicModel where {M<:MetabolicModel} - metabolicModel::M - exchange_rxn_mets::Dict{String,String} - biomass_rxn::Maybe{String} - model_names::Array{String} - - CommunityModel( - metabolicModel::M = StandardModel(); - exchange_rxn_mets = Dict{String,String}(), - biomass_rxn = nothing, - model_names = String[], - ) where {M<:MetabolicModel} = - new{M}(metabolicModel, exchange_rxn_mets, biomass_rxn, model_names) -end - -@_inherit_model_methods CommunityModel () metabolicModel () reactions metabolites stoichiometry bounds balance objective diff --git a/test/reconstruction/community.jl b/test/reconstruction/community.jl deleted file mode 100644 index e038b9601..000000000 --- a/test/reconstruction/community.jl +++ /dev/null @@ -1,263 +0,0 @@ -@testset "MatrixModel: Detailed community stoichiometrix matrix check" begin - m1 = test_toyModel() - m2 = test_toyModel() - ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - - c1 = join_with_exchanges(MatrixModel, [m1, m2], ex_rxn_mets) - - # test of stoichs are the same - @test all(c1.S[1:6, 1:7] .== c1.S[7:12, 8:14]) - # test if each models exchange reactions have been added to the environmental exchange properly - @test sum(c1.S[:, 4]) == 0 - @test sum(c1.S[:, 5]) == 0 - @test sum(c1.S[:, 11]) == 0 - @test sum(c1.S[:, 12]) == 0 - @test sum(c1.S[:, 15]) == -1 - @test sum(c1.S[:, 16]) == -1 - # test if exchange metablites with environment are added properly - @test c1.S[13, 4] == c1.S[13, 11] == 1 - @test c1.S[14, 5] == c1.S[14, 12] == 1 - # test if environmental exchanges have been added properly - @test c1.S[13, 15] == c1.S[14, 16] == -1 - # test of bounds set properly - lower_bound, upper_bound = bounds(c1) - @test all(lower_bound[1:14] .== -upper_bound[1:14] .== -1000) - @test all(lower_bound[15:16] .== -upper_bound[15:16] .== 0.0) - - add_community_objective!( - c1, - Dict("species_1_biomass[c]" => 1.0, "species_2_biomass[c]" => 1.0), - ) - @test c1.S[6, end] == -1.0 - @test c1.S[12, end] == -1.0 - - c2 = join_with_exchanges( - MatrixModel, - [m1, m2], - ex_rxn_mets; - biomass_ids = ["biomass1", "biomass1"], - ) - # test if same base stoich matrix - @test all(c2.S[1:14, 1:16] .== c1.S[:, 1:16]) - # test if biomass reaction and metabolites are added correctly - @test all(c2.S[:, end] .== 0) - @test c2.S[15, 7] == 1 - @test c2.S[16, 14] == 1 - - update_community_objective!( - c2, - "community_biomass", - Dict("species_1_biomass1" => 0.1, "species_2_biomass1" => 0.9), - ) - @test c2.S[15, end] == -0.1 - @test c2.S[16, end] == -0.9 -end - -@testset "MatrixModel: Small model join" begin - m1 = load_model(model_paths["e_coli_core.json"]) - m2 = load_model(MatrixModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - - community = join_with_exchanges( - MatrixModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) - community.xl[env_ex_inds] .= m2.xl[m2_ex_inds] - community.xu[env_ex_inds] .= m2.xu[m2_ex_inds] - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(community, Tulip.Optimizer) - @test size(stoichiometry(community)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end - -@testset "MatrixModel: Heterogenous model join" begin - m1 = load_model(MatrixModel, model_paths["e_coli_core.json"]) - m2 = load_model(MatrixModel, model_paths["iJO1366.mat"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ec_iJO1366_core_53p95M"] - - community = join_with_exchanges( - MatrixModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) - m1_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m1)) - - for (env_ex, m2_ex, m1_ex) in zip(env_ex_inds, m2_ex_inds, m1_ex_inds) - m2lb = isnothing(m2_ex) ? 0.0 : m2.xl[m2_ex] - m2ub = isnothing(m2_ex) ? 0.0 : m2.xu[m2_ex] - - m1lb = isnothing(m1_ex) ? 0.0 : m1.xl[m1_ex] - m1ub = isnothing(m1_ex) ? 0.0 : m1.xu[m1_ex] - - community.xl[env_ex] = m1lb + m2lb - community.xu[env_ex] = m1ub + m2ub - end - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ec_iJO1366_core_53p95M" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict( - community, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test size(stoichiometry(community)) == (2203, 3003) - @test isapprox(d["community_biomass"], 0.8739215069675402, atol = TEST_TOLERANCE) -end - -@testset "MatrixModel: Community model modifications" begin - m1 = load_model(MatrixModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for - ex_rxn in reactions(m1) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM"] - - community = - join_with_exchanges(MatrixModel, [m1], exchange_rxn_mets; biomass_ids = biomass_ids) - - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m1_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m1)) - community.xl[env_ex_inds] .= m1.xl[m1_ex_inds] - community.xu[env_ex_inds] .= m1.xu[m1_ex_inds] - - m2 = load_model(MatrixModel, model_paths["e_coli_core.json"]) - - community = add_model_with_exchanges( - community, - m2, - exchange_rxn_mets; - model_name = "species_2", - biomass_id = "BIOMASS_Ecoli_core_w_GAM", - ) - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(community, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(community, Tulip.Optimizer) - - @test size(stoichiometry(community)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end - -@testset "ObjectModel: Detailed community stoichiometrix matrix check" begin - m1 = test_toyModel() - m2 = test_toyModel() - ex_rxn_mets = Dict("EX_m1(e)" => "m1[e]", "EX_m3(e)" => "m3[e]") - - c1 = join_with_exchanges(ObjectModel, [m1, m2], ex_rxn_mets) - @test size(stoichiometry(c1)) == (14, 16) - - # test if each models exchange reactions have been added to the environmental exchange properly - @test c1.reactions["EX_m1(e)"].metabolites["m1[e]"] == -1 - @test c1.reactions["EX_m3(e)"].metabolites["m3[e]"] == -1 - - # test if exchange metabolites with environment are added properly - @test "m1[e]" in metabolites(c1) - @test "m3[e]" in metabolites(c1) - - # test if environmental exchanges have been added properly - @test c1.reactions["species_1_EX_m1(e)"].metabolites["m1[e]"] == -1 - @test c1.reactions["species_1_EX_m1(e)"].metabolites["species_1_m1[e]"] == 1 - @test c1.reactions["species_2_EX_m3(e)"].metabolites["m3[e]"] == -1 - @test c1.reactions["species_2_EX_m3(e)"].metabolites["species_2_m3[e]"] == 1 - - # test of bounds set properly - lower_bound, upper_bound = bounds(c1) # this only works because the insertion order is preserved (they get added last) - @test all(lower_bound[1:14] .== -upper_bound[1:14] .== -1000) - @test all(lower_bound[15:16] .== -upper_bound[15:16] .== 0.0) - - add_community_objective!( - c1, - Dict("species_1_biomass[c]" => 1.0, "species_2_biomass[c]" => 1.0), - ) - @test c1.reactions["community_biomass"].metabolites["species_2_biomass[c]"] == -1 - @test c1.reactions["community_biomass"].metabolites["species_1_biomass[c]"] == -1 - - c2 = join_with_exchanges( - ObjectModel, - [m1, m2], - ex_rxn_mets; - biomass_ids = ["biomass1", "biomass1"], - ) - @test size(stoichiometry(c2)) == (16, 17) - - # test if biomass reaction and metabolites are added correctly - @test isempty(c2.reactions["community_biomass"].metabolites) -end - -@testset "ObjectModel: coarse community models checks" begin - m1 = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m1, ex_rxn))) for - ex_rxn in reactions(m1) if looks_like_exchange_reaction(ex_rxn) - ) - - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - - c = join_with_exchanges( - ObjectModel, - [m1, m1], - exchange_rxn_mets; - biomass_ids = biomass_ids, - ) - - for rid in keys(exchange_rxn_mets) - c.reactions[rid].lower_bound = m1.reactions[rid].lower_bound - c.reactions[rid].upper_bound = m1.reactions[rid].upper_bound - end - - @test c.reactions["species_1_BIOMASS_Ecoli_core_w_GAM"].metabolites["species_1_BIOMASS_Ecoli_core_w_GAM"] == - 1.0 - - biomass_ids = Dict( - "species_1_BIOMASS_Ecoli_core_w_GAM" => 1.0, - "species_2_BIOMASS_Ecoli_core_w_GAM" => 1.0, - ) - - update_community_objective!(c, "community_biomass", biomass_ids) - - d = flux_balance_analysis_dict(c, Tulip.Optimizer) - @test size(stoichiometry(c)) == (166, 211) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl new file mode 100644 index 000000000..98268d5a4 --- /dev/null +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -0,0 +1,333 @@ +@testset "BalancedGrowthCommunityModel: simple model" begin + m1 = ObjectModel(id = "Model1") + add_metabolites!( + m1, + [ + Metabolite("A"), + Metabolite("B"), + Metabolite("Ae"), + Metabolite("Be"), + Metabolite("X1"), + ], + ) + add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) + add_reactions!( + m1, + [ + Reaction("EX_A", Dict("Ae" => -1), :bidirectional), + Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), + Reaction("r2", Dict("A" => -1, "B" => 1, "X1" => 1), :bidirectional), + Reaction("r3", Dict("B" => -1, "Be" => 1), :forward), + Reaction("EX_B", Dict("Be" => -1), :forward), + ], + ) + + m2 = ObjectModel(id = "Model2") + add_metabolites!( + m2, + [ + Metabolite("Ae"), + Metabolite("A"), + Metabolite("C"), + Metabolite("Ce"), + Metabolite("X2"), + ], + ) + add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) + add_reactions!( + m2, + [ + Reaction("r3", Dict("C" => -1, "Ce" => 1), :forward), + Reaction("EX_C", Dict("Ce" => -1), :forward), + Reaction("EX_A", Dict("Ae" => -1), :bidirectional), + Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), + Reaction("r2", Dict("A" => -1, "C" => 1, "X2" => 1), :bidirectional), + ], + ) + + cm1 = CommunityMember( + id = "m1", + abundance = 0.2, + model = m1, + exchange_reaction_ids = ["EX_A", "EX_B"], + biomass_metabolite_id = "X1", + ) + @test contains(sprint(show, MIME("text/plain"), cm1), "A CommunityMember with") + + cm2 = CommunityMember( + id = "m2", + abundance = 0.8, + model = m2, + exchange_reaction_ids = ["EX_A", "EX_C"], + biomass_metabolite_id = "X2", + ) + + cm = BalancedGrowthCommunityModel( + members = [cm1, cm2], + env_met_flux_bounds = Dict("Ae" => (-10, 10)), + ) + @test contains( + sprint(show, MIME("text/plain"), cm), + "A balanced growth community model with", + ) + + @test issetequal( + reactions(cm), + [ + "m1#EX_A" + "m1#r1" + "m1#r2" + "m1#r3" + "m1#EX_B" + "m2#r3" + "m2#EX_C" + "m2#EX_A" + "m2#r1" + "m2#r2" + "EX_Ae" + "EX_Be" + "EX_Ce" + "equal_growth_rates_biomass_function" + ], + ) + + @test issetequal( + metabolites(cm), + [ + "m1#A" + "m1#B" + "m1#Ae" + "m1#Be" + "m1#X1" + "m2#Ae" + "m2#A" + "m2#C" + "m2#Ce" + "m2#X2" + "ENV_Ae" + "ENV_Be" + "ENV_Ce" + ], + ) + + @test issetequal( + genes(cm), + [ + "m1#g1" + "m1#g2" + "m1#g3" + "m1#g4" + "m2#g1" + "m2#g2" + "m2#g3" + "m2#g4" + ], + ) + + @test n_reactions(cm) == 14 + @test n_metabolites(cm) == 13 + @test n_genes(cm) == 8 + + @test all( + stoichiometry(cm) .== [ + 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 -0.8 + 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 -1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 + ], + ) + + lbs, ubs = bounds(cm) + @test all( + lbs .== [ + -200.0 + -200.0 + -200.0 + 0.0 + 0.0 + 0.0 + 0.0 + -800.0 + -800.0 + -800.0 + -10.0 + -1000.0 + -1000.0 + 0.0 + ], + ) + @test all( + ubs .== [ + 200.0 + 200.0 + 200.0 + 200.0 + 200.0 + 800.0 + 800.0 + 800.0 + 800.0 + 800.0 + 10.0 + 1000.0 + 1000.0 + 1000.0 + ], + ) + + @test all(objective(cm) .== [ + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 1.0 + ]) + + @test n_coupling_constraints(cm) == 0 + @test isempty(coupling(cm)) + @test all(isempty.(coupling_bounds(cm))) +end + +@testset "BalancedGrowthCommunityModel: e coli core" begin + ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) + + add_biomass_metabolite!(ecoli, "BIOMASS_Ecoli_core_w_GAM") + + ecoli.reactions["EX_glc__D_e"].lower_bound = -1000 + + ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) + + a1 = 0.2 # abundance species 1 + a2 = 0.8 # abundance species 2 + + cm1 = CommunityMember( + id = "ecoli1", + abundance = a1, + model = ecoli, + exchange_reaction_ids = ex_rxns, + biomass_metabolite_id = "biomass", + ) + cm2 = CommunityMember( + id = "ecoli2", + abundance = a2, + model = ecoli, + exchange_reaction_ids = ex_rxns, + biomass_metabolite_id = "biomass", + ) + + cm = BalancedGrowthCommunityModel( + members = [cm1, cm2], + env_met_flux_bounds = Dict("glc__D_e" => (-10, 10)), + ) + + d = flux_balance_analysis_dict(cm, Tulip.Optimizer) + + @test isapprox(d[cm.objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) + + @test isapprox( + d["ecoli1#BIOMASS_Ecoli_core_w_GAM"], + a1 * d[cm.objective_id], + atol = TEST_TOLERANCE, + ) + + @test isapprox( + d["ecoli2#BIOMASS_Ecoli_core_w_GAM"], + a2 * d[cm.objective_id], + atol = TEST_TOLERANCE, + ) + + @test isapprox( + d["EX_glc__D_e"], + d["ecoli1#EX_glc__D_e"] + d["ecoli2#EX_glc__D_e"], + atol = TEST_TOLERANCE, + ) +end + +@testset "BalancedGrowthCommunityModel: enzyme constrained e coli" begin + ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) + + modded_ecoli = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") + @test "biomass" in metabolites(modded_ecoli) + @test !("biomass" in metabolites(ecoli)) + + @test haskey(modded_ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") + @test !haskey(ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") + + get_reaction_isozymes = + rid -> + haskey(ecoli_core_reaction_kcats, rid) ? + collect( + Isozyme( + stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ) for (i, grr) in enumerate(reaction_gene_association(ecoli, rid)) + ) : nothing + + get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) + + total_gene_product_mass = 100.0 + + gm = + ecoli |> + with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> + with_changed_bounds(["EX_glc__D_e"]; lower = [-1000.0], upper = [0]) |> + with_gecko( + reaction_isozymes = get_reaction_isozymes, + gene_product_bounds = g -> (0.0, 10.0), + gene_product_molar_mass = get_gene_product_mass, + gene_product_mass_group_bound = _ -> total_gene_product_mass, + ) + + ex_rxns = find_exchange_reaction_ids(ecoli) + + a1 = 0.2 # abundance species 1 + a2 = 0.8 # abundance species 2 + + cm1 = CommunityMember( + id = "ecoli1", + abundance = a1, + model = gm, + exchange_reaction_ids = ex_rxns, + biomass_metabolite_id = "biomass", + ) + cm2 = CommunityMember( + id = "ecoli2", + abundance = a2, + model = gm, + exchange_reaction_ids = ex_rxns, + biomass_metabolite_id = "biomass", + ) + + cm = BalancedGrowthCommunityModel(members = [cm1, cm2]) + + opt_model = flux_balance_analysis( + cm, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + + @test isapprox(d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) + + @test length(flux_dict(cm, opt_model)) == length(flux_vec(cm, opt_model)) + + +end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl deleted file mode 100644 index e0c9c0a7d..000000000 --- a/test/types/CommunityModel.jl +++ /dev/null @@ -1,48 +0,0 @@ -@testset "Construction" begin - cm = CommunityModel() - @test isa(cm, CommunityModel) - cm = CommunityModel(test_toyModel()) - @test isa(cm, CommunityModel) -end - -@testset "Basic getters" begin - cm = CommunityModel(test_toyModel()) - @test reactions(cm) == reactions(test_toyModel()) - @test metabolites(cm) == metabolites(test_toyModel()) - @test stoichiometry(cm) == stoichiometry(test_toyModel()) - cm = CommunityModel(test_LP()) - @test bounds(cm) == bounds(test_LP()) - @test objective(cm) == objective(test_LP()) -end - -@testset "Actual use case" begin - m1 = load_model(CoreModel, model_paths["e_coli_core.json"]) - m2 = deepcopy(m1) - exchange_rxn_mets = Dict( - ex_rxn => first(keys(reaction_stoichiometry(m2, ex_rxn))) for - ex_rxn in reactions(m2) if looks_like_exchange_reaction(ex_rxn) - ) - biomass_ids = ["BIOMASS_Ecoli_core_w_GAM", "BIOMASS_Ecoli_core_w_GAM"] - community = join_with_exchanges( - CoreModel, - [m1, m2], - exchange_rxn_mets; - biomass_ids = biomass_ids, - model_names = ["m1", "m2"], - ) - env_ex_inds = indexin(keys(exchange_rxn_mets), reactions(community)) - m2_ex_inds = indexin(keys(exchange_rxn_mets), reactions(m2)) - community.xl[env_ex_inds] .= m2.xl[m2_ex_inds] - community.xu[env_ex_inds] .= m2.xu[m2_ex_inds] - biomass_ids = - Dict("m1_BIOMASS_Ecoli_core_w_GAM" => 1.0, "m2_BIOMASS_Ecoli_core_w_GAM" => 1.0) - update_community_objective!(community, "community_biomass", biomass_ids) - cm = CommunityModel( - community, - exchange_rxn_mets = exchange_rxn_mets, - biomass_rxn = "community_biomass", - model_names = ["m1", "m2"], - ) - d = flux_balance_analysis_dict(cm, Tulip.Optimizer) - @test isapprox(d["community_biomass"], 0.41559777495618294, atol = TEST_TOLERANCE) -end diff --git a/test/types/temp_comm.jl b/test/types/temp_comm.jl new file mode 100644 index 000000000..db4bfa9fc --- /dev/null +++ b/test/types/temp_comm.jl @@ -0,0 +1,86 @@ +using COBREXA.Reconstruction +using COBREXA.Types +using COBREXA.Accessors +using COBREXA.Analysis + +using Tulip + +m1 = ObjectModel(id = "Model1") +add_metabolites!( + m1, + [ + Metabolite("A"), + Metabolite("B"), + Metabolite("Ae"), + Metabolite("Be"), + Metabolite("X1"), + ], +) +add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) +add_reactions!( + m1, + [ + Reaction("EX_A", Dict("Ae" => -1), :bidirectional), + Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), + Reaction("r2", Dict("A" => -1, "B" => 1, "X1" => 1), :bidirectional), + Reaction("r3", Dict("B" => -1, "Be" => 1), :forward), + Reaction("EX_B", Dict("Be" => -1), :forward), + ], +) + +m2 = ObjectModel(id = "Model2") +add_metabolites!( + m2, + [ + Metabolite("Ae"), + Metabolite("A"), + Metabolite("C"), + Metabolite("Ce"), + Metabolite("X2"), + ], +) +add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) +add_reactions!( + m2, + [ + Reaction("r3", Dict("C" => -1, "Ce" => 1), :forward), + Reaction("EX_C", Dict("Ce" => -1), :forward), + Reaction("EX_A", Dict("Ae" => -1), :bidirectional), + Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), + Reaction("r2", Dict("A" => -1, "C" => 1, "X2" => 1), :bidirectional), + ], +) + +cm1 = CommunityMember( + id = "m1", + abundance = 0.2, + model = m1, + exchange_reaction_ids = ["EX_A", "EX_B"], + biomass_metabolite_id = "X1", +) +cm2 = CommunityMember( + id = "m2", + abundance = 0.8, + model = m2, + exchange_reaction_ids = ["EX_A", "EX_C"], + biomass_metabolite_id = "X2", +) + + +cm = CommunityModel(members = [cm1, cm2], env_met_flux_bounds = Dict("Ae" => (-10, 10))) + +reactions(cm) +metabolites(cm) +genes(cm) + +n_reactions(cm) +n_metabolites(cm) +n_genes(cm) + +stoichiometry(cm) +bounds(cm) +objective(cm) + + + +d = flux_balance_analysis_dict(cm, Tulip.Optimizer) From f81668b0c2f54f46ac56bd005ad8b960702bd225 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 9 Nov 2022 21:17:38 +0100 Subject: [PATCH 055/531] implement reviews --- src/io/show/BalancedGrowthCommunityModel.jl | 25 +----- src/reconstruction/ObjectModel.jl | 4 +- .../misc/BalancedGrowthCommunityModel.jl | 21 +++++ .../models/BalancedGrowthCommunityModel.jl | 25 ++++-- test/types/BalancedGrowthCommunityModel.jl | 15 ++-- test/types/temp_comm.jl | 86 ------------------- 6 files changed, 52 insertions(+), 124 deletions(-) delete mode 100644 test/types/temp_comm.jl diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl index 089a2ca2c..a8c221a30 100644 --- a/src/io/show/BalancedGrowthCommunityModel.jl +++ b/src/io/show/BalancedGrowthCommunityModel.jl @@ -1,28 +1,7 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println(io, "A $(typeof(cm.model)) CommunityMember with:") - println(io, " id $(cm.id),") - println(io, " abundance $(cm.abundance),") - println( - io, - " underlying model of $(n_reactions(cm.model)) reactions and $(n_metabolites(cm.model)) metabolites,", - ) - println(io, " biomass metabolite $(cm.biomass_metabolite_id),") - println( - io, - " and $(length(cm.exchange_reaction_ids)) exchange reactions that will be connected to the environment.", - ) + println(io, "A $(typeof(cm.model)) community member with $(n_reactions(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.") end function Base.show(io::Base.IO, ::MIME"text/plain", cm::BalancedGrowthCommunityModel) - println(io, "A balanced growth community model with:") - println(io, " $(length(cm.members)) underlying models,") - println(io, " objective $(cm.objective_id),") - if isempty(cm.env_met_flux_bounds) - println(io, " and no constraints on environmental metabolite fluxes.") - else - println(io, " and constraints on the following environmental metabolite fluxes:") - for (k, v) in cm.env_met_flux_bounds - println(io, " $(first(v)) ≤ $(k) ≤ $(last(v))") - end - end + println(io, "A balanced growth community model comprised of $(length(cm.members)) underlying models.") end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 67e3001c2..57f449e1b 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -281,8 +281,8 @@ function add_biomass_metabolite( m.metabolites[biomass_metabolite_id] = Metabolite(biomass_metabolite_id) m.reactions = copy(model.reactions) - m.reactions[biomass_rxn_id].metabolites = - copy(model.reactions[biomass_rxn_id].metabolites) + m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) + m.reactions[biomass_rxn_id].metabolites = copy(model.reactions[biomass_rxn_id].metabolites) m.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 m diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/BalancedGrowthCommunityModel.jl index 20a37bf8a..7e774e37e 100644 --- a/src/types/misc/BalancedGrowthCommunityModel.jl +++ b/src/types/misc/BalancedGrowthCommunityModel.jl @@ -32,3 +32,24 @@ function env_ex_matrix(m::CommunityMember, env_mets) end return mat end + +""" +$(TYPEDSIGNATURES) + +A helper function to find the index of the appropriate model. Assumes each `id` +is delimited by `#` that separates the model ID prefix and the original id. +""" +function access_community_member(cm::BalancedGrowthCommunityModel, id::String, accessor::Function) + id_split = split(id, "#") + idx = findfirst(startswith(first(id_split)), m.id for m in cm.members) + isnothing(idx) && return nothing # can only access inside community member + accessor(cm.members[idx].model, string(last(id_split))) +end + + +""" +$(TYPEDSIGNATURES) + +A helper function to add the id of the community member as a prefix to some string. +""" +add_community_prefix(m::CommunityMember, str::String; delim = "#") = m.id * delim * str diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index e3e0a0fc7..a29668a58 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -64,7 +64,7 @@ Consequently, exchange reactions of the original model will look like followed by the environmental metabolite id. """ function Accessors.reactions(cm::BalancedGrowthCommunityModel) - rxns = [m.id * "#" * rid for m in cm.members for rid in reactions(m.model)] + rxns = [add_community_prefix(m, rid) for m in cm.members for rid in reactions(m.model)] env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] return [rxns; env_exs; cm.objective_id] end @@ -91,8 +91,7 @@ respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The environmental metabolites have no prefix. """ function Accessors.metabolites(cm::BalancedGrowthCommunityModel) - mets = [m.id * "#" * mid for m in cm.members for mid in metabolites(m.model)] - + mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] return [mets; "ENV_" .* get_env_mets(cm)] end @@ -117,7 +116,7 @@ genes have the `id` of the respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. """ Accessors.genes(cm::BalancedGrowthCommunityModel) = - [m.id * "#" * gid for m in cm.members for gid in genes(m.model)] + [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] """ $(TYPEDSIGNATURES) @@ -141,7 +140,7 @@ function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) zero_rxns = spzeros(size(model_S, 1), length(env_mets)) obj_rxn = spzeros(size(model_S, 1)) obj_rxn[indexin( - [m.id * "#" * m.biomass_metabolite_id for m in cm.members], + [add_community_prefix(m, m.biomass_metabolite_id) for m in cm.members], metabolites(cm), )] .= [-m.abundance for m in cm.members] # fix units of biomass @@ -244,7 +243,7 @@ $(TYPEDSIGNATURES) Returns the semantically meaningful reactions of the model. """ Accessors.fluxes(cm::BalancedGrowthCommunityModel) = [ - vcat([fluxes(m.model) for m in cm.members]...) + vcat([add_community_prefix.(Ref(m), fluxes(m.model)) for m in cm.members]...) ["EX_" * env_met for env_met in get_env_mets(cm)] cm.objective_id ] @@ -256,3 +255,17 @@ Return the semantically meaningful reactions of the model. """ Accessors.n_fluxes(cm::BalancedGrowthCommunityModel) = sum(n_fluxes(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 + +""" +$(TYPEDSIGNATURES) + +Returns the sets of genes that need to be present so that the reaction can work. +""" +Accessors.reaction_gene_association( + cm::BalancedGrowthCommunityModel, + reaction_id::String, +) = access_community_member(cm, reaction_id, reaction_gene_association) + +# TODO make a macro that implements access_community_member for all the other +# accessors. Might be a little trick since there are two levels: one upper +# level/environmental and another lower level/species \ No newline at end of file diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 98268d5a4..ddfd7ac02 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -52,7 +52,7 @@ exchange_reaction_ids = ["EX_A", "EX_B"], biomass_metabolite_id = "X1", ) - @test contains(sprint(show, MIME("text/plain"), cm1), "A CommunityMember with") + @test contains(sprint(show, MIME("text/plain"), cm1), "community member") cm2 = CommunityMember( id = "m2", @@ -68,7 +68,7 @@ ) @test contains( sprint(show, MIME("text/plain"), cm), - "A balanced growth community model with", + "balanced growth", ) @test issetequal( @@ -259,6 +259,10 @@ end d["ecoli1#EX_glc__D_e"] + d["ecoli2#EX_glc__D_e"], atol = TEST_TOLERANCE, ) + + # test if model can be converted to another type + om = convert(ObjectModel, cm) + @test n_reactions(om) == n_reactions(cm) end @testset "BalancedGrowthCommunityModel: enzyme constrained e coli" begin @@ -325,9 +329,6 @@ end modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - @test isapprox(d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) - - @test length(flux_dict(cm, opt_model)) == length(flux_vec(cm, opt_model)) - - + f_d = flux_dict(cm, opt_model) + @test isapprox(f_d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) end diff --git a/test/types/temp_comm.jl b/test/types/temp_comm.jl deleted file mode 100644 index db4bfa9fc..000000000 --- a/test/types/temp_comm.jl +++ /dev/null @@ -1,86 +0,0 @@ -using COBREXA.Reconstruction -using COBREXA.Types -using COBREXA.Accessors -using COBREXA.Analysis - -using Tulip - -m1 = ObjectModel(id = "Model1") -add_metabolites!( - m1, - [ - Metabolite("A"), - Metabolite("B"), - Metabolite("Ae"), - Metabolite("Be"), - Metabolite("X1"), - ], -) -add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) -add_reactions!( - m1, - [ - Reaction("EX_A", Dict("Ae" => -1), :bidirectional), - Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), - Reaction("r2", Dict("A" => -1, "B" => 1, "X1" => 1), :bidirectional), - Reaction("r3", Dict("B" => -1, "Be" => 1), :forward), - Reaction("EX_B", Dict("Be" => -1), :forward), - ], -) - -m2 = ObjectModel(id = "Model2") -add_metabolites!( - m2, - [ - Metabolite("Ae"), - Metabolite("A"), - Metabolite("C"), - Metabolite("Ce"), - Metabolite("X2"), - ], -) -add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) -add_reactions!( - m2, - [ - Reaction("r3", Dict("C" => -1, "Ce" => 1), :forward), - Reaction("EX_C", Dict("Ce" => -1), :forward), - Reaction("EX_A", Dict("Ae" => -1), :bidirectional), - Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), - Reaction("r2", Dict("A" => -1, "C" => 1, "X2" => 1), :bidirectional), - ], -) - -cm1 = CommunityMember( - id = "m1", - abundance = 0.2, - model = m1, - exchange_reaction_ids = ["EX_A", "EX_B"], - biomass_metabolite_id = "X1", -) -cm2 = CommunityMember( - id = "m2", - abundance = 0.8, - model = m2, - exchange_reaction_ids = ["EX_A", "EX_C"], - biomass_metabolite_id = "X2", -) - - -cm = CommunityModel(members = [cm1, cm2], env_met_flux_bounds = Dict("Ae" => (-10, 10))) - -reactions(cm) -metabolites(cm) -genes(cm) - -n_reactions(cm) -n_metabolites(cm) -n_genes(cm) - -stoichiometry(cm) -bounds(cm) -objective(cm) - - - -d = flux_balance_analysis_dict(cm, Tulip.Optimizer) From 4a85fde2475bd4bda317309597b0037b8cb512c9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 9 Nov 2022 22:31:55 +0100 Subject: [PATCH 056/531] add macro that generates accessors --- src/io/show/BalancedGrowthCommunityModel.jl | 10 ++++- src/reconstruction/ObjectModel.jl | 3 +- .../misc/BalancedGrowthCommunityModel.jl | 9 +++- .../models/BalancedGrowthCommunityModel.jl | 44 +++++++++++++------ test/types/BalancedGrowthCommunityModel.jl | 5 +-- 5 files changed, 48 insertions(+), 23 deletions(-) diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl index a8c221a30..41a9d8c6b 100644 --- a/src/io/show/BalancedGrowthCommunityModel.jl +++ b/src/io/show/BalancedGrowthCommunityModel.jl @@ -1,7 +1,13 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println(io, "A $(typeof(cm.model)) community member with $(n_reactions(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.") + println( + io, + "A $(typeof(cm.model)) community member with $(n_reactions(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.", + ) end function Base.show(io::Base.IO, ::MIME"text/plain", cm::BalancedGrowthCommunityModel) - println(io, "A balanced growth community model comprised of $(length(cm.members)) underlying models.") + println( + io, + "A balanced growth community model comprised of $(length(cm.members)) underlying models.", + ) end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 57f449e1b..6b5f1d0b1 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -282,7 +282,8 @@ function add_biomass_metabolite( m.reactions = copy(model.reactions) m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) - m.reactions[biomass_rxn_id].metabolites = copy(model.reactions[biomass_rxn_id].metabolites) + m.reactions[biomass_rxn_id].metabolites = + copy(model.reactions[biomass_rxn_id].metabolites) m.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 m diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/BalancedGrowthCommunityModel.jl index 7e774e37e..0adfd4d7f 100644 --- a/src/types/misc/BalancedGrowthCommunityModel.jl +++ b/src/types/misc/BalancedGrowthCommunityModel.jl @@ -39,10 +39,15 @@ $(TYPEDSIGNATURES) A helper function to find the index of the appropriate model. Assumes each `id` is delimited by `#` that separates the model ID prefix and the original id. """ -function access_community_member(cm::BalancedGrowthCommunityModel, id::String, accessor::Function) +function access_community_member( + cm::BalancedGrowthCommunityModel, + id::String, + accessor::Function; + default = nothing, +) id_split = split(id, "#") idx = findfirst(startswith(first(id_split)), m.id for m in cm.members) - isnothing(idx) && return nothing # can only access inside community member + isnothing(idx) && return default # can only access inside community member accessor(cm.members[idx].model, string(last(id_split))) end diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index a29668a58..65243fc36 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -91,7 +91,8 @@ respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The environmental metabolites have no prefix. """ function Accessors.metabolites(cm::BalancedGrowthCommunityModel) - mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] + mets = + [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] return [mets; "ENV_" .* get_env_mets(cm)] end @@ -256,16 +257,31 @@ Return the semantically meaningful reactions of the model. Accessors.n_fluxes(cm::BalancedGrowthCommunityModel) = sum(n_fluxes(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 -""" -$(TYPEDSIGNATURES) - -Returns the sets of genes that need to be present so that the reaction can work. -""" -Accessors.reaction_gene_association( - cm::BalancedGrowthCommunityModel, - reaction_id::String, -) = access_community_member(cm, reaction_id, reaction_gene_association) - -# TODO make a macro that implements access_community_member for all the other -# accessors. Might be a little trick since there are two levels: one upper -# level/environmental and another lower level/species \ No newline at end of file +#= +This loops implements the rest of the accssors through access_community_member. +Since most of the environmental reactions are generated programmtically, they +will not have things like annotations etc. For this reason, these methods will +only work if they access something inside the community members. +=# +for (func, def) in ( + (:reaction_gene_association, nothing), + (:reaction_subsystem, nothing), + (:reaction_stoichiometry, nothing), + (:metabolite_formula, nothing), + (:metabolite_charge, nothing), + (:metabolite_compartment, nothing), + (:reaction_annotations, Dict()), + (:metabolite_annotations, Dict()), + (:gene_annotations, Dict()), + (:reaction_notes, Dict()), + (:metabolite_notes, Dict()), + (:gene_notes, Dict()), + (:reaction_name, nothing), + (:metabolite_name, nothing), + (:gene_name, nothing), +) + @eval begin # TODO add docstrings somehow + Accessors.$func(cm::BalancedGrowthCommunityModel, id::String) = + access_community_member(cm, id, $func; default = $def) + end +end diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index ddfd7ac02..bab1032aa 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -66,10 +66,7 @@ members = [cm1, cm2], env_met_flux_bounds = Dict("Ae" => (-10, 10)), ) - @test contains( - sprint(show, MIME("text/plain"), cm), - "balanced growth", - ) + @test contains(sprint(show, MIME("text/plain"), cm), "balanced growth") @test issetequal( reactions(cm), From 5986a3c1311f342e8dab8a3ea6782529c016e5a2 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 9 Nov 2022 23:51:24 +0100 Subject: [PATCH 057/531] removed @add_reactions --- .../04b_standardmodel_construction.jl | 17 +++--- src/reconstruction/ObjectModel.jl | 54 ------------------- test/reconstruction/add_reactions.jl | 25 --------- .../gapfill_minimum_reactions.jl | 32 ++++++----- test/reconstruction/gecko.jl | 19 ++++--- 5 files changed, 35 insertions(+), 112 deletions(-) delete mode 100644 test/reconstruction/add_reactions.jl diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index c4ecd4312..4477f691c 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -25,6 +25,7 @@ gene_list = [Gene(string("g", num)) for num = 1:8] add_genes!(model, gene_list) # ### Add metabolites to the model + metabolite_list = [Metabolite(string("m", num)) for num = 1:4] metabolite_list[1].formula = "C6H12O6" # can edit metabolites, etc. directly @@ -33,29 +34,23 @@ add_metabolites!(model, metabolite_list) # ### Add reactions to the model -# There are two ways to create and add reactions to a model. -# These are using functions, or macros. - r_m1 = Reaction("EX_m1", Dict("m1" => -1.0), :bidirectional) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) r1 = Reaction("r1", Dict("m1" => -1.0, "m2" => 1.0), :forward) r1.gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])] # add some gene reaction rules r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :reverse) r3 = Reaction("r3", Dict("m2" => -1.0, "m3" => 1.0), :bidirectional) +r4 = Reaction("r3", Dict("m2" => -1.0, "m4" => 1.0), :forward) +r_m3 = Reaction("r3", Dict("m3" => -1.0), :bidirectional) +r_m4 = Reaction("r3", Dict("m4" => -1.0), :forward) +r5 = Reaction("r5", Dict("m4" => -1.0, "m2" => 1.0), :forward) -add_reactions!(model, [r1, r2, r3, r_m1]) # function approach +add_reactions!(model, [r1, r2, r3, r_m1, r4, r_m3, r_m4, r5]) # function approach m1 = metabolite_list[1] m2 = metabolite_list[2] m3 = metabolite_list[3] m4 = metabolite_list[4] -@add_reactions! model begin # macro approach - "r4", m2 → m4, 0, 1000 - "r_m3", m3 ↔ nothing, -1000, 1000 - "r_m4", m4 → nothing - "r5", m4 → m2 -end - model.reactions["r4"].gene_associations = [Isozyme(x) for x in [["g5"], ["g6", "g7"], ["g8"]]] diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 510ef9c86..cc351b315 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -58,60 +58,6 @@ add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) """ $(TYPEDSIGNATURES) -Shortcut to add multiple reactions and their lower and upper bounds - -Call variants -------------- -``` -@add_reactions! model begin - reaction_name, reaction -end - -@add_reactions! model begin - reaction_name, reaction, lower_bound -end - -@add_reactions! model begin - reaction_name, reaction, lower_bound, upper_bound -end -``` - -Examples --------- -``` -@add_reactions! model begin - "v1", nothing → A, 0, 500 - "v2", A ↔ B + C, -500 - "v3", B + C → nothing -end -``` -""" -macro add_reactions!(model::Symbol, ex::Expr) - model = esc(model) - all_reactions = Expr(:block) - for line in MacroTools.striplines(ex).args - args = line.args - id = esc(args[1]) - reaction = esc(args[2]) - push!(all_reactions.args, :(r = $reaction)) - push!(all_reactions.args, :(r.id = $id)) - if length(args) == 3 - lb = args[3] - push!(all_reactions.args, :(r.lower_bound = $lb)) - elseif length(args) == 4 - lb = args[3] - ub = args[4] - push!(all_reactions.args, :(r.lower_bound = $lb)) - push!(all_reactions.args, :(r.upper_bound = $ub)) - end - push!(all_reactions.args, :(add_reaction!($model, r))) - end - return all_reactions -end - -""" -$(TYPEDSIGNATURES) - Remove all genes with `ids` from `model`. If `knockout_reactions` is true, then also constrain reactions that require the genes to function to carry zero flux. diff --git a/test/reconstruction/add_reactions.jl b/test/reconstruction/add_reactions.jl deleted file mode 100644 index f5d7b24b2..000000000 --- a/test/reconstruction/add_reactions.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "@add_reactions! helper" begin - mod = ObjectModel(id = "testmodel") - A = Metabolite(id = "A") - B = Metabolite(id = "B") - C = Metabolite(id = "C") - add_metabolites!(mod, [A, B, C]) - - @add_reactions! mod begin - "v1", nothing ↔ A - "v2", nothing ↔ B, -500 - "v3", nothing ↔ C, -500, 500 - end - - rxn = mod.reactions["v1"] - @test rxn.lower_bound == -1000.0 - @test rxn.upper_bound == 1000.0 - - rxn = mod.reactions["v2"] - @test rxn.lower_bound == -500 - @test rxn.upper_bound == 1000.0 - - rxn = mod.reactions["v3"] - @test rxn.lower_bound == -500 - @test rxn.upper_bound == 500 -end diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 09f0bb1fe..d710d6db2 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -6,20 +6,24 @@ (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id = "m$i") for i = 1:8] - @add_reactions! model begin - "r1", nothing → m1, 0, 1 - "r2", m1 ↔ m2, -10, 100 - "r3", m1 → m3, 0, 100 - "r4", m2 ↔ m4, 0, 100 - # "r5", m3 → m4, 0, 100 - "r6", m4 → nothing, 0, 100 - # "r7", m2 → m7 + m6, 0, 100 - "r8", m7 → m8, 0, 100 - "r9", m8 → nothing, 0, 100 - # "r10", m6 → nothing, 0, 100 - "r11", m2 + m3 + m7 → nothing, 0, 100 - "r12", m3 → m5, -10, 10 - end + add_reactions!( + model, + [ + # Reaction("r1", Dict("m1" => 1), :forward; upper_bound = 1), + Reaction("r2", Dict("m1" => -1, "m2" => 1), :bidirectional; lower_bound = -10), + Reaction("r3", Dict("m1" => -1, "m3" => 1), :forward), + Reaction("r4", Dict("m2" => -1, "m4" => 1), :bidirectional), + # Reaction("r5", Dict("m3" => -1, "m4" => 1), :forward), + Reaction("r6", Dict("m4" => -1), :forward), + # Reaction("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1), :forward), + Reaction("r8", Dict("m7" => -1, "m8" => 1), :forward), + Reaction("r9", Dict("m8" => -1), :forward), + # Reaction("r10", Dict("m6" => -1), :forward), + Reaction("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1), :forward), + Reaction("r12", Dict("m3" => -1, "m5" => 1), :forward; lower_bound = -10, upper_bound = 10), + + ] + ) model.objective = Dict("r11" => 1) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index b689edadc..3a17c4ccc 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -78,14 +78,17 @@ end m3 = Metabolite("m3") m4 = Metabolite("m4") - @add_reactions! m begin - "r1", nothing → m1, 0, 100 - "r2", nothing → m2, 0, 100 - "r3", m1 + m2 → m3, 0, 100 - "r4", m3 → m4, 0, 100 - "r5", m2 ↔ m4, -100, 100 - "r6", m4 → nothing, 0, 100 - end + add_reactions!( + model, + [ + Reaction("r1"; Dict("m1" => 1), :forward), + Reaction("r2"; Dict("m2" => 1), :forward), + Reaction("r3"; Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), + Reaction("r4"; Dict("m3" => -1, "m4" => 1), :forward), + Reaction("r5"; Dict("m2" => -1, "m4" => -1), :bidirectional), + Reaction("r6"; Dict("m4" => -1), :forward), + ] + ) gs = [Gene("g$i") for i = 1:5] From 71716a9c8565436a0fd08fd82e5658f2f89e7799 Mon Sep 17 00:00:00 2001 From: exaexa Date: Thu, 10 Nov 2022 08:06:06 +0000 Subject: [PATCH 058/531] automatic formatting triggered by @exaexa on PR #697 --- test/reconstruction/gapfill_minimum_reactions.jl | 11 ++++++++--- test/reconstruction/gecko.jl | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index d710d6db2..a7f378dc3 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -20,9 +20,14 @@ Reaction("r9", Dict("m8" => -1), :forward), # Reaction("r10", Dict("m6" => -1), :forward), Reaction("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1), :forward), - Reaction("r12", Dict("m3" => -1, "m5" => 1), :forward; lower_bound = -10, upper_bound = 10), - - ] + Reaction( + "r12", + Dict("m3" => -1, "m5" => 1), + :forward; + lower_bound = -10, + upper_bound = 10, + ), + ], ) model.objective = Dict("r11" => 1) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 3a17c4ccc..104c669fb 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -87,7 +87,7 @@ end Reaction("r4"; Dict("m3" => -1, "m4" => 1), :forward), Reaction("r5"; Dict("m2" => -1, "m4" => -1), :bidirectional), Reaction("r6"; Dict("m4" => -1), :forward), - ] + ], ) gs = [Gene("g$i") for i = 1:5] From a400d3203b70bf5c5551b8b0a88325fa646e8f41 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 10 Nov 2022 11:01:24 +0100 Subject: [PATCH 059/531] fix tests --- test/reconstruction/gapfill_minimum_reactions.jl | 13 ++++++++----- test/reconstruction/gecko.jl | 14 +++++++------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index a7f378dc3..5a5b45f13 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -6,11 +6,15 @@ (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id = "m$i") for i = 1:8] + #= + here reactions are added to the model, but some are commented out. The goal + of the gap filling is to identify these commented reactions. + =# add_reactions!( model, [ - # Reaction("r1", Dict("m1" => 1), :forward; upper_bound = 1), - Reaction("r2", Dict("m1" => -1, "m2" => 1), :bidirectional; lower_bound = -10), + Reaction("r1", Dict("m1" => 1), :forward; default_bound = 1), + Reaction("r2", Dict("m1" => -1, "m2" => 1), :bidirectional; default_bound = 10), Reaction("r3", Dict("m1" => -1, "m3" => 1), :forward), Reaction("r4", Dict("m2" => -1, "m4" => 1), :bidirectional), # Reaction("r5", Dict("m3" => -1, "m4" => 1), :forward), @@ -23,9 +27,8 @@ Reaction( "r12", Dict("m3" => -1, "m5" => 1), - :forward; - lower_bound = -10, - upper_bound = 10, + :bidirectional; + default_bound = 10, ), ], ) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 104c669fb..2915458f3 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -79,14 +79,14 @@ end m4 = Metabolite("m4") add_reactions!( - model, + m, [ - Reaction("r1"; Dict("m1" => 1), :forward), - Reaction("r2"; Dict("m2" => 1), :forward), - Reaction("r3"; Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), - Reaction("r4"; Dict("m3" => -1, "m4" => 1), :forward), - Reaction("r5"; Dict("m2" => -1, "m4" => -1), :bidirectional), - Reaction("r6"; Dict("m4" => -1), :forward), + Reaction("r1", Dict("m1" => 1), :forward), + Reaction("r2", Dict("m2" => 1), :forward), + Reaction("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), + Reaction("r4", Dict("m3" => -1, "m4" => 1), :forward), + Reaction("r5", Dict("m2" => -1, "m4" => -1), :bidirectional), + Reaction("r6", Dict("m4" => -1), :forward), ], ) From b9ce5e883d625ac93d46f73e1bdfe6a7272ae7a8 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 10 Nov 2022 11:01:39 +0100 Subject: [PATCH 060/531] fix more tests --- test/reconstruction/gecko.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/gecko.jl index 2915458f3..f9893edbd 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/gecko.jl @@ -85,7 +85,7 @@ end Reaction("r2", Dict("m2" => 1), :forward), Reaction("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), Reaction("r4", Dict("m3" => -1, "m4" => 1), :forward), - Reaction("r5", Dict("m2" => -1, "m4" => -1), :bidirectional), + Reaction("r5", Dict("m2" => -1, "m4" => 1), :bidirectional), Reaction("r6", Dict("m4" => -1), :forward), ], ) From 4978efc50529fb82d199fe807f55393a9b203b65 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 15 Dec 2022 13:52:30 +0100 Subject: [PATCH 061/531] n_variables --- docs/src/examples/04_standardmodel.jl | 2 +- src/analysis/flux_variability_analysis.jl | 8 ++++---- src/analysis/max_min_driving_force.jl | 4 ++-- src/analysis/modifications/generic.jl | 2 +- .../gapfill_minimum_reactions.jl | 4 ++-- src/analysis/sampling/affine_hit_and_run.jl | 4 ++-- src/analysis/sampling/warmup_variability.jl | 6 +++--- src/io/show/AbstractMetabolicModel.jl | 4 ++-- src/io/show/BalancedGrowthCommunityModel.jl | 2 +- src/reconstruction/MatrixCoupling.jl | 12 +++++------ src/reconstruction/MatrixModel.jl | 6 +++--- src/reconstruction/gecko.jl | 2 +- src/reconstruction/smoment.jl | 2 +- src/types/accessors/AbstractMetabolicModel.jl | 10 +++++----- src/types/misc/gecko.jl | 2 +- src/types/misc/smoment.jl | 2 +- .../models/BalancedGrowthCommunityModel.jl | 8 ++++---- src/types/models/HDF5Model.jl | 2 +- src/types/models/JSONModel.jl | 2 +- src/types/models/MATModel.jl | 20 +++++++++---------- src/types/models/ObjectModel.jl | 4 ++-- src/types/models/SBMLModel.jl | 2 +- src/types/wrappers/GeckoModel.jl | 2 +- src/types/wrappers/MatrixCoupling.jl | 4 ++-- src/types/wrappers/SMomentModel.jl | 2 +- test/analysis/flux_balance_analysis.jl | 2 +- test/analysis/sampling/affine_hit_and_run.jl | 2 +- test/io/h5.jl | 2 +- test/io/io.jl | 8 ++++---- test/io/sbml.jl | 2 +- test/reconstruction/MatrixCoupling.jl | 4 ++-- test/reconstruction/MatrixModel.jl | 6 +++--- test/types/BalancedGrowthCommunityModel.jl | 4 ++-- test/types/ObjectModel.jl | 2 +- test/utils/MatrixModel.jl | 2 +- 35 files changed, 76 insertions(+), 76 deletions(-) diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl index ceb40b533..2cb8e3260 100644 --- a/docs/src/examples/04_standardmodel.jl +++ b/docs/src/examples/04_standardmodel.jl @@ -79,7 +79,7 @@ model.genes[random_gene_id] random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] model.metabolites[random_metabolite_id] # -random_reaction_id = reactions(model)[rand(1:n_reactions(model))] +random_reaction_id = reactions(model)[rand(1:n_variables(model))] model.reactions[random_reaction_id] # `ObjectModel` can be used to build your own metabolic model or modify an diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index aee0a7a61..fa5510b89 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -55,7 +55,7 @@ function flux_variability_analysis( bounds = z -> (z, Inf), ret = objective_value, ) - if size(fluxes, 1) != n_reactions(model) + if size(fluxes, 1) != n_variables(model) throw( DomainError( size(fluxes, 1), @@ -189,7 +189,7 @@ function reaction_variability_analysis( optimizer; kwargs..., ) - if any((reaction_indexes .< 1) .| (reaction_indexes .> n_reactions(model))) + if any((reaction_indexes .< 1) .| (reaction_indexes .> n_variables(model))) throw(DomainError(reaction_indexes, "Flux index out of range")) end @@ -199,7 +199,7 @@ function reaction_variability_analysis( reaction_indexes, 1:length(reaction_indexes), 1.0, - n_reactions(model), + n_variables(model), length(reaction_indexes), ), optimizer; @@ -215,7 +215,7 @@ Shortcut for [`reaction_variability_analysis`](@ref) that examines all reactions reaction_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = reaction_variability_analysis( model, - collect(1:n_reactions(model)), + collect(1:n_variables(model)), optimizer; kwargs..., ) diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl index b84a0b777..4d888bc5a 100644 --- a/src/analysis/max_min_driving_force.jl +++ b/src/analysis/max_min_driving_force.jl @@ -71,7 +71,7 @@ function max_min_driving_force( @variables opt_model begin mmdf logcs[1:n_metabolites(model)] - dgrs[1:n_reactions(model)] + dgrs[1:n_variables(model)] end # set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption) @@ -215,7 +215,7 @@ function max_min_driving_force_variability( dgr_variants = [ [[_mmdf_add_df_bound(lb, ub), _mmdf_dgr_objective(ridx, sense)]] for - ridx = 1:n_reactions(model), sense in [MAX_SENSE, MIN_SENSE] + ridx = 1:n_variables(model), sense in [MAX_SENSE, MIN_SENSE] ] concen_variants = [ [[_mmdf_add_df_bound(lb, ub), _mmdf_concen_objective(midx, sense)]] for diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 2a56c9cb6..9a2b896e0 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -54,7 +54,7 @@ change_objective( ) # Initialize weights - opt_weights = spzeros(n_reactions(model)) + opt_weights = spzeros(n_variables(model)) isempty(weights) && (weights = ones(length(objective_indices))) # equal weights diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl index a91b76e1d..f2cfd0c5f 100644 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ b/src/analysis/reconstruction/gapfill_minimum_reactions.jl @@ -60,7 +60,7 @@ function gapfill_minimum_reactions( # stoichiometry extended_stoichiometry = [[ stoichiometry(model) - spzeros(length(univs.new_mids), n_reactions(model)) + spzeros(length(univs.new_mids), n_variables(model)) ] univs.stoichiometry] # make the model anew (we can't really use make_optimization_model because @@ -69,7 +69,7 @@ function gapfill_minimum_reactions( # tiny temporary wrapper for this. # keep this in sync with src/base/solver.jl, except for adding balances. opt_model = Model(optimizer) - @variable(opt_model, x[1:n_reactions(model)]) + @variable(opt_model, x[1:n_variables(model)]) xl, xu = bounds(model) @constraint(opt_model, lbs, xl .<= x) @constraint(opt_model, ubs, x .<= xu) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index 41776fb24..c0f00fb4d 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -38,13 +38,13 @@ function affine_hit_and_run( chains = length(workers), seed = rand(Int), ) - @assert size(warmup_points, 1) == n_reactions(m) + @assert size(warmup_points, 1) == n_variables(m) lbs, ubs = bounds(m) C = coupling(m) cl, cu = coupling_bounds(m) if isnothing(C) - C = zeros(0, n_reactions(m)) + C = zeros(0, n_variables(m)) cl = zeros(0) cu = zeros(0) end diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl index db41e1aae..9c6b8c3f0 100644 --- a/src/analysis/sampling/warmup_variability.jl +++ b/src/analysis/sampling/warmup_variability.jl @@ -12,7 +12,7 @@ function warmup_from_variability( seed = rand(Int); kwargs..., ) - nr = n_reactions(model) + nr = n_variables(model) n_points > 2 * nr && throw( DomainError( @@ -48,8 +48,8 @@ single column in the result. function warmup_from_variability( model::AbstractMetabolicModel, optimizer, - min_reactions::AbstractVector{Int} = 1:n_reactions(model), - max_reactions::AbstractVector{Int} = 1:n_reactions(model); + min_reactions::AbstractVector{Int} = 1:n_variables(model), + max_reactions::AbstractVector{Int} = 1:n_variables(model); modifications = [], workers::Vector{Int} = [myid()], )::Matrix{Float64} diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index 155aeb1cd..dabd6d4b3 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -5,11 +5,11 @@ Pretty printing of everything metabolic-modelish. """ function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m))") - if n_reactions(m) <= constants.default_stoich_show_size + if n_variables(m) <= constants.default_stoich_show_size println(io, stoichiometry(m)) else # too big to display nicely println(io, "S = [...]") end - _pretty_print_keyvals(io, "Number of reactions: ", string(n_reactions(m))) + _pretty_print_keyvals(io, "Number of reactions: ", string(n_variables(m))) _pretty_print_keyvals(io, "Number of metabolites: ", string(n_metabolites(m))) end diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl index 41a9d8c6b..02e6c3d84 100644 --- a/src/io/show/BalancedGrowthCommunityModel.jl +++ b/src/io/show/BalancedGrowthCommunityModel.jl @@ -1,7 +1,7 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) println( io, - "A $(typeof(cm.model)) community member with $(n_reactions(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.", + "A $(typeof(cm.model)) community member with $(n_variables(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.", ) end diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 2131ef19e..852328440 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -16,7 +16,7 @@ function add_reactions( new_lm = add_reactions(m.lm, s, b, c, xl, xu, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), m.cl, m.cu, ) @@ -50,7 +50,7 @@ function add_reactions( ) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), m.cl, m.cu, ) @@ -71,7 +71,7 @@ function add_reactions( new_lm = add_reactions(m.lm, Sp, b, c, xl, xu, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), m.cl, m.cu, ) @@ -90,7 +90,7 @@ function add_reactions( new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m1.C, spzeros(size(m1.C, 1), n_reactions(new_lm) - n_reactions(m1.lm))), + hcat(m1.C, spzeros(size(m1.C, 1), n_variables(new_lm) - n_variables(m1.lm))), m1.cl, m1.cu, ) @@ -123,7 +123,7 @@ function add_reactions( ) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_reactions(new_lm) - n_reactions(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), m.cl, m.cu, ) @@ -182,7 +182,7 @@ function add_coupling_constraints!( all([length(cu), length(cl)] .== size(C, 1)) || throw(DimensionMismatch("mismatched numbers of constraints")) - size(C, 2) == n_reactions(m) || + size(C, 2) == n_variables(m) || throw(DimensionMismatch("mismatched number of reactions")) m.C = vcat(m.C, sparse(C)) diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 38b9a3731..71196c21b 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -180,7 +180,7 @@ function add_reactions( new_mets = vcat(m.mets, mets[new_metabolites]) - zero_block = spzeros(length(new_metabolites), n_reactions(m)) + zero_block = spzeros(length(new_metabolites), n_variables(m)) ext_s = vcat(sparse(m.S), zero_block) mapping = [findfirst(isequal(met), new_mets) for met in mets] @@ -308,7 +308,7 @@ end end @_remove_fn reaction MatrixModel Int inplace plural begin - mask = .!in.(1:n_reactions(model), Ref(reaction_idxs)) + mask = .!in.(1:n_variables(model), Ref(reaction_idxs)) model.S = model.S[:, mask] model.c = model.c[mask] model.xl = model.xl[mask] @@ -351,7 +351,7 @@ end remove_reactions!( model, [ - ridx for ridx in 1:n_reactions(model) if + ridx for ridx in 1:n_variables(model) if any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) ], ) diff --git a/src/reconstruction/gecko.jl b/src/reconstruction/gecko.jl index c22c40f9e..ab1dada8c 100644 --- a/src/reconstruction/gecko.jl +++ b/src/reconstruction/gecko.jl @@ -59,7 +59,7 @@ function make_gecko_model( gene_name_lookup = Dict(gids .=> 1:length(gids)) gene_row_lookup = Dict{Int,Int}() - for i = 1:n_reactions(model) + for i = 1:n_variables(model) isozymes = ris_(rids[i]) if isnothing(isozymes) push!(columns, Types._GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) diff --git a/src/reconstruction/smoment.jl b/src/reconstruction/smoment.jl index c59b9a817..5f49b79d7 100644 --- a/src/reconstruction/smoment.jl +++ b/src/reconstruction/smoment.jl @@ -37,7 +37,7 @@ function make_smoment_model( (lbs, ubs) = bounds(model) rids = reactions(model) - for i = 1:n_reactions(model) + for i = 1:n_variables(model) isozyme = ris_(rids[i]) if isnothing(isozyme) # non-enzymatic reaction (or a totally ignored one) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 33195b795..070709043 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -41,7 +41,7 @@ $(TYPEDSIGNATURES) Get the number of reactions in a model. """ -function n_reactions(a::AbstractMetabolicModel)::Int +function n_variables(a::AbstractMetabolicModel)::Int length(reactions(a)) end @@ -115,7 +115,7 @@ function fluxes(a::AbstractMetabolicModel)::Vector{String} end function n_fluxes(a::AbstractMetabolicModel)::Int - n_reactions(a) + n_variables(a) end """ @@ -123,11 +123,11 @@ $(TYPEDSIGNATURES) Retrieve a sparse matrix that describes the correspondence of a solution of the linear system to the fluxes (see [`fluxes`](@ref) for rationale). Returns a -sparse matrix of size `(n_reactions(a), n_fluxes(a))`. For most models, this is +sparse matrix of size `(n_variables(a), n_fluxes(a))`. For most models, this is an identity matrix. """ function reaction_flux(a::AbstractMetabolicModel)::SparseMat - nr = n_reactions(a) + nr = n_variables(a) nf = n_fluxes(a) nr == nf || missing_impl_error(reaction_flux, (a,)) spdiagm(fill(1, nr)) @@ -140,7 +140,7 @@ Get a matrix of coupling constraint definitions of a model. By default, there is no coupling in the models. """ function coupling(a::AbstractMetabolicModel)::SparseMat - return spzeros(0, n_reactions(a)) + return spzeros(0, n_variables(a)) end """ diff --git a/src/types/misc/gecko.jl b/src/types/misc/gecko.jl index 58f5daaf1..11fe7894e 100644 --- a/src/types/misc/gecko.jl +++ b/src/types/misc/gecko.jl @@ -27,7 +27,7 @@ gecko_column_reactions(columns, inner) = sparse( [col.reaction_idx for col in columns], 1:length(columns), [col.direction >= 0 ? 1 : -1 for col in columns], - n_reactions(inner), + n_variables(inner), length(columns), ) diff --git a/src/types/misc/smoment.jl b/src/types/misc/smoment.jl index 2627e3e46..6146b4ef5 100644 --- a/src/types/misc/smoment.jl +++ b/src/types/misc/smoment.jl @@ -18,6 +18,6 @@ smoment_column_reactions(model::SMomentModel) = sparse( [col.reaction_idx for col in model.columns], 1:length(model.columns), [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_reactions(model.inner), + n_variables(model.inner), length(model.columns), ) diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 65243fc36..75a73ff23 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -75,8 +75,8 @@ $(TYPEDSIGNATURES) Return the number of reactions in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). """ -function Accessors.n_reactions(cm::BalancedGrowthCommunityModel) - num_model_reactions = sum(n_reactions(m.model) for m in cm.members) +function Accessors.n_variables(cm::BalancedGrowthCommunityModel) + num_model_reactions = sum(n_variables(m.model) for m in cm.members) # assume each env metabolite gets an env exchange num_env_metabolites = length(get_env_mets(cm)) return num_model_reactions + num_env_metabolites + 1 # add 1 for the community biomass @@ -188,7 +188,7 @@ rate/balanced growth objective. Consequently, the relation `community_growth * abundance_species_i = growth_species_i` should hold. """ function Accessors.objective(cm::BalancedGrowthCommunityModel) - vec = spzeros(n_reactions(cm)) + vec = spzeros(n_variables(cm)) vec[end] = 1.0 return vec end @@ -200,7 +200,7 @@ Coupling constraint matrix for a [`BalancedGrowthCommunityModel`](@ref). """ function Accessors.coupling(cm::BalancedGrowthCommunityModel) coups = blockdiag([coupling(m.model) for m in cm.members]...) - n = n_reactions(cm) + n = n_variables(cm) return [coups spzeros(size(coups, 1), n - size(coups, 2))] end diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index cf92ddc79..a6b244c05 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -33,7 +33,7 @@ function Accessors.precache!(model::HDF5Model)::Nothing nothing end -function Accessors.n_reactions(model::HDF5Model)::Int +function Accessors.n_variables(model::HDF5Model)::Int precache!(model) length(model.h5["reactions"]) end diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 23b9572b6..c9f724665 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -71,7 +71,7 @@ end _parse_notes(x)::Notes = _parse_annotations(x) -Accessors.n_reactions(model::JSONModel) = length(model.rxns) +Accessors.n_variables(model::JSONModel) = length(model.rxns) Accessors.n_metabolites(model::JSONModel) = length(model.mets) Accessors.n_genes(model::JSONModel) = length(model.genes) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 393e909c5..c6b2cd6c0 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -11,7 +11,7 @@ struct MATModel <: AbstractMetabolicModel end Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) -Accessors.n_reactions(m::MATModel)::Int = size(m.mat["S"], 2) +Accessors.n_variables(m::MATModel)::Int = size(m.mat["S"], 2) """ $(TYPEDSIGNATURES) @@ -20,9 +20,9 @@ Extracts reaction names from `rxns` key in the MAT file. """ function Accessors.reactions(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") - reshape(m.mat["rxns"], n_reactions(m)) + reshape(m.mat["rxns"], n_variables(m)) else - "rxn" .* string.(1:n_reactions(m)) + "rxn" .* string.(1:n_variables(m)) end end @@ -62,8 +62,8 @@ $(TYPEDSIGNATURES) Extracts bounds from the MAT file, saved under `lb` and `ub`. """ Accessors.bounds(m::MATModel) = ( - reshape(get(m.mat, "lb", fill(-Inf, n_reactions(m), 1)), n_reactions(m)), - reshape(get(m.mat, "ub", fill(Inf, n_reactions(m), 1)), n_reactions(m)), + reshape(get(m.mat, "lb", fill(-Inf, n_variables(m), 1)), n_variables(m)), + reshape(get(m.mat, "ub", fill(Inf, n_variables(m), 1)), n_variables(m)), ) """ @@ -85,7 +85,7 @@ $(TYPEDSIGNATURES) Extracts the objective from the MAT model (defaults to zeroes). """ Accessors.objective(m::MATModel) = - sparse(reshape(get(m.mat, "c", zeros(n_reactions(m), 1)), n_reactions(m))) + sparse(reshape(get(m.mat, "c", zeros(n_variables(m), 1)), n_variables(m))) """ $(TYPEDSIGNATURES) @@ -93,8 +93,8 @@ $(TYPEDSIGNATURES) Extract coupling matrix stored, in `C` key. """ Accessors.coupling(m::MATModel) = - _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_reactions(m)+1:end, :]) : - sparse(get(m.mat, "C", zeros(0, n_reactions(m)))) + _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_variables(m)+1:end, :]) : + sparse(get(m.mat, "C", zeros(0, n_variables(m)))) """ $(TYPEDSIGNATURES) @@ -106,7 +106,7 @@ function Accessors.coupling_bounds(m::MATModel) if _mat_has_squashed_coupling(m.mat) ( sparse(fill(-Inf, nc)), - sparse(reshape(m.mat["b"], length(m.mat["b"]))[n_reactions(m)+1:end]), + sparse(reshape(m.mat["b"], length(m.mat["b"]))[n_variables(m)+1:end]), ) else ( @@ -232,7 +232,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) lb, ub = bounds(m) cl, cu = coupling_bounds(m) - nr = n_reactions(m) + nr = n_variables(m) nm = n_metabolites(m) return MATModel( Dict( diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 51e83f540..5a8fb03e2 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -64,7 +64,7 @@ $(TYPEDSIGNATURES) Return the number of reactions contained in `model`. """ -Accessors.n_reactions(model::ObjectModel)::Int = length(model.reactions) +Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) """ $(TYPEDSIGNATURES) @@ -134,7 +134,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat push!(SV, coeff) end end - return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_reactions(model)) + return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) end """ diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index c9e156d72..8426ed257 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -32,7 +32,7 @@ $(TYPEDSIGNATURES) Efficient counting of reactions in [`SBMLModel`](@ref). """ -Accessors.n_reactions(model::SBMLModel)::Int = length(model.sbml.reactions) +Accessors.n_variables(model::SBMLModel)::Int = length(model.sbml.reactions) """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index ca427500e..c31f72a66 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -137,7 +137,7 @@ $(TYPEDSIGNATURES) Returns the number of all irreversible reactions in `model` as well as the number of gene products that take part in enzymatic reactions. """ -Accessors.n_reactions(model::GeckoModel) = length(model.columns) + n_genes(model) +Accessors.n_variables(model::GeckoModel) = length(model.columns) + n_genes(model) """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/MatrixCoupling.jl b/src/types/wrappers/MatrixCoupling.jl index 786c0e54f..0d1f30f88 100644 --- a/src/types/wrappers/MatrixCoupling.jl +++ b/src/types/wrappers/MatrixCoupling.jl @@ -25,7 +25,7 @@ mutable struct MatrixCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetab ) where {M<:AbstractMetabolicModel} length(cu) == length(cl) || throw(DimensionMismatch("`cl` and `cu` need to have the same size")) - size(C) == (length(cu), n_reactions(lm)) || + size(C) == (length(cu), n_variables(lm)) || throw(DimensionMismatch("wrong dimensions of `C`")) new{M}(lm, sparse(C), collect(cl), collect(cu)) @@ -80,7 +80,7 @@ function Base.convert( (cl, cu) = coupling_bounds(mm) MatrixCoupling(convert(M, mm), coupling(mm), cl, cu) else - MatrixCoupling(convert(M, mm), spzeros(0, n_reactions(mm)), spzeros(0), spzeros(0)) + MatrixCoupling(convert(M, mm), spzeros(0, n_variables(mm)), spzeros(0), spzeros(0)) end end diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index a14be4245..77ad6b05b 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -100,7 +100,7 @@ $(TYPEDSIGNATURES) The number of reactions (including split ones) in [`SMomentModel`](@ref). """ -Accessors.n_reactions(model::SMomentModel) = length(model.columns) +Accessors.n_variables(model::SMomentModel) = length(model.columns) """ $(TYPEDSIGNATURES) diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index 07b472f58..bc087584b 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -96,7 +96,7 @@ end γ = 40 # construct coupling bounds - nr = n_reactions(model) + nr = n_variables(model) biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], reactions(model))) Cf = sparse(1.0I, nr, nr) diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl index 8159e0883..a98bd48ae 100644 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ b/test/analysis/sampling/affine_hit_and_run.jl @@ -2,7 +2,7 @@ model = load_model(model_paths["e_coli_core.json"]) - cm = MatrixCoupling(model, zeros(1, n_reactions(model)), [17.0], [19.0]) + cm = MatrixCoupling(model, zeros(1, n_variables(model)), [17.0], [19.0]) pfk, tala = indexin(["PFK", "TALA"], reactions(cm)) cm.C[:, [pfk, tala]] .= 1.0 diff --git a/test/io/h5.jl b/test/io/h5.jl index 72fda108d..4a9d214be 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -12,7 +12,7 @@ @test !isnothing(h5.h5) # briefly test that the loading is okay - @test n_reactions(model) == n_reactions(h5) + @test n_variables(model) == n_variables(h5) @test n_metabolites(model) == n_metabolites(h5) @test issetequal(reactions(model), reactions(h5)) @test issetequal(metabolites(model), metabolites(h5)) diff --git a/test/io/io.jl b/test/io/io.jl index ce44a7216..ad1ee32af 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -2,15 +2,15 @@ sbmlmodel = load_model(model_paths["iJO1366.xml"]) @test sbmlmodel isa SBMLModel - @test n_reactions(sbmlmodel) == 2583 + @test n_variables(sbmlmodel) == 2583 matlabmodel = load_model(model_paths["iJO1366.mat"]) @test matlabmodel isa MATModel - @test n_reactions(matlabmodel) == 2583 + @test n_variables(matlabmodel) == 2583 jsonmodel = load_model(model_paths["iJO1366.json"]) @test jsonmodel isa JSONModel - @test n_reactions(jsonmodel) == 2583 + @test n_variables(jsonmodel) == 2583 @test Set(lowercase.(reactions(sbmlmodel))) == Set("r_" .* lowercase.(reactions(matlabmodel))) @@ -19,7 +19,7 @@ # specifically test parsing of gene-reaction associations in Recon reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) - @test n_reactions(reconmodel) == 10600 + @test n_variables(reconmodel) == 10600 recon_grrs = [ r.gene_associations for (i, r) in reconmodel.reactions if !isnothing(r.gene_associations) diff --git a/test/io/sbml.jl b/test/io/sbml.jl index 9ff8ecdca..eef9b1c28 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -4,7 +4,7 @@ m = convert(MatrixModel, sbmlm) @test size(stoichiometry(sbmlm)) == (92, 95) - @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_reactions(sbmlm)) + @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_variables(sbmlm)) @test length(m.S.nzval) == 380 @test length.(bounds(sbmlm)) == (95, 95) @test length.(bounds(m)) == (95, 95) diff --git a/test/reconstruction/MatrixCoupling.jl b/test/reconstruction/MatrixCoupling.jl index 690254d6f..97019ea48 100644 --- a/test/reconstruction/MatrixCoupling.jl +++ b/test/reconstruction/MatrixCoupling.jl @@ -25,7 +25,7 @@ cp = test_coupledLP() n_c = n_coupling_constraints(cp) new_cp = remove_coupling_constraints(cp, 1) - @test size(coupling(cp)) == (n_c, n_reactions(cp)) + @test size(coupling(cp)) == (n_c, n_variables(cp)) @test n_c - 1 == n_coupling_constraints(new_cp) @test n_coupling_constraints(cp) == n_c new_cp = remove_coupling_constraints(cp, [1, 2]) @@ -117,7 +117,7 @@ end @testset "Remove reactions" begin cp = convert(MatrixModelWithCoupling, test_LP()) - cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_reactions(cp)), -1.0, 1.0) + cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_variables(cp)), -1.0, 1.0) new_cp = remove_reactions(cp, [3, 2]) @test new_cp isa MatrixModelWithCoupling diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index 2fb3d512f..49cbbc999 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -85,7 +85,7 @@ end 1.0, check_consistency = true, ) - @test n_reactions(cp) + 1 == n_reactions(new_cp) + @test n_variables(cp) + 1 == n_variables(new_cp) (new_cp, new_reactions, new_mets) = add_reactions( cp, @@ -98,7 +98,7 @@ end ["m1", "m2", "m3", "m6"], check_consistency = true, ) - @test n_reactions(cp) == n_reactions(new_cp) + @test n_variables(cp) == n_variables(new_cp) @test n_metabolites(cp) + 1 == n_metabolites(new_cp) end @@ -167,7 +167,7 @@ end # proper subset of existing metabolites cp = test_LP() new_cp = add_reactions(cp, [-1.0], zeros(1), 1.0, 0.0, 1.0, "r4", ["m1"]) - @test n_reactions(cp) + 1 == n_reactions(new_cp) + @test n_variables(cp) + 1 == n_variables(new_cp) @test_throws DimensionMismatch add_reactions( cp, diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index bab1032aa..1c5a80c75 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -121,7 +121,7 @@ ], ) - @test n_reactions(cm) == 14 + @test n_variables(cm) == 14 @test n_metabolites(cm) == 13 @test n_genes(cm) == 8 @@ -259,7 +259,7 @@ end # test if model can be converted to another type om = convert(ObjectModel, cm) - @test n_reactions(om) == n_reactions(cm) + @test n_variables(om) == n_variables(cm) end @testset "BalancedGrowthCommunityModel: enzyme constrained e coli" begin diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index c8bfdf984..3331a855a 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -49,7 +49,7 @@ @test "r1" in reactions(model) @test "m4" in metabolites(model) @test "g2" in genes(model) - @test n_reactions(model) == 4 + @test n_variables(model) == 4 @test n_metabolites(model) == 4 @test n_genes(model) == 3 diff --git a/test/utils/MatrixModel.jl b/test/utils/MatrixModel.jl index 64c3121c5..d808700c1 100644 --- a/test/utils/MatrixModel.jl +++ b/test/utils/MatrixModel.jl @@ -1,6 +1,6 @@ @testset "MatrixModel utilities" begin cp = test_LP() - @test n_reactions(cp) == 3 + @test n_variables(cp) == 3 @test n_metabolites(cp) == 4 @test n_coupling_constraints(cp) == 0 From e1627579059081f1848b83b2d0e5fc873fe6dfef Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 15 Dec 2022 14:06:38 +0100 Subject: [PATCH 062/531] reactions -> variables --- docs/src/examples/01_loading.jl | 6 +-- docs/src/examples/03b_accessors.jl | 6 +-- docs/src/examples/04_standardmodel.jl | 4 +- .../04b_standardmodel_construction.jl | 2 +- docs/src/examples/06_fva.jl | 2 +- docs/src/examples/10_crowding.jl | 2 +- docs/src/examples/11_growth.jl | 2 +- docs/src/examples/14_smoment.jl | 2 +- docs/src/examples/15_gecko.jl | 2 +- docs/src/examples/16_hit_and_run.jl | 2 +- src/analysis/envelopes.jl | 4 +- src/analysis/flux_variability_analysis.jl | 2 +- src/analysis/max_min_driving_force.jl | 10 ++--- src/analysis/minimize_metabolic_adjustment.jl | 4 +- src/analysis/modifications/crowding.jl | 2 +- src/analysis/modifications/generic.jl | 6 +-- src/analysis/modifications/knockout.jl | 2 +- src/analysis/modifications/loopless.jl | 2 +- src/io/h5.jl | 2 +- src/reconstruction/MatrixCoupling.jl | 14 +++---- src/reconstruction/MatrixModel.jl | 10 ++--- src/reconstruction/ObjectModel.jl | 2 +- src/reconstruction/gecko.jl | 2 +- src/reconstruction/smoment.jl | 2 +- src/types/abstract/AbstractMetabolicModel.jl | 2 +- src/types/accessors/AbstractMetabolicModel.jl | 14 +++---- src/types/accessors/ModelWrapper.jl | 2 +- .../misc/BalancedGrowthCommunityModel.jl | 2 +- src/types/misc/ObjectModel.jl | 8 ++-- .../models/BalancedGrowthCommunityModel.jl | 4 +- src/types/models/HDF5Model.jl | 2 +- src/types/models/JSONModel.jl | 8 ++-- src/types/models/MATModel.jl | 10 ++--- src/types/models/MatrixModel.jl | 6 +-- src/types/models/ObjectModel.jl | 10 ++--- src/types/models/SBMLModel.jl | 4 +- src/types/wrappers/GeckoModel.jl | 8 ++-- src/types/wrappers/SMomentModel.jl | 6 +-- src/utils/fluxes.jl | 4 +- src/utils/looks_like.jl | 20 +++++----- test/analysis/envelopes.jl | 4 +- test/analysis/fba_with_crowding.jl | 2 +- test/analysis/flux_balance_analysis.jl | 4 +- test/analysis/max_min_driving_force.jl | 2 +- .../analysis/minimize_metabolic_adjustment.jl | 2 +- test/analysis/sampling/affine_hit_and_run.jl | 2 +- test/analysis/sampling/warmup_variability.jl | 2 +- test/io/h5.jl | 6 +-- test/io/io.jl | 8 ++-- test/io/json.jl | 2 +- test/io/sbml.jl | 2 +- test/reconstruction/MatrixModel.jl | 2 +- test/reconstruction/SerializedModel.jl | 4 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- test/types/JSONModel.jl | 4 +- test/types/MATModel.jl | 4 +- test/types/MatrixCoupling.jl | 8 ++-- test/types/MatrixModel.jl | 8 ++-- test/types/ObjectModel.jl | 8 ++-- test/types/SBMLModel.jl | 8 ++-- test/types/abstract/AbstractMetabolicModel.jl | 4 +- test/utils/ObjectModel.jl | 6 +-- test/utils/Serialized.jl | 4 +- test/utils/looks_like.jl | 38 +++++++++---------- 64 files changed, 170 insertions(+), 170 deletions(-) diff --git a/docs/src/examples/01_loading.jl b/docs/src/examples/01_loading.jl index 211393f4a..361d78d0b 100644 --- a/docs/src/examples/01_loading.jl +++ b/docs/src/examples/01_loading.jl @@ -57,12 +57,12 @@ typeof(sbml_model.sbml) mat_model.mat # In all cases, you can access the data in the model in the same way, e.g., -# using [`reactions`](@ref) to get a list of the reactions in the models: +# using [`variables`](@ref) to get a list of the reactions in the models: -reactions(mat_model)[1:5] +variables(mat_model)[1:5] # -reactions(json_model)[1:5] +variables(json_model)[1:5] # You can use the [generic accessors](03_exploring.md) to gather more information about # the model contents, [convert the models](02_convert_save.md) into formats more suitable for diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl index 1af182175..afec61973 100644 --- a/docs/src/examples/03b_accessors.jl +++ b/docs/src/examples/03b_accessors.jl @@ -16,10 +16,10 @@ using COBREXA download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); js = load_model("e_coli_core.json") -reactions(js) +variables(js) # std = convert(MatrixModel, js) -reactions(std) +variables(std) # All accessors allow systematic access to information about reactions, # stoichiometry, metabolite properties and chemistry, genes, and various model @@ -27,7 +27,7 @@ reactions(std) # # The most notable ones include: # -# - [`reactions`](@ref), [`metabolites`](@ref) and [`genes`](@ref) return +# - [`variables`](@ref), [`metabolites`](@ref) and [`genes`](@ref) return # respective vectors of identifiers of reactions, metabolites and genes present # in the model, # - [`stoichiometry`](@ref) returns the S matrix diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl index 2cb8e3260..aac2f756a 100644 --- a/docs/src/examples/04_standardmodel.jl +++ b/docs/src/examples/04_standardmodel.jl @@ -43,7 +43,7 @@ model = load_model(ObjectModel, "e_coli_core.json") # we specifically want to lo # and `Reaction`s. Ordered dictionaries are used because the order of the # reactions and metabolites are important for constructing a stoichiometric # matrix since the rows and columns should correspond to the order of the metabolites -# and reactions returned by calling the accessors `metabolites` and `reactions`. +# and reactions returned by calling the accessors `metabolites` and `variables`. # Each `ObjectModel` is composed of the following fields: @@ -79,7 +79,7 @@ model.genes[random_gene_id] random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] model.metabolites[random_metabolite_id] # -random_reaction_id = reactions(model)[rand(1:n_variables(model))] +random_reaction_id = variables(model)[rand(1:n_variables(model))] model.reactions[random_reaction_id] # `ObjectModel` can be used to build your own metabolic model or modify an diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 4477f691c..a7b9f49ca 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -19,7 +19,7 @@ gene_list = [Gene(string("g", num)) for num = 1:8] #md # !!! warning "Warning: Don't accidentally overwrite the generic accessors" #md # It may be tempting to call a variable `genes`, `metabolites`, or -#md # `reactions`. However, these names conflict with generic accessors +#md # `variables`. However, these names conflict with generic accessors #md # functions and will create problems downstream. add_genes!(model, gene_list) diff --git a/docs/src/examples/06_fva.jl b/docs/src/examples/06_fva.jl index 6e548af8f..c729726f8 100644 --- a/docs/src/examples/06_fva.jl +++ b/docs/src/examples/06_fva.jl @@ -71,7 +71,7 @@ flux_variability_summary((min_fluxes, max_fluxes)) # biomass "growth" along with the minimized/maximized reaction flux. # First, find the index of biomass reaction in all reactions -biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], reactions(model))) +biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], variables(model))) # Now run the FVA: vs = flux_variability_analysis( diff --git a/docs/src/examples/10_crowding.jl b/docs/src/examples/10_crowding.jl index bd4f40a29..14b341968 100644 --- a/docs/src/examples/10_crowding.jl +++ b/docs/src/examples/10_crowding.jl @@ -36,7 +36,7 @@ import Random Random.seed!(1) # for repeatability of random numbers below rid_crowding_weight = Dict( - rid => 0.002 + 0.003 * rand() for rid in reactions(model) if + rid => 0.002 + 0.003 * rand() for rid in variables(model) if !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) ) diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl index dcfe71200..de658aade 100644 --- a/docs/src/examples/11_growth.jl +++ b/docs/src/examples/11_growth.jl @@ -58,7 +58,7 @@ flux_summary( # The effect of all nutrients on the metabolism can be scanned using [`screen`](@ref). The [`change_bound`](@ref) function is, for this purpose, packed in a variant specified [`with_changed_bound`](@ref): -exchanges = filter(looks_like_exchange_reaction, reactions(model)) +exchanges = filter(looks_like_exchange_reaction, variables(model)) exchanges .=> screen( model, diff --git a/docs/src/examples/14_smoment.jl b/docs/src/examples/14_smoment.jl index 8f7b16625..463fd0af5 100644 --- a/docs/src/examples/14_smoment.jl +++ b/docs/src/examples/14_smoment.jl @@ -37,7 +37,7 @@ rxns = filter( !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && !isnothing(reaction_gene_association(model, x)), - reactions(model), + variables(model), ) # The information about each enzyme and its capabilities is stored in an diff --git a/docs/src/examples/15_gecko.jl b/docs/src/examples/15_gecko.jl index 030fda407..02d5a963c 100644 --- a/docs/src/examples/15_gecko.jl +++ b/docs/src/examples/15_gecko.jl @@ -35,7 +35,7 @@ rxns = filter( !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && !isnothing(reaction_gene_association(model, x)), - reactions(model), + variables(model), ) # The main difference from sMOMENT comes from allowing multiple isozymes per diff --git a/docs/src/examples/16_hit_and_run.jl b/docs/src/examples/16_hit_and_run.jl index 807238988..b147ed2cf 100644 --- a/docs/src/examples/16_hit_and_run.jl +++ b/docs/src/examples/16_hit_and_run.jl @@ -64,7 +64,7 @@ samples = affine_hit_and_run(model, warmup_points, sample_iters = 201:210, chain using CairoMakie -o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], reactions(model)) +o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], variables(model)) scatter( samples[o2, :], diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl index 11f5eff10..5606ae3a9 100644 --- a/src/analysis/envelopes.jl +++ b/src/analysis/envelopes.jl @@ -6,7 +6,7 @@ Version of [`envelope_lattice`](@ref) that works on string reaction IDs instead of integer indexes. """ envelope_lattice(model::AbstractMetabolicModel, rids::Vector{String}; kwargs...) = - envelope_lattice(model, Vector{Int}(indexin(rids, reactions(model))); kwargs...) + envelope_lattice(model, Vector{Int}(indexin(rids, variables(model))); kwargs...) """ $(TYPEDSIGNATURES) @@ -39,7 +39,7 @@ objective_envelope( kwargs..., ) = objective_envelope( model, - Vector{Int}(indexin(rids, reactions(model))), + Vector{Int}(indexin(rids, variables(model))), args...; kwargs..., ) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index fa5510b89..dc04255c0 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -179,7 +179,7 @@ end $(TYPEDSIGNATURES) A variant for [`flux_variability_analysis`](@ref) that examines actual -reactions (selected by their indexes in `reactions` argument) instead of whole +reactions (selected by their indexes in `variables` argument) instead of whole fluxes. This may be useful for models where the sets of reactions and fluxes differ. """ diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl index 4d888bc5a..55b9ee7bb 100644 --- a/src/analysis/max_min_driving_force.jl +++ b/src/analysis/max_min_driving_force.jl @@ -93,20 +93,20 @@ function max_min_driving_force( haskey(reaction_standard_gibbs_free_energies, rid) && abs(get(flux_solution, rid, small_flux_tol / 2)) > small_flux_tol && !(rid in ignore_reaction_ids), - reactions(model), + variables(model), ) - active_ridxs = Int.(indexin(active_rids, reactions(model))) + active_ridxs = Int.(indexin(active_rids, variables(model))) # give dummy dG0 for reactions that don't have data dg0s = - [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] + [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in variables(model)] S = stoichiometry(model) @constraint(opt_model, dgrs .== dg0s .+ (R * T) * S' * logcs) # thermodynamics should correspond to the fluxes - flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in reactions(model)] + flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in variables(model)] # only constrain reactions that have thermo data @constraint(opt_model, dgrs[active_ridxs] .* flux_signs[active_ridxs] .<= 0) @@ -154,7 +154,7 @@ function max_min_driving_force( return ( mmdf = value(opt_model[:mmdf]), dg_reactions = Dict( - rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(reactions(model)) + rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(variables(model)) ), concentrations = Dict( mid => exp(value(opt_model[:logcs][i])) for diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index ad33a4cd7..178abbfe1 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -2,7 +2,7 @@ $(TYPEDSIGNATURES) Run minimization of metabolic adjustment (MOMA) on `model` with respect to -`flux_ref`, which is a vector of fluxes in the order of `reactions(model)`. +`flux_ref`, which is a vector of fluxes in the order of `variables(model)`. MOMA finds the shortest Euclidian distance between `flux_ref` and `model` with `modifications`: ``` @@ -70,7 +70,7 @@ dictionary of fluxes. """ minimize_metabolic_adjustment(flux_ref_dict::Dict{String,Float64}) = (model, opt_model) -> - minimize_metabolic_adjustment([flux_ref_dict[rid] for rid in reactions(model)])( + minimize_metabolic_adjustment([flux_ref_dict[rid] for rid in variables(model)])( model, opt_model, ) diff --git a/src/analysis/modifications/crowding.jl b/src/analysis/modifications/crowding.jl index 8bad4c23c..4c004a7b1 100644 --- a/src/analysis/modifications/crowding.jl +++ b/src/analysis/modifications/crowding.jl @@ -30,7 +30,7 @@ instead of reaction indices mapped to weights. """ add_crowding_constraints(weights::Dict{String,Float64}) = (model, opt_model) -> begin - idxs = indexin(keys(weights), reactions(model)) + idxs = indexin(keys(weights), variables(model)) nothing in idxs && throw(ArgumentError("Reaction id not found in model.")) add_crowding_constraints(Dict(zip(Int.(idxs), values(weights))))(model, opt_model) end diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 9a2b896e0..7fedee64d 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -19,7 +19,7 @@ Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) """ change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin - ind = first(indexin([id], reactions(model))) + ind = first(indexin([id], variables(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) set_optmodel_bound!(ind, opt_model, lower = lower_bound, upper = upper_bound) end @@ -43,10 +43,10 @@ change_objective( # Construct objective_indices array if typeof(new_objective) == String - objective_indices = indexin([new_objective], reactions(model)) + objective_indices = indexin([new_objective], variables(model)) else objective_indices = - [first(indexin([rxnid], reactions(model))) for rxnid in new_objective] + [first(indexin([rxnid], variables(model))) for rxnid in new_objective] end any(isnothing.(objective_indices)) && throw( diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index 743c33660..36e74e273 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -34,7 +34,7 @@ overloaded so that the knockouts may work differently (more efficiently) with other models. """ function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) - for (rxn_num, rxn_id) in enumerate(reactions(model)) + for (rxn_num, rxn_id) in enumerate(variables(model)) rga = reaction_gene_association(model, rxn_id) if !isnothing(rga) && all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) diff --git a/src/analysis/modifications/loopless.jl b/src/analysis/modifications/loopless.jl index 99e895b1e..8c17e57a4 100644 --- a/src/analysis/modifications/loopless.jl +++ b/src/analysis/modifications/loopless.jl @@ -28,7 +28,7 @@ add_loopless_constraints(; (model, opt_model) -> begin internal_rxn_idxs = [ - ridx for (ridx, rid) in enumerate(reactions(model)) if + ridx for (ridx, rid) in enumerate(variables(model)) if !is_boundary(reaction_stoichiometry(model, rid)) ] diff --git a/src/io/h5.jl b/src/io/h5.jl index f27e28782..b71e05817 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -21,7 +21,7 @@ disk storage, writing the data to disk (using this function) is the only way to make new HDF5 models. """ function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Model - rxns = reactions(model) + rxns = variables(model) rxnp = sortperm(rxns) mets = metabolites(model) metp = sortperm(mets) diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 852328440..e4dba8843 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -304,9 +304,9 @@ end end @_remove_fn reaction MatrixCoupling Int inplace plural begin - orig_rxns = reactions(model.lm) + orig_rxns = variables(model.lm) remove_reactions!(model.lm, reaction_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] + model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] nothing end @@ -317,7 +317,7 @@ end @_remove_fn reaction MatrixCoupling Int plural begin n = copy(model) n.lm = remove_reactions(n.lm, reaction_idxs) - n.C = n.C[:, in.(reactions(model.lm), Ref(Set(reactions(n.lm))))] + n.C = n.C[:, in.(variables(model.lm), Ref(Set(variables(n.lm))))] return n end @@ -326,7 +326,7 @@ end end @_remove_fn reaction MatrixCoupling String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) + remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) end @_remove_fn reaction MatrixCoupling String begin @@ -334,7 +334,7 @@ end end @_remove_fn reaction MatrixCoupling String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) + remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) end @_remove_fn metabolite MatrixCoupling Int inplace begin @@ -342,9 +342,9 @@ end end @_remove_fn metabolite MatrixCoupling Int plural inplace begin - orig_rxns = reactions(model.lm) + orig_rxns = variables(model.lm) model.lm = remove_metabolites(model.lm, metabolite_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(reactions(model.lm))))] + model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] nothing end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 71196c21b..e53fc0fbf 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -284,7 +284,7 @@ end @_change_bounds_fn MatrixModel String inplace plural begin change_bounds!( model, - Vector{Int}(indexin(rxn_ids, reactions(model))), + Vector{Int}(indexin(rxn_ids, variables(model))), lower = lower, upper = upper, ) @@ -297,7 +297,7 @@ end @_change_bounds_fn MatrixModel String plural begin change_bounds( model, - Int.(indexin(rxn_ids, reactions(model))), + Int.(indexin(rxn_ids, variables(model))), lower = lower, upper = upper, ) @@ -332,7 +332,7 @@ end end @_remove_fn reaction MatrixModel String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, reactions(model)))) + remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) end @_remove_fn reaction MatrixModel String begin @@ -340,7 +340,7 @@ end end @_remove_fn reaction MatrixModel String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, reactions(model)))) + remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) end @_remove_fn metabolite MatrixModel Int inplace begin @@ -423,7 +423,7 @@ function change_objective!( rxn_ids::Vector{String}; weights = ones(length(rxn_ids)), ) - idxs = indexin(rxn_ids, reactions(model)) + idxs = indexin(rxn_ids, variables(model)) any(isnothing(idx) for idx in idxs) && throw(DomainError(rxn_ids, "Some reaction ids not found in the model")) change_objective!(model, Int.(idxs); weights) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index ecf6df0d4..8ac234bb4 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -129,7 +129,7 @@ end end @_remove_fn reaction ObjectModel String inplace begin - if !(reaction_id in reactions(model)) + if !(reaction_id in variables(model)) @models_log @info "Reaction $reaction_id not found in model." else delete!(model.reactions, reaction_id) diff --git a/src/reconstruction/gecko.jl b/src/reconstruction/gecko.jl index ab1dada8c..fd424ad1b 100644 --- a/src/reconstruction/gecko.jl +++ b/src/reconstruction/gecko.jl @@ -54,7 +54,7 @@ function make_gecko_model( gids = genes(model) (lbs, ubs) = bounds(model) - rids = reactions(model) + rids = variables(model) gene_name_lookup = Dict(gids .=> 1:length(gids)) gene_row_lookup = Dict{Int,Int}() diff --git a/src/reconstruction/smoment.jl b/src/reconstruction/smoment.jl index 5f49b79d7..ceccd1e9c 100644 --- a/src/reconstruction/smoment.jl +++ b/src/reconstruction/smoment.jl @@ -35,7 +35,7 @@ function make_smoment_model( columns = Vector{Types._SMomentColumn}() (lbs, ubs) = bounds(model) - rids = reactions(model) + rids = variables(model) for i = 1:n_variables(model) isozyme = ris_(rids[i]) diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index 5e627b443..53f26f6cf 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -7,7 +7,7 @@ functions. If you want your model type to work with COBREXA, add the `AbstractMetabolicModel` as its supertype, and implement the accessor functions. Accessors -[`reactions`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), +[`variables`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), [`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not mandatory and default to safe "empty" values. """ diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 070709043..a67cf9967 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -19,8 +19,8 @@ modeling, such as metabolite exchanges, separate forward and reverse reactions, supplies of enzymatic and genetic material and virtual cell volume, etc. To simplify the view of the model contents use [`reaction_flux`](@ref). """ -function reactions(a::AbstractMetabolicModel)::Vector{String} - missing_impl_error(reactions, (a,)) +function variables(a::AbstractMetabolicModel)::Vector{String} + missing_impl_error(variables, (a,)) end """ @@ -29,7 +29,7 @@ $(TYPEDSIGNATURES) Return a vector of metabolite identifiers in a model. The vector precisely corresponds to the rows in [`stoichiometry`](@ref) matrix. -As with [`reactions`](@ref)s, some metabolites in models may be virtual, +As with [`variables`](@ref)s, some metabolites in models may be virtual, representing purely technical equality constraints. """ function metabolites(a::AbstractMetabolicModel)::Vector{String} @@ -42,7 +42,7 @@ $(TYPEDSIGNATURES) Get the number of reactions in a model. """ function n_variables(a::AbstractMetabolicModel)::Int - length(reactions(a)) + length(variables(a)) end """ @@ -101,7 +101,7 @@ end """ $(TYPEDSIGNATURES) -In some models, the [`reactions`](@ref) that correspond to the columns of +In some models, the [`variables`](@ref) that correspond to the columns of [`stoichiometry`](@ref) matrix do not fully represent the semantic contents of the model; for example, fluxes may be split into forward and reverse reactions, reactions catalyzed by distinct enzymes, etc. Together with @@ -111,7 +111,7 @@ flux is decomposed into individual reactions. By default (and in most models), fluxes and reactions perfectly correspond. """ function fluxes(a::AbstractMetabolicModel)::Vector{String} - reactions(a) + variables(a) end function n_fluxes(a::AbstractMetabolicModel)::Int @@ -228,7 +228,7 @@ function reaction_stoichiometry( mets = metabolites(m) Dict( mets[k] => v for - (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], reactions(m)))])...) + (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], variables(m)))])...) ) end diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index e059b0efc..ea6358e2a 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -14,7 +14,7 @@ end # The list of inherited functions must be synced with the methods available for [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () reactions metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/BalancedGrowthCommunityModel.jl index 0adfd4d7f..f6e880dc5 100644 --- a/src/types/misc/BalancedGrowthCommunityModel.jl +++ b/src/types/misc/BalancedGrowthCommunityModel.jl @@ -27,7 +27,7 @@ function env_ex_matrix(m::CommunityMember, env_mets) !(env_met in metabolites(m.model)) && continue rex = first(indexin([env_met], get_exchange_mets(m))) isnothing(rex) && continue - ex_ridx = first(indexin([m.exchange_reaction_ids[rex]], reactions(m.model))) + ex_ridx = first(indexin([m.exchange_reaction_ids[rex]], variables(m.model))) mat[env_met_idx, ex_ridx] = 1.0 end return mat diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index b062d2d01..c5bf01be3 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -53,16 +53,16 @@ Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) $(TYPEDSIGNATURES) Return the lower bounds for all reactions in `model`. -Order matches that of the reaction IDs returned by [`reactions`](@ref). +Order matches that of the reaction IDs returned by [`variables`](@ref). """ lower_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].lower_bound for rxn in reactions(model)] + [model.reactions[rxn].lower_bound for rxn in variables(model)] """ $(TYPEDSIGNATURES) Return the upper bounds for all reactions in `model`. -Order matches that of the reaction IDs returned in [`reactions`](@ref). +Order matches that of the reaction IDs returned in [`variables`](@ref). """ upper_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].upper_bound for rxn in reactions(model)] + [model.reactions[rxn].upper_bound for rxn in variables(model)] diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 75a73ff23..942792456 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -63,8 +63,8 @@ Consequently, exchange reactions of the original model will look like `species1#EX_...`. All exchange environmental reactions have `EX_` as a prefix followed by the environmental metabolite id. """ -function Accessors.reactions(cm::BalancedGrowthCommunityModel) - rxns = [add_community_prefix(m, rid) for m in cm.members for rid in reactions(m.model)] +function Accessors.variables(cm::BalancedGrowthCommunityModel) + rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] return [rxns; env_exs; cm.objective_id] end diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index a6b244c05..ab217176f 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -38,7 +38,7 @@ function Accessors.n_variables(model::HDF5Model)::Int length(model.h5["reactions"]) end -function Accessors.reactions(model::HDF5Model)::Vector{String} +function Accessors.variables(model::HDF5Model)::Vector{String} precache!(model) # TODO is there any reasonable method to mmap strings from HDF5? read(model.h5["reactions"]) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index c9f724665..917fde4e3 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -18,7 +18,7 @@ changes in `json` invalidate the cache. ```` model = load_json_model("some_model.json") model.json # see the actual underlying JSON -reactions(model) # see the list of reactions +variables(model) # see the list of reactions ```` # Fields @@ -80,7 +80,7 @@ $(TYPEDSIGNATURES) Extract reaction names (stored as `.id`) from JSON model. """ -Accessors.reactions(model::JSONModel) = +Accessors.variables(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] """ @@ -106,7 +106,7 @@ Get the stoichiometry. Assuming the information is stored in reaction object under key `.metabolites`. """ function Accessors.stoichiometry(model::JSONModel) - rxn_ids = reactions(model) + rxn_ids = variables(model) met_ids = metabolites(model) n_entries = 0 @@ -298,7 +298,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) return mm end - rxn_ids = reactions(mm) + rxn_ids = variables(mm) met_ids = metabolites(mm) gene_ids = genes(mm) S = stoichiometry(mm) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index c6b2cd6c0..0bcdb4dd6 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -18,7 +18,7 @@ $(TYPEDSIGNATURES) Extracts reaction names from `rxns` key in the MAT file. """ -function Accessors.reactions(m::MATModel)::Vector{String} +function Accessors.variables(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") reshape(m.mat["rxns"], n_variables(m)) else @@ -133,7 +133,7 @@ Extracts the associations from `grRules` key, if present. """ function Accessors.reaction_gene_association(m::MATModel, rid::String) if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), reactions(m))] + grr = m.mat["grRules"][findfirst(==(rid), variables(m))] typeof(grr) == String ? parse_grr(grr) : nothing else nothing @@ -199,7 +199,7 @@ $(TYPEDSIGNATURES) Extract reaction name from `rxnNames`. """ Accessors.reaction_name(m::MATModel, rid::String) = maybemap( - x -> x[findfirst(==(rid), reactions(m))], + x -> x[findfirst(==(rid), variables(m))], gets(m.mat, nothing, constants.keynames.rxnnames), ) @@ -237,7 +237,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) return MATModel( Dict( "S" => stoichiometry(m), - "rxns" => reactions(m), + "rxns" => variables(m), "mets" => metabolites(m), "lb" => Vector(lb), "ub" => Vector(ub), @@ -252,7 +252,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) "", maybemap.( x -> unparse_grr(String, x), - reaction_gene_association.(Ref(m), reactions(m)), + reaction_gene_association.(Ref(m), variables(m)), ), ), "metFormulas" => diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index b9ace4987..20387de3f 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -52,7 +52,7 @@ $(TYPEDSIGNATURES) Get the reactions in a `MatrixModel`. """ -Accessors.reactions(a::MatrixModel)::Vector{String} = a.rxns +Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns """ $(TYPEDSIGNATURES) @@ -163,10 +163,10 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode objective(m), xl, xu, - reactions(m), + variables(m), metabolites(m), Vector{Maybe{GeneAssociationsDNF}}([ - reaction_gene_association(m, id) for id in reactions(m) + reaction_gene_association(m, id) for id in variables(m) ]), ) end diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 5a8fb03e2..46735220c 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -57,7 +57,7 @@ Return a vector of reaction id strings contained in `model`. The order of reaction ids returned here matches the order used to construct the stoichiometric matrix. """ -Accessors.reactions(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) +Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) """ $(TYPEDSIGNATURES) @@ -117,7 +117,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat sizehint!(SV, n_entries) # establish the ordering - rxns = reactions(model) + rxns = variables(model) met_idx = Dict(mid => i for (i, mid) in enumerate(metabolites(model))) # fill the matrix entries @@ -141,7 +141,7 @@ end $(TYPEDSIGNATURES) Return the lower and upper bounds, respectively, for reactions in `model`. -Order matches that of the reaction ids returned in `reactions()`. +Order matches that of the reaction ids returned in `variables()`. """ Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) @@ -313,7 +313,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) gids = genes(model) metids = metabolites(model) - rxnids = reactions(model) + rxnids = variables(model) for gid in gids modelgenes[gid] = Gene( @@ -339,7 +339,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) S = stoichiometry(model) lbs, ubs = bounds(model) obj_idxs, obj_vals = findnz(objective(model)) - modelobjective = Dict(k => v for (k, v) in zip(reactions(model)[obj_idxs], obj_vals)) + modelobjective = Dict(k => v for (k, v) in zip(variables(model)[obj_idxs], obj_vals)) for (i, rid) in enumerate(rxnids) rmets = Dict{String,Float64}() for (j, stoich) in zip(findnz(S[:, i])...) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 8426ed257..442643593 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -16,7 +16,7 @@ $(TYPEDSIGNATURES) Get reactions from a [`SBMLModel`](@ref). """ -Accessors.reactions(model::SBMLModel)::Vector{String} = +Accessors.variables(model::SBMLModel)::Vector{String} = [k for k in keys(model.sbml.reactions)] """ @@ -287,7 +287,7 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) end mets = metabolites(mm) - rxns = reactions(mm) + rxns = variables(mm) stoi = stoichiometry(mm) (lbs, ubs) = bounds(mm) comps = default.("compartment", metabolite_compartment.(Ref(mm), mets)) diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index c31f72a66..9f8b0e217 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -66,7 +66,7 @@ stoichiometry, not in [`coupling`](@ref)), and `inner`, which is the original wrapped model. The `objective` of the model includes also the extra columns for individual genes, as held by `coupling_row_gene_product`. -Implementation exposes the split reactions (available as `reactions(model)`), +Implementation exposes the split reactions (available as `variables(model)`), but retains the original "simple" reactions accessible by [`fluxes`](@ref). The related constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). @@ -106,7 +106,7 @@ end $(TYPEDSIGNATURES) Return the objective of the [`GeckoModel`](@ref). Note, the objective is with -respect to the internal variables, i.e. [`reactions(model)`](@ref), which are +respect to the internal variables, i.e. [`variables(model)`](@ref), which are the unidirectional reactions and the genes involved in enzymatic reactions that have kinetic data. """ @@ -119,8 +119,8 @@ Returns the internal reactions in a [`GeckoModel`](@ref) (these may be split to forward- and reverse-only parts with different isozyme indexes; reactions IDs are mangled accordingly with suffixes). """ -function Accessors.reactions(model::GeckoModel) - inner_reactions = reactions(model.inner) +function Accessors.variables(model::GeckoModel) + inner_reactions = variables(model.inner) mangled_reactions = [ gecko_reaction_name( inner_reactions[col.reaction_idx], diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 77ad6b05b..40f3902b9 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -47,7 +47,7 @@ columns to the stoichiometry and data of the internal wrapped model, and as specified in sMOMENT algorithm. This implementation allows easy access to fluxes from the split reactions -(available in `reactions(model)`), while the original "simple" reactions from +(available in `variables(model)`), while the original "simple" reactions from the wrapped model are retained as [`fluxes`](@ref). All additional constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). @@ -87,8 +87,8 @@ Returns the internal reactions in a [`SMomentModel`](@ref) (these may be split to forward- and reverse-only parts; reactions IDs are mangled accordingly with suffixes). """ -Accessors.reactions(model::SMomentModel) = - let inner_reactions = reactions(model.inner) +Accessors.variables(model::SMomentModel) = + let inner_reactions = variables(model.inner) [ smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for col in model.columns diff --git a/src/utils/fluxes.jl b/src/utils/fluxes.jl index fd4651196..1c840a9aa 100644 --- a/src/utils/fluxes.jl +++ b/src/utils/fluxes.jl @@ -6,7 +6,7 @@ produce them, given the flux distribution supplied in `flux_dict`. """ function metabolite_fluxes(model::AbstractMetabolicModel, flux_dict::Dict{String,Float64}) S = stoichiometry(model) - rids = reactions(model) + rids = variables(model) mids = metabolites(model) producing = Dict{String,Dict{String,Float64}}() @@ -52,7 +52,7 @@ atom_fluxes(model, fluxes)["C"] ``` """ function atom_fluxes(model::AbstractMetabolicModel, reaction_fluxes::Dict{String,Float64}) - rids = reactions(model) + rids = variables(model) atom_flux = Dict{String,Float64}() for (ridx, rid) in enumerate(rids) haskey(reaction_fluxes, rid) || continue diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index fbfe8abf5..f6402f346 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -17,13 +17,13 @@ alternative. # Example ``` -findall(looks_like_exchange_reaction, reactions(model)) # returns indices -filter(looks_like_exchange_reaction, reactions(model)) # returns Strings +findall(looks_like_exchange_reaction, variables(model)) # returns indices +filter(looks_like_exchange_reaction, variables(model)) # returns Strings # to use the optional arguments you need to expand the function's arguments # using an anonymous function -findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(model)) # returns indices -filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), reactions(model)) # returns Strings +findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns indices +filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns Strings ``` """ function looks_like_exchange_reaction( @@ -43,7 +43,7 @@ Shortcut for finding exchange reaction indexes in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ find_exchange_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_exchange_reaction(id; kwargs...), reactions(m)) + findall(id -> looks_like_exchange_reaction(id; kwargs...), variables(m)) """ $(TYPEDSIGNATURES) @@ -52,7 +52,7 @@ Shortcut for finding exchange reaction identifiers in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ find_exchange_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_exchange_reaction(id, kwargs...), reactions(m)) + filter(id -> looks_like_exchange_reaction(id, kwargs...), variables(m)) """ $(TYPEDSIGNATURES) @@ -70,8 +70,8 @@ alternative. # Example ``` -filter(looks_like_biomass_reaction, reactions(model)) # returns strings -findall(looks_like_biomass_reaction, reactions(model)) # returns indices +filter(looks_like_biomass_reaction, variables(model)) # returns strings +findall(looks_like_biomass_reaction, variables(model)) # returns indices ``` """ function looks_like_biomass_reaction( @@ -91,7 +91,7 @@ Shortcut for finding biomass reaction indexes in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ find_biomass_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) + findall(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) """ $(TYPEDSIGNATURES) @@ -100,7 +100,7 @@ Shortcut for finding biomass reaction identifiers in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ find_biomass_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_biomass_reaction(id; kwargs...), reactions(m)) + filter(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) """ $(TYPEDSIGNATURES) diff --git a/test/analysis/envelopes.jl b/test/analysis/envelopes.jl index eb34b2e1d..cc78b4af5 100644 --- a/test/analysis/envelopes.jl +++ b/test/analysis/envelopes.jl @@ -6,12 +6,12 @@ lat = collect.(envelope_lattice(m, rxns; samples = 3)) @test lat == [[0, 500, 1000], [-1000, 0, 1000], [-1000, 0, 1000]] - @test lat == collect.(envelope_lattice(m, reactions(m)[rxns]; samples = 3)) + @test lat == collect.(envelope_lattice(m, variables(m)[rxns]; samples = 3)) vals = objective_envelope( m, - reactions(m)[rxns], + variables(m)[rxns], Tulip.Optimizer; lattice_args = (samples = 3, ranges = [(-5, 5), (-5, 5), (-5, 5)]), workers = W, diff --git a/test/analysis/fba_with_crowding.jl b/test/analysis/fba_with_crowding.jl index 3650852ed..118b6adf6 100644 --- a/test/analysis/fba_with_crowding.jl +++ b/test/analysis/fba_with_crowding.jl @@ -1,7 +1,7 @@ @testset "FBA with crowding constraints" begin model = load_model(model_paths["e_coli_core.json"]) rid_weight = Dict( - rid => 0.004 for rid in reactions(model) if + rid => 0.004 for rid in variables(model) if !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) ) diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index bc087584b..4ccc211dc 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -26,7 +26,7 @@ fluxes_vec = flux_balance_analysis_vec(cp, Tulip.Optimizer) @test all(fluxes_vec .== sol) fluxes_dict = flux_balance_analysis_dict(cp, Tulip.Optimizer) - rxns = reactions(cp) + rxns = variables(cp) @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) end @@ -97,7 +97,7 @@ end # construct coupling bounds nr = n_variables(model) - biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], reactions(model))) + biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], variables(model))) Cf = sparse(1.0I, nr, nr) Cf[:, biomass_index] .= -γ diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 5e6c7e065..9ecbf3800 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -50,7 +50,7 @@ modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - pyk_idx = first(indexin(["PYK"], reactions(model))) + pyk_idx = first(indexin(["PYK"], variables(model))) @test isapprox( sols[pyk_idx, 1].dg_reactions["PYK"], -1.5895040002691128; diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl index dbb115992..1e7e807c1 100644 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ b/test/analysis/minimize_metabolic_adjustment.jl @@ -1,7 +1,7 @@ @testset "MOMA" begin model = test_toyModel() - sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in reactions(model)] + sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in variables(model)] moma = minimize_metabolic_adjustment_analysis_dict( model, diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl index a98bd48ae..e31ec13a0 100644 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ b/test/analysis/sampling/affine_hit_and_run.jl @@ -4,7 +4,7 @@ cm = MatrixCoupling(model, zeros(1, n_variables(model)), [17.0], [19.0]) - pfk, tala = indexin(["PFK", "TALA"], reactions(cm)) + pfk, tala = indexin(["PFK", "TALA"], variables(cm)) cm.C[:, [pfk, tala]] .= 1.0 warmup = warmup_from_variability(cm, Tulip.Optimizer; workers = W) diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl index 507eda1d1..6393b9abf 100644 --- a/test/analysis/sampling/warmup_variability.jl +++ b/test/analysis/sampling/warmup_variability.jl @@ -9,7 +9,7 @@ workers = W, ) - idx = first(indexin([rid], reactions(model))) + idx = first(indexin([rid], variables(model))) @test size(pts) == (95, 100) @test all(pts[idx, :] .>= -2) @test all(pts[idx, :] .<= 2) diff --git a/test/io/h5.jl b/test/io/h5.jl index 4a9d214be..92d0a6479 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -14,13 +14,13 @@ # briefly test that the loading is okay @test n_variables(model) == n_variables(h5) @test n_metabolites(model) == n_metabolites(h5) - @test issetequal(reactions(model), reactions(h5)) + @test issetequal(variables(model), variables(h5)) @test issetequal(metabolites(model), metabolites(h5)) @test issorted(metabolites(h5)) - @test issorted(reactions(h5)) + @test issorted(variables(h5)) @test size(stoichiometry(model)) == size(stoichiometry(h5)) @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) - rxnp = sortperm(reactions(model)) + rxnp = sortperm(variables(model)) @test bounds(model)[1][rxnp] == bounds(h5)[1] @test bounds(model)[2][rxnp] == bounds(h5)[2] @test objective(model)[rxnp] == objective(h5) diff --git a/test/io/io.jl b/test/io/io.jl index ad1ee32af..eb565e51c 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -12,10 +12,10 @@ @test jsonmodel isa JSONModel @test n_variables(jsonmodel) == 2583 - @test Set(lowercase.(reactions(sbmlmodel))) == - Set("r_" .* lowercase.(reactions(matlabmodel))) - @test Set(lowercase.(reactions(sbmlmodel))) == - Set("r_" .* lowercase.(reactions(jsonmodel))) + @test Set(lowercase.(variables(sbmlmodel))) == + Set("r_" .* lowercase.(variables(matlabmodel))) + @test Set(lowercase.(variables(sbmlmodel))) == + Set("r_" .* lowercase.(variables(jsonmodel))) # specifically test parsing of gene-reaction associations in Recon reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) diff --git a/test/io/json.jl b/test/io/json.jl index 5c297a5cc..90f3cad5c 100644 --- a/test/io/json.jl +++ b/test/io/json.jl @@ -4,7 +4,7 @@ stdmodel = convert(ObjectModel, jsonmodel) # test if same reaction ids - @test issetequal(reactions(jsonmodel), reactions(stdmodel)) + @test issetequal(variables(jsonmodel), variables(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) # not the best tests since it is possible that error could cancel each other out: diff --git a/test/io/sbml.jl b/test/io/sbml.jl index eef9b1c28..c342a6a04 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -11,7 +11,7 @@ @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) @test metabolites(m)[1:3] == ["M_succoa_c", "M_ac_c", "M_fru_b"] - @test reactions(m)[1:3] == ["R_EX_fum_e", "R_ACONTb", "R_GLNS"] + @test variables(m)[1:3] == ["R_EX_fum_e", "R_ACONTb", "R_GLNS"] cm = convert(MatrixModelWithCoupling, sbmlm) @test n_coupling_constraints(cm) == 0 diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index 49cbbc999..51ec05fa6 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -208,7 +208,7 @@ end @test objective(modLp) == objective(lp)[2:3] @test bounds(modLp)[1] == bounds(lp)[1][2:3] @test bounds(modLp)[2] == bounds(lp)[2][2:3] - @test reactions(modLp) == reactions(lp)[2:3] + @test variables(modLp) == variables(lp)[2:3] @test metabolites(modLp) == metabolites(lp) end diff --git a/test/reconstruction/SerializedModel.jl b/test/reconstruction/SerializedModel.jl index d3ce0a17c..21d41e1ff 100644 --- a/test/reconstruction/SerializedModel.jl +++ b/test/reconstruction/SerializedModel.jl @@ -8,8 +8,8 @@ @test typeof(m2) == typeof(m) sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = remove_reaction(sm, reactions(m)[3]) + m2 = remove_reaction(sm, variables(m)[3]) @test typeof(m2) == typeof(m) - @test !(reactions(m)[3] in reactions(m2)) + @test !(variables(m)[3] in variables(m2)) end diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 1c5a80c75..a44d9c7c9 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -69,7 +69,7 @@ @test contains(sprint(show, MIME("text/plain"), cm), "balanced growth") @test issetequal( - reactions(cm), + variables(cm), [ "m1#EX_A" "m1#r1" diff --git a/test/types/JSONModel.jl b/test/types/JSONModel.jl index 3cdf11fe8..97b8ff05e 100644 --- a/test/types/JSONModel.jl +++ b/test/types/JSONModel.jl @@ -5,8 +5,8 @@ sm = convert(ObjectModel, jm) jm2 = convert(JSONModel, sm) - @test Set(reactions(jm)) == Set(reactions(sm)) - @test Set(reactions(jm)) == Set(reactions(jm2)) + @test Set(variables(jm)) == Set(variables(sm)) + @test Set(variables(jm)) == Set(variables(jm2)) end @testset "JSONModel generic interface" begin diff --git a/test/types/MATModel.jl b/test/types/MATModel.jl index e575e8a0a..4bd536a7c 100644 --- a/test/types/MATModel.jl +++ b/test/types/MATModel.jl @@ -6,8 +6,8 @@ sm = convert(ObjectModel, mm) mm2 = convert(MATModel, sm) - @test Set(reactions(mm)) == Set(reactions(sm)) - @test Set(reactions(mm)) == Set(reactions(mm2)) + @test Set(variables(mm)) == Set(variables(sm)) + @test Set(variables(mm)) == Set(variables(mm2)) end @testset "MATModel generic interface" begin diff --git a/test/types/MatrixCoupling.jl b/test/types/MatrixCoupling.jl index 2c2ad3de5..99fc355f2 100644 --- a/test/types/MatrixCoupling.jl +++ b/test/types/MatrixCoupling.jl @@ -12,9 +12,9 @@ end sm = convert(ObjectModel, cm) cm2 = convert(MatrixModelWithCoupling, sm) - @test Set(reactions(cm)) == Set(reactions(sm)) - @test Set(reactions(cm)) == Set(reactions(cm2)) + @test Set(variables(cm)) == Set(variables(sm)) + @test Set(variables(cm)) == Set(variables(cm2)) - @test reaction_gene_association(sm, reactions(sm)[1]) == - reaction_gene_association(cm, reactions(sm)[1]) + @test reaction_gene_association(sm, variables(sm)[1]) == + reaction_gene_association(cm, variables(sm)[1]) end diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index 1be562143..a9c4f220c 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -11,9 +11,9 @@ end sm = convert(ObjectModel, cm) cm2 = convert(MatrixModel, sm) - @test Set(reactions(cm)) == Set(reactions(sm)) - @test Set(reactions(cm)) == Set(reactions(cm2)) + @test Set(variables(cm)) == Set(variables(sm)) + @test Set(variables(cm)) == Set(variables(cm2)) - @test reaction_gene_association(sm, reactions(sm)[1]) == - reaction_gene_association(cm, reactions(sm)[1]) + @test reaction_gene_association(sm, variables(sm)[1]) == + reaction_gene_association(cm, variables(sm)[1]) end diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 3331a855a..612d4885e 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -46,7 +46,7 @@ @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") - @test "r1" in reactions(model) + @test "r1" in variables(model) @test "m4" in metabolites(model) @test "g2" in genes(model) @test n_variables(model) == 4 @@ -132,7 +132,7 @@ jsonmodel = convert(JSONModel, model) stdmodel = convert(ObjectModel, jsonmodel) - @test issetequal(reactions(jsonmodel), reactions(stdmodel)) + @test issetequal(variables(jsonmodel), variables(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) jlbs, jubs = bounds(jsonmodel) @@ -141,8 +141,8 @@ @test issetequal(jubs, subs) jS = stoichiometry(jsonmodel) sS = stoichiometry(stdmodel) - j_r1_index = findfirst(x -> x == "r1", reactions(jsonmodel)) - s_r1_index = findfirst(x -> x == "r1", reactions(stdmodel)) + j_r1_index = findfirst(x -> x == "r1", variables(jsonmodel)) + s_r1_index = findfirst(x -> x == "r1", variables(stdmodel)) j_m1_index = findfirst(x -> x == "m1", metabolites(jsonmodel)) j_m2_index = findfirst(x -> x == "m2", metabolites(jsonmodel)) s_m1_index = findfirst(x -> x == "m1", metabolites(stdmodel)) diff --git a/test/types/SBMLModel.jl b/test/types/SBMLModel.jl index 3753f484b..1cb52cb59 100644 --- a/test/types/SBMLModel.jl +++ b/test/types/SBMLModel.jl @@ -4,8 +4,8 @@ sm = convert(ObjectModel, sbmlm) sbmlm2 = convert(SBMLModel, sm) - @test Set(reactions(sbmlm)) == Set(reactions(sbmlm2)) - @test Set(reactions(sbmlm)) == Set(reactions(sm)) + @test Set(variables(sbmlm)) == Set(variables(sbmlm2)) + @test Set(variables(sbmlm)) == Set(variables(sm)) @test Set(metabolites(sbmlm)) == Set(metabolites(sbmlm2)) sp(x) = x.species @test all([ @@ -15,7 +15,7 @@ ) && issetequal( sp.(sbmlm.sbml.reactions[i].products), sp.(sbmlm2.sbml.reactions[i].products), - ) for i in reactions(sbmlm2) + ) for i in variables(sbmlm2) ]) st(x) = isnothing(x.stoichiometry) ? 1.0 : x.stoichiometry @test all([ @@ -25,7 +25,7 @@ ) && issetequal( st.(sbmlm.sbml.reactions[i].products), st.(sbmlm2.sbml.reactions[i].products), - ) for i in reactions(sbmlm2) + ) for i in variables(sbmlm2) ]) end diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index 29abf7cba..714e9494d 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -4,9 +4,9 @@ struct FakeModel <: AbstractMetabolicModel end @testset "Base abstract model methods require proper minimal implementation" begin - @test_throws MethodError reactions(123) + @test_throws MethodError variables(123) x = FakeModel(123) - for m in [reactions, metabolites, stoichiometry, bounds, objective] + for m in [variables, metabolites, stoichiometry, bounds, objective] @test_throws MethodError m(x) end end diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl index 5ce5dd2a0..045dbaae4 100644 --- a/test/utils/ObjectModel.jl +++ b/test/utils/ObjectModel.jl @@ -12,9 +12,9 @@ cbm = make_optimization_model(model, Tulip.Optimizer) ubs = cbm[:ubs] lbs = cbm[:lbs] - glucose_index = first(indexin(["EX_glc__D_e"], reactions(model))) - o2_index = first(indexin(["EX_o2_e"], reactions(model))) - atpm_index = first(indexin(["ATPM"], reactions(model))) + glucose_index = first(indexin(["EX_glc__D_e"], variables(model))) + o2_index = first(indexin(["EX_o2_e"], variables(model))) + atpm_index = first(indexin(["ATPM"], variables(model))) set_optmodel_bound!(glucose_index, cbm; upper = -1.0, lower = -1.0) @test normalized_rhs(ubs[glucose_index]) == -1.0 @test normalized_rhs(lbs[glucose_index]) == 1.0 diff --git a/test/utils/Serialized.jl b/test/utils/Serialized.jl index 646c29a18..32d325362 100644 --- a/test/utils/Serialized.jl +++ b/test/utils/Serialized.jl @@ -14,8 +14,8 @@ @test sm2.m == nothing # nothing is cached here @test isequal(m, deserialize(tmpfile("toy2.serialized"))) # it was written as-is @test issetequal( - reactions(convert(ObjectModel, sm)), - reactions(convert(ObjectModel, sm2)), + variables(convert(ObjectModel, sm)), + variables(convert(ObjectModel, sm2)), ) sm.m = nothing @test issetequal( diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index 9c813ef6b..e3aa8f8f8 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -1,9 +1,9 @@ @testset "Looks like in MatrixModel, detailed test" begin cp = test_LP() - @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) + @test isempty(filter(looks_like_exchange_reaction, variables(cp))) cp = test_simpleLP() - @test isempty(filter(looks_like_exchange_reaction, reactions(cp))) + @test isempty(filter(looks_like_exchange_reaction, variables(cp))) cp = MatrixModel( [-1.0 -1 -2; 0 -1 0; 0 0 0], @@ -14,7 +14,7 @@ ["EX_m1"; "r2"; "r3"], ["m1"; "m2"; "m3"], ) - @test filter(looks_like_exchange_reaction, reactions(cp)) == ["EX_m1"] + @test filter(looks_like_exchange_reaction, variables(cp)) == ["EX_m1"] cp = MatrixModel( [-1.0 0 0; 0 0 -1; 0 -1 0], @@ -25,55 +25,55 @@ ["EX_m1"; "Exch_m3"; "Ex_m2"], ["m1"; "m2"; "m3_e"], ) - @test filter(looks_like_exchange_reaction, reactions(cp)) == + @test filter(looks_like_exchange_reaction, variables(cp)) == ["EX_m1", "Exch_m3", "Ex_m2"] @test filter( x -> looks_like_exchange_reaction(x; exchange_prefixes = ["Exch_"]), - reactions(cp), + variables(cp), ) == ["Exch_m3"] # this is originally the "toyModel1.mat" cp = test_toyModel() - @test filter(looks_like_exchange_reaction, reactions(cp)) == + @test filter(looks_like_exchange_reaction, variables(cp)) == ["EX_m1(e)", "EX_m3(e)", "EX_biomass(e)"] @test filter( x -> looks_like_exchange_reaction(x; exclude_biomass = true), - reactions(cp), + variables(cp), ) == ["EX_m1(e)", "EX_m3(e)"] @test filter(looks_like_extracellular_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"] - @test filter(looks_like_biomass_reaction, reactions(cp)) == + @test filter(looks_like_biomass_reaction, variables(cp)) == ["EX_biomass(e)", "biomass1"] @test filter( x -> looks_like_biomass_reaction(x; exclude_exchanges = true), - reactions(cp), + variables(cp), ) == ["biomass1"] end @testset "Looks like functions, basic" begin model = load_model(model_paths["e_coli_core.json"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = load_model(model_paths["e_coli_core.xml"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = load_model(model_paths["e_coli_core.mat"]) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = convert(ObjectModel, model) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = convert(MatrixModelWithCoupling, model) - @test length(filter(looks_like_exchange_reaction, reactions(model))) == 20 - @test length(filter(looks_like_biomass_reaction, reactions(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 end From 8cf63f700e0fec768343cc3586e0b276820ea047 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 15 Dec 2022 14:15:11 +0100 Subject: [PATCH 063/531] fluxes -> reactions --- docs/src/examples/14_smoment.jl | 4 ++-- docs/src/examples/15_gecko.jl | 2 +- src/analysis/flux_variability_analysis.jl | 10 +++++----- src/analysis/sampling/affine_hit_and_run.jl | 2 +- src/solver.jl | 4 ++-- src/types/accessors/AbstractMetabolicModel.jl | 18 +++++++++--------- src/types/accessors/ModelWrapper.jl | 2 +- .../models/BalancedGrowthCommunityModel.jl | 14 +++++++------- src/types/wrappers/GeckoModel.jl | 6 +++--- src/types/wrappers/SMomentModel.jl | 6 +++--- 10 files changed, 34 insertions(+), 34 deletions(-) diff --git a/docs/src/examples/14_smoment.jl b/docs/src/examples/14_smoment.jl index 463fd0af5..d0e943151 100644 --- a/docs/src/examples/14_smoment.jl +++ b/docs/src/examples/14_smoment.jl @@ -88,7 +88,7 @@ flux_balance_analysis_dict(smoment_model, GLPK.Optimizer) # (Notice that the total reaction fluxes are reported despite the fact that # reactions are indeed split in the model! The underlying mechanism is provided -# by [`reaction_flux`](@ref) accessor.) +# by [`reaction_variables`](@ref) accessor.) # [Variability](06_fva.md) of the sMOMENT model can be explored as such: @@ -101,5 +101,5 @@ flux_variability_analysis(smoment_model, GLPK.Optimizer, bounds = gamma_bounds(0 affine_hit_and_run( smoment_model, warmup_from_variability(smoment_model, GLPK.Optimizer), - )' * reaction_flux(smoment_model) + )' * reaction_variables(smoment_model) ) diff --git a/docs/src/examples/15_gecko.jl b/docs/src/examples/15_gecko.jl index 02d5a963c..3be74ffab 100644 --- a/docs/src/examples/15_gecko.jl +++ b/docs/src/examples/15_gecko.jl @@ -97,4 +97,4 @@ flux_variability_analysis(gecko_model, GLPK.Optimizer, bounds = gamma_bounds(0.9 # ...and sampling: affine_hit_and_run(gecko_model, warmup_from_variability(gecko_model, GLPK.Optimizer))' * -reaction_flux(gecko_model) +reaction_variables(gecko_model) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index dc04255c0..192698f43 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -2,7 +2,7 @@ $(TYPEDSIGNATURES) Flux variability analysis solves a pair of optimization problems in `model` for -each flux `f` described in `fluxes`: +each flux `f` described in `reactions`: ``` min,max fᵀxᵢ s.t. S x = b @@ -109,13 +109,13 @@ function flux_variability_analysis( optimizer; kwargs..., ) - if any((flux_indexes .< 1) .| (flux_indexes .> n_fluxes(model))) + if any((flux_indexes .< 1) .| (flux_indexes .> n_reactions(model))) throw(DomainError(flux_indexes, "Flux index out of range")) end flux_variability_analysis( model, - reaction_flux(model)[:, flux_indexes], + reaction_variables(model)[:, flux_indexes], optimizer; kwargs..., ) @@ -128,7 +128,7 @@ A simpler version of [`flux_variability_analysis`](@ref) that maximizes and minimizes all declared fluxes in the model. Arguments are forwarded. """ flux_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = - flux_variability_analysis(model, reaction_flux(model), optimizer; kwargs...) + flux_variability_analysis(model, reaction_variables(model), optimizer; kwargs...) """ $(TYPEDSIGNATURES) @@ -157,7 +157,7 @@ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer kwargs..., ret = sol -> flux_vector(model, sol), ) - flxs = fluxes(model) + flxs = reactions(model) dicts = zip.(Ref(flxs), vs) return (Dict(flxs .=> Dict.(dicts[:, 1])), Dict(flxs .=> Dict.(dicts[:, 2]))) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index c0f00fb4d..cfaec932a 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -27,7 +27,7 @@ warmup_points = warmup_from_variability(model, GLPK.Optimizer) samples = affine_hit_and_run(model, warmup_points, sample_iters = 101:105) # convert the result to flux (for models where the distinction matters): -fluxes = reaction_flux(model)' * samples +fluxes = reaction_variables(model)' * samples ``` """ function affine_hit_and_run( diff --git a/src/solver.jl b/src/solver.jl index 1b7383a68..e38df81f6 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -125,7 +125,7 @@ flux_vector(flux_balance_analysis(model, ...)) ``` """ flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? reaction_flux(model)' * value.(opt_model[:x]) : nothing + is_solved(opt_model) ? reaction_variables(model)' * value.(opt_model[:x]) : nothing """ $(TYPEDSIGNATURES) @@ -139,7 +139,7 @@ flux_dict(model, flux_balance_analysis(model, ...)) """ flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = is_solved(opt_model) ? - Dict(fluxes(model) .=> reaction_flux(model)' * value.(opt_model[:x])) : nothing + Dict(reactions(model) .=> reaction_variables(model)' * value.(opt_model[:x])) : nothing """ $(TYPEDSIGNATURES) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index a67cf9967..04147cbd6 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -17,7 +17,7 @@ For technical reasons, the "reactions" may sometimes not be true reactions but various virtual and helper pseudo-reactions that are used in the metabolic modeling, such as metabolite exchanges, separate forward and reverse reactions, supplies of enzymatic and genetic material and virtual cell volume, etc. To -simplify the view of the model contents use [`reaction_flux`](@ref). +simplify the view of the model contents use [`reaction_variables`](@ref). """ function variables(a::AbstractMetabolicModel)::Vector{String} missing_impl_error(variables, (a,)) @@ -105,16 +105,16 @@ In some models, the [`variables`](@ref) that correspond to the columns of [`stoichiometry`](@ref) matrix do not fully represent the semantic contents of the model; for example, fluxes may be split into forward and reverse reactions, reactions catalyzed by distinct enzymes, etc. Together with -[`reaction_flux`](@ref) (and [`n_fluxes`](@ref)) this specifies how the +[`reaction_variables`](@ref) (and [`n_reactions`](@ref)) this specifies how the flux is decomposed into individual reactions. By default (and in most models), fluxes and reactions perfectly correspond. """ -function fluxes(a::AbstractMetabolicModel)::Vector{String} +function reactions(a::AbstractMetabolicModel)::Vector{String} variables(a) end -function n_fluxes(a::AbstractMetabolicModel)::Int +function n_reactions(a::AbstractMetabolicModel)::Int n_variables(a) end @@ -122,14 +122,14 @@ end $(TYPEDSIGNATURES) Retrieve a sparse matrix that describes the correspondence of a solution of the -linear system to the fluxes (see [`fluxes`](@ref) for rationale). Returns a -sparse matrix of size `(n_variables(a), n_fluxes(a))`. For most models, this is +linear system to the fluxes (see [`reactions`](@ref) for rationale). Returns a +sparse matrix of size `(n_variables(a), n_reactions(a))`. For most models, this is an identity matrix. """ -function reaction_flux(a::AbstractMetabolicModel)::SparseMat +function reaction_variables(a::AbstractMetabolicModel)::SparseMat nr = n_variables(a) - nf = n_fluxes(a) - nr == nf || missing_impl_error(reaction_flux, (a,)) + nf = n_reactions(a) + nr == nf || missing_impl_error(reaction_variables, (a,)) spdiagm(fill(1, nr)) end diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index ea6358e2a..38bb72cbd 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -14,7 +14,7 @@ end # The list of inherited functions must be synced with the methods available for [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective fluxes n_fluxes reaction_flux coupling n_coupling_constraints coupling_bounds genes n_genes precache! +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 942792456..c2c6fb2d1 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -230,10 +230,10 @@ $(TYPEDSIGNATURES) Returns a matrix, which when multipled by the solution of a constraints based problem, yields the semantically meaningful fluxes that correspond to -[`fluxes`](@ref). +[`reactions`](@ref). """ -function Accessors.reaction_flux(cm::BalancedGrowthCommunityModel) - rfs = blockdiag([reaction_flux(m.model) for m in cm.members]...) +function Accessors.reaction_variables(cm::BalancedGrowthCommunityModel) + rfs = blockdiag([reaction_variables(m.model) for m in cm.members]...) nr = length(get_env_mets(cm)) + 1 # env ex + obj blockdiag(rfs, spdiagm(fill(1, nr))) end @@ -243,8 +243,8 @@ $(TYPEDSIGNATURES) Returns the semantically meaningful reactions of the model. """ -Accessors.fluxes(cm::BalancedGrowthCommunityModel) = [ - vcat([add_community_prefix.(Ref(m), fluxes(m.model)) for m in cm.members]...) +Accessors.reactions(cm::BalancedGrowthCommunityModel) = [ + vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) ["EX_" * env_met for env_met in get_env_mets(cm)] cm.objective_id ] @@ -254,8 +254,8 @@ $(TYPEDSIGNATURES) Return the semantically meaningful reactions of the model. """ -Accessors.n_fluxes(cm::BalancedGrowthCommunityModel) = - sum(n_fluxes(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 +Accessors.n_reactions(cm::BalancedGrowthCommunityModel) = + sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 #= This loops implements the rest of the accssors through access_community_member. diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/GeckoModel.jl index 9f8b0e217..9a39603ae 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/GeckoModel.jl @@ -67,7 +67,7 @@ wrapped model. The `objective` of the model includes also the extra columns for individual genes, as held by `coupling_row_gene_product`. Implementation exposes the split reactions (available as `variables(model)`), -but retains the original "simple" reactions accessible by [`fluxes`](@ref). +but retains the original "simple" reactions accessible by [`reactions`](@ref). The related constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). @@ -162,8 +162,8 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original fluxes in the wrapped model. """ -function Accessors.reaction_flux(model::GeckoModel) - rxnmat = gecko_column_reactions(model)' * reaction_flux(model.inner) +function Accessors.reaction_variables(model::GeckoModel) + rxnmat = gecko_column_reactions(model)' * reaction_variables(model.inner) [ rxnmat spzeros(n_genes(model), size(rxnmat, 2)) diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index 40f3902b9..b62ebcfd2 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -48,7 +48,7 @@ as specified in sMOMENT algorithm. This implementation allows easy access to fluxes from the split reactions (available in `variables(model)`), while the original "simple" reactions from -the wrapped model are retained as [`fluxes`](@ref). All additional constraints +the wrapped model are retained as [`reactions`](@ref). All additional constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). # Fields @@ -116,8 +116,8 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`SMomentModel`](@ref) to the original fluxes in the wrapped model. """ -Accessors.reaction_flux(model::SMomentModel) = - smoment_column_reactions(model)' * reaction_flux(model.inner) +Accessors.reaction_variables(model::SMomentModel) = + smoment_column_reactions(model)' * reaction_variables(model.inner) """ $(TYPEDSIGNATURES) From f41cbce00730be4cd1fa8e028c2d412622521a1b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 15 Dec 2022 14:34:07 +0100 Subject: [PATCH 064/531] format --- src/reconstruction/MatrixModel.jl | 2 +- src/types/misc/gecko.jl | 2 +- src/types/models/SBMLModel.jl | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index e53fc0fbf..188b34932 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -351,7 +351,7 @@ end remove_reactions!( model, [ - ridx for ridx in 1:n_variables(model) if + ridx for ridx = 1:n_variables(model) if any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) ], ) diff --git a/src/types/misc/gecko.jl b/src/types/misc/gecko.jl index 11fe7894e..4aa76b75a 100644 --- a/src/types/misc/gecko.jl +++ b/src/types/misc/gecko.jl @@ -40,7 +40,7 @@ Compute the part of the coupling for [`GeckoModel`](@ref) that limits the gecko_reaction_coupling(model::GeckoModel) = let tmp = [ (col.reaction_coupling_row, i, col.direction) for - (i, col) = enumerate(model.columns) if col.reaction_coupling_row != 0 + (i, col) in enumerate(model.columns) if col.reaction_coupling_row != 0 ] sparse( [row for (row, _, _) in tmp], diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 442643593..3ed316537 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -164,7 +164,7 @@ function _sbml_export_cvterms(annotations::Annotations)::Vector{SBML.CVTerm} biological_qualifier = :is, resource_uris = [ id == "RESOURCE_URI" ? val : "http://identifiers.org/$id/$val" for - (id, vals) = annotations if id != "sbo" for val in vals + (id, vals) in annotations if id != "sbo" for val in vals ], ), ] From e9cd0b90e1876ad6566af1cd5a20d46ebb9a72eb Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 20 Dec 2022 14:45:08 +0100 Subject: [PATCH 065/531] fix bug --- src/types/misc/BalancedGrowthCommunityModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/BalancedGrowthCommunityModel.jl index f6e880dc5..0cbc81dfa 100644 --- a/src/types/misc/BalancedGrowthCommunityModel.jl +++ b/src/types/misc/BalancedGrowthCommunityModel.jl @@ -13,7 +13,7 @@ $(TYPEDSIGNATURES) A helper function to get the unique environmental metabolites. """ get_env_mets(cm::BalancedGrowthCommunityModel) = - unique(hcat([get_exchange_mets(m) for m in cm.members]...)) + unique(vcat([get_exchange_mets(m) for m in cm.members]...)) """ $(TYPEDSIGNATURES) From b44b42f50a6fe77fa764710824cdb92b7a5baf9d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 21 Dec 2022 10:32:56 +0100 Subject: [PATCH 066/531] add helper functions --- src/utils/BalancedGrowthCommunityModel.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/utils/BalancedGrowthCommunityModel.jl diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl new file mode 100644 index 000000000..fd93afdeb --- /dev/null +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -0,0 +1,22 @@ +""" +$(TYPEDSIGNATURES) + +Extract the solution of a specific `community_member` from `opt_model`, which is +a solved optimization model built from the `community_model`. Removes the +`community_member` prefix in the string ids of the returned dictionary. +""" +get_solution(community_member::CommunityMember, opt_model, community_model::BalancedGrowthCommunityModel) = is_solved(opt_model) ? Dict( + string(last(split(rid, community_member.id*"#"))) => val for (rid, val) in zip(reactions(community_model), reaction_variables(model)' * value.(opt_model[:x])) if startswith(rid, community_member.id*"#") +) : nothing + + +""" +$(TYPEDSIGNATURES) + +Extract the environmental exchanges from `opt_model`, which is a solved +optimization model built from the `community_model`. +""" +get_environmental_exchanges(opt_model, community_model::BalancedGrowthCommunityModel) = is_solved(opt_model) ? Dict( + rid => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if !any(startswith(rid, cm.id*"#") for cm in community_model.members) +) : nothing + From 8e02e227180f2122276c083db4c01cf8304f8f7d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 21 Dec 2022 10:40:45 +0100 Subject: [PATCH 067/531] fix bug --- src/utils/BalancedGrowthCommunityModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl index fd93afdeb..8033cd722 100644 --- a/src/utils/BalancedGrowthCommunityModel.jl +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -6,7 +6,7 @@ a solved optimization model built from the `community_model`. Removes the `community_member` prefix in the string ids of the returned dictionary. """ get_solution(community_member::CommunityMember, opt_model, community_model::BalancedGrowthCommunityModel) = is_solved(opt_model) ? Dict( - string(last(split(rid, community_member.id*"#"))) => val for (rid, val) in zip(reactions(community_model), reaction_variables(model)' * value.(opt_model[:x])) if startswith(rid, community_member.id*"#") + string(last(split(rid, community_member.id*"#"))) => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if startswith(rid, community_member.id*"#") ) : nothing From 98e803d466426b9d6a2059d2c25bf5aa024f57f7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 21 Dec 2022 14:44:43 +0100 Subject: [PATCH 068/531] change order of args --- src/utils/BalancedGrowthCommunityModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl index 8033cd722..a66c259aa 100644 --- a/src/utils/BalancedGrowthCommunityModel.jl +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -5,7 +5,7 @@ Extract the solution of a specific `community_member` from `opt_model`, which is a solved optimization model built from the `community_model`. Removes the `community_member` prefix in the string ids of the returned dictionary. """ -get_solution(community_member::CommunityMember, opt_model, community_model::BalancedGrowthCommunityModel) = is_solved(opt_model) ? Dict( +get_solution(community_model::BalancedGrowthCommunityModel, opt_model, community_member::CommunityMember) = is_solved(opt_model) ? Dict( string(last(split(rid, community_member.id*"#"))) => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if startswith(rid, community_member.id*"#") ) : nothing @@ -16,7 +16,7 @@ $(TYPEDSIGNATURES) Extract the environmental exchanges from `opt_model`, which is a solved optimization model built from the `community_model`. """ -get_environmental_exchanges(opt_model, community_model::BalancedGrowthCommunityModel) = is_solved(opt_model) ? Dict( +get_environmental_exchanges(community_model::BalancedGrowthCommunityModel, opt_model) = is_solved(opt_model) ? Dict( rid => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if !any(startswith(rid, cm.id*"#") for cm in community_model.members) ) : nothing From 6592b2cb13a509723c72e3b0f77c0dedd836ec40 Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 21 Dec 2022 13:46:51 +0000 Subject: [PATCH 069/531] automatic formatting triggered by @stelmo on PR #712 --- src/utils/BalancedGrowthCommunityModel.jl | 27 +++++++++++++++++------ 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl index a66c259aa..5262bb5f8 100644 --- a/src/utils/BalancedGrowthCommunityModel.jl +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -5,9 +5,18 @@ Extract the solution of a specific `community_member` from `opt_model`, which is a solved optimization model built from the `community_model`. Removes the `community_member` prefix in the string ids of the returned dictionary. """ -get_solution(community_model::BalancedGrowthCommunityModel, opt_model, community_member::CommunityMember) = is_solved(opt_model) ? Dict( - string(last(split(rid, community_member.id*"#"))) => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if startswith(rid, community_member.id*"#") -) : nothing +get_solution( + community_model::BalancedGrowthCommunityModel, + opt_model, + community_member::CommunityMember, +) = + is_solved(opt_model) ? + Dict( + string(last(split(rid, community_member.id * "#"))) => val for (rid, val) in zip( + reactions(community_model), + reaction_variables(community_model)' * value.(opt_model[:x]), + ) if startswith(rid, community_member.id * "#") + ) : nothing """ @@ -16,7 +25,11 @@ $(TYPEDSIGNATURES) Extract the environmental exchanges from `opt_model`, which is a solved optimization model built from the `community_model`. """ -get_environmental_exchanges(community_model::BalancedGrowthCommunityModel, opt_model) = is_solved(opt_model) ? Dict( - rid => val for (rid, val) in zip(reactions(community_model), reaction_variables(community_model)' * value.(opt_model[:x])) if !any(startswith(rid, cm.id*"#") for cm in community_model.members) -) : nothing - +get_environmental_exchanges(community_model::BalancedGrowthCommunityModel, opt_model) = + is_solved(opt_model) ? + Dict( + rid => val for (rid, val) in zip( + reactions(community_model), + reaction_variables(community_model)' * value.(opt_model[:x]), + ) if !any(startswith(rid, cm.id * "#") for cm in community_model.members) + ) : nothing From b698151a1f6b3d22e607b9a273f54c5b321eb9ee Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 10 Nov 2022 00:02:24 +0100 Subject: [PATCH 070/531] removed stoich and compress model pretty print --- src/io/show/AbstractMetabolicModel.jl | 9 +-------- src/misc/constants.jl | 1 - 2 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index dabd6d4b3..cc286531c 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -4,12 +4,5 @@ $(TYPEDSIGNATURES) Pretty printing of everything metabolic-modelish. """ function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) - _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m))") - if n_variables(m) <= constants.default_stoich_show_size - println(io, stoichiometry(m)) - else # too big to display nicely - println(io, "S = [...]") - end - _pretty_print_keyvals(io, "Number of reactions: ", string(n_variables(m))) - _pretty_print_keyvals(io, "Number of metabolites: ", string(n_metabolites(m))) + _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m)) with $(n_reactions(m)) reactions and $(n_metabolites(m)) metabolites.") end diff --git a/src/misc/constants.jl b/src/misc/constants.jl index da21f5e40..183ab4440 100644 --- a/src/misc/constants.jl +++ b/src/misc/constants.jl @@ -4,7 +4,6 @@ A named tuple that contains the magic values that are used globally for whatever purposes. """ const constants = ( - default_stoich_show_size = 50_000, default_reaction_bound = 1e3, tolerance = 1e-6, sampling_keep_iters = 100, From 737ee6fb422f1eb122ce04a817398c9865e876a3 Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 10 Nov 2022 09:28:54 +0000 Subject: [PATCH 071/531] automatic formatting triggered by @stelmo on PR #698 --- src/io/show/AbstractMetabolicModel.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index cc286531c..2f2722547 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -4,5 +4,9 @@ $(TYPEDSIGNATURES) Pretty printing of everything metabolic-modelish. """ function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) - _pretty_print_keyvals(io, "", "Metabolic model of type $(typeof(m)) with $(n_reactions(m)) reactions and $(n_metabolites(m)) metabolites.") + _pretty_print_keyvals( + io, + "", + "Metabolic model of type $(typeof(m)) with $(n_reactions(m)) reactions and $(n_metabolites(m)) metabolites.", + ) end From c541f90ed4bcddf2152c67eabcf268c0c209fe2f Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 12 Dec 2022 12:21:47 +0100 Subject: [PATCH 072/531] Update src/io/show/AbstractMetabolicModel.jl Co-authored-by: Mirek Kratochvil --- src/io/show/AbstractMetabolicModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index 2f2722547..7ff57ea50 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -7,6 +7,6 @@ function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) _pretty_print_keyvals( io, "", - "Metabolic model of type $(typeof(m)) with $(n_reactions(m)) reactions and $(n_metabolites(m)) metabolites.", + "$(typeof(m)){$(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites}", ) end From bbe05657aed9909a7c733a84287e272b5017a4e4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 3 Jan 2023 11:35:02 +0100 Subject: [PATCH 073/531] further vastly simplify the printing of metabolic models and their contents --- src/io/show/AbstractMetabolicModel.jl | 5 +- src/io/show/Gene.jl | 5 -- src/io/show/Metabolite.jl | 10 ---- src/io/show/Reaction.jl | 55 --------------------- src/io/show/pretty_printing.jl | 45 ----------------- src/io/show/print_kwdef.jl | 69 +++++++++++++++++++++++++++ 6 files changed, 71 insertions(+), 118 deletions(-) delete mode 100644 src/io/show/Gene.jl delete mode 100644 src/io/show/Metabolite.jl delete mode 100644 src/io/show/Reaction.jl delete mode 100644 src/io/show/pretty_printing.jl create mode 100644 src/io/show/print_kwdef.jl diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index 7ff57ea50..88f1217c1 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -4,9 +4,8 @@ $(TYPEDSIGNATURES) Pretty printing of everything metabolic-modelish. """ function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) - _pretty_print_keyvals( + print( io, - "", - "$(typeof(m)){$(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites}", + "$(typeof(m))(#= $(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites =#)", ) end diff --git a/src/io/show/Gene.jl b/src/io/show/Gene.jl deleted file mode 100644 index 90d6e9066..000000000 --- a/src/io/show/Gene.jl +++ /dev/null @@ -1,5 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", g::Gene) - for fname in fieldnames(Gene) - _pretty_print_keyvals(io, "Gene.$(string(fname)): ", getfield(g, fname)) - end -end diff --git a/src/io/show/Metabolite.jl b/src/io/show/Metabolite.jl deleted file mode 100644 index ed31a0368..000000000 --- a/src/io/show/Metabolite.jl +++ /dev/null @@ -1,10 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", m::Metabolite) - for fname in fieldnames(Metabolite) - if fname == :charge - c = isnothing(getfield(m, fname)) ? nothing : string(getfield(m, fname)) - _pretty_print_keyvals(io, "Metabolite.$(string(fname)): ", c) - else - _pretty_print_keyvals(io, "Metabolite.$(string(fname)): ", getfield(m, fname)) - end - end -end diff --git a/src/io/show/Reaction.jl b/src/io/show/Reaction.jl deleted file mode 100644 index 90ac06999..000000000 --- a/src/io/show/Reaction.jl +++ /dev/null @@ -1,55 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Nicely format a substance list. -""" -function _pretty_substances(ss::Vector{String})::String - if isempty(ss) - "∅" - elseif length(ss) > 5 - join([ss[1], ss[2], "...", ss[end-1], ss[end]], " + ") - else - join(ss, " + ") - end -end - -function Base.show(io::Base.IO, ::MIME"text/plain", r::Reaction) - if r.upper_bound > 0.0 && r.lower_bound < 0.0 - arrow = " ↔ " - elseif r.upper_bound <= 0.0 && r.lower_bound < 0.0 - arrow = " ← " - elseif r.upper_bound > 0.0 && r.lower_bound >= 0.0 - arrow = " → " - else - arrow = " →|← " # blocked reaction - end - substrates = - ["$(-v) $k" for (k, v) in Iterators.filter(((_, v)::Pair -> v < 0), r.metabolites)] - products = - ["$v $k" for (k, v) in Iterators.filter(((_, v)::Pair -> v >= 0), r.metabolites)] - - for fname in fieldnames(Reaction) - if fname == :metabolites - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - _pretty_substances(substrates) * arrow * _pretty_substances(products), - ) - elseif fname == :gene_associations - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - maybemap(x -> unparse_grr(String, x), r.gene_associations), - ) - elseif fname in (:lower_bound, :upper_bound, :objective_coefficient) - _pretty_print_keyvals( - io, - "Reaction.$(string(fname)): ", - string(getfield(r, fname)), - ) - else - _pretty_print_keyvals(io, "Reaction.$(string(fname)): ", getfield(r, fname)) - end - end -end diff --git a/src/io/show/pretty_printing.jl b/src/io/show/pretty_printing.jl deleted file mode 100644 index 8df6f6462..000000000 --- a/src/io/show/pretty_printing.jl +++ /dev/null @@ -1,45 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Nicely prints keys and values. -""" -_pretty_print_keyvals(io, def::String, payload; kwargs...) = - _pretty_print_keyvals(io, def, isnothing(payload) ? "---" : string(payload); kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specialization of `_pretty_print_keyvals` for plain strings. -""" -function _pretty_print_keyvals(io, def::String, payload::String) - print(io, def) - if isempty(payload) - println(io, "---") - else - println(io, payload) - end -end - -""" -$(TYPEDSIGNATURES) - -Specialization of `_pretty_print_keyvals` for dictionaries. -""" -function _pretty_print_keyvals(io, def::String, payload::Dict) - - print(io, def) - if isempty(payload) - println(io, "---") - else - println(io, "") - for (k, v) in payload - if length(v) > 2 && length(v[1]) < 20 - println(io, "\t", k, ": ", v[1], ", ..., ", v[end]) - elseif length(v[1]) > 20 # basically for envipath annotations... or long notes - println(io, "\t", k, ": ", v[1][1:20], "...") - else - println(io, "\t", k, ": ", v) - end - end - end -end diff --git a/src/io/show/print_kwdef.jl b/src/io/show/print_kwdef.jl new file mode 100644 index 000000000..51a730f44 --- /dev/null +++ b/src/io/show/print_kwdef.jl @@ -0,0 +1,69 @@ + +""" +$(TYPEDSIGNATURES) + +Prints out a display-style (multi-line etc.) version of the structure `x` +defined using `Base.@kwdef` to the `io` stream. Falls back to +[`print_kwdef`](@ref) if the IO context has the `:compact` flag set. +""" +function display_kwdef(io::Base.IO, x::T) where {T} + if get(io, :compact, false) + print_kwdef(io, x) + else + print(io, T) + println(io, "(") + for field in fieldnames(T) + print(io, " ") + print(io, field) + print(io, " = ") + show(IOContext(io, :compact => true), getfield(x, field)) + println(io, ",") + end + print(io, ")") + end + nothing +end + +""" +$(TYPEDSIGNATURES) + +Prints out an inline-style (single-line) version of the structure `x` defined +using `Base.@kwdef` to the `io` stream. +""" +function print_kwdef(io::Base.IO, x::T) where {T} + print(io, T) + print(io, "(") + first = true + for field in fieldnames(T) + if first + first = false + else + print(io, ", ") + end + print(io, field) + print(io, "=") + show(IOContext(io, :compact => true), getfield(x, field)) + end + print(io, ")") + nothing +end + +""" +$(TYPEDSIGNATURES) + +Creates overloads of `Base.show` and `Base.print` for a given type. +""" +macro kwdef_printing(t) + :( + begin + Base.show(io::Base.IO, ::MIME"text/plain", x::$t) = display_kwdef(io, x) + Base.show(io::Base.IO, x::$t) = print_kwdef(io, x) + Base.print(io::Base.IO, x::$t) = print_kwdef(io, x) + end + ) +end + +@kwdef_printing Reaction +@kwdef_printing Metabolite +@kwdef_printing Gene +@kwdef_printing Isozyme From 658959a54529f36763aa570a28828d141d5164c1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 3 Jan 2023 11:39:01 +0100 Subject: [PATCH 074/531] simplify testing of reaction print-out --- test/types/Reaction.jl | 42 +----------------------------------------- 1 file changed, 1 insertion(+), 41 deletions(-) diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index c90223fa4..516571b85 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -33,50 +33,10 @@ @test all( contains.( sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "glycolysis", "blah", "biocyc", "(g2 and g1) or (g3)"], + ["r1", "100.0", "glycolysis", "blah", "biocyc", "g1", "g2", "g3"], ), ) - rlongfor = Reaction( - "rlongfor", - Dict( - m1.id => -1.0, - m2.id => -1.0, - m3.id => -1.0, - m4.id => -1.0, - m5.id => -1.0, - m6.id => -1.0, - m7.id => 1.0, - m8.id => 1.0, - m9.id => 1.0, - m10.id => 1.0, - m11.id => 1.0, - m12.id => 1.0, - ), - :forward, - ) - @test contains(sprint(show, MIME("text/plain"), rlongfor), "...") - - rlongrev = Reaction( - "rlongrev", - Dict( - m1.id => -1.0, - m2.id => -1.0, - m3.id => -1.0, - m4.id => -1.0, - m5.id => -1.0, - m6.id => -1.0, - m7.id => 1.0, - m8.id => 1.0, - m9.id => 1.0, - m10.id => 1.0, - m11.id => 1.0, - m12.id => 1.0, - ), - :reverse, - ) - @test occursin("...", sprint(show, MIME("text/plain"), rlongrev)) - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) @test r2.lower_bound == -1000.0 && r2.upper_bound == 0.0 From 23ae0dc30c8a2a7197d02013a353226328204459 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 8 Jan 2023 20:39:39 +0100 Subject: [PATCH 075/531] delete reaction construction functions --- src/reconstruction/Reaction.jl | 74 --------------------------------- test/reconstruction/Reaction.jl | 36 ---------------- 2 files changed, 110 deletions(-) delete mode 100644 src/reconstruction/Reaction.jl delete mode 100644 test/reconstruction/Reaction.jl diff --git a/src/reconstruction/Reaction.jl b/src/reconstruction/Reaction.jl deleted file mode 100644 index 3fe897e1b..000000000 --- a/src/reconstruction/Reaction.jl +++ /dev/null @@ -1,74 +0,0 @@ -""" -$(TYPEDEF) - -A small helper type for constructing reactions inline - -# Fields -$(TYPEDFIELDS) -""" -struct _Stoichiometry - s::Dict{String,Float64} -end - -const _Stoichiometrizable = Union{Metabolite,_Stoichiometry} - -Base.convert(::Type{_Stoichiometry}, ::Nothing) = _Stoichiometry(Dict()) -Base.convert(::Type{_Stoichiometry}, m::Metabolite) = _Stoichiometry(Dict(m.id => 1.0)) - -Base.:*(a::Real, m::Metabolite) = _Stoichiometry(Dict(m.id => a)) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `metabolite1 + metabolite2`. Add 2 groups of [`Metabolite`](@ref)s -together to form reactions inline. Use with `+`, `*`, [`→`](@ref) and similar -operators. -""" -function Base.:+(a::_Stoichiometrizable, b::_Stoichiometrizable) - ad = convert(_Stoichiometry, a).s - bd = convert(_Stoichiometry, b).s - _Stoichiometry( - Dict( - mid => get(ad, mid, 0.0) + get(bd, mid, 0.0) for - mid in union(keys(ad), keys(bd)) - ), - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function _make_reaction_dict(r, p) - rd = convert(_Stoichiometry, r).s - pd = convert(_Stoichiometry, p).s - return Dict{String,Float64}( - mid => get(pd, mid, 0.0) - get(rd, mid, 0.0) for mid in union(keys(rd), keys(pd)) - ) -end - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates → products`. Make a forward-only [`Reaction`](@ref) -from `substrates` and `products`. -""" -→(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :forward) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates ← products`. Make a reverse-only [`Reaction`](@ref) -from `substrates` and `products`. -""" -←(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :reverse) - -""" -$(TYPEDSIGNATURES) - -Shorthand for `substrates ↔ products`. Make a bidirectional (reversible) -[`Reaction`](@ref) from `substrates` and `products`. -""" -↔(substrates::Maybe{_Stoichiometrizable}, products::Maybe{_Stoichiometrizable}) = - Reaction("", _make_reaction_dict(substrates, products), :bidirectional) diff --git a/test/reconstruction/Reaction.jl b/test/reconstruction/Reaction.jl deleted file mode 100644 index 7b08f1279..000000000 --- a/test/reconstruction/Reaction.jl +++ /dev/null @@ -1,36 +0,0 @@ -@testset "Construction overloading" begin - model = load_model(ObjectModel, model_paths["iJO1366.json"]) - - rxn_original = model.reactions["NADH16pp"] - nadh = model.metabolites["nadh_c"] - h_c = model.metabolites["h_c"] - q8 = model.metabolites["q8_c"] - q8h2 = model.metabolites["q8h2_c"] - nad = model.metabolites["nad_c"] - h_p = model.metabolites["h_p"] - - rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lower_bound == 0.0 && rxn.upper_bound > 0.0 - - rxn = 1.0 * nadh + 4.0 * h_c + q8 ← 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lower_bound < 0.0 && rxn.upper_bound == 0.0 - - rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 ↔ q8h2 + nad + 3.0 * h_p - @test rxn.lower_bound < 0.0 && rxn.upper_bound > 0.0 - - rxn = 1.0 * nadh → nothing - @test length(rxn.metabolites) == 1 - - rxn = nothing → nadh - @test length(rxn.metabolites) == 1 - - rxn = nothing → 1.0nadh - @test length(rxn.metabolites) == 1 - - rxn = 1.0 * nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test prod(values(rxn.metabolites)) == -12 - @test ("q8h2_c" in [x for x in keys(rxn.metabolites)]) - - rxn = nadh + 4.0 * h_c + 1.0 * q8 → 1.0 * q8h2 + 1.0 * nad + 3.0 * h_p - @test rxn.lower_bound == 0.0 && rxn.upper_bound > 0.0 -end From 0f902234ee9d245800e82063a6489b525f4db628 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 9 Jan 2023 23:17:53 +0100 Subject: [PATCH 076/531] sed gecko --- docs/src/concepts/4_wrappers.md | 4 +- .../{15_gecko.jl => 15_enzyme_constrained.jl} | 24 +++--- src/analysis.jl | 2 +- .../{gecko.jl => enzyme_constrained.jl} | 20 ++--- src/reconstruction/modifications/enzymes.jl | 8 +- .../misc/{gecko.jl => enzyme_constrained.jl} | 24 +++--- ...eckoModel.jl => EnzymeConstrainedModel.jl} | 82 +++++++++---------- src/utils/enzyme_constrained.jl | 10 +-- .../{gecko.jl => enzyme_constrained.jl} | 6 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- 10 files changed, 91 insertions(+), 91 deletions(-) rename docs/src/examples/{15_gecko.jl => 15_enzyme_constrained.jl} (76%) rename src/reconstruction/{gecko.jl => enzyme_constrained.jl} (88%) rename src/types/misc/{gecko.jl => enzyme_constrained.jl} (67%) rename src/types/wrappers/{GeckoModel.jl => EnzymeConstrainedModel.jl} (67%) rename test/reconstruction/{gecko.jl => enzyme_constrained.jl} (97%) diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index 02d10c69b..425c11c87 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -6,11 +6,11 @@ structure, COBREXA.jl supports a class of model _wrappers_, which are basically small layers that add or change the functionality of a given base models. Types [`Serialized`](@ref), [`MatrixCoupling`](@ref), [`SMomentModel`](@ref), and -[`GeckoModel`](@ref) all work in this manner -- add some extra functionality to +[`EnzymeConstrainedModel`](@ref) all work in this manner -- add some extra functionality to the "base". Technically, they are all subtypes of the abstract type [`AbstractModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) and can thus be used in all standard analysis functions. Similarly, the model -wraps can be stacked -- it is easy to e.g. serialize a [`GeckoModel`](@ref), or +wraps can be stacked -- it is easy to e.g. serialize a [`EnzymeConstrainedModel`](@ref), or to add coupling to an existing [`SMomentModel`](@ref). As the main benefit of the approach, creating model variants using the wrapper diff --git a/docs/src/examples/15_gecko.jl b/docs/src/examples/15_enzyme_constrained.jl similarity index 76% rename from docs/src/examples/15_gecko.jl rename to docs/src/examples/15_enzyme_constrained.jl index 3be74ffab..deb445b59 100644 --- a/docs/src/examples/15_gecko.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -57,8 +57,8 @@ gene_product_bounds = Dict(genes(model) .=> Ref((0.0, 10.0))) # With this, the construction of the model constrained by all enzymatic # information is straightforward: -gecko_model = - model |> with_gecko(; +enzyme_constrained_model = + model |> with_enzyme_constrained(; reaction_isozymes = rxn_isozymes, gene_product_bounds, gene_product_molar_mass = gene_product_masses, @@ -66,35 +66,35 @@ gecko_model = gene_product_mass_group_bound = _ -> 100.0, # the total limit of mass in the single category ) -# (Alternatively, you may use [`make_gecko_model`](@ref), which does the same +# (Alternatively, you may use [`make_enzyme_constrained_model`](@ref), which does the same # without piping by `|>`.) -# The stoichiometry and coupling in the gecko model is noticeably more complex; +# The stoichiometry and coupling in the enzyme_constrained model is noticeably more complex; # you may notice new "reactions" added that simulate the gene product # utilization: -[stoichiometry(gecko_model); coupling(gecko_model)] +[stoichiometry(enzyme_constrained_model); coupling(enzyme_constrained_model)] # Again, the resulting model can be used in any type of analysis. For example, flux balance analysis: -opt_model = flux_balance_analysis(gecko_model, GLPK.Optimizer) +opt_model = flux_balance_analysis(enzyme_constrained_model, GLPK.Optimizer) # Get the fluxes -flux_sol = flux_dict(gecko_model, opt_model) +flux_sol = flux_dict(enzyme_constrained_model, opt_model) # Get the gene product concentrations -gp_concs = gene_product_dict(gecko_model, opt_model) +gp_concs = gene_product_dict(enzyme_constrained_model, opt_model) # Get the total masses assigned to each mass group -gene_product_mass_group_dict(gecko_model, opt_model) +gene_product_mass_group_dict(enzyme_constrained_model, opt_model) # Variability: -flux_variability_analysis(gecko_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) +flux_variability_analysis(enzyme_constrained_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) # ...and sampling: -affine_hit_and_run(gecko_model, warmup_from_variability(gecko_model, GLPK.Optimizer))' * -reaction_variables(gecko_model) +affine_hit_and_run(enzyme_constrained_model, warmup_from_variability(enzyme_constrained_model, GLPK.Optimizer))' * +reaction_variables(enzyme_constrained_model) diff --git a/src/analysis.jl b/src/analysis.jl index 451cf9af2..cc65b9e96 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -24,7 +24,7 @@ using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types -using ..Types: _GeckoReactionColumn, _SMomentColumn +using ..Types: _EnzymeConstrainedReactionColumn, _SMomentColumn using Distributed using DistributedData diff --git a/src/reconstruction/gecko.jl b/src/reconstruction/enzyme_constrained.jl similarity index 88% rename from src/reconstruction/gecko.jl rename to src/reconstruction/enzyme_constrained.jl index fd424ad1b..ef693b2f6 100644 --- a/src/reconstruction/gecko.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -1,8 +1,8 @@ """ $(TYPEDSIGNATURES) -Wrap a model into a [`GeckoModel`](@ref), following the structure given by -GECKO algorithm (see [`GeckoModel`](@ref) documentation for details). +Wrap a model into a [`EnzymeConstrainedModel`](@ref), following the structure given by +GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for details). # Arguments @@ -23,7 +23,7 @@ GECKO algorithm (see [`GeckoModel`](@ref) documentation for details). Alternatively, all function arguments may be also supplied as dictionaries that provide the same data lookup. """ -function make_gecko_model( +function make_enzyme_constrained_model( model::AbstractMetabolicModel; reaction_isozymes::Union{Function,Dict{String,Vector{Isozyme}}}, gene_product_bounds::Union{Function,Dict{String,Tuple{Float64,Float64}}}, @@ -48,7 +48,7 @@ function make_gecko_model( (grp -> gene_product_mass_group_bound[grp]) # ...it would be nicer to have an overload for this, but kwargs can't be used for dispatch - columns = Vector{Types._GeckoReactionColumn}() + columns = Vector{Types._EnzymeConstrainedReactionColumn}() coupling_row_reaction = Int[] coupling_row_gene_product = Int[] @@ -62,7 +62,7 @@ function make_gecko_model( for i = 1:n_variables(model) isozymes = ris_(rids[i]) if isnothing(isozymes) - push!(columns, Types._GeckoReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) + push!(columns, Types._EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) continue end @@ -108,7 +108,7 @@ function make_gecko_model( # make a new column push!( columns, - Types._GeckoReactionColumn( + Types._EnzymeConstrainedReactionColumn( i, iidx, dir, @@ -133,16 +133,16 @@ function make_gecko_model( mg_gid_lookup[mg] = [gid] end end - coupling_row_mass_group = Vector{Types._GeckoCapacity}() + coupling_row_mass_group = Vector{Types._EnzymeConstrainedCapacity}() for (grp, gs) in mg_gid_lookup idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] mms = gpmm_.(gs) - push!(coupling_row_mass_group, Types._GeckoCapacity(grp, idxs, mms, gmgb_(grp))) + push!(coupling_row_mass_group, Types._EnzymeConstrainedCapacity(grp, idxs, mms, gmgb_(grp))) end - GeckoModel( + EnzymeConstrainedModel( [ - Types.gecko_column_reactions(columns, model)' * objective(model) + Types.enzyme_constrained_column_reactions(columns, model)' * objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index 6f22cf800..0aefd77ab 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -10,8 +10,8 @@ with_smoment(; kwargs...) = model -> make_smoment_model(model; kwargs...) """ $(TYPEDSIGNATURES) -Specifies a model variant which adds extra semantics of the Gecko algorithm, -giving a [`GeckoModel`](@ref). The arguments are forwarded to -[`make_gecko_model`](@ref). Intended for usage with [`screen`](@ref). +Specifies a model variant which adds extra semantics of the EnzymeConstrained algorithm, +giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to +[`make_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_gecko(; kwargs...) = model -> make_gecko_model(model; kwargs...) +with_enzyme_constrained(; kwargs...) = model -> make_enzyme_constrained_model(model; kwargs...) diff --git a/src/types/misc/gecko.jl b/src/types/misc/enzyme_constrained.jl similarity index 67% rename from src/types/misc/gecko.jl rename to src/types/misc/enzyme_constrained.jl index 4aa76b75a..c745e2d9a 100644 --- a/src/types/misc/gecko.jl +++ b/src/types/misc/enzyme_constrained.jl @@ -2,9 +2,9 @@ """ $(TYPEDSIGNATURES) -Internal helper for systematically naming reactions in [`GeckoModel`](@ref). +Internal helper for systematically naming reactions in [`EnzymeConstrainedModel`](@ref). """ -gecko_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = +enzyme_constrained_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = direction == 0 ? original_name : direction > 0 ? "$original_name#forward#$isozyme_idx" : "$original_name#reverse#$isozyme_idx" @@ -15,15 +15,15 @@ $(TYPEDSIGNATURES) Retrieve a utility mapping between reactions and split reactions; rows correspond to "original" reactions, columns correspond to "split" reactions. """ -gecko_column_reactions(model::GeckoModel) = - gecko_column_reactions(model.columns, model.inner) +enzyme_constrained_column_reactions(model::EnzymeConstrainedModel) = + enzyme_constrained_column_reactions(model.columns, model.inner) """ $(TYPEDSIGNATURES) -Helper method that doesn't require the whole [`GeckoModel`](@ref). +Helper method that doesn't require the whole [`EnzymeConstrainedModel`](@ref). """ -gecko_column_reactions(columns, inner) = sparse( +enzyme_constrained_column_reactions(columns, inner) = sparse( [col.reaction_idx for col in columns], 1:length(columns), [col.direction >= 0 ? 1 : -1 for col in columns], @@ -34,10 +34,10 @@ gecko_column_reactions(columns, inner) = sparse( """ $(TYPEDSIGNATURES) -Compute the part of the coupling for [`GeckoModel`](@ref) that limits the +Compute the part of the coupling for [`EnzymeConstrainedModel`](@ref) that limits the "arm" reactions (which group the individual split unidirectional reactions). """ -gecko_reaction_coupling(model::GeckoModel) = +enzyme_constrained_reaction_coupling(model::EnzymeConstrainedModel) = let tmp = [ (col.reaction_coupling_row, i, col.direction) for (i, col) in enumerate(model.columns) if col.reaction_coupling_row != 0 @@ -54,10 +54,10 @@ gecko_reaction_coupling(model::GeckoModel) = """ $(TYPEDSIGNATURES) -Compute the part of the coupling for GeckoModel that limits the amount of each +Compute the part of the coupling for EnzymeConstrainedModel that limits the amount of each kind of protein available. """ -gecko_gene_product_coupling(model::GeckoModel) = +enzyme_constrained_gene_product_coupling(model::EnzymeConstrainedModel) = let tmp = [ (row, i, val) for (i, col) in enumerate(model.columns) for @@ -75,10 +75,10 @@ gecko_gene_product_coupling(model::GeckoModel) = """ $(TYPEDSIGNATURES) -Compute the part of the coupling for [`GeckoModel`](@ref) that limits the total +Compute the part of the coupling for [`EnzymeConstrainedModel`](@ref) that limits the total mass of each group of gene products. """ -function gecko_mass_group_coupling(model::GeckoModel) +function enzyme_constrained_mass_group_coupling(model::EnzymeConstrainedModel) tmp = [ # mm = molar mass, mg = mass group, i = row idx, j = col idx (i, j, mm) for (i, mg) in enumerate(model.coupling_row_mass_group) for (j, mm) in zip(mg.gene_product_idxs, mg.gene_product_molar_masses) diff --git a/src/types/wrappers/GeckoModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl similarity index 67% rename from src/types/wrappers/GeckoModel.jl rename to src/types/wrappers/EnzymeConstrainedModel.jl index 9a39603ae..ace1a52ca 100644 --- a/src/types/wrappers/GeckoModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -1,12 +1,12 @@ """ $(TYPEDEF) -A helper type for describing the contents of [`GeckoModel`](@ref)s. +A helper type for describing the contents of [`EnzymeConstrainedModel`](@ref)s. # Fields $(TYPEDFIELDS) """ -struct _GeckoReactionColumn +struct _EnzymeConstrainedReactionColumn reaction_idx::Int isozyme_idx::Int direction::Int @@ -25,7 +25,7 @@ the grouping type, e.g. metabolic or membrane groups etc. # Fields $(TYPEDFIELDS) """ -struct _GeckoCapacity +struct _EnzymeConstrainedCapacity group_id::String gene_product_idxs::Vector{Int} gene_product_molar_masses::Vector{Float64} @@ -40,7 +40,7 @@ A model with complex enzyme concentration and capacity bounds, as described in genome-scale metabolic model by incorporating enzymatic constraints." Molecular systems biology 13.8 (2017): 935.* -Use [`make_gecko_model`](@ref) or [`with_gecko`](@ref) to construct this kind +Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constrained`](@ref) to construct this kind of model. The model wraps another "internal" model, and adds following modifications: @@ -74,28 +74,28 @@ The related constraints are implemented using [`coupling`](@ref) and # Fields $(TYPEDFIELDS) """ -struct GeckoModel <: AbstractModelWrapper +struct EnzymeConstrainedModel <: AbstractModelWrapper objective::SparseVec - columns::Vector{_GeckoReactionColumn} + columns::Vector{_EnzymeConstrainedReactionColumn} coupling_row_reaction::Vector{Int} coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} - coupling_row_mass_group::Vector{_GeckoCapacity} + coupling_row_mass_group::Vector{_EnzymeConstrainedCapacity} inner::AbstractMetabolicModel end -Accessors.unwrap_model(model::GeckoModel) = model.inner +Accessors.unwrap_model(model::EnzymeConstrainedModel) = model.inner """ $(TYPEDSIGNATURES) -Return a stoichiometry of the [`GeckoModel`](@ref). The enzymatic reactions are +Return a stoichiometry of the [`EnzymeConstrainedModel`](@ref). The enzymatic reactions are split into unidirectional forward and reverse ones, each of which may have multiple variants per isozyme. """ -function Accessors.stoichiometry(model::GeckoModel) - irrevS = stoichiometry(model.inner) * gecko_column_reactions(model) - enzS = gecko_gene_product_coupling(model) +function Accessors.stoichiometry(model::EnzymeConstrainedModel) + irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) + enzS = enzyme_constrained_gene_product_coupling(model) [ irrevS spzeros(size(irrevS, 1), size(enzS, 1)) -enzS I(size(enzS, 1)) @@ -105,24 +105,24 @@ end """ $(TYPEDSIGNATURES) -Return the objective of the [`GeckoModel`](@ref). Note, the objective is with +Return the objective of the [`EnzymeConstrainedModel`](@ref). Note, the objective is with respect to the internal variables, i.e. [`variables(model)`](@ref), which are the unidirectional reactions and the genes involved in enzymatic reactions that have kinetic data. """ -Accessors.objective(model::GeckoModel) = model.objective +Accessors.objective(model::EnzymeConstrainedModel) = model.objective """ $(TYPEDSIGNATURES) -Returns the internal reactions in a [`GeckoModel`](@ref) (these may be split +Returns the internal reactions in a [`EnzymeConstrainedModel`](@ref) (these may be split to forward- and reverse-only parts with different isozyme indexes; reactions IDs are mangled accordingly with suffixes). """ -function Accessors.variables(model::GeckoModel) +function Accessors.variables(model::EnzymeConstrainedModel) inner_reactions = variables(model.inner) mangled_reactions = [ - gecko_reaction_name( + enzyme_constrained_reaction_name( inner_reactions[col.reaction_idx], col.direction, col.isozyme_idx, @@ -137,14 +137,14 @@ $(TYPEDSIGNATURES) Returns the number of all irreversible reactions in `model` as well as the number of gene products that take part in enzymatic reactions. """ -Accessors.n_variables(model::GeckoModel) = length(model.columns) + n_genes(model) +Accessors.n_variables(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) """ $(TYPEDSIGNATURES) -Return variable bounds for [`GeckoModel`](@ref). +Return variable bounds for [`EnzymeConstrainedModel`](@ref). """ -function Accessors.bounds(model::GeckoModel) +function Accessors.bounds(model::EnzymeConstrainedModel) lbs = [ [col.lb for col in model.columns] [lb for (_, (lb, _)) in model.coupling_row_gene_product] @@ -159,11 +159,11 @@ end """ $(TYPEDSIGNATURES) -Get the mapping of the reaction rates in [`GeckoModel`](@ref) to the original +Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to the original fluxes in the wrapped model. """ -function Accessors.reaction_variables(model::GeckoModel) - rxnmat = gecko_column_reactions(model)' * reaction_variables(model.inner) +function Accessors.reaction_variables(model::EnzymeConstrainedModel) + rxnmat = enzyme_constrained_column_reactions(model)' * reaction_variables(model.inner) [ rxnmat spzeros(n_genes(model), size(rxnmat, 2)) @@ -173,14 +173,14 @@ end """ $(TYPEDSIGNATURES) -Return the coupling of [`GeckoModel`](@ref). That combines the coupling of the +Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the coupling of the wrapped model, coupling for split (arm) reactions, and the coupling for the total enzyme capacity. """ -function Accessors.coupling(model::GeckoModel) - innerC = coupling(model.inner) * gecko_column_reactions(model) - rxnC = gecko_reaction_coupling(model) - enzcap = gecko_mass_group_coupling(model) +function Accessors.coupling(model::EnzymeConstrainedModel) + innerC = coupling(model.inner) * enzyme_constrained_column_reactions(model) + rxnC = enzyme_constrained_reaction_coupling(model) + enzcap = enzyme_constrained_mass_group_coupling(model) [ innerC spzeros(size(innerC, 1), n_genes(model)) rxnC spzeros(size(rxnC, 1), n_genes(model)) @@ -191,10 +191,10 @@ end """ $(TYPEDSIGNATURES) -Count the coupling constraints in [`GeckoModel`](@ref) (refer to +Count the coupling constraints in [`EnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for details). """ -Accessors.n_coupling_constraints(model::GeckoModel) = +Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = n_coupling_constraints(model.inner) + length(model.coupling_row_reaction) + length(model.coupling_row_mass_group) @@ -202,10 +202,10 @@ Accessors.n_coupling_constraints(model::GeckoModel) = """ $(TYPEDSIGNATURES) -The coupling bounds for [`GeckoModel`](@ref) (refer to [`coupling`](@ref) for +The coupling bounds for [`EnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for details). """ -function Accessors.coupling_bounds(model::GeckoModel) +function Accessors.coupling_bounds(model::EnzymeConstrainedModel) (iclb, icub) = coupling_bounds(model.inner) (ilb, iub) = bounds(model.inner) return ( @@ -226,9 +226,9 @@ end $(TYPEDSIGNATURES) Return the balance of the reactions in the inner model, concatenated with a vector of -zeros representing the enzyme balance of a [`GeckoModel`](@ref). +zeros representing the enzyme balance of a [`EnzymeConstrainedModel`](@ref). """ -Accessors.balance(model::GeckoModel) = +Accessors.balance(model::EnzymeConstrainedModel) = [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] """ @@ -236,27 +236,27 @@ $(TYPEDSIGNATURES) Return the number of genes that have enzymatic constraints associated with them. """ -Accessors.n_genes(model::GeckoModel) = length(model.coupling_row_gene_product) +Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) """ $(TYPEDSIGNATURES) Return the gene ids of genes that have enzymatic constraints associated with them. """ -Accessors.genes(model::GeckoModel) = +Accessors.genes(model::EnzymeConstrainedModel) = genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] """ $(TYPEDSIGNATURES) -Return the ids of all metabolites, both real and pseudo, for a [`GeckoModel`](@ref). +Return the ids of all metabolites, both real and pseudo, for a [`EnzymeConstrainedModel`](@ref). """ -Accessors.metabolites(model::GeckoModel) = - [metabolites(model.inner); genes(model) .* "#gecko"] +Accessors.metabolites(model::EnzymeConstrainedModel) = + [metabolites(model.inner); genes(model) .* "#enzyme_constrained"] """ $(TYPEDSIGNATURES) -Return the number of metabolites, both real and pseudo, for a [`GeckoModel`](@ref). +Return the number of metabolites, both real and pseudo, for a [`EnzymeConstrainedModel`](@ref). """ -Accessors.n_metabolites(model::GeckoModel) = n_metabolites(model.inner) + n_genes(model) +Accessors.n_metabolites(model::EnzymeConstrainedModel) = n_metabolites(model.inner) + n_genes(model) diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index a3fc8e0e1..906baf830 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -6,7 +6,7 @@ argument `opt_model` is a solved optimization problem, typically returned by [`flux_balance_analysis`](@ref). See [`flux_dict`](@ref) for the corresponding function that returns a dictionary of solved fluxes. """ -gene_product_dict(model::GeckoModel, opt_model) = +gene_product_dict(model::EnzymeConstrainedModel, opt_model) = is_solved(opt_model) ? Dict(genes(model) .=> value.(opt_model[:x])[(length(model.columns)+1):end]) : nothing @@ -15,14 +15,14 @@ $(TYPEDSIGNATURES) A pipe-able variant of [`gene_product_dict`](@ref). """ -gene_product_dict(model::GeckoModel) = x -> gene_product_dict(model, x) +gene_product_dict(model::EnzymeConstrainedModel) = x -> gene_product_dict(model, x) """ $(TYPEDSIGNATURES) -Extract the mass utilization in mass groups from a solved [`GeckoModel`](@ref). +Extract the mass utilization in mass groups from a solved [`EnzymeConstrainedModel`](@ref). """ -gene_product_mass_group_dict(model::GeckoModel, opt_model) = +gene_product_mass_group_dict(model::EnzymeConstrainedModel, opt_model) = is_solved(opt_model) ? Dict( grp.group_id => dot( @@ -36,7 +36,7 @@ $(TYPEDSIGNATURES) A pipe-able variant of [`gene_product_mass_group_dict`](@ref). """ -gene_product_mass_group_dict(model::GeckoModel) = +gene_product_mass_group_dict(model::EnzymeConstrainedModel) = x -> gene_product_mass_group_dict(model, x) """ diff --git a/test/reconstruction/gecko.jl b/test/reconstruction/enzyme_constrained.jl similarity index 97% rename from test/reconstruction/gecko.jl rename to test/reconstruction/enzyme_constrained.jl index f9893edbd..4f86e9166 100644 --- a/test/reconstruction/gecko.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -23,7 +23,7 @@ lower = [-1000.0, -1.0], upper = [nothing, 12.0], ) |> - with_gecko( + with_enzyme_constrained( reaction_isozymes = get_reaction_isozymes, gene_product_bounds = g -> g == "b2779" ? (0.01, 0.06) : (0.0, 1.0), gene_product_molar_mass = get_gene_product_mass, @@ -72,7 +72,7 @@ end original GECKO paper. This model is nice to troubleshoot with, because the stoich matrix is small. =# - m = ObjectModel(id = "gecko") + m = ObjectModel(id = "enzyme_constrained") m1 = Metabolite("m1") m2 = Metabolite("m2") m3 = Metabolite("m3") @@ -122,7 +122,7 @@ end gene_product_mass_group_bound = Dict("uncategorized" => 0.5) - gm = make_gecko_model( + gm = make_enzyme_constrained_model( m; reaction_isozymes = Dict( rid => r.gene_associations for (rid, r) in m.reactions if diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index a44d9c7c9..ad7e5c0b7 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -291,7 +291,7 @@ end ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> with_changed_bounds(["EX_glc__D_e"]; lower = [-1000.0], upper = [0]) |> - with_gecko( + with_enzyme_constrained( reaction_isozymes = get_reaction_isozymes, gene_product_bounds = g -> (0.0, 10.0), gene_product_molar_mass = get_gene_product_mass, From ced3590ae450b9456650fc60afbb28284107b7d0 Mon Sep 17 00:00:00 2001 From: stelmo Date: Mon, 9 Jan 2023 22:26:53 +0000 Subject: [PATCH 077/531] automatic formatting triggered by @stelmo on PR #719 --- docs/src/examples/15_enzyme_constrained.jl | 12 +++++++++--- src/reconstruction/enzyme_constrained.jl | 10 ++++++++-- src/reconstruction/modifications/enzymes.jl | 3 ++- src/types/wrappers/EnzymeConstrainedModel.jl | 6 ++++-- 4 files changed, 23 insertions(+), 8 deletions(-) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index deb445b59..ec9955b66 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -93,8 +93,14 @@ gene_product_mass_group_dict(enzyme_constrained_model, opt_model) # Variability: -flux_variability_analysis(enzyme_constrained_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) +flux_variability_analysis( + enzyme_constrained_model, + GLPK.Optimizer, + bounds = gamma_bounds(0.95), +) # ...and sampling: -affine_hit_and_run(enzyme_constrained_model, warmup_from_variability(enzyme_constrained_model, GLPK.Optimizer))' * -reaction_variables(enzyme_constrained_model) +affine_hit_and_run( + enzyme_constrained_model, + warmup_from_variability(enzyme_constrained_model, GLPK.Optimizer), +)' * reaction_variables(enzyme_constrained_model) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index ef693b2f6..f33d94c68 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -62,7 +62,10 @@ function make_enzyme_constrained_model( for i = 1:n_variables(model) isozymes = ris_(rids[i]) if isnothing(isozymes) - push!(columns, Types._EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], [])) + push!( + columns, + Types._EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], []), + ) continue end @@ -137,7 +140,10 @@ function make_enzyme_constrained_model( for (grp, gs) in mg_gid_lookup idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] mms = gpmm_.(gs) - push!(coupling_row_mass_group, Types._EnzymeConstrainedCapacity(grp, idxs, mms, gmgb_(grp))) + push!( + coupling_row_mass_group, + Types._EnzymeConstrainedCapacity(grp, idxs, mms, gmgb_(grp)), + ) end EnzymeConstrainedModel( diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index 0aefd77ab..6b6660f96 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -14,4 +14,5 @@ Specifies a model variant which adds extra semantics of the EnzymeConstrained al giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to [`make_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_enzyme_constrained(; kwargs...) = model -> make_enzyme_constrained_model(model; kwargs...) +with_enzyme_constrained(; kwargs...) = + model -> make_enzyme_constrained_model(model; kwargs...) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index ace1a52ca..adb350bc6 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -137,7 +137,8 @@ $(TYPEDSIGNATURES) Returns the number of all irreversible reactions in `model` as well as the number of gene products that take part in enzymatic reactions. """ -Accessors.n_variables(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) +Accessors.n_variables(model::EnzymeConstrainedModel) = + length(model.columns) + n_genes(model) """ $(TYPEDSIGNATURES) @@ -259,4 +260,5 @@ $(TYPEDSIGNATURES) Return the number of metabolites, both real and pseudo, for a [`EnzymeConstrainedModel`](@ref). """ -Accessors.n_metabolites(model::EnzymeConstrainedModel) = n_metabolites(model.inner) + n_genes(model) +Accessors.n_metabolites(model::EnzymeConstrainedModel) = + n_metabolites(model.inner) + n_genes(model) From a5019ecbd5b30d61cd1865c0dbc21539d05fe958 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 9 Jan 2023 23:37:56 +0100 Subject: [PATCH 078/531] sed reverse --- docs/src/examples/04b_standardmodel_construction.jl | 2 +- src/types/Reaction.jl | 4 ++-- test/reconstruction/ObjectModel.jl | 6 +++--- test/reconstruction/gapfill_minimum_reactions.jl | 2 +- test/types/ObjectModel.jl | 2 +- test/types/Reaction.jl | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index a7b9f49ca..73d9860d3 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -37,7 +37,7 @@ add_metabolites!(model, metabolite_list) r_m1 = Reaction("EX_m1", Dict("m1" => -1.0), :bidirectional) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) r1 = Reaction("r1", Dict("m1" => -1.0, "m2" => 1.0), :forward) r1.gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])] # add some gene reaction rules -r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :reverse) +r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :backward) r3 = Reaction("r3", Dict("m2" => -1.0, "m3" => 1.0), :bidirectional) r4 = Reaction("r3", Dict("m2" => -1.0, "m4" => 1.0), :forward) r_m3 = Reaction("r3", Dict("m3" => -1.0), :bidirectional) diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index ad1ecfac7..96107855f 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -34,7 +34,7 @@ $(TYPEDSIGNATURES) Convenience constructor for `Reaction`. The reaction equation is specified using `metabolites`, which is a dictionary mapping metabolite ids to stoichiometric coefficients. The direcion of the reaction is set through `dir` which can take -`:bidirectional`, `:forward`, and `:reverse` as values. Finally, the +`:bidirectional`, `:forward`, and `:backward` as values. Finally, the `default_bound` is the value taken to mean infinity in the context of constraint based models, often this is set to a very high flux value like 1000. """ @@ -47,7 +47,7 @@ function Reaction( if dir == :forward lower_bound = 0.0 upper_bound = default_bound - elseif dir == :reverse + elseif dir == :backward lower_bound = -default_bound upper_bound = 0.0 elseif dir == :bidirectional diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 6b8db53e6..edd9b94e4 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -20,9 +20,9 @@ r1 = Reaction("r1", Dict(m1.id => -1.0, m2.id => 1.0), :forward) r2 = Reaction("r2", Dict(m2.id => -2.0, m3.id => 1.0), :bidirectional) r2.gene_associations = [Isozyme(x) for x in [["g2"], ["g1", "g3"]]] - r3 = Reaction("r3", Dict(m1.id => -1.0, m4.id => 2.0), :reverse) - r4 = Reaction("r4", Dict(m1.id => -5.0, m4.id => 2.0), :reverse) - r5 = Reaction("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0), :reverse) + r3 = Reaction("r3", Dict(m1.id => -1.0, m4.id => 2.0), :backward) + r4 = Reaction("r4", Dict(m1.id => -5.0, m4.id => 2.0), :backward) + r5 = Reaction("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0), :backward) rxns = [r1, r2] diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 5a5b45f13..921df6bbd 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -43,7 +43,7 @@ rA = Reaction("rA", Dict("m1" => -1, "m2" => 1, "m3" => 1), :forward) rB = Reaction("rB", Dict("m2" => -1, "m9" => 1), :forward) rC = Reaction("rC", Dict("m9" => -1, "m10" => 1), :bidirectional) - rD = Reaction("rC", Dict("m10" => -1), :reverse) + rD = Reaction("rC", Dict("m10" => -1), :backward) universal_reactions = [r5, r7, r10, rA, rB, rC, rD] diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 612d4885e..f1e087a87 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -29,7 +29,7 @@ r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) + r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :backward) r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 516571b85..10c536a83 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -37,7 +37,7 @@ ), ) - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :reverse) + r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :backward) @test r2.lower_bound == -1000.0 && r2.upper_bound == 0.0 r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) From 746d1aa19eaba9ca759a678ee66d705213792f91 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 9 Jan 2023 23:21:09 +0100 Subject: [PATCH 079/531] sed smoment --- docs/src/concepts/4_wrappers.md | 16 +++-- ...jl => 14_simplified_enzyme_constrained.jl} | 33 ++++++---- docs/src/examples/15_enzyme_constrained.jl | 4 +- src/analysis.jl | 2 +- src/reconstruction/modifications/enzymes.jl | 7 +- ...nt.jl => simplified_enzyme_constrained.jl} | 19 +++--- .../misc/simplified_enzyme_constrained.jl | 24 +++++++ src/types/misc/smoment.jl | 23 ------- src/types/wrappers/SMomentModel.jl | 64 ++++++++++--------- src/utils/enzyme_constrained.jl | 21 +++--- ...nt.jl => simplified_enzyme_constrained.jl} | 10 +-- 11 files changed, 120 insertions(+), 103 deletions(-) rename docs/src/examples/{14_smoment.jl => 14_simplified_enzyme_constrained.jl} (74%) rename src/reconstruction/{smoment.jl => simplified_enzyme_constrained.jl} (77%) create mode 100644 src/types/misc/simplified_enzyme_constrained.jl delete mode 100644 src/types/misc/smoment.jl rename test/reconstruction/{smoment.jl => simplified_enzyme_constrained.jl} (83%) diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index 425c11c87..e59e600da 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -5,13 +5,15 @@ To simplify doing (and undoing) simple modifications to the existing model structure, COBREXA.jl supports a class of model _wrappers_, which are basically small layers that add or change the functionality of a given base models. -Types [`Serialized`](@ref), [`MatrixCoupling`](@ref), [`SMomentModel`](@ref), and -[`EnzymeConstrainedModel`](@ref) all work in this manner -- add some extra functionality to -the "base". Technically, they are all subtypes of the abstract type -[`AbstractModelWrapper`](@ref), which itself is a subtype of [`AbstractMetabolicModel`](@ref) -and can thus be used in all standard analysis functions. Similarly, the model -wraps can be stacked -- it is easy to e.g. serialize a [`EnzymeConstrainedModel`](@ref), or -to add coupling to an existing [`SMomentModel`](@ref). +Types [`Serialized`](@ref), [`MatrixCoupling`](@ref), +[`SimplifiedEnzymeConstrainedModel`](@ref), and +[`EnzymeConstrainedModel`](@ref) all work in this manner -- add some extra +functionality to the "base". Technically, they are all subtypes of the abstract +type [`AbstractModelWrapper`](@ref), which itself is a subtype of +[`AbstractMetabolicModel`](@ref) and can thus be used in all standard analysis +functions. Similarly, the model wraps can be stacked -- it is easy to e.g. +serialize a [`EnzymeConstrainedModel`](@ref), or to add coupling to an existing +[`SimplifiedEnzymeConstrainedModel`](@ref). As the main benefit of the approach, creating model variants using the wrapper approach is usually more efficient than recomputing the models in place. The diff --git a/docs/src/examples/14_smoment.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl similarity index 74% rename from docs/src/examples/14_smoment.jl rename to docs/src/examples/14_simplified_enzyme_constrained.jl index d0e943151..ed5ddc56b 100644 --- a/docs/src/examples/14_smoment.jl +++ b/docs/src/examples/14_simplified_enzyme_constrained.jl @@ -59,32 +59,35 @@ rxn_isozymes = Dict( # Once the data is gathered, we create a model that wraps the original model # with additional sMOMENT structure: -smoment_model = - model |> with_smoment( +simplified_enzyme_constrained_model = + model |> with_simplified_enzyme_constrained( reaction_isozyme = rxn_isozymes, gene_product_molar_mass = gene_product_masses, total_enzyme_capacity = 50.0, ) -# (You could alternatively use the [`make_smoment_model`](@ref) to create the -# structure more manually; but [`with_smoment`](@ref) is easier to use e.g. +# (You could alternatively use the [`make_simplified_enzyme_constrained_model`](@ref) to create the +# structure more manually; but [`with_simplified_enzyme_constrained`](@ref) is easier to use e.g. # with [`screen`](@ref).) # In turn, you should have a complete model with unidirectional reactions and # additional coupling, as specified by the sMOMENT method: -[stoichiometry(smoment_model); coupling(smoment_model)] +[ + stoichiometry(simplified_enzyme_constrained_model) + coupling(simplified_enzyme_constrained_model) +] -# the type (SMomentModel) is a model wrapper -- it is a thin additional layer +# the type (SimplifiedEnzymeConstrainedModel) is a model wrapper -- it is a thin additional layer # that just adds the necessary sMOMENT-relevant information atop the original # model, which is unmodified. That makes the process very efficient and # suitable for large-scale data processing. You can still access the original -# "base" model hidden in the SMomentModel using [`unwrap_model`](@ref). +# "base" model hidden in the SimplifiedEnzymeConstrainedModel using [`unwrap_model`](@ref). -# Other than that, the [`SMomentModel`](@ref) is a model type like any other, +# Other than that, the [`SimplifiedEnzymeConstrainedModel`](@ref) is a model type like any other, # and you can run any analysis you want on it, such as FBA: -flux_balance_analysis_dict(smoment_model, GLPK.Optimizer) +flux_balance_analysis_dict(simplified_enzyme_constrained_model, GLPK.Optimizer) # (Notice that the total reaction fluxes are reported despite the fact that # reactions are indeed split in the model! The underlying mechanism is provided @@ -92,14 +95,18 @@ flux_balance_analysis_dict(smoment_model, GLPK.Optimizer) # [Variability](06_fva.md) of the sMOMENT model can be explored as such: -flux_variability_analysis(smoment_model, GLPK.Optimizer, bounds = gamma_bounds(0.95)) +flux_variability_analysis( + simplified_enzyme_constrained_model, + GLPK.Optimizer, + bounds = gamma_bounds(0.95), +) # ...and a sMOMENT model sample can be obtained [as usual with # sampling](16_hit_and_run.md): ( affine_hit_and_run( - smoment_model, - warmup_from_variability(smoment_model, GLPK.Optimizer), - )' * reaction_variables(smoment_model) + simplified_enzyme_constrained_model, + warmup_from_variability(simplified_enzyme_constrained_model, GLPK.Optimizer), + )' * reaction_variables(simplified_enzyme_constrained_model) ) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index ec9955b66..d528a9ace 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -10,13 +10,13 @@ # 2017](https://doi.org/10.15252/msb.20167411). # # The analysis method and implementation in COBREXA is similar to -# [sMOMENT](14_smoment.md), but GECKO is able to process and represent much +# [sMOMENT](14_simplified_enzyme_constrained.md), but GECKO is able to process and represent much # larger scale of the constraints -- mainly, it supports multiple isozymes for # each reaction, and the isozymes can be grouped into "enzyme mass groups" to # simplify interpretation of data from proteomics. # For demonstration, we will generate artificial random data in a way similar -# to the [sMOMENT example](14_smoment.md): +# to the [sMOMENT example](14_simplified_enzyme_constrained.md): !isfile("e_coli_core.json") && download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") diff --git a/src/analysis.jl b/src/analysis.jl index cc65b9e96..5e9fbd0f4 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -24,7 +24,7 @@ using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types -using ..Types: _EnzymeConstrainedReactionColumn, _SMomentColumn +using ..Types: _EnzymeConstrainedReactionColumn, _SimplifiedEnzymeConstrainedColumn using Distributed using DistributedData diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index 6b6660f96..c2a89e51b 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -2,10 +2,11 @@ $(TYPEDSIGNATURES) Specifies a model variant which adds extra semantics of the sMOMENT algorithm, -giving a [`SMomentModel`](@ref). The arguments are forwarded to -[`make_smoment_model`](@ref). Intended for usage with [`screen`](@ref). +giving a [`SimplifiedEnzymeConstrainedModel`](@ref). The arguments are forwarded to +[`make_simplified_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_smoment(; kwargs...) = model -> make_smoment_model(model; kwargs...) +with_simplified_enzyme_constrained(; kwargs...) = + model -> make_simplified_enzyme_constrained_model(model; kwargs...) """ $(TYPEDSIGNATURES) diff --git a/src/reconstruction/smoment.jl b/src/reconstruction/simplified_enzyme_constrained.jl similarity index 77% rename from src/reconstruction/smoment.jl rename to src/reconstruction/simplified_enzyme_constrained.jl index ceccd1e9c..c1c244fee 100644 --- a/src/reconstruction/smoment.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -3,14 +3,14 @@ $(TYPEDSIGNATURES) Construct a model with a structure given by sMOMENT algorithm; returns a -[`SMomentModel`](@ref) (see the documentation for details). +[`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). # Arguments - `reaction_isozyme` parameter is a function that returns a single [`Isozyme`](@ref) for each reaction, or `nothing` if the reaction is not enzymatic. If the reaction has multiple isozymes, use - [`smoment_isozyme_speed`](@ref) to select the fastest one, as recommended by + [`simplified_enzyme_constrained_isozyme_speed`](@ref) to select the fastest one, as recommended by the sMOMENT paper. - `gene_product_molar_mass` parameter is a function that returns a molar mass of each gene product as specified by sMOMENT. @@ -19,7 +19,7 @@ Construct a model with a structure given by sMOMENT algorithm; returns a Alternatively, all function arguments also accept dictionaries that are used to provide the same data lookup. """ -function make_smoment_model( +function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; reaction_isozyme::Union{Function,Dict{String,Isozyme}}, gene_product_molar_mass::Union{Function,Dict{String,Float64}}, @@ -32,7 +32,7 @@ function make_smoment_model( gene_product_molar_mass isa Function ? gene_product_molar_mass : (gid -> gene_product_molar_mass[gid]) - columns = Vector{Types._SMomentColumn}() + columns = Vector{Types._SimplifiedEnzymeConstrainedColumn}() (lbs, ubs) = bounds(model) rids = variables(model) @@ -41,7 +41,10 @@ function make_smoment_model( isozyme = ris_(rids[i]) if isnothing(isozyme) # non-enzymatic reaction (or a totally ignored one) - push!(columns, Types._SMomentColumn(i, 0, lbs[i], ubs[i], 0)) + push!( + columns, + Types._SimplifiedEnzymeConstrainedColumn(i, 0, lbs[i], ubs[i], 0), + ) continue end @@ -51,7 +54,7 @@ function make_smoment_model( # reaction can run in reverse push!( columns, - Types._SMomentColumn( + Types._SimplifiedEnzymeConstrainedColumn( i, -1, max(-ubs[i], 0), @@ -65,7 +68,7 @@ function make_smoment_model( # reaction can run forward push!( columns, - Types._SMomentColumn( + Types._SimplifiedEnzymeConstrainedColumn( i, 1, max(lbs[i], 0), @@ -76,5 +79,5 @@ function make_smoment_model( end end - return SMomentModel(columns, total_enzyme_capacity, model) + return SimplifiedEnzymeConstrainedModel(columns, total_enzyme_capacity, model) end diff --git a/src/types/misc/simplified_enzyme_constrained.jl b/src/types/misc/simplified_enzyme_constrained.jl new file mode 100644 index 000000000..6d5711be0 --- /dev/null +++ b/src/types/misc/simplified_enzyme_constrained.jl @@ -0,0 +1,24 @@ + +""" +$(TYPEDSIGNATURES) + +Internal helper for systematically naming reactions in [`SimplifiedEnzymeConstrainedModel`](@ref). +""" +simplified_enzyme_constrained_reaction_name(original_name::String, direction::Int) = + direction == 0 ? original_name : + direction > 0 ? "$original_name#forward" : "$original_name#reverse" + +""" +$(TYPEDSIGNATURES) + +Retrieve a utility mapping between reactions and split reactions; rows +correspond to "original" reactions, columns correspond to "split" reactions. +""" +simplified_enzyme_constrained_column_reactions(model::SimplifiedEnzymeConstrainedModel) = + sparse( + [col.reaction_idx for col in model.columns], + 1:length(model.columns), + [col.direction >= 0 ? 1 : -1 for col in model.columns], + n_variables(model.inner), + length(model.columns), + ) diff --git a/src/types/misc/smoment.jl b/src/types/misc/smoment.jl deleted file mode 100644 index 6146b4ef5..000000000 --- a/src/types/misc/smoment.jl +++ /dev/null @@ -1,23 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`SMomentModel`](@ref). -""" -smoment_reaction_name(original_name::String, direction::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward" : "$original_name#reverse" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -smoment_column_reactions(model::SMomentModel) = sparse( - [col.reaction_idx for col in model.columns], - 1:length(model.columns), - [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_variables(model.inner), - length(model.columns), -) diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SMomentModel.jl index b62ebcfd2..684d6cc18 100644 --- a/src/types/wrappers/SMomentModel.jl +++ b/src/types/wrappers/SMomentModel.jl @@ -2,12 +2,12 @@ """ $(TYPEDEF) -A helper type that describes the contents of [`SMomentModel`](@ref)s. +A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. # Fields $(TYPEDFIELDS) """ -struct _SMomentColumn +struct _SimplifiedEnzymeConstrainedColumn reaction_idx::Int # number of the corresponding reaction in the inner model direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) @@ -22,7 +22,7 @@ An enzyme-capacity-constrained model using sMOMENT algorithm, as described by *Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of metabolic models with enzyme constraints" BMC bioinformatics, 2020*. -Use [`make_smoment_model`](@ref) or [`with_smoment`](@ref) to construct the +Use [`make_simplified_enzyme_constrained_model`](@ref) or [`with_simplified_enzyme_constrained`](@ref) to construct the models. The model is constructed as follows: @@ -35,7 +35,7 @@ The model is constructed as follows: - the total consumption of the enzyme capacity is constrained to a fixed maximum. -The `SMomentModel` structure contains a worked-out representation of the +The `SimplifiedEnzymeConstrainedModel` structure contains a worked-out representation of the optimization problem atop a wrapped [`AbstractMetabolicModel`](@ref), in particular the separation of certain reactions into unidirectional forward and reverse parts, an "enzyme capacity" required for each reaction, and the value of the maximum @@ -54,99 +54,101 @@ are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). # Fields $(TYPEDFIELDS) """ -struct SMomentModel <: AbstractModelWrapper - columns::Vector{_SMomentColumn} +struct SimplifiedEnzymeConstrainedModel <: AbstractModelWrapper + columns::Vector{_SimplifiedEnzymeConstrainedColumn} total_enzyme_capacity::Float64 inner::AbstractMetabolicModel end -Accessors.unwrap_model(model::SMomentModel) = model.inner +Accessors.unwrap_model(model::SimplifiedEnzymeConstrainedModel) = model.inner """ $(TYPEDSIGNATURES) -Return a stoichiometry of the [`SMomentModel`](@ref). The enzymatic reactions +Return a stoichiometry of the [`SimplifiedEnzymeConstrainedModel`](@ref). The enzymatic reactions are split into unidirectional forward and reverse ones. """ -Accessors.stoichiometry(model::SMomentModel) = - stoichiometry(model.inner) * smoment_column_reactions(model) +Accessors.stoichiometry(model::SimplifiedEnzymeConstrainedModel) = + stoichiometry(model.inner) * simplified_enzyme_constrained_column_reactions(model) """ $(TYPEDSIGNATURES) -Reconstruct an objective of the [`SMomentModel`](@ref). +Reconstruct an objective of the [`SimplifiedEnzymeConstrainedModel`](@ref). """ -Accessors.objective(model::SMomentModel) = - smoment_column_reactions(model)' * objective(model.inner) +Accessors.objective(model::SimplifiedEnzymeConstrainedModel) = + simplified_enzyme_constrained_column_reactions(model)' * objective(model.inner) """ $(TYPEDSIGNATURES) -Returns the internal reactions in a [`SMomentModel`](@ref) (these may be split +Returns the internal reactions in a [`SimplifiedEnzymeConstrainedModel`](@ref) (these may be split to forward- and reverse-only parts; reactions IDs are mangled accordingly with suffixes). """ -Accessors.variables(model::SMomentModel) = +Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = let inner_reactions = variables(model.inner) [ - smoment_reaction_name(inner_reactions[col.reaction_idx], col.direction) for - col in model.columns + simplified_enzyme_constrained_reaction_name( + inner_reactions[col.reaction_idx], + col.direction, + ) for col in model.columns ] end """ $(TYPEDSIGNATURES) -The number of reactions (including split ones) in [`SMomentModel`](@ref). +The number of reactions (including split ones) in [`SimplifiedEnzymeConstrainedModel`](@ref). """ -Accessors.n_variables(model::SMomentModel) = length(model.columns) +Accessors.n_variables(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) """ $(TYPEDSIGNATURES) -Return the variable bounds for [`SMomentModel`](@ref). +Return the variable bounds for [`SimplifiedEnzymeConstrainedModel`](@ref). """ -Accessors.bounds(model::SMomentModel) = +Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) """ $(TYPEDSIGNATURES) -Get the mapping of the reaction rates in [`SMomentModel`](@ref) to the original +Get the mapping of the reaction rates in [`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the wrapped model. """ -Accessors.reaction_variables(model::SMomentModel) = - smoment_column_reactions(model)' * reaction_variables(model.inner) +Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = + simplified_enzyme_constrained_column_reactions(model)' * reaction_variables(model.inner) """ $(TYPEDSIGNATURES) -Return the coupling of [`SMomentModel`](@ref). That combines the coupling of +Return the coupling of [`SimplifiedEnzymeConstrainedModel`](@ref). That combines the coupling of the wrapped model, coupling for split reactions, and the coupling for the total enzyme capacity. """ -Accessors.coupling(model::SMomentModel) = vcat( - coupling(model.inner) * smoment_column_reactions(model), +Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) = vcat( + coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model), [col.capacity_required for col in model.columns]', ) """ $(TYPEDSIGNATURES) -Count the coupling constraints in [`SMomentModel`](@ref) (refer to +Count the coupling constraints in [`SimplifiedEnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for details). """ -Accessors.n_coupling_constraints(model::SMomentModel) = +Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = n_coupling_constraints(model.inner) + 1 """ $(TYPEDSIGNATURES) -The coupling bounds for [`SMomentModel`](@ref) (refer to [`coupling`](@ref) for +The coupling bounds for [`SimplifiedEnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for details). """ -Accessors.coupling_bounds(model::SMomentModel) = +Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = let (iclb, icub) = coupling_bounds(model.inner) (vcat(iclb, [0.0]), vcat(icub, [model.total_enzyme_capacity])) end diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index 906baf830..0568a393a 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -42,9 +42,9 @@ gene_product_mass_group_dict(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Extract the total mass utilization in a solved [`SMomentModel`](@ref). +Extract the total mass utilization in a solved [`SimplifiedEnzymeConstrainedModel`](@ref). """ -gene_product_mass(model::SMomentModel, opt_model) = +gene_product_mass(model::SimplifiedEnzymeConstrainedModel, opt_model) = is_solved(opt_model) ? sum((col.capacity_required for col in model.columns) .* value.(opt_model[:x])) : nothing @@ -53,13 +53,14 @@ $(TYPEDSIGNATURES) A pipe-able variant of [`gene_product_mass`](@ref). """ -gene_product_mass(model::SMomentModel) = x -> gene_product_mass(model, x) +gene_product_mass(model::SimplifiedEnzymeConstrainedModel) = + x -> gene_product_mass(model, x) """ $(TYPEDSIGNATURES) Compute a "score" for picking the most viable isozyme for -[`make_smoment_model`](@ref), based on maximum kcat divided by relative mass of +[`make_simplified_enzyme_constrained_model`](@ref), based on maximum kcat divided by relative mass of the isozyme. This is used because sMOMENT algorithm can not handle multiple isozymes for one reaction. @@ -67,24 +68,24 @@ isozymes for one reaction. This function does not take the direction of the reaction into account, i.e. the maximum forward or reverse turnover number is used internally. """ -smoment_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = +simplified_enzyme_constrained_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = max(isozyme.kcat_forward, isozyme.kcat_backward) / sum(count * gene_product_molar_mass(gene) for (gene, count) in isozyme.stoichiometry) """ $(TYPEDSIGNATURES) -A piping- and argmax-friendly overload of [`smoment_isozyme_speed`](@ref). +A piping- and argmax-friendly overload of [`simplified_enzyme_constrained_isozyme_speed`](@ref). # Example ``` gene_mass_function = gid -> 1.234 -best_isozyme_for_smoment = argmax( - smoment_isozyme_speed(gene_mass_function), +best_isozyme_for_simplified_enzyme_constrained = argmax( + simplified_enzyme_constrained_isozyme_speed(gene_mass_function), my_isozyme_vector, ) ``` """ -smoment_isozyme_speed(gene_product_molar_mass::Function) = - isozyme -> smoment_isozyme_speed(isozyme, gene_product_molar_mass) +simplified_enzyme_constrained_isozyme_speed(gene_product_molar_mass::Function) = + isozyme -> simplified_enzyme_constrained_isozyme_speed(isozyme, gene_product_molar_mass) diff --git a/test/reconstruction/smoment.jl b/test/reconstruction/simplified_enzyme_constrained.jl similarity index 83% rename from test/reconstruction/smoment.jl rename to test/reconstruction/simplified_enzyme_constrained.jl index 809b5aab5..7e16e9f7c 100644 --- a/test/reconstruction/smoment.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -7,7 +7,7 @@ rid -> haskey(ecoli_core_reaction_kcats, rid) ? argmax( - smoment_isozyme_speed(get_gene_product_mass), + simplified_enzyme_constrained_isozyme_speed(get_gene_product_mass), Isozyme( stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], @@ -15,22 +15,22 @@ ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) ) : nothing - smoment_model = + simplified_enzyme_constrained_model = model |> with_changed_bounds( ["EX_glc__D_e", "GLCpts"], lower = [-1000.0, -1.0], upper = [nothing, 12.0], ) |> - with_smoment( + with_simplified_enzyme_constrained( reaction_isozyme = get_reaction_isozyme, gene_product_molar_mass = get_gene_product_mass, total_enzyme_capacity = 100.0, ) - objective(smoment_model) + objective(simplified_enzyme_constrained_model) rxn_fluxes = flux_balance_analysis_dict( - smoment_model, + simplified_enzyme_constrained_model, Tulip.Optimizer; modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) From d4d44c5b14dac38c2b1284faecccec07ee7dd8ea Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 9 Jan 2023 23:28:45 +0100 Subject: [PATCH 080/531] sed reaction_gene_association( --- docs/src/examples/07_gene_deletion.jl | 2 +- docs/src/examples/14_simplified_enzyme_constrained.jl | 4 ++-- docs/src/examples/15_enzyme_constrained.jl | 4 ++-- src/analysis/modifications/knockout.jl | 2 +- src/reconstruction/ObjectModel.jl | 2 +- src/types/accessors/AbstractMetabolicModel.jl | 2 +- src/types/models/JSONModel.jl | 4 ++-- src/types/models/MATModel.jl | 2 +- src/types/models/MatrixModel.jl | 6 +++--- src/types/models/ObjectModel.jl | 4 ++-- src/types/models/SBMLModel.jl | 4 ++-- test/reconstruction/enzyme_constrained.jl | 4 ++-- test/reconstruction/simplified_enzyme_constrained.jl | 2 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- test/types/MatrixCoupling.jl | 4 ++-- test/types/MatrixModel.jl | 4 ++-- test/types/ObjectModel.jl | 4 ++-- 17 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/src/examples/07_gene_deletion.jl b/docs/src/examples/07_gene_deletion.jl index 6362ea7f9..0844ecd0a 100644 --- a/docs/src/examples/07_gene_deletion.jl +++ b/docs/src/examples/07_gene_deletion.jl @@ -43,7 +43,7 @@ variability_with_knockout = # knockout modification. This knocks out all genes that can run the FBA # reaction: -reaction_gene_association(model, "R_FBA") +reaction_gene_associations(model, "R_FBA") # flux_with_double_knockout = flux_balance_analysis_dict( model, diff --git a/docs/src/examples/14_simplified_enzyme_constrained.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl index ed5ddc56b..a008c7a1b 100644 --- a/docs/src/examples/14_simplified_enzyme_constrained.jl +++ b/docs/src/examples/14_simplified_enzyme_constrained.jl @@ -36,7 +36,7 @@ rxns = filter( x -> !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_association(model, x)), + !isnothing(reaction_gene_associations(model, x)), variables(model), ) @@ -47,7 +47,7 @@ rxns = filter( rxn_isozymes = Dict( rxn => Isozyme( - Dict(vcat(reaction_gene_association(model, rxn)...) .=> 1), + Dict(vcat(reaction_gene_associations(model, rxn)...) .=> 1), randn() * 100 + 600, #forward kcat randn() * 100 + 500, #reverse kcat ) for rxn in rxns diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index d528a9ace..a670080ba 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -34,7 +34,7 @@ rxns = filter( x -> !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_association(model, x)), + !isnothing(reaction_gene_associations(model, x)), variables(model), ) @@ -47,7 +47,7 @@ rxn_isozymes = Dict( Dict(isozyme_genes .=> 1), randn() * 100 + 600, #forward kcat randn() * 100 + 500, #reverse kcat - ) for isozyme_genes in reaction_gene_association(model, rxn) + ) for isozyme_genes in reaction_gene_associations(model, rxn) ] for rxn in rxns ) diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index 36e74e273..fc130ca2d 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -35,7 +35,7 @@ other models. """ function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) for (rxn_num, rxn_id) in enumerate(variables(model)) - rga = reaction_gene_association(model, rxn_id) + rga = reaction_gene_associations(model, rxn_id) if !isnothing(rga) && all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) set_optmodel_bound!(rxn_num, opt_model, lower = 0, upper = 0) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 8ac234bb4..d1fdf627d 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -76,7 +76,7 @@ function remove_genes!( for (rid, r) in model.reactions if !isnothing(r.gene_associations) && all( any(in.(gids, Ref(conjunction))) for - conjunction in reaction_gene_association(model, rid) + conjunction in reaction_gene_associations(model, rid) ) push!(rm_reactions, rid) end diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 04147cbd6..cdf168032 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -195,7 +195,7 @@ Returns the sets of genes that need to be present so that the reaction can work For simplicity, `nothing` may be returned, meaning that the reaction always takes place. (in DNF, that would be equivalent to returning `[[]]`.) """ -function reaction_gene_association( +function reaction_gene_associations( a::AbstractMetabolicModel, reaction_id::String, )::Maybe{GeneAssociationsDNF} diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 917fde4e3..e1b938234 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -165,7 +165,7 @@ $(TYPEDSIGNATURES) Parses the `.gene_reaction_rule` from reactions. """ -Accessors.reaction_gene_association(model::JSONModel, rid::String) = maybemap( +Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( parse_grr, get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), ) @@ -338,7 +338,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) res["annotation"] = reaction_annotations(mm, rid) res["notes"] = reaction_notes(mm, rid) - grr = reaction_gene_association(mm, rid) + grr = reaction_gene_associations(mm, rid) if !isnothing(grr) res["gene_reaction_rule"] = unparse_grr(String, grr) end diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 0bcdb4dd6..3549b3d8c 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -131,7 +131,7 @@ $(TYPEDSIGNATURES) Extracts the associations from `grRules` key, if present. """ -function Accessors.reaction_gene_association(m::MATModel, rid::String) +function Accessors.reaction_gene_associations(m::MATModel, rid::String) if haskey(m.mat, "grRules") grr = m.mat["grRules"][findfirst(==(rid), variables(m))] typeof(grr) == String ? parse_grr(grr) : nothing diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 20387de3f..c8618a531 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -131,7 +131,7 @@ $(TYPEDSIGNATURES) Retrieve the gene reaction associations from [`MatrixModel`](@ref) by reaction index. """ -Accessors.reaction_gene_association( +Accessors.reaction_gene_associations( model::MatrixModel, ridx::Int, )::Maybe{GeneAssociationsDNF} = model.grrs[ridx] @@ -141,7 +141,7 @@ $(TYPEDSIGNATURES) Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID. """ -Accessors.reaction_gene_association( +Accessors.reaction_gene_associations( model::MatrixModel, rid::String, )::Maybe{GeneAssociationsDNF} = model.grrs[first(indexin([rid], model.rxns))] @@ -166,7 +166,7 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode variables(m), metabolites(m), Vector{Maybe{GeneAssociationsDNF}}([ - reaction_gene_association(m, id) for id in variables(m) + reaction_gene_associations(m, id) for id in variables(m) ]), ) end diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 46735220c..8f59b278a 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -168,7 +168,7 @@ $(TYPEDSIGNATURES) Return the gene reaction rule in string format for reaction with `id` in `model`. Return `nothing` if not available. """ -function Accessors.reaction_gene_association( +function Accessors.reaction_gene_associations( model::ObjectModel, id::String, )::Maybe{GeneAssociationsDNF} @@ -345,7 +345,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) for (j, stoich) in zip(findnz(S[:, i])...) rmets[metids[j]] = stoich end - rgas = reaction_gene_association(model, rid) + rgas = reaction_gene_associations(model, rid) modelreactions[rid] = Reaction( rid; name = reaction_name(model, rid), diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 3ed316537..88b271369 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -109,7 +109,7 @@ $(TYPEDSIGNATURES) Retrieve the reaction gene associations from [`SBMLModel`](@ref). """ -Accessors.reaction_gene_association( +Accessors.reaction_gene_associations( model::SBMLModel, rid::String, )::Maybe{GeneAssociationsDNF} = @@ -344,7 +344,7 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) upper_bound = "UPPER_BOUND", gene_product_association = maybemap( x -> unparse_grr(SBML.GeneProductAssociation, x), - reaction_gene_association(mm, rid), + reaction_gene_associations(mm, rid), ), reversible = true, sbo = _sbml_export_sbo(reaction_annotations(mm, rid)), diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 4f86e9166..93bebfd55 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -9,7 +9,7 @@ stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) + ) for (i, grr) in enumerate(reaction_gene_associations(model, rid)) ) : nothing get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) @@ -126,7 +126,7 @@ end m; reaction_isozymes = Dict( rid => r.gene_associations for (rid, r) in m.reactions if - !isnothing(reaction_gene_association(m, rid)) && rid in ["r3", "r4", "r5"] + !isnothing(reaction_gene_associations(m, rid)) && rid in ["r3", "r4", "r5"] ), gene_product_bounds, gene_product_molar_mass, diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 7e16e9f7c..60516970d 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -12,7 +12,7 @@ stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_association(model, rid)) + ) for (i, grr) in enumerate(reaction_gene_associations(model, rid)) ) : nothing simplified_enzyme_constrained_model = diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index ad7e5c0b7..8dba92ca6 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -280,7 +280,7 @@ end stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_association(ecoli, rid)) + ) for (i, grr) in enumerate(reaction_gene_associations(ecoli, rid)) ) : nothing get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) diff --git a/test/types/MatrixCoupling.jl b/test/types/MatrixCoupling.jl index 99fc355f2..c2b816fd8 100644 --- a/test/types/MatrixCoupling.jl +++ b/test/types/MatrixCoupling.jl @@ -15,6 +15,6 @@ end @test Set(variables(cm)) == Set(variables(sm)) @test Set(variables(cm)) == Set(variables(cm2)) - @test reaction_gene_association(sm, variables(sm)[1]) == - reaction_gene_association(cm, variables(sm)[1]) + @test reaction_gene_associations(sm, variables(sm)[1]) == + reaction_gene_associations(cm, variables(sm)[1]) end diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index a9c4f220c..22e9c7ed8 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -14,6 +14,6 @@ end @test Set(variables(cm)) == Set(variables(sm)) @test Set(variables(cm)) == Set(variables(cm2)) - @test reaction_gene_association(sm, variables(sm)[1]) == - reaction_gene_association(cm, variables(sm)[1]) + @test reaction_gene_associations(sm, variables(sm)[1]) == + reaction_gene_associations(cm, variables(sm)[1]) end diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index f1e087a87..67b678645 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -90,12 +90,12 @@ Ref( COBREXA.Internal.unparse_grr( String, - reaction_gene_association(model, "r1"), + reaction_gene_associations(model, "r1"), ), ), ), ) - @test isnothing(reaction_gene_association(model, "r2")) + @test isnothing(reaction_gene_associations(model, "r2")) @test metabolite_formula(model, "m2")["C"] == 2 @test isnothing(metabolite_formula(model, "m3")) From 676df266047201aac4da437a1c4b1c409d34ef68 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 10 Jan 2023 09:10:20 +0100 Subject: [PATCH 081/531] finish --- docs/src/examples/03b_accessors.jl | 2 +- src/types/accessors/ModelWrapper.jl | 2 +- src/types/models/BalancedGrowthCommunityModel.jl | 2 +- src/types/models/MATModel.jl | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl index afec61973..7a011f596 100644 --- a/docs/src/examples/03b_accessors.jl +++ b/docs/src/examples/03b_accessors.jl @@ -35,7 +35,7 @@ variables(std) # - [`bounds`](@ref) return lower and upper bounds of reaction rates # - [`metabolite_charge`](@ref) and [`metabolite_formula`](@ref) return details about metabolites # - [`objective`](@ref) returns the objective of the model (usually labeled as `c`) -# - [`reaction_gene_association`](@ref) describes the dependency of a reaction on gene products +# - [`reaction_gene_associations`](@ref) describes the dependency of a reaction on gene products # # A complete, up-to-date list of accessors can be always generated using `methodswith`: diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 38bb72cbd..d4add7ce1 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -16,7 +16,7 @@ end @inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! -@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_association reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes +@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes @inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index c2c6fb2d1..019608c49 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -264,7 +264,7 @@ will not have things like annotations etc. For this reason, these methods will only work if they access something inside the community members. =# for (func, def) in ( - (:reaction_gene_association, nothing), + (:reaction_gene_associations, nothing), (:reaction_subsystem, nothing), (:reaction_stoichiometry, nothing), (:metabolite_formula, nothing), diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 3549b3d8c..3c7c17ec2 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -252,7 +252,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) "", maybemap.( x -> unparse_grr(String, x), - reaction_gene_association.(Ref(m), variables(m)), + reaction_gene_associations.(Ref(m), variables(m)), ), ), "metFormulas" => From adc46dcae928792e5040f6c227f3e7f824cff088 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 10 Jan 2023 22:23:44 +0100 Subject: [PATCH 082/531] rename file --- .../{SMomentModel.jl => SimplifiedEnzymeConstrainedModel.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/types/wrappers/{SMomentModel.jl => SimplifiedEnzymeConstrainedModel.jl} (100%) diff --git a/src/types/wrappers/SMomentModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl similarity index 100% rename from src/types/wrappers/SMomentModel.jl rename to src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl From 0f1655becc8aa9da35632811a568bfe52635e5b7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 10 Jan 2023 23:39:06 +0100 Subject: [PATCH 083/531] change enzyme interface --- src/types/Gene.jl | 1 + src/types/GeneAssociations.jl | 2 -- src/types/Reaction.jl | 2 ++ src/types/accessors/AbstractMetabolicModel.jl | 30 ++++++++++++++++++ src/types/models/ObjectModel.jl | 31 +++++++++++++++++++ 5 files changed, 64 insertions(+), 2 deletions(-) diff --git a/src/types/Gene.jl b/src/types/Gene.jl index 04c7a9a29..b6bf847b8 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -10,6 +10,7 @@ Base.@kwdef mutable struct Gene id::String name::Maybe{String} = nothing sequence::Maybe{String} = nothing + protein_molar_mass::Maybe{Float64} = nothing notes::Notes = Notes() annotations::Annotations = Annotations() end diff --git a/src/types/GeneAssociations.jl b/src/types/GeneAssociations.jl index 3e88c9ef8..97d41a206 100644 --- a/src/types/GeneAssociations.jl +++ b/src/types/GeneAssociations.jl @@ -10,8 +10,6 @@ $(TYPEDFIELDS) Base.@kwdef mutable struct Isozyme stoichiometry::Dict{String,Float64} annotation::Annotations = Annotations() - kcat_forward::Maybe{Float64} = nothing - kcat_backward::Maybe{Float64} = nothing end """ diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 96107855f..1ef56c838 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -16,6 +16,8 @@ Base.@kwdef mutable struct Reaction upper_bound::Float64 = constants.default_reaction_bound gene_associations::Maybe{GeneAssociations} = nothing subsystem::Maybe{String} = nothing + kcat_forward::Maybe{Float64} = nothing + kcat_backward::Maybe{Float64} = nothing notes::Notes = Notes() annotations::Annotations = Annotations() end diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index cdf168032..aed972bfc 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -369,3 +369,33 @@ By default, it should be safe to do nothing. function precache!(a::AbstractMetabolicModel)::Nothing nothing end + +""" +$(TYPEDSIGNATURES) + +Return the molar mass of translated gene with ID `gid`. +""" +gene_protein_molar_mass(model::AbstractMetabolicModel, gid::String) = nothing + +""" +$(TYPEDSIGNATURES) + +Return the enzyme turnover number of reaction with ID `rid` for the reaction in +the forward direction. +""" +reaction_kcat_forward(model::AbstractMetabolicModel, rid::String) = nothing + +""" +$(TYPEDSIGNATURES) + +Return the enzyme turnover number of reaction with ID `rid` for the reaction in +the backward direction. +""" +reaction_kcat_backward(model::AbstractMetabolicModel, rid::String) = nothing + +""" +$(TYPEDSIGNATURES) + +Return all the [`Isozyme`](@ref)s associated with a `model` and reaction `rid`. +""" +reaction_isozymes(model::AbstractMetabolicModel, rid::String) = nothing diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 8f59b278a..e97b1a9fa 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -294,6 +294,37 @@ Return the name of gene with ID `id`. """ Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name +""" +$(TYPEDSIGNATURES) + +Return the molar mass of translated gene with ID `gid`. +""" +Accessors.gene_protein_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].protein_molar_mass + +""" +$(TYPEDSIGNATURES) + +Return the enzyme turnover number of reaction with ID `rid` for the reaction in +the forward direction. +""" +Accessors.reaction_kcat_forward(model::ObjectModel, rid::String) = model.reactions[rid].kcat_forward + +""" +$(TYPEDSIGNATURES) + +Return the enzyme turnover number of reaction with ID `rid` for the reaction in +the backward direction. +""" +Accessors.reaction_kcat_backward(model::ObjectModel, rid::String) = model.reactions[rid].kcat_backward + +""" +$(TYPEDSIGNATURES) + +Return the [`Isozyme`](@ref)s associated with the `model` and reaction `rid`. +""" +Accessors.reaction_kcat_backward(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations + + """ $(TYPEDSIGNATURES) From f59e5ebd9278a7dff47f36badf18847edd5ded87 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 00:25:21 +0100 Subject: [PATCH 084/531] Change SMoment to work with new accessors --- .../simplified_enzyme_constrained.jl | 54 ++++++++++--------- src/utils/enzyme_constrained.jl | 34 ------------ .../simplified_enzyme_constrained.jl | 30 +++++------ 3 files changed, 42 insertions(+), 76 deletions(-) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index c1c244fee..5d59b78cf 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -1,44 +1,46 @@ - """ $(TYPEDSIGNATURES) Construct a model with a structure given by sMOMENT algorithm; returns a [`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). -# Arguments - -- `reaction_isozyme` parameter is a function that returns a single - [`Isozyme`](@ref) for each reaction, or `nothing` if the reaction is not - enzymatic. If the reaction has multiple isozymes, use - [`simplified_enzyme_constrained_isozyme_speed`](@ref) to select the fastest one, as recommended by - the sMOMENT paper. -- `gene_product_molar_mass` parameter is a function that returns a molar mass - of each gene product as specified by sMOMENT. +# Arguments +- a `model` that implements the accessors `gene_protein_molar_mass`, + `reaction_isozymes`, `reaction_kcat_forward`, and `reaction_kcat_backward`. - `total_enzyme_capacity` is the maximum "enzyme capacity" in the model. -Alternatively, all function arguments also accept dictionaries that are used to -provide the same data lookup. +# Notes +The SMOMENT algorithm only uses one isozyme per reaction. If multiple isozymes +are present the "fastest" isozyme will be used. This is determined based on +maximum kcat (forward or backward) divided by mass of the isozyme. + +Reactions with no turnover number data, or non-enzymatic reactions that should +be ignored should have `nothing` in the `gene_associations` field of the +associated reaction. """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; - reaction_isozyme::Union{Function,Dict{String,Isozyme}}, - gene_product_molar_mass::Union{Function,Dict{String,Float64}}, total_enzyme_capacity::Float64, ) - ris_ = - reaction_isozyme isa Function ? reaction_isozyme : - (rid -> get(reaction_isozyme, rid, nothing)) - gpmm_ = - gene_product_molar_mass isa Function ? gene_product_molar_mass : - (gid -> gene_product_molar_mass[gid]) + # helper function to rank the isozymes by relative speed + speed_enzyme(model, rid, isozyme) = max(reaction_kcat_forward(model, rid), reaction_kcat_backward(model, rid)) / + sum(count * gene_protein_molar_mass(model, gid) for (gid, count) in isozyme.stoichiometry) + # helper function to return the fastest isozyme or nothing + ris_(model, rid) = begin + isozymes = reaction_isozymes(model, rid) + isnothing(isozymes) && return nothing + argmax(isozyme -> speed_enzyme(model, rid, isozyme), isozymes) + end + columns = Vector{Types._SimplifiedEnzymeConstrainedColumn}() (lbs, ubs) = bounds(model) rids = variables(model) for i = 1:n_variables(model) - isozyme = ris_(rids[i]) + isozyme = ris_(model, rids[i]) + if isnothing(isozyme) # non-enzymatic reaction (or a totally ignored one) push!( @@ -48,9 +50,9 @@ function make_simplified_enzyme_constrained_model( continue end - mw = sum(gpmm_(gid) * ps for (gid, ps) in isozyme.stoichiometry) + mw = sum(gene_protein_molar_mass(model,gid) * ps for (gid, ps) in isozyme.stoichiometry) - if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance + if min(lbs[i], ubs[i]) < 0 && reaction_kcat_backward(model, rids[i]) > constants.tolerance # reaction can run in reverse push!( columns, @@ -59,12 +61,12 @@ function make_simplified_enzyme_constrained_model( -1, max(-ubs[i], 0), -lbs[i], - mw / isozyme.kcat_backward, + mw / reaction_kcat_backward(model, rids[i]), ), ) end - if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > constants.tolerance + if max(lbs[i], ubs[i]) > 0 && reaction_kcat_forward(model, rids[i]) > constants.tolerance # reaction can run forward push!( columns, @@ -73,7 +75,7 @@ function make_simplified_enzyme_constrained_model( 1, max(lbs[i], 0), ubs[i], - mw / isozyme.kcat_forward, + mw / reaction_kcat_forward(model, rids[i]), ), ) end diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index 0568a393a..285b807d1 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -55,37 +55,3 @@ A pipe-able variant of [`gene_product_mass`](@ref). """ gene_product_mass(model::SimplifiedEnzymeConstrainedModel) = x -> gene_product_mass(model, x) - -""" -$(TYPEDSIGNATURES) - -Compute a "score" for picking the most viable isozyme for -[`make_simplified_enzyme_constrained_model`](@ref), based on maximum kcat divided by relative mass of -the isozyme. This is used because sMOMENT algorithm can not handle multiple -isozymes for one reaction. - -# Note -This function does not take the direction of the reaction into account, i.e. the -maximum forward or reverse turnover number is used internally. -""" -simplified_enzyme_constrained_isozyme_speed(isozyme::Isozyme, gene_product_molar_mass) = - max(isozyme.kcat_forward, isozyme.kcat_backward) / - sum(count * gene_product_molar_mass(gene) for (gene, count) in isozyme.stoichiometry) - -""" -$(TYPEDSIGNATURES) - -A piping- and argmax-friendly overload of [`simplified_enzyme_constrained_isozyme_speed`](@ref). - -# Example -``` -gene_mass_function = gid -> 1.234 - -best_isozyme_for_simplified_enzyme_constrained = argmax( - simplified_enzyme_constrained_isozyme_speed(gene_mass_function), - my_isozyme_vector, -) -``` -""" -simplified_enzyme_constrained_isozyme_speed(gene_product_molar_mass::Function) = - isozyme -> simplified_enzyme_constrained_isozyme_speed(isozyme, gene_product_molar_mass) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 60516970d..67c491e95 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -1,19 +1,19 @@ @testset "SMOMENT" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) - - get_reaction_isozyme = - rid -> - haskey(ecoli_core_reaction_kcats, rid) ? - argmax( - simplified_enzyme_constrained_isozyme_speed(get_gene_product_mass), - Isozyme( - stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_associations(model, rid)) - ) : nothing + for gid in genes(model) + model.genes[gid].protein_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) + end + for rid in reactions(model) + if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr + model.reactions[rid].kcat_forward = ecoli_core_reaction_kcats[rid][1][1] # all kcat forwards the same + model.reactions[rid].kcat_backward = ecoli_core_reaction_kcats[rid][1][2] # all kcat forwards the same + newisozymes = Isozyme[] + for (i, grr) in enumerate(reaction_gene_associations(model, rid)) + push!(newisozymes, Isozyme(stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]))) + end + model.reactions[rid].gene_associations = newisozymes + end + end simplified_enzyme_constrained_model = model |> @@ -23,8 +23,6 @@ upper = [nothing, 12.0], ) |> with_simplified_enzyme_constrained( - reaction_isozyme = get_reaction_isozyme, - gene_product_molar_mass = get_gene_product_mass, total_enzyme_capacity = 100.0, ) objective(simplified_enzyme_constrained_model) From 10771a80e8fd7225670372599482861ec750a172 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 00:43:56 +0100 Subject: [PATCH 085/531] more accessors --- src/types/Gene.jl | 2 +- src/types/GeneAssociations.jl | 8 +++--- src/types/accessors/AbstractMetabolicModel.jl | 20 ++------------- src/types/models/ObjectModel.jl | 25 +++---------------- 4 files changed, 12 insertions(+), 43 deletions(-) diff --git a/src/types/Gene.jl b/src/types/Gene.jl index b6bf847b8..ed8412d60 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -10,7 +10,7 @@ Base.@kwdef mutable struct Gene id::String name::Maybe{String} = nothing sequence::Maybe{String} = nothing - protein_molar_mass::Maybe{Float64} = nothing + product_molar_mass::Maybe{Float64} = nothing notes::Notes = Notes() annotations::Annotations = Annotations() end diff --git a/src/types/GeneAssociations.jl b/src/types/GeneAssociations.jl index 97d41a206..fffa5e53d 100644 --- a/src/types/GeneAssociations.jl +++ b/src/types/GeneAssociations.jl @@ -8,7 +8,9 @@ stoichiometry or turnover numbers. $(TYPEDFIELDS) """ Base.@kwdef mutable struct Isozyme - stoichiometry::Dict{String,Float64} + gene_product_stoichiometry::Dict{String,Float64} + kcat_forward::Maybe{Float64} = nothing + kcat_backward::Maybe{Float64} = nothing annotation::Annotations = Annotations() end @@ -17,10 +19,10 @@ $(TYPEDSIGNATURES) A convenience constructor for [`Isozyme`](@ref) that takes a string gene reaction rule and converts it into the appropriate format. Assumes the -stoichiometry for each subunit is 1. +`gene_product_stoichiometry` for each subunit is 1. """ Isozyme(gids::Vector{String}; kwargs...) = - Isozyme(; stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) + Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) """ const GeneAssociations diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index aed972bfc..74ef1f39f 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -375,27 +375,11 @@ $(TYPEDSIGNATURES) Return the molar mass of translated gene with ID `gid`. """ -gene_protein_molar_mass(model::AbstractMetabolicModel, gid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the enzyme turnover number of reaction with ID `rid` for the reaction in -the forward direction. -""" -reaction_kcat_forward(model::AbstractMetabolicModel, rid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the enzyme turnover number of reaction with ID `rid` for the reaction in -the backward direction. -""" -reaction_kcat_backward(model::AbstractMetabolicModel, rid::String) = nothing +gene_product_molar_mass(model::AbstractMetabolicModel, gid::String)::Maybe{Float64} = nothing """ $(TYPEDSIGNATURES) Return all the [`Isozyme`](@ref)s associated with a `model` and reaction `rid`. """ -reaction_isozymes(model::AbstractMetabolicModel, rid::String) = nothing +reaction_isozymes(model::AbstractMetabolicModel, rid::String)::Maybe{Vector{Isozyme}} = nothing diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index e97b1a9fa..c8d58cd29 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -173,7 +173,7 @@ function Accessors.reaction_gene_associations( id::String, )::Maybe{GeneAssociationsDNF} isnothing(model.reactions[id].gene_associations) && return nothing - [collect(keys(rga.stoichiometry)) for rga in model.reactions[id].gene_associations] + [collect(keys(rga.gene_product_stoichiometry)) for rga in model.reactions[id].gene_associations] end """ @@ -299,31 +299,14 @@ $(TYPEDSIGNATURES) Return the molar mass of translated gene with ID `gid`. """ -Accessors.gene_protein_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].protein_molar_mass - -""" -$(TYPEDSIGNATURES) - -Return the enzyme turnover number of reaction with ID `rid` for the reaction in -the forward direction. -""" -Accessors.reaction_kcat_forward(model::ObjectModel, rid::String) = model.reactions[rid].kcat_forward - -""" -$(TYPEDSIGNATURES) - -Return the enzyme turnover number of reaction with ID `rid` for the reaction in -the backward direction. -""" -Accessors.reaction_kcat_backward(model::ObjectModel, rid::String) = model.reactions[rid].kcat_backward +Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].product_molar_mass """ $(TYPEDSIGNATURES) Return the [`Isozyme`](@ref)s associated with the `model` and reaction `rid`. """ -Accessors.reaction_kcat_backward(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations - +Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations """ $(TYPEDSIGNATURES) @@ -385,7 +368,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) upper_bound = ubs[i], gene_associations = isnothing(rgas) ? nothing : [ - Isozyme(; stoichiometry = Dict(k => 1.0 for k in rga)) for rga in rgas + Isozyme(; gene_product_stoichiometry = Dict(k => 1.0 for k in rga)) for rga in rgas ], notes = reaction_notes(model, rid), annotations = reaction_annotations(model, rid), From 099a30bd9e9a930dd6ae2fb2cf1810ff893efc14 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 10:38:18 +0100 Subject: [PATCH 086/531] fixed smoment bug. working! --- .../simplified_enzyme_constrained.jl | 31 ++++++++++--------- .../simplified_enzyme_constrained.jl | 21 ++++++++++--- 2 files changed, 32 insertions(+), 20 deletions(-) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 5d59b78cf..c478041fa 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -5,32 +5,32 @@ Construct a model with a structure given by sMOMENT algorithm; returns a [`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). # Arguments -- a `model` that implements the accessors `gene_protein_molar_mass`, - `reaction_isozymes`, `reaction_kcat_forward`, and `reaction_kcat_backward`. +- a `model` that implements the accessors `gene_product_molar_mass`, + `reaction_isozymes`. - `total_enzyme_capacity` is the maximum "enzyme capacity" in the model. # Notes -The SMOMENT algorithm only uses one isozyme per reaction. If multiple isozymes -are present the "fastest" isozyme will be used. This is determined based on -maximum kcat (forward or backward) divided by mass of the isozyme. +The SMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple +isozymes are present the "fastest" isozyme will be used. This is determined +based on maximum kcat (forward or backward) divided by mass of the isozyme. Reactions with no turnover number data, or non-enzymatic reactions that should -be ignored should have `nothing` in the `gene_associations` field of the -associated reaction. +be ignored, must have `nothing` in the `gene_associations` field of the +associated reaction. """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; total_enzyme_capacity::Float64, ) # helper function to rank the isozymes by relative speed - speed_enzyme(model, rid, isozyme) = max(reaction_kcat_forward(model, rid), reaction_kcat_backward(model, rid)) / - sum(count * gene_protein_molar_mass(model, gid) for (gid, count) in isozyme.stoichiometry) + speed_enzyme(model, isozyme) = max(isozyme.kcat_forward, isozyme.kcat_backward) / + sum(count * gene_product_molar_mass(model, gid) for (gid, count) in isozyme.gene_product_stoichiometry) # helper function to return the fastest isozyme or nothing ris_(model, rid) = begin isozymes = reaction_isozymes(model, rid) isnothing(isozymes) && return nothing - argmax(isozyme -> speed_enzyme(model, rid, isozyme), isozymes) + argmax(isozyme -> speed_enzyme(model, isozyme), isozymes) end columns = Vector{Types._SimplifiedEnzymeConstrainedColumn}() @@ -39,6 +39,7 @@ function make_simplified_enzyme_constrained_model( rids = variables(model) for i = 1:n_variables(model) + isozyme = ris_(model, rids[i]) if isnothing(isozyme) @@ -50,9 +51,9 @@ function make_simplified_enzyme_constrained_model( continue end - mw = sum(gene_protein_molar_mass(model,gid) * ps for (gid, ps) in isozyme.stoichiometry) + mw = sum(gene_product_molar_mass(model,gid) * ps for (gid, ps) in isozyme.gene_product_stoichiometry) - if min(lbs[i], ubs[i]) < 0 && reaction_kcat_backward(model, rids[i]) > constants.tolerance + if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance # reaction can run in reverse push!( columns, @@ -61,12 +62,12 @@ function make_simplified_enzyme_constrained_model( -1, max(-ubs[i], 0), -lbs[i], - mw / reaction_kcat_backward(model, rids[i]), + mw / isozyme.kcat_backward, ), ) end - if max(lbs[i], ubs[i]) > 0 && reaction_kcat_forward(model, rids[i]) > constants.tolerance + if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > constants.tolerance # reaction can run forward push!( columns, @@ -75,7 +76,7 @@ function make_simplified_enzyme_constrained_model( 1, max(lbs[i], 0), ubs[i], - mw / reaction_kcat_forward(model, rids[i]), + mw / isozyme.kcat_forward, ), ) end diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 67c491e95..c5c1bf59e 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -1,17 +1,29 @@ @testset "SMOMENT" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) + + # add molar masses to gene products for gid in genes(model) - model.genes[gid].protein_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) + model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) end + model.genes["s0001"] = Gene(id="s0001"; product_molar_mass = 0.0) + + # update isozymes with kinetic information for rid in reactions(model) if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr - model.reactions[rid].kcat_forward = ecoli_core_reaction_kcats[rid][1][1] # all kcat forwards the same - model.reactions[rid].kcat_backward = ecoli_core_reaction_kcats[rid][1][2] # all kcat forwards the same newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(model, rid)) - push!(newisozymes, Isozyme(stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]))) + push!( + newisozymes, + Isozyme( + gene_product_stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2] + ) + ) end model.reactions[rid].gene_associations = newisozymes + else + model.reactions[rid].gene_associations = nothing end end @@ -25,7 +37,6 @@ with_simplified_enzyme_constrained( total_enzyme_capacity = 100.0, ) - objective(simplified_enzyme_constrained_model) rxn_fluxes = flux_balance_analysis_dict( simplified_enzyme_constrained_model, From 9adde3da130624507a7c5388d7c5a95c70cacaa4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 11:15:05 +0100 Subject: [PATCH 087/531] extend accessors --- src/misc/constants.jl | 1 + src/types/Gene.jl | 2 ++ src/types/accessors/AbstractMetabolicModel.jl | 14 ++++++++++++++ src/types/models/ObjectModel.jl | 14 ++++++++++++++ 4 files changed, 31 insertions(+) diff --git a/src/misc/constants.jl b/src/misc/constants.jl index 183ab4440..ab59dac67 100644 --- a/src/misc/constants.jl +++ b/src/misc/constants.jl @@ -5,6 +5,7 @@ whatever purposes. """ const constants = ( default_reaction_bound = 1e3, + default_gene_product_bound = 1e3, tolerance = 1e-6, sampling_keep_iters = 100, sampling_size = 1000, diff --git a/src/types/Gene.jl b/src/types/Gene.jl index ed8412d60..a1229689f 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -11,6 +11,8 @@ Base.@kwdef mutable struct Gene name::Maybe{String} = nothing sequence::Maybe{String} = nothing product_molar_mass::Maybe{Float64} = nothing + product_lower_bound::Float64 = 0 + product_upper_bound::Float64 = constants.default_gene_product_bound notes::Notes = Notes() annotations::Annotations = Annotations() end diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 74ef1f39f..0cdf7e04b 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -383,3 +383,17 @@ $(TYPEDSIGNATURES) Return all the [`Isozyme`](@ref)s associated with a `model` and reaction `rid`. """ reaction_isozymes(model::AbstractMetabolicModel, rid::String)::Maybe{Vector{Isozyme}} = nothing + +""" +$(TYPEDSIGNATURES) + +Return the lower bound of the gene product concentration associated with the `model` and gene `gid`. +""" +gene_product_lower_bound(model::AbstractMetabolicModel, gid::String)::Float64 = 0.0 + +""" +$(TYPEDSIGNATURES) + +Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. +""" +gene_product_upper_bound(model::AbstractMetabolicModel, gid::String)::Float64 = constants.default_gene_product_bound \ No newline at end of file diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index c8d58cd29..029d40f05 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -311,6 +311,20 @@ Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[r """ $(TYPEDSIGNATURES) +Return the lower bound of the gene product concentration associated with the `model` and gene `gid`. +""" +Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = model.genes[gid].product_lower_bound + +""" +$(TYPEDSIGNATURES) + +Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. +""" +Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = model.genes[gid].product_upper_bound + +""" +$(TYPEDSIGNATURES) + Convert any `AbstractMetabolicModel` into a `ObjectModel`. Note, some data loss may occur since only the generic interface is used during the conversion process. Additionally, assume the stoichiometry for each gene association is 1. From 13488c38f2ec158e4aa80dd2791559b2e5337358 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 13:07:00 +0100 Subject: [PATCH 088/531] Change Gecko to work with new accessors --- src/reconstruction/enzyme_constrained.jl | 92 +++++++++++------------ test/reconstruction/enzyme_constrained.jl | 80 ++++++++++---------- 2 files changed, 88 insertions(+), 84 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index f33d94c68..62278e768 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -1,52 +1,49 @@ """ $(TYPEDSIGNATURES) -Wrap a model into a [`EnzymeConstrainedModel`](@ref), following the structure given by -GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for details). +Wrap a model into a [`EnzymeConstrainedModel`](@ref), following the structure +given by GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for +details). Multiple capacity constraints can be placed on the model using the +kwargs. # Arguments - -- `reaction_isozymes` is a function that returns a vector of [`Isozyme`](@ref)s - for each reaction, or `nothing` if the reaction is not enzymatic. -- `gene_product_bounds` is a function that returns lower and upper bound for - concentration for a given gene product (specified by the same string gene ID as in - `reaction_isozymes`), as `Tuple{Float64,Float64}`. -- `gene_product_molar_mass` is a function that returns a numeric molar mass of - a given gene product specified by string gene ID. -- `gene_product_mass_group` is a function that returns a string group identifier for a - given gene product, again specified by string gene ID. By default, all gene - products belong to group `"uncategorized"` which is the behavior of original +- a `model` that implements the accessors `gene_product_molar_mass`, + `reaction_isozymes`, `gene_product_lower_bound`, `gene_product_upper_bound`. +- `gene_product_mass_group` is a dict that returns a vector of gene IDs + associated with each a named capacity constraint. By default, all gene + products belong to group `"uncategorized"`, which is the behavior of original GECKO. -- `gene_product_mass_group_bound` is a function that returns the maximum mass for a given - mass group. +- `gene_product_mass_group_bound` is a dict that returns the capacity + limitation for a given named capacity constraint. + +# Example +``` +ecmodel = make_enzyme_constrained_model( + model; + gene_product_mass_group = Dict( + "membrane" => ["e1", "e2"], + "total" => ["e1", "e2", "e3"], + ), + gene_product_mass_group_bound = Dict( + "membrane" => 0.2, + "total" => 0.5, + ), +) +``` -Alternatively, all function arguments may be also supplied as dictionaries that -provide the same data lookup. +# Notes +Reactions with no turnover number data, or non-enzymatic reactions that should +be ignored, must have `nothing` in the `gene_associations` field of the +associated reaction. """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; - reaction_isozymes::Union{Function,Dict{String,Vector{Isozyme}}}, - gene_product_bounds::Union{Function,Dict{String,Tuple{Float64,Float64}}}, - gene_product_molar_mass::Union{Function,Dict{String,Float64}}, - gene_product_mass_group::Union{Function,Dict{String,String}} = _ -> "uncategorized", - gene_product_mass_group_bound::Union{Function,Dict{String,Float64}}, + gene_product_mass_group::Dict{String, Vector{String}} = Dict("uncategorized" => genes(model)), + gene_product_mass_group_bound::Dict{String, Float64} = Dict("uncategorized" => 0.5), ) - ris_ = - reaction_isozymes isa Function ? reaction_isozymes : - (rid -> get(reaction_isozymes, rid, nothing)) - gpb_ = - gene_product_bounds isa Function ? gene_product_bounds : - (gid -> gene_product_bounds[gid]) - gpmm_ = - gene_product_molar_mass isa Function ? gene_product_molar_mass : - (gid -> gene_product_molar_mass[gid]) - gmg_ = - gene_product_mass_group isa Function ? gene_product_mass_group : - (gid -> gene_product_mass_group[gid]) - gmgb_ = - gene_product_mass_group_bound isa Function ? gene_product_mass_group_bound : - (grp -> gene_product_mass_group_bound[grp]) - # ...it would be nicer to have an overload for this, but kwargs can't be used for dispatch + gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) + + gpmm_(gid) = gene_product_molar_mass(model, gid) columns = Vector{Types._EnzymeConstrainedReactionColumn}() coupling_row_reaction = Int[] @@ -60,7 +57,8 @@ function make_enzyme_constrained_model( gene_row_lookup = Dict{Int,Int}() for i = 1:n_variables(model) - isozymes = ris_(rids[i]) + isozymes = reaction_isozymes(model, rids[i]) + if isnothing(isozymes) push!( columns, @@ -104,7 +102,7 @@ function make_enzyme_constrained_model( length(coupling_row_gene_product) end (row_idx, stoich / kcat) - end for (gene, stoich) in isozyme.stoichiometry if + end for (gene, stoich) in isozyme.gene_product_stoichiometry if haskey(gene_name_lookup, gene) ) @@ -129,11 +127,13 @@ function make_enzyme_constrained_model( # prepare enzyme capacity constraints mg_gid_lookup = Dict{String,Vector{String}}() for gid in gids[coupling_row_gene_product] - mg = gmg_(gid) - if haskey(mg_gid_lookup, mg) - push!(mg_gid_lookup[mg], gid) - else - mg_gid_lookup[mg] = [gid] + for (mg, mg_gids) in gene_product_mass_group # each gid can belong to multiple mass groups + gid ∉ mg_gids && continue + if haskey(mg_gid_lookup, mg) + push!(mg_gid_lookup[mg], gid) + else + mg_gid_lookup[mg] = [gid] + end end end coupling_row_mass_group = Vector{Types._EnzymeConstrainedCapacity}() @@ -142,7 +142,7 @@ function make_enzyme_constrained_model( mms = gpmm_.(gs) push!( coupling_row_mass_group, - Types._EnzymeConstrainedCapacity(grp, idxs, mms, gmgb_(grp)), + Types._EnzymeConstrainedCapacity(grp, idxs, mms, gene_product_mass_group_bound[grp]), ) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 93bebfd55..e79905f93 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -1,21 +1,41 @@ @testset "GECKO" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - get_reaction_isozymes = - rid -> - haskey(ecoli_core_reaction_kcats, rid) ? - collect( - Isozyme( - stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_associations(model, rid)) - ) : nothing - - get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) + # add molar masses to gene products + for gid in genes(model) + model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) + end + model.genes["s0001"] = Gene(id="s0001"; product_molar_mass = 0.0) + + # update isozymes with kinetic information + for rid in reactions(model) + if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr + newisozymes = Isozyme[] + for (i, grr) in enumerate(reaction_gene_associations(model, rid)) + push!( + newisozymes, + Isozyme( + gene_product_stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2] + ) + ) + end + model.reactions[rid].gene_associations = newisozymes + else + model.reactions[rid].gene_associations = nothing + end + end total_gene_product_mass = 100.0 + # set gene product bounds + for gid in genes(model) + lb, ub = gid == "b2779" ? (0.01, 0.06) : (0.0, 1.0) + model.genes[gid].product_lower_bound = lb + model.genes[gid].product_upper_bound = ub + end + gm = model |> with_changed_bounds( @@ -24,10 +44,7 @@ upper = [nothing, 12.0], ) |> with_enzyme_constrained( - reaction_isozymes = get_reaction_isozymes, - gene_product_bounds = g -> g == "b2779" ? (0.01, 0.06) : (0.0, 1.0), - gene_product_molar_mass = get_gene_product_mass, - gene_product_mass_group_bound = _ -> total_gene_product_mass, + gene_product_mass_group_bound = Dict("uncategorized" => total_gene_product_mass), ) opt_model = flux_balance_analysis( @@ -90,9 +107,13 @@ end ], ) - gs = [Gene("g$i") for i = 1:5] + gs = [ + Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) + Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) + Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) + Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) + ] - m.reactions["r2"].gene_associations = [Isozyme(["g5"])] m.reactions["r3"].gene_associations = [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] m.reactions["r4"].gene_associations = [ @@ -101,7 +122,7 @@ end ] m.reactions["r5"].gene_associations = [ Isozyme(; - stoichiometry = Dict("g3" => 1, "g4" => 2), + gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), kcat_forward = 70.0, kcat_backward = 70.0, ), @@ -111,26 +132,9 @@ end add_genes!(m, gs) add_metabolites!(m, [m1, m2, m3, m4]) - gene_product_bounds = Dict( - "g1" => (0.0, 10.0), - "g2" => (0.0, 10.0), - "g3" => (0.0, 10.0), - "g4" => (0.0, 10.0), - ) - - gene_product_molar_mass = Dict("g1" => 1.0, "g2" => 2.0, "g3" => 3.0, "g4" => 4.0) - - gene_product_mass_group_bound = Dict("uncategorized" => 0.5) - gm = make_enzyme_constrained_model( m; - reaction_isozymes = Dict( - rid => r.gene_associations for (rid, r) in m.reactions if - !isnothing(reaction_gene_associations(m, rid)) && rid in ["r3", "r4", "r5"] - ), - gene_product_bounds, - gene_product_molar_mass, - gene_product_mass_group_bound, + gene_product_mass_group_bound = Dict("uncategorized" => 0.5), ) opt_model = flux_balance_analysis( @@ -147,5 +151,5 @@ end @test isapprox(gene_products["g4"], 0.09090909090607537, atol = TEST_TOLERANCE) @test isapprox(mass_groups["uncategorized"], 0.5, atol = TEST_TOLERANCE) @test length(genes(gm)) == 4 - @test length(genes(gm.inner)) == 5 + @test length(genes(gm.inner)) == 4 end From a0d2fe2c8addd492628848d771d3bf860f8f7e70 Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 11 Jan 2023 12:13:34 +0000 Subject: [PATCH 089/531] Format and fix bugs triggered by @stelmo on PR #724 --- src/reconstruction/enzyme_constrained.jl | 15 ++++-- .../simplified_enzyme_constrained.jl | 20 +++++--- src/types/Gene.jl | 2 +- src/types/accessors/AbstractMetabolicModel.jl | 9 ++-- src/types/models/ObjectModel.jl | 20 +++++--- test/reconstruction/enzyme_constrained.jl | 16 +++--- .../simplified_enzyme_constrained.jl | 16 +++--- test/types/BalancedGrowthCommunityModel.jl | 50 ++++++++++++------- test/types/ObjectModel.jl | 4 +- test/types/Reaction.jl | 4 +- 10 files changed, 98 insertions(+), 58 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 62278e768..90030de12 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -38,11 +38,13 @@ associated reaction. """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; - gene_product_mass_group::Dict{String, Vector{String}} = Dict("uncategorized" => genes(model)), - gene_product_mass_group_bound::Dict{String, Float64} = Dict("uncategorized" => 0.5), + gene_product_mass_group::Dict{String,Vector{String}} = Dict( + "uncategorized" => genes(model), + ), + gene_product_mass_group_bound::Dict{String,Float64} = Dict("uncategorized" => 0.5), ) gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) - + gpmm_(gid) = gene_product_molar_mass(model, gid) columns = Vector{Types._EnzymeConstrainedReactionColumn}() @@ -142,7 +144,12 @@ function make_enzyme_constrained_model( mms = gpmm_.(gs) push!( coupling_row_mass_group, - Types._EnzymeConstrainedCapacity(grp, idxs, mms, gene_product_mass_group_bound[grp]), + Types._EnzymeConstrainedCapacity( + grp, + idxs, + mms, + gene_product_mass_group_bound[grp], + ), ) end diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index c478041fa..06d1d7f4a 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -4,7 +4,7 @@ $(TYPEDSIGNATURES) Construct a model with a structure given by sMOMENT algorithm; returns a [`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). -# Arguments +# Arguments - a `model` that implements the accessors `gene_product_molar_mass`, `reaction_isozymes`. - `total_enzyme_capacity` is the maximum "enzyme capacity" in the model. @@ -12,7 +12,7 @@ Construct a model with a structure given by sMOMENT algorithm; returns a # Notes The SMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple isozymes are present the "fastest" isozyme will be used. This is determined -based on maximum kcat (forward or backward) divided by mass of the isozyme. +based on maximum kcat (forward or backward) divided by mass of the isozyme. Reactions with no turnover number data, or non-enzymatic reactions that should be ignored, must have `nothing` in the `gene_associations` field of the @@ -23,16 +23,19 @@ function make_simplified_enzyme_constrained_model( total_enzyme_capacity::Float64, ) # helper function to rank the isozymes by relative speed - speed_enzyme(model, isozyme) = max(isozyme.kcat_forward, isozyme.kcat_backward) / - sum(count * gene_product_molar_mass(model, gid) for (gid, count) in isozyme.gene_product_stoichiometry) + speed_enzyme(model, isozyme) = + max(isozyme.kcat_forward, isozyme.kcat_backward) / sum( + count * gene_product_molar_mass(model, gid) for + (gid, count) in isozyme.gene_product_stoichiometry + ) # helper function to return the fastest isozyme or nothing ris_(model, rid) = begin - isozymes = reaction_isozymes(model, rid) + isozymes = reaction_isozymes(model, rid) isnothing(isozymes) && return nothing argmax(isozyme -> speed_enzyme(model, isozyme), isozymes) end - + columns = Vector{Types._SimplifiedEnzymeConstrainedColumn}() (lbs, ubs) = bounds(model) @@ -51,7 +54,10 @@ function make_simplified_enzyme_constrained_model( continue end - mw = sum(gene_product_molar_mass(model,gid) * ps for (gid, ps) in isozyme.gene_product_stoichiometry) + mw = sum( + gene_product_molar_mass(model, gid) * ps for + (gid, ps) in isozyme.gene_product_stoichiometry + ) if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance # reaction can run in reverse diff --git a/src/types/Gene.jl b/src/types/Gene.jl index a1229689f..5d5961e6a 100644 --- a/src/types/Gene.jl +++ b/src/types/Gene.jl @@ -11,7 +11,7 @@ Base.@kwdef mutable struct Gene name::Maybe{String} = nothing sequence::Maybe{String} = nothing product_molar_mass::Maybe{Float64} = nothing - product_lower_bound::Float64 = 0 + product_lower_bound::Float64 = 0 product_upper_bound::Float64 = constants.default_gene_product_bound notes::Notes = Notes() annotations::Annotations = Annotations() diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 0cdf7e04b..64d6cb958 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -375,14 +375,16 @@ $(TYPEDSIGNATURES) Return the molar mass of translated gene with ID `gid`. """ -gene_product_molar_mass(model::AbstractMetabolicModel, gid::String)::Maybe{Float64} = nothing +gene_product_molar_mass(model::AbstractMetabolicModel, gid::String)::Maybe{Float64} = + nothing """ $(TYPEDSIGNATURES) Return all the [`Isozyme`](@ref)s associated with a `model` and reaction `rid`. """ -reaction_isozymes(model::AbstractMetabolicModel, rid::String)::Maybe{Vector{Isozyme}} = nothing +reaction_isozymes(model::AbstractMetabolicModel, rid::String)::Maybe{Vector{Isozyme}} = + nothing """ $(TYPEDSIGNATURES) @@ -396,4 +398,5 @@ $(TYPEDSIGNATURES) Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. """ -gene_product_upper_bound(model::AbstractMetabolicModel, gid::String)::Float64 = constants.default_gene_product_bound \ No newline at end of file +gene_product_upper_bound(model::AbstractMetabolicModel, gid::String)::Float64 = + constants.default_gene_product_bound diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 029d40f05..1f27b2c66 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -173,7 +173,10 @@ function Accessors.reaction_gene_associations( id::String, )::Maybe{GeneAssociationsDNF} isnothing(model.reactions[id].gene_associations) && return nothing - [collect(keys(rga.gene_product_stoichiometry)) for rga in model.reactions[id].gene_associations] + [ + collect(keys(rga.gene_product_stoichiometry)) for + rga in model.reactions[id].gene_associations + ] end """ @@ -299,28 +302,32 @@ $(TYPEDSIGNATURES) Return the molar mass of translated gene with ID `gid`. """ -Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].product_molar_mass +Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = + model.genes[gid].product_molar_mass """ $(TYPEDSIGNATURES) Return the [`Isozyme`](@ref)s associated with the `model` and reaction `rid`. """ -Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations +Accessors.reaction_isozymes(model::ObjectModel, rid::String) = + model.reactions[rid].gene_associations """ $(TYPEDSIGNATURES) Return the lower bound of the gene product concentration associated with the `model` and gene `gid`. """ -Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = model.genes[gid].product_lower_bound +Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = + model.genes[gid].product_lower_bound """ $(TYPEDSIGNATURES) Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. """ -Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = model.genes[gid].product_upper_bound +Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = + model.genes[gid].product_upper_bound """ $(TYPEDSIGNATURES) @@ -382,7 +389,8 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) upper_bound = ubs[i], gene_associations = isnothing(rgas) ? nothing : [ - Isozyme(; gene_product_stoichiometry = Dict(k => 1.0 for k in rga)) for rga in rgas + Isozyme(; gene_product_stoichiometry = Dict(k => 1.0 for k in rga)) for + rga in rgas ], notes = reaction_notes(model, rid), annotations = reaction_annotations(model, rid), diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index e79905f93..592d8a47b 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -5,7 +5,7 @@ for gid in genes(model) model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) end - model.genes["s0001"] = Gene(id="s0001"; product_molar_mass = 0.0) + model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) # update isozymes with kinetic information for rid in reactions(model) @@ -13,12 +13,14 @@ newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(model, rid)) push!( - newisozymes, + newisozymes, Isozyme( - gene_product_stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + gene_product_stoichiometry = Dict( + grr .=> ecoli_core_protein_stoichiometry[rid][i], + ), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2] - ) + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ), ) end model.reactions[rid].gene_associations = newisozymes @@ -44,7 +46,9 @@ upper = [nothing, 12.0], ) |> with_enzyme_constrained( - gene_product_mass_group_bound = Dict("uncategorized" => total_gene_product_mass), + gene_product_mass_group_bound = Dict( + "uncategorized" => total_gene_product_mass, + ), ) opt_model = flux_balance_analysis( diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index c5c1bf59e..81e95b9bc 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -5,7 +5,7 @@ for gid in genes(model) model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) end - model.genes["s0001"] = Gene(id="s0001"; product_molar_mass = 0.0) + model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) # update isozymes with kinetic information for rid in reactions(model) @@ -13,12 +13,14 @@ newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(model, rid)) push!( - newisozymes, + newisozymes, Isozyme( - gene_product_stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), + gene_product_stoichiometry = Dict( + grr .=> ecoli_core_protein_stoichiometry[rid][i], + ), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2] - ) + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ), ) end model.reactions[rid].gene_associations = newisozymes @@ -34,9 +36,7 @@ lower = [-1000.0, -1.0], upper = [nothing, 12.0], ) |> - with_simplified_enzyme_constrained( - total_enzyme_capacity = 100.0, - ) + with_simplified_enzyme_constrained(total_enzyme_capacity = 100.0) rxn_fluxes = flux_balance_analysis_dict( simplified_enzyme_constrained_model, diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 8dba92ca6..295c50720 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -265,37 +265,49 @@ end @testset "BalancedGrowthCommunityModel: enzyme constrained e coli" begin ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) + # test added biomass metabolite modded_ecoli = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") @test "biomass" in metabolites(modded_ecoli) @test !("biomass" in metabolites(ecoli)) - @test haskey(modded_ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") @test !haskey(ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") - get_reaction_isozymes = - rid -> - haskey(ecoli_core_reaction_kcats, rid) ? - collect( - Isozyme( - stoichiometry = Dict(grr .=> ecoli_core_protein_stoichiometry[rid][i]), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) for (i, grr) in enumerate(reaction_gene_associations(ecoli, rid)) - ) : nothing - - get_gene_product_mass = gid -> get(ecoli_core_gene_product_masses, gid, 0.0) - - total_gene_product_mass = 100.0 + # add molar masses to gene products + for gid in genes(ecoli) + ecoli.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) + ecoli.genes[gid].product_upper_bound = 10.0 + end + ecoli.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) + ecoli.genes["s0001"].product_upper_bound = 10.0 + + # update isozymes with kinetic information + for rid in reactions(ecoli) + if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr + newisozymes = Isozyme[] + for (i, grr) in enumerate(reaction_gene_associations(ecoli, rid)) + push!( + newisozymes, + Isozyme( + gene_product_stoichiometry = Dict( + grr .=> ecoli_core_protein_stoichiometry[rid][i], + ), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ), + ) + end + ecoli.reactions[rid].gene_associations = newisozymes + else + ecoli.reactions[rid].gene_associations = nothing + end + end gm = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> with_changed_bounds(["EX_glc__D_e"]; lower = [-1000.0], upper = [0]) |> with_enzyme_constrained( - reaction_isozymes = get_reaction_isozymes, - gene_product_bounds = g -> (0.0, 10.0), - gene_product_molar_mass = get_gene_product_mass, - gene_product_mass_group_bound = _ -> total_gene_product_mass, + gene_product_mass_group_bound = Dict("uncategorized" => 100.0), ) ex_rxns = find_exchange_reaction_ids(ecoli) diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 67b678645..3c061a694 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -22,8 +22,8 @@ r1.lower_bound = -100.0 r1.upper_bound = 100.0 r1.gene_associations = [ - Isozyme(stoichiometry = Dict("g1" => 1, "g2" => 1)), - Isozyme(stoichiometry = Dict("g3" => 1)), + Isozyme(gene_product_stoichiometry = Dict("g1" => 1, "g2" => 1)), + Isozyme(gene_product_stoichiometry = Dict("g3" => 1)), ] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 10c536a83..14dd14769 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -23,8 +23,8 @@ r1.lower_bound = -100.0 r1.upper_bound = 100.0 r1.gene_associations = [ - Isozyme(stoichiometry = Dict("g1" => 1, "g2" => 1)), - Isozyme(stoichiometry = Dict("g3" => 1)), + Isozyme(gene_product_stoichiometry = Dict("g1" => 1, "g2" => 1)), + Isozyme(gene_product_stoichiometry = Dict("g3" => 1)), ] r1.subsystem = "glycolysis" r1.notes = Dict("notes" => ["blah", "blah"]) From c8a7d9019dfa1eb43f060d9c265b2ff9cb2b1db0 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 15:34:27 +0100 Subject: [PATCH 090/531] Remove unused fields in Reaction --- src/types/Reaction.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 1ef56c838..96107855f 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -16,8 +16,6 @@ Base.@kwdef mutable struct Reaction upper_bound::Float64 = constants.default_reaction_bound gene_associations::Maybe{GeneAssociations} = nothing subsystem::Maybe{String} = nothing - kcat_forward::Maybe{Float64} = nothing - kcat_backward::Maybe{Float64} = nothing notes::Notes = Notes() annotations::Annotations = Annotations() end From 14a2b5d3775fc000f1a4daa8cd54d3ec89bf2cea Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 7 Jan 2023 21:31:39 +0100 Subject: [PATCH 091/531] Add constrained allocation FBA via modification --- src/reconstruction/ObjectModel.jl | 109 +++++++++++++++++- src/reconstruction/modifications/enzymes.jl | 9 ++ test/reconstruction/constrained_allocation.jl | 84 ++++++++++++++ 3 files changed, 201 insertions(+), 1 deletion(-) create mode 100644 test/reconstruction/constrained_allocation.jl diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index d1fdf627d..460318054 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -206,7 +206,8 @@ change_objective!(model::ObjectModel, rxn_id::String) = change_objective!(model, $(TYPEDSIGNATURES) Add a biomass metabolite called `biomass_metabolite_id` with stoichiometry 1 to -the biomass reaction, called `biomass_rxn_id` in `model`. +the biomass reaction, called `biomass_rxn_id` in `model`. Changes the model in +place. """ function add_biomass_metabolite!( model::ObjectModel, @@ -217,6 +218,12 @@ function add_biomass_metabolite!( add_metabolite!(model, Metabolite(biomass_metabolite_id)) end +""" +$(TYPEDSIGNATURES) + +Variant of [`add_biomass_metabolite!`](@ref) that does not modify the original +model, but makes a shallow copy with the modification included. +""" function add_biomass_metabolite( model::ObjectModel, biomass_rxn_id::String; @@ -234,3 +241,103 @@ function add_biomass_metabolite( m end + +""" +$(TYPEDSIGNATURES) + +To `biomass_rxn_id` in `model`, add a pseudo-isozyme that approximates the +effect ribosome synthesis has on growth. + +# Bacterial growth law models +Numerous experimental studies have shown that during steady state growth the +cellular density of E. coli (and probably other bacteria) is constant. As a +consequence of this, growth law models typically assume that the total proteome +capacity (mass fraction of protein in the cell) is limited. Further, it has been +shown experimentally that the ribosomal protein content of a bacterial cell +increases with faster growth rate. Ribosomes are used to make proteins (and +themselves), leading to a trade-off: faster growth requires more ribosomes to +make more enzymes to grow, but this reduces the amount of proteome space "left +over" for biosynthetic enzymes. See Mori, Matteo, et al. "Constrained allocation +flux balance analysis." PLoS computational biology 12.6 (2016) for more details. + +# Implementation +By adding a protein cost to biomass synthesis this effect can be simulated. The +parameter `weight` needs to be estimated from data, but acts like a turnover +number. Lower `weight` means more ribosome is required for growth (`ribosome = +growth/weight`). The molar mass of the ribosome is `1`. + +# Note +1. This modifications makes the underlying biomass reaction unidirectional. +2. The `pseudoribosome_id` defaults to `pseudoribosome` and must be manually included + in any capacity bound later used in enzyme constrained models. +3. This modification also adds a pseudogene called `pseudoribosome_id`. + +The pseudo-isozyme acts like a regular gene product, +``` +ribosome = weight * biomass_flux +``` +""" +function add_pseudoribosome!( + model::ObjectModel, + biomass_rxn_id::String, + weight::Float64; + pseudoribosome_id = "pseudoribosome", +) + # ensure unidirectional + model.reactions[biomass_rxn_id].lower_bound = 0.0 + model.reactions[biomass_rxn_id].upper_bound = constants.default_reaction_bound + + # add ribosome kinetics + model.reactions[biomass_rxn_id].gene_associations = [ + Isozyme( + kcat_forward = weight, + kcat_backward = 0.0, + gene_product_stoichiometry = Dict(pseudoribosome_id => 1.0), + ), + ] + + # add ribosome gene + model.genes[pseudoribosome_id] = Gene(id = pseudoribosome_id, product_molar_mass = 1.0) + + nothing +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`add_pseudoribosome!`](@ref) that does not modify the original +model, but makes a shallow copy with the modification included. +""" +function add_pseudoribosome( + model::ObjectModel, + biomass_rxn_id::String, + weight::Float64; + pseudoribosome_id = "pseudoribosome", +) + m = copy(model) + + # unidirectional biomass + m.reactions = copy(model.reactions) + m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) + m.reactions[biomass_rxn_id].lower_bound = 0.0 + m.reactions[biomass_rxn_id].upper_bound = constants.default_reaction_bound + + # add ribosome kinetics + if !isnothing(model.reactions[biomass_rxn_id].gene_associations) + m.reactions[biomass_rxn_id].gene_associations = + copy(model.reactions[biomass_rxn_id].gene_associations) + end + m.reactions[biomass_rxn_id].gene_associations = [ + Isozyme( + kcat_forward = weight, + kcat_backward = 0.0, + gene_product_stoichiometry = Dict(pseudoribosome_id => 1.0), + ), + ] + + # add ribosome gene + m.genes = copy(model.genes) + m.genes[pseudoribosome_id] = Gene(id = pseudoribosome_id, product_molar_mass = 1.0) + + m +end diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index c2a89e51b..8835a6ac3 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -17,3 +17,12 @@ giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to """ with_enzyme_constrained(; kwargs...) = model -> make_enzyme_constrained_model(model; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that adds a pseudoribosome to a model. Args and kwargs +are forwarded to [`add_pseudoribosome`](@ref). +""" +with_pseudoribosome(args...; kwargs...) = + model -> add_pseudoribosome(model, args...; kwargs...) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl new file mode 100644 index 000000000..5d8f409a1 --- /dev/null +++ b/test/reconstruction/constrained_allocation.jl @@ -0,0 +1,84 @@ +@testset "Constrained allocation FBA" begin + + m = ObjectModel(id = "") + m1 = Metabolite("m1") + m2 = Metabolite("m2") + m3 = Metabolite("m3") + m4 = Metabolite("m4") + + add_reactions!( + m, + [ + Reaction("r1", Dict("m1" => 1), :forward), + Reaction("r2", Dict("m2" => 1), :forward), + Reaction("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), + Reaction("r4", Dict("m3" => -1, "m4" => 1), :forward), + Reaction("r5", Dict("m2" => -1, "m4" => 1), :bidirectional), + Reaction("r6", Dict("m4" => -1), :bidirectional), # different! + ], + ) + + gs = [ + Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) + Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) + Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) + Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) + ] + + m.reactions["r3"].gene_associations = + [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] + m.reactions["r4"].gene_associations = [ + Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0), + Isozyme(["g2"]; kcat_forward = 3.0, kcat_backward = 3.0), + ] + m.reactions["r5"].gene_associations = [ + Isozyme(; + gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), + kcat_forward = 70.0, + kcat_backward = 70.0, + ), + ] + m.reactions["r6"].gene_associations = [ # this should get removed + Isozyme(; + gene_product_stoichiometry = Dict("g3" => 1), + kcat_forward = 10.0, + kcat_backward = 0.0, + ), + ] + m.objective = Dict("r6" => 1.0) + + add_genes!(m, gs) + add_metabolites!(m, [m1, m2, m3, m4]) + + ribomodel = m |> with_pseudoribosome("r6", 0.2) + + @test haskey(ribomodel.genes, "pseudoribosome") + @test first(ribomodel.reactions["r6"].gene_associations).kcat_forward == 0.2 + @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 + + + cam = make_simplified_enzyme_constrained_model(ribomodel; total_enzyme_capacity = 0.5) + + @test coupling(cam)[1, 7] == 5.0 + + rxn_fluxes = flux_balance_analysis_dict( + cam, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + + @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) + + # test inplace variant + add_pseudoribosome!(m, "r6", 0.2) + cam = make_simplified_enzyme_constrained_model(m; total_enzyme_capacity = 0.5) + + @test coupling(cam)[1, 7] == 5.0 + + rxn_fluxes = flux_balance_analysis_dict( + cam, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) +end From 980e15e6deac7ca082d31a5fe1eeb5c0fdfe94d6 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 9 Jan 2023 23:58:58 +0100 Subject: [PATCH 092/531] Deprecate crowding constraints --- docs/src/concepts/2_modifications.md | 2 +- docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl | 14 ++++++ docs/src/examples/10_crowding.jl | 51 --------------------- src/analysis/modifications/crowding.jl | 36 --------------- test/analysis/fba_with_crowding.jl | 25 ---------- 5 files changed, 15 insertions(+), 113 deletions(-) create mode 100644 docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl delete mode 100644 docs/src/examples/10_crowding.jl delete mode 100644 src/analysis/modifications/crowding.jl delete mode 100644 test/analysis/fba_with_crowding.jl diff --git a/docs/src/concepts/2_modifications.md b/docs/src/concepts/2_modifications.md index 2ded14a34..16f4322b4 100644 --- a/docs/src/concepts/2_modifications.md +++ b/docs/src/concepts/2_modifications.md @@ -13,7 +13,7 @@ tuning the optimizer, or change the raw values in the linear model, such as: - [`change_constraint`](@ref) and [`change_objective`](@ref) - [`change_sense`](@ref), [`change_optimizer`](@ref), [`change_optimizer_attribute`](@ref) - [`silence`](@ref) -- [`knockout`](@ref), [`add_crowding_constraints`](@ref) +- [`knockout`](@ref) - [`add_loopless_constraints`](@ref) Compared to the [variant system](1_screen.md) and the [model diff --git a/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl b/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl new file mode 100644 index 000000000..b28d58188 --- /dev/null +++ b/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl @@ -0,0 +1,14 @@ +# # FBA with ??? + +# Let's starting with loading the models and packages. + +!isfile("e_coli_core.xml") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") + +using COBREXA, Tulip + +model = load_model("e_coli_core.xml") + +import Random +Random.seed!(1) # for repeatability of random numbers below + diff --git a/docs/src/examples/10_crowding.jl b/docs/src/examples/10_crowding.jl deleted file mode 100644 index 14b341968..000000000 --- a/docs/src/examples/10_crowding.jl +++ /dev/null @@ -1,51 +0,0 @@ -# # FBA with crowding - -# Here we will use [`flux_balance_analysis`](@ref) to explore the metabolism of -# the toy *E. coli* model that additionally respects common protein crowding -# constraints. In particular, the model is limited by the amount of protein -# required to run certain reactions. If that data is available, the predictions -# are accordingly more realistic. See [Beg, et al., "Intracellular crowding -# defines the mode and sequence of substrate uptake by Escherichia coli and -# constrains its metabolic activity.", Proceedings of the National Academy of -# Sciences,2007](https://doi.org/10.1073/pnas.0609845104) for more details. -# -# As usual, the same model modification can be transparently used with many -# other analysis functions, including [`flux_variability_analysis`](@ref) and -# [`parsimonious_flux_balance_analysis`](@ref). - -# Let's starting with loading the models and packages. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, Tulip - -model = load_model("e_coli_core.xml") - -# To describe the protein crowding, each of the enzymes that catalyze the -# reactions gets an associated weight per unit of reaction conversion rate. The -# total sum of all weights multiplied by the flux in the model must be lower -# than 1. -# -# The weights are prepared in a dictionary; for simplicity we assume that the -# relative weight of all enzymes is random between 0.002 and 0.005. -# enzymes are of the same size. Reactions that are not present in the -# dictionary (typically exchanges) are ignored. - -import Random -Random.seed!(1) # for repeatability of random numbers below - -rid_crowding_weight = Dict( - rid => 0.002 + 0.003 * rand() for rid in variables(model) if - !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) -) - -# With this, the crowding constraints are added with modification -# [`add_crowding_constraints`](@ref): -loopless_crowding_fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [add_crowding_constraints(rid_crowding_weight)], -) -# -flux_summary(loopless_crowding_fluxes) diff --git a/src/analysis/modifications/crowding.jl b/src/analysis/modifications/crowding.jl deleted file mode 100644 index 4c004a7b1..000000000 --- a/src/analysis/modifications/crowding.jl +++ /dev/null @@ -1,36 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Adds a molecular crowding constraint to the optimization problem: `∑ wᵢ × vᵢ ≤ 1` where `wᵢ` -is a weight and `vᵢ` is a flux index in the model's reactions specified in `weights` as `vᵢ -=> wᵢ` pairs. - -See Beg, Qasim K., et al. "Intracellular crowding defines the mode and sequence of -substrate uptake by Escherichia coli and constrains its metabolic activity." Proceedings of -the National Academy of Sciences 104.31 (2007) for more details. -""" -add_crowding_constraints(weights::Dict{Int64,Float64}) = - (model, opt_model) -> begin - idxs = collect(keys(weights)) # order of keys and values is the same - ws = values(weights) - # since fluxes can be positive or negative, need absolute value: ∑ wᵢ × |vᵢ| ≤ 1 - # introduce slack variables to handle this - @variable(opt_model, crowding_slack[1:length(weights)]) - @constraint(opt_model, crowding_slack .>= opt_model[:x][idxs]) - @constraint(opt_model, crowding_slack .>= -opt_model[:x][idxs]) - @constraint(opt_model, sum(w * crowding_slack[i] for (i, w) in enumerate(ws)) <= 1) - end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_crowding_constraints`](@ref) that takes a dictinary of reactions `ids` -instead of reaction indices mapped to weights. -""" -add_crowding_constraints(weights::Dict{String,Float64}) = - (model, opt_model) -> begin - idxs = indexin(keys(weights), variables(model)) - nothing in idxs && throw(ArgumentError("Reaction id not found in model.")) - add_crowding_constraints(Dict(zip(Int.(idxs), values(weights))))(model, opt_model) - end diff --git a/test/analysis/fba_with_crowding.jl b/test/analysis/fba_with_crowding.jl deleted file mode 100644 index 118b6adf6..000000000 --- a/test/analysis/fba_with_crowding.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "FBA with crowding constraints" begin - model = load_model(model_paths["e_coli_core.json"]) - rid_weight = Dict( - rid => 0.004 for rid in variables(model) if - !looks_like_biomass_reaction(rid) && !looks_like_exchange_reaction(rid) - ) - - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 1000), - add_crowding_constraints(rid_weight), - change_constraint("EX_glc__D_e"; lower_bound = -6), - ], - ) - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.491026987015203, - atol = TEST_TOLERANCE, - ) - - @test isapprox(sol["EX_ac_e"], 0.7084745257320869, atol = TEST_TOLERANCE) -end From 59b8567791bd60fe6ca3c0932c19e1dfb0ad52ec Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 11 Jan 2023 16:53:10 +0100 Subject: [PATCH 093/531] add new accessors to wrappers --- src/types/accessors/ModelWrapper.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index d4add7ce1..0f2e33c43 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -16,8 +16,8 @@ end @inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! -@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes +@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes @inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes -@inherit_model_methods_fn AbstractModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes +@inherit_model_methods_fn AbstractModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes gene_product_molar_mass gene_product_lower_bound gene_product_upper_bound From 71009e65dcc1ac5c7d306785158fc560e2d8f6b9 Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 11 Jan 2023 15:55:56 +0000 Subject: [PATCH 094/531] automatic formatting triggered by @stelmo on PR #725 --- docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl b/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl index b28d58188..047d20558 100644 --- a/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl +++ b/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl @@ -11,4 +11,3 @@ model = load_model("e_coli_core.xml") import Random Random.seed!(1) # for repeatability of random numbers below - From 46d29f3820dc4bcef1d4c228354ab1e0bbfbbecb Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 12 Jan 2023 10:37:31 +0100 Subject: [PATCH 095/531] add todo --- docs/src/examples/{10_ADD_ANOTHER_EXAMPLE.jl => 10_crowding.jl} | 2 ++ 1 file changed, 2 insertions(+) rename docs/src/examples/{10_ADD_ANOTHER_EXAMPLE.jl => 10_crowding.jl} (89%) diff --git a/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl b/docs/src/examples/10_crowding.jl similarity index 89% rename from docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl rename to docs/src/examples/10_crowding.jl index 047d20558..668d6bcfa 100644 --- a/docs/src/examples/10_ADD_ANOTHER_EXAMPLE.jl +++ b/docs/src/examples/10_crowding.jl @@ -11,3 +11,5 @@ model = load_model("e_coli_core.xml") import Random Random.seed!(1) # for repeatability of random numbers below + +# TODO add crowding example via smoment \ No newline at end of file From 9c10454821adf8a8f57d2b41ce1bc96301ba7be4 Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 12 Jan 2023 10:16:18 +0000 Subject: [PATCH 096/531] automatic formatting triggered by @stelmo on PR #725 --- docs/src/examples/10_crowding.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/10_crowding.jl b/docs/src/examples/10_crowding.jl index 668d6bcfa..f3820b425 100644 --- a/docs/src/examples/10_crowding.jl +++ b/docs/src/examples/10_crowding.jl @@ -12,4 +12,4 @@ model = load_model("e_coli_core.xml") import Random Random.seed!(1) # for repeatability of random numbers below -# TODO add crowding example via smoment \ No newline at end of file +# TODO add crowding example via smoment From 06cef68e0cb8480dd44bc73c8a5f49fd3abcfd3f Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 13 Jan 2023 13:53:57 +0100 Subject: [PATCH 097/531] add isozyme modifications --- src/reconstruction/ObjectModel.jl | 84 +++++++++++++++++++ src/reconstruction/modifications/enzymes.jl | 9 ++ test/reconstruction/constrained_allocation.jl | 23 +++++ 3 files changed, 116 insertions(+) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 460318054..d83e86383 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -341,3 +341,87 @@ function add_pseudoribosome( m end + +""" +$(TYPEDSIGNATURES) + +Add `isozymes` to `rxn_id` in `model`. Overwrites the currently stored isozymes. Assumes genes +are already in `model`. +""" +add_isozymes!( + model::ObjectModel, + rxn_id::String, + isozymes::Vector{Isozyme}, +) = model.reactions[rxn_id].gene_associations = isozymes + +""" +$(TYPEDSIGNATURES) + +For each pair of `isozymes` and `rxn_id` in `isozymes_vector` and +`rxn_id_vector`, call [`add_isozymes!`](@ref) to add the isozymes to the +`model`. +""" +function add_isozymes!( + model::ObjectModel, + rxn_id_vector::Vector{String}, + isozymes_vector::Vector{Vector{Isozyme}}, +) + for (rid, isozymes) in zip(rxn_id_vector, isozymes_vector) + add_isozymes!(model, rid, isozymes) + end +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`add_isozymes!`](@ref) that returns a copied model instead of +modifying the input. +""" +function add_isozymes( + model::ObjectModel, + rxn_id::String, + isozymes::Vector{Isozyme}, +) + + m = copy(model) + m.reactions = copy(model.reactions) + + m.reactions[rxn_id] = copy(model.reactions[rxn_id]) + if !isnothing(model.reactions[rxn_id].gene_associations) + m.reactions[rxn_id].gene_associations = + copy(model.reactions[rxn_id].gene_associations) + end + m.reactions[rxn_id].gene_associations = isozymes + + m +end + +""" +$(TYPEDSIGNATURES) + +For each pair of `isozymes` and `rxn_id` in `isozymes_vector` and +`rxn_id_vector`, call [`add_isozymes`](@ref) to add the isozymes to the +`model`. +""" +function add_isozymes( + model::ObjectModel, + rxn_id_vector::Vector{String}, + isozymes_vector::Vector{Vector{Isozyme}}, +) + + m = copy(model) + m.reactions = copy(model.reactions) + + for (rxn_id, isozymes) in zip(rxn_id_vector, isozymes_vector) + + m.reactions[rxn_id] = copy(model.reactions[rxn_id]) + if !isnothing(model.reactions[rxn_id].gene_associations) + m.reactions[rxn_id].gene_associations = + copy(model.reactions[rxn_id].gene_associations) + end + m.reactions[rxn_id].gene_associations = isozymes + + end + + m +end diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index 8835a6ac3..82afce364 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -26,3 +26,12 @@ are forwarded to [`add_pseudoribosome`](@ref). """ with_pseudoribosome(args...; kwargs...) = model -> add_pseudoribosome(model, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that overwrites the current isozymes associated with the +model through calling [`add_isozymes`](@ref). +""" +with_isozymes(args...) = + model -> add_isozymes(model, args...) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 5d8f409a1..c560e09c8 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -81,4 +81,27 @@ modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) + + # test with_isozyme functions + iso1 = Isozyme(["g1"]; kcat_forward = 200.0, kcat_backward = 300.0) + iso2 = Isozyme(["g2"]; kcat_forward = 100.0, kcat_backward = 500.0) + m2 = m |> with_isozymes( + ["r3", "r4"], + [[iso1], [iso2]], + ) + @test first(m2.reactions["r3"].gene_associations).kcat_backward == 300.0 + @test first(m2.reactions["r4"].gene_associations).kcat_backward == 500.0 + @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 + + m2 = m |> with_isozymes( + "r3", + [iso2], + ) + @test first(m2.reactions["r3"].gene_associations).kcat_backward == 500.0 + @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 + + add_isozymes!(m, ["r3", "r4"], [[iso1], [iso2]]) + @test first(m.reactions["r3"].gene_associations).kcat_backward == 300.0 + @test first(m.reactions["r4"].gene_associations).kcat_backward == 500.0 + end From 7f2917ad49040bdd1648bb7e66de8be248d20376 Mon Sep 17 00:00:00 2001 From: stelmo Date: Fri, 13 Jan 2023 12:56:00 +0000 Subject: [PATCH 098/531] automatic formatting triggered by @stelmo on PR #717 --- src/reconstruction/ObjectModel.jl | 17 +++++------------ src/reconstruction/modifications/enzymes.jl | 3 +-- test/reconstruction/constrained_allocation.jl | 16 +++++----------- 3 files changed, 11 insertions(+), 25 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index d83e86383..f78698793 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -348,11 +348,8 @@ $(TYPEDSIGNATURES) Add `isozymes` to `rxn_id` in `model`. Overwrites the currently stored isozymes. Assumes genes are already in `model`. """ -add_isozymes!( - model::ObjectModel, - rxn_id::String, - isozymes::Vector{Isozyme}, -) = model.reactions[rxn_id].gene_associations = isozymes +add_isozymes!(model::ObjectModel, rxn_id::String, isozymes::Vector{Isozyme}) = + model.reactions[rxn_id].gene_associations = isozymes """ $(TYPEDSIGNATURES) @@ -375,13 +372,9 @@ end $(TYPEDSIGNATURES) Variant of [`add_isozymes!`](@ref) that returns a copied model instead of -modifying the input. +modifying the input. """ -function add_isozymes( - model::ObjectModel, - rxn_id::String, - isozymes::Vector{Isozyme}, -) +function add_isozymes(model::ObjectModel, rxn_id::String, isozymes::Vector{Isozyme}) m = copy(model) m.reactions = copy(model.reactions) @@ -413,7 +406,7 @@ function add_isozymes( m.reactions = copy(model.reactions) for (rxn_id, isozymes) in zip(rxn_id_vector, isozymes_vector) - + m.reactions[rxn_id] = copy(model.reactions[rxn_id]) if !isnothing(model.reactions[rxn_id].gene_associations) m.reactions[rxn_id].gene_associations = diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index 82afce364..f09e962cb 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -33,5 +33,4 @@ $(TYPEDSIGNATURES) Specifies a model variant that overwrites the current isozymes associated with the model through calling [`add_isozymes`](@ref). """ -with_isozymes(args...) = - model -> add_isozymes(model, args...) +with_isozymes(args...) = model -> add_isozymes(model, args...) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index c560e09c8..769681941 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -85,23 +85,17 @@ # test with_isozyme functions iso1 = Isozyme(["g1"]; kcat_forward = 200.0, kcat_backward = 300.0) iso2 = Isozyme(["g2"]; kcat_forward = 100.0, kcat_backward = 500.0) - m2 = m |> with_isozymes( - ["r3", "r4"], - [[iso1], [iso2]], - ) + m2 = m |> with_isozymes(["r3", "r4"], [[iso1], [iso2]]) @test first(m2.reactions["r3"].gene_associations).kcat_backward == 300.0 @test first(m2.reactions["r4"].gene_associations).kcat_backward == 500.0 @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 - - m2 = m |> with_isozymes( - "r3", - [iso2], - ) + + m2 = m |> with_isozymes("r3", [iso2]) @test first(m2.reactions["r3"].gene_associations).kcat_backward == 500.0 @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 - + add_isozymes!(m, ["r3", "r4"], [[iso1], [iso2]]) @test first(m.reactions["r3"].gene_associations).kcat_backward == 300.0 @test first(m.reactions["r4"].gene_associations).kcat_backward == 500.0 - + end From 430f63bd24cd2cac4dd1eccd33cc19c2a5f054a6 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 16 Jan 2023 13:21:43 +0100 Subject: [PATCH 099/531] implement reviews --- src/reconstruction/ObjectModel.jl | 22 +++++++++---------- src/reconstruction/modifications/enzymes.jl | 10 ++++----- test/reconstruction/constrained_allocation.jl | 6 ++--- 3 files changed, 19 insertions(+), 19 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index f78698793..0f9745790 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -268,20 +268,20 @@ growth/weight`). The molar mass of the ribosome is `1`. # Note 1. This modifications makes the underlying biomass reaction unidirectional. -2. The `pseudoribosome_id` defaults to `pseudoribosome` and must be manually included +2. The `virtualribosome_id` defaults to `virtualribosome` and must be manually included in any capacity bound later used in enzyme constrained models. -3. This modification also adds a pseudogene called `pseudoribosome_id`. +3. This modification also adds a pseudogene called `virtualribosome_id`. The pseudo-isozyme acts like a regular gene product, ``` ribosome = weight * biomass_flux ``` """ -function add_pseudoribosome!( +function add_virtualribosome!( model::ObjectModel, biomass_rxn_id::String, weight::Float64; - pseudoribosome_id = "pseudoribosome", + virtualribosome_id = "virtualribosome", ) # ensure unidirectional model.reactions[biomass_rxn_id].lower_bound = 0.0 @@ -292,12 +292,12 @@ function add_pseudoribosome!( Isozyme( kcat_forward = weight, kcat_backward = 0.0, - gene_product_stoichiometry = Dict(pseudoribosome_id => 1.0), + gene_product_stoichiometry = Dict(virtualribosome_id => 1.0), ), ] # add ribosome gene - model.genes[pseudoribosome_id] = Gene(id = pseudoribosome_id, product_molar_mass = 1.0) + model.genes[virtualribosome_id] = Gene(id = virtualribosome_id, product_molar_mass = 1.0) nothing end @@ -305,14 +305,14 @@ end """ $(TYPEDSIGNATURES) -Variant of [`add_pseudoribosome!`](@ref) that does not modify the original +Variant of [`add_virtualribosome!`](@ref) that does not modify the original model, but makes a shallow copy with the modification included. """ -function add_pseudoribosome( +function add_virtualribosome( model::ObjectModel, biomass_rxn_id::String, weight::Float64; - pseudoribosome_id = "pseudoribosome", + virtualribosome_id = "virtualribosome", ) m = copy(model) @@ -331,13 +331,13 @@ function add_pseudoribosome( Isozyme( kcat_forward = weight, kcat_backward = 0.0, - gene_product_stoichiometry = Dict(pseudoribosome_id => 1.0), + gene_product_stoichiometry = Dict(virtualribosome_id => 1.0), ), ] # add ribosome gene m.genes = copy(model.genes) - m.genes[pseudoribosome_id] = Gene(id = pseudoribosome_id, product_molar_mass = 1.0) + m.genes[virtualribosome_id] = Gene(id = virtualribosome_id, product_molar_mass = 1.0) m end diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/modifications/enzymes.jl index f09e962cb..10d1e0119 100644 --- a/src/reconstruction/modifications/enzymes.jl +++ b/src/reconstruction/modifications/enzymes.jl @@ -21,11 +21,11 @@ with_enzyme_constrained(; kwargs...) = """ $(TYPEDSIGNATURES) -Specifies a model variant that adds a pseudoribosome to a model. Args and kwargs -are forwarded to [`add_pseudoribosome`](@ref). +Specifies a model variant that adds a virtualribosome to a model. Args and kwargs +are forwarded to [`add_virtualribosome`](@ref). """ -with_pseudoribosome(args...; kwargs...) = - model -> add_pseudoribosome(model, args...; kwargs...) +with_virtualribosome(args...; kwargs...) = + model -> add_virtualribosome(model, args...; kwargs...) """ $(TYPEDSIGNATURES) @@ -33,4 +33,4 @@ $(TYPEDSIGNATURES) Specifies a model variant that overwrites the current isozymes associated with the model through calling [`add_isozymes`](@ref). """ -with_isozymes(args...) = model -> add_isozymes(model, args...) +with_isozymes(args...; kwargs...) = model -> add_isozymes(model, args...; kwargs...) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 769681941..6293824df 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -50,9 +50,9 @@ add_genes!(m, gs) add_metabolites!(m, [m1, m2, m3, m4]) - ribomodel = m |> with_pseudoribosome("r6", 0.2) + ribomodel = m |> with_virtualribosome("r6", 0.2) - @test haskey(ribomodel.genes, "pseudoribosome") + @test haskey(ribomodel.genes, "virtualribosome") @test first(ribomodel.reactions["r6"].gene_associations).kcat_forward == 0.2 @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 @@ -70,7 +70,7 @@ @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) # test inplace variant - add_pseudoribosome!(m, "r6", 0.2) + add_virtualribosome!(m, "r6", 0.2) cam = make_simplified_enzyme_constrained_model(m; total_enzyme_capacity = 0.5) @test coupling(cam)[1, 7] == 5.0 From 3f42ebdf938e943d3207e9b030177b8cafb27a5a Mon Sep 17 00:00:00 2001 From: stelmo Date: Mon, 16 Jan 2023 12:25:19 +0000 Subject: [PATCH 100/531] automatic formatting triggered by @stelmo on PR #717 --- src/reconstruction/ObjectModel.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 0f9745790..a52455124 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -297,7 +297,8 @@ function add_virtualribosome!( ] # add ribosome gene - model.genes[virtualribosome_id] = Gene(id = virtualribosome_id, product_molar_mass = 1.0) + model.genes[virtualribosome_id] = + Gene(id = virtualribosome_id, product_molar_mass = 1.0) nothing end From 84b55fb89cd2773acb190376d4bfb767f72351f8 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 7 Jan 2023 17:03:31 +0100 Subject: [PATCH 101/531] add macro --- src/moduletools.jl | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/src/moduletools.jl b/src/moduletools.jl index aad5ab758..0c8231834 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -41,3 +41,35 @@ end @export_locals end + +""" +@using_all() + +A convenience macro that brings the names of all the COBREXA modules into scope. + +When calling `COBREXA.@using_all` the following code gets injected: +``` +using COBREXA.Types, + COBREXA.Accessors, + COBREXA.Analysis, + COBREXA.Analysis.Modifications, + COBREXA.Reconstruction, + COBREXA.Reconstruction.Modifications, + COBREXA.Utils, + COBREXA.IO, + COBREXA.Solver +``` +""" +macro using_all() + quote + using COBREXA.Types, + COBREXA.Accessors, + COBREXA.Analysis, + COBREXA.Analysis.Modifications, + COBREXA.Reconstruction, + COBREXA.Reconstruction.Modifications, + COBREXA.Utils, + COBREXA.IO, + COBREXA.Solver + end +end \ No newline at end of file From 8ef7b30e2b208fd899969e87cc67f1ab94150f8e Mon Sep 17 00:00:00 2001 From: stelmo Date: Sat, 7 Jan 2023 16:12:37 +0000 Subject: [PATCH 102/531] automatic formatting triggered by @stelmo on PR #716 --- src/moduletools.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/moduletools.jl b/src/moduletools.jl index 0c8231834..86911c038 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -60,7 +60,7 @@ using COBREXA.Types, COBREXA.Solver ``` """ -macro using_all() +macro using_all() quote using COBREXA.Types, COBREXA.Accessors, @@ -72,4 +72,4 @@ macro using_all() COBREXA.IO, COBREXA.Solver end -end \ No newline at end of file +end From 83daac52b91b64045b16038555ac85b31f0fe648 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 16 Jan 2023 12:38:51 +0100 Subject: [PATCH 103/531] draft an import-everything-module --- src/COBREXA.jl | 12 ++++++++++++ src/moduletools.jl | 47 ++++++++++++++++------------------------------ 2 files changed, 28 insertions(+), 31 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 09ea06382..cdd74ae78 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -51,4 +51,16 @@ using .ModuleTools @inc reconstruction @inc utils +module Everything + using ..ModuleTools + @reexport Accessors + @reexport Analysis + @reexport Analysis Modifications + @reexport IO + @reexport Reconstruction + @reexport Reconstruction Modifications + @reexport Solver + @reexport Utils +end + end # module diff --git a/src/moduletools.jl b/src/moduletools.jl index 86911c038..9d3e8cfd6 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -39,37 +39,22 @@ macro export_locals() end end -@export_locals +# re-export all imported things +# (many thanks to Reexport.jl for inspiration here!) +macro reexport(mods...) + importexpr = Expr(:import, Expr(:., :., :., mods...)) + modulename = foldl((l,r)->Expr(:., l,QuoteNode(r)), mods) + esc(quote + $importexpr + for sym in names($modulename) + Base.isexported($modulename, sym) || continue + typeof($(Expr(:., modulename, :sym))) == Module && continue + sym in [:eval, :include] && continue + @eval const $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) + @eval export $(Expr(:$, :sym)) + end + end) end -""" -@using_all() - -A convenience macro that brings the names of all the COBREXA modules into scope. - -When calling `COBREXA.@using_all` the following code gets injected: -``` -using COBREXA.Types, - COBREXA.Accessors, - COBREXA.Analysis, - COBREXA.Analysis.Modifications, - COBREXA.Reconstruction, - COBREXA.Reconstruction.Modifications, - COBREXA.Utils, - COBREXA.IO, - COBREXA.Solver -``` -""" -macro using_all() - quote - using COBREXA.Types, - COBREXA.Accessors, - COBREXA.Analysis, - COBREXA.Analysis.Modifications, - COBREXA.Reconstruction, - COBREXA.Reconstruction.Modifications, - COBREXA.Utils, - COBREXA.IO, - COBREXA.Solver - end +@export_locals end From 99978d7b3eaebfcfa61b3593be4441e21091717a Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 10:19:51 +0100 Subject: [PATCH 104/531] rename reconstruction mods to pipes (which they are) + some minor cleaning --- src/COBREXA.jl | 2 +- src/moduletools.jl | 2 +- src/reconstruction.jl | 8 ++++---- src/reconstruction/{modifications => pipes}/enzymes.jl | 0 src/reconstruction/{modifications => pipes}/generic.jl | 0 test/runtests.jl | 10 +--------- 6 files changed, 7 insertions(+), 15 deletions(-) rename src/reconstruction/{modifications => pipes}/enzymes.jl (100%) rename src/reconstruction/{modifications => pipes}/generic.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index cdd74ae78..3843bb886 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -58,7 +58,7 @@ module Everything @reexport Analysis Modifications @reexport IO @reexport Reconstruction - @reexport Reconstruction Modifications + @reexport Reconstruction Pipes @reexport Solver @reexport Utils end diff --git a/src/moduletools.jl b/src/moduletools.jl index 9d3e8cfd6..8717eec9d 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -43,7 +43,7 @@ end # (many thanks to Reexport.jl for inspiration here!) macro reexport(mods...) importexpr = Expr(:import, Expr(:., :., :., mods...)) - modulename = foldl((l,r)->Expr(:., l,QuoteNode(r)), mods) + modulename = foldl((l, r) -> Expr(:., l, QuoteNode(r)), mods) esc(quote $importexpr for sym in names($modulename) diff --git a/src/reconstruction.jl b/src/reconstruction.jl index b6a763d1d..266e7029f 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -24,7 +24,7 @@ using MacroTools @inc_dir reconstruction """ - module Modifications + module Pipes Functions that create model variants, typically for efficient use in [`screen`](@ref) and similar functions. @@ -32,7 +32,7 @@ Functions that create model variants, typically for efficient use in # Exports $(EXPORTS) """ -module Modifications +module Pipes using ..ModuleTools @dse end @@ -41,9 +41,9 @@ end end # this needs to import from Reconstruction -@inject Reconstruction.Modifications begin +@inject Reconstruction.Pipes begin using ..Reconstruction - @inc_dir reconstruction modifications + @inc_dir reconstruction pipes @export_locals end diff --git a/src/reconstruction/modifications/enzymes.jl b/src/reconstruction/pipes/enzymes.jl similarity index 100% rename from src/reconstruction/modifications/enzymes.jl rename to src/reconstruction/pipes/enzymes.jl diff --git a/src/reconstruction/modifications/generic.jl b/src/reconstruction/pipes/generic.jl similarity index 100% rename from src/reconstruction/modifications/generic.jl rename to src/reconstruction/pipes/generic.jl diff --git a/test/runtests.jl b/test/runtests.jl index 467d3325a..ca47cbe5f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,14 +1,6 @@ using COBREXA, Test -using COBREXA.Types, - COBREXA.Accessors, - COBREXA.Analysis, - COBREXA.Analysis.Modifications, - COBREXA.Reconstruction, - COBREXA.Reconstruction.Modifications, - COBREXA.Utils, - COBREXA.IO, - COBREXA.Solver +using COBREXA.Everything using Aqua using Clarabel From ae5bdf7bf8e0ef8cba01f2a0f53069ee24491dde Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 10:33:16 +0100 Subject: [PATCH 105/531] fix: yeah let's forget about types, that's the way --- src/COBREXA.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 3843bb886..936af8ed4 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -60,6 +60,7 @@ module Everything @reexport Reconstruction @reexport Reconstruction Pipes @reexport Solver + @reexport Types @reexport Utils end From c8280b24577b3362b69705e55b70e4189b6f21fb Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 11:50:58 +0100 Subject: [PATCH 106/531] clean up some stuff before cutting --- src/types/accessors/AbstractMetabolicModel.jl | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 64d6cb958..c71926ba4 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -3,8 +3,10 @@ # IMPORTANT # # This file provides a list of "officially supported" accessors that should -# work with all subtypes of [`AbstractMetabolicModel`](@ref). Keep this synced with the -# automatically derived methods for [`AbstractModelWrapper`](@ref). +# work with all subtypes of [`AbstractMetabolicModel`](@ref). +# +# Keep this synced with the automatically derived methods for +# [`AbstractModelWrapper`](@ref). # """ @@ -26,6 +28,15 @@ end """ $(TYPEDSIGNATURES) +Get the number of reactions in a model. +""" +function n_variables(a::AbstractMetabolicModel)::Int + length(variables(a)) +end + +""" +$(TYPEDSIGNATURES) + Return a vector of metabolite identifiers in a model. The vector precisely corresponds to the rows in [`stoichiometry`](@ref) matrix. @@ -39,15 +50,6 @@ end """ $(TYPEDSIGNATURES) -Get the number of reactions in a model. -""" -function n_variables(a::AbstractMetabolicModel)::Int - length(variables(a)) -end - -""" -$(TYPEDSIGNATURES) - Get the number of metabolites in a model. """ function n_metabolites(a::AbstractMetabolicModel)::Int @@ -108,12 +110,18 @@ reactions catalyzed by distinct enzymes, etc. Together with [`reaction_variables`](@ref) (and [`n_reactions`](@ref)) this specifies how the flux is decomposed into individual reactions. -By default (and in most models), fluxes and reactions perfectly correspond. +By default (and in most models), variables and reactions correspond one-to-one. """ function reactions(a::AbstractMetabolicModel)::Vector{String} variables(a) end +""" +$(TYPEDSIGNATURES) + +The number of recations in the model. The overload may be much more efficient +than measuring `length(reactions(model))`. +""" function n_reactions(a::AbstractMetabolicModel)::Int n_variables(a) end From 4f7535497ccece9a9a9011cdc53a2e565d1e7e73 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 14:54:21 +0100 Subject: [PATCH 107/531] improve a comment --- src/types/accessors/ModelWrapper.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 0f2e33c43..7d5a211be 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -11,7 +11,8 @@ end # # IMPORTANT # -# The list of inherited functions must be synced with the methods available for [`AbstractMetabolicModel`](@ref). +# The list of inherited functions must be synced with the methods available for +# [`AbstractMetabolicModel`](@ref). # @inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! From 8569bd8519874943dcce4f5cc57f81f6a9ceccac Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 14:54:30 +0100 Subject: [PATCH 108/531] fix name conflict --- src/types/models/MATModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index e1b6e84ef..e2d4c84d8 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -168,7 +168,7 @@ $(TYPEDSIGNATURES) Extract metabolite compartment from `metCompartment` or `metCompartments`. """ -function metabolite_compartment(m::MATModel, mid::String) +function Accessors.metabolite_compartment(m::MATModel, mid::String) res = maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metcompartments), From 55cf111df368ed2f0676f9361e9eaac1f1b27639 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 14:54:49 +0100 Subject: [PATCH 109/531] fix merge problems with SBMLmodel --- src/types/models/SBMLModel.jl | 8 ++++---- test/analysis/envelopes.jl | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index e54d456c2..c9cf5b05c 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -41,9 +41,9 @@ end """ $(TYPEDSIGNATURES) -Get reactions from a [`SBMLModel`](@ref). +Get variables (aka reactions) from a [`SBMLModel`](@ref). """ -Accessors.reactions(model::SBMLModel)::Vector{String} = model.reaction_ids +Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids """ $(TYPEDSIGNATURES) @@ -55,9 +55,9 @@ Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids """ $(TYPEDSIGNATURES) -Efficient counting of reactions in [`SBMLModel`](@ref). +Efficient counting of variables (aka reactions) in [`SBMLModel`](@ref). """ -Accessors.n_reactions(model::SBMLModel)::Int = length(model.reaction_ids) +Accessors.n_variables(model::SBMLModel)::Int = length(model.reaction_ids) """ $(TYPEDSIGNATURES) diff --git a/test/analysis/envelopes.jl b/test/analysis/envelopes.jl index ee0138a85..9a5d8d746 100644 --- a/test/analysis/envelopes.jl +++ b/test/analysis/envelopes.jl @@ -5,7 +5,7 @@ rxns = [1, 2, 3] lat = collect.(envelope_lattice(m, rxns; samples = 3)) - @test lat == [[0, 500, 1000], [-1000, 0, 1000], [-1000, 0, 1000]] + @test lat == [[-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0]] @test lat == collect.(envelope_lattice(m, variables(m)[rxns]; samples = 3)) vals = From d2d237080eb4f8356e1f5d73fb66b7a067a6082c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 23 Jan 2023 14:54:57 +0100 Subject: [PATCH 110/531] fix merge leftover in model IO tests --- test/io/mat.jl | 2 +- test/io/sbml.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/test/io/mat.jl b/test/io/mat.jl index 80a8fbcc7..cd0beed82 100644 --- a/test/io/mat.jl +++ b/test/io/mat.jl @@ -15,7 +15,7 @@ end end @testset "Import yeast-GEM (mat)" begin - m = load_model(StandardModel, model_paths["yeast-GEM.mat"]) + m = load_model(ObjectModel, model_paths["yeast-GEM.mat"]) @test n_metabolites(m) == 2744 @test n_reactions(m) == 4063 @test n_genes(m) == 1160 diff --git a/test/io/sbml.jl b/test/io/sbml.jl index 901d9e974..c9e9abd0c 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -26,7 +26,7 @@ end end @testset "Import yeast-GEM (sbml)" begin - m = load_model(StandardModel, model_paths["yeast-GEM.xml"]) + m = load_model(ObjectModel, model_paths["yeast-GEM.xml"]) @test n_metabolites(m) == 2744 @test n_reactions(m) == 4063 @test n_genes(m) == 1160 From 0fbf78c49ee767a00b6febd9ea6d46dbe0978dd5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Feb 2023 14:02:30 +0100 Subject: [PATCH 111/531] bump SBML dependency --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 95a32d321..5e4794f4b 100644 --- a/Project.toml +++ b/Project.toml @@ -32,7 +32,7 @@ JuMP = "1" MAT = "0.10" MacroTools = "0.5.6" OrderedCollections = "1.4" -SBML = "~1.3" +SBML = "~1.3, ~1.4" StableRNGs = "1.0" Tulip = "0.7.0, 0.8.0, 0.9.2" julia = "1.5" From c8a167e1cc2e208dd39fd46dfda0290e62d11894 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Feb 2023 14:02:51 +0100 Subject: [PATCH 112/531] mark modules to avoid chaos --- src/types.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types.jl b/src/types.jl index e53077459..5c4b89469 100644 --- a/src/types.jl +++ b/src/types.jl @@ -24,7 +24,7 @@ using SparseArrays @inc_dir types abstract @export_locals -end +end # module Types """ module Accessors @@ -65,7 +65,7 @@ using .Internal @inc_dir types accessors @export_locals -end +end # module Accessors # the modules depend on each other so we have to inject the stuff like this @inject Types begin From d13b9c4db1c285109c79bd081aaea035117ca7b1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Feb 2023 14:03:09 +0100 Subject: [PATCH 113/531] move accessors misc to bits (it's a pre-include) --- src/types.jl | 6 ++---- src/types/accessors/{misc => bits}/missing_impl.jl | 0 2 files changed, 2 insertions(+), 4 deletions(-) rename src/types/accessors/{misc => bits}/missing_impl.jl (100%) diff --git a/src/types.jl b/src/types.jl index 5c4b89469..c496aaf2d 100644 --- a/src/types.jl +++ b/src/types.jl @@ -54,10 +54,8 @@ $(EXPORTS) module Internal using ..ModuleTools @dse - # TODO: Note to self: we might be a bit more systematic here -- these are - # "pre-includes" (might go into bits/), contrasting to "post-includes" (which - # may stay in misc/) - @inc_dir types accessors misc + import ..Accessors + @inc_dir types accessors bits @export_locals end diff --git a/src/types/accessors/misc/missing_impl.jl b/src/types/accessors/bits/missing_impl.jl similarity index 100% rename from src/types/accessors/misc/missing_impl.jl rename to src/types/accessors/bits/missing_impl.jl From c00e228d54a5e97984e7ff9bf4d6849806424417 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Feb 2023 14:03:35 +0100 Subject: [PATCH 114/531] extensible variable semantics (prototype) --- src/types/accessors/AbstractMetabolicModel.jl | 60 +++----- src/types/accessors/bits/semantics.jl | 135 ++++++++++++++++++ 2 files changed, 155 insertions(+), 40 deletions(-) create mode 100644 src/types/accessors/bits/semantics.jl diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index c71926ba4..d7f8b0d3c 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -100,46 +100,26 @@ function objective(a::AbstractMetabolicModel)::SparseVec missing_impl_error(objective, (a,)) end -""" -$(TYPEDSIGNATURES) - -In some models, the [`variables`](@ref) that correspond to the columns of -[`stoichiometry`](@ref) matrix do not fully represent the semantic contents of -the model; for example, fluxes may be split into forward and reverse reactions, -reactions catalyzed by distinct enzymes, etc. Together with -[`reaction_variables`](@ref) (and [`n_reactions`](@ref)) this specifies how the -flux is decomposed into individual reactions. - -By default (and in most models), variables and reactions correspond one-to-one. -""" -function reactions(a::AbstractMetabolicModel)::Vector{String} - variables(a) -end - -""" -$(TYPEDSIGNATURES) - -The number of recations in the model. The overload may be much more efficient -than measuring `length(reactions(model))`. -""" -function n_reactions(a::AbstractMetabolicModel)::Int - n_variables(a) -end - -""" -$(TYPEDSIGNATURES) - -Retrieve a sparse matrix that describes the correspondence of a solution of the -linear system to the fluxes (see [`reactions`](@ref) for rationale). Returns a -sparse matrix of size `(n_variables(a), n_reactions(a))`. For most models, this is -an identity matrix. -""" -function reaction_variables(a::AbstractMetabolicModel)::SparseMat - nr = n_variables(a) - nf = n_reactions(a) - nr == nf || missing_impl_error(reaction_variables, (a,)) - spdiagm(fill(1, nr)) -end +@make_variable_semantics( + :reaction, + "reaction fluxes", + """ +Typically, one variable describes one reaction flux; but in specific cases the +variables may have separate meanings (as with enzyme supply reactions and +various helper values), and multiple variables may combine into one reaction +flux, such as with separate bidirectional reactions. +""" +) + +@make_variable_semantics( + :enzymes, + "enzyme supplies", + """ +Certain model types define a supply of enzymes that is typically required to +"run" the reactions; enzyme supplies define variables that are used to model +these values. +""" +) """ $(TYPEDSIGNATURES) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl new file mode 100644 index 000000000..ff21e74de --- /dev/null +++ b/src/types/accessors/bits/semantics.jl @@ -0,0 +1,135 @@ +function make_mapping_mtx(rows, cols, col_row_val) + rowidx = Dict(rows .=> 1:length(rows)) + colidx = Dict(cols .=> 1:length(cols)) + n = sum(length.(values(col_row_val))) + R = Vector{Int}(undef, n) + C = Vector{Int}(undef, n) + V = Vector{Float64}(undef, n) + i = 1 + for (cid, col_val) in col_row_val + for (rid, val) in col_val + R[i] = rowidx[rid] + C[i] = colidx[cid] + V[i] = val + i += 1 + end + end + sparse(R, C, V, length(rows), length(cols)) +end + +const variable_semantics = Symbol[] + +using ..Accessors + +function make_variable_semantics( + themodule::Module, + source, + sym::Symbol, + name::String, + example::String, +) + plural = Symbol(sym, :s) + count = Symbol(:n_, plural) + mapping = Symbol(plural, :_variables) + mapping_mtx = Symbol(plural, :_variables_matrix) + push!(themodule.Internal.variable_semantics, sym) + + pluralfn = Expr( + :macrocall, + Symbol("@doc"), + source, + Expr( + :string, + :TYPEDSIGNATURES, + """ + +List the semantics for model variables that can be interpreted as $name. + +$example + +Use [`$count`](@ref) to quickly determine the amount of $name in the +model. See the documentation of [`$mapping`](@ref) for closer +definition of the correspondence of $name and model variables. +""", + ), + :(function $plural(a::AbstractMetabolicModel)::Vector{String} + x = collect(keys($mapping)) + sort!(x) + x + end), + ) + + countfn = Expr( + :macrocall, + Symbol("@doc"), + source, + Expr( + :string, + :TYPEDSIGNATURES, + """ + +Count of $name that the model describes, should be equal to the length of +vector returned by [`$plural`]. +""", + ), + :(function $count(a::AbstractMetabolicModel)::Int + length($mapping) + end), + ) + + mappingfn = Expr( + :macrocall, + Symbol("@doc"), + source, + Expr( + :string, + :TYPEDSIGNATURES, + """ + +Bipartite mapping of $name described by the model to the actual +variables in the model. Returns a dictionary of $name assigned to the +variable IDs and their linear coefficients. See the documentation of +[`$plural`](@ref) for semantics. + +To improve the performance, you may want to use [`$mapping_mtx`](@ref). +""", + ), + :(function $mapping(a::AbstractMetabolicModel)::Dict{String,Dict{String,Float64}} + Dict() + end), + ) + + mtxfn = Expr( + :macrocall, + Symbol("@doc"), + source, + Expr( + :string, + :TYPEDSIGNATURES, + """ + +Bipartite mapping of $name described by the model to the actual +variables in the model, described as a sparse matrix mapping with rows +corresponding to model variables and columns corresponding to $name. + +By default, this is derived from [`$mapping`](@ref) in all models. For +safety reasons, this is never automatically inherited by wrappers. +""", + ), + :(function $mapping_mtx(a::AbstractMetabolicModel)::SparseMat + make_mapping_mtx(variables(a), $plural(a), $mapping(a)) + end), + ) + + code = Expr(:block, pluralfn, countfn, mappingfn, mtxfn) + + Base.eval(themodule, code) +end + +macro make_variable_semantics(sym, name, example) + src = __source__ + # TODO actually use the source + quote + $make_variable_semantics($Accessors, $src, $sym, $name, $example) + end +end From 7012d151371820c6b52c14cc51340347ea2f8242 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Feb 2023 15:46:21 +0100 Subject: [PATCH 115/531] actually add reactions to models --- src/types.jl | 1 + src/types/accessors/bits/semantics.jl | 11 +++++++++++ src/types/models/HDF5Model.jl | 2 ++ src/types/models/JSONModel.jl | 2 ++ src/types/models/MATModel.jl | 2 ++ src/types/models/MatrixModel.jl | 2 ++ src/types/models/ObjectModel.jl | 2 ++ src/types/models/SBMLModel.jl | 23 ++--------------------- 8 files changed, 24 insertions(+), 21 deletions(-) diff --git a/src/types.jl b/src/types.jl index c496aaf2d..31f186b7f 100644 --- a/src/types.jl +++ b/src/types.jl @@ -55,6 +55,7 @@ module Internal using ..ModuleTools @dse import ..Accessors + using SparseArrays @inc_dir types accessors bits @export_locals end diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index ff21e74de..e2bca503a 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -133,3 +133,14 @@ macro make_variable_semantics(sym, name, example) $make_variable_semantics($Accessors, $src, $sym, $name, $example) end end + +macro all_variables_are_reactions(mt) + m = esc(mt) + # TODO docs + quote + $Accessors.reactions(model::$m) = $Accessors.variables(model) + $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) + $Accessors.reactions_variables(model::$m) = Dict(var => Dict(var, 1.0) for var=variables(model)) + $Accessors.reactions_variables_matrix(model::$m) = $SparseArrays.spdiagm(fill(1, $Accessors.n_variables(model))) + end +end diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index ab217176f..3fb8a20d3 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -44,6 +44,8 @@ function Accessors.variables(model::HDF5Model)::Vector{String} read(model.h5["reactions"]) end +Accessors.Internal.@all_variables_are_reactions HDF5Model + function Accessors.n_metabolites(model::HDF5Model)::Int precache!(model) length(model.h5["metabolites"]) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index e1b938234..0ec7d32bf 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -99,6 +99,8 @@ Extract gene names from a JSON model. Accessors.genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] +Accessors.Internal.@all_variables_are_reactions JSONModel + """ $(TYPEDSIGNATURES) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index e2d4c84d8..294075f59 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -26,6 +26,8 @@ function Accessors.variables(m::MATModel)::Vector{String} end end +Accessors.Internal.@all_variables_are_reactions MATModel + """ $(TYPEDSIGNATURES) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index c8618a531..e4dbfc681 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -54,6 +54,8 @@ Get the reactions in a `MatrixModel`. """ Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns +Accessors.Internal.@all_variables_are_reactions MatrixModel + """ $(TYPEDSIGNATURES) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 1f27b2c66..59efa9c61 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -66,6 +66,8 @@ Return the number of reactions contained in `model`. """ Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) +Accessors.Internal.@all_variables_are_reactions ObjectModel + """ $(TYPEDSIGNATURES) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index c9cf5b05c..9f87f8c0a 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -45,6 +45,8 @@ Get variables (aka reactions) from a [`SBMLModel`](@ref). """ Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids +Accessors.Internal.@all_variables_are_reactions SBMLModel + """ $(TYPEDSIGNATURES) @@ -55,20 +57,6 @@ Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids """ $(TYPEDSIGNATURES) -Efficient counting of variables (aka reactions) in [`SBMLModel`](@ref). -""" -Accessors.n_variables(model::SBMLModel)::Int = length(model.reaction_ids) - -""" -$(TYPEDSIGNATURES) - -Efficient counting of metabolites in [`SBMLModel`](@ref). -""" -Accessors.n_metabolites(model::SBMLModel)::Int = length(model.metabolite_ids) - -""" -$(TYPEDSIGNATURES) - Recreate the stoichiometry matrix from the [`SBMLModel`](@ref). """ function Accessors.stoichiometry(model::SBMLModel)::SparseMat @@ -211,13 +199,6 @@ Accessors.genes(model::SBMLModel)::Vector{String} = model.gene_ids """ $(TYPEDSIGNATURES) -Get number of genes in [`SBMLModel`](@ref). -""" -Accessors.n_genes(model::SBMLModel)::Int = length(model.gene_ids) - -""" -$(TYPEDSIGNATURES) - Retrieve the reaction gene associations from [`SBMLModel`](@ref). """ Accessors.reaction_gene_associations( From a9755981cece5f97f2967d06a6ee84696e8845e7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Feb 2023 11:05:57 +0100 Subject: [PATCH 116/531] format --- src/types/accessors/bits/semantics.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index e2bca503a..8ddf71881 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -140,7 +140,9 @@ macro all_variables_are_reactions(mt) quote $Accessors.reactions(model::$m) = $Accessors.variables(model) $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) - $Accessors.reactions_variables(model::$m) = Dict(var => Dict(var, 1.0) for var=variables(model)) - $Accessors.reactions_variables_matrix(model::$m) = $SparseArrays.spdiagm(fill(1, $Accessors.n_variables(model))) + $Accessors.reactions_variables(model::$m) = + Dict(var => Dict(var, 1.0) for var in variables(model)) + $Accessors.reactions_variables_matrix(model::$m) = + $SparseArrays.spdiagm(fill(1, $Accessors.n_variables(model))) end end From fd8438b43ae0b59d60b44f6d125790e0adac4343 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Feb 2023 11:38:38 +0100 Subject: [PATCH 117/531] fix tests --- src/analysis/flux_variability_analysis.jl | 4 ++-- src/analysis/sampling/affine_hit_and_run.jl | 2 +- src/solver.jl | 6 ++++-- src/types/accessors/bits/semantics.jl | 12 ++++++------ src/types/models/BalancedGrowthCommunityModel.jl | 5 +++-- src/types/wrappers/EnzymeConstrainedModel.jl | 6 ++++-- .../wrappers/SimplifiedEnzymeConstrainedModel.jl | 6 ++++-- src/utils/BalancedGrowthCommunityModel.jl | 4 ++-- 8 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/flux_variability_analysis.jl index f9d97ff2b..6f700eec7 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/flux_variability_analysis.jl @@ -115,7 +115,7 @@ function flux_variability_analysis( flux_variability_analysis( model, - reaction_variables(model)[:, flux_indexes], + reaction_variables_matrix(model)[:, flux_indexes], optimizer; kwargs..., ) @@ -128,7 +128,7 @@ A simpler version of [`flux_variability_analysis`](@ref) that maximizes and minimizes all declared fluxes in the model. Arguments are forwarded. """ flux_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = - flux_variability_analysis(model, reaction_variables(model), optimizer; kwargs...) + flux_variability_analysis(model, reaction_variables_matrix(model), optimizer; kwargs...) """ $(TYPEDSIGNATURES) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index cfaec932a..072ff0d09 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -27,7 +27,7 @@ warmup_points = warmup_from_variability(model, GLPK.Optimizer) samples = affine_hit_and_run(model, warmup_points, sample_iters = 101:105) # convert the result to flux (for models where the distinction matters): -fluxes = reaction_variables(model)' * samples +fluxes = reaction_variables_matrix(model)' * samples ``` """ function affine_hit_and_run( diff --git a/src/solver.jl b/src/solver.jl index e38df81f6..759a85b10 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -125,7 +125,8 @@ flux_vector(flux_balance_analysis(model, ...)) ``` """ flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? reaction_variables(model)' * value.(opt_model[:x]) : nothing + is_solved(opt_model) ? reaction_variables_matrix(model)' * value.(opt_model[:x]) : + nothing """ $(TYPEDSIGNATURES) @@ -139,7 +140,8 @@ flux_dict(model, flux_balance_analysis(model, ...)) """ flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = is_solved(opt_model) ? - Dict(reactions(model) .=> reaction_variables(model)' * value.(opt_model[:x])) : nothing + Dict(reactions(model) .=> reaction_variables_matrix(model)' * value.(opt_model[:x])) : + nothing """ $(TYPEDSIGNATURES) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 8ddf71881..e6ff48660 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -30,8 +30,8 @@ function make_variable_semantics( ) plural = Symbol(sym, :s) count = Symbol(:n_, plural) - mapping = Symbol(plural, :_variables) - mapping_mtx = Symbol(plural, :_variables_matrix) + mapping = Symbol(sym, :_variables) + mapping_mtx = Symbol(sym, :_variables_matrix) push!(themodule.Internal.variable_semantics, sym) pluralfn = Expr( @@ -140,9 +140,9 @@ macro all_variables_are_reactions(mt) quote $Accessors.reactions(model::$m) = $Accessors.variables(model) $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) - $Accessors.reactions_variables(model::$m) = - Dict(var => Dict(var, 1.0) for var in variables(model)) - $Accessors.reactions_variables_matrix(model::$m) = - $SparseArrays.spdiagm(fill(1, $Accessors.n_variables(model))) + $Accessors.reaction_variables(model::$m) = + Dict(var => Dict(var => 1.0) for var in variables(model)) + $Accessors.reaction_variables_matrix(model::$m) = + $SparseArrays.spdiagm(fill(1.0, $Accessors.n_variables(model))) end end diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 019608c49..97533da34 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -232,8 +232,9 @@ Returns a matrix, which when multipled by the solution of a constraints based problem, yields the semantically meaningful fluxes that correspond to [`reactions`](@ref). """ -function Accessors.reaction_variables(cm::BalancedGrowthCommunityModel) - rfs = blockdiag([reaction_variables(m.model) for m in cm.members]...) +function Accessors.reaction_variables_matrix(cm::BalancedGrowthCommunityModel) + # TODO add the non-matrix form! + rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) nr = length(get_env_mets(cm)) + 1 # env ex + obj blockdiag(rfs, spdiagm(fill(1, nr))) end diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index adb350bc6..35557e6af 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -163,8 +163,10 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to the original fluxes in the wrapped model. """ -function Accessors.reaction_variables(model::EnzymeConstrainedModel) - rxnmat = enzyme_constrained_column_reactions(model)' * reaction_variables(model.inner) +function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) + # TODO this needs the normal reaction_variables overload + rxnmat = + enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) [ rxnmat spzeros(n_genes(model), size(rxnmat, 2)) diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 684d6cc18..904a40e87 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -118,8 +118,10 @@ $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the wrapped model. """ -Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = - simplified_enzyme_constrained_column_reactions(model)' * reaction_variables(model.inner) +Accessors.reaction_variables_matrix(model::SimplifiedEnzymeConstrainedModel) = + simplified_enzyme_constrained_column_reactions(model)' * + reaction_variables_matrix(model.inner) +# TODO: convert to dict representation!!! """ $(TYPEDSIGNATURES) diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl index 5262bb5f8..52cbbba7f 100644 --- a/src/utils/BalancedGrowthCommunityModel.jl +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -14,7 +14,7 @@ get_solution( Dict( string(last(split(rid, community_member.id * "#"))) => val for (rid, val) in zip( reactions(community_model), - reaction_variables(community_model)' * value.(opt_model[:x]), + reaction_variables_matrix(community_model)' * value.(opt_model[:x]), ) if startswith(rid, community_member.id * "#") ) : nothing @@ -30,6 +30,6 @@ get_environmental_exchanges(community_model::BalancedGrowthCommunityModel, opt_m Dict( rid => val for (rid, val) in zip( reactions(community_model), - reaction_variables(community_model)' * value.(opt_model[:x]), + reaction_variables_matrix(community_model)' * value.(opt_model[:x]), ) if !any(startswith(rid, cm.id * "#") for cm in community_model.members) ) : nothing From 1ff23f244586d3f76aaf97cf60b4d2ee54714322 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Feb 2023 11:45:00 +0100 Subject: [PATCH 118/531] english, julia! do you speak it? --- docs/src/examples/14_simplified_enzyme_constrained.jl | 4 ++-- docs/src/examples/15_enzyme_constrained.jl | 2 +- src/reconstruction/pipes/enzymes.jl | 4 ++-- src/types/wrappers/EnzymeConstrainedModel.jl | 2 +- src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 2 +- test/reconstruction/simplified_enzyme_constrained.jl | 2 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/src/examples/14_simplified_enzyme_constrained.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl index a008c7a1b..99484c3a4 100644 --- a/docs/src/examples/14_simplified_enzyme_constrained.jl +++ b/docs/src/examples/14_simplified_enzyme_constrained.jl @@ -60,14 +60,14 @@ rxn_isozymes = Dict( # with additional sMOMENT structure: simplified_enzyme_constrained_model = - model |> with_simplified_enzyme_constrained( + model |> with_simplified_enzyme_constraints( reaction_isozyme = rxn_isozymes, gene_product_molar_mass = gene_product_masses, total_enzyme_capacity = 50.0, ) # (You could alternatively use the [`make_simplified_enzyme_constrained_model`](@ref) to create the -# structure more manually; but [`with_simplified_enzyme_constrained`](@ref) is easier to use e.g. +# structure more manually; but [`with_simplified_enzyme_constraints`](@ref) is easier to use e.g. # with [`screen`](@ref).) # In turn, you should have a complete model with unidirectional reactions and diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index a670080ba..5d2968016 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -58,7 +58,7 @@ gene_product_bounds = Dict(genes(model) .=> Ref((0.0, 10.0))) # information is straightforward: enzyme_constrained_model = - model |> with_enzyme_constrained(; + model |> with_enzyme_constraints(; reaction_isozymes = rxn_isozymes, gene_product_bounds, gene_product_molar_mass = gene_product_masses, diff --git a/src/reconstruction/pipes/enzymes.jl b/src/reconstruction/pipes/enzymes.jl index 10d1e0119..4aac0b119 100644 --- a/src/reconstruction/pipes/enzymes.jl +++ b/src/reconstruction/pipes/enzymes.jl @@ -5,7 +5,7 @@ Specifies a model variant which adds extra semantics of the sMOMENT algorithm, giving a [`SimplifiedEnzymeConstrainedModel`](@ref). The arguments are forwarded to [`make_simplified_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_simplified_enzyme_constrained(; kwargs...) = +with_simplified_enzyme_constraints(; kwargs...) = model -> make_simplified_enzyme_constrained_model(model; kwargs...) """ @@ -15,7 +15,7 @@ Specifies a model variant which adds extra semantics of the EnzymeConstrained al giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to [`make_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_enzyme_constrained(; kwargs...) = +with_enzyme_constraints(; kwargs...) = model -> make_enzyme_constrained_model(model; kwargs...) """ diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 35557e6af..9f03b223d 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -40,7 +40,7 @@ A model with complex enzyme concentration and capacity bounds, as described in genome-scale metabolic model by incorporating enzymatic constraints." Molecular systems biology 13.8 (2017): 935.* -Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constrained`](@ref) to construct this kind +Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constraints`](@ref) to construct this kind of model. The model wraps another "internal" model, and adds following modifications: diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 904a40e87..6c5cce372 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -22,7 +22,7 @@ An enzyme-capacity-constrained model using sMOMENT algorithm, as described by *Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of metabolic models with enzyme constraints" BMC bioinformatics, 2020*. -Use [`make_simplified_enzyme_constrained_model`](@ref) or [`with_simplified_enzyme_constrained`](@ref) to construct the +Use [`make_simplified_enzyme_constrained_model`](@ref) or [`with_simplified_enzyme_constraints`](@ref) to construct the models. The model is constructed as follows: diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 592d8a47b..e8cd20a92 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -45,7 +45,7 @@ lower = [-1000.0, -1.0], upper = [nothing, 12.0], ) |> - with_enzyme_constrained( + with_enzyme_constraints( gene_product_mass_group_bound = Dict( "uncategorized" => total_gene_product_mass, ), diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 81e95b9bc..c4c064dc1 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -36,7 +36,7 @@ lower = [-1000.0, -1.0], upper = [nothing, 12.0], ) |> - with_simplified_enzyme_constrained(total_enzyme_capacity = 100.0) + with_simplified_enzyme_constraints(total_enzyme_capacity = 100.0) rxn_fluxes = flux_balance_analysis_dict( simplified_enzyme_constrained_model, diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 295c50720..df2c32060 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -306,7 +306,7 @@ end ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> with_changed_bounds(["EX_glc__D_e"]; lower = [-1000.0], upper = [0]) |> - with_enzyme_constrained( + with_enzyme_constraints( gene_product_mass_group_bound = Dict("uncategorized" => 100.0), ) From efb6ed866f811d9a1af5ca83a398e8a4492679d7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 12:10:13 +0100 Subject: [PATCH 119/531] provide the correct base functions for enzyme models --- src/types.jl | 1 + src/types/accessors/bits/semantics.jl | 11 ++++++++++ src/types/wrappers/EnzymeConstrainedModel.jl | 18 ++++++++++++++--- .../SimplifiedEnzymeConstrainedModel.jl | 20 ++++++++++++++++--- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/src/types.jl b/src/types.jl index 31f186b7f..6eeab2a45 100644 --- a/src/types.jl +++ b/src/types.jl @@ -54,6 +54,7 @@ $(EXPORTS) module Internal using ..ModuleTools @dse + import ...Types import ..Accessors using SparseArrays @inc_dir types accessors bits diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index e6ff48660..1a9fe1a51 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -17,6 +17,17 @@ function make_mapping_mtx(rows, cols, col_row_val) sparse(R, C, V, length(rows), length(cols)) end +function make_mapping_dict( + vars, + semantics, + mtx::Types.SparseMat, +)::Dict{String,Dict{String,Float64}} + Dict( + sid => Dict(vars[vidx] => val for (vidx, val) in zip(findnz(mtx[:, sidx])...)) for + (sidx, sid) in enumerate(semantics) + ) +end + const variable_semantics = Symbol[] using ..Accessors diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 9f03b223d..f4e12896f 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -160,11 +160,10 @@ end """ $(TYPEDSIGNATURES) -Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to the original -fluxes in the wrapped model. +Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to +the original fluxes in the wrapped model (as a matrix). """ function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) - # TODO this needs the normal reaction_variables overload rxnmat = enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) [ @@ -176,6 +175,19 @@ end """ $(TYPEDSIGNATURES) +Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to +the original fluxes in the wrapped model +""" +Accessors.reaction_variables(model::EnzymeConstrainedModel) = + Accessors.Internal.make_mapping_dict( + variables(model), + semantics(model), + reaction_variables_matrix(model), + ) # TODO currently inefficient + +""" +$(TYPEDSIGNATURES) + Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the coupling of the wrapped model, coupling for split (arm) reactions, and the coupling for the total enzyme capacity. diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 6c5cce372..6af2d9f8f 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -115,13 +115,27 @@ Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Get the mapping of the reaction rates in [`SimplifiedEnzymeConstrainedModel`](@ref) to the original -fluxes in the wrapped model. +Get the mapping of the reaction rates in +[`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the +wrapped model (as a matrix). """ Accessors.reaction_variables_matrix(model::SimplifiedEnzymeConstrainedModel) = simplified_enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) -# TODO: convert to dict representation!!! + +""" +$(TYPEDSIGNATURES) + +Get the mapping of the reaction rates in +[`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the +wrapped model. +""" +Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = + Accessors.Internal.make_mapping_dict( + variables(model), + semantics(model), + reaction_variables_matrix(model), + ) # TODO currently inefficient """ $(TYPEDSIGNATURES) From 01392853d62089bc5d3f129c42fe6a2cf1765937 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 15:43:52 +0100 Subject: [PATCH 120/531] rework the variability for the generalized semantics (wip) --- ...ty_analysis.jl => variability_analysis.jl} | 210 +++++++++--------- src/types/accessors/bits/semantics.jl | 17 +- 2 files changed, 124 insertions(+), 103 deletions(-) rename src/analysis/{flux_variability_analysis.jl => variability_analysis.jl} (54%) diff --git a/src/analysis/flux_variability_analysis.jl b/src/analysis/variability_analysis.jl similarity index 54% rename from src/analysis/flux_variability_analysis.jl rename to src/analysis/variability_analysis.jl index 6f700eec7..5bebc7b83 100644 --- a/src/analysis/flux_variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -2,7 +2,7 @@ $(TYPEDSIGNATURES) Flux variability analysis solves a pair of optimization problems in `model` for -each flux `f` described in `reactions`: +each reaction flux `f` described by [`reactions`](@ref): ``` min,max fᵀxᵢ s.t. S x = b @@ -14,19 +14,108 @@ where Z₀:= cᵀx₀ is the objective value of an optimal solution of the assoc FBA problem (see [`flux_balance_analysis`](@ref) for a related analysis, also for explanation of the `modifications` argument). -The `bounds` is a user-supplied function that specifies the objective bounds -for the variability optimizations, by default it restricts the flux objective -value to the precise optimum reached in FBA. It can return `-Inf` and `Inf` in -first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and -[`objective_bounds`](@ref) for simple bounds. +This is the simplest overload of the function which forwards the arguments to +more complicated ones; see documentation of [`variability_analysis`](@ref) for +details. Arguments `reaction_ids` and `reaction_indexes` may be used to +restrict the analysis to a specified subset of reactions. + +# Example +``` +model = load_model("e_coli_core.json") +flux_variability_analysis(model, GLPK.optimizer) +``` +""" +function flux_variability_analysis( + model::AbstractMetabolicModel, + optimizer; + reaction_ids::Maybe{Vector{String}} = nothing, + reaction_indexes::Maybe{Vector{Int}} = nothing, + kwargs..., +) + variability_analysis( + Val{:reaction}, + model, + optimizer; + ids = reaction_ids, + indexes = reaction_indexes, + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +Run the variability analysis over a selected semantics defined by a symbol, +such as `:reaction`. All other arguments are forwarded. +""" +variability_analysis(semantics::Symbol, args...; kwargs...) = + variability_analysis(Val(semantics), args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +A variability analysis over a selected semantics, picking up only objects +specified by IDs or indexes from the selected semantics. For semantics +`Val(:reaction)`, this is equivalent to [`flux_variability_analysis`](@ref). +""" +function variability_analysis( + semantics::Val{Semantics}, + model::AbstractMetabolicModel, + optimizer; + ids::Maybe{Vector{String}} = nothing, + indexes::Maybe{Vector{Int}} = nothing, + kwargs..., +) where {Semantics} + sem = Accessors.Internal.get_semantics(semantics) + isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) + (sem_ids, n_ids, _, sem_varmtx) = sem + + if isnothing(indexes) + idxs = if isnothing(ids) + collect(1:n_ids(model)) + else + findindex(ids, sem_ids(model)) + end + any(isnothing.(idxs)) && + throw(DomainError(ids[isnothing(idxs)], "Unknown IDs specified")) + indexes = Int.(idxs) + end + + if any((indexes .< 1) .| (indexes .> n_ids(model))) + throw(DomainError(indexes, "Index out of range")) + end + + variability_analysis( + model, + optimizer; + directions = sem_varmtx(model)[:, indexes], + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +A generic variability analysis function that maximizes and minimizes the flux +in the directions defined by colums of matrix `directions`. + +Argument `modifications` is applied to the model as in +[`flux_balance_analysis`](@ref). + +The `bounds` argument is a user-supplied function that specifies the objective +bounds for the variability optimizations as a tuple of minimum and maximum. By +default, it restricts the flux objective value to be greater or equal to the +optimum reached in flux balance analysis. It can return `-Inf` or `Inf` in the +first or second field to completely ignore the limit. Use +[`gamma_bounds`](@ref) and [`objective_bounds`](@ref) for simple bounds. `optimizer` must be set to a `JuMP`-compatible optimizer. The computation of the individual optimization problems is transparently distributed to `workers` -(see `Distributed.workers()`). The value of Z₀ can be optionally supplied in -argument `optimal_objective_value`, which prevents this function from calling -the non-parallelizable FBA. Separating the single-threaded FBA and -multithreaded variability computation can be used to improve resource -allocation efficiency in many common use-cases. +(see `Distributed.workers()`). The value of optimal flux can be optionally +supplied in argument `optimal_objective_value`, which prevents this function +from calling the non-parallelizable flux balance analysis. Separating the +single-threaded FBA and multithreaded variability computation can be used to +improve resource allocation efficiency in many common use-cases. `ret` is a function used to extract results from optimized JuMP models of the individual fluxes. By default, it calls and returns the value of @@ -35,31 +124,25 @@ a function that returns a more elaborate data structure; such as `m -> (JuMP.objective_value(m), JuMP.value.(m[:x]))`. Returns a matrix of extracted `ret` values for minima and maxima, of total size -(`size(fluxes,2)`,2). The optimizer result status is checked with +(`size(directions,2)`,2). The optimizer result status is checked with [`is_solved`](@ref); `nothing` is returned if the optimization failed for any reason. - -# Example -``` -model = load_model("e_coli_core.json") -flux_variability_analysis(model, GLPK.optimizer) -``` """ -function flux_variability_analysis( +function variability_analysis( model::AbstractMetabolicModel, - fluxes::SparseMat, optimizer; + directions::SparseMat, modifications = [], workers = [myid()], optimal_objective_value = nothing, bounds = z -> (z, Inf), ret = objective_value, ) - if size(fluxes, 1) != n_variables(model) + if size(objectives, 1) != n_variables(model) throw( DomainError( - size(fluxes, 1), - "Flux matrix size is not compatible with model reaction count.", + size(directions, 1), + "Directions matrix size is not compatible with model variable count.", ), ) end @@ -71,7 +154,7 @@ function flux_variability_analysis( ) : optimal_objective_value, ) - flux_vector = [fluxes[:, i] for i = 1:size(fluxes, 2)] + flux_vector = [directions[:, i] for i = 1:size(directions, 2)] return screen_optmodel_modifications( model, @@ -101,39 +184,8 @@ end """ $(TYPEDSIGNATURES) -An overload of [`flux_variability_analysis`](@ref) that explores the fluxes specified by integer indexes -""" -function flux_variability_analysis( - model::AbstractMetabolicModel, - flux_indexes::Vector{Int}, - optimizer; - kwargs..., -) - if any((flux_indexes .< 1) .| (flux_indexes .> n_reactions(model))) - throw(DomainError(flux_indexes, "Flux index out of range")) - end - - flux_variability_analysis( - model, - reaction_variables_matrix(model)[:, flux_indexes], - optimizer; - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A simpler version of [`flux_variability_analysis`](@ref) that maximizes and -minimizes all declared fluxes in the model. Arguments are forwarded. -""" -flux_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = - flux_variability_analysis(model, reaction_variables_matrix(model), optimizer; kwargs...) - -""" -$(TYPEDSIGNATURES) A variant of [`flux_variability_analysis`](@ref) that returns the individual -maximized and minimized fluxes as two dictionaries (of dictionaries). All +maximized and minimized fluxes as two dictionaries of dictionaries. All keyword arguments except `ret` are passed through. # Example @@ -151,6 +203,7 @@ mins, maxs = flux_variability_analysis_dict( ``` """ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer; kwargs...) + # TODO generalize this (requires smart analysis results) vs = flux_variability_analysis( model, optimizer; @@ -174,48 +227,3 @@ function _max_variability_flux(opt_model, flux, sense, ret) is_solved(opt_model) ? ret(opt_model) : nothing end - -""" -$(TYPEDSIGNATURES) - -A variant for [`flux_variability_analysis`](@ref) that examines actual -reactions (selected by their indexes in `variables` argument) instead of whole -fluxes. This may be useful for models where the sets of reactions and fluxes -differ. -""" -function reaction_variability_analysis( - model::AbstractMetabolicModel, - reaction_indexes::Vector{Int}, - optimizer; - kwargs..., -) - if any((reaction_indexes .< 1) .| (reaction_indexes .> n_variables(model))) - throw(DomainError(reaction_indexes, "Flux index out of range")) - end - - flux_variability_analysis( - model, - sparse( - reaction_indexes, - 1:length(reaction_indexes), - 1.0, - n_variables(model), - length(reaction_indexes), - ), - optimizer; - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for [`reaction_variability_analysis`](@ref) that examines all reactions. -""" -reaction_variability_analysis(model::AbstractMetabolicModel, optimizer; kwargs...) = - reaction_variability_analysis( - model, - collect(1:n_variables(model)), - optimizer; - kwargs..., - ) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 1a9fe1a51..69096a698 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -30,7 +30,18 @@ end const variable_semantics = Symbol[] -using ..Accessors +function get_semantics( + ::Val{Semantics}, +)::Maybe{Tuple{Function,Function,Function,Function}} where {Semantics} + if Semantics in variable_semantics + return ( + Base.eval(Accessors, Symbol(Semantics, :s)), + Base.eval(Accessors, Symbol(:n_, Semantics, :s)), + Base.eval(Accessors, Symbol(Semantics, :_variables)), + Base.eval(Accessors, Symbol(Semantics, :_variables_matrix)), + ) + end +end function make_variable_semantics( themodule::Module, @@ -39,6 +50,8 @@ function make_variable_semantics( name::String, example::String, ) + sym in themodule.Internal.variable_semantics && return + plural = Symbol(sym, :s) count = Symbol(:n_, plural) mapping = Symbol(sym, :_variables) @@ -139,7 +152,7 @@ end macro make_variable_semantics(sym, name, example) src = __source__ - # TODO actually use the source + # TODO actually use the source, or evade macro if impossible quote $make_variable_semantics($Accessors, $src, $sym, $name, $example) end From 56ba0424f003a3aedd0c2cb8b40d494488f86d94 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 18:29:53 +0100 Subject: [PATCH 121/531] fix --- src/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 5bebc7b83..fc22131e2 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -33,7 +33,7 @@ function flux_variability_analysis( kwargs..., ) variability_analysis( - Val{:reaction}, + Val(:reaction), model, optimizer; ids = reaction_ids, From da423a8737e0385f4d1f6f75dc9da371fea50f67 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 18:32:50 +0100 Subject: [PATCH 122/531] fix FVA tests --- ...ty_analysis.jl => variability_analysis.jl} | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) rename test/analysis/{flux_variability_analysis.jl => variability_analysis.jl} (80%) diff --git a/test/analysis/flux_variability_analysis.jl b/test/analysis/variability_analysis.jl similarity index 80% rename from test/analysis/flux_variability_analysis.jl rename to test/analysis/variability_analysis.jl index c9835d116..7155d6a9b 100644 --- a/test/analysis/flux_variability_analysis.jl +++ b/test/analysis/variability_analysis.jl @@ -12,7 +12,7 @@ rates = reaction_variability_analysis(cp, optimizer) @test fluxes == rates - fluxes = flux_variability_analysis(cp, [2], optimizer) + fluxes = flux_variability_analysis(cp, optimizer, reaction_indexes = [2]) @test size(fluxes) == (1, 2) @test isapprox(fluxes, [2 2], atol = TEST_TOLERANCE) @@ -58,15 +58,24 @@ atol = TEST_TOLERANCE, ) - @test isempty(flux_variability_analysis(cp, Vector{Int}(), Tulip.Optimizer)) - @test_throws DomainError flux_variability_analysis(cp, [-1], Tulip.Optimizer) - @test_throws DomainError flux_variability_analysis(cp, [99999999], Tulip.Optimizer) + @test isempty(flux_variability_analysis(cp, Tulip.Optimizer, reaction_ids = String[])) + @test_throws DomainError flux_variability_analysis( + cp, + Tulip.Optimizer, + reaction_indexes = [-1], + ) + @test_throws DomainError flux_variability_analysis( + cp, + Tulip.Optimizer, + reaction_ids = ["not a reaction!"], + ) end @testset "Parallel FVA" begin cp = test_simpleLP() - fluxes = flux_variability_analysis(cp, [1, 2], Tulip.Optimizer; workers = W) + fluxes = + flux_variability_analysis(cp, Tulip.Optimizer; workers = W, reaction_ids = [1, 2]) @test isapprox( fluxes, [ From 9c5aabc9cc31f509a78327635a3633a54e27b119 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 19:04:02 +0100 Subject: [PATCH 123/531] more fixes --- src/types/accessors/bits/semantics.jl | 4 ++-- test/analysis/variability_analysis.jl | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 69096a698..cb628db91 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -32,7 +32,7 @@ const variable_semantics = Symbol[] function get_semantics( ::Val{Semantics}, -)::Maybe{Tuple{Function,Function,Function,Function}} where {Semantics} +)::Types.Maybe{Tuple{Function,Function,Function,Function}} where {Semantics} if Semantics in variable_semantics return ( Base.eval(Accessors, Symbol(Semantics, :s)), @@ -165,7 +165,7 @@ macro all_variables_are_reactions(mt) $Accessors.reactions(model::$m) = $Accessors.variables(model) $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) $Accessors.reaction_variables(model::$m) = - Dict(var => Dict(var => 1.0) for var in variables(model)) + Dict(var => Dict(var => 1.0) for var in $Accessors.variables(model)) $Accessors.reaction_variables_matrix(model::$m) = $SparseArrays.spdiagm(fill(1.0, $Accessors.n_variables(model))) end diff --git a/test/analysis/variability_analysis.jl b/test/analysis/variability_analysis.jl index 7155d6a9b..92abc27c1 100644 --- a/test/analysis/variability_analysis.jl +++ b/test/analysis/variability_analysis.jl @@ -74,8 +74,12 @@ end @testset "Parallel FVA" begin cp = test_simpleLP() - fluxes = - flux_variability_analysis(cp, Tulip.Optimizer; workers = W, reaction_ids = [1, 2]) + fluxes = flux_variability_analysis( + cp, + Tulip.Optimizer; + workers = W, + reaction_indexes = [1, 2], + ) @test isapprox( fluxes, [ From 1f812e4546d3cd6e8d23501eb3e6511d88fd09a2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 20:03:02 +0100 Subject: [PATCH 124/531] even more --- src/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index fc22131e2..3a63bf50a 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -138,7 +138,7 @@ function variability_analysis( bounds = z -> (z, Inf), ret = objective_value, ) - if size(objectives, 1) != n_variables(model) + if size(directions, 1) != n_variables(model) throw( DomainError( size(directions, 1), From e68d94c7d363e5cf0a86225388b7229a4063a50b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 20:22:46 +0100 Subject: [PATCH 125/531] hopefully the last one --- test/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/analysis/variability_analysis.jl b/test/analysis/variability_analysis.jl index 92abc27c1..baedc3282 100644 --- a/test/analysis/variability_analysis.jl +++ b/test/analysis/variability_analysis.jl @@ -9,7 +9,7 @@ 2.0 2.0 ] - rates = reaction_variability_analysis(cp, optimizer) + rates = variability_analysis(cp, optimizer) @test fluxes == rates fluxes = flux_variability_analysis(cp, optimizer, reaction_indexes = [2]) From 17958d0ee67dfc7cc468ec5d69d96c8892278783 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 21:38:52 +0100 Subject: [PATCH 126/531] make a good default --- src/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 3a63bf50a..146ed529f 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -131,7 +131,7 @@ reason. function variability_analysis( model::AbstractMetabolicModel, optimizer; - directions::SparseMat, + directions::SparseMat = spdiagm(fill(1.0, n_variables(model))), modifications = [], workers = [myid()], optimal_objective_value = nothing, From 81068f2395e93f0dd1f8fcdcce7c6ae40b4b9dbd Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 22:05:31 +0100 Subject: [PATCH 127/531] linter pls. --- src/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 146ed529f..351898884 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -74,7 +74,7 @@ function variability_analysis( idxs = if isnothing(ids) collect(1:n_ids(model)) else - findindex(ids, sem_ids(model)) + indexin(ids, sem_ids(model)) end any(isnothing.(idxs)) && throw(DomainError(ids[isnothing(idxs)], "Unknown IDs specified")) From 2fd0c34470ce45fe045d5e56265b1136dcbcc0b5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 7 Feb 2023 22:33:55 +0100 Subject: [PATCH 128/531] vectorize --- src/analysis/variability_analysis.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 351898884..1444a5df2 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -77,7 +77,7 @@ function variability_analysis( indexin(ids, sem_ids(model)) end any(isnothing.(idxs)) && - throw(DomainError(ids[isnothing(idxs)], "Unknown IDs specified")) + throw(DomainError(ids[isnothing.(idxs)], "Unknown IDs specified")) indexes = Int.(idxs) end From 4e2478aa2508b0172d128f9a60e02d829d929cf9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 8 Feb 2023 14:39:22 +0100 Subject: [PATCH 129/531] add docs --- src/types/accessors/bits/semantics.jl | 61 ++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 10 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index cb628db91..4b3cf5c22 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -1,12 +1,22 @@ -function make_mapping_mtx(rows, cols, col_row_val) - rowidx = Dict(rows .=> 1:length(rows)) - colidx = Dict(cols .=> 1:length(cols)) - n = sum(length.(values(col_row_val))) +""" +$(TYPEDSIGNATURES) + +A helper function to quickly create a sparse matrix from a dictionary that +describes it. Reverse of [`make_mapping_dict`](@ref). +""" +function make_mapping_mtx( + vars::Vector{String}, + semantics::Vector{String}, + var_sem_val::Dict{String,Dict{String,Float64}}, +)::Types.SparseMat + rowidx = Dict(vars .=> 1:length(vars)) + colidx = Dict(semantics .=> 1:length(semantics)) + n = sum(length.(values(var_sem_val))) R = Vector{Int}(undef, n) C = Vector{Int}(undef, n) V = Vector{Float64}(undef, n) i = 1 - for (cid, col_val) in col_row_val + for (cid, col_val) in var_sem_val for (rid, val) in col_val R[i] = rowidx[rid] C[i] = colidx[cid] @@ -14,12 +24,18 @@ function make_mapping_mtx(rows, cols, col_row_val) i += 1 end end - sparse(R, C, V, length(rows), length(cols)) + sparse(R, C, V, length(vars), length(semantics)) end +""" +$(TYPEDSIGNATURES) + +A helper function to quickly create a sparse matrix from a dictionary that +describes it. Reverse of [`make_mapping_mtx`](@ref). +""" function make_mapping_dict( - vars, - semantics, + vars::Vector{String}, + semantics::Vector{String}, mtx::Types.SparseMat, )::Dict{String,Dict{String,Float64}} Dict( @@ -30,6 +46,12 @@ end const variable_semantics = Symbol[] +""" +$(TYPEDSIGNATURES) + +Get a tuple of functions that work with the given semantics, or `nothing` if +the semantics doesn't exist. +""" function get_semantics( ::Val{Semantics}, )::Types.Maybe{Tuple{Function,Function,Function,Function}} where {Semantics} @@ -43,6 +65,16 @@ function get_semantics( end end +""" +$(TYPEDSIGNATURES) + +Inject a new functionality for variable semantics defined by `sym` into +`themodule` (which should ideally be COBREXA.Accessors). + +`name` is a human readable description of the semantic object. `example` is a +string that closer describes the semantics, which is inserted into the main +semantic-accessing function. +""" function make_variable_semantics( themodule::Module, source, @@ -150,17 +182,26 @@ safety reasons, this is never automatically inherited by wrappers. Base.eval(themodule, code) end +""" +$(TYPEDSIGNATURES) + +Convenience macro for running [`make_variable_semantics`](@ref). +""" macro make_variable_semantics(sym, name, example) src = __source__ - # TODO actually use the source, or evade macro if impossible quote $make_variable_semantics($Accessors, $src, $sym, $name, $example) end end +""" +$(TYPEDSIGNATURES) + +Convenience helper -- many models carry no other variable semantics than the +reactions; this macro declares precisely the same about the model type. +""" macro all_variables_are_reactions(mt) m = esc(mt) - # TODO docs quote $Accessors.reactions(model::$m) = $Accessors.variables(model) $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) From 16124167385f6c2f2c43efa9fc737589251bfc08 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 11:31:40 +0100 Subject: [PATCH 130/531] add support for quadratic-objective models --- src/solver.jl | 8 +++++++- src/types/accessors/AbstractMetabolicModel.jl | 18 ++++++++++++++---- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 759a85b10..3685d8a43 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -37,7 +37,13 @@ function make_optimization_model( optimization_model = Model(optimizer) @variable(optimization_model, x[1:n]) - @objective(optimization_model, sense, objective(model)' * x) + let obj = objective(model) + if obj isa AbstractVector + @objective(optimization_model, sense, obj' * x) + else + @objective(optimization_model, sense, x' * obj * [x; 1]) + end + end @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance @constraint(optimization_model, lbs, xl .<= x) # lower bounds @constraint(optimization_model, ubs, x .<= xu) # upper bounds diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index d7f8b0d3c..9eadb4ad3 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -92,11 +92,21 @@ end """ $(TYPEDSIGNATURES) -Get the objective vector of the model. Analysis functions, such as -[`flux_balance_analysis`](@ref), are supposed to maximize `dot(objective, x)` -where `x` is a feasible solution of the model. +Get the linear objective vector or the quadratic objective affine matrix of the +model. + +Analysis functions, such as [`flux_balance_analysis`](@ref), are supposed to +maximize `objective' * x` where `x` is a feasible solution of the model (in +case the objective is a sparse vector), or `x' * objective * [x; 1]` (in +case the objective is a sparse matrix). + +The objective matrix is extended by 1 column to allow affine quadratic form +objectives. Symmetry is not required. + +Use [`affine_quadratic_objective`](@ref) to simplify creation of the quadratic +matrices. """ -function objective(a::AbstractMetabolicModel)::SparseVec +function objective(a::AbstractMetabolicModel)::Union{SparseVec,SparseMat} missing_impl_error(objective, (a,)) end From e8731cf221c328d5bcba5b0948aaad5bfa047339 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 12:03:25 +0100 Subject: [PATCH 131/531] convert MOMA to a model wrapper --- src/analysis/minimize_metabolic_adjustment.jl | 81 +++---------------- src/types.jl | 4 +- src/types/wrappers/MinimizeAdjustment.jl | 30 +++++++ .../analysis/minimize_metabolic_adjustment.jl | 13 +-- 4 files changed, 48 insertions(+), 80 deletions(-) create mode 100644 src/types/wrappers/MinimizeAdjustment.jl diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 178abbfe1..c6a8979ec 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -2,9 +2,9 @@ $(TYPEDSIGNATURES) Run minimization of metabolic adjustment (MOMA) on `model` with respect to -`flux_ref`, which is a vector of fluxes in the order of `variables(model)`. -MOMA finds the shortest Euclidian distance between `flux_ref` and `model` with -`modifications`: +`reference_flux`, which is a vector of fluxes in the order of +`variables(model)`. MOMA finds the shortest Euclidian distance between +`reference_flux` and `model` with `modifications`: ``` min Σᵢ (xᵢ - flux_refᵢ)² s.t. S x = b @@ -17,15 +17,16 @@ more details. Additional arguments are passed to [`flux_balance_analysis`](@ref). -Returns an optimized model that contains the resultant nearest flux. +Returns an optimized model that contains the feasible flux nearest to the +reference. # Example ``` model = load_model("e_coli_core.json") -flux_ref = flux_balance_analysis_vec(model, Gurobi.Optimizer) +reference_flux = flux_balance_analysis_vec(model, Gurobi.Optimizer) optmodel = minimize_metabolic_adjustment( model, - flux_ref, + reference_flux, Gurobi.Optimizer; modifications = [change_constraint("PFL"; lower_bound=0, upper_bound=0)], # find flux of mutant that is closest to the wild type (reference) model ) @@ -34,75 +35,11 @@ value.(solution[:x]) # extract the flux from the optimizer """ minimize_metabolic_adjustment_analysis( model::AbstractMetabolicModel, - flux_ref::Union{Dict{String,Float64},Vector{Float64}}, + flux_ref::Union{Vector{Float64}}, optimizer; - modifications = [], kwargs..., ) = flux_balance_analysis( - model, + model |> MinimizeAdjustmentModel(reference_flux), optimizer; - modifications = vcat([minimize_metabolic_adjustment(flux_ref)], modifications), kwargs..., ) - -""" -$(TYPEDSIGNATURES) - -An optimization model modification that implements the MOMA in -[`minimize_metabolic_adjustment_analysis`](@ref). -""" -minimize_metabolic_adjustment(flux_ref::Vector{Float64}) = - (model, opt_model) -> begin - length(opt_model[:x]) == length(flux_ref) || throw( - DomainError( - flux_ref, - "length of the reference flux doesn't match the one in the optimization model", - ), - ) - @objective(opt_model, Min, sum((opt_model[:x] .- flux_ref) .^ 2)) - end - -""" -$(TYPEDSIGNATURES) - -Overload of [`minimize_metabolic_adjustment`](@ref) that works with a -dictionary of fluxes. -""" -minimize_metabolic_adjustment(flux_ref_dict::Dict{String,Float64}) = - (model, opt_model) -> - minimize_metabolic_adjustment([flux_ref_dict[rid] for rid in variables(model)])( - model, - opt_model, - ) - -""" -$(TYPEDSIGNATURES) - -Perform minimization of metabolic adjustment (MOMA) and return a vector of fluxes in the -same order as the reactions in `model`. Arguments are forwarded to -[`minimize_metabolic_adjustment`](@ref) internally. - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -minimize_metabolic_adjustment_analysis_vec( - model::AbstractMetabolicModel, - args...; - kwargs..., -) = flux_vector(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -Perform minimization of metabolic adjustment (MOMA) and return a dictionary mapping the -reaction IDs to fluxes. Arguments are forwarded to [`minimize_metabolic_adjustment`](@ref) -internally. - -This function is kept for backwards compatibility, use [`flux_vector`](@ref) -instead. -""" -minimize_metabolic_adjustment_analysis_dict( - model::AbstractMetabolicModel, - args...; - kwargs..., -) = flux_dict(model, minimize_metabolic_adjustment_analysis(model, args...; kwargs...)) diff --git a/src/types.jl b/src/types.jl index 6eeab2a45..2f5034c4b 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,5 +1,4 @@ -# make the module for types and load basic abstract types """ module Types @@ -46,7 +45,7 @@ using SparseArrays """ module Internal -Internal helpers for types. +Internal helpers for accessors. # Exports $(EXPORTS) @@ -81,6 +80,7 @@ end # module Accessors end @inject Types.Internal begin + # TODO where is this declared? using ..Types using ..Accessors using ..Log.Internal: @models_log diff --git a/src/types/wrappers/MinimizeAdjustment.jl b/src/types/wrappers/MinimizeAdjustment.jl new file mode 100644 index 000000000..eca5a2655 --- /dev/null +++ b/src/types/wrappers/MinimizeAdjustment.jl @@ -0,0 +1,30 @@ + +""" +$(TYPEDEF) + +A wrapper that adds a quadratic objective that minimizes total squared error +("adjustment") from a certain reference variable assignment ("reference flux"). + +Length of `reference_assignment` must be the same as the the number of +variables in the inner model. + +This is used to implement [`minimize_metabolic_adjustment`](@ref). + +# Example +``` +m = load_model("e_coli_core.xml") +adjusted_model = m |> MinimizeAdjustmentModel(fill(0.1, n_variables(m))) +``` +""" +struct MinimizeAdjustmentModel <: AbstractModelWrapper + inner::AbstractMetabolicModel + reference_assignment::Vector{Float64} +end +#TODO: sparse MinimizeAdjustmentModel with dict/sparseVec +#TODO: MinimizeReactionAdjustmentModel? +#TODO: MinimizeEnzymeAdjustmentModel? + +Accessors.unwrap_model(m::MinimizeAdjustmentModel) = m.inner + +Accessors.objective(m::MinimizeAdjustmentModel)::SparseMat = + [spdiagm(fill(-0.5, n_variables(m))) m.reference_assignment] diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl index 1e7e807c1..8e86c58aa 100644 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ b/test/analysis/minimize_metabolic_adjustment.jl @@ -3,12 +3,13 @@ sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in variables(model)] - moma = minimize_metabolic_adjustment_analysis_dict( - model, - sol, - Clarabel.Optimizer; - modifications = [silence], - ) + moma = + minimize_metabolic_adjustment_analysis( + model, + sol, + Clarabel.Optimizer; + modifications = [silence], + ) |> flux_dict(model) @test isapprox(moma["biomass1"], 0.07692307692307691, atol = QP_TEST_TOLERANCE) end From b9faf9e0dd43b95fb198e606579b85f6ee2a5f09 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 14:12:29 +0100 Subject: [PATCH 132/531] materialize the "distance" quadratic objective --- src/types/accessors/AbstractMetabolicModel.jl | 4 ++-- src/types/wrappers/MinimizeAdjustment.jl | 2 +- src/utils/quadratic_objective.jl | 10 ++++++++++ 3 files changed, 13 insertions(+), 3 deletions(-) create mode 100644 src/utils/quadratic_objective.jl diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 9eadb4ad3..227faa4ab 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -103,8 +103,8 @@ case the objective is a sparse matrix). The objective matrix is extended by 1 column to allow affine quadratic form objectives. Symmetry is not required. -Use [`affine_quadratic_objective`](@ref) to simplify creation of the quadratic -matrices. +Use [`negative_squared_distance_objective`](@ref) to simplify creation of the +common nearest-feasible-solution objectives. """ function objective(a::AbstractMetabolicModel)::Union{SparseVec,SparseMat} missing_impl_error(objective, (a,)) diff --git a/src/types/wrappers/MinimizeAdjustment.jl b/src/types/wrappers/MinimizeAdjustment.jl index eca5a2655..7c19ec46b 100644 --- a/src/types/wrappers/MinimizeAdjustment.jl +++ b/src/types/wrappers/MinimizeAdjustment.jl @@ -27,4 +27,4 @@ end Accessors.unwrap_model(m::MinimizeAdjustmentModel) = m.inner Accessors.objective(m::MinimizeAdjustmentModel)::SparseMat = - [spdiagm(fill(-0.5, n_variables(m))) m.reference_assignment] + COBREXA.Utils.negative_squared_distance_objective(m.reference_assignment) diff --git a/src/utils/quadratic_objective.jl b/src/utils/quadratic_objective.jl new file mode 100644 index 000000000..81af9ef36 --- /dev/null +++ b/src/utils/quadratic_objective.jl @@ -0,0 +1,10 @@ + +""" +$(TYPEDSIGNATURES) + +Produce a matrix for [`objective`](@ref) so that the model minimizes the +squared distance from the `center` variable assignment. Length of `center` +should be the same as number of variables in the model. +""" +negative_squared_distance_objective(center::Vector{Float64}) = + [spdiagm(fill(-0.5, length(center))) center] From c9ca101f2bcd667d2722f82e9524d5e30a013f71 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 14:14:44 +0100 Subject: [PATCH 133/531] fix arg name --- src/analysis/minimize_metabolic_adjustment.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index c6a8979ec..c1d6986de 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -35,7 +35,7 @@ value.(solution[:x]) # extract the flux from the optimizer """ minimize_metabolic_adjustment_analysis( model::AbstractMetabolicModel, - flux_ref::Union{Vector{Float64}}, + reference_flux::Union{Vector{Float64}}, optimizer; kwargs..., ) = flux_balance_analysis( From 602b9585ffe7d31909361a42ae55e637bf78990b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 14:32:24 +0100 Subject: [PATCH 134/531] make a proper MOMA pipe --- src/analysis/minimize_metabolic_adjustment.jl | 2 +- src/reconstruction/pipes/minimize_adjustment.jl | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 src/reconstruction/pipes/minimize_adjustment.jl diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index c1d6986de..4879c76d6 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -39,7 +39,7 @@ minimize_metabolic_adjustment_analysis( optimizer; kwargs..., ) = flux_balance_analysis( - model |> MinimizeAdjustmentModel(reference_flux), + model |> COBREXA.Reconstruction.Pipes.minimize_adjustment(reference_flux), optimizer; kwargs..., ) diff --git a/src/reconstruction/pipes/minimize_adjustment.jl b/src/reconstruction/pipes/minimize_adjustment.jl new file mode 100644 index 000000000..d1a6128bb --- /dev/null +++ b/src/reconstruction/pipes/minimize_adjustment.jl @@ -0,0 +1,8 @@ + +""" +$(TYPEDSIGNATURES) + +Pipe-able version of the [`MinimizeAdjustmentModel`](@ref) wrapper. +""" +minimize_adjustment(reference_flux::Vector{Float64}) = + model -> MinimizeAdjustmentModel(model, reference_flux) From 368500b224d04f2b605f8f079c5140f81ce79dfb Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 9 Feb 2023 15:17:20 +0100 Subject: [PATCH 135/531] fix the imports --- src/analysis/minimize_metabolic_adjustment.jl | 2 +- src/reconstruction.jl | 3 +++ src/types/wrappers/MinimizeAdjustment.jl | 2 +- src/utils.jl | 1 + 4 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 4879c76d6..502eeb33e 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -39,7 +39,7 @@ minimize_metabolic_adjustment_analysis( optimizer; kwargs..., ) = flux_balance_analysis( - model |> COBREXA.Reconstruction.Pipes.minimize_adjustment(reference_flux), + model |> Reconstruction.Pipes.minimize_adjustment(reference_flux), optimizer; kwargs..., ) diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 266e7029f..2783ee712 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -43,7 +43,10 @@ end # this needs to import from Reconstruction @inject Reconstruction.Pipes begin using ..Reconstruction + using ..Types @inc_dir reconstruction pipes @export_locals end + +@inject Analysis import ..Reconstruction diff --git a/src/types/wrappers/MinimizeAdjustment.jl b/src/types/wrappers/MinimizeAdjustment.jl index 7c19ec46b..01c9661c6 100644 --- a/src/types/wrappers/MinimizeAdjustment.jl +++ b/src/types/wrappers/MinimizeAdjustment.jl @@ -27,4 +27,4 @@ end Accessors.unwrap_model(m::MinimizeAdjustmentModel) = m.inner Accessors.objective(m::MinimizeAdjustmentModel)::SparseMat = - COBREXA.Utils.negative_squared_distance_objective(m.reference_assignment) + Utils.negative_squared_distance_objective(m.reference_assignment) diff --git a/src/utils.jl b/src/utils.jl index 74a27f589..233a7d3e7 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -33,5 +33,6 @@ using SparseArrays @export_locals end +@inject Types import ...Utils @inject Analysis using ...Utils: objective_bounds @inject Analysis.Modifications using ...Utils: is_boundary From 3e59d39423e0da92832b7c02558b887694ba8348 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Feb 2023 13:18:47 +0100 Subject: [PATCH 136/531] add reaction, metabolite, gene non-inplace + pipes --- src/reconstruction/ObjectModel.jl | 75 +++++++++++++++++++++++++++-- src/reconstruction/pipes/generic.jl | 63 +++++++++++++++++++----- 2 files changed, 124 insertions(+), 14 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index a52455124..ba86003d6 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -15,7 +15,30 @@ $(TYPEDSIGNATURES) Add `rxn` to `model` based on reaction `id`. """ -add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) +add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn,]) + +""" +$(TYPEDSIGNATURES) + +Add `rxns` to `model` and return a shallow copied version of the model. +""" +function add_reactions(model::ObjectModel, rxns::Vector{Reaction}) + m = copy(model) + + m.reactions = copy(m.reactions) + for rxn in rxns + m.reactions[rxn.id] = rxn + end + + return m +end + +""" +$(TYPEDSIGNATURES) + +Add `rxn` to `model`, and return a shallow copied version of the model. +""" +add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn,]) """ $(TYPEDSIGNATURES) @@ -34,7 +57,30 @@ $(TYPEDSIGNATURES) Add `met` to `model` based on metabolite `id`. """ -add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met]) +add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met,]) + +""" +$(TYPEDSIGNATURES) + +Add `mets` to `model` and return a shallow copied version of the model. +""" +function add_metabolites(model::ObjectModel, mets::Vector{Metabolite}) + m = copy(model) + + m.metabolites = copy(m.metabolites) + for met in mets + m.metabolites[met.id] = met + end + + return m +end + +""" +$(TYPEDSIGNATURES) + +Add `met` to `model` and return a shallow copied version of the model. +""" +add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met,]) """ $(TYPEDSIGNATURES) @@ -53,7 +99,30 @@ $(TYPEDSIGNATURES) Add `gene` to `model` based on gene `id`. """ -add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) +add_genes!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) + +""" +$(TYPEDSIGNATURES) + +Add `gns` to `model` and return a shallow copied version of the model. +""" +function add_genes(model::ObjectModel, genes::Vector{Gene}) + m = copy(model) + + m.genes = copy(m.genes) + for gn in genes + m.genes[gn.id] = gn + end + + return m +end + +""" +$(TYPEDSIGNATURES) + +Add `gene` to `model` and return a shallow copied version of the model. +""" +add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene,]) """ $(TYPEDSIGNATURES) diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index d065b709c..1b6ccb354 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -1,6 +1,54 @@ """ $(TYPEDSIGNATURES) +Specifies a model variant with reactions added. Forwards the arguments to +[`add_reactions`](@ref). +""" +with_added_reactions(args...; kwargs...) = m -> add_reactions(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with an added reaction. Forwards the arguments to +[`add_reaction`](@ref). +""" +with_added_reaction(args...; kwargs...) = m -> add_reaction(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with metabolites added. Forwards the arguments to +[`add_metabolites`](@ref). +""" +with_added_metabolites(args...; kwargs...) = m -> add_metabolites(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with an added metabolite. Forwards the arguments to +[`add_metabolite`](@ref). +""" +with_added_metabolite(args...; kwargs...) = m -> add_metabolite(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with genes added. Forwards the arguments to +[`add_genes`](@ref). +""" +with_added_genes(args...; kwargs...) = m -> add_genes(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with an added gene. Forwards the arguments to +[`add_gene`](@ref). +""" +with_added_gene(args...; kwargs...) = m -> add_gene(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + Specifies a model variant that has a new bound set. Forwards arguments to [`change_bound`](@ref). Intended for usage with [`screen`](@ref). """ @@ -18,7 +66,7 @@ with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs. $(TYPEDSIGNATURES) Specifies a model variant without a certain metabolite. Forwards arguments to -[`remove_metabolite`](@ref). Intended to be used with [`screen`](@ref). +[`remove_metabolite`](@ref). """ with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) @@ -34,16 +82,8 @@ with_removed_metabolites(args...; kwargs...) = """ $(TYPEDSIGNATURES) -Specifies a model variant with reactions added. Forwards the arguments to -[`add_reactions`](@ref). Intended to be used with [`screen`](@ref). -""" -with_added_reactions(args...; kwargs...) = m -> add_reactions(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - Specifies a model variant without a certain reaction. Forwards arguments to -[`remove_reaction`](@ref). Intended to be used with [`screen`](@ref). +[`remove_reaction`](@ref). """ with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) @@ -55,7 +95,6 @@ Plural version of [`with_removed_reaction`](@ref), calls """ with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) - """ $(TYPEDSIGNATURES) @@ -64,3 +103,5 @@ Forwards arguments to [`add_biomass_metabolite`](@ref). """ with_added_biomass_metabolite(args...; kwargs...) = m -> add_biomass_metabolite(m, args...; kwargs...) + + From 05a0dd54f4efeed856147cbe44112b9c72775b11 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Feb 2023 13:19:33 +0100 Subject: [PATCH 137/531] format --- src/reconstruction/ObjectModel.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index ba86003d6..055364060 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -15,7 +15,7 @@ $(TYPEDSIGNATURES) Add `rxn` to `model` based on reaction `id`. """ -add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn,]) +add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) """ $(TYPEDSIGNATURES) @@ -38,7 +38,7 @@ $(TYPEDSIGNATURES) Add `rxn` to `model`, and return a shallow copied version of the model. """ -add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn,]) +add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) """ $(TYPEDSIGNATURES) @@ -57,7 +57,7 @@ $(TYPEDSIGNATURES) Add `met` to `model` based on metabolite `id`. """ -add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met,]) +add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met]) """ $(TYPEDSIGNATURES) @@ -80,7 +80,7 @@ $(TYPEDSIGNATURES) Add `met` to `model` and return a shallow copied version of the model. """ -add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met,]) +add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) """ $(TYPEDSIGNATURES) @@ -122,7 +122,7 @@ $(TYPEDSIGNATURES) Add `gene` to `model` and return a shallow copied version of the model. """ -add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene,]) +add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) """ $(TYPEDSIGNATURES) From ad7d8022cacb104eb3cc0fe8db9ba183ff4abc0b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Feb 2023 13:51:17 +0100 Subject: [PATCH 138/531] remove switching argument for reaction constructors --- .../04b_standardmodel_construction.jl | 20 +++--- src/types/Reaction.jl | 70 ++++++++++++------- test/reconstruction/ObjectModel.jl | 10 +-- test/reconstruction/constrained_allocation.jl | 12 ++-- test/reconstruction/enzyme_constrained.jl | 12 ++-- .../gapfill_minimum_reactions.jl | 41 ++++++----- test/types/BalancedGrowthCommunityModel.jl | 20 +++--- test/types/ObjectModel.jl | 6 +- test/types/Reaction.jl | 12 ++-- 9 files changed, 110 insertions(+), 93 deletions(-) diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 73d9860d3..de4642178 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -34,17 +34,17 @@ add_metabolites!(model, metabolite_list) # ### Add reactions to the model -r_m1 = Reaction("EX_m1", Dict("m1" => -1.0), :bidirectional) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) -r1 = Reaction("r1", Dict("m1" => -1.0, "m2" => 1.0), :forward) +r_m1 = ReactionBidirectional("EX_m1", Dict("m1" => -1.0)) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) +r1 = ReactionForward("r1", Dict("m1" => -1.0, "m2" => 1.0)) r1.gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])] # add some gene reaction rules -r2 = Reaction("r2", Dict("m2" => -1.0, "m1" => 1.0), :backward) -r3 = Reaction("r3", Dict("m2" => -1.0, "m3" => 1.0), :bidirectional) -r4 = Reaction("r3", Dict("m2" => -1.0, "m4" => 1.0), :forward) -r_m3 = Reaction("r3", Dict("m3" => -1.0), :bidirectional) -r_m4 = Reaction("r3", Dict("m4" => -1.0), :forward) -r5 = Reaction("r5", Dict("m4" => -1.0, "m2" => 1.0), :forward) - -add_reactions!(model, [r1, r2, r3, r_m1, r4, r_m3, r_m4, r5]) # function approach +r2 = ReactionBackward("r2", Dict("m2" => -1.0, "m1" => 1.0)) +r3 = ReactionBidirectional("r3", Dict("m2" => -1.0, "m3" => 1.0)) +r4 = ReactionForward("r3", Dict("m2" => -1.0, "m4" => 1.0)) +r_m3 = ReactionBidirectional("r3", Dict("m3" => -1.0)) +r_m4 = ReactionForward("r3", Dict("m4" => -1.0)) +r5 = ReactionForward("r5", Dict("m4" => -1.0, "m2" => 1.0)) + +add_reactions!(model, [r1, r2, r3, r_m1, r4, r_m3, r_m4, r5]) m1 = metabolite_list[1] m2 = metabolite_list[2] diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 96107855f..0230b8f15 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -31,35 +31,53 @@ Reaction(id; kwargs...) = Reaction(; id, kwargs...) """ $(TYPEDSIGNATURES) -Convenience constructor for `Reaction`. The reaction equation is specified using -`metabolites`, which is a dictionary mapping metabolite ids to stoichiometric -coefficients. The direcion of the reaction is set through `dir` which can take -`:bidirectional`, `:forward`, and `:backward` as values. Finally, the +Convenience constructor for `Reaction` that generates a reaction constrained to +carry flux only in the forward direction relative to the `metabolites`, which is +a dictionary mapping metabolite ids to stoichiometric coefficients. The +`default_bound` is the value taken to mean infinity in the context of constraint +based models, often this is set to a very high flux value like 1000. + +See also: [`Reaction`](@ref), [`ReactionBackward`](@ref), [`ReactionBidirectional`](@ref) +""" +ReactionForward(id::String, metabolites; default_bound = constants.default_reaction_bound) = + Reaction(id; metabolites = metabolites, lower_bound = 0.0, upper_bound = default_bound) + +""" +$(TYPEDSIGNATURES) + +Convenience constructor for `Reaction` that generates a reaction constrained to +carry flux only in the backward direction relative to the `metabolites`, which is +a dictionary mapping metabolite ids to stoichiometric coefficients. The `default_bound` is the value taken to mean infinity in the context of constraint based models, often this is set to a very high flux value like 1000. + +See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBidirectional`](@ref) """ -function Reaction( +ReactionBackward( id::String, - metabolites, - dir = :bidirectional; + metabolites; default_bound = constants.default_reaction_bound, +) = Reaction(id; metabolites = metabolites, lower_bound = -default_bound, upper_bound = 0.0) + +""" +$(TYPEDSIGNATURES) + +Convenience constructor for `Reaction` that generates a reaction constrained to +carry flux in both the forward and backward direction relative to the +`metabolites`, which is a dictionary mapping metabolite ids to stoichiometric +coefficients. The `default_bound` is the value taken to mean infinity in the +context of constraint based models, often this is set to a very high flux value +like 1000. + +See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBackward`](@ref) +""" +ReactionBidirectional( + id::String, + metabolites; + default_bound = constants.default_reaction_bound, +) = Reaction( + id; + metabolites = metabolites, + lower_bound = -default_bound, + upper_bound = default_bound, ) - if dir == :forward - lower_bound = 0.0 - upper_bound = default_bound - elseif dir == :backward - lower_bound = -default_bound - upper_bound = 0.0 - elseif dir == :bidirectional - lower_bound = -default_bound - upper_bound = default_bound - else - throw(DomainError(dir, "unsupported direction")) - end - Reaction( - id; - metabolites = metabolites, - lower_bound = lower_bound, - upper_bound = upper_bound, - ) -end diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index edd9b94e4..87aa4bc2a 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -17,12 +17,12 @@ g6 = Gene("g6") g7 = Gene("g7") - r1 = Reaction("r1", Dict(m1.id => -1.0, m2.id => 1.0), :forward) - r2 = Reaction("r2", Dict(m2.id => -2.0, m3.id => 1.0), :bidirectional) + r1 = ReactionForward("r1", Dict(m1.id => -1.0, m2.id => 1.0)) + r2 = ReactionBidirectional("r2", Dict(m2.id => -2.0, m3.id => 1.0)) r2.gene_associations = [Isozyme(x) for x in [["g2"], ["g1", "g3"]]] - r3 = Reaction("r3", Dict(m1.id => -1.0, m4.id => 2.0), :backward) - r4 = Reaction("r4", Dict(m1.id => -5.0, m4.id => 2.0), :backward) - r5 = Reaction("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0), :backward) + r3 = ReactionBackward("r3", Dict(m1.id => -1.0, m4.id => 2.0)) + r4 = ReactionBackward("r4", Dict(m1.id => -5.0, m4.id => 2.0)) + r5 = ReactionBackward("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0)) rxns = [r1, r2] diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 6293824df..c33ab87fb 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -9,12 +9,12 @@ add_reactions!( m, [ - Reaction("r1", Dict("m1" => 1), :forward), - Reaction("r2", Dict("m2" => 1), :forward), - Reaction("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), - Reaction("r4", Dict("m3" => -1, "m4" => 1), :forward), - Reaction("r5", Dict("m2" => -1, "m4" => 1), :bidirectional), - Reaction("r6", Dict("m4" => -1), :bidirectional), # different! + ReactionForward("r1", Dict("m1" => 1)), + ReactionForward("r2", Dict("m2" => 1)), + ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), + ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), + ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), + ReactionBidirectional("r6", Dict("m4" => -1)), # different! ], ) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index e8cd20a92..53490a3e8 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -102,12 +102,12 @@ end add_reactions!( m, [ - Reaction("r1", Dict("m1" => 1), :forward), - Reaction("r2", Dict("m2" => 1), :forward), - Reaction("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1), :forward), - Reaction("r4", Dict("m3" => -1, "m4" => 1), :forward), - Reaction("r5", Dict("m2" => -1, "m4" => 1), :bidirectional), - Reaction("r6", Dict("m4" => -1), :forward), + ReactionForward("r1", Dict("m1" => 1)), + ReactionForward("r2", Dict("m2" => 1)), + ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), + ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), + ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), + ReactionForward("r6", Dict("m4" => -1)), ], ) diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 921df6bbd..65c6b5848 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -13,21 +13,20 @@ add_reactions!( model, [ - Reaction("r1", Dict("m1" => 1), :forward; default_bound = 1), - Reaction("r2", Dict("m1" => -1, "m2" => 1), :bidirectional; default_bound = 10), - Reaction("r3", Dict("m1" => -1, "m3" => 1), :forward), - Reaction("r4", Dict("m2" => -1, "m4" => 1), :bidirectional), - # Reaction("r5", Dict("m3" => -1, "m4" => 1), :forward), - Reaction("r6", Dict("m4" => -1), :forward), - # Reaction("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1), :forward), - Reaction("r8", Dict("m7" => -1, "m8" => 1), :forward), - Reaction("r9", Dict("m8" => -1), :forward), - # Reaction("r10", Dict("m6" => -1), :forward), - Reaction("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1), :forward), - Reaction( + ReactionForward("r1", Dict("m1" => 1); default_bound = 1), + ReactionBidirectional("r2", Dict("m1" => -1, "m2" => 1); default_bound = 10), + ReactionForward("r3", Dict("m1" => -1, "m3" => 1)), + ReactionBidirectional("r4", Dict("m2" => -1, "m4" => 1)), + # ReactionForward("r5", Dict("m3" => -1, "m4" => 1)), + ReactionForward("r6", Dict("m4" => -1)), + # ReactionForward("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1)), + ReactionForward("r8", Dict("m7" => -1, "m8" => 1)), + ReactionForward("r9", Dict("m8" => -1)), + # ReactionForward("r10", Dict("m6" => -1)), + ReactionForward("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1)), + ReactionBidirectional( "r12", - Dict("m3" => -1, "m5" => 1), - :bidirectional; + Dict("m3" => -1, "m5" => 1); default_bound = 10, ), ], @@ -37,13 +36,13 @@ add_metabolites!(model, [m1, m2, m3, m4, m5, m7, m8]) - r5 = Reaction("r5", Dict("m3" => -1, "m4" => 1), :forward) - r7 = Reaction("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1), :forward) - r10 = Reaction("r10", Dict("m6" => -1), :forward) - rA = Reaction("rA", Dict("m1" => -1, "m2" => 1, "m3" => 1), :forward) - rB = Reaction("rB", Dict("m2" => -1, "m9" => 1), :forward) - rC = Reaction("rC", Dict("m9" => -1, "m10" => 1), :bidirectional) - rD = Reaction("rC", Dict("m10" => -1), :backward) + r5 = ReactionForward("r5", Dict("m3" => -1, "m4" => 1)) + r7 = ReactionForward("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1)) + r10 = ReactionForward("r10", Dict("m6" => -1)) + rA = ReactionForward("rA", Dict("m1" => -1, "m2" => 1, "m3" => 1)) + rB = ReactionForward("rB", Dict("m2" => -1, "m9" => 1)) + rC = ReactionBidirectional("rC", Dict("m9" => -1, "m10" => 1)) + rD = ReactionBackward("rC", Dict("m10" => -1)) universal_reactions = [r5, r7, r10, rA, rB, rC, rD] diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index df2c32060..0e9daf46b 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -14,11 +14,11 @@ add_reactions!( m1, [ - Reaction("EX_A", Dict("Ae" => -1), :bidirectional), - Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), - Reaction("r2", Dict("A" => -1, "B" => 1, "X1" => 1), :bidirectional), - Reaction("r3", Dict("B" => -1, "Be" => 1), :forward), - Reaction("EX_B", Dict("Be" => -1), :forward), + ReactionBidirectional("EX_A", Dict("Ae" => -1)), + ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), + ReactionBidirectional("r2", Dict("A" => -1, "B" => 1, "X1" => 1)), + ReactionForward("r3", Dict("B" => -1, "Be" => 1)), + ReactionForward("EX_B", Dict("Be" => -1)), ], ) @@ -37,11 +37,11 @@ add_reactions!( m2, [ - Reaction("r3", Dict("C" => -1, "Ce" => 1), :forward), - Reaction("EX_C", Dict("Ce" => -1), :forward), - Reaction("EX_A", Dict("Ae" => -1), :bidirectional), - Reaction("r1", Dict("Ae" => -1, "A" => 1), :bidirectional), - Reaction("r2", Dict("A" => -1, "C" => 1, "X2" => 1), :bidirectional), + ReactionForward("r3", Dict("C" => -1, "Ce" => 1)), + ReactionForward("EX_C", Dict("Ce" => -1)), + ReactionBidirectional("EX_A", Dict("Ae" => -1)), + ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), + ReactionBidirectional("r2", Dict("A" => -1, "C" => 1, "X2" => 1)), ], ) diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 3c061a694..9b4d04604 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -29,9 +29,9 @@ r1.notes = Dict("notes" => ["blah", "blah"]) r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :backward) - r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) - r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) + r2 = ReactionBackward("r2", Dict(m1.id => -2.0, m4.id => 1.0)) + r3 = ReactionForward("r3", Dict(m3.id => -1.0, m4.id => 1.0)) + r4 = ReactionBidirectional("r4", Dict(m3.id => -1.0, m4.id => 1.0)) r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) mets = [m1, m2, m3, m4] diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 14dd14769..aa76527be 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -37,13 +37,13 @@ ), ) - r2 = Reaction("r2", Dict(m1.id => -2.0, m4.id => 1.0), :backward) + r2 = ReactionBackward("r2", Dict(m1.id => -2.0, m4.id => 1.0)) @test r2.lower_bound == -1000.0 && r2.upper_bound == 0.0 - r3 = Reaction("r3", Dict(m3.id => -1.0, m4.id => 1.0), :forward) + r3 = ReactionForward("r3", Dict(m3.id => -1.0, m4.id => 1.0)) @test r3.lower_bound == 0.0 && r3.upper_bound == 1000.0 - r4 = Reaction("r4", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) + r4 = ReactionBidirectional("r4", Dict(m3.id => -1.0, m4.id => 1.0)) r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) @test r4.lower_bound == -1000.0 && r4.upper_bound == 1000.0 @@ -53,15 +53,15 @@ id = check_duplicate_reaction(r4, rd) @test id == "r3" - r5 = Reaction("r5", Dict(m3.id => -11.0, m4.id => 1.0), :bidirectional) + r5 = ReactionBidirectional("r5", Dict(m3.id => -11.0, m4.id => 1.0)) id = check_duplicate_reaction(r5, rd) @test id == "r3" - r5 = Reaction("r5", Dict(m3.id => -11.0, m4.id => 1.0), :bidirectional) + r5 = ReactionBidirectional("r5", Dict(m3.id => -11.0, m4.id => 1.0)) id = check_duplicate_reaction(r5, rd; only_metabolites = false) @test isnothing(id) - r5 = Reaction("r5", Dict(m3.id => -1.0, m4.id => 1.0), :bidirectional) + r5 = ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)) id = check_duplicate_reaction(r5, rd; only_metabolites = false) @test id == "r3" end From b939472fc21c859d1856e1169359d827f8d9deef Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Feb 2023 13:56:34 +0100 Subject: [PATCH 139/531] fix test --- src/reconstruction/ObjectModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 055364060..e9c2927c2 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -99,7 +99,7 @@ $(TYPEDSIGNATURES) Add `gene` to `model` based on gene `id`. """ -add_genes!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) +add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) """ $(TYPEDSIGNATURES) @@ -122,7 +122,7 @@ $(TYPEDSIGNATURES) Add `gene` to `model` and return a shallow copied version of the model. """ -add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) +add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene,]) """ $(TYPEDSIGNATURES) From a763433aa384677ae4b3ae1508f2e401184120bb Mon Sep 17 00:00:00 2001 From: stelmo Date: Sat, 11 Feb 2023 12:58:25 +0000 Subject: [PATCH 140/531] automatic formatting triggered by @stelmo on PR #739 --- src/reconstruction/ObjectModel.jl | 2 +- src/reconstruction/pipes/generic.jl | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index e9c2927c2..1b92c97a9 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -122,7 +122,7 @@ $(TYPEDSIGNATURES) Add `gene` to `model` and return a shallow copied version of the model. """ -add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene,]) +add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) """ $(TYPEDSIGNATURES) diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index 1b6ccb354..54e632e04 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -2,7 +2,7 @@ $(TYPEDSIGNATURES) Specifies a model variant with reactions added. Forwards the arguments to -[`add_reactions`](@ref). +[`add_reactions`](@ref). """ with_added_reactions(args...; kwargs...) = m -> add_reactions(m, args...; kwargs...) @@ -18,7 +18,7 @@ with_added_reaction(args...; kwargs...) = m -> add_reaction(m, args...; kwargs.. $(TYPEDSIGNATURES) Specifies a model variant with metabolites added. Forwards the arguments to -[`add_metabolites`](@ref). +[`add_metabolites`](@ref). """ with_added_metabolites(args...; kwargs...) = m -> add_metabolites(m, args...; kwargs...) @@ -34,7 +34,7 @@ with_added_metabolite(args...; kwargs...) = m -> add_metabolite(m, args...; kwar $(TYPEDSIGNATURES) Specifies a model variant with genes added. Forwards the arguments to -[`add_genes`](@ref). +[`add_genes`](@ref). """ with_added_genes(args...; kwargs...) = m -> add_genes(m, args...; kwargs...) @@ -66,7 +66,7 @@ with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs. $(TYPEDSIGNATURES) Specifies a model variant without a certain metabolite. Forwards arguments to -[`remove_metabolite`](@ref). +[`remove_metabolite`](@ref). """ with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) @@ -83,7 +83,7 @@ with_removed_metabolites(args...; kwargs...) = $(TYPEDSIGNATURES) Specifies a model variant without a certain reaction. Forwards arguments to -[`remove_reaction`](@ref). +[`remove_reaction`](@ref). """ with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) @@ -103,5 +103,3 @@ Forwards arguments to [`add_biomass_metabolite`](@ref). """ with_added_biomass_metabolite(args...; kwargs...) = m -> add_biomass_metabolite(m, args...; kwargs...) - - From b92ecd0e37d42a2cd9ca00f93351fa7ddc696539 Mon Sep 17 00:00:00 2001 From: stelmo Date: Sat, 11 Feb 2023 13:10:21 +0000 Subject: [PATCH 141/531] automatic formatting triggered by @stelmo on PR #740 --- src/types/Reaction.jl | 6 +++--- test/reconstruction/gapfill_minimum_reactions.jl | 6 +----- 2 files changed, 4 insertions(+), 8 deletions(-) diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 0230b8f15..00a348212 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -37,7 +37,7 @@ a dictionary mapping metabolite ids to stoichiometric coefficients. The `default_bound` is the value taken to mean infinity in the context of constraint based models, often this is set to a very high flux value like 1000. -See also: [`Reaction`](@ref), [`ReactionBackward`](@ref), [`ReactionBidirectional`](@ref) +See also: [`Reaction`](@ref), [`ReactionBackward`](@ref), [`ReactionBidirectional`](@ref) """ ReactionForward(id::String, metabolites; default_bound = constants.default_reaction_bound) = Reaction(id; metabolites = metabolites, lower_bound = 0.0, upper_bound = default_bound) @@ -51,7 +51,7 @@ a dictionary mapping metabolite ids to stoichiometric coefficients. The `default_bound` is the value taken to mean infinity in the context of constraint based models, often this is set to a very high flux value like 1000. -See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBidirectional`](@ref) +See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBidirectional`](@ref) """ ReactionBackward( id::String, @@ -69,7 +69,7 @@ coefficients. The `default_bound` is the value taken to mean infinity in the context of constraint based models, often this is set to a very high flux value like 1000. -See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBackward`](@ref) +See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBackward`](@ref) """ ReactionBidirectional( id::String, diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 65c6b5848..6e7eb8315 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -24,11 +24,7 @@ ReactionForward("r9", Dict("m8" => -1)), # ReactionForward("r10", Dict("m6" => -1)), ReactionForward("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1)), - ReactionBidirectional( - "r12", - Dict("m3" => -1, "m5" => 1); - default_bound = 10, - ), + ReactionBidirectional("r12", Dict("m3" => -1, "m5" => 1); default_bound = 10), ], ) From ffdfb1b289e28c0b2184fea206cf0d801732c7fb Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Feb 2023 17:46:11 +0100 Subject: [PATCH 142/531] remove superfluous s --- src/types/accessors/AbstractMetabolicModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 227faa4ab..519e65dfe 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -122,7 +122,7 @@ flux, such as with separate bidirectional reactions. ) @make_variable_semantics( - :enzymes, + :enzyme, "enzyme supplies", """ Certain model types define a supply of enzymes that is typically required to From a1c2e6b39c41c54352a883ab7fa244cac0441d0f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 11 Feb 2023 23:25:51 +0100 Subject: [PATCH 143/531] fix the default variable semantics accessor calls --- src/types/accessors/bits/semantics.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 4b3cf5c22..bae805d8e 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -109,7 +109,7 @@ definition of the correspondence of $name and model variables. """, ), :(function $plural(a::AbstractMetabolicModel)::Vector{String} - x = collect(keys($mapping)) + x = collect(keys($mapping(a))) sort!(x) x end), @@ -129,7 +129,7 @@ vector returned by [`$plural`]. """, ), :(function $count(a::AbstractMetabolicModel)::Int - length($mapping) + length($mapping(a)) end), ) From c37313646144d31c0a64c09fc83bd6bdcf333715 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 10:21:17 +0100 Subject: [PATCH 144/531] add balance (from #726) --- src/types/models/BalancedGrowthCommunityModel.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 97533da34..02573f5a7 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -119,6 +119,12 @@ appended as a prefix with the delimiter `#`. Accessors.genes(cm::BalancedGrowthCommunityModel) = [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] +""" +$(TYPEDSIGNATURES) +Return the balance of `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). +""" +Accessors.balance(cm::BalancedGrowthCommunityModel) = [vcat([balance(m.model) .* m.abundance for m in cm.members]...); spzeros(length(get_env_mets(cm)))] + """ $(TYPEDSIGNATURES) From 4f657e143170eb9c414985b003cd428f600492f9 Mon Sep 17 00:00:00 2001 From: stelmo Date: Sun, 12 Feb 2023 09:23:55 +0000 Subject: [PATCH 145/531] automatic formatting triggered by @stelmo on PR #743 --- src/types/models/BalancedGrowthCommunityModel.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 02573f5a7..504a11cbb 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -123,7 +123,10 @@ Accessors.genes(cm::BalancedGrowthCommunityModel) = $(TYPEDSIGNATURES) Return the balance of `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). """ -Accessors.balance(cm::BalancedGrowthCommunityModel) = [vcat([balance(m.model) .* m.abundance for m in cm.members]...); spzeros(length(get_env_mets(cm)))] +Accessors.balance(cm::BalancedGrowthCommunityModel) = [ + vcat([balance(m.model) .* m.abundance for m in cm.members]...) + spzeros(length(get_env_mets(cm))) +] """ $(TYPEDSIGNATURES) From 9218627d25ec11fcdd186f4edd860c2300d8f80b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 11 Feb 2023 23:25:51 +0100 Subject: [PATCH 146/531] fix the default variable semantics accessor calls --- src/types/wrappers/EnzymeConstrainedModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index f4e12896f..51cb8eb03 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -181,7 +181,7 @@ the original fluxes in the wrapped model Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - semantics(model), + reactions(model), reaction_variables_matrix(model), ) # TODO currently inefficient From 3856612866a561b88bda78d1a4dcdd4f6332dfae Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 00:20:14 +0100 Subject: [PATCH 147/531] Add enzyme semantics to EnzymeConstrainedModel --- src/solver.jl | 27 +++++++++++++++----- src/types/wrappers/EnzymeConstrainedModel.jl | 10 ++++++++ src/utils/BalancedGrowthCommunityModel.jl | 2 +- 3 files changed, 31 insertions(+), 8 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 3685d8a43..ac8673b1b 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -120,6 +120,24 @@ solved_objective_value(flux_balance_analysis(model, ...)) solved_objective_value(opt_model)::Maybe{Float64} = is_solved(opt_model) ? objective_value(opt_model) : nothing +function get_solution(semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model) where {Semantics} + sem = Accessors.Internal.get_semantics(semantics) + isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) + (_, _, _, sem_varmtx) = sem + is_solved(opt_model) ? sem_varmtx(model)' * value.(opt_model[:x]) : nothing +end + +get_solution(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = get_solution(Val(semantics), model, opt_model) + +function get_solution_dict(semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model) where {Semantics} + sem = Accessors.Internal.get_semantics(semantics) + isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) + (ids, _, _, sem_varmtx) = sem + is_solved(opt_model) ? Dict(ids .=> sem_varmtx(model)' * value.(opt_model[:x])) : nothing +end + +get_solution_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = get_solution_dict(Val(semantics), model, opt_model) + """ $(TYPEDSIGNATURES) @@ -130,9 +148,7 @@ Returns a vector of fluxes of the model, if solved. flux_vector(flux_balance_analysis(model, ...)) ``` """ -flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? reaction_variables_matrix(model)' * value.(opt_model[:x]) : - nothing +flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = get_solution(:reaction, model, opt_model) """ $(TYPEDSIGNATURES) @@ -144,10 +160,7 @@ Returns the fluxes of the model as a reaction-keyed dictionary, if solved. flux_dict(model, flux_balance_analysis(model, ...)) ``` """ -flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = - is_solved(opt_model) ? - Dict(reactions(model) .=> reaction_variables_matrix(model)' * value.(opt_model[:x])) : - nothing +flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = get_solution_dict(:reaction, model, opt_model) """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 51cb8eb03..27a8f2acb 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -188,6 +188,16 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) +Get a mapping of enzyme variables to variables - for enzyme constrained models, +this is just a direct mapping. +""" +Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict( + gid => Dict(gid => 1.0) for gid in genes(model) +) # this is enough for all the semantics to work + +""" +$(TYPEDSIGNATURES) + Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the coupling of the wrapped model, coupling for split (arm) reactions, and the coupling for the total enzyme capacity. diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/BalancedGrowthCommunityModel.jl index 52cbbba7f..d52d12c3a 100644 --- a/src/utils/BalancedGrowthCommunityModel.jl +++ b/src/utils/BalancedGrowthCommunityModel.jl @@ -5,7 +5,7 @@ Extract the solution of a specific `community_member` from `opt_model`, which is a solved optimization model built from the `community_model`. Removes the `community_member` prefix in the string ids of the returned dictionary. """ -get_solution( +get_community_member_solution( community_model::BalancedGrowthCommunityModel, opt_model, community_member::CommunityMember, From d046f42f3855c93e34eec1ca1c312941434af3bd Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 10:26:11 +0100 Subject: [PATCH 148/531] Update src/types/wrappers/EnzymeConstrainedModel.jl Co-authored-by: Mirek Kratochvil --- src/solver.jl | 4 ++-- src/types/wrappers/EnzymeConstrainedModel.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index ac8673b1b..88362a5a3 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -127,13 +127,13 @@ function get_solution(semantics::Val{Semantics}, model::AbstractMetabolicModel, is_solved(opt_model) ? sem_varmtx(model)' * value.(opt_model[:x]) : nothing end -get_solution(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = get_solution(Val(semantics), model, opt_model) +get_solution(semantics::Symbol, model::AbstractMetabolicModel) = opt_model -> get_solution(Val(semantics), model, opt_model) function get_solution_dict(semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model) where {Semantics} sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (ids, _, _, sem_varmtx) = sem - is_solved(opt_model) ? Dict(ids .=> sem_varmtx(model)' * value.(opt_model[:x])) : nothing + is_solved(opt_model) ? Dict(ids(model) .=> sem_varmtx(model)' * value.(opt_model[:x])) : nothing end get_solution_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = get_solution_dict(Val(semantics), model, opt_model) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 27a8f2acb..149f1f178 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -188,7 +188,7 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Get a mapping of enzyme variables to variables - for enzyme constrained models, +Get a mapping of enzyme variables to variables -- for enzyme constrained models, this is just a direct mapping. """ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict( From e39d6aeef24b59b479b3a565415e219a9f232940 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 10:40:15 +0100 Subject: [PATCH 149/531] change get_value to values --- src/solver.jl | 27 +++++++++++++++----- src/types/wrappers/EnzymeConstrainedModel.jl | 5 ++-- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 88362a5a3..365b38618 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -120,23 +120,34 @@ solved_objective_value(flux_balance_analysis(model, ...)) solved_objective_value(opt_model)::Maybe{Float64} = is_solved(opt_model) ? objective_value(opt_model) : nothing -function get_solution(semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model) where {Semantics} +function values( + semantics::Val{Semantics}, + model::AbstractMetabolicModel, + opt_model, +) where {Semantics} sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (_, _, _, sem_varmtx) = sem is_solved(opt_model) ? sem_varmtx(model)' * value.(opt_model[:x]) : nothing end -get_solution(semantics::Symbol, model::AbstractMetabolicModel) = opt_model -> get_solution(Val(semantics), model, opt_model) +values(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = + values(Val(semantics), model, opt_model) -function get_solution_dict(semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model) where {Semantics} +function values_dict( + semantics::Val{Semantics}, + model::AbstractMetabolicModel, + opt_model, +) where {Semantics} sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (ids, _, _, sem_varmtx) = sem - is_solved(opt_model) ? Dict(ids(model) .=> sem_varmtx(model)' * value.(opt_model[:x])) : nothing + is_solved(opt_model) ? Dict(ids(model) .=> sem_varmtx(model)' * value.(opt_model[:x])) : + nothing end -get_solution_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = get_solution_dict(Val(semantics), model, opt_model) +values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = + values_dict(Val(semantics), model, opt_model) """ $(TYPEDSIGNATURES) @@ -148,7 +159,8 @@ Returns a vector of fluxes of the model, if solved. flux_vector(flux_balance_analysis(model, ...)) ``` """ -flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = get_solution(:reaction, model, opt_model) +flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = + get_solution(:reaction, model, opt_model) """ $(TYPEDSIGNATURES) @@ -160,7 +172,8 @@ Returns the fluxes of the model as a reaction-keyed dictionary, if solved. flux_dict(model, flux_balance_analysis(model, ...)) ``` """ -flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = get_solution_dict(:reaction, model, opt_model) +flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = + get_solution_dict(:reaction, model, opt_model) """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 149f1f178..5d2c3b4ea 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -191,9 +191,8 @@ $(TYPEDSIGNATURES) Get a mapping of enzyme variables to variables -- for enzyme constrained models, this is just a direct mapping. """ -Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict( - gid => Dict(gid => 1.0) for gid in genes(model) -) # this is enough for all the semantics to work +Accessors.enzyme_variables(model::EnzymeConstrainedModel) = + Dict(gid => Dict(gid => 1.0) for gid in genes(model)) # this is enough for all the semantics to work """ $(TYPEDSIGNATURES) From 22f532b7e87f639add085dac3a434bbe3a9d4297 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 11:30:13 +0100 Subject: [PATCH 150/531] remove flux_vec and flux_dict --- docs/src/examples/15_enzyme_constrained.jl | 4 +- src/analysis/flux_balance_analysis.jl | 8 +-- .../parsimonious_flux_balance_analysis.jl | 8 +-- src/analysis/variability_analysis.jl | 2 +- src/solver.jl | 63 ++++++++++++------- src/utils/enzyme_constrained.jl | 19 ------ src/utils/flux_summary.jl | 2 +- .../analysis/minimize_metabolic_adjustment.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 8 +-- test/types/BalancedGrowthCommunityModel.jl | 2 +- 10 files changed, 58 insertions(+), 60 deletions(-) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index 5d2968016..babad0336 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -81,11 +81,11 @@ opt_model = flux_balance_analysis(enzyme_constrained_model, GLPK.Optimizer) # Get the fluxes -flux_sol = flux_dict(enzyme_constrained_model, opt_model) +flux_sol = values_dict(:reaction, enzyme_constrained_model, opt_model) # Get the gene product concentrations -gp_concs = gene_product_dict(enzyme_constrained_model, opt_model) +gp_concs = values_dict(:enzyme, enzyme_constrained_model, opt_model) # Get the total masses assigned to each mass group diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index bfe69afb7..a8689d89c 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -6,7 +6,7 @@ of the model, if the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). -This function is kept for backwards compatibility, use [`flux_vector`](@ref) +This function is kept for backwards compatibility, use [`values`](@ref) instead. """ flux_balance_analysis_vec( @@ -14,7 +14,7 @@ flux_balance_analysis_vec( args...; kwargs..., )::Maybe{Vector{Float64}} = - flux_vector(model, flux_balance_analysis(model, args...; kwargs...)) + values(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -22,7 +22,7 @@ $(TYPEDSIGNATURES) A variant of FBA that returns a dictionary assigning fluxes to reactions, if the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). -This function is kept for backwards compatibility, use [`flux_dict`](@ref) +This function is kept for backwards compatibility, use [`values_dict`](@ref) instead. """ flux_balance_analysis_dict( @@ -30,7 +30,7 @@ flux_balance_analysis_dict( args...; kwargs..., )::Maybe{Dict{String,Float64}} = - flux_dict(model, flux_balance_analysis(model, args...; kwargs...)) + values_dict(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 8f8ab04f4..993b5d3bb 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -94,11 +94,11 @@ Returns a vector of fluxes in the same order as the reactions in `model`. Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. -This function is kept for backwards compatibility, use [`flux_vector`](@ref) +This function is kept for backwards compatibility, use [`values`](@ref) instead. """ parsimonious_flux_balance_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = - flux_vector(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) + values(:reaction, model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -107,8 +107,8 @@ Perform parsimonious flux balance analysis on `model` using `optimizer`. Returns a dictionary mapping the reaction IDs to fluxes. Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. -This function is kept for backwards compatibility, use [`flux_dict`](@ref) +This function is kept for backwards compatibility, use [`values_dict`](@ref) instead. """ parsimonious_flux_balance_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = - flux_dict(model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) + values_dict(:reaction, model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 1444a5df2..15d16b33a 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -208,7 +208,7 @@ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer model, optimizer; kwargs..., - ret = sol -> flux_vector(model, sol), + ret = sol -> values(:reaction, model, sol), ) flxs = reactions(model) dicts = zip.(Ref(flxs), vs) diff --git a/src/solver.jl b/src/solver.jl index 365b38618..f9939b955 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -120,6 +120,17 @@ solved_objective_value(flux_balance_analysis(model, ...)) solved_objective_value(opt_model)::Maybe{Float64} = is_solved(opt_model) ? objective_value(opt_model) : nothing +""" +$(TYPEDSIGNATURES) + +From the optimized model, returns a vector of values for the selected +`semantics`. If the model did not solve, returns `nothing`. + +# Example +``` +values(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) +``` +""" function values( semantics::Val{Semantics}, model::AbstractMetabolicModel, @@ -131,9 +142,31 @@ function values( is_solved(opt_model) ? sem_varmtx(model)' * value.(opt_model[:x]) : nothing end +""" +$(TYPEDSIGNATURES) + +Convenience variant of [`values`](@ref). + +# Example +``` +values(:reaction, model, flux_balance_analysis(model, ...)) # in order of reactions(model) +``` +""" values(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = values(Val(semantics), model, opt_model) +""" +$(TYPEDSIGNATURES) + +From the optimized model, returns a dictionary mapping semantic IDs to their +solved values for the selected `semantics`. If the model did not solve, returns +`nothing`. + +# Example +``` +values_dict(Val(:reaction), model, flux_balance_analysis(model, ...)) +``` +""" function values_dict( semantics::Val{Semantics}, model::AbstractMetabolicModel, @@ -146,46 +179,30 @@ function values_dict( nothing end -values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = - values_dict(Val(semantics), model, opt_model) - """ $(TYPEDSIGNATURES) -Returns a vector of fluxes of the model, if solved. +Convenience variant of [`values_dict`](@ref). # Example ``` -flux_vector(flux_balance_analysis(model, ...)) +values_dict(:reaction, model, flux_balance_analysis(model, ...)) ``` """ -flux_vector(model::AbstractMetabolicModel, opt_model)::Maybe{Vector{Float64}} = - get_solution(:reaction, model, opt_model) - -""" -$(TYPEDSIGNATURES) - -Returns the fluxes of the model as a reaction-keyed dictionary, if solved. - -# Example -``` -flux_dict(model, flux_balance_analysis(model, ...)) -``` -""" -flux_dict(model::AbstractMetabolicModel, opt_model)::Maybe{Dict{String,Float64}} = - get_solution_dict(:reaction, model, opt_model) +values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = + values_dict(Val(semantics), model, opt_model) """ $(TYPEDSIGNATURES) -A pipeable variant of `flux_dict`. +A pipeable variant of the convenience variant of [`values_dict`](@ref). # Example ``` -flux_balance_analysis(model, ...) |> flux_dict(model) +flux_balance_analysis(model, ...) |> values_dict(:reaction, model) ``` """ -flux_dict(model::AbstractMetabolicModel) = opt_model -> flux_dict(model, opt_model) +values_dict(semantics::Symbol, model::AbstractMetabolicModel) = opt_model -> values_dict(Val(semantics), model, opt_model) @export_locals end diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index 285b807d1..3ae4197d9 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -1,25 +1,6 @@ """ $(TYPEDSIGNATURES) -Return a dictionary mapping protein molar concentrations to their ids. The -argument `opt_model` is a solved optimization problem, typically returned by -[`flux_balance_analysis`](@ref). See [`flux_dict`](@ref) for the corresponding -function that returns a dictionary of solved fluxes. -""" -gene_product_dict(model::EnzymeConstrainedModel, opt_model) = - is_solved(opt_model) ? - Dict(genes(model) .=> value.(opt_model[:x])[(length(model.columns)+1):end]) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_dict`](@ref). -""" -gene_product_dict(model::EnzymeConstrainedModel) = x -> gene_product_dict(model, x) - -""" -$(TYPEDSIGNATURES) - Extract the mass utilization in mass groups from a solved [`EnzymeConstrainedModel`](@ref). """ gene_product_mass_group_dict(model::EnzymeConstrainedModel, opt_model) = diff --git a/src/utils/flux_summary.jl b/src/utils/flux_summary.jl index c52f1fa78..55a06fbf6 100644 --- a/src/utils/flux_summary.jl +++ b/src/utils/flux_summary.jl @@ -13,7 +13,7 @@ larger than `large_flux_bound` are only stored if `keep_unbounded` is `true`. # Example ``` -julia> sol = flux_dict(flux_balance_analysis(model, Tulip.Optimizer)) +julia> sol = values_dict(:reaction, model, flux_balance_analysis(model, Tulip.Optimizer)) julia> fr = flux_summary(sol) Biomass: BIOMASS_Ecoli_core_w_GAM: 0.8739 diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl index 8e86c58aa..2661c70b1 100644 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ b/test/analysis/minimize_metabolic_adjustment.jl @@ -9,7 +9,7 @@ sol, Clarabel.Optimizer; modifications = [silence], - ) |> flux_dict(model) + ) |> values_dict(:reaction, model) @test isapprox(moma["biomass1"], 0.07692307692307691, atol = QP_TEST_TOLERANCE) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 53490a3e8..21e734d63 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -57,8 +57,8 @@ modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = flux_dict(gm, opt_model) - prot_concens = gene_product_dict(gm, opt_model) + rxn_fluxes = values_dict(:reaction, (gm, opt_model)) + prot_concens = values(gm, opt_model) @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], @@ -147,8 +147,8 @@ end modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = flux_dict(gm, opt_model) - gene_products = gene_product_dict(gm, opt_model) + rxn_fluxes = values_dict(:reaction, gm, opt_model) + gene_products = values_dict(:enzyme, gm, opt_model) mass_groups = gene_product_mass_group_dict(gm, opt_model) @test isapprox(rxn_fluxes["r6"], 3.181818181753438, atol = TEST_TOLERANCE) diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 0e9daf46b..d53002fc8 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -338,6 +338,6 @@ end modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - f_d = flux_dict(cm, opt_model) + f_d = values_dict(:reaction, cm, opt_model) @test isapprox(f_d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) end From 3db69b308c3752ac421a968dcd7ab41a223ad1cf Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 11:36:15 +0100 Subject: [PATCH 151/531] remove values -> values_vec --- src/analysis/flux_balance_analysis.jl | 6 +++--- .../parsimonious_flux_balance_analysis.jl | 16 ++++++++++++---- src/analysis/variability_analysis.jl | 2 +- src/solver.jl | 19 ++++++++++--------- test/reconstruction/enzyme_constrained.jl | 2 +- 5 files changed, 27 insertions(+), 18 deletions(-) diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index a8689d89c..92bc7ac81 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -6,7 +6,7 @@ of the model, if the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). -This function is kept for backwards compatibility, use [`values`](@ref) +This function is kept for backwards compatibility, use [`values_vec`](@ref) instead. """ flux_balance_analysis_vec( @@ -14,7 +14,7 @@ flux_balance_analysis_vec( args...; kwargs..., )::Maybe{Vector{Float64}} = - values(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) + values_vec(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -22,7 +22,7 @@ $(TYPEDSIGNATURES) A variant of FBA that returns a dictionary assigning fluxes to reactions, if the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). -This function is kept for backwards compatibility, use [`values_dict`](@ref) +This function is kept for backwards compatibility, use [`values_vec_dict`](@ref) instead. """ flux_balance_analysis_dict( diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 993b5d3bb..860248e2e 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -94,11 +94,15 @@ Returns a vector of fluxes in the same order as the reactions in `model`. Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. -This function is kept for backwards compatibility, use [`values`](@ref) +This function is kept for backwards compatibility, use [`values_vec`](@ref) instead. """ parsimonious_flux_balance_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = - values(:reaction, model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) + values_vec( + :reaction, + model, + parsimonious_flux_balance_analysis(model, args...; kwargs...), + ) """ $(TYPEDSIGNATURES) @@ -107,8 +111,12 @@ Perform parsimonious flux balance analysis on `model` using `optimizer`. Returns a dictionary mapping the reaction IDs to fluxes. Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. -This function is kept for backwards compatibility, use [`values_dict`](@ref) +This function is kept for backwards compatibility, use [`values_vec_dict`](@ref) instead. """ parsimonious_flux_balance_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = - values_dict(:reaction, model, parsimonious_flux_balance_analysis(model, args...; kwargs...)) + values_dict( + :reaction, + model, + parsimonious_flux_balance_analysis(model, args...; kwargs...), + ) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 15d16b33a..3eb7608fc 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -208,7 +208,7 @@ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer model, optimizer; kwargs..., - ret = sol -> values(:reaction, model, sol), + ret = sol -> values_vec(:reaction, model, sol), ) flxs = reactions(model) dicts = zip.(Ref(flxs), vs) diff --git a/src/solver.jl b/src/solver.jl index f9939b955..e70d3eb7c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -128,10 +128,10 @@ From the optimized model, returns a vector of values for the selected # Example ``` -values(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) +values_vec(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) ``` """ -function values( +function values_vec( semantics::Val{Semantics}, model::AbstractMetabolicModel, opt_model, @@ -145,15 +145,15 @@ end """ $(TYPEDSIGNATURES) -Convenience variant of [`values`](@ref). +Convenience variant of [`values_vec`](@ref). # Example ``` -values(:reaction, model, flux_balance_analysis(model, ...)) # in order of reactions(model) +values_vec(:reaction, model, flux_balance_analysis(model, ...)) # in order of reactions(model) ``` """ -values(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = - values(Val(semantics), model, opt_model) +values_vec(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = + values_vec(Val(semantics), model, opt_model) """ $(TYPEDSIGNATURES) @@ -182,7 +182,7 @@ end """ $(TYPEDSIGNATURES) -Convenience variant of [`values_dict`](@ref). +Convenience variant of [`values_vec_dict`](@ref). # Example ``` @@ -195,14 +195,15 @@ values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = """ $(TYPEDSIGNATURES) -A pipeable variant of the convenience variant of [`values_dict`](@ref). +A pipeable variant of the convenience variant of [`values_vec_dict`](@ref). # Example ``` flux_balance_analysis(model, ...) |> values_dict(:reaction, model) ``` """ -values_dict(semantics::Symbol, model::AbstractMetabolicModel) = opt_model -> values_dict(Val(semantics), model, opt_model) +values_dict(semantics::Symbol, model::AbstractMetabolicModel) = + opt_model -> values_dict(Val(semantics), model, opt_model) @export_locals end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 21e734d63..2c68c82e2 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -57,7 +57,7 @@ modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = values_dict(:reaction, (gm, opt_model)) + rxn_fluxes = values_dict(:reaction, gm, opt_model) prot_concens = values(gm, opt_model) @test isapprox( From b1c47d126f8b47fbdc760cf74e9372439ab9ca40 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 12:25:56 +0100 Subject: [PATCH 152/531] use semantics on enzyme groups and fix bug --- docs/src/examples/15_enzyme_constrained.jl | 2 +- src/analysis/flux_balance_analysis.jl | 2 +- .../parsimonious_flux_balance_analysis.jl | 2 +- src/solver.jl | 4 ++-- src/types/accessors/AbstractMetabolicModel.jl | 10 +++++++++ src/types/wrappers/EnzymeConstrainedModel.jl | 15 +++++++++++++ src/utils/enzyme_constrained.jl | 22 ------------------- test/reconstruction/enzyme_constrained.jl | 8 +++---- 8 files changed, 34 insertions(+), 31 deletions(-) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index babad0336..4888b43e8 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -89,7 +89,7 @@ gp_concs = values_dict(:enzyme, enzyme_constrained_model, opt_model) # Get the total masses assigned to each mass group -gene_product_mass_group_dict(enzyme_constrained_model, opt_model) +values_dict(:enzyme_group, enzyme_constrained_model, opt_model) # Variability: diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 92bc7ac81..36974ff8d 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -22,7 +22,7 @@ $(TYPEDSIGNATURES) A variant of FBA that returns a dictionary assigning fluxes to reactions, if the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). -This function is kept for backwards compatibility, use [`values_vec_dict`](@ref) +This function is kept for backwards compatibility, use [`values_dict`](@ref) instead. """ flux_balance_analysis_dict( diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 860248e2e..cfffeb412 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -111,7 +111,7 @@ Perform parsimonious flux balance analysis on `model` using `optimizer`. Returns a dictionary mapping the reaction IDs to fluxes. Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. -This function is kept for backwards compatibility, use [`values_vec_dict`](@ref) +This function is kept for backwards compatibility, use [`values_dict`](@ref) instead. """ parsimonious_flux_balance_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = diff --git a/src/solver.jl b/src/solver.jl index e70d3eb7c..a4aff02bb 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -182,7 +182,7 @@ end """ $(TYPEDSIGNATURES) -Convenience variant of [`values_vec_dict`](@ref). +Convenience variant of [`values_dict`](@ref). # Example ``` @@ -195,7 +195,7 @@ values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = """ $(TYPEDSIGNATURES) -A pipeable variant of the convenience variant of [`values_vec_dict`](@ref). +A pipeable variant of the convenience variant of [`values_dict`](@ref). # Example ``` diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 519e65dfe..b446f5f69 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -131,6 +131,16 @@ these values. """ ) +@make_variable_semantics( + :enzyme_group, + "enzyme group", + """ +Certain model types use enzymes to catalyze reactions. These enzymes typically +have capacity limitations (e.g. membrane or cytosol density constraints). Enzyme +groups collect these sets of enzymes for convenient analysis. +""" +) + """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 5d2c3b4ea..bddfe2a0f 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -197,6 +197,21 @@ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) +Get a mapping of enzyme groups to variables. +""" +function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) + enz_ids = genes(model) + Dict( + grp.group_id => Dict( + enz_ids[idx] => mm for + (idx, mm) in zip(grp.gene_product_idxs, grp.gene_product_molar_masses) + ) for grp in model.coupling_row_mass_group + ) +end + +""" +$(TYPEDSIGNATURES) + Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the coupling of the wrapped model, coupling for split (arm) reactions, and the coupling for the total enzyme capacity. diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl index 3ae4197d9..9a917b259 100644 --- a/src/utils/enzyme_constrained.jl +++ b/src/utils/enzyme_constrained.jl @@ -1,28 +1,6 @@ """ $(TYPEDSIGNATURES) -Extract the mass utilization in mass groups from a solved [`EnzymeConstrainedModel`](@ref). -""" -gene_product_mass_group_dict(model::EnzymeConstrainedModel, opt_model) = - is_solved(opt_model) ? - Dict( - grp.group_id => dot( - value.(opt_model[:x])[length(model.columns).+grp.gene_product_idxs], - grp.gene_product_molar_masses, - ) for grp in model.coupling_row_mass_group - ) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_mass_group_dict`](@ref). -""" -gene_product_mass_group_dict(model::EnzymeConstrainedModel) = - x -> gene_product_mass_group_dict(model, x) - -""" -$(TYPEDSIGNATURES) - Extract the total mass utilization in a solved [`SimplifiedEnzymeConstrainedModel`](@ref). """ gene_product_mass(model::SimplifiedEnzymeConstrainedModel, opt_model) = diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 2c68c82e2..beec04884 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -58,7 +58,7 @@ ) rxn_fluxes = values_dict(:reaction, gm, opt_model) - prot_concens = values(gm, opt_model) + prot_concens = values_dict(:enzyme, gm, opt_model) @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], @@ -67,7 +67,7 @@ ) prot_mass = sum(ecoli_core_gene_product_masses[gid] * c for (gid, c) in prot_concens) - mass_groups = gene_product_mass_group_dict(gm, opt_model) + mass_groups = values_dict(:enzyme_group, gm, opt_model) @test isapprox(prot_mass, total_gene_product_mass, atol = TEST_TOLERANCE) @test isapprox(prot_mass, mass_groups["uncategorized"], atol = TEST_TOLERANCE) @@ -83,7 +83,7 @@ change_optimizer_attribute("IPM_IterationsLimit", 1000), ], ) - mass_groups_min = gene_product_mass_group_dict(gm, opt_model) + mass_groups_min = values_dict(:enzyme_group, gm, opt_model) @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] end @@ -149,7 +149,7 @@ end rxn_fluxes = values_dict(:reaction, gm, opt_model) gene_products = values_dict(:enzyme, gm, opt_model) - mass_groups = gene_product_mass_group_dict(gm, opt_model) + mass_groups = values_dict(:enzyme_group, gm, opt_model) @test isapprox(rxn_fluxes["r6"], 3.181818181753438, atol = TEST_TOLERANCE) @test isapprox(gene_products["g4"], 0.09090909090607537, atol = TEST_TOLERANCE) From d4fb601f0a0aad091d3f4ea2156b91431d2d0ebf Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 10 Nov 2022 13:01:32 +0100 Subject: [PATCH 153/531] delete MMDF analysis and move to wrapper --- src/analysis/max_min_driving_force.jl | 274 ------------ src/reconstruction/pipes/thermodynamic.jl | 8 + src/reconstruction/thermodynamic.jl | 10 + src/types/accessors/AbstractMetabolicModel.jl | 19 + src/types/wrappers/MaxMinDrivingForceModel.jl | 413 ++++++++++++++++++ test/analysis/max_min_driving_force.jl | 71 +-- 6 files changed, 490 insertions(+), 305 deletions(-) delete mode 100644 src/analysis/max_min_driving_force.jl create mode 100644 src/reconstruction/pipes/thermodynamic.jl create mode 100644 src/reconstruction/thermodynamic.jl create mode 100644 src/types/wrappers/MaxMinDrivingForceModel.jl diff --git a/src/analysis/max_min_driving_force.jl b/src/analysis/max_min_driving_force.jl deleted file mode 100644 index 55b9ee7bb..000000000 --- a/src/analysis/max_min_driving_force.jl +++ /dev/null @@ -1,274 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Perform a max-min driving force analysis on the `model`, as defined by Noor, et al., -"Pathway thermodynamics highlights kinetic obstacles in central metabolism.", PLoS -computational biology, 2014. - -The function uses the supplied `optimizer` and `reaction_standard_gibbs_free_energies`. -Optionally, `flux_solution` can be used to set the directions of each reaction in `model` -(all reactions are assumed to proceed forward and are active by default). The supplied -`flux_solution` should be free of internal cycles i.e. thermodynamically consistent. This -optional input is important if a reaction in `model` normally runs in reverse (negative -flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` are also -ignored in the analysis function (for numerical stability). - -The max-min driving force algorithm returns the Gibbs free energy of the reactions, the -concentrations of metabolites and the actual maximum minimum driving force. The optimization -problem solved is: -``` -max min -ΔᵣG -s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) - ΔᵣG ≤ 0 - ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas constant, T is -the temperature, S is the stoichiometry of the model, and C is the vector of metabolite -concentrations (and their respective lower and upper bounds). - -In case no feasible solution exists, `nothing` is returned. - -Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the -max-min driving force. This should include water and proton importers. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` need to be -specified so that they can be ignored in the calculations. Effectively this assumes an -aqueous environment at constant pH is used. - -`constant_concentrations` is used to fix the concentrations of certain metabolites (such as -CO₂). `concentration_ratios` is used to specify additional constraints on metabolite pair -concentrations (typically, this is done with various cofactors such as the ATP/ADP ratio. -For example, you can fix the concentration of ATP to be always 5× higher than of ADP by -specifying `Dict(("ATP","ADP") => 5.0)` - -`concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in the -optimization problems. - -`T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. -""" -function max_min_driving_force( - model::AbstractMetabolicModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}, - optimizer; - flux_solution::Dict{String,Float64} = Dict{String,Float64}(), - proton_ids::Vector{String} = ["h_c", "h_e"], - water_ids::Vector{String} = ["h2o_c", "h2o_e"], - constant_concentrations::Dict{String,Float64} = Dict{String,Float64}(), - concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ - Tuple{String,String}, - Float64, - }(), - concentration_lb = 1e-9, - concentration_ub = 100e-3, - T = constants.T, - R = constants.R, - small_flux_tol = 1e-6, - modifications = [], - ignore_reaction_ids = [], -) - opt_model = Model(optimizer) - - @variables opt_model begin - mmdf - logcs[1:n_metabolites(model)] - dgrs[1:n_variables(model)] - end - - # set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption) - proton_idxs = Int.(indexin(proton_ids, metabolites(model))) - for idx in proton_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # set water concentrations to zero (aqueous condition assumptions) - water_idxs = Int.(indexin(water_ids, metabolites(model))) - for idx in water_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # only consider reactions with supplied thermodynamic data AND have a flux bigger than - # small_flux_tol => finds a thermodynamic profile that explains flux_solution - active_rids = filter( - rid -> - haskey(reaction_standard_gibbs_free_energies, rid) && - abs(get(flux_solution, rid, small_flux_tol / 2)) > small_flux_tol && - !(rid in ignore_reaction_ids), - variables(model), - ) - active_ridxs = Int.(indexin(active_rids, variables(model))) - - # give dummy dG0 for reactions that don't have data - dg0s = - [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in variables(model)] - - S = stoichiometry(model) - - @constraint(opt_model, dgrs .== dg0s .+ (R * T) * S' * logcs) - - # thermodynamics should correspond to the fluxes - flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in variables(model)] - - # only constrain reactions that have thermo data - @constraint(opt_model, dgrs[active_ridxs] .* flux_signs[active_ridxs] .<= 0) - - # add the absolute bounds - missing_mets = - [mid for mid in keys(constant_concentrations) if !(mid in metabolites(model))] - !isempty(missing_mets) && - throw(DomainError(missing_mets, "metabolite(s) not found in model.")) - for (midx, mid) in enumerate(metabolites(model)) # idx in opt_model (missing ignore_metabolites) - midx in water_idxs && continue - midx in proton_idxs && continue - if haskey(constant_concentrations, mid) - JuMP.fix(logcs[midx], log(constant_concentrations[mid])) - else - # this metabolite needs bounds - @constraint( - opt_model, - log(concentration_lb) <= logcs[midx] <= log(concentration_ub) - ) - end - end - - # add the relative bounds - for ((mid1, mid2), val) in concentration_ratios - idxs = indexin([mid1, mid2], metabolites(model)) # TODO: this is not performant - any(isnothing.(idxs)) && - throw(DomainError((mid1, mid2), "metabolite pair not found in model.")) - @constraint(opt_model, logcs[idxs[1]] == log(val) + logcs[idxs[2]]) - end - - @constraint(opt_model, mmdf .<= -dgrs[active_ridxs] .* flux_signs[active_ridxs]) - - @objective(opt_model, Max, mmdf) - - # apply the modifications, if any - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - is_solved(opt_model) || return nothing - - return ( - mmdf = value(opt_model[:mmdf]), - dg_reactions = Dict( - rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(variables(model)) - ), - concentrations = Dict( - mid => exp(value(opt_model[:logcs][i])) for - (i, mid) in enumerate(metabolites(model)) - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -Perform a variant of flux variability analysis on a max min driving force type problem. -Arguments are forwarded to [`max_min_driving_force`](@ref). Calls [`screen`](@ref) -internally and possibly distributes computation across `workers`. If -`optimal_objective_value = nothing`, the function first performs regular max min driving -force analysis to find the max min driving force of the model and sets this to -`optimal_objective_value`. Then iteratively maximizes and minimizes the driving force across -each reaction, and then the concentrations while staying close to the original max min -driving force as specified in `bounds`. - -The `bounds` is a user-supplied function that specifies the max min driving force bounds for -the variability optimizations, by default it restricts the flux objective value to the -precise optimum reached in the normal max min driving force analysis. It can return `-Inf` -and `Inf` in first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and -[`objective_bounds`](@ref) for simple bounds. - -Returns a matrix of solutions to [`max_min_driving_force`](@ref) additionally constrained as -described above, where the rows are in the order of the reactions and then the metabolites -of the `model`. For the reaction rows the first column is the maximum dG of that reaction, -and the second column is the minimum dG of that reaction subject to the above constraints. -For the metabolite rows, the first column is the maximum concentration, and the second column -is the minimum concentration subject to the constraints above. -""" -function max_min_driving_force_variability( - model::AbstractMetabolicModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}, - optimizer; - workers = [myid()], - optimal_objective_value = nothing, - bounds = z -> (z, Inf), - modifications = [], - kwargs..., -) - if isnothing(optimal_objective_value) - initsol = max_min_driving_force( - model, - reaction_standard_gibbs_free_energies, - optimizer; - modifications, - kwargs..., - ) - mmdf = initsol.mmdf - else - mmdf = optimal_objective_value - end - - lb, ub = bounds(mmdf) - - dgr_variants = [ - [[_mmdf_add_df_bound(lb, ub), _mmdf_dgr_objective(ridx, sense)]] for - ridx = 1:n_variables(model), sense in [MAX_SENSE, MIN_SENSE] - ] - concen_variants = [ - [[_mmdf_add_df_bound(lb, ub), _mmdf_concen_objective(midx, sense)]] for - midx = 1:n_metabolites(model), sense in [MAX_SENSE, MIN_SENSE] - ] - - return screen( - model; - args = [dgr_variants; concen_variants], - analysis = (m, args) -> max_min_driving_force( - m, - reaction_standard_gibbs_free_energies, - optimizer; - modifications = [args; modifications], - kwargs..., - ), - workers, - ) -end - -""" -$(TYPEDSIGNATURES) - -Helper function to change the objective to optimizing some dG. -""" -function _mmdf_dgr_objective(ridx, sense) - (model, opt_model) -> begin - @objective(opt_model, sense, opt_model[:dgrs][ridx]) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function to change the objective to optimizing some concentration. -""" -function _mmdf_concen_objective(midx, sense) - (model, opt_model) -> begin - @objective(opt_model, sense, opt_model[:logcs][midx]) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function to add a new constraint on the driving force. -""" -function _mmdf_add_df_bound(lb, ub) - (model, opt_model) -> begin - if lb == ub - fix(opt_model[:mmdf], lb; force = true) - else - @constraint(opt_model, lb <= opt_model[:mmdf] <= ub) - end - end -end diff --git a/src/reconstruction/pipes/thermodynamic.jl b/src/reconstruction/pipes/thermodynamic.jl new file mode 100644 index 000000000..1509dc5d1 --- /dev/null +++ b/src/reconstruction/pipes/thermodynamic.jl @@ -0,0 +1,8 @@ +""" +$(TYPEDSIGNATURES) + +A pipe-able function that specifies a model variant that solves the max-min +driving force problem. Calls [`make_max_min_driving_force_model`](@ref) +internally. +""" +with_max_min_driving_force_analysis(args...; kwargs...) = m -> make_max_min_driving_force_model(m, args...; kwargs...) diff --git a/src/reconstruction/thermodynamic.jl b/src/reconstruction/thermodynamic.jl new file mode 100644 index 000000000..c09d98d11 --- /dev/null +++ b/src/reconstruction/thermodynamic.jl @@ -0,0 +1,10 @@ +""" +$(TYPEDSIGNATURES) + +Construct a [`MaxMinDrivingForceModel`](@ref) so that max min driving force +analysis can be performed on `model`. +""" +make_max_min_driving_force_model( + model::AbstractMetabolicModel; + kwargs... +) = MaxMinDrivingForceModel(; inner = model, kwargs...) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index b446f5f69..392c5d47c 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -141,6 +141,25 @@ groups collect these sets of enzymes for convenient analysis. """ ) +@make_variable_semantics( + :metabolite_log_concentration, + "metabolite log concentration", + """ +Certain model types use metabolite concentrations instead of reaction fluxes are +variables. This semantic grouping uses the log (base e) metabolite concentration +to make thermodynamic calculations easier. +""" +) + +@make_variable_semantics( + :gibbs_free_energy_reaction, + "Gibbs free energy of reaction", + """ +Thermodynamic models need to ensure that the ΔG of each reaction is negative +(2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. +""" +) + """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/types/wrappers/MaxMinDrivingForceModel.jl new file mode 100644 index 000000000..2c39720cd --- /dev/null +++ b/src/types/wrappers/MaxMinDrivingForceModel.jl @@ -0,0 +1,413 @@ +""" +$(TYPEDEF) + +Return a [`MaxMinDrivingForceModel`](@ref) that can be used to perform max-min +driving force analysis. It is based on the work by Noor, et al., "Pathway +thermodynamics highlights kinetic obstacles in central metabolism.", PLoS +computational biology, 2014. + +When [`flux_balance_analysis`](@ref) is called in this type of model, the +max-min driving force algorithm is solved. It returns the Gibbs free energy of +the reactions, the concentrations of metabolites, and the actual maximum minimum +driving force across all reactions in the model. The optimization problem solved +is: +``` +max min -ΔᵣG +s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) + ΔᵣG ≤ 0 + ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) +``` +where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas +constant, T is the temperature, S is the stoichiometry of the model, and C is +the vector of metabolite concentrations (and their respective lower and upper +bounds). + +In case no feasible solution exists, `nothing` is returned. + +Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` +*need* to be specified so that they can be ignored in the calculations. +Effectively this assumes an aqueous environment at constant pH is used. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct MaxMinDrivingForceModel <: AbstractModelWrapper + "A dictionary mapping ΔrG⁰ to reactions." + reaction_standard_gibbs_free_energies::Dict{String,Float64} = Dict{String,Float64}() + + "A cycle-free reference flux solution that is used to set the directions of the reactions." + flux_solution::Dict{String,Float64} = Dict{String,Float64}() + + "Metabolite ids of protons." + proton_ids::Vector{String} = ["h_c", "h_e"] + + "Metabolite ids of water." + water_ids::Vector{String} = ["h2o_c", "h2o_e"] + + "A dictionationay mapping metabolite ids to concentrations that are held constant." + constant_concentrations::Dict{String,Float64} = Dict{String,Float64}() + + "A dictionary mapping metabolite ids to constant concentration ratios in the form `(m1, m2) = r === m1/m2 = r`." + concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ + Tuple{String,String}, + Float64, + }() + + "Global metabolite concentration lower bound." + concentration_lb = 1e-9 + + "Global metabolite concentration upper bound." + concentration_ub = 100e-3 + + "Thermodynamic temperature." + T::Float64 = constants.T + + "Real gas constant." + R::Float64 = constants.R + + "Tolerance use to distinguish flux carrying reactions from zero flux reactions." + small_flux_tol::Float64 = 1e-6 + + "Maximum absolute ΔG bound allowed by a reaction." + max_dg_bound::Float64 = 1000.0 + + "Reaction ids that are ignored internally during thermodynamic calculations. This should include water and proton importers." + ignore_reaction_ids::Vector{String} = String[] + + "Inner metabolic model calculations are based on." + inner::AbstractMetabolicModel +end + +Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner + +""" +$(TYPEDSIGNATURES) + +The variables for max-min driving force analysis are the actual maximum minimum +driving force of the model, the log metabolite concentrations, and the gibbs +free energy reaction potentials across each reaction. +""" +Accessors.variables(model::MaxMinDrivingForceModel) = ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] + +""" +$(TYPEDSIGNATURES) + +Helper function that returns the unmangled variable IDs. +""" +get_unmangled_variables(model::MaxMinDrivingForceModel) = ["mmdf"; metabolites(model); reactions(model)] + +""" +$(TYPEDSIGNATURES) + +The number of variables is 1 + the number of metabolites + the number of +reactions, where the 1 comes from the total max-min driving force. +""" +Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + n_reactions(model) + +""" +$(TYPEDSIGNATURES) + +Log metabolite concentration mapping to model variables. +""" +Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = + Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) + +""" +$(TYPEDSIGNATURES) + +Gibbs free energy of reaction mapping to model variables. +""" +Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = + Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) + +""" +$(TYPEDSIGNATURES) + +For this kind of the model, the objective is the the max-min-driving force which +is at index 1 in the variables. +""" +Accessors.objective(model::MaxMinDrivingForceModel) = [1.0; fill(0.0, n_variables(model)-1)] + +""" +$(TYPEDSIGNATURES) + +Get the equality constraint rhs of `model`. +""" +function Accessors.balance(model::MaxMinDrivingForceModel) + # proton water balance + num_proton_water = length(model.proton_ids) + length(model.water_ids) + proton_water_vec = spzeros(num_proton_water) + + # constant concentration balance + const_conc_vec = log.(collect(values(model.constant_concentrations))) + + # ratio balance + const_ratio_vec = log.(collect(values(model.concentration_ratios))) + + # give dummy dG0 for reactions that don't have data + dg0s = [get(model.reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] + + return [ + proton_water_vec + const_conc_vec + const_ratio_vec + dg0s + ] +end + +""" +$(TYPEDSIGNATURES) + +Get the equality constraint lhs of `model`. +""" +function Accessors.stoichiometry(model::MaxMinDrivingForceModel) + var_ids = get_unmangled_variables(model) + + # set proton and water equality constraints + num_proton_water = length(model.proton_ids) + length(model.water_ids) + proton_water_mat = spzeros(num_proton_water, n_variables(model)) + idxs = indexin([model.proton_ids; model.water_ids], var_ids) + for (i, j) in enumerate(idxs) + isnothing(j) && throw(error("Water or proton ID not found in model.")) + proton_water_mat[i, j] = 1.0 + end + + # constant concentration constraints + const_conc_mat = spzeros(length(model.constant_concentrations), n_variables(model)) + ids = collect(keys(model.constant_concentrations)) + idxs = indexin(ids, var_ids) + for (i, j) in enumerate(idxs) + isnothing(j) && throw(DomainError(ids[j], "Constant metabolite ID not found in model.")) + const_conc_mat[i, j] = 1.0 + end + + # add the relative bounds + const_ratio_mat = spzeros(length(model.concentration_ratios), n_variables(model)) + for (i, (mid1, mid2)) in enumerate(keys(model.concentration_ratios)) + idxs = indexin([mid1, mid2], var_ids) + any(isnothing.(idxs)) && throw(DomainError((mid1, mid2), "Metabolite ratio pair not found in model.")) + const_ratio_mat[i, first(idxs)] = 1.0 + const_ratio_mat[i, last(idxs)] = -1.0 + end + + # add ΔG relationships + dgrs = spdiagm(ones(length(reactions(model)))) + S = stoichiometry(model.inner) + stoich_mat = -(model.R * model.T) * S' + dg_mat = [spzeros(n_reactions(model)) stoich_mat dgrs] + + return [ + proton_water_mat + const_conc_mat + const_ratio_mat + dg_mat + ] +end + +""" +$(TYPEDSIGNATURES) + +Get the simple variable bounds of `model`. +""" +function Accessors.bounds(model::MaxMinDrivingForceModel) + var_ids = get_unmangled_variables(model) + + lbs = fill(-model.max_dg_bound, n_variables(model)) + ubs = fill(model.max_dg_bound, n_variables(model)) + + # mmdf must be positive for problem to be feasible (it is defined as -ΔG) + lbs[1] = 0.0 + ubs[1] = 1000.0 + + # log concentrations + lbs[2:(1+n_metabolites(model))] .= log(model.concentration_lb) + ubs[2:(1+n_metabolites(model))] .= log(model.concentration_ub) + + # need to make special adjustments for the constants + idxs = indexin([model.proton_ids; model.water_ids], var_ids) + lbs[idxs] .= -1.0 + ubs[idxs] .= 1.0 + + # ΔG for each reaction can be any sign, but that is filled before default + + return (lbs, ubs) +end + +""" +$(TYPEDSIGNATURES) + +A helper function that returns the reaction ids that are active. Active reaction +have thermodynamic data AND a flux bigger than `small_flux_tol` AND are not +ignored. +""" +_get_active_reaction_ids(model::MaxMinDrivingForceModel) = filter( + rid -> + haskey(model.reaction_standard_gibbs_free_energies, rid) && + abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > model.small_flux_tol && + !(rid in model.ignore_reaction_ids), + reactions(model), +) + +""" +$(TYPEDSIGNATURES) + +Return the coupling of a max-min driving force model. +""" +function Accessors.coupling(model::MaxMinDrivingForceModel) + + # only constrain reactions that have thermo data + active_rids = _get_active_reaction_ids(model) + idxs = Int.(indexin(active_rids, reactions(model))) + + # thermodynamic sign should correspond to the fluxes + flux_signs = spzeros(length(idxs), n_reactions(model)) + for (i, j) in enumerate(idxs) + flux_signs[i, j] = sign(model.flux_solution[reactions(model)[j]]) + end + + neg_dg_mat = [ + spzeros(length(idxs)) spzeros(length(idxs), n_metabolites(model)) flux_signs + ] + + mmdf_mat = sparse( + [ + -ones(length(idxs)) spzeros(length(idxs), n_metabolites(model)) -flux_signs + ] + ) + + return [ + neg_dg_mat + mmdf_mat + ] +end + +""" +$(TYPEDSIGNATURES) + +Return the coupling bounds of a max-min driving force model. +""" +function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) + n = length(_get_active_reaction_ids(model)) + neg_dg_lb = fill(-model.max_dg_bound, n) + neg_dg_ub = fill(0.0, n) + + mmdf_lb = fill(0.0, n) + mmdf_ub = fill(model.max_dg_bound, n) + + return ( + [neg_dg_lb; mmdf_lb], + [neg_dg_ub; mmdf_ub] + ) +end + +# function Accessors. + +# """ +# $(TYPEDSIGNATURES) + +# Perform a variant of flux variability analysis on a max min driving force type problem. +# Arguments are forwarded to [`max_min_driving_force`](@ref). Calls [`screen`](@ref) +# internally and possibly distributes computation across `workers`. If +# `optimal_objective_value = nothing`, the function first performs regular max min driving +# force analysis to find the max min driving force of the model and sets this to +# `optimal_objective_value`. Then iteratively maximizes and minimizes the driving force across +# each reaction, and then the concentrations while staying close to the original max min +# driving force as specified in `bounds`. + +# The `bounds` is a user-supplied function that specifies the max min driving force bounds for +# the variability optimizations, by default it restricts the flux objective value to the +# precise optimum reached in the normal max min driving force analysis. It can return `-Inf` +# and `Inf` in first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and +# [`objective_bounds`](@ref) for simple bounds. + +# Returns a matrix of solutions to [`max_min_driving_force`](@ref) additionally constrained as +# described above, where the rows are in the order of the reactions and then the metabolites +# of the `model`. For the reaction rows the first column is the maximum dG of that reaction, +# and the second column is the minimum dG of that reaction subject to the above constraints. +# For the metabolite rows, the first column is the maximum concentration, and the second column +# is the minimum concentration subject to the constraints above. +# """ +# function max_min_driving_force_variability( +# model::AbstractMetabolicModel, +# reaction_standard_gibbs_free_energies::Dict{String,Float64}, +# optimizer; +# workers = [myid()], +# optimal_objective_value = nothing, +# bounds = z -> (z, Inf), +# modifications = [], +# kwargs..., +# ) +# if isnothing(optimal_objective_value) +# initsol = max_min_driving_force( +# model, +# reaction_standard_gibbs_free_energies, +# optimizer; +# modifications, +# kwargs..., +# ) +# mmdf = initsol.mmdf +# else +# mmdf = optimal_objective_value +# end + +# lb, ub = bounds(mmdf) + +# dgr_variants = [ +# [[_mmdf_add_df_bound(lb, ub), _mmdf_dgr_objective(ridx, sense)]] for +# ridx = 1:n_variables(model), sense in [MAX_SENSE, MIN_SENSE] +# ] +# concen_variants = [ +# [[_mmdf_add_df_bound(lb, ub), _mmdf_concen_objective(midx, sense)]] for +# midx = 1:n_metabolites(model), sense in [MAX_SENSE, MIN_SENSE] +# ] + +# return screen( +# model; +# args = [dgr_variants; concen_variants], +# analysis = (m, args) -> max_min_driving_force( +# m, +# reaction_standard_gibbs_free_energies, +# optimizer; +# modifications = [args; modifications], +# kwargs..., +# ), +# workers, +# ) +# end + +# """ +# $(TYPEDSIGNATURES) + +# Helper function to change the objective to optimizing some dG. +# """ +# function _mmdf_dgr_objective(ridx, sense) +# (model, opt_model) -> begin +# @objective(opt_model, sense, opt_model[:dgrs][ridx]) +# end +# end + +# """ +# $(TYPEDSIGNATURES) + +# Helper function to change the objective to optimizing some concentration. +# """ +# function _mmdf_concen_objective(midx, sense) +# (model, opt_model) -> begin +# @objective(opt_model, sense, opt_model[:logcs][midx]) +# end +# end + +# """ +# $(TYPEDSIGNATURES) + +# Helper function to add a new constraint on the driving force. +# """ +# function _mmdf_add_df_bound(lb, ub) +# (model, opt_model) -> begin +# if lb == ub +# fix(opt_model[:mmdf], lb; force = true) +# else +# @constraint(opt_model, lb <= opt_model[:mmdf] <= ub) +# end +# end +# end diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 9ecbf3800..232ce9c8f 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -8,11 +8,10 @@ modifications = [add_loopless_constraints()], ) - sol = max_min_driving_force( - model, + mmdfm = make_max_min_driving_force_model( + model; reaction_standard_gibbs_free_energies, - Tulip.Optimizer; - flux_solution = flux_solution, + flux_solution, proton_ids = ["h_c", "h_e"], water_ids = ["h2o_c", "h2o_e"], concentration_ratios = Dict( @@ -23,37 +22,47 @@ concentration_lb = 1e-6, concentration_ub = 100e-3, ignore_reaction_ids = ["H2Ot"], - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - @test isapprox(sol.mmdf, 1.7661155558545698, atol = TEST_TOLERANCE) - - sols = max_min_driving_force_variability( - model, - reaction_standard_gibbs_free_energies, + opt_model = flux_balance_analysis( + mmdfm, Tulip.Optimizer; - bounds = gamma_bounds(0.9), - flux_solution = flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict{Tuple{String,String},Float64}( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - constant_concentrations = Dict{String,Float64}( - # "pi_c" => 10e-3 - ), - concentration_lb = 1e-6, - concentration_ub = 100e-3, - ignore_reaction_ids = ["H2Ot"], modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - pyk_idx = first(indexin(["PYK"], variables(model))) - @test isapprox( - sols[pyk_idx, 1].dg_reactions["PYK"], - -1.5895040002691128; - atol = TEST_TOLERANCE, - ) + # get mmdf + @test isapprox(solved_objective_value(opt_model), 1.7661155558545698, atol = TEST_TOLERANCE) + + # values_dict(:reaction, mmdfm, opt_model) # TODO throw missing semantics error + @test length(values_dict(:metabolite_log_concentration, mmdfm, opt_model)) == 72 + @test length(values_dict(:gibbs_free_energy_reaction, mmdfm, opt_model)) == 95 + + # sols = max_min_driving_force_variability( + # model, + # reaction_standard_gibbs_free_energies, + # Tulip.Optimizer; + # bounds = gamma_bounds(0.9), + # flux_solution = flux_solution, + # proton_ids = ["h_c", "h_e"], + # water_ids = ["h2o_c", "h2o_e"], + # concentration_ratios = Dict{Tuple{String,String},Float64}( + # ("atp_c", "adp_c") => 10.0, + # ("nadh_c", "nad_c") => 0.13, + # ("nadph_c", "nadp_c") => 1.3, + # ), + # constant_concentrations = Dict{String,Float64}( + # # "pi_c" => 10e-3 + # ), + # concentration_lb = 1e-6, + # concentration_ub = 100e-3, + # ignore_reaction_ids = ["H2Ot"], + # modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + # ) + + # pyk_idx = first(indexin(["PYK"], variables(model))) + # @test isapprox( + # sols[pyk_idx, 1].dg_reactions["PYK"], + # -1.5895040002691128; + # atol = TEST_TOLERANCE, + # ) end From daa6f3e3f734a7123c95a310987e5d89bfc834cc Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 14:24:43 +0100 Subject: [PATCH 154/531] working variability analysis --- src/reconstruction/pipes/thermodynamic.jl | 3 +- src/reconstruction/thermodynamic.jl | 8 +- src/types/accessors/AbstractMetabolicModel.jl | 2 +- src/types/wrappers/MaxMinDrivingForceModel.jl | 182 ++++-------------- test/analysis/max_min_driving_force.jl | 46 ++--- 5 files changed, 60 insertions(+), 181 deletions(-) diff --git a/src/reconstruction/pipes/thermodynamic.jl b/src/reconstruction/pipes/thermodynamic.jl index 1509dc5d1..348de78fd 100644 --- a/src/reconstruction/pipes/thermodynamic.jl +++ b/src/reconstruction/pipes/thermodynamic.jl @@ -5,4 +5,5 @@ A pipe-able function that specifies a model variant that solves the max-min driving force problem. Calls [`make_max_min_driving_force_model`](@ref) internally. """ -with_max_min_driving_force_analysis(args...; kwargs...) = m -> make_max_min_driving_force_model(m, args...; kwargs...) +with_max_min_driving_force_analysis(args...; kwargs...) = + m -> make_max_min_driving_force_model(m, args...; kwargs...) diff --git a/src/reconstruction/thermodynamic.jl b/src/reconstruction/thermodynamic.jl index c09d98d11..a2d35afa2 100644 --- a/src/reconstruction/thermodynamic.jl +++ b/src/reconstruction/thermodynamic.jl @@ -2,9 +2,7 @@ $(TYPEDSIGNATURES) Construct a [`MaxMinDrivingForceModel`](@ref) so that max min driving force -analysis can be performed on `model`. +analysis can be performed on `model`. """ -make_max_min_driving_force_model( - model::AbstractMetabolicModel; - kwargs... -) = MaxMinDrivingForceModel(; inner = model, kwargs...) +make_max_min_driving_force_model(model::AbstractMetabolicModel; kwargs...) = + MaxMinDrivingForceModel(; inner = model, kwargs...) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 392c5d47c..2f31faa7e 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -156,7 +156,7 @@ to make thermodynamic calculations easier. "Gibbs free energy of reaction", """ Thermodynamic models need to ensure that the ΔG of each reaction is negative -(2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. +(2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. """ ) diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/types/wrappers/MaxMinDrivingForceModel.jl index 2c39720cd..1d2228d68 100644 --- a/src/types/wrappers/MaxMinDrivingForceModel.jl +++ b/src/types/wrappers/MaxMinDrivingForceModel.jl @@ -34,43 +34,41 @@ $(TYPEDFIELDS) Base.@kwdef mutable struct MaxMinDrivingForceModel <: AbstractModelWrapper "A dictionary mapping ΔrG⁰ to reactions." reaction_standard_gibbs_free_energies::Dict{String,Float64} = Dict{String,Float64}() - + "A cycle-free reference flux solution that is used to set the directions of the reactions." flux_solution::Dict{String,Float64} = Dict{String,Float64}() - + "Metabolite ids of protons." proton_ids::Vector{String} = ["h_c", "h_e"] - + "Metabolite ids of water." water_ids::Vector{String} = ["h2o_c", "h2o_e"] - + "A dictionationay mapping metabolite ids to concentrations that are held constant." constant_concentrations::Dict{String,Float64} = Dict{String,Float64}() - + "A dictionary mapping metabolite ids to constant concentration ratios in the form `(m1, m2) = r === m1/m2 = r`." - concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ - Tuple{String,String}, - Float64, - }() - + concentration_ratios::Dict{Tuple{String,String},Float64} = + Dict{Tuple{String,String},Float64}() + "Global metabolite concentration lower bound." concentration_lb = 1e-9 - + "Global metabolite concentration upper bound." concentration_ub = 100e-3 - + "Thermodynamic temperature." T::Float64 = constants.T - + "Real gas constant." R::Float64 = constants.R - + "Tolerance use to distinguish flux carrying reactions from zero flux reactions." small_flux_tol::Float64 = 1e-6 "Maximum absolute ΔG bound allowed by a reaction." max_dg_bound::Float64 = 1000.0 - + "Reaction ids that are ignored internally during thermodynamic calculations. This should include water and proton importers." ignore_reaction_ids::Vector{String} = String[] @@ -87,14 +85,16 @@ The variables for max-min driving force analysis are the actual maximum minimum driving force of the model, the log metabolite concentrations, and the gibbs free energy reaction potentials across each reaction. """ -Accessors.variables(model::MaxMinDrivingForceModel) = ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] +Accessors.variables(model::MaxMinDrivingForceModel) = + ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] """ $(TYPEDSIGNATURES) Helper function that returns the unmangled variable IDs. """ -get_unmangled_variables(model::MaxMinDrivingForceModel) = ["mmdf"; metabolites(model); reactions(model)] +get_unmangled_variables(model::MaxMinDrivingForceModel) = + ["mmdf"; metabolites(model); reactions(model)] """ $(TYPEDSIGNATURES) @@ -102,7 +102,8 @@ $(TYPEDSIGNATURES) The number of variables is 1 + the number of metabolites + the number of reactions, where the 1 comes from the total max-min driving force. """ -Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + n_reactions(model) +Accessors.n_variables(model::MaxMinDrivingForceModel) = + 1 + n_metabolites(model) + n_reactions(model) """ $(TYPEDSIGNATURES) @@ -120,13 +121,15 @@ Gibbs free energy of reaction mapping to model variables. Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) + """ $(TYPEDSIGNATURES) For this kind of the model, the objective is the the max-min-driving force which is at index 1 in the variables. """ -Accessors.objective(model::MaxMinDrivingForceModel) = [1.0; fill(0.0, n_variables(model)-1)] +Accessors.objective(model::MaxMinDrivingForceModel) = + [1.0; fill(0.0, n_variables(model) - 1)] """ $(TYPEDSIGNATURES) @@ -145,7 +148,10 @@ function Accessors.balance(model::MaxMinDrivingForceModel) const_ratio_vec = log.(collect(values(model.concentration_ratios))) # give dummy dG0 for reactions that don't have data - dg0s = [get(model.reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] + dg0s = [ + get(model.reaction_standard_gibbs_free_energies, rid, 0.0) for + rid in reactions(model) + ] return [ proton_water_vec @@ -177,7 +183,8 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) ids = collect(keys(model.constant_concentrations)) idxs = indexin(ids, var_ids) for (i, j) in enumerate(idxs) - isnothing(j) && throw(DomainError(ids[j], "Constant metabolite ID not found in model.")) + isnothing(j) && + throw(DomainError(ids[j], "Constant metabolite ID not found in model.")) const_conc_mat[i, j] = 1.0 end @@ -185,7 +192,8 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) const_ratio_mat = spzeros(length(model.concentration_ratios), n_variables(model)) for (i, (mid1, mid2)) in enumerate(keys(model.concentration_ratios)) idxs = indexin([mid1, mid2], var_ids) - any(isnothing.(idxs)) && throw(DomainError((mid1, mid2), "Metabolite ratio pair not found in model.")) + any(isnothing.(idxs)) && + throw(DomainError((mid1, mid2), "Metabolite ratio pair not found in model.")) const_ratio_mat[i, first(idxs)] = 1.0 const_ratio_mat[i, last(idxs)] = -1.0 end @@ -218,7 +226,7 @@ function Accessors.bounds(model::MaxMinDrivingForceModel) # mmdf must be positive for problem to be feasible (it is defined as -ΔG) lbs[1] = 0.0 ubs[1] = 1000.0 - + # log concentrations lbs[2:(1+n_metabolites(model))] .= log(model.concentration_lb) ubs[2:(1+n_metabolites(model))] .= log(model.concentration_ub) @@ -243,7 +251,8 @@ ignored. _get_active_reaction_ids(model::MaxMinDrivingForceModel) = filter( rid -> haskey(model.reaction_standard_gibbs_free_energies, rid) && - abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > model.small_flux_tol && + abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > + model.small_flux_tol && !(rid in model.ignore_reaction_ids), reactions(model), ) @@ -251,7 +260,7 @@ _get_active_reaction_ids(model::MaxMinDrivingForceModel) = filter( """ $(TYPEDSIGNATURES) -Return the coupling of a max-min driving force model. +Return the coupling of a max-min driving force model. """ function Accessors.coupling(model::MaxMinDrivingForceModel) @@ -264,7 +273,7 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) for (i, j) in enumerate(idxs) flux_signs[i, j] = sign(model.flux_solution[reactions(model)[j]]) end - + neg_dg_mat = [ spzeros(length(idxs)) spzeros(length(idxs), n_metabolites(model)) flux_signs ] @@ -272,9 +281,9 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) mmdf_mat = sparse( [ -ones(length(idxs)) spzeros(length(idxs), n_metabolites(model)) -flux_signs - ] + ], ) - + return [ neg_dg_mat mmdf_mat @@ -294,120 +303,5 @@ function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) mmdf_lb = fill(0.0, n) mmdf_ub = fill(model.max_dg_bound, n) - return ( - [neg_dg_lb; mmdf_lb], - [neg_dg_ub; mmdf_ub] - ) + return ([neg_dg_lb; mmdf_lb], [neg_dg_ub; mmdf_ub]) end - -# function Accessors. - -# """ -# $(TYPEDSIGNATURES) - -# Perform a variant of flux variability analysis on a max min driving force type problem. -# Arguments are forwarded to [`max_min_driving_force`](@ref). Calls [`screen`](@ref) -# internally and possibly distributes computation across `workers`. If -# `optimal_objective_value = nothing`, the function first performs regular max min driving -# force analysis to find the max min driving force of the model and sets this to -# `optimal_objective_value`. Then iteratively maximizes and minimizes the driving force across -# each reaction, and then the concentrations while staying close to the original max min -# driving force as specified in `bounds`. - -# The `bounds` is a user-supplied function that specifies the max min driving force bounds for -# the variability optimizations, by default it restricts the flux objective value to the -# precise optimum reached in the normal max min driving force analysis. It can return `-Inf` -# and `Inf` in first and second pair to remove the limit. Use [`gamma_bounds`](@ref) and -# [`objective_bounds`](@ref) for simple bounds. - -# Returns a matrix of solutions to [`max_min_driving_force`](@ref) additionally constrained as -# described above, where the rows are in the order of the reactions and then the metabolites -# of the `model`. For the reaction rows the first column is the maximum dG of that reaction, -# and the second column is the minimum dG of that reaction subject to the above constraints. -# For the metabolite rows, the first column is the maximum concentration, and the second column -# is the minimum concentration subject to the constraints above. -# """ -# function max_min_driving_force_variability( -# model::AbstractMetabolicModel, -# reaction_standard_gibbs_free_energies::Dict{String,Float64}, -# optimizer; -# workers = [myid()], -# optimal_objective_value = nothing, -# bounds = z -> (z, Inf), -# modifications = [], -# kwargs..., -# ) -# if isnothing(optimal_objective_value) -# initsol = max_min_driving_force( -# model, -# reaction_standard_gibbs_free_energies, -# optimizer; -# modifications, -# kwargs..., -# ) -# mmdf = initsol.mmdf -# else -# mmdf = optimal_objective_value -# end - -# lb, ub = bounds(mmdf) - -# dgr_variants = [ -# [[_mmdf_add_df_bound(lb, ub), _mmdf_dgr_objective(ridx, sense)]] for -# ridx = 1:n_variables(model), sense in [MAX_SENSE, MIN_SENSE] -# ] -# concen_variants = [ -# [[_mmdf_add_df_bound(lb, ub), _mmdf_concen_objective(midx, sense)]] for -# midx = 1:n_metabolites(model), sense in [MAX_SENSE, MIN_SENSE] -# ] - -# return screen( -# model; -# args = [dgr_variants; concen_variants], -# analysis = (m, args) -> max_min_driving_force( -# m, -# reaction_standard_gibbs_free_energies, -# optimizer; -# modifications = [args; modifications], -# kwargs..., -# ), -# workers, -# ) -# end - -# """ -# $(TYPEDSIGNATURES) - -# Helper function to change the objective to optimizing some dG. -# """ -# function _mmdf_dgr_objective(ridx, sense) -# (model, opt_model) -> begin -# @objective(opt_model, sense, opt_model[:dgrs][ridx]) -# end -# end - -# """ -# $(TYPEDSIGNATURES) - -# Helper function to change the objective to optimizing some concentration. -# """ -# function _mmdf_concen_objective(midx, sense) -# (model, opt_model) -> begin -# @objective(opt_model, sense, opt_model[:logcs][midx]) -# end -# end - -# """ -# $(TYPEDSIGNATURES) - -# Helper function to add a new constraint on the driving force. -# """ -# function _mmdf_add_df_bound(lb, ub) -# (model, opt_model) -> begin -# if lb == ub -# fix(opt_model[:mmdf], lb; force = true) -# else -# @constraint(opt_model, lb <= opt_model[:mmdf] <= ub) -# end -# end -# end diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 232ce9c8f..4d638a701 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -30,39 +30,25 @@ modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - # get mmdf - @test isapprox(solved_objective_value(opt_model), 1.7661155558545698, atol = TEST_TOLERANCE) + # get mmdf + @test isapprox( + solved_objective_value(opt_model), + 1.7661155558545698, + atol = TEST_TOLERANCE, + ) # values_dict(:reaction, mmdfm, opt_model) # TODO throw missing semantics error @test length(values_dict(:metabolite_log_concentration, mmdfm, opt_model)) == 72 @test length(values_dict(:gibbs_free_energy_reaction, mmdfm, opt_model)) == 95 - - # sols = max_min_driving_force_variability( - # model, - # reaction_standard_gibbs_free_energies, - # Tulip.Optimizer; - # bounds = gamma_bounds(0.9), - # flux_solution = flux_solution, - # proton_ids = ["h_c", "h_e"], - # water_ids = ["h2o_c", "h2o_e"], - # concentration_ratios = Dict{Tuple{String,String},Float64}( - # ("atp_c", "adp_c") => 10.0, - # ("nadh_c", "nad_c") => 0.13, - # ("nadph_c", "nadp_c") => 1.3, - # ), - # constant_concentrations = Dict{String,Float64}( - # # "pi_c" => 10e-3 - # ), - # concentration_lb = 1e-6, - # concentration_ub = 100e-3, - # ignore_reaction_ids = ["H2Ot"], - # modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - # ) - # pyk_idx = first(indexin(["PYK"], variables(model))) - # @test isapprox( - # sols[pyk_idx, 1].dg_reactions["PYK"], - # -1.5895040002691128; - # atol = TEST_TOLERANCE, - # ) + sols = variability_analysis( + Val(:gibbs_free_energy_reaction), + mmdfm, + Tulip.Optimizer; + bounds = gamma_bounds(0.9), + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + + pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reactions(mmdfm))) + @test isapprox(sols[pyk_idx, 2], -1.5895040002691128; atol = TEST_TOLERANCE) end From 135950780f5a24942f525cc19be12a7c144d94c4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 16:27:48 +0100 Subject: [PATCH 155/531] sed: lower -> lower_bound and sed: upper -> upper_bound --- docs/src/examples/07_restricting_reactions.jl | 18 +++++--- docs/src/examples/11_growth.jl | 14 ++++--- docs/src/examples/13_moma.jl | 2 +- docs/src/quickstart.md | 10 ++--- docs/src/quickstart.md.template | 10 ++--- src/analysis/modifications/generic.jl | 7 +++- src/analysis/modifications/knockout.jl | 2 +- src/macros/change_bounds.jl | 10 ++--- src/reconstruction/MatrixCoupling.jl | 32 ++++++++++---- src/reconstruction/MatrixModel.jl | 34 +++++++++------ src/reconstruction/ObjectModel.jl | 12 +++--- src/solver.jl | 8 ++-- test/analysis/sampling/warmup_variability.jl | 2 +- test/reconstruction/MatrixCoupling.jl | 28 +++++++++---- test/reconstruction/MatrixModel.jl | 42 ++++++++++++++----- test/reconstruction/ObjectModel.jl | 32 ++++++++++---- test/reconstruction/enzyme_constrained.jl | 4 +- .../simplified_enzyme_constrained.jl | 4 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- test/utils/ObjectModel.jl | 4 +- 20 files changed, 181 insertions(+), 96 deletions(-) diff --git a/docs/src/examples/07_restricting_reactions.jl b/docs/src/examples/07_restricting_reactions.jl index 222db3d8b..ee31cad6b 100644 --- a/docs/src/examples/07_restricting_reactions.jl +++ b/docs/src/examples/07_restricting_reactions.jl @@ -32,7 +32,7 @@ model = load_model(ObjectModel, "e_coli_core.json") # faster) in certain situations: flux1 = flux_balance_analysis_vec( - model |> with_changed_bound("FBA", lower = 0.0, upper = 0.0), + model |> with_changed_bound("FBA", lower_bound = 0.0, upper_bound = 0.0), GLPK.Optimizer, ); @@ -77,8 +77,8 @@ running_reactions = [(rid, x) for (rid, x) in original_flux if abs(x) > 1e-3] screen( model, variants = [ - [with_changed_bound(rid, lower = -0.5 * abs(x), upper = 0.5 * abs(x))] for - (rid, x) in running_reactions + [with_changed_bound(rid, lower_bound = -0.5 * abs(x), upper_bound = 0.5 * abs(x))] + for (rid, x) in running_reactions ], args = running_reactions, analysis = (m, rid, _) -> @@ -106,8 +106,16 @@ biomass_mtx = screen( model, variants = [ [ - with_changed_bound(rid1, lower = -0.5 * abs(x1), upper = 0.5 * abs(x1)), - with_changed_bound(rid2, lower = -0.5 * abs(x2), upper = 0.5 * abs(x2)), + with_changed_bound( + rid1, + lower_bound = -0.5 * abs(x1), + upper_bound = 0.5 * abs(x1), + ), + with_changed_bound( + rid2, + lower_bound = -0.5 * abs(x2), + upper_bound = 0.5 * abs(x2), + ), ] for (rid1, rid2, x1, x2) in running_reaction_combinations ], analysis = m -> diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl index de658aade..a61f0dba8 100644 --- a/docs/src/examples/11_growth.jl +++ b/docs/src/examples/11_growth.jl @@ -28,7 +28,7 @@ model = load_model(ObjectModel, "e_coli_core.xml") biomass = "R_BIOMASS_Ecoli_core_w_GAM" -model_limited = change_bound(model, "R_EX_glc__D_e", lower = -1.0) +model_limited = change_bound(model, "R_EX_glc__D_e", lower_bound = -1.0) #md # !!! tip "Exchange directions" #md # By a convention, the direction of exchange reaction usually goes from the @@ -51,7 +51,7 @@ flux_summary(flux_balance_analysis_dict(model_limited, GLPK.Optimizer)) flux_summary( flux_balance_analysis_dict( - change_bound(model, "R_EX_o2_e", lower = 0.0), + change_bound(model, "R_EX_o2_e", lower_bound = 0.0), GLPK.Optimizer, ), ) @@ -62,7 +62,9 @@ exchanges = filter(looks_like_exchange_reaction, variables(model)) exchanges .=> screen( model, - variants = [[with_changed_bound(exchange, lower = 0.0)] for exchange in exchanges], + variants = [ + [with_changed_bound(exchange, lower_bound = 0.0)] for exchange in exchanges + ], analysis = m -> begin res = flux_balance_analysis_dict(m, GLPK.Optimizer) isnothing(res) ? nothing : res[biomass] @@ -89,8 +91,8 @@ selected_exchanges = [ screen( model, variants = [ - [with_changed_bounds([e1, e2], lower = [-1.0, -0.1])] for e1 in selected_exchanges, - e2 in selected_exchanges + [with_changed_bounds([e1, e2], lower_bound = [-1.0, -0.1])] for + e1 in selected_exchanges, e2 in selected_exchanges ], analysis = m -> begin res = flux_balance_analysis_dict(m, GLPK.Optimizer) @@ -115,7 +117,7 @@ screen( # given the directionality convention of the exchanges, actually maximizes the # flux through all exchange reactions along their direction). -model_with_bounded_production = change_bound(model, biomass, lower = 0.1) #minimum required growth +model_with_bounded_production = change_bound(model, biomass, lower_bound = 0.1) #minimum required growth minimal_intake_production = flux_balance_analysis_dict( model_with_bounded_production, diff --git a/docs/src/examples/13_moma.jl b/docs/src/examples/13_moma.jl index b69a28a16..518933034 100644 --- a/docs/src/examples/13_moma.jl +++ b/docs/src/examples/13_moma.jl @@ -29,7 +29,7 @@ reference_flux = flux_balance_analysis_dict(model, Clarabel.Optimizer; modifications = [silence]) # As the change here, we manually knock out CYTBD reaction: -changed_model = change_bound(model, "R_CYTBD", lower = 0.0, upper = 0.0); +changed_model = change_bound(model, "R_CYTBD", lower_bound = 0.0, upper_bound = 0.0); # Now, let's find a flux that minimizes the organism's metabolic adjustment for # this model: diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md index 8dfb99c4a..bffc50d9a 100644 --- a/docs/src/quickstart.md +++ b/docs/src/quickstart.md @@ -73,10 +73,10 @@ m = convert(ObjectModel, model) screen(m, # the base model variants=[ # this specifies how to generate the desired model variants [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower=0.0, upper=0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower=0.0, upper=0.0), - with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable both + [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0)], # disable oxygen + [with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable CO2 + [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0), + with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable both ], # this specifies what to do with the model variants (received as the argument `x`) analysis = x -> @@ -113,7 +113,7 @@ worker_list = workers() res = screen(m, variants=[ # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower=0.0, upper=0.0)] + [with_changed_bound(reaction_id, lower_bound =0.0, upper_bound =0.0)] for reaction_id in reactions(m) ], analysis = model -> begin diff --git a/docs/src/quickstart.md.template b/docs/src/quickstart.md.template index 16670aacc..f2fadddd8 100644 --- a/docs/src/quickstart.md.template +++ b/docs/src/quickstart.md.template @@ -17,10 +17,10 @@ m = convert(ObjectModel, model) screen(m, # the base model variants=[ # this specifies how to generate the desired model variants [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower=0.0, upper=0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower=0.0, upper=0.0), - with_changed_bound("R_CO2t", lower=0.0, upper=0.0)], # disable both + [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0)], # disable oxygen + [with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable CO2 + [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0), + with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable both ], # this specifies what to do with the model variants (received as the argument `x`) analysis = x -> @@ -57,7 +57,7 @@ worker_list = workers() res = screen(m, variants=[ # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower=0.0, upper=0.0)] + [with_changed_bound(reaction_id, lower_bound =0.0, upper_bound =0.0)] for reaction_id in reactions(m) ], analysis = model -> begin diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 7fedee64d..249493a52 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -21,7 +21,12 @@ change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin ind = first(indexin([id], variables(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!(ind, opt_model, lower = lower_bound, upper = upper_bound) + set_optmodel_bound!( + ind, + opt_model, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) end """ diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index fc130ca2d..e99252737 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -38,7 +38,7 @@ function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector rga = reaction_gene_associations(model, rxn_id) if !isnothing(rga) && all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) - set_optmodel_bound!(rxn_num, opt_model, lower = 0, upper = 0) + set_optmodel_bound!(rxn_num, opt_model, lower_bound = 0, upper_bound = 0) end end end diff --git a/src/macros/change_bounds.jl b/src/macros/change_bounds.jl index 5073f2f48..8fd45c52a 100644 --- a/src/macros/change_bounds.jl +++ b/src/macros/change_bounds.jl @@ -34,8 +34,8 @@ macro _change_bounds_fn(model_type, idx_type, args...) $fname( model::$model_type, $idx_var::$idx_type; - lower = $missing_default, - upper = $missing_default, + lower_bound =$missing_default, + upper_bound =$missing_default, ) Change the specified reaction flux bound$(plural_s) in the model @@ -43,7 +43,7 @@ macro _change_bounds_fn(model_type, idx_type, args...) # Example ``` - $(inplace ? "new_model = " : "")$fname(model, $example_idx, lower=$(-0.5 .* example_val), upper=$example_val) + $(inplace ? "new_model = " : "")$fname(model, $example_idx, lower_bound =$(-0.5 .* example_val), upper_bound =$example_val) ``` """ @@ -57,8 +57,8 @@ macro _change_bounds_fn(model_type, idx_type, args...) $fname( model::$model_type, $idx_var::$idx_type; - lower = $missing_default, - upper = $missing_default, + lower_bound = $missing_default, + upper_bound = $missing_default, ) = $body ), ), diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index e4dba8843..6b388fc70 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -260,42 +260,58 @@ end # TODO see if some of these can be derived from AbstractModelWrapper @_change_bounds_fn MatrixCoupling Int inplace begin - change_bound!(model.lm, rxn_idx, lower = lower, upper = upper) + change_bound!(model.lm, rxn_idx, lower_bound = lower_bound, upper_bound = upper_bound) end @_change_bounds_fn MatrixCoupling Int inplace plural begin - change_bounds!(model.lm, rxn_idxs, lower = lower, upper = upper) + change_bounds!(model.lm, rxn_idxs, lower_bound = lower_bound, upper_bound = upper_bound) end @_change_bounds_fn MatrixCoupling String inplace begin - change_bound!(model.lm, rxn_id, lower = lower, upper = upper) + change_bound!(model.lm, rxn_id, lower_bound = lower_bound, upper_bound = upper_bound) end @_change_bounds_fn MatrixCoupling String inplace plural begin - change_bounds!(model.lm, rxn_ids, lower = lower, upper = upper) + change_bounds!(model.lm, rxn_ids, lower_bound = lower_bound, upper_bound = upper_bound) end @_change_bounds_fn MatrixCoupling Int begin n = copy(model) - n.lm = change_bound(model.lm, rxn_idx, lower = lower, upper = upper) + n.lm = change_bound( + model.lm, + rxn_idx, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) n end @_change_bounds_fn MatrixCoupling Int plural begin n = copy(model) - n.lm = change_bounds(model.lm, rxn_idxs, lower = lower, upper = upper) + n.lm = change_bounds( + model.lm, + rxn_idxs, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) n end @_change_bounds_fn MatrixCoupling String begin n = copy(model) - n.lm = change_bound(model.lm, rxn_id, lower = lower, upper = upper) + n.lm = + change_bound(model.lm, rxn_id, lower_bound = lower_bound, upper_bound = upper_bound) n end @_change_bounds_fn MatrixCoupling String plural begin n = copy(model) - n.lm = change_bounds(model.lm, rxn_ids, lower = lower, upper = upper) + n.lm = change_bounds( + model.lm, + rxn_ids, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) n end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 188b34932..94b2aa241 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -254,52 +254,62 @@ function verify_consistency( end @_change_bounds_fn MatrixModel Int inplace begin - isnothing(lower) || (model.xl[rxn_idx] = lower) - isnothing(upper) || (model.xu[rxn_idx] = upper) + isnothing(lower_bound) || (model.xl[rxn_idx] = lower_bound) + isnothing(upper_bound) || (model.xu[rxn_idx] = upper_bound) nothing end @_change_bounds_fn MatrixModel Int inplace plural begin - for (i, l, u) in zip(rxn_idxs, lower, upper) - change_bound!(model, i, lower = l, upper = u) + for (i, l, u) in zip(rxn_idxs, lower_bound, upper_bound) + change_bound!(model, i, lower_bound = l, upper_bound = u) end end @_change_bounds_fn MatrixModel Int begin - change_bounds(model, [rxn_idx], lower = [lower], upper = [upper]) + change_bounds( + model, + [rxn_idx], + lower_bound = [lower_bound], + upper_bound = [upper_bound], + ) end @_change_bounds_fn MatrixModel Int plural begin n = copy(model) n.xl = copy(n.xl) n.xu = copy(n.xu) - change_bounds!(n, rxn_idxs, lower = lower, upper = upper) + change_bounds!(n, rxn_idxs, lower_bound = lower_bound, upper_bound = upper_bound) n end @_change_bounds_fn MatrixModel String inplace begin - change_bounds!(model, [rxn_id], lower = [lower], upper = [upper]) + change_bounds!( + model, + [rxn_id], + lower_bound = [lower_bound], + upper_bound = [upper_bound], + ) end @_change_bounds_fn MatrixModel String inplace plural begin change_bounds!( model, Vector{Int}(indexin(rxn_ids, variables(model))), - lower = lower, - upper = upper, + lower_bound = lower_bound, + upper_bound = upper_bound, ) end @_change_bounds_fn MatrixModel String begin - change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) + change_bounds(model, [rxn_id], lower_bound = [lower_bound], upper_bound = [upper_bound]) end @_change_bounds_fn MatrixModel String plural begin change_bounds( model, Int.(indexin(rxn_ids, variables(model))), - lower = lower, - upper = upper, + lower_bound = lower_bound, + upper_bound = upper_bound, ) end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 1b92c97a9..bfca51665 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -172,19 +172,19 @@ remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) @_change_bounds_fn ObjectModel String inplace begin - isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) - isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) + isnothing(lower_bound) || (model.reactions[rxn_id].lower_bound = lower_bound) + isnothing(upper_bound) || (model.reactions[rxn_id].upper_bound = upper_bound) nothing end @_change_bounds_fn ObjectModel String inplace plural begin - for (i, l, u) in zip(rxn_ids, lower, upper) - change_bound!(model, i, lower = l, upper = u) + for (i, l, u) in zip(rxn_ids, lower_bound, upper_bound) + change_bound!(model, i, lower_bound = l, upper_bound = u) end end @_change_bounds_fn ObjectModel String begin - change_bounds(model, [rxn_id], lower = [lower], upper = [upper]) + change_bounds(model, [rxn_id], lower_bound = [lower_bound], upper_bound = [upper_bound]) end @_change_bounds_fn ObjectModel String plural begin @@ -193,7 +193,7 @@ end for i in rxn_ids n.reactions[i] = copy(n.reactions[i]) end - change_bounds!(n, rxn_ids, lower = lower, upper = upper) + change_bounds!(n, rxn_ids, lower_bound = lower_bound, upper_bound = upper_bound) return n end diff --git a/src/solver.jl b/src/solver.jl index 3685d8a43..1e5acc973 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -100,11 +100,11 @@ not be changed. function set_optmodel_bound!( vidx, opt_model; - lower::Maybe{Real} = nothing, - upper::Maybe{Real} = nothing, + lower_bound::Maybe{Real} = nothing, + upper_bound::Maybe{Real} = nothing, ) - isnothing(lower) || set_normalized_rhs(opt_model[:lbs][vidx], -lower) - isnothing(upper) || set_normalized_rhs(opt_model[:ubs][vidx], upper) + isnothing(lower_bound) || set_normalized_rhs(opt_model[:lbs][vidx], -lower_bound) + isnothing(upper_bound) || set_normalized_rhs(opt_model[:ubs][vidx], upper_bound) end """ diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl index 6393b9abf..a329267ff 100644 --- a/test/analysis/sampling/warmup_variability.jl +++ b/test/analysis/sampling/warmup_variability.jl @@ -3,7 +3,7 @@ rid = "EX_glc__D_e" pts = warmup_from_variability( - model |> with_changed_bound(rid; lower = -2, upper = 2), + model |> with_changed_bound(rid; lower_bound = -2, upper_bound = 2), Tulip.Optimizer, 100; workers = W, diff --git a/test/reconstruction/MatrixCoupling.jl b/test/reconstruction/MatrixCoupling.jl index 97019ea48..554f604f5 100644 --- a/test/reconstruction/MatrixCoupling.jl +++ b/test/reconstruction/MatrixCoupling.jl @@ -151,32 +151,42 @@ end cp = convert(MatrixModelWithCoupling, test_LP()) @test cp isa MatrixModelWithCoupling - change_bound!(cp, 1, lower = -10, upper = 10) + change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) @test cp.lm.xl[1] == -10 @test cp.lm.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) + change_bounds!(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) @test cp.lm.xl[2] == -12.2 @test cp.lm.xu[1] == 11 - change_bound!(cp, "r1", lower = -101, upper = 101) + change_bound!(cp, "r1", lower_bound = -101, upper_bound = 101) @test cp.lm.xl[1] == -101 @test cp.lm.xu[1] == 101 - change_bounds!(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 233.0]) + change_bounds!( + cp, + ["r1", "r2"]; + lower_bound = [-113, -12.23], + upper_bound = [113, 233.0], + ) @test cp.lm.xl[2] == -12.23 @test cp.lm.xu[1] == 113 - new_model = change_bound(cp, 1, lower = -10, upper = 10) + new_model = change_bound(cp, 1, lower_bound = -10, upper_bound = 10) @test new_model.lm.xl[1] == -10 @test new_model.lm.xu[1] == 10 - new_model = change_bounds(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) + new_model = + change_bounds(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) @test new_model.lm.xl[2] == -12.2 @test new_model.lm.xu[1] == 11 - new_model = change_bound(cp, "r1", lower = -101, upper = 101) + new_model = change_bound(cp, "r1", lower_bound = -101, upper_bound = 101) @test new_model.lm.xl[1] == -101 @test new_model.lm.xu[1] == 101 - new_model = - change_bounds(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 233.0]) + new_model = change_bounds( + cp, + ["r1", "r2"]; + lower_bound = [-113, -12.23], + upper_bound = [113, 233.0], + ) @test new_model.lm.xl[2] == -12.23 @test new_model.lm.xu[1] == 113 diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index 51ec05fa6..dfe3725de 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -1,40 +1,60 @@ @testset "Change bounds" begin cp = test_LP() - change_bound!(cp, 1, lower = -10, upper = 10) + change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) @test cp.xl[1] == -10 @test cp.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) + change_bounds!(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) @test cp.xl[2] == -12.2 @test cp.xu[1] == 11 - change_bound!(cp, "r1", lower = -101, upper = 101) + change_bound!(cp, "r1", lower_bound = -101, upper_bound = 101) @test cp.xl[1] == -101 @test cp.xu[1] == 101 - change_bounds!(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [114, 233.0]) + change_bounds!( + cp, + ["r1", "r2"]; + lower_bound = [-113, -12.23], + upper_bound = [114, 233.0], + ) @test cp.xl[2] == -12.23 @test cp.xu[1] == 114 - change_bounds!(cp, ["r1", "r2"]; lower = [-114, nothing], upper = [nothing, 2333.0]) + change_bounds!( + cp, + ["r1", "r2"]; + lower_bound = [-114, nothing], + upper_bound = [nothing, 2333.0], + ) @test cp.xl[1] == -114 @test cp.xl[2] == -12.23 @test cp.xu[1] == 114 @test cp.xu[2] == 2333 - new_model = change_bound(cp, 1, lower = -10, upper = 10) + new_model = change_bound(cp, 1, lower_bound = -10, upper_bound = 10) @test new_model.xl[1] == -10 @test new_model.xu[1] == 10 - new_model = change_bounds(cp, [1, 2]; lower = [-11, -12.2], upper = [11, 23.0]) + new_model = + change_bounds(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) @test new_model.xl[2] == -12.2 @test new_model.xu[1] == 11 - new_model = change_bound(cp, "r1", lower = -101, upper = 101) + new_model = change_bound(cp, "r1", lower_bound = -101, upper_bound = 101) @test new_model.xl[1] == -101 @test new_model.xu[1] == 101 - new_model = change_bounds(cp, ["r1", "r2"]; lower = [-113, -12.23], upper = [113, 1000]) + new_model = change_bounds( + cp, + ["r1", "r2"]; + lower_bound = [-113, -12.23], + upper_bound = [113, 1000], + ) @test new_model.xl[2] == -12.23 @test new_model.xu[1] == 113 - new_model = - change_bounds(cp, ["r1", "r2"]; lower = [nothing, -10], upper = [110, nothing]) + new_model = change_bounds( + cp, + ["r1", "r2"]; + lower_bound = [nothing, -10], + upper_bound = [110, nothing], + ) @test new_model.xl[1] == -114 @test new_model.xl[2] == -10 @test new_model.xu[1] == 110 diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 87aa4bc2a..dde7ab2f5 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -32,39 +32,53 @@ model.genes = OrderedDict(g.id => g for g in gene_vec) # change bound tests - in place - change_bound!(model, "r2"; lower = -10, upper = 10) + change_bound!(model, "r2"; lower_bound = -10, upper_bound = 10) @test model.reactions["r2"].lower_bound == -10 @test model.reactions["r2"].upper_bound == 10 - change_bound!(model, "r2"; lower = -100) + change_bound!(model, "r2"; lower_bound = -100) @test model.reactions["r2"].lower_bound == -100 @test model.reactions["r2"].upper_bound == 10 - change_bound!(model, "r2"; upper = 111) + change_bound!(model, "r2"; upper_bound = 111) @test model.reactions["r2"].lower_bound == -100 @test model.reactions["r2"].upper_bound == 111 - change_bounds!(model, ["r1", "r2"]; lower = [-110, -220], upper = [110.0, 220.0]) + change_bounds!( + model, + ["r1", "r2"]; + lower_bound = [-110, -220], + upper_bound = [110.0, 220.0], + ) @test model.reactions["r1"].lower_bound == -110 @test model.reactions["r1"].upper_bound == 110 @test model.reactions["r2"].lower_bound == -220 @test model.reactions["r2"].upper_bound == 220 # change bound - new model - new_model = change_bound(model, "r2"; lower = -10, upper = 10) + new_model = change_bound(model, "r2"; lower_bound = -10, upper_bound = 10) @test new_model.reactions["r2"].lower_bound == -10 @test new_model.reactions["r2"].upper_bound == 10 - new_model = change_bound(model, "r2"; lower = -10) + new_model = change_bound(model, "r2"; lower_bound = -10) @test new_model.reactions["r2"].lower_bound == -10 @test new_model.reactions["r2"].upper_bound == 220 - new_model = change_bounds(model, ["r1", "r2"]; lower = [-10, -20], upper = [10.0, 20.0]) + new_model = change_bounds( + model, + ["r1", "r2"]; + lower_bound = [-10, -20], + upper_bound = [10.0, 20.0], + ) @test new_model.reactions["r1"].lower_bound == -10 @test new_model.reactions["r1"].upper_bound == 10 @test new_model.reactions["r2"].lower_bound == -20 @test new_model.reactions["r2"].upper_bound == 20 - new_model = - change_bounds(model, ["r1", "r2"]; lower = [-10, nothing], upper = [nothing, 20.0]) + new_model = change_bounds( + model, + ["r1", "r2"]; + lower_bound = [-10, nothing], + upper_bound = [nothing, 20.0], + ) @test new_model.reactions["r1"].lower_bound == -10 @test new_model.reactions["r1"].upper_bound == 110 @test new_model.reactions["r2"].lower_bound == -220 diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 53490a3e8..177080a56 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -42,8 +42,8 @@ model |> with_changed_bounds( ["EX_glc__D_e", "GLCpts"]; - lower = [-1000.0, -1.0], - upper = [nothing, 12.0], + lower_bound = [-1000.0, -1.0], + upper_bound = [nothing, 12.0], ) |> with_enzyme_constraints( gene_product_mass_group_bound = Dict( diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index c4c064dc1..0d13745f3 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -33,8 +33,8 @@ model |> with_changed_bounds( ["EX_glc__D_e", "GLCpts"], - lower = [-1000.0, -1.0], - upper = [nothing, 12.0], + lower_bound = [-1000.0, -1.0], + upper_bound = [nothing, 12.0], ) |> with_simplified_enzyme_constraints(total_enzyme_capacity = 100.0) diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 0e9daf46b..abf105d69 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -305,7 +305,7 @@ end gm = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> - with_changed_bounds(["EX_glc__D_e"]; lower = [-1000.0], upper = [0]) |> + with_changed_bounds(["EX_glc__D_e"]; lower_bound = [-1000.0], upper_bound = [0]) |> with_enzyme_constraints( gene_product_mass_group_bound = Dict("uncategorized" => 100.0), ) diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl index 045dbaae4..f620a00f5 100644 --- a/test/utils/ObjectModel.jl +++ b/test/utils/ObjectModel.jl @@ -15,10 +15,10 @@ glucose_index = first(indexin(["EX_glc__D_e"], variables(model))) o2_index = first(indexin(["EX_o2_e"], variables(model))) atpm_index = first(indexin(["ATPM"], variables(model))) - set_optmodel_bound!(glucose_index, cbm; upper = -1.0, lower = -1.0) + set_optmodel_bound!(glucose_index, cbm; upper_bound = -1.0, lower_bound = -1.0) @test normalized_rhs(ubs[glucose_index]) == -1.0 @test normalized_rhs(lbs[glucose_index]) == 1.0 - set_optmodel_bound!(o2_index, cbm; upper = 1.0, lower = 1.0) + set_optmodel_bound!(o2_index, cbm; upper_bound = 1.0, lower_bound = 1.0) @test normalized_rhs(ubs[o2_index]) == 1.0 @test normalized_rhs(lbs[o2_index]) == -1.0 end From d18fdbc1490dd5980472d7ce9a664ecc9e50aaac Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 17:50:05 +0100 Subject: [PATCH 156/531] use kwarg shortcut (from review) --- src/reconstruction/MatrixCoupling.jl | 32 +++++++--------------------- src/reconstruction/MatrixModel.jl | 15 +++++-------- src/reconstruction/ObjectModel.jl | 2 +- 3 files changed, 14 insertions(+), 35 deletions(-) diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 6b388fc70..7225e8e96 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -260,58 +260,42 @@ end # TODO see if some of these can be derived from AbstractModelWrapper @_change_bounds_fn MatrixCoupling Int inplace begin - change_bound!(model.lm, rxn_idx, lower_bound = lower_bound, upper_bound = upper_bound) + change_bound!(model.lm, rxn_idx; lower_bound, upper_bound) end @_change_bounds_fn MatrixCoupling Int inplace plural begin - change_bounds!(model.lm, rxn_idxs, lower_bound = lower_bound, upper_bound = upper_bound) + change_bounds!(model.lm, rxn_idxs; lower_bound, upper_bound) end @_change_bounds_fn MatrixCoupling String inplace begin - change_bound!(model.lm, rxn_id, lower_bound = lower_bound, upper_bound = upper_bound) + change_bound!(model.lm, rxn_id; lower_bound, upper_bound) end @_change_bounds_fn MatrixCoupling String inplace plural begin - change_bounds!(model.lm, rxn_ids, lower_bound = lower_bound, upper_bound = upper_bound) + change_bounds!(model.lm, rxn_ids; lower_bound, upper_bound) end @_change_bounds_fn MatrixCoupling Int begin n = copy(model) - n.lm = change_bound( - model.lm, - rxn_idx, - lower_bound = lower_bound, - upper_bound = upper_bound, - ) + n.lm = change_bound(model.lm, rxn_idx; lower_bound, upper_bound) n end @_change_bounds_fn MatrixCoupling Int plural begin n = copy(model) - n.lm = change_bounds( - model.lm, - rxn_idxs, - lower_bound = lower_bound, - upper_bound = upper_bound, - ) + n.lm = change_bounds(model.lm, rxn_idxs; lower_bound, upper_bound) n end @_change_bounds_fn MatrixCoupling String begin n = copy(model) - n.lm = - change_bound(model.lm, rxn_id, lower_bound = lower_bound, upper_bound = upper_bound) + n.lm = change_bound(model.lm, rxn_id; lower_bound, upper_bound) n end @_change_bounds_fn MatrixCoupling String plural begin n = copy(model) - n.lm = change_bounds( - model.lm, - rxn_ids, - lower_bound = lower_bound, - upper_bound = upper_bound, - ) + n.lm = change_bounds(model.lm, rxn_ids; lower_bound, upper_bound) n end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 94b2aa241..20ae1931d 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -278,7 +278,7 @@ end n = copy(model) n.xl = copy(n.xl) n.xu = copy(n.xu) - change_bounds!(n, rxn_idxs, lower_bound = lower_bound, upper_bound = upper_bound) + change_bounds!(n, rxn_idxs; lower_bound, upper_bound) n end @@ -294,9 +294,9 @@ end @_change_bounds_fn MatrixModel String inplace plural begin change_bounds!( model, - Vector{Int}(indexin(rxn_ids, variables(model))), - lower_bound = lower_bound, - upper_bound = upper_bound, + Vector{Int}(indexin(rxn_ids, variables(model))); + lower_bound, + upper_bound, ) end @@ -305,12 +305,7 @@ end end @_change_bounds_fn MatrixModel String plural begin - change_bounds( - model, - Int.(indexin(rxn_ids, variables(model))), - lower_bound = lower_bound, - upper_bound = upper_bound, - ) + change_bounds(model, Int.(indexin(rxn_ids, variables(model))); lower_bound, upper_bound) end @_remove_fn reaction MatrixModel Int inplace begin diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index bfca51665..b4098ae95 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -193,7 +193,7 @@ end for i in rxn_ids n.reactions[i] = copy(n.reactions[i]) end - change_bounds!(n, rxn_ids, lower_bound = lower_bound, upper_bound = upper_bound) + change_bounds!(n, rxn_ids; lower_bound, upper_bound) return n end From 0e8c7802127416b0ca7a044bde9ea1f417cbb06f Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 18:43:40 +0100 Subject: [PATCH 157/531] implement reviews --- src/types/accessors/AbstractMetabolicModel.jl | 4 +-- src/types/misc/thermodynamic.jl | 23 +++++++++++++ src/types/wrappers/MaxMinDrivingForceModel.jl | 34 +++---------------- 3 files changed, 30 insertions(+), 31 deletions(-) create mode 100644 src/types/misc/thermodynamic.jl diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 2f31faa7e..0dc66ec0d 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -155,8 +155,8 @@ to make thermodynamic calculations easier. :gibbs_free_energy_reaction, "Gibbs free energy of reaction", """ -Thermodynamic models need to ensure that the ΔG of each reaction is negative -(2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. +Some thermodynamic models need to ensure that the ΔG of each reaction is +negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. """ ) diff --git a/src/types/misc/thermodynamic.jl b/src/types/misc/thermodynamic.jl new file mode 100644 index 000000000..86c3b7c50 --- /dev/null +++ b/src/types/misc/thermodynamic.jl @@ -0,0 +1,23 @@ +""" +$(TYPEDSIGNATURES) + +A helper function that returns the reaction ids that are active. Active reaction +have thermodynamic data AND a flux bigger than `small_flux_tol` AND are not +ignored. +""" +active_reaction_ids(model::MaxMinDrivingForceModel) = filter( + rid -> + haskey(model.reaction_standard_gibbs_free_energies, rid) && + abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > + model.small_flux_tol && + !(rid in model.ignore_reaction_ids), + reactions(model), +) + +""" +$(TYPEDSIGNATURES) + +Helper function that returns the unmangled variable IDs. +""" +original_variables(model::MaxMinDrivingForceModel) = + ["mmdf"; metabolites(model); reactions(model)] diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/types/wrappers/MaxMinDrivingForceModel.jl index 1d2228d68..43536cdf2 100644 --- a/src/types/wrappers/MaxMinDrivingForceModel.jl +++ b/src/types/wrappers/MaxMinDrivingForceModel.jl @@ -35,7 +35,7 @@ Base.@kwdef mutable struct MaxMinDrivingForceModel <: AbstractModelWrapper "A dictionary mapping ΔrG⁰ to reactions." reaction_standard_gibbs_free_energies::Dict{String,Float64} = Dict{String,Float64}() - "A cycle-free reference flux solution that is used to set the directions of the reactions." + "A cycle-free reference reaction flux solution that is used to set the directions of the reactions. For example, this could be generated this using loopless FBA." flux_solution::Dict{String,Float64} = Dict{String,Float64}() "Metabolite ids of protons." @@ -91,14 +91,6 @@ Accessors.variables(model::MaxMinDrivingForceModel) = """ $(TYPEDSIGNATURES) -Helper function that returns the unmangled variable IDs. -""" -get_unmangled_variables(model::MaxMinDrivingForceModel) = - ["mmdf"; metabolites(model); reactions(model)] - -""" -$(TYPEDSIGNATURES) - The number of variables is 1 + the number of metabolites + the number of reactions, where the 1 comes from the total max-min driving force. """ @@ -167,7 +159,7 @@ $(TYPEDSIGNATURES) Get the equality constraint lhs of `model`. """ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) - var_ids = get_unmangled_variables(model) + var_ids = Internal.original_variables(model) # set proton and water equality constraints num_proton_water = length(model.proton_ids) + length(model.water_ids) @@ -218,7 +210,7 @@ $(TYPEDSIGNATURES) Get the simple variable bounds of `model`. """ function Accessors.bounds(model::MaxMinDrivingForceModel) - var_ids = get_unmangled_variables(model) + var_ids = Internal.original_variables(model) lbs = fill(-model.max_dg_bound, n_variables(model)) ubs = fill(model.max_dg_bound, n_variables(model)) @@ -244,28 +236,12 @@ end """ $(TYPEDSIGNATURES) -A helper function that returns the reaction ids that are active. Active reaction -have thermodynamic data AND a flux bigger than `small_flux_tol` AND are not -ignored. -""" -_get_active_reaction_ids(model::MaxMinDrivingForceModel) = filter( - rid -> - haskey(model.reaction_standard_gibbs_free_energies, rid) && - abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > - model.small_flux_tol && - !(rid in model.ignore_reaction_ids), - reactions(model), -) - -""" -$(TYPEDSIGNATURES) - Return the coupling of a max-min driving force model. """ function Accessors.coupling(model::MaxMinDrivingForceModel) # only constrain reactions that have thermo data - active_rids = _get_active_reaction_ids(model) + active_rids = Internal.active_reaction_ids(model) idxs = Int.(indexin(active_rids, reactions(model))) # thermodynamic sign should correspond to the fluxes @@ -296,7 +272,7 @@ $(TYPEDSIGNATURES) Return the coupling bounds of a max-min driving force model. """ function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) - n = length(_get_active_reaction_ids(model)) + n = length(Internal.active_reaction_ids(model)) neg_dg_lb = fill(-model.max_dg_bound, n) neg_dg_ub = fill(0.0, n) From 310fdf2d0b37c7c865104841c409bc02e9a9329a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 23:04:41 +0100 Subject: [PATCH 158/531] add test for multiple bounds case --- test/reconstruction/enzyme_constrained.jl | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 155446db4..904b73c44 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -138,7 +138,8 @@ end gm = make_enzyme_constrained_model( m; - gene_product_mass_group_bound = Dict("uncategorized" => 0.5), + gene_product_mass_group = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), + gene_product_mass_group_bound = Dict("uncategorized" => 0.5, "bound2" => 0.04), ) opt_model = flux_balance_analysis( @@ -151,9 +152,10 @@ end gene_products = values_dict(:enzyme, gm, opt_model) mass_groups = values_dict(:enzyme_group, gm, opt_model) - @test isapprox(rxn_fluxes["r6"], 3.181818181753438, atol = TEST_TOLERANCE) - @test isapprox(gene_products["g4"], 0.09090909090607537, atol = TEST_TOLERANCE) + @test isapprox(rxn_fluxes["r6"], 1.1688888886502442, atol = TEST_TOLERANCE) + @test isapprox(gene_products["g4"], 0.02666666666304931, atol = TEST_TOLERANCE) @test isapprox(mass_groups["uncategorized"], 0.5, atol = TEST_TOLERANCE) + @test isapprox(mass_groups["bound2"], 0.04, atol = TEST_TOLERANCE) @test length(genes(gm)) == 4 @test length(genes(gm.inner)) == 4 end From 0c7400205a8ee187d6520b199897a602235640a6 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 23:16:53 +0100 Subject: [PATCH 159/531] add more convenient enzyme model constructor --- src/reconstruction/enzyme_constrained.jl | 16 ++++++++++++++++ src/reconstruction/pipes/enzymes.jl | 4 ++-- test/reconstruction/enzyme_constrained.jl | 6 +----- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 90030de12..fc38096b3 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -165,3 +165,19 @@ function make_enzyme_constrained_model( model, ) end + +""" +$(TYPEDSIGNATURES) + +A convenience wrapper around [`make_enzyme_constrained_model`](@ref) that +enforces a global enzyme capacity limitation across all genes in the model with +`total_capacity_limitation` being the bound. +""" +make_enzyme_constrained_model( + model::AbstractMetabolicModel, + total_capacity_limitation::Float64, +) = make_enzyme_constrained_model( + model; + gene_product_mass_group = Dict("uncategorized" => genes(model)), + gene_product_mass_group_bound = Dict("uncategorized" => total_capacity_limitation), +) diff --git a/src/reconstruction/pipes/enzymes.jl b/src/reconstruction/pipes/enzymes.jl index 4aac0b119..947558265 100644 --- a/src/reconstruction/pipes/enzymes.jl +++ b/src/reconstruction/pipes/enzymes.jl @@ -15,8 +15,8 @@ Specifies a model variant which adds extra semantics of the EnzymeConstrained al giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to [`make_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_enzyme_constraints(; kwargs...) = - model -> make_enzyme_constrained_model(model; kwargs...) +with_enzyme_constraints(args...; kwargs...) = + model -> make_enzyme_constrained_model(model, args...; kwargs...) """ $(TYPEDSIGNATURES) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 155446db4..e6dac66f2 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -45,11 +45,7 @@ lower_bound = [-1000.0, -1.0], upper_bound = [nothing, 12.0], ) |> - with_enzyme_constraints( - gene_product_mass_group_bound = Dict( - "uncategorized" => total_gene_product_mass, - ), - ) + with_enzyme_constraints(total_gene_product_mass) opt_model = flux_balance_analysis( gm, From 37d7c87cc1934fb5f1b67d903964d9c59e89cb11 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 14 Feb 2023 11:33:25 +0100 Subject: [PATCH 160/531] DNF-explosion avoidance --- src/analysis/modifications/knockout.jl | 10 +++--- src/types/accessors/AbstractMetabolicModel.jl | 35 +++++++++++++++++++ src/types/accessors/ModelWrapper.jl | 3 ++ src/types/misc/gene_associations.jl | 23 ++++++++++++ src/types/models/JSONModel.jl | 11 ++++++ src/types/models/MATModel.jl | 16 ++++++++- src/types/models/SBMLModel.jl | 11 ++++++ 7 files changed, 103 insertions(+), 6 deletions(-) diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index e99252737..6612ecd24 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -34,11 +34,11 @@ overloaded so that the knockouts may work differently (more efficiently) with other models. """ function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) - for (rxn_num, rxn_id) in enumerate(variables(model)) - rga = reaction_gene_associations(model, rxn_id) - if !isnothing(rga) && - all([any(in.(gene_ids, Ref(conjunction))) for conjunction in rga]) - set_optmodel_bound!(rxn_num, opt_model, lower_bound = 0, upper_bound = 0) + #TODO this should preferably work on reactions. Make it a wrapper. + KOs = Set(gene_ids) + for (ridx, rid) in enumerate(variables(model)) + if eval_reaction_gene_association(model, rid, falses = KOs) == false # also tests for nothing! + set_optmodel_bound!(ridx, opt_model, lower_bound = 0, upper_bound = 0) end end end diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 0dc66ec0d..fd52c8aea 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -232,6 +232,41 @@ end """ $(TYPEDSIGNATURES) +Directly evaluate the reaction-gene-association from the data available in the +model. The default implementation evaluates the GRR over the DNF formula +available from [`reaction_gene_associations`](@ref), which may be detrimental +in models where the DNF formula gets exceedingly big while it is in fact not +required for the given analysis, such as for calculating the gene knockouts. In +such cases, users may provide overload of +[`eval_reaction_gene_association`](@ref) to skip the DNF conversion. + +The evaluation takes the first set of gene identifiers of `falses` and `trues` +that isn't `nothing` and considers these to be evaluated as such, while all +other identifiers form the complement with the negated evaluation; at least one +must be supplied. +""" +function eval_reaction_gene_association( + a::AbstractMetabolicModel, + reaction_id::String; + falses::Maybe{AbstractSet{String}} = nothing, + trues::Maybe{AbstractSet{String}} = nothing, +) + isnothing(falses) || return maybemap( + grr -> any(!any(in(falses), clause) for clause in grr), + reaction_gene_associations(a, reaction_id), + ) + + isnothing(trues) || return maybemap( + grr -> any(all(in(trues), clause) for clause in grr), + reaction_gene_associations(a, reaction_id), + ) + + throw(ArgumentError("at least one of 'falses' and 'trues' must be specified")) +end + +""" +$(TYPEDSIGNATURES) + Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not, return `nothing`. """ diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 7d5a211be..55f8a8c08 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -19,6 +19,9 @@ end @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes +eval_reaction_gene_association(w::ModelWrapper, rid::String; kwargs...) = + eval_reaction_gene_association(unwrap_model(w), rid; kwargs...) + @inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes @inherit_model_methods_fn AbstractModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes gene_product_molar_mass gene_product_lower_bound gene_product_upper_bound diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 38e6d4777..7a5160160 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -22,6 +22,29 @@ end """ $(TYPEDSIGNATURES) +Evaluate the SBML `GeneProductAssociation` in the same way as +[`Accessors.eval_reaction_gene_association`](@ref). +""" +function eval_grr( + gpa::SBML.GeneProductAssociation; + falses::Maybe{AbstractSet{String}} = nothing, + trues::Maybe{AbstractSet{String}} = nothing, +)::Bool + @assert !(isnothing(falses) && isnothing(trues)) "at least one of 'trues' and 'falses' must be defined" + + e(x::SBML.GeneProductAssociation) = + throw(DomainError(x, "evaluating unsupported GeneProductAssociation contents")) + e(x::SBML.GPARef) = + !isnothing(falses) ? !(x.gene_product in falses) : (x.gene_product in trues) + e(x::SBML.GPAAnd) = all(e, x.terms) + e(x::SBML.GPAOr) = any(e, x.terms) + + e(gpa) +end + +""" +$(TYPEDSIGNATURES) + Convert a GeneAssociation to the corresponding `SBML.jl` structure. """ function unparse_grr( diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 0ec7d32bf..9c0dddff8 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -175,6 +175,17 @@ Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( """ $(TYPEDSIGNATURES) +Parse and directly evaluate the `.gene_reaction_rule` in the reaction. +""" +Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs...) = + maybemap( + x -> eval_grr(parse_grr_to_sbml(x); kwargs...), + get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), + ) + +""" +$(TYPEDSIGNATURES) + Parses the `.subsystem` out from reactions. """ Accessors.reaction_subsystem(model::JSONModel, rid::String) = diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 294075f59..4173bec11 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -131,7 +131,7 @@ end """ $(TYPEDSIGNATURES) -Extracts the associations from `grRules` key, if present. +Extract and parse the associations from `grRules` key, if present. """ function Accessors.reaction_gene_associations(m::MATModel, rid::String) if haskey(m.mat, "grRules") @@ -145,6 +145,20 @@ end """ $(TYPEDSIGNATURES) +Extract and directly evaluate the associations from `grRules` key, if present. +""" +function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwargs...) + if haskey(m.mat, "grRules") + grr = m.mat["grRules"][findfirst(==(rid), variables(m))] + typeof(grr) == String ? eval_grr(parse_grr_to_sbml(grr); kwargs...) : nothing + else + nothing + end +end + +""" +$(TYPEDSIGNATURES) + Extract metabolite formula from key `metFormula` or `metFormulas`. """ Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 9f87f8c0a..526cfe2a2 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -210,6 +210,17 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) +Evaluate the gene association formula directly from the SBML Math structure. +""" +Accessors.eval_reaction_gene_association(model::SBMLModel, rid::String; kwargs...) = + maybemap( + x -> eval_grr(x; kwargs...), + model.sbml.reactions[rid].gene_product_association, + ) + +""" +$(TYPEDSIGNATURES) + Get [`MetaboliteFormula`](@ref) from a chosen metabolite from [`SBMLModel`](@ref). """ Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = From f2b84be3b1ed199887600dc2948c11ea5162a4e7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 14 Feb 2023 12:02:52 +0100 Subject: [PATCH 161/531] port the GRR parsing to 2.0 --- Project.toml | 2 + src/types.jl | 1 + src/types/misc/gene_associations.jl | 214 +++++++++++------- test/data_static.jl | 84 +------ test/io/io.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 6 +- .../simplified_enzyme_constrained.jl | 6 +- test/runtests.jl | 1 + test/types/MatrixModel.jl | 4 +- test/types/Reaction.jl | 2 +- 10 files changed, 141 insertions(+), 181 deletions(-) diff --git a/Project.toml b/Project.toml index 5e4794f4b..05f45a51c 100644 --- a/Project.toml +++ b/Project.toml @@ -14,6 +14,7 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MAT = "23992714-dd62-5051-b70f-ba57cb901cac" MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" +PikaParser = "3bbf5609-3e7b-44cd-8549-7c69f321e792" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" SBML = "e5567a89-2604-4b09-9718-f5f78e97c3bb" @@ -32,6 +33,7 @@ JuMP = "1" MAT = "0.10" MacroTools = "0.5.6" OrderedCollections = "1.4" +PikaParser = "0.5" SBML = "~1.3, ~1.4" StableRNGs = "1.0" Tulip = "0.7.0, 0.8.0, 0.9.2" diff --git a/src/types.jl b/src/types.jl index 2f5034c4b..f2caa9414 100644 --- a/src/types.jl +++ b/src/types.jl @@ -87,6 +87,7 @@ end using SBML using SparseArrays + import PikaParser as PP @inc_dir types misc @export_locals diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 7a5160160..8a027e0e9 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -1,22 +1,30 @@ """ $(TYPEDSIGNATURES) - -Parse `SBML.GeneProductAssociation` structure to the simpler [`GeneAssociations`](@ref). -The input must be (implicitly) in a positive DNF. +Parse `SBML.GeneProductAssociation` structure and convert it to a strictly +positive DNF [`GeneAssociation`](@ref). Negation (`SBML.GPANot`) is not +supported. """ -function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociationsDNF - parse_ref(x) = - typeof(x) == SBML.GPARef ? [x.gene_product] : - begin - @models_log @warn "Could not parse a part of gene association, ignoring: $x" - String[] +function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation + + function fold_and(dnfs::Vector{Vector{Vector{String}}})::Vector{Vector{String}} + if isempty(dnfs) + [String[]] + else + unique(unique(String[l; r]) for l in dnfs[1] for r in fold_and(dnfs[2:end])) end - parse_and(x) = - typeof(x) == SBML.GPAAnd ? vcat([parse_and(i) for i in x.terms]...) : parse_ref(x) - parse_or(x) = - typeof(x) == SBML.GPAOr ? vcat([parse_or(i) for i in x.terms]...) : [parse_and(x)] - return parse_or(gpa) + end + + dnf(x::SBML.GPARef) = [[x.gene_product]] + dnf(x::SBML.GPAOr) = collect(Set(vcat(dnf.(x.terms)...))) + dnf(x::SBML.GPAAnd) = fold_and(dnf.(x.terms)) + dnf(x) = throw( + DomainError( + x, + "unsupported gene product association contents of type $(typeof(x))", + ), + ) + return dnf(gpa) end """ @@ -72,69 +80,109 @@ julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") parse_grr(s::String) = maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{GeneAssociationsDNF} """ -$(TYPEDSIGNATURES) - -Internal helper for parsing the string GRRs into SBML data structures. More -general than [`parse_grr`](@ref). +PikaParser grammar for stringy GRR expressions. """ -function parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} - s = str - toks = String[] - m = Nothing - while !isnothing( - begin - m = match(r"( +|[a-zA-Z0-9_-]+|[^ a-zA-Z0-9_()-]+|[(]|[)])(.*)", s) - end, - ) - tok = strip(m.captures[1]) - !isempty(tok) && push!(toks, tok) - s = m.captures[2] +const grr_grammar = begin + # characters that typically form the identifiers + isident(x::Char) = + isletter(x) || + isdigit(x) || + x == '_' || + x == '-' || + x == ':' || + x == '.' || + x == '\'' || + x == '[' || + x == ']' || + x == '\x03' # a very ugly exception for badly parsed MAT files + + # scanner helpers + eat(p) = m -> begin + last = 0 + for i in eachindex(m) + p(m[i]) || break + last = i + end + last end - fail() = throw(DomainError(str, "Could not parse GRR")) - - # shunting yard - ops = Symbol[] - vals = SBML.GeneProductAssociation[] - fold(sym, op) = - while !isempty(ops) && last(ops) == sym - r = pop!(vals) - l = pop!(vals) - pop!(ops) - push!(vals, op([l, r])) - end - for tok in toks - if tok in ["and", "AND", "&", "&&"] - push!(ops, :and) - elseif tok in ["or", "OR", "|", "||"] - fold(:and, SBML.GPAAnd) - push!(ops, :or) - elseif tok == "(" - push!(ops, :paren) - elseif tok == ")" - fold(:and, SBML.GPAAnd) - fold(:or, SBML.GPAOr) - if isempty(ops) || last(ops) != :paren - fail() - else - pop!(ops) - end - else - push!(vals, SBML.GPARef(tok)) - end + # eat one of keywords + kws(w...) = m -> begin + last = eat(isident)(m) + m[begin:last] in w ? last : 0 end - fold(:and, SBML.GPAAnd) - fold(:or, SBML.GPAOr) + PP.make_grammar( + [:expr], + PP.flatten( + Dict( + :space => PP.first(PP.scan(eat(isspace)), PP.epsilon), + :id => PP.scan(eat(isident)), + :orop => + PP.first(PP.tokens("||"), PP.token('|'), PP.scan(kws("OR", "or"))), + :andop => PP.first( + PP.tokens("&&"), + PP.token('&'), + PP.scan(kws("AND", "and")), + ), + :expr => PP.seq(:space, :orexpr, :space, PP.end_of_input), + :orexpr => PP.first( + :or => PP.seq(:andexpr, :space, :orop, :space, :orexpr), + :andexpr, + ), + :andexpr => PP.first( + :and => PP.seq(:baseexpr, :space, :andop, :space, :andexpr), + :baseexpr, + ), + :baseexpr => PP.first( + :id, + :parenexpr => PP.seq( + PP.token('('), + :space, + :orexpr, + :space, + PP.token(')'), + ), + ), + ), + Char, + ), + ) +end - if !isempty(ops) || length(vals) > 1 - fail() - end +grr_grammar_open(m, _) = + m.rule == :expr ? Bool[0, 1, 0, 0] : + m.rule == :parenexpr ? Bool[0, 0, 1, 0, 0] : + m.rule in [:or, :and] ? Bool[1, 0, 0, 0, 1] : + m.rule in [:andexpr, :orexpr, :notexpr, :baseexpr] ? Bool[1] : + (false for _ in m.submatches) + +grr_grammar_fold(m, _, subvals) = + m.rule == :id ? SBML.GPARef(m.view) : + m.rule == :and ? SBML.GPAAnd([subvals[1], subvals[5]]) : + m.rule == :or ? SBML.GPAOr([subvals[1], subvals[5]]) : + m.rule == :parenexpr ? subvals[3] : + m.rule == :expr ? subvals[2] : isempty(subvals) ? nothing : subvals[1] + +""" +$(TYPEDSIGNATURES) - if isempty(vals) - nothing +Internal helper for parsing the string GRRs into SBML data structures. More +general than [`parse_grr`](@ref). +""" +function parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} + all(isspace, str) && return nothing + tree = PP.parse_lex(grr_grammar, str) + match = PP.find_match_at!(tree, :expr, 1) + if match > 0 + return PP.traverse_match( + tree, + match, + open = grr_grammar_open, + fold = grr_grammar_fold, + ) else - first(vals) + throw(DomainError(str, "cannot parse GRR")) end end @@ -147,23 +195,17 @@ string. # Example ``` julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) -"(YIL010W and YLR043C) or (YIL010W and YGR209C)" +"(YIL010W && YLR043C) || (YIL010W && YGR209C)" ``` """ -unparse_grr(::Type{String}, grr::GeneAssociationsDNF)::String = unparse_grr_from_dnf(grr) -unparse_grr(::Type{String}, isozymes::Vector{Isozyme})::String = - unparse_grr_from_dnf([collect(keys(iso.stoichiometry)) for iso in isozymes]) - -""" -$(TYPEDSIGNATURES) - -Internal helper for parsing the GRRs into their human readable versions. -""" -function unparse_grr_from_dnf(grr::GeneAssociationsDNF) - grr_strings = String[] - for gr in grr - push!(grr_strings, "(" * join([g for g in gr], " and ") * ")") - end - grr_string = join(grr_strings, " or ") - return grr_string +function unparse_grr( + ::Type{String}, + grr::GeneAssociationDNF; + and = " && ", + or = " || ", +)::String + return join(("(" * join(gr, and) * ")" for gr in grr), or) end + +unparse_grr(::Type{String}, isozymes::Vector{Isozyme}; kwargs...)::String = + unparse_grr(String, [collect(keys(iso.stoichiometry)) for iso in isozymes]; kwargs...) diff --git a/test/data_static.jl b/test/data_static.jl index f06a71d5b..b8e072fa3 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -300,93 +300,11 @@ const ecoli_core_gene_product_masses = Dict{String,Float64}( "b2279" => 10.845, ) -const ecoli_core_protein_stoichiometry = Dict{String,Vector{Vector{Float64}}}( - #= - Data made up, each isozyme is assumed to be composed of - only one subunit each. - =# - "ACALD" => [[1.0], [1.0]], - "PTAr" => [[1.0], [1.0]], - "ALCD2x" => [[1.0], [1.0], [1.0]], - "PDH" => [[1.0, 1.0, 1.0]], - "PYK" => [[1.0], [1.0]], - "CO2t" => [[1.0]], - "MALt2_2" => [[1.0]], - "CS" => [[1.0]], - "PGM" => [[1.0], [1.0], [1.0]], - "TKT1" => [[1.0], [1.0]], - "ACONTa" => [[1.0], [1.0]], - "GLNS" => [[1.0], [1.0]], - "ICL" => [[1.0]], - "FBA" => [[1.0], [1.0], [1.0]], - "FORt2" => [[1.0], [1.0]], - "G6PDH2r" => [[1.0]], - "AKGDH" => [[1.0, 1.0, 1.0]], - "TKT2" => [[1.0], [1.0]], - "FRD7" => [[1.0, 1.0, 1.0, 1.0]], - "SUCOAS" => [[1.0, 1.0]], - "FBP" => [[1.0], [1.0]], - "ICDHyr" => [[1.0]], - "AKGt2r" => [[1.0]], - "GLUSy" => [[1.0, 1.0]], - "TPI" => [[1.0]], - "FORt" => [[1.0], [1.0]], - "ACONTb" => [[1.0], [1.0]], - "GLNabc" => [[1.0, 1.0, 1.0]], - "RPE" => [[1.0], [1.0]], - "ACKr" => [[1.0], [1.0], [1.0]], - "THD2" => [[1.0, 1.0]], - "PFL" => [[1.0, 1.0, 1.0], [1.0, 1.0], [1.0, 1.0], [1.0, 1.0]], - "RPI" => [[1.0], [1.0]], - "D_LACt2" => [[1.0], [1.0]], - "TALA" => [[1.0], [1.0]], - "PPCK" => [[1.0]], - "ACt2r" => [[1.0]], - "NH4t" => [[1.0], [1.0]], - "PGL" => [[1.0]], - "NADTRHD" => [[1.0], [1.0, 1.0]], - "PGK" => [[1.0]], - "LDH_D" => [[1.0], [1.0]], - "ME1" => [[1.0]], - "PIt2r" => [[1.0], [1.0]], - "ATPS4r" => [ - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], - [1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0], - ], - "GLCpts" => [[1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0, 1.0], [1.0, 1.0, 1.0, 1.0]], - "GLUDy" => [[1.0]], - "CYTBD" => [[1.0, 1.0], [1.0, 1.0]], - "FUMt2_2" => [[1.0]], - "FRUpts2" => [[1.0, 1.0, 1.0, 1.0, 1.0]], - "GAPD" => [[1.0]], - "H2Ot" => [[1.0], [1.0]], - "PPC" => [[1.0]], - "NADH16" => [[1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0, 1.0]], - "PFK" => [[1.0], [1.0]], - "MDH" => [[1.0]], - "PGI" => [[1.0]], - "O2t" => [[1.0]], - "ME2" => [[1.0]], - "GND" => [[1.0]], - "SUCCt2_2" => [[1.0]], - "GLUN" => [[1.0], [1.0], [1.0]], - "ETOHt2r" => [[1.0]], - "ADK1" => [[1.0]], - "ACALDt" => [[1.0]], - "SUCDi" => [[1.0, 1.0, 1.0, 1.0]], - "ENO" => [[1.0]], - "MALS" => [[1.0], [1.0]], - "GLUt2r" => [[1.0]], - "PPS" => [[1.0]], - "FUM" => [[1.0], [1.0], [1.0]], -) - const ecoli_core_reaction_kcats = Dict{String,Vector{Tuple{Float64,Float64}}}( #= Data taken from Heckmann, David, et al. "Machine learning applied to enzyme turnover numbers reveals protein structural correlates and improves metabolic - models." Nature communications 9.1 (2018): 1-10. Assume forward and reverse - kcats are the same, and each isozyme has the same kcat. + models." Nature communications 9.1 (2018): 1-10. =# "ACALD" => [(568.1130792316333, 568.1130792316333), (568.856126503717, 568.856126503717)], diff --git a/test/io/io.jl b/test/io/io.jl index eb565e51c..479562301 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -25,5 +25,5 @@ (i, r) in reconmodel.reactions if !isnothing(r.gene_associations) ] @test length(recon_grrs) == 5938 - @test sum(length.(recon_grrs)) == 13903 + @test sum(length.(recon_grrs)) == 31504 end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 155446db4..fedbe535b 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -15,9 +15,7 @@ push!( newisozymes, Isozyme( - gene_product_stoichiometry = Dict( - grr .=> ecoli_core_protein_stoichiometry[rid][i], - ), + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], ), @@ -62,7 +60,7 @@ @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.812827846796761, + 0.8128427019072836, atol = TEST_TOLERANCE, ) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 0d13745f3..b385d23f8 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -15,9 +15,7 @@ push!( newisozymes, Isozyme( - gene_product_stoichiometry = Dict( - grr .=> ecoli_core_protein_stoichiometry[rid][i], - ), + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], ), @@ -46,7 +44,7 @@ @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8907273630431708, + 0.8907347602586123, atol = TEST_TOLERANCE, ) end diff --git a/test/runtests.jl b/test/runtests.jl index ca47cbe5f..d1fcd5fa9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -64,6 +64,7 @@ run_test_file("data_downloaded.jl") @testset "COBREXA test suite" begin run_test_dir(joinpath("types", "abstract"), "Abstract types") run_test_dir("types", "Model types and wrappers") + run_test_dir(joinpath("types", "misc"), "Model helper functions") run_test_file("log.jl") run_test_dir("utils", "Utilities") run_test_dir("io", "I/O functions") diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index 22e9c7ed8..eaa710053 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -14,6 +14,6 @@ end @test Set(variables(cm)) == Set(variables(sm)) @test Set(variables(cm)) == Set(variables(cm2)) - @test reaction_gene_associations(sm, variables(sm)[1]) == - reaction_gene_associations(cm, variables(sm)[1]) + @test sort(sort.(reaction_gene_association(sm, reactions(sm)[1]))) == + sort(sort.(reaction_gene_association(cm, reactions(sm)[1]))) end diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index aa76527be..71965ff3a 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -33,7 +33,7 @@ @test all( contains.( sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "glycolysis", "blah", "biocyc", "g1", "g2", "g3"], + ["r1", "100.0", "g1 && g2", "glycolysis", "blah", "biocyc", "g3"], ), ) From 13c126506c049af48c7fe798ba0710969f6f0984 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 14 Feb 2023 13:27:42 +0100 Subject: [PATCH 162/531] fix merge problems --- src/types.jl | 2 +- src/types/accessors/AbstractMetabolicModel.jl | 4 ++-- src/types/accessors/ModelWrapper.jl | 2 +- src/types/misc/gene_associations.jl | 12 ++++++------ src/types/models/JSONModel.jl | 7 +++++-- src/types/models/MatrixModel.jl | 2 +- test/types/BalancedGrowthCommunityModel.jl | 4 +--- test/types/MatrixModel.jl | 4 ++-- test/types/Reaction.jl | 2 +- 9 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/types.jl b/src/types.jl index f2caa9414..eea398a33 100644 --- a/src/types.jl +++ b/src/types.jl @@ -81,7 +81,7 @@ end @inject Types.Internal begin # TODO where is this declared? - using ..Types + using ...Types using ..Accessors using ..Log.Internal: @models_log diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index fd52c8aea..0edcfd8f2 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -251,12 +251,12 @@ function eval_reaction_gene_association( falses::Maybe{AbstractSet{String}} = nothing, trues::Maybe{AbstractSet{String}} = nothing, ) - isnothing(falses) || return maybemap( + isnothing(falses) || return Types.Internal.maybemap( grr -> any(!any(in(falses), clause) for clause in grr), reaction_gene_associations(a, reaction_id), ) - isnothing(trues) || return maybemap( + isnothing(trues) || return Types.Internal.maybemap( grr -> any(all(in(trues), clause) for clause in grr), reaction_gene_associations(a, reaction_id), ) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 55f8a8c08..dd51ee950 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -19,7 +19,7 @@ end @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes -eval_reaction_gene_association(w::ModelWrapper, rid::String; kwargs...) = +eval_reaction_gene_association(w::AbstractModelWrapper, rid::String; kwargs...) = eval_reaction_gene_association(unwrap_model(w), rid; kwargs...) @inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index 8a027e0e9..a9b88b1ad 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -2,10 +2,10 @@ """ $(TYPEDSIGNATURES) Parse `SBML.GeneProductAssociation` structure and convert it to a strictly -positive DNF [`GeneAssociation`](@ref). Negation (`SBML.GPANot`) is not +positive DNF [`GeneAssociationsDNF`](@ref). Negation (`SBML.GPANot`) is not supported. """ -function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociation +function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociationsDNF function fold_and(dnfs::Vector{Vector{Vector{String}}})::Vector{Vector{String}} if isempty(dnfs) @@ -53,7 +53,7 @@ end """ $(TYPEDSIGNATURES) -Convert a GeneAssociation to the corresponding `SBML.jl` structure. +Convert [`GeneAssociationsDNF`](@ref) to the corresponding `SBML.jl` structure. """ function unparse_grr( ::Type{SBML.GeneProductAssociation}, @@ -66,7 +66,7 @@ end $(TYPEDSIGNATURES) Parse a DNF gene association rule in format `(YIL010W and YLR043C) or (YIL010W -and YGR209C)` to `GeneAssociation. Also accepts `OR`, `|`, `||`, `AND`, `&`, +and YGR209C)` to `GeneAssociationsDNF`. Also accepts `OR`, `|`, `||`, `AND`, `&`, and `&&`. # Example @@ -200,12 +200,12 @@ julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) """ function unparse_grr( ::Type{String}, - grr::GeneAssociationDNF; + grr::GeneAssociationsDNF; and = " && ", or = " || ", )::String return join(("(" * join(gr, and) * ")" for gr in grr), or) end -unparse_grr(::Type{String}, isozymes::Vector{Isozyme}; kwargs...)::String = +unparse_grr(::Type{String}, isozymes::GeneAssociations; kwargs...)::String = unparse_grr(String, [collect(keys(iso.stoichiometry)) for iso in isozymes]; kwargs...) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 9c0dddff8..06bf287cd 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -179,8 +179,11 @@ Parse and directly evaluate the `.gene_reaction_rule` in the reaction. """ Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs...) = maybemap( - x -> eval_grr(parse_grr_to_sbml(x); kwargs...), - get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), + x -> eval_grr(x; kwargs...), + maybemap( + parse_grr_to_sbml, + get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), + ), ) """ diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index e4dbfc681..bb613bdc7 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -141,7 +141,7 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociation`](@ref) from [`MatrixModel`](@ref) by reaction ID. +Retrieve the [`GeneAssociations`](@ref) from [`MatrixModel`](@ref) by reaction ID. """ Accessors.reaction_gene_associations( model::MatrixModel, diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index e6143a39e..12a9cb69a 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -288,9 +288,7 @@ end push!( newisozymes, Isozyme( - gene_product_stoichiometry = Dict( - grr .=> ecoli_core_protein_stoichiometry[rid][i], - ), + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), kcat_forward = ecoli_core_reaction_kcats[rid][i][1], kcat_backward = ecoli_core_reaction_kcats[rid][i][2], ), diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index eaa710053..c6ddfcbec 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -14,6 +14,6 @@ end @test Set(variables(cm)) == Set(variables(sm)) @test Set(variables(cm)) == Set(variables(cm2)) - @test sort(sort.(reaction_gene_association(sm, reactions(sm)[1]))) == - sort(sort.(reaction_gene_association(cm, reactions(sm)[1]))) + @test sort(sort.(reaction_gene_associations(sm, reactions(sm)[1]))) == + sort(sort.(reaction_gene_associations(cm, reactions(sm)[1]))) end diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 71965ff3a..ddcce07e2 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -33,7 +33,7 @@ @test all( contains.( sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "g1 && g2", "glycolysis", "blah", "biocyc", "g3"], + ["r1", "100.0", "glycolysis", "blah", "biocyc", "g3"], ), ) From e563596c88fb5794c2844cdc7906a8f7b3121eb5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 14 Feb 2023 14:34:46 +0100 Subject: [PATCH 163/531] forgotten files --- test/types/misc/gene_associations.jl | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 test/types/misc/gene_associations.jl diff --git a/test/types/misc/gene_associations.jl b/test/types/misc/gene_associations.jl new file mode 100644 index 000000000..99d280216 --- /dev/null +++ b/test/types/misc/gene_associations.jl @@ -0,0 +1,17 @@ + +@testset "GRR parsing" begin + @test sort( + sort.( + COBREXA.Types.Internal.parse_grr( + "(αλφα OR βητα\x03) AND (R2-D2's_gene OR prefix:su[ff]ix)", + ) + ), + ) == [ + ["R2-D2's_gene", "αλφα"], + ["R2-D2's_gene", "βητα\x03"], + ["prefix:su[ff]ix", "αλφα"], + ["prefix:su[ff]ix", "βητα\x03"], + ] + @test_throws DomainError COBREXA.Types.Internal.parse_grr("(") + @test isnothing(COBREXA.Types.Internal.parse_grr(" ")) +end From 9f57b0b831957ce54d70dec296b73c826dc44f39 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 09:55:46 +0100 Subject: [PATCH 164/531] remove and clean up GeneAssociations alias - it was too similar to GeneAssociationsDNF - Vector{Isozyme} actually explains the contents very well, and no complexity is hidden --- src/types/GeneAssociations.jl | 10 ---------- src/types/Reaction.jl | 2 +- src/types/misc/gene_associations.jl | 2 +- src/types/models/MatrixModel.jl | 8 +++++--- 4 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/types/GeneAssociations.jl b/src/types/GeneAssociations.jl index fffa5e53d..13ec2d555 100644 --- a/src/types/GeneAssociations.jl +++ b/src/types/GeneAssociations.jl @@ -23,13 +23,3 @@ reaction rule and converts it into the appropriate format. Assumes the """ Isozyme(gids::Vector{String}; kwargs...) = Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) - -""" - const GeneAssociations - -An association of genes to a reaction. Each [`Isozyme`](@ref) represents a -distinct enzyme that can catalyze a certain reaction. All the gene products in -an isozyme are required for the enzyme to function. Multiple [`Isozyme`](@ref)s -can catalyze the same reaction, but function independently. -""" -const GeneAssociations = Vector{Isozyme} diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl index 00a348212..4824eb9ed 100644 --- a/src/types/Reaction.jl +++ b/src/types/Reaction.jl @@ -14,7 +14,7 @@ Base.@kwdef mutable struct Reaction metabolites::Dict{String,Float64} = Dict{String,Float64}() lower_bound::Float64 = -constants.default_reaction_bound upper_bound::Float64 = constants.default_reaction_bound - gene_associations::Maybe{GeneAssociations} = nothing + gene_associations::Maybe{Vector{Isozyme}} = nothing subsystem::Maybe{String} = nothing notes::Notes = Notes() annotations::Annotations = Annotations() diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl index a9b88b1ad..80c983285 100644 --- a/src/types/misc/gene_associations.jl +++ b/src/types/misc/gene_associations.jl @@ -207,5 +207,5 @@ function unparse_grr( return join(("(" * join(gr, and) * ")" for gr in grr), or) end -unparse_grr(::Type{String}, isozymes::GeneAssociations; kwargs...)::String = +unparse_grr(::Type{String}, isozymes::AbstractVector{Isozyme}; kwargs...)::String = unparse_grr(String, [collect(keys(iso.stoichiometry)) for iso in isozymes]; kwargs...) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index bb613bdc7..8d1227c07 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -130,8 +130,9 @@ Accessors.reaction_stoichiometry(m::MatrixModel, ridx)::Dict{String,Float64} = """ $(TYPEDSIGNATURES) -Retrieve the gene reaction associations from [`MatrixModel`](@ref) by reaction -index. +Retrieve the [`GeneAssociationsDNF`](@ref) from [`MatrixModel`](@ref) by +reaction index (for [`MatrixModel`](@ref) this is typically faster than +retrieving by ID). """ Accessors.reaction_gene_associations( model::MatrixModel, @@ -141,7 +142,8 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) -Retrieve the [`GeneAssociations`](@ref) from [`MatrixModel`](@ref) by reaction ID. +Retrieve the [`GeneAssociationsDNF`](@ref) from [`MatrixModel`](@ref) by +reaction ID. """ Accessors.reaction_gene_associations( model::MatrixModel, From 0f4378d51ff7875db1336efa66c4edc77168beee Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 13 Feb 2023 20:31:50 +0100 Subject: [PATCH 165/531] add annotation details for model --- src/types/models/ObjectModel.jl | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 59efa9c61..88d0e6720 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -33,20 +33,26 @@ keys(model.reactions) $(TYPEDFIELDS) """ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel - "Name of the model" + "Name of the model." id::String - "Ordered dictionary of reactions" + "Ordered dictionary of reactions." reactions::OrderedDict{String,Reaction} = OrderedDict{String,Reaction}() - "Ordered dictionary of metabolites" + "Ordered dictionary of metabolites." metabolites::OrderedDict{String,Metabolite} = OrderedDict{String,Metabolite}() - "Ordered dictionary of genes" + "Ordered dictionary of genes." genes::OrderedDict{String,Gene} = OrderedDict{String,Gene}() - "Model objective" + "Model objective." objective::Dict{String,Float64} = Dict{String,Float64}() + + "Machine readable reference to organism embedded via MIRIAM annotation. This should include species name, taxonomy ID, and url to the genome." + annotations::Vector{Tuple{String, String, String}} = Vector{Tuple{String, String, String}}() + + "Reference information for the model. This should include the DOI and author contact information." + references::Dict{String, String} = Dict{String, String}() end # AbstractMetabolicModel interface follows From f52a6ed84bd13ee766f5e9c66bcfefe4aa97851e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 13 Feb 2023 20:39:45 +0100 Subject: [PATCH 166/531] co-opt pre-existing annotations and notes instead --- src/types/models/ObjectModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 88d0e6720..7aa0d12d1 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -49,10 +49,10 @@ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel objective::Dict{String,Float64} = Dict{String,Float64}() "Machine readable reference to organism embedded via MIRIAM annotation. This should include species name, taxonomy ID, and url to the genome." - annotations::Vector{Tuple{String, String, String}} = Vector{Tuple{String, String, String}}() + annotations::Annotations = Annotations() "Reference information for the model. This should include the DOI and author contact information." - references::Dict{String, String} = Dict{String, String}() + notes::Notes = Notes() end # AbstractMetabolicModel interface follows From ab4c52e4c628ed427fa16ad76048d209d7991e35 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 12:29:26 +0100 Subject: [PATCH 167/531] add accessors --- src/types/accessors/AbstractMetabolicModel.jl | 17 +++++++++++++++++ src/types/models/ObjectModel.jl | 17 +++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 0edcfd8f2..e6071ee5c 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -462,3 +462,20 @@ Return the upper bound of the gene product concentration associated with the `mo """ gene_product_upper_bound(model::AbstractMetabolicModel, gid::String)::Float64 = constants.default_gene_product_bound + +""" +$(TYPEDSIGNATURES) + +Return the notes associated with a `model`. At minimum this should include the +model authors, contact information, and DOI of the associated publication. +""" +model_notes(model::AbstractMetabolicModel)::Notes = Dict() + +""" +$(TYPEDSIGNATURES) + +Return the annotations associated with a `model`. Typically, these should be +encoded in MIRIAM format. At minimum it should include the full species name +with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. +""" +model_annotations(model::AbstractMetabolicModel)::Annotations = Dict() diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 7aa0d12d1..b477d6b1e 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -340,6 +340,23 @@ Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = """ $(TYPEDSIGNATURES) +Return the notes associated with a `model`. At minimum this should include the +model authors, contact information, and DOI of the associated publication. +""" +model_notes(model::ObjectModel)::Notes = model.notes + +""" +$(TYPEDSIGNATURES) + +Return the annotations associated with a `model`. Typically, these should be +encoded in MIRIAM format. At minimum it should include the full species name +with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. +""" +model_annotations(model::ObjectModel)::Annotations = model.annotations + +""" +$(TYPEDSIGNATURES) + Convert any `AbstractMetabolicModel` into a `ObjectModel`. Note, some data loss may occur since only the generic interface is used during the conversion process. Additionally, assume the stoichiometry for each gene association is 1. From 17c9a4648074101aff1d31ee3fe4e678785d4213 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 12:30:08 +0100 Subject: [PATCH 168/531] format --- src/types/models/ObjectModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index b477d6b1e..f7df49087 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -352,7 +352,7 @@ Return the annotations associated with a `model`. Typically, these should be encoded in MIRIAM format. At minimum it should include the full species name with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. """ -model_annotations(model::ObjectModel)::Annotations = model.annotations +model_annotations(model::ObjectModel)::Annotations = model.annotations """ $(TYPEDSIGNATURES) From cd4d9912fa30529b9c4ed1b8e19ecf29ecf241f6 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 15:50:14 +0100 Subject: [PATCH 169/531] keep information with convert --- src/types/models/ObjectModel.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index f7df49087..196b58480 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -33,9 +33,6 @@ keys(model.reactions) $(TYPEDFIELDS) """ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel - "Name of the model." - id::String - "Ordered dictionary of reactions." reactions::OrderedDict{String,Reaction} = OrderedDict{String,Reaction}() @@ -343,7 +340,7 @@ $(TYPEDSIGNATURES) Return the notes associated with a `model`. At minimum this should include the model authors, contact information, and DOI of the associated publication. """ -model_notes(model::ObjectModel)::Notes = model.notes +Accessors.model_notes(model::ObjectModel)::Notes = model.notes """ $(TYPEDSIGNATURES) @@ -352,7 +349,7 @@ Return the annotations associated with a `model`. Typically, these should be encoded in MIRIAM format. At minimum it should include the full species name with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. """ -model_annotations(model::ObjectModel)::Annotations = model.annotations +Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations """ $(TYPEDSIGNATURES) @@ -366,7 +363,9 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) return model end - id = "" # TODO: add accessor to get model ID + modelnotes = model_notes(model) + model_annotations = model_annotations(model) + modelreactions = OrderedDict{String,Reaction}() modelmetabolites = OrderedDict{String,Metabolite}() modelgenes = OrderedDict{String,Gene}() @@ -424,10 +423,11 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) end return ObjectModel(; - id, reactions = modelreactions, metabolites = modelmetabolites, genes = modelgenes, objective = modelobjective, + notes = modelnotes, + annotations = modelannotations, ) end From 38dce9e1282ff2686fe691ed6387ae83642340ab Mon Sep 17 00:00:00 2001 From: stelmo Date: Tue, 14 Feb 2023 14:56:02 +0000 Subject: [PATCH 170/531] automatic formatting triggered by @stelmo on PR #750 --- src/types/accessors/AbstractMetabolicModel.jl | 2 +- src/types/models/ObjectModel.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index e6071ee5c..0fdbdb543 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -476,6 +476,6 @@ $(TYPEDSIGNATURES) Return the annotations associated with a `model`. Typically, these should be encoded in MIRIAM format. At minimum it should include the full species name -with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. +with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. """ model_annotations(model::AbstractMetabolicModel)::Annotations = Dict() diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 196b58480..7254738e5 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -347,7 +347,7 @@ $(TYPEDSIGNATURES) Return the annotations associated with a `model`. Typically, these should be encoded in MIRIAM format. At minimum it should include the full species name -with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. +with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. """ Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations From 1915296d328c3116412cfaff121db83b6cdeb3b3 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 16:05:38 +0100 Subject: [PATCH 171/531] partially extend notes to JSONModel --- src/types/models/JSONModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 06bf287cd..981c9d975 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -322,7 +322,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) ocs = objective(mm) json = Dict{String,Any}() - json["id"] = "model" # default + json["id"] = get(model_notes, "id", "missing model ID") json[first(constants.keynames.genes)] = [ Dict([ From 0a16969f162ec65ebbd06ec97ae12b8e5c424ad5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 18:26:32 +0100 Subject: [PATCH 172/531] remove id kwarg from ObjectModel --- docs/src/examples/04b_standardmodel_construction.jl | 2 +- src/types/misc/ObjectModel.jl | 11 ++++++----- test/analysis/knockouts.jl | 4 ++-- test/reconstruction/ObjectModel.jl | 2 +- test/reconstruction/constrained_allocation.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 2 +- test/reconstruction/gapfill_minimum_reactions.jl | 2 +- test/reconstruction/knockouts.jl | 4 ++-- test/types/BalancedGrowthCommunityModel.jl | 4 ++-- test/types/ObjectModel.jl | 2 +- 10 files changed, 18 insertions(+), 17 deletions(-) diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index de4642178..91abd356f 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -10,7 +10,7 @@ using COBREXA # In `COBREXA`, model construction is primarily supported through `ObjectModel`s. # To begin, create an empty `ObjectModel`. -model = ObjectModel("FirstModel") # assign model id = "FirstModel" +model = ObjectModel() # assign model id = "FirstModel" # Next, genes, metabolites and reactions need to be added to the model. diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index c5bf01be3..3b3ee06de 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -5,11 +5,12 @@ $(TYPEDSIGNATURES) Shallow copy of a [`ObjectModel`](@ref) """ Base.copy(m::ObjectModel) = ObjectModel( - id = m.id, reactions = m.reactions, metabolites = m.metabolites, genes = m.genes, objective = m.objective, + notes = m.notes, + annotations = m.annotations, ) """ @@ -53,16 +54,16 @@ Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) $(TYPEDSIGNATURES) Return the lower bounds for all reactions in `model`. -Order matches that of the reaction IDs returned by [`variables`](@ref). +Order matches that of the reaction IDs returned by [`reactions`](@ref). """ lower_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].lower_bound for rxn in variables(model)] + [model.reactions[rxn].lower_bound for rxn in reactions(model)] """ $(TYPEDSIGNATURES) Return the upper bounds for all reactions in `model`. -Order matches that of the reaction IDs returned in [`variables`](@ref). +Order matches that of the reaction IDs returned in [`reactions`](@ref). """ upper_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].upper_bound for rxn in variables(model)] + [model.reactions[rxn].upper_bound for rxn in reactions(model)] diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index c8154ed11..5dc12f8ce 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -1,5 +1,5 @@ @testset "single_knockout" begin - m = ObjectModel(id = "testmodel") + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) @@ -59,7 +59,7 @@ end @testset "multiple_knockouts" begin - m = ObjectModel(id = "testmodel") + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index dde7ab2f5..9c4c05854 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -26,7 +26,7 @@ rxns = [r1, r2] - model = ObjectModel(id = "model") + model = ObjectModel() model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) model.genes = OrderedDict(g.id => g for g in gene_vec) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index c33ab87fb..252c5c970 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -1,6 +1,6 @@ @testset "Constrained allocation FBA" begin - m = ObjectModel(id = "") + m = ObjectModel() m1 = Metabolite("m1") m2 = Metabolite("m2") m3 = Metabolite("m3") diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index c5617daec..3702e313f 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -87,7 +87,7 @@ end original GECKO paper. This model is nice to troubleshoot with, because the stoich matrix is small. =# - m = ObjectModel(id = "enzyme_constrained") + m = ObjectModel() m1 = Metabolite("m1") m2 = Metabolite("m2") m3 = Metabolite("m3") diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl index 6e7eb8315..a141b9ba3 100644 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ b/test/reconstruction/gapfill_minimum_reactions.jl @@ -2,7 +2,7 @@ #= Implement the small model that should be gapfilled. =# - model = ObjectModel(id = "partial model") + model = ObjectModel() (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id = "m$i") for i = 1:8] diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl index 4f9176fb1..8133009af 100644 --- a/test/reconstruction/knockouts.jl +++ b/test/reconstruction/knockouts.jl @@ -6,7 +6,7 @@ is available, but inside a group all of the genes need to be available """ @testset "knockout_single_gene" begin - m = ObjectModel(id = "testmodel") + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite("B")) add_gene!(m, Gene("g1")) @@ -52,7 +52,7 @@ is available, but inside a group all of the genes need to be available end @testset "knockout_multiple_genes" begin - m = ObjectModel(id = "testmodel") + m = ObjectModel() add_metabolite!(m, Metabolite("A")) add_metabolite!(m, Metabolite(id = "B")) add_gene!(m, Gene("g1")) diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 12a9cb69a..2c175a2d6 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -1,5 +1,5 @@ @testset "BalancedGrowthCommunityModel: simple model" begin - m1 = ObjectModel(id = "Model1") + m1 = ObjectModel() add_metabolites!( m1, [ @@ -22,7 +22,7 @@ ], ) - m2 = ObjectModel(id = "Model2") + m2 = ObjectModel() add_metabolites!( m2, [ diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 9b4d04604..f1528a1c3 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -38,7 +38,7 @@ gs = [g1, g2, g3] rxns = [r1, r2, r3, r4] - model = ObjectModel(id = "model") + model = ObjectModel() model.reactions = OrderedDict(r.id => r for r in rxns) model.metabolites = OrderedDict(m.id => m for m in mets) model.genes = OrderedDict(g.id => g for g in gs) From e242559ffebdfafd2926d258bd9201c08324febd Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 20:00:02 +0100 Subject: [PATCH 173/531] fix bug --- src/types/accessors/ModelWrapper.jl | 2 +- src/types/models/ObjectModel.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index dd51ee950..b0935dc0f 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -15,7 +15,7 @@ end # [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 7254738e5..f2cc00f64 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -364,7 +364,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) end modelnotes = model_notes(model) - model_annotations = model_annotations(model) + modelannotations = model_annotations(model) modelreactions = OrderedDict{String,Reaction}() modelmetabolites = OrderedDict{String,Metabolite}() From 434237eca39c475cbfe8766b74f7bba2d0f60ebc Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 14 Feb 2023 21:08:01 +0100 Subject: [PATCH 174/531] fix another bug --- src/types/models/JSONModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 981c9d975..9145b1032 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -322,7 +322,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) ocs = objective(mm) json = Dict{String,Any}() - json["id"] = get(model_notes, "id", "missing model ID") + json["id"] = get(model_notes(mm), "id", "missing model ID") json[first(constants.keynames.genes)] = [ Dict([ From 22ba547c2b6ec0480da491d5b3539271696ac39e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 10:06:45 +0100 Subject: [PATCH 175/531] ignore `id` export in JSON altogether (cobrapy doesn't object at all) --- src/types/models/JSONModel.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 9145b1032..39fa0b011 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -322,7 +322,6 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) ocs = objective(mm) json = Dict{String,Any}() - json["id"] = get(model_notes(mm), "id", "missing model ID") json[first(constants.keynames.genes)] = [ Dict([ From 4a2667962fc95a46f6b4be21ea030fc4ababe61e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 10:14:44 +0100 Subject: [PATCH 176/531] clean comment --- docs/src/examples/04b_standardmodel_construction.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl index 91abd356f..8ccf7c76f 100644 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ b/docs/src/examples/04b_standardmodel_construction.jl @@ -10,7 +10,7 @@ using COBREXA # In `COBREXA`, model construction is primarily supported through `ObjectModel`s. # To begin, create an empty `ObjectModel`. -model = ObjectModel() # assign model id = "FirstModel" +model = ObjectModel() # Next, genes, metabolites and reactions need to be added to the model. From d5da5b28fc8f972d41a7ea217c22663a97bbecaa Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 10:27:59 +0100 Subject: [PATCH 177/531] export notes/annotations to JSON --- src/types/models/JSONModel.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 39fa0b011..9daf49e51 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -304,6 +304,10 @@ Return the name of gene with ID `gid`. Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) +Accessors.model_annotations(model::JSONModel)::Annotations = get(model["annotation"]) + +Accessors.model_notes(model::JSONModel)::Notes = get(model["notes"]) + """ $(TYPEDSIGNATURES) @@ -323,6 +327,9 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) json = Dict{String,Any}() + json["annotation"] = model_annotation(mm) + json["notes"] = model_nodes(mm) + json[first(constants.keynames.genes)] = [ Dict([ "id" => gid, From d07038b35adb54cbb946087c8e7657a05266b732 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 10:28:13 +0100 Subject: [PATCH 178/531] simplify ObjectModel notes/annotations --- src/types/models/ObjectModel.jl | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index f2cc00f64..d9d0486bb 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -363,9 +363,6 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) return model end - modelnotes = model_notes(model) - modelannotations = model_annotations(model) - modelreactions = OrderedDict{String,Reaction}() modelmetabolites = OrderedDict{String,Metabolite}() modelgenes = OrderedDict{String,Gene}() @@ -427,7 +424,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) metabolites = modelmetabolites, genes = modelgenes, objective = modelobjective, - notes = modelnotes, - annotations = modelannotations, + notes = model_notes(model), + annotations = model_annotations(model), ) end From 31b31aa500c94381bec6b96d0f0c1f9451ba60e0 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 15 Feb 2023 15:37:41 +0100 Subject: [PATCH 179/531] model notes/annotations for SBML --- Project.toml | 2 +- src/types/models/SBMLModel.jl | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 05f45a51c..45b1aeba1 100644 --- a/Project.toml +++ b/Project.toml @@ -34,7 +34,7 @@ MAT = "0.10" MacroTools = "0.5.6" OrderedCollections = "1.4" PikaParser = "0.5" -SBML = "~1.3, ~1.4" +SBML = "1.4.2" StableRNGs = "1.0" Tulip = "0.7.0, 0.8.0, 0.9.2" julia = "1.5" diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 526cfe2a2..4b1652505 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -379,6 +379,11 @@ Return the notes about gene with ID `gid`. Accessors.gene_notes(model::SBMLModel, gid::String) = _sbml_import_notes(model.sbml.gene_products[gid].notes) +Accessors.model_annotations(model::SBMLModel) = + _sbml_import_cvterms(model.sbo, model.cv_terms) + +Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.notes) + """ $(TYPEDSIGNATURES) @@ -473,6 +478,9 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) Dict(rid => oc for (rid, oc) in zip(rxns, objective(mm)) if oc != 0), ), ), + notes = _sbml_export_notes(model.notes(mm)), + sbo = _sbml_export_sbo(model.annotations), + cv_terms = _sbml_export_cvterms(model.annotations), ), ) end From c6f06e05f4cac1d5b67f145f5f79c902fa5efe07 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 16:06:37 +0100 Subject: [PATCH 180/531] small fix for JSONModel --- src/types/models/JSONModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 9daf49e51..135d4337e 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -304,9 +304,9 @@ Return the name of gene with ID `gid`. Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) -Accessors.model_annotations(model::JSONModel)::Annotations = get(model["annotation"]) +Accessors.model_annotations(model::JSONModel)::Annotations = get(model.json, "annotation", Annotations()) -Accessors.model_notes(model::JSONModel)::Notes = get(model["notes"]) +Accessors.model_notes(model::JSONModel)::Notes = get(model.json, "notes", Notes()) """ $(TYPEDSIGNATURES) From 19266a215fab79b6dae2091e2c44f1c17238b8bd Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 15 Feb 2023 15:09:45 +0000 Subject: [PATCH 181/531] automatic formatting triggered by @stelmo on PR #750 --- src/types/models/JSONModel.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 135d4337e..dc3460845 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -304,7 +304,8 @@ Return the name of gene with ID `gid`. Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) -Accessors.model_annotations(model::JSONModel)::Annotations = get(model.json, "annotation", Annotations()) +Accessors.model_annotations(model::JSONModel)::Annotations = + get(model.json, "annotation", Annotations()) Accessors.model_notes(model::JSONModel)::Notes = get(model.json, "notes", Notes()) From 0d6d49f1469ff048cb9dd81350c3e6e2239cd430 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 18:30:47 +0100 Subject: [PATCH 182/531] fix spellinb gug --- src/types/models/JSONModel.jl | 4 ++-- src/types/models/SBMLModel.jl | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index dc3460845..c0088132c 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -328,8 +328,8 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) json = Dict{String,Any}() - json["annotation"] = model_annotation(mm) - json["notes"] = model_nodes(mm) + json["annotation"] = model_annotations(mm) + json["notes"] = model_notes(mm) json[first(constants.keynames.genes)] = [ Dict([ diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 4b1652505..c76729916 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -380,9 +380,9 @@ Accessors.gene_notes(model::SBMLModel, gid::String) = _sbml_import_notes(model.sbml.gene_products[gid].notes) Accessors.model_annotations(model::SBMLModel) = - _sbml_import_cvterms(model.sbo, model.cv_terms) + _sbml_import_cvterms(model.sbml.sbo, model.sbml.cv_terms) -Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.notes) +Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.sbml.notes) """ $(TYPEDSIGNATURES) @@ -478,9 +478,9 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) Dict(rid => oc for (rid, oc) in zip(rxns, objective(mm)) if oc != 0), ), ), - notes = _sbml_export_notes(model.notes(mm)), - sbo = _sbml_export_sbo(model.annotations), - cv_terms = _sbml_export_cvterms(model.annotations), + notes = _sbml_export_notes(model_notes(mm)), + sbo = _sbml_export_sbo(model_annotations(mm)), + cv_terms = _sbml_export_cvterms(model_annotations(mm)), ), ) end From b7e228c639a4cc476acbbca88ca5da74e9b25e20 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 18:14:46 +0100 Subject: [PATCH 183/531] remove ObjectModel accessors docstrings --- src/types/models/ObjectModel.jl | 181 -------------------------------- 1 file changed, 181 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index d9d0486bb..5847313c6 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -53,59 +53,21 @@ Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel end # AbstractMetabolicModel interface follows -""" -$(TYPEDSIGNATURES) -Return a vector of reaction id strings contained in `model`. -The order of reaction ids returned here matches the order used to construct the -stoichiometric matrix. -""" Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) -""" -$(TYPEDSIGNATURES) - -Return the number of reactions contained in `model`. -""" Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) Accessors.Internal.@all_variables_are_reactions ObjectModel -""" -$(TYPEDSIGNATURES) - -Return a vector of metabolite id strings contained in `model`. -The order of metabolite strings returned here matches the order used to construct -the stoichiometric matrix. -""" Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) -""" -$(TYPEDSIGNATURES) - -Return the number of metabolites in `model`. -""" Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) -""" -$(TYPEDSIGNATURES) - -Return a vector of gene id strings in `model`. -""" Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) -""" -$(TYPEDSIGNATURES) - -Return the number of genes in `model`. -""" Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) -""" -$(TYPEDSIGNATURES) - -Return the stoichiometric matrix associated with `model` in sparse format. -""" function Accessors.stoichiometry(model::ObjectModel)::SparseMat n_entries = 0 for (_, r) in model.reactions @@ -142,37 +104,14 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) end -""" -$(TYPEDSIGNATURES) - -Return the lower and upper bounds, respectively, for reactions in `model`. -Order matches that of the reaction ids returned in `variables()`. -""" Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) -""" -$(TYPEDSIGNATURES) - -Return the balance of the linear problem, i.e. b in Sv = 0 where S is the stoichiometric matrix -and v is the flux vector. -""" Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) -""" -$(TYPEDSIGNATURES) - -Return sparse objective vector for `model`. -""" Accessors.objective(model::ObjectModel)::SparseVec = sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) -""" -$(TYPEDSIGNATURES) - -Return the gene reaction rule in string format for reaction with `id` in `model`. -Return `nothing` if not available. -""" function Accessors.reaction_gene_associations( model::ObjectModel, id::String, @@ -184,180 +123,60 @@ function Accessors.reaction_gene_associations( ] end -""" -$(TYPEDSIGNATURES) - -Return the formula of reaction `id` in `model`. -Return `nothing` if not present. -""" Accessors.metabolite_formula(model::ObjectModel, id::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.metabolites[id].formula) -""" -$(TYPEDSIGNATURES) - -Return the charge associated with metabolite `id` in `model`. -Return nothing if not present. -""" Accessors.metabolite_charge(model::ObjectModel, id::String)::Maybe{Int} = model.metabolites[id].charge -""" -$(TYPEDSIGNATURES) - -Return compartment associated with metabolite `id` in `model`. -Return `nothing` if not present. -""" Accessors.metabolite_compartment(model::ObjectModel, id::String)::Maybe{String} = model.metabolites[id].compartment -""" -$(TYPEDSIGNATURES) - -Return the subsystem associated with reaction `id` in `model`. -Return `nothing` if not present. -""" Accessors.reaction_subsystem(model::ObjectModel, id::String)::Maybe{String} = model.reactions[id].subsystem -""" -$(TYPEDSIGNATURES) - -Return the notes associated with metabolite `id` in `model`. -Return an empty Dict if not present. -""" Accessors.metabolite_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.metabolites[id].notes -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with metabolite `id` in `model`. -Return an empty Dict if not present. -""" Accessors.metabolite_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.metabolites[id].annotations -""" -$(TYPEDSIGNATURES) - -Return the notes associated with gene `id` in `model`. -Return an empty Dict if not present. -""" Accessors.gene_notes(model::ObjectModel, gid::String) = model.genes[gid].notes -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with gene `id` in `model`. -Return an empty Dict if not present. -""" Accessors.gene_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.genes[id].annotations -""" -$(TYPEDSIGNATURES) - -Return the notes associated with reaction `id` in `model`. -Return an empty Dict if not present. -""" Accessors.reaction_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.reactions[id].notes -""" -$(TYPEDSIGNATURES) - -Return the annotation associated with reaction `id` in `model`. -Return an empty Dict if not present. -""" Accessors.reaction_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.reactions[id].annotations -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -""" Accessors.reaction_stoichiometry(m::ObjectModel, rid::String)::Dict{String,Float64} = m.reactions[rid].metabolites -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `id`. -""" Accessors.reaction_name(m::ObjectModel, rid::String) = m.reactions[rid].name -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `id`. -""" Accessors.metabolite_name(m::ObjectModel, mid::String) = m.metabolites[mid].name -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `id`. -""" Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name -""" -$(TYPEDSIGNATURES) - -Return the molar mass of translated gene with ID `gid`. -""" Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].product_molar_mass -""" -$(TYPEDSIGNATURES) - -Return the [`Isozyme`](@ref)s associated with the `model` and reaction `rid`. -""" Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations -""" -$(TYPEDSIGNATURES) - -Return the lower bound of the gene product concentration associated with the `model` and gene `gid`. -""" Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = model.genes[gid].product_lower_bound -""" -$(TYPEDSIGNATURES) - -Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. -""" Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = model.genes[gid].product_upper_bound -""" -$(TYPEDSIGNATURES) - -Return the notes associated with a `model`. At minimum this should include the -model authors, contact information, and DOI of the associated publication. -""" Accessors.model_notes(model::ObjectModel)::Notes = model.notes -""" -$(TYPEDSIGNATURES) - -Return the annotations associated with a `model`. Typically, these should be -encoded in MIRIAM format. At minimum it should include the full species name -with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. -""" Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations -""" -$(TYPEDSIGNATURES) - -Convert any `AbstractMetabolicModel` into a `ObjectModel`. Note, some data loss -may occur since only the generic interface is used during the conversion -process. Additionally, assume the stoichiometry for each gene association is 1. -""" function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) if typeof(model) == ObjectModel return model From 93b9add82a2e45f065aa316ba96e6d42d51e19ef Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:17:35 +0100 Subject: [PATCH 184/531] Remove docstrings for JSONModel --- src/types/models/JSONModel.jl | 71 ++++++++++------------------------- 1 file changed, 20 insertions(+), 51 deletions(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index c0088132c..efe5fc246 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -71,41 +71,46 @@ end _parse_notes(x)::Notes = _parse_annotations(x) +""" +$(TYPEDSIGNATURES) +""" Accessors.n_variables(model::JSONModel) = length(model.rxns) + +""" +$(TYPEDSIGNATURES) +""" Accessors.n_metabolites(model::JSONModel) = length(model.mets) -Accessors.n_genes(model::JSONModel) = length(model.genes) """ $(TYPEDSIGNATURES) +""" +Accessors.n_genes(model::JSONModel) = length(model.genes) -Extract reaction names (stored as `.id`) from JSON model. +""" +$(TYPEDSIGNATURES) """ Accessors.variables(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] """ $(TYPEDSIGNATURES) - -Extract metabolite names (stored as `.id`) from JSON model. """ Accessors.metabolites(model::JSONModel) = [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] """ $(TYPEDSIGNATURES) - -Extract gene names from a JSON model. """ Accessors.genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] +""" +$(TYPEDSIGNATURES) +""" Accessors.Internal.@all_variables_are_reactions JSONModel """ $(TYPEDSIGNATURES) - -Get the stoichiometry. Assuming the information is stored in reaction object -under key `.metabolites`. """ function Accessors.stoichiometry(model::JSONModel) rxn_ids = variables(model) @@ -145,9 +150,6 @@ end """ $(TYPEDSIGNATURES) - -Get the bounds for reactions, assuming the information is stored in -`.lower_bound` and `.upper_bound`. """ Accessors.bounds(model::JSONModel) = ( [get(rxn, "lower_bound", -constants.default_reaction_bound) for rxn in model.rxns], @@ -156,16 +158,12 @@ Accessors.bounds(model::JSONModel) = ( """ $(TYPEDSIGNATURES) - -Collect `.objective_coefficient` keys from model reactions. """ Accessors.objective(model::JSONModel) = sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in model.rxns]) """ $(TYPEDSIGNATURES) - -Parses the `.gene_reaction_rule` from reactions. """ Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( parse_grr, @@ -174,8 +172,6 @@ Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( """ $(TYPEDSIGNATURES) - -Parse and directly evaluate the `.gene_reaction_rule` in the reaction. """ Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs...) = maybemap( @@ -186,42 +182,29 @@ Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs.. ), ) -""" -$(TYPEDSIGNATURES) - -Parses the `.subsystem` out from reactions. -""" Accessors.reaction_subsystem(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "subsystem", nothing) """ $(TYPEDSIGNATURES) - -Parse and return the metabolite `.formula` """ Accessors.metabolite_formula(model::JSONModel, mid::String) = maybemap(parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) """ $(TYPEDSIGNATURES) - -Return the metabolite `.charge` """ Accessors.metabolite_charge(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "charge", 0) """ $(TYPEDSIGNATURES) - -Return the metabolite `.compartment` """ Accessors.metabolite_compartment(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "compartment", nothing) """ $(TYPEDSIGNATURES) - -Gene annotations from the [`JSONModel`](@ref). """ Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = maybemap( _parse_annotations, @@ -230,16 +213,12 @@ Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = maybema """ $(TYPEDSIGNATURES) - -Gene notes from the [`JSONModel`](@ref). """ Accessors.gene_notes(model::JSONModel, gid::String)::Notes = maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) """ $(TYPEDSIGNATURES) - -Reaction annotations from the [`JSONModel`](@ref). """ Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = maybemap( _parse_annotations, @@ -248,16 +227,12 @@ Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = may """ $(TYPEDSIGNATURES) - -Reaction notes from the [`JSONModel`](@ref). """ Accessors.reaction_notes(model::JSONModel, rid::String)::Notes = maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) """ $(TYPEDSIGNATURES) - -Metabolite annotations from the [`JSONModel`](@ref). """ Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = maybemap( _parse_annotations, @@ -266,53 +241,47 @@ Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = m """ $(TYPEDSIGNATURES) - -Metabolite notes from the [`JSONModel`](@ref). """ Accessors.metabolite_notes(model::JSONModel, mid::String)::Notes = maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. """ Accessors.reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = model.rxns[model.rxn_index[rid]]["metabolites"] """ $(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. """ Accessors.reaction_name(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "name", nothing) """ $(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. """ Accessors.metabolite_name(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "name", nothing) """ $(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. """ Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) +""" +$(TYPEDSIGNATURES) +""" Accessors.model_annotations(model::JSONModel)::Annotations = get(model.json, "annotation", Annotations()) +""" +$(TYPEDSIGNATURES) +""" Accessors.model_notes(model::JSONModel)::Notes = get(model.json, "notes", Notes()) """ $(TYPEDSIGNATURES) - -Convert any [`AbstractMetabolicModel`](@ref) to [`JSONModel`](@ref). """ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) if typeof(mm) == JSONModel From 370b5cf07124b95543192d24abbba17b34cf2f8a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:19:35 +0100 Subject: [PATCH 185/531] re-add basic docstrings to ObjectModel --- src/types/models/ObjectModel.jl | 99 +++++++++++++++++++++++++++++++++ 1 file changed, 99 insertions(+) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 5847313c6..4993c1bdd 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -54,20 +54,44 @@ end # AbstractMetabolicModel interface follows +""" +$(TYPEDSIGNATURES) +""" Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) +""" +$(TYPEDSIGNATURES) +""" Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) +""" +$(TYPEDSIGNATURES) +""" Accessors.Internal.@all_variables_are_reactions ObjectModel +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) +""" +$(TYPEDSIGNATURES) +""" Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) +""" +$(TYPEDSIGNATURES) +""" Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) +""" +$(TYPEDSIGNATURES) +""" Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) +""" +$(TYPEDSIGNATURES) +""" function Accessors.stoichiometry(model::ObjectModel)::SparseMat n_entries = 0 for (_, r) in model.reactions @@ -104,14 +128,26 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) end +""" +$(TYPEDSIGNATURES) +""" Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) +""" +$(TYPEDSIGNATURES) +""" Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) +""" +$(TYPEDSIGNATURES) +""" Accessors.objective(model::ObjectModel)::SparseVec = sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) +""" +$(TYPEDSIGNATURES) +""" function Accessors.reaction_gene_associations( model::ObjectModel, id::String, @@ -123,60 +159,123 @@ function Accessors.reaction_gene_associations( ] end +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_formula(model::ObjectModel, id::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.metabolites[id].formula) +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_charge(model::ObjectModel, id::String)::Maybe{Int} = model.metabolites[id].charge +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_compartment(model::ObjectModel, id::String)::Maybe{String} = model.metabolites[id].compartment +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_subsystem(model::ObjectModel, id::String)::Maybe{String} = model.reactions[id].subsystem +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.metabolites[id].notes +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.metabolites[id].annotations +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_notes(model::ObjectModel, gid::String) = model.genes[gid].notes +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.genes[id].annotations +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.reactions[id].notes +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.reactions[id].annotations +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_stoichiometry(m::ObjectModel, rid::String)::Dict{String,Float64} = m.reactions[rid].metabolites +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_name(m::ObjectModel, rid::String) = m.reactions[rid].name +""" +$(TYPEDSIGNATURES) +""" Accessors.metabolite_name(m::ObjectModel, mid::String) = m.metabolites[mid].name +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].product_molar_mass +""" +$(TYPEDSIGNATURES) +""" Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = model.genes[gid].product_lower_bound +""" +$(TYPEDSIGNATURES) +""" Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = model.genes[gid].product_upper_bound +""" +$(TYPEDSIGNATURES) +""" Accessors.model_notes(model::ObjectModel)::Notes = model.notes +""" +$(TYPEDSIGNATURES) +""" Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations +""" +$(TYPEDSIGNATURES) +""" function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) if typeof(model) == ObjectModel return model From ebd8ae2bdef09f080b58032c32c9599f9f413b8f Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:20:21 +0100 Subject: [PATCH 186/531] add basic docstrings to HDF5Model --- src/types/models/HDF5Model.jl | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index 3fb8a20d3..b38372ac4 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -33,11 +33,17 @@ function Accessors.precache!(model::HDF5Model)::Nothing nothing end +""" +$(TYPEDSIGNATURES) +""" function Accessors.n_variables(model::HDF5Model)::Int precache!(model) length(model.h5["reactions"]) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.variables(model::HDF5Model)::Vector{String} precache!(model) # TODO is there any reasonable method to mmap strings from HDF5? @@ -46,31 +52,49 @@ end Accessors.Internal.@all_variables_are_reactions HDF5Model +""" +$(TYPEDSIGNATURES) +""" function Accessors.n_metabolites(model::HDF5Model)::Int precache!(model) length(model.h5["metabolites"]) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.metabolites(model::HDF5Model)::Vector{String} precache!(model) read(model.h5["metabolites"]) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.stoichiometry(model::HDF5Model)::SparseMat precache!(model) h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} precache!(model) (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.balance(model::HDF5Model)::SparseVec precache!(model) h5_read_sparse(SparseVec, model.h5["balance"]) end +""" +$(TYPEDSIGNATURES) +""" function Accessors.objective(model::HDF5Model)::SparseVec precache!(model) h5_read_sparse(SparseVec, model.h5["objective"]) From 33a09b8a67a0ed229ae93f8c78342e12194ff485 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:22:07 +0100 Subject: [PATCH 187/531] remove docstrings from MATModel --- src/types/models/MATModel.jl | 40 ------------------------------------ 1 file changed, 40 deletions(-) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 4173bec11..ee16969a4 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -15,8 +15,6 @@ Accessors.n_variables(m::MATModel)::Int = size(m.mat["S"], 2) """ $(TYPEDSIGNATURES) - -Extracts reaction names from `rxns` key in the MAT file. """ function Accessors.variables(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") @@ -30,8 +28,6 @@ Accessors.Internal.@all_variables_are_reactions MATModel """ $(TYPEDSIGNATURES) - -Guesses whether C in the MAT file is stored in A=[S;C]. """ _mat_has_squashed_coupling(mat) = haskey(mat, "A") && haskey(mat, "b") && length(mat["b"]) == size(mat["A"], 1) @@ -39,8 +35,6 @@ _mat_has_squashed_coupling(mat) = """ $(TYPEDSIGNATURES) - -Extracts metabolite names from `mets` key in the MAT file. """ function Accessors.metabolites(m::MATModel)::Vector{String} nm = n_metabolites(m) @@ -53,15 +47,11 @@ end """ $(TYPEDSIGNATURES) - -Extract the stoichiometry matrix, stored under key `S`. """ Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) """ $(TYPEDSIGNATURES) - -Extracts bounds from the MAT file, saved under `lb` and `ub`. """ Accessors.bounds(m::MATModel) = ( reshape(get(m.mat, "lb", fill(-Inf, n_variables(m), 1)), n_variables(m)), @@ -70,8 +60,6 @@ Accessors.bounds(m::MATModel) = ( """ $(TYPEDSIGNATURES) - -Extracts balance from the MAT model, defaulting to zeroes if not present. """ function Accessors.balance(m::MATModel) b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) @@ -83,16 +71,12 @@ end """ $(TYPEDSIGNATURES) - -Extracts the objective from the MAT model (defaults to zeroes). """ Accessors.objective(m::MATModel) = sparse(reshape(get(m.mat, "c", zeros(n_variables(m), 1)), n_variables(m))) """ $(TYPEDSIGNATURES) - -Extract coupling matrix stored, in `C` key. """ Accessors.coupling(m::MATModel) = _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_variables(m)+1:end, :]) : @@ -100,8 +84,6 @@ Accessors.coupling(m::MATModel) = """ $(TYPEDSIGNATURES) - -Extracts the coupling constraints. Currently, there are several accepted ways to store these in MATLAB models; this takes the constraints from vectors `cl` and `cu`. """ function Accessors.coupling_bounds(m::MATModel) nc = n_coupling_constraints(m) @@ -120,8 +102,6 @@ end """ $(TYPEDSIGNATURES) - -Extracts the possible gene list from `genes` key. """ function Accessors.genes(m::MATModel) x = get(m.mat, "genes", []) @@ -130,8 +110,6 @@ end """ $(TYPEDSIGNATURES) - -Extract and parse the associations from `grRules` key, if present. """ function Accessors.reaction_gene_associations(m::MATModel, rid::String) if haskey(m.mat, "grRules") @@ -144,8 +122,6 @@ end """ $(TYPEDSIGNATURES) - -Extract and directly evaluate the associations from `grRules` key, if present. """ function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwargs...) if haskey(m.mat, "grRules") @@ -158,8 +134,6 @@ end """ $(TYPEDSIGNATURES) - -Extract metabolite formula from key `metFormula` or `metFormulas`. """ Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( x -> parse_formula(x[findfirst(==(mid), metabolites(m))]), @@ -168,8 +142,6 @@ Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( """ $(TYPEDSIGNATURES) - -Extract metabolite charge from `metCharge` or `metCharges`. """ function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} met_charge = maybemap( @@ -181,8 +153,6 @@ end """ $(TYPEDSIGNATURES) - -Extract metabolite compartment from `metCompartment` or `metCompartments`. """ function Accessors.metabolite_compartment(m::MATModel, mid::String) res = maybemap( @@ -200,8 +170,6 @@ end """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. """ function Accessors.reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} ridx = first(indexin([rid], m.mat["rxns"])) @@ -210,8 +178,6 @@ end """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction at index `ridx`. """ function Accessors.reaction_stoichiometry(m::MATModel, ridx)::Dict{String,Float64} met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) @@ -220,8 +186,6 @@ end """ $(TYPEDSIGNATURES) - -Extract reaction name from `rxnNames`. """ Accessors.reaction_name(m::MATModel, rid::String) = maybemap( x -> x[findfirst(==(rid), variables(m))], @@ -230,8 +194,6 @@ Accessors.reaction_name(m::MATModel, rid::String) = maybemap( """ $(TYPEDSIGNATURES) - -Extract metabolite name from `metNames`. """ Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( x -> x[findfirst(==(mid), metabolites(m))], @@ -247,8 +209,6 @@ Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( """ $(TYPEDSIGNATURES) - -Convert any metabolic model to `MATModel`. """ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) if typeof(m) == MATModel From 2ab3085f1a5a928b1424b94d8f136c58c044ffd7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:23:05 +0100 Subject: [PATCH 188/531] remove docstrings from MatrixModel --- src/types/models/MatrixModel.jl | 31 +------------------------------ 1 file changed, 1 insertion(+), 30 deletions(-) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 8d1227c07..bedefdf48 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -49,8 +49,6 @@ end """ $(TYPEDSIGNATURES) - -Get the reactions in a `MatrixModel`. """ Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns @@ -58,45 +56,31 @@ Accessors.Internal.@all_variables_are_reactions MatrixModel """ $(TYPEDSIGNATURES) - -Metabolites in a `MatrixModel`. """ Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets """ $(TYPEDSIGNATURES) - -`MatrixModel` stoichiometry matrix. """ Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S """ $(TYPEDSIGNATURES) - -`MatrixModel` flux bounds. """ Accessors.bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) """ $(TYPEDSIGNATURES) - -`MatrixModel` target flux balance. """ Accessors.balance(a::MatrixModel)::SparseVec = a.b """ $(TYPEDSIGNATURES) - -`MatrixModel` objective vector. """ Accessors.objective(a::MatrixModel)::SparseVec = a.c """ $(TYPEDSIGNATURES) - -Collect all genes contained in the [`MatrixModel`](@ref). The call is expensive -for large models, because the vector is not stored and instead gets rebuilt -each time this function is called. """ function Accessors.genes(a::MatrixModel)::Vector{String} res = Set{String}() @@ -113,26 +97,18 @@ end """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. -Accessors.""" +""" Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction at index `ridx`. """ Accessors.reaction_stoichiometry(m::MatrixModel, ridx)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) """ $(TYPEDSIGNATURES) - -Retrieve the [`GeneAssociationsDNF`](@ref) from [`MatrixModel`](@ref) by -reaction index (for [`MatrixModel`](@ref) this is typically faster than -retrieving by ID). """ Accessors.reaction_gene_associations( model::MatrixModel, @@ -141,9 +117,6 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) - -Retrieve the [`GeneAssociationsDNF`](@ref) from [`MatrixModel`](@ref) by -reaction ID. """ Accessors.reaction_gene_associations( model::MatrixModel, @@ -152,8 +125,6 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) - -Make a `MatrixModel` out of any compatible model type. """ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicModel} if typeof(m) == MatrixModel From 710df298b349e73d87d65c5d2e9cd1f3d9b651d0 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:25:11 +0100 Subject: [PATCH 189/531] remove docstrings from SBMLModel --- src/types/models/SBMLModel.jl | 50 +++-------------------------------- 1 file changed, 3 insertions(+), 47 deletions(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index c76729916..48a163369 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -40,8 +40,6 @@ end """ $(TYPEDSIGNATURES) - -Get variables (aka reactions) from a [`SBMLModel`](@ref). """ Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids @@ -49,15 +47,11 @@ Accessors.Internal.@all_variables_are_reactions SBMLModel """ $(TYPEDSIGNATURES) - -Get metabolites from a [`SBMLModel`](@ref). """ Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids """ $(TYPEDSIGNATURES) - -Recreate the stoichiometry matrix from the [`SBMLModel`](@ref). """ function Accessors.stoichiometry(model::SBMLModel)::SparseMat @@ -98,9 +92,6 @@ end """ $(TYPEDSIGNATURES) - -Get the lower and upper flux bounds of model [`SBMLModel`](@ref). Throws `DomainError` in -case if the SBML contains mismatching units. """ function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} # There are multiple ways in SBML to specify a lower/upper bound. There are @@ -157,15 +148,11 @@ end """ $(TYPEDSIGNATURES) - -Balance vector of a [`SBMLModel`](@ref). This is always zero. """ Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) """ $(TYPEDSIGNATURES) - -Objective of the [`SBMLModel`](@ref). """ function Accessors.objective(model::SBMLModel)::SparseVec res = sparsevec([], [], n_reactions(model)) @@ -191,15 +178,11 @@ end """ $(TYPEDSIGNATURES) - -Get genes of a [`SBMLModel`](@ref). """ Accessors.genes(model::SBMLModel)::Vector{String} = model.gene_ids """ $(TYPEDSIGNATURES) - -Retrieve the reaction gene associations from [`SBMLModel`](@ref). """ Accessors.reaction_gene_associations( model::SBMLModel, @@ -209,8 +192,6 @@ Accessors.reaction_gene_associations( """ $(TYPEDSIGNATURES) - -Evaluate the gene association formula directly from the SBML Math structure. """ Accessors.eval_reaction_gene_association(model::SBMLModel, rid::String; kwargs...) = maybemap( @@ -220,24 +201,18 @@ Accessors.eval_reaction_gene_association(model::SBMLModel, rid::String; kwargs.. """ $(TYPEDSIGNATURES) - -Get [`MetaboliteFormula`](@ref) from a chosen metabolite from [`SBMLModel`](@ref). """ Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.sbml.species[mid].formula) """ $(TYPEDSIGNATURES) - -Get the compartment of a chosen metabolite from [`SBMLModel`](@ref). """ Accessors.metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment """ $(TYPEDSIGNATURES) - -Get charge of a chosen metabolite from [`SBMLModel`](@ref). """ Accessors.metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = model.sbml.species[mid].charge @@ -293,8 +268,6 @@ end """ $(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid`. """ function Accessors.reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} s = Dict{String,Float64}() @@ -310,45 +283,33 @@ end """ $(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. """ Accessors.reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name """ $(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. """ Accessors.metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name """ $(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. """ Accessors.gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name """ $(TYPEDSIGNATURES) - -Return the annotations of reaction with ID `rid`. """ Accessors.reaction_annotations(model::SBMLModel, rid::String) = _sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms) """ $(TYPEDSIGNATURES) - -Return the annotations of metabolite with ID `mid`. """ Accessors.metabolite_annotations(model::SBMLModel, mid::String) = _sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms) """ $(TYPEDSIGNATURES) - -Return the annotations of gene with ID `gid`. """ Accessors.gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( model.sbml.gene_products[gid].sbo, @@ -357,24 +318,18 @@ Accessors.gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms """ $(TYPEDSIGNATURES) - -Return the notes about reaction with ID `rid`. """ Accessors.reaction_notes(model::SBMLModel, rid::String) = _sbml_import_notes(model.sbml.reactions[rid].notes) """ $(TYPEDSIGNATURES) - -Return the notes about metabolite with ID `mid`. """ Accessors.metabolite_notes(model::SBMLModel, mid::String) = _sbml_import_notes(model.sbml.species[mid].notes) """ $(TYPEDSIGNATURES) - -Return the notes about gene with ID `gid`. """ Accessors.gene_notes(model::SBMLModel, gid::String) = _sbml_import_notes(model.sbml.gene_products[gid].notes) @@ -382,12 +337,13 @@ Accessors.gene_notes(model::SBMLModel, gid::String) = Accessors.model_annotations(model::SBMLModel) = _sbml_import_cvterms(model.sbml.sbo, model.sbml.cv_terms) +""" +$(TYPEDSIGNATURES) +""" Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.sbml.notes) """ $(TYPEDSIGNATURES) - -Convert any metabolic model to [`SBMLModel`](@ref). """ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) if typeof(mm) == SBMLModel From d6fb4047cd02758510403ea80d2803d1c081c27b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:25:42 +0100 Subject: [PATCH 190/531] remove docstrings from Serialized --- src/types/models/Serialized.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/types/models/Serialized.jl b/src/types/models/Serialized.jl index 0eb9d0bcc..1d03aa95a 100644 --- a/src/types/models/Serialized.jl +++ b/src/types/models/Serialized.jl @@ -20,8 +20,6 @@ end """ $(TYPEDSIGNATURES) - -Unwrap the serialized model (precaching it transparently). """ function Accessors.unwrap_model(m::Serialized) precache!(m) @@ -30,8 +28,6 @@ end """ $(TYPEDSIGNATURES) - -Load the `Serialized` model from disk in case it's not alreadly loaded. """ function Accessors.precache!(model::Serialized)::Nothing if isnothing(model.m) From c58a1d24122c755f0ea2f5ad25acb5bdb39ed8b9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:28:47 +0100 Subject: [PATCH 191/531] remove docstrings for MatrixCoupling --- src/types/wrappers/MatrixCoupling.jl | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/types/wrappers/MatrixCoupling.jl b/src/types/wrappers/MatrixCoupling.jl index 0d1f30f88..ae9946eff 100644 --- a/src/types/wrappers/MatrixCoupling.jl +++ b/src/types/wrappers/MatrixCoupling.jl @@ -34,30 +34,22 @@ end """ $(TYPEDSIGNATURES) - -Get the internal [`MatrixModel`](@ref) out of [`MatrixCoupling`](@ref). """ Accessors.unwrap_model(a::MatrixCoupling) = a.lm """ $(TYPEDSIGNATURES) - -Coupling constraint matrix for a `MatrixCoupling`. """ Accessors.coupling(a::MatrixCoupling)::SparseMat = vcat(coupling(a.lm), a.C) """ $(TYPEDSIGNATURES) - -The number of coupling constraints in a `MatrixCoupling`. """ Accessors.n_coupling_constraints(a::MatrixCoupling)::Int = n_coupling_constraints(a.lm) + size(a.C, 1) """ $(TYPEDSIGNATURES) - -Coupling bounds for a `MatrixCoupling`. """ Accessors.coupling_bounds(a::MatrixCoupling)::Tuple{Vector{Float64},Vector{Float64}} = vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) From 7101d9911f99b76c81011577e13f563120518f37 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:33:19 +0100 Subject: [PATCH 192/531] simplify docstrings for SimplifiedEnzymeConstrainedModel --- .../SimplifiedEnzymeConstrainedModel.jl | 23 ++++--------------- 1 file changed, 4 insertions(+), 19 deletions(-) diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 6af2d9f8f..6da42aa9f 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -74,18 +74,12 @@ Accessors.stoichiometry(model::SimplifiedEnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) - -Reconstruct an objective of the [`SimplifiedEnzymeConstrainedModel`](@ref). """ Accessors.objective(model::SimplifiedEnzymeConstrainedModel) = simplified_enzyme_constrained_column_reactions(model)' * objective(model.inner) """ $(TYPEDSIGNATURES) - -Returns the internal reactions in a [`SimplifiedEnzymeConstrainedModel`](@ref) (these may be split -to forward- and reverse-only parts; reactions IDs are mangled accordingly with -suffixes). """ Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = let inner_reactions = variables(model.inner) @@ -99,15 +93,11 @@ Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) - -The number of reactions (including split ones) in [`SimplifiedEnzymeConstrainedModel`](@ref). """ Accessors.n_variables(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) """ $(TYPEDSIGNATURES) - -Return the variable bounds for [`SimplifiedEnzymeConstrainedModel`](@ref). """ Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) @@ -140,9 +130,10 @@ Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Return the coupling of [`SimplifiedEnzymeConstrainedModel`](@ref). That combines the coupling of -the wrapped model, coupling for split reactions, and the coupling for the total -enzyme capacity. +Return the coupling of a [`SimplifiedEnzymeConstrainedModel`](@ref). This +combines the coupling of the wrapped model, coupling for split reactions, and +the coupling for the total enzyme capacity, which is added as a +[`coupling_bounds`](@ref). """ Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) = vcat( coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model), @@ -151,18 +142,12 @@ Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) = vcat( """ $(TYPEDSIGNATURES) - -Count the coupling constraints in [`SimplifiedEnzymeConstrainedModel`](@ref) (refer to -[`coupling`](@ref) for details). """ Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = n_coupling_constraints(model.inner) + 1 """ $(TYPEDSIGNATURES) - -The coupling bounds for [`SimplifiedEnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for -details). """ Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = let (iclb, icub) = coupling_bounds(model.inner) From 54f40f5fd7541a8616689a031f46648511d4b709 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:35:01 +0100 Subject: [PATCH 193/531] remove docstrings for macro from JSONModel --- src/types/models/JSONModel.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index efe5fc246..00e57c4ee 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -104,9 +104,6 @@ $(TYPEDSIGNATURES) Accessors.genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] -""" -$(TYPEDSIGNATURES) -""" Accessors.Internal.@all_variables_are_reactions JSONModel """ From 15906289bde0906f7693b0fe6d379c5d12d1ca76 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:36:39 +0100 Subject: [PATCH 194/531] simplify docstrings for MaxMinDrivingForceModel --- src/types/wrappers/MaxMinDrivingForceModel.jl | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/types/wrappers/MaxMinDrivingForceModel.jl index 43536cdf2..8c3e8c737 100644 --- a/src/types/wrappers/MaxMinDrivingForceModel.jl +++ b/src/types/wrappers/MaxMinDrivingForceModel.jl @@ -90,9 +90,6 @@ Accessors.variables(model::MaxMinDrivingForceModel) = """ $(TYPEDSIGNATURES) - -The number of variables is 1 + the number of metabolites + the number of -reactions, where the 1 comes from the total max-min driving force. """ Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + n_reactions(model) @@ -125,8 +122,6 @@ Accessors.objective(model::MaxMinDrivingForceModel) = """ $(TYPEDSIGNATURES) - -Get the equality constraint rhs of `model`. """ function Accessors.balance(model::MaxMinDrivingForceModel) # proton water balance @@ -155,8 +150,6 @@ end """ $(TYPEDSIGNATURES) - -Get the equality constraint lhs of `model`. """ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) @@ -206,8 +199,6 @@ end """ $(TYPEDSIGNATURES) - -Get the simple variable bounds of `model`. """ function Accessors.bounds(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) @@ -235,8 +226,6 @@ end """ $(TYPEDSIGNATURES) - -Return the coupling of a max-min driving force model. """ function Accessors.coupling(model::MaxMinDrivingForceModel) @@ -268,8 +257,6 @@ end """ $(TYPEDSIGNATURES) - -Return the coupling bounds of a max-min driving force model. """ function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) n = length(Internal.active_reaction_ids(model)) From 60629c51f3752e31d68692dcebcced40952bbdc9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:40:02 +0100 Subject: [PATCH 195/531] simply docstrings for EnzymeConstrainedModel --- src/types/wrappers/EnzymeConstrainedModel.jl | 34 ++++++-------------- 1 file changed, 9 insertions(+), 25 deletions(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index bddfe2a0f..09b5d5b91 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -89,9 +89,10 @@ Accessors.unwrap_model(model::EnzymeConstrainedModel) = model.inner """ $(TYPEDSIGNATURES) -Return a stoichiometry of the [`EnzymeConstrainedModel`](@ref). The enzymatic reactions are -split into unidirectional forward and reverse ones, each of which may have -multiple variants per isozyme. +Return a stoichiometry of the [`EnzymeConstrainedModel`](@ref). The enzymatic +reactions are split into unidirectional forward and reverse ones, each of which +may have multiple variants per isozyme. The matrix includes the virtual enzyme +balances. """ function Accessors.stoichiometry(model::EnzymeConstrainedModel) irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) @@ -105,10 +106,8 @@ end """ $(TYPEDSIGNATURES) -Return the objective of the [`EnzymeConstrainedModel`](@ref). Note, the objective is with -respect to the internal variables, i.e. [`variables(model)`](@ref), which are -the unidirectional reactions and the genes involved in enzymatic reactions that -have kinetic data. +Return the objective of the [`EnzymeConstrainedModel`](@ref). Note, by default +the objective is inferred from the underlying model. """ Accessors.objective(model::EnzymeConstrainedModel) = model.objective @@ -133,17 +132,12 @@ end """ $(TYPEDSIGNATURES) - -Returns the number of all irreversible reactions in `model` as well as the -number of gene products that take part in enzymatic reactions. """ Accessors.n_variables(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) """ $(TYPEDSIGNATURES) - -Return variable bounds for [`EnzymeConstrainedModel`](@ref). """ function Accessors.bounds(model::EnzymeConstrainedModel) lbs = [ @@ -212,9 +206,9 @@ end """ $(TYPEDSIGNATURES) -Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the coupling of the -wrapped model, coupling for split (arm) reactions, and the coupling for the total -enzyme capacity. +Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the +coupling of the wrapped model, coupling for split (arm) reactions with enzymes, +and the coupling for the total enzyme capacity. """ function Accessors.coupling(model::EnzymeConstrainedModel) innerC = coupling(model.inner) * enzyme_constrained_column_reactions(model) @@ -229,9 +223,6 @@ end """ $(TYPEDSIGNATURES) - -Count the coupling constraints in [`EnzymeConstrainedModel`](@ref) (refer to -[`coupling`](@ref) for details). """ Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = n_coupling_constraints(model.inner) + @@ -240,9 +231,6 @@ Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) - -The coupling bounds for [`EnzymeConstrainedModel`](@ref) (refer to [`coupling`](@ref) for -details). """ function Accessors.coupling_bounds(model::EnzymeConstrainedModel) (iclb, icub) = coupling_bounds(model.inner) @@ -272,8 +260,6 @@ Accessors.balance(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) - -Return the number of genes that have enzymatic constraints associated with them. """ Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) @@ -295,8 +281,6 @@ Accessors.metabolites(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) - -Return the number of metabolites, both real and pseudo, for a [`EnzymeConstrainedModel`](@ref). """ Accessors.n_metabolites(model::EnzymeConstrainedModel) = n_metabolites(model.inner) + n_genes(model) From 48fac10ed2a82d0498dba0353009304b4aefd952 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:42:25 +0100 Subject: [PATCH 196/531] fix docstring error for ObjectModel --- src/types/models/ObjectModel.jl | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 4993c1bdd..dd45100f7 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -64,9 +64,6 @@ $(TYPEDSIGNATURES) """ Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) -""" -$(TYPEDSIGNATURES) -""" Accessors.Internal.@all_variables_are_reactions ObjectModel """ From 6d171c3392b384cf3c12a1386a0d53a4839d8818 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:44:17 +0100 Subject: [PATCH 197/531] simplify docstrings for BalancedGrowthCommunityModel --- .../models/BalancedGrowthCommunityModel.jl | 21 ++----------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index 504a11cbb..fb53fe240 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -71,9 +71,6 @@ end """ $(TYPEDSIGNATURES) - -Return the number of reactions in `cm`, which is a -[`BalancedGrowthCommunityModel`](@ref). """ function Accessors.n_variables(cm::BalancedGrowthCommunityModel) num_model_reactions = sum(n_variables(m.model) for m in cm.members) @@ -98,9 +95,6 @@ end """ $(TYPEDSIGNATURES) - -Return the number of metabolites in `cm`, which is a -[`BalancedGrowthCommunityModel`](@ref). """ function Accessors.n_metabolites(cm::BalancedGrowthCommunityModel) num_model_reactions = sum(n_metabolites(m.model) for m in cm.members) @@ -121,7 +115,6 @@ Accessors.genes(cm::BalancedGrowthCommunityModel) = """ $(TYPEDSIGNATURES) -Return the balance of `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). """ Accessors.balance(cm::BalancedGrowthCommunityModel) = [ vcat([balance(m.model) .* m.abundance for m in cm.members]...) @@ -130,8 +123,6 @@ Accessors.balance(cm::BalancedGrowthCommunityModel) = [ """ $(TYPEDSIGNATURES) - -Return the number of metabolites in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). """ Accessors.n_genes(cm::BalancedGrowthCommunityModel) = sum(n_genes(m.model) for m in cm.members) @@ -139,8 +130,8 @@ Accessors.n_genes(cm::BalancedGrowthCommunityModel) = """ $(TYPEDSIGNATURES) -Return the overall stoichiometric matrix for a [`BalancedGrowthCommunityModel`](@ref), built -from the underlying models. +Return the overall stoichiometric matrix for a +[`BalancedGrowthCommunityModel`](@ref), built from the underlying models. """ function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) env_mets = get_env_mets(cm) @@ -204,8 +195,6 @@ end """ $(TYPEDSIGNATURES) - -Coupling constraint matrix for a [`BalancedGrowthCommunityModel`](@ref). """ function Accessors.coupling(cm::BalancedGrowthCommunityModel) coups = blockdiag([coupling(m.model) for m in cm.members]...) @@ -215,8 +204,6 @@ end """ $(TYPEDSIGNATURES) - -The number of coupling constraints in a [`BalancedGrowthCommunityModel`](@ref). """ Accessors.n_coupling_constraints(cm::BalancedGrowthCommunityModel) = sum(n_coupling_constraints(m.model) for m in cm.members) @@ -250,8 +237,6 @@ end """ $(TYPEDSIGNATURES) - -Returns the semantically meaningful reactions of the model. """ Accessors.reactions(cm::BalancedGrowthCommunityModel) = [ vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) @@ -261,8 +246,6 @@ Accessors.reactions(cm::BalancedGrowthCommunityModel) = [ """ $(TYPEDSIGNATURES) - -Return the semantically meaningful reactions of the model. """ Accessors.n_reactions(cm::BalancedGrowthCommunityModel) = sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 From 59c4083326a019688efcc6f88dcf49a6a814e398 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:50:40 +0100 Subject: [PATCH 198/531] add argument type --- src/types/models/MatrixModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index bedefdf48..d4f7ef1fd 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -104,7 +104,7 @@ Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float """ $(TYPEDSIGNATURES) """ -Accessors.reaction_stoichiometry(m::MatrixModel, ridx)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::MatrixModel, ridx::Int64)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) """ From 8c2a9398c46959afafa7fc121703760423b9a63a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 15 Feb 2023 23:52:40 +0100 Subject: [PATCH 199/531] fix more argument types --- src/types/models/MATModel.jl | 2 +- src/types/models/MatrixModel.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index ee16969a4..dd4e62ac8 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -179,7 +179,7 @@ end """ $(TYPEDSIGNATURES) """ -function Accessors.reaction_stoichiometry(m::MATModel, ridx)::Dict{String,Float64} +function Accessors.reaction_stoichiometry(m::MATModel, ridx::Int)::Dict{String,Float64} met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) Dict(m.mat["mets"][met_ind] => m.mat["S"][met_ind, ridx] for met_ind in met_inds) end diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index d4f7ef1fd..d134ba524 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -104,7 +104,7 @@ Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float """ $(TYPEDSIGNATURES) """ -Accessors.reaction_stoichiometry(m::MatrixModel, ridx::Int64)::Dict{String,Float64} = +Accessors.reaction_stoichiometry(m::MatrixModel, ridx::Int)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) """ From 109d0ff50cbda3037c132206c177b977c3c7709d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 10:55:23 +0100 Subject: [PATCH 200/531] ignore cartesian indices for now --- src/types/models/MATModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index dd4e62ac8..fd5dde547 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -172,7 +172,7 @@ end $(TYPEDSIGNATURES) """ function Accessors.reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} - ridx = first(indexin([rid], m.mat["rxns"])) + ridx = first(indexin([rid], m.mat["rxns"]))[1] # get the index out of the cartesian index reaction_stoichiometry(m, ridx) end From 526794c5e1e67b2d8ae8c8b73d86aa949869a4fa Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 11:02:23 +0100 Subject: [PATCH 201/531] remove all docstrings for base models --- src/types/models/HDF5Model.jl | 24 --------- src/types/models/JSONModel.jl | 81 ---------------------------- src/types/models/MATModel.jl | 60 --------------------- src/types/models/MatrixModel.jl | 36 ------------- src/types/models/ObjectModel.jl | 96 --------------------------------- src/types/models/SBMLModel.jl | 73 +------------------------ src/types/models/Serialized.jl | 6 --- 7 files changed, 1 insertion(+), 375 deletions(-) diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index b38372ac4..3fb8a20d3 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -33,17 +33,11 @@ function Accessors.precache!(model::HDF5Model)::Nothing nothing end -""" -$(TYPEDSIGNATURES) -""" function Accessors.n_variables(model::HDF5Model)::Int precache!(model) length(model.h5["reactions"]) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.variables(model::HDF5Model)::Vector{String} precache!(model) # TODO is there any reasonable method to mmap strings from HDF5? @@ -52,49 +46,31 @@ end Accessors.Internal.@all_variables_are_reactions HDF5Model -""" -$(TYPEDSIGNATURES) -""" function Accessors.n_metabolites(model::HDF5Model)::Int precache!(model) length(model.h5["metabolites"]) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.metabolites(model::HDF5Model)::Vector{String} precache!(model) read(model.h5["metabolites"]) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.stoichiometry(model::HDF5Model)::SparseMat precache!(model) h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} precache!(model) (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.balance(model::HDF5Model)::SparseVec precache!(model) h5_read_sparse(SparseVec, model.h5["balance"]) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.objective(model::HDF5Model)::SparseVec precache!(model) h5_read_sparse(SparseVec, model.h5["objective"]) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 00e57c4ee..32393c252 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -71,44 +71,23 @@ end _parse_notes(x)::Notes = _parse_annotations(x) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_variables(model::JSONModel) = length(model.rxns) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_metabolites(model::JSONModel) = length(model.mets) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_genes(model::JSONModel) = length(model.genes) -""" -$(TYPEDSIGNATURES) -""" Accessors.variables(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolites(model::JSONModel) = [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] -""" -$(TYPEDSIGNATURES) -""" Accessors.genes(model::JSONModel) = [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] Accessors.Internal.@all_variables_are_reactions JSONModel -""" -$(TYPEDSIGNATURES) -""" function Accessors.stoichiometry(model::JSONModel) rxn_ids = variables(model) met_ids = metabolites(model) @@ -145,31 +124,19 @@ function Accessors.stoichiometry(model::JSONModel) return SparseArrays.sparse(MI, RI, SV, length(met_ids), length(rxn_ids)) end -""" -$(TYPEDSIGNATURES) -""" Accessors.bounds(model::JSONModel) = ( [get(rxn, "lower_bound", -constants.default_reaction_bound) for rxn in model.rxns], [get(rxn, "upper_bound", constants.default_reaction_bound) for rxn in model.rxns], ) -""" -$(TYPEDSIGNATURES) -""" Accessors.objective(model::JSONModel) = sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in model.rxns]) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( parse_grr, get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), ) -""" -$(TYPEDSIGNATURES) -""" Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs...) = maybemap( x -> eval_grr(x; kwargs...), @@ -182,104 +149,56 @@ Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs.. Accessors.reaction_subsystem(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "subsystem", nothing) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_formula(model::JSONModel, mid::String) = maybemap(parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_charge(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "charge", 0) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_compartment(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "compartment", nothing) -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = maybemap( _parse_annotations, get(model.genes[model.gene_index[gid]], "annotation", nothing), ) -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_notes(model::JSONModel, gid::String)::Notes = maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = maybemap( _parse_annotations, get(model.rxns[model.rxn_index[rid]], "annotation", nothing), ) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_notes(model::JSONModel, rid::String)::Notes = maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = maybemap( _parse_annotations, get(model.mets[model.met_index[mid]], "annotation", nothing), ) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_notes(model::JSONModel, mid::String)::Notes = maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = model.rxns[model.rxn_index[rid]]["metabolites"] -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_name(model::JSONModel, rid::String) = get(model.rxns[model.rxn_index[rid]], "name", nothing) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_name(model::JSONModel, mid::String) = get(model.mets[model.met_index[mid]], "name", nothing) -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_name(model::JSONModel, gid::String) = get(model.genes[model.gene_index[gid]], "name", nothing) -""" -$(TYPEDSIGNATURES) -""" Accessors.model_annotations(model::JSONModel)::Annotations = get(model.json, "annotation", Annotations()) -""" -$(TYPEDSIGNATURES) -""" Accessors.model_notes(model::JSONModel)::Notes = get(model.json, "notes", Notes()) -""" -$(TYPEDSIGNATURES) -""" function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) if typeof(mm) == JSONModel return mm diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index fd5dde547..68b3be4b3 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -13,9 +13,6 @@ end Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) Accessors.n_variables(m::MATModel)::Int = size(m.mat["S"], 2) -""" -$(TYPEDSIGNATURES) -""" function Accessors.variables(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") reshape(m.mat["rxns"], n_variables(m)) @@ -26,16 +23,10 @@ end Accessors.Internal.@all_variables_are_reactions MATModel -""" -$(TYPEDSIGNATURES) -""" _mat_has_squashed_coupling(mat) = haskey(mat, "A") && haskey(mat, "b") && length(mat["b"]) == size(mat["A"], 1) -""" -$(TYPEDSIGNATURES) -""" function Accessors.metabolites(m::MATModel)::Vector{String} nm = n_metabolites(m) if haskey(m.mat, "mets") @@ -45,22 +36,13 @@ function Accessors.metabolites(m::MATModel)::Vector{String} end end -""" -$(TYPEDSIGNATURES) -""" Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) -""" -$(TYPEDSIGNATURES) -""" Accessors.bounds(m::MATModel) = ( reshape(get(m.mat, "lb", fill(-Inf, n_variables(m), 1)), n_variables(m)), reshape(get(m.mat, "ub", fill(Inf, n_variables(m), 1)), n_variables(m)), ) -""" -$(TYPEDSIGNATURES) -""" function Accessors.balance(m::MATModel) b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) if _mat_has_squashed_coupling(m.mat) @@ -69,22 +51,13 @@ function Accessors.balance(m::MATModel) sparse(reshape(b, n_metabolites(m))) end -""" -$(TYPEDSIGNATURES) -""" Accessors.objective(m::MATModel) = sparse(reshape(get(m.mat, "c", zeros(n_variables(m), 1)), n_variables(m))) -""" -$(TYPEDSIGNATURES) -""" Accessors.coupling(m::MATModel) = _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_variables(m)+1:end, :]) : sparse(get(m.mat, "C", zeros(0, n_variables(m)))) -""" -$(TYPEDSIGNATURES) -""" function Accessors.coupling_bounds(m::MATModel) nc = n_coupling_constraints(m) if _mat_has_squashed_coupling(m.mat) @@ -100,17 +73,11 @@ function Accessors.coupling_bounds(m::MATModel) end end -""" -$(TYPEDSIGNATURES) -""" function Accessors.genes(m::MATModel) x = get(m.mat, "genes", []) reshape(x, length(x)) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.reaction_gene_associations(m::MATModel, rid::String) if haskey(m.mat, "grRules") grr = m.mat["grRules"][findfirst(==(rid), variables(m))] @@ -120,9 +87,6 @@ function Accessors.reaction_gene_associations(m::MATModel, rid::String) end end -""" -$(TYPEDSIGNATURES) -""" function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwargs...) if haskey(m.mat, "grRules") grr = m.mat["grRules"][findfirst(==(rid), variables(m))] @@ -132,17 +96,11 @@ function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwar end end -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( x -> parse_formula(x[findfirst(==(mid), metabolites(m))]), gets(m.mat, nothing, constants.keynames.metformulas), ) -""" -$(TYPEDSIGNATURES) -""" function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} met_charge = maybemap( x -> x[findfirst(==(mid), metabolites(m))], @@ -151,9 +109,6 @@ function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} maybemap(Int, isnan(met_charge) ? nothing : met_charge) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.metabolite_compartment(m::MATModel, mid::String) res = maybemap( x -> x[findfirst(==(mid), metabolites(m))], @@ -168,33 +123,21 @@ function Accessors.metabolite_compartment(m::MATModel, mid::String) ) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} ridx = first(indexin([rid], m.mat["rxns"]))[1] # get the index out of the cartesian index reaction_stoichiometry(m, ridx) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.reaction_stoichiometry(m::MATModel, ridx::Int)::Dict{String,Float64} met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) Dict(m.mat["mets"][met_ind] => m.mat["S"][met_ind, ridx] for met_ind in met_inds) end -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_name(m::MATModel, rid::String) = maybemap( x -> x[findfirst(==(rid), variables(m))], gets(m.mat, nothing, constants.keynames.rxnnames), ) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( x -> x[findfirst(==(mid), metabolites(m))], gets(m.mat, nothing, constants.keynames.metnames), @@ -207,9 +150,6 @@ Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( # here are very likely completely incompatible with >50% of the MATLAB models # out there. -""" -$(TYPEDSIGNATURES) -""" function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) if typeof(m) == MATModel return m diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index d134ba524..ab03ae587 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -47,41 +47,20 @@ mutable struct MatrixModel <: AbstractMetabolicModel end end -""" -$(TYPEDSIGNATURES) -""" Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns Accessors.Internal.@all_variables_are_reactions MatrixModel -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets -""" -$(TYPEDSIGNATURES) -""" Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S -""" -$(TYPEDSIGNATURES) -""" Accessors.bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) -""" -$(TYPEDSIGNATURES) -""" Accessors.balance(a::MatrixModel)::SparseVec = a.b -""" -$(TYPEDSIGNATURES) -""" Accessors.objective(a::MatrixModel)::SparseVec = a.c -""" -$(TYPEDSIGNATURES) -""" function Accessors.genes(a::MatrixModel)::Vector{String} res = Set{String}() for grr in a.grrs @@ -95,37 +74,22 @@ function Accessors.genes(a::MatrixModel)::Vector{String} sort(collect(res)) end -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_stoichiometry(m::MatrixModel, ridx::Int)::Dict{String,Float64} = Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_gene_associations( model::MatrixModel, ridx::Int, )::Maybe{GeneAssociationsDNF} = model.grrs[ridx] -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_gene_associations( model::MatrixModel, rid::String, )::Maybe{GeneAssociationsDNF} = model.grrs[first(indexin([rid], model.rxns))] -""" -$(TYPEDSIGNATURES) -""" function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicModel} if typeof(m) == MatrixModel return m diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index dd45100f7..5847313c6 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -54,41 +54,20 @@ end # AbstractMetabolicModel interface follows -""" -$(TYPEDSIGNATURES) -""" Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) Accessors.Internal.@all_variables_are_reactions ObjectModel -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) -""" -$(TYPEDSIGNATURES) -""" Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) -""" -$(TYPEDSIGNATURES) -""" function Accessors.stoichiometry(model::ObjectModel)::SparseMat n_entries = 0 for (_, r) in model.reactions @@ -125,26 +104,14 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) end -""" -$(TYPEDSIGNATURES) -""" Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) -""" -$(TYPEDSIGNATURES) -""" Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) -""" -$(TYPEDSIGNATURES) -""" Accessors.objective(model::ObjectModel)::SparseVec = sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) -""" -$(TYPEDSIGNATURES) -""" function Accessors.reaction_gene_associations( model::ObjectModel, id::String, @@ -156,123 +123,60 @@ function Accessors.reaction_gene_associations( ] end -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_formula(model::ObjectModel, id::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.metabolites[id].formula) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_charge(model::ObjectModel, id::String)::Maybe{Int} = model.metabolites[id].charge -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_compartment(model::ObjectModel, id::String)::Maybe{String} = model.metabolites[id].compartment -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_subsystem(model::ObjectModel, id::String)::Maybe{String} = model.reactions[id].subsystem -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.metabolites[id].notes -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.metabolites[id].annotations -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_notes(model::ObjectModel, gid::String) = model.genes[gid].notes -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.genes[id].annotations -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_notes(model::ObjectModel, id::String)::Maybe{Notes} = model.reactions[id].notes -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = model.reactions[id].annotations -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_stoichiometry(m::ObjectModel, rid::String)::Dict{String,Float64} = m.reactions[rid].metabolites -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_name(m::ObjectModel, rid::String) = m.reactions[rid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_name(m::ObjectModel, mid::String) = m.metabolites[mid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = model.genes[gid].product_molar_mass -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_isozymes(model::ObjectModel, rid::String) = model.reactions[rid].gene_associations -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = model.genes[gid].product_lower_bound -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = model.genes[gid].product_upper_bound -""" -$(TYPEDSIGNATURES) -""" Accessors.model_notes(model::ObjectModel)::Notes = model.notes -""" -$(TYPEDSIGNATURES) -""" Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations -""" -$(TYPEDSIGNATURES) -""" function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) if typeof(model) == ObjectModel return model diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 48a163369..a4933e6b0 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -38,21 +38,12 @@ function SBMLModel(sbml::SBML.Model, active_objective::String = "") ) end -""" -$(TYPEDSIGNATURES) -""" Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids Accessors.Internal.@all_variables_are_reactions SBMLModel -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids -""" -$(TYPEDSIGNATURES) -""" function Accessors.stoichiometry(model::SBMLModel)::SparseMat # find the vector size for preallocation @@ -90,9 +81,6 @@ function Accessors.stoichiometry(model::SBMLModel)::SparseMat return sparse(Rows, Cols, Vals, n_metabolites(model), n_reactions(model)) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} # There are multiple ways in SBML to specify a lower/upper bound. There are # the "global" model bounds that we completely ignore now because no one @@ -146,14 +134,8 @@ function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float6 ) end -""" -$(TYPEDSIGNATURES) -""" Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) -""" -$(TYPEDSIGNATURES) -""" function Accessors.objective(model::SBMLModel)::SparseVec res = sparsevec([], [], n_reactions(model)) @@ -176,44 +158,26 @@ function Accessors.objective(model::SBMLModel)::SparseVec return res end -""" -$(TYPEDSIGNATURES) -""" Accessors.genes(model::SBMLModel)::Vector{String} = model.gene_ids -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_gene_associations( model::SBMLModel, rid::String, )::Maybe{GeneAssociationsDNF} = maybemap(parse_grr, model.sbml.reactions[rid].gene_product_association) -""" -$(TYPEDSIGNATURES) -""" Accessors.eval_reaction_gene_association(model::SBMLModel, rid::String; kwargs...) = maybemap( x -> eval_grr(x; kwargs...), model.sbml.reactions[rid].gene_product_association, ) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = maybemap(parse_formula, model.sbml.species[mid].formula) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_compartment(model::SBMLModel, mid::String) = model.sbml.species[mid].compartment -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = model.sbml.species[mid].charge @@ -266,9 +230,7 @@ function _sbml_export_notes(notes::Notes)::Maybe{String} nothing end -""" -$(TYPEDSIGNATURES) -""" + function Accessors.reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} s = Dict{String,Float64}() default1(x) = isnothing(x) ? 1 : x @@ -281,70 +243,37 @@ function Accessors.reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{Strin return s end -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_annotations(model::SBMLModel, rid::String) = _sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_annotations(model::SBMLModel, mid::String) = _sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms) -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( model.sbml.gene_products[gid].sbo, model.sbml.gene_products[gid].cv_terms, ) -""" -$(TYPEDSIGNATURES) -""" Accessors.reaction_notes(model::SBMLModel, rid::String) = _sbml_import_notes(model.sbml.reactions[rid].notes) -""" -$(TYPEDSIGNATURES) -""" Accessors.metabolite_notes(model::SBMLModel, mid::String) = _sbml_import_notes(model.sbml.species[mid].notes) -""" -$(TYPEDSIGNATURES) -""" Accessors.gene_notes(model::SBMLModel, gid::String) = _sbml_import_notes(model.sbml.gene_products[gid].notes) Accessors.model_annotations(model::SBMLModel) = _sbml_import_cvterms(model.sbml.sbo, model.sbml.cv_terms) -""" -$(TYPEDSIGNATURES) -""" Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.sbml.notes) -""" -$(TYPEDSIGNATURES) -""" function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) if typeof(mm) == SBMLModel return mm diff --git a/src/types/models/Serialized.jl b/src/types/models/Serialized.jl index 1d03aa95a..ce2ee318c 100644 --- a/src/types/models/Serialized.jl +++ b/src/types/models/Serialized.jl @@ -18,17 +18,11 @@ mutable struct Serialized{M} <: AbstractModelWrapper where {M<:AbstractMetabolic new{T}(model, filename) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.unwrap_model(m::Serialized) precache!(m) m.m end -""" -$(TYPEDSIGNATURES) -""" function Accessors.precache!(model::Serialized)::Nothing if isnothing(model.m) model.m = deserialize(model.filename) From 99517f0d032c1e830ee226a4d637ada2c8ff2a89 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 11:12:43 +0100 Subject: [PATCH 202/531] get rid of docstrings in wrapped models --- src/types/wrappers/EnzymeConstrainedModel.jl | 89 ++++--------------- src/types/wrappers/MatrixCoupling.jl | 17 ---- src/types/wrappers/MaxMinDrivingForceModel.jl | 42 ++------- .../SimplifiedEnzymeConstrainedModel.jl | 67 ++++---------- 4 files changed, 41 insertions(+), 174 deletions(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 09b5d5b91..65745d37d 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -40,17 +40,20 @@ A model with complex enzyme concentration and capacity bounds, as described in genome-scale metabolic model by incorporating enzymatic constraints." Molecular systems biology 13.8 (2017): 935.* -Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constraints`](@ref) to construct this kind -of model. +Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constraints`](@ref) +to construct this kind of model. The model wraps another "internal" model, and adds following modifications: - enzymatic reactions with known enzyme information are split into multiple - forward and reverse variants for each isozyme, + forward and reverse variants for each isozyme (affects the stoichiometric + matrix), +- each enzyme reaction may have multiple variants per isozyme, thus the + stoichiometric matrix will include all these virtual enzyme balances, - reaction coupling is added to ensure the groups of isozyme reactions obey the - global reaction flux bounds from the original model, -- gene concentrations specified by each reaction and its gene product stoichiometry, - can constrained by the user to reflect measurements, such as - from mass spectrometry, + global reaction flux bounds from the original model (affects the coupling), +- gene concentrations specified by each reaction and its gene product + stoichiometry, can constrained by the user to reflect measurements, such as + from mass spectrometry (affects the simple bounds), - additional coupling is added to simulate total masses of different proteins grouped by type (e.g., membrane-bound and free-floating proteins), which can be again constrained by the user (this is slightly generalized from original @@ -60,11 +63,11 @@ The model wraps another "internal" model, and adds following modifications: The structure contains fields `columns` that describe the contents of the stoichiometry matrix columns, `coupling_row_reaction`, `coupling_row_gene_product` and `coupling_row_mass_group` that describe -correspondence of the coupling rows to original model and determine the -coupling bounds (note: the coupling for gene product is actually added to -stoichiometry, not in [`coupling`](@ref)), and `inner`, which is the original -wrapped model. The `objective` of the model includes also the extra columns for -individual genes, as held by `coupling_row_gene_product`. +correspondence of the coupling rows to original model and determine the coupling +bounds (note: the coupling for gene product is actually added to stoichiometry, +not in [`coupling`](@ref)), and `inner`, which is the original wrapped model. +The `objective` of the model includes also the extra columns for individual +genes, as held by `coupling_row_gene_product`. Implementation exposes the split reactions (available as `variables(model)`), but retains the original "simple" reactions accessible by [`reactions`](@ref). @@ -86,14 +89,6 @@ end Accessors.unwrap_model(model::EnzymeConstrainedModel) = model.inner -""" -$(TYPEDSIGNATURES) - -Return a stoichiometry of the [`EnzymeConstrainedModel`](@ref). The enzymatic -reactions are split into unidirectional forward and reverse ones, each of which -may have multiple variants per isozyme. The matrix includes the virtual enzyme -balances. -""" function Accessors.stoichiometry(model::EnzymeConstrainedModel) irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) enzS = enzyme_constrained_gene_product_coupling(model) @@ -103,21 +98,8 @@ function Accessors.stoichiometry(model::EnzymeConstrainedModel) ] end -""" -$(TYPEDSIGNATURES) - -Return the objective of the [`EnzymeConstrainedModel`](@ref). Note, by default -the objective is inferred from the underlying model. -""" Accessors.objective(model::EnzymeConstrainedModel) = model.objective -""" -$(TYPEDSIGNATURES) - -Returns the internal reactions in a [`EnzymeConstrainedModel`](@ref) (these may be split -to forward- and reverse-only parts with different isozyme indexes; reactions -IDs are mangled accordingly with suffixes). -""" function Accessors.variables(model::EnzymeConstrainedModel) inner_reactions = variables(model.inner) mangled_reactions = [ @@ -130,15 +112,9 @@ function Accessors.variables(model::EnzymeConstrainedModel) [mangled_reactions; genes(model)] end -""" -$(TYPEDSIGNATURES) -""" Accessors.n_variables(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) -""" -$(TYPEDSIGNATURES) -""" function Accessors.bounds(model::EnzymeConstrainedModel) lbs = [ [col.lb for col in model.columns] @@ -203,13 +179,6 @@ function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) ) end -""" -$(TYPEDSIGNATURES) - -Return the coupling of [`EnzymeConstrainedModel`](@ref). That combines the -coupling of the wrapped model, coupling for split (arm) reactions with enzymes, -and the coupling for the total enzyme capacity. -""" function Accessors.coupling(model::EnzymeConstrainedModel) innerC = coupling(model.inner) * enzyme_constrained_column_reactions(model) rxnC = enzyme_constrained_reaction_coupling(model) @@ -221,17 +190,11 @@ function Accessors.coupling(model::EnzymeConstrainedModel) ] end -""" -$(TYPEDSIGNATURES) -""" Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = n_coupling_constraints(model.inner) + length(model.coupling_row_reaction) + length(model.coupling_row_mass_group) -""" -$(TYPEDSIGNATURES) -""" function Accessors.coupling_bounds(model::EnzymeConstrainedModel) (iclb, icub) = coupling_bounds(model.inner) (ilb, iub) = bounds(model.inner) @@ -249,38 +212,16 @@ function Accessors.coupling_bounds(model::EnzymeConstrainedModel) ) end -""" -$(TYPEDSIGNATURES) - -Return the balance of the reactions in the inner model, concatenated with a vector of -zeros representing the enzyme balance of a [`EnzymeConstrainedModel`](@ref). -""" Accessors.balance(model::EnzymeConstrainedModel) = [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] -""" -$(TYPEDSIGNATURES) -""" Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) -""" -$(TYPEDSIGNATURES) - -Return the gene ids of genes that have enzymatic constraints associated with them. -""" Accessors.genes(model::EnzymeConstrainedModel) = genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] -""" -$(TYPEDSIGNATURES) - -Return the ids of all metabolites, both real and pseudo, for a [`EnzymeConstrainedModel`](@ref). -""" Accessors.metabolites(model::EnzymeConstrainedModel) = [metabolites(model.inner); genes(model) .* "#enzyme_constrained"] -""" -$(TYPEDSIGNATURES) -""" Accessors.n_metabolites(model::EnzymeConstrainedModel) = n_metabolites(model.inner) + n_genes(model) diff --git a/src/types/wrappers/MatrixCoupling.jl b/src/types/wrappers/MatrixCoupling.jl index ae9946eff..ac982ab58 100644 --- a/src/types/wrappers/MatrixCoupling.jl +++ b/src/types/wrappers/MatrixCoupling.jl @@ -32,33 +32,16 @@ mutable struct MatrixCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetab end end -""" -$(TYPEDSIGNATURES) -""" Accessors.unwrap_model(a::MatrixCoupling) = a.lm -""" -$(TYPEDSIGNATURES) -""" Accessors.coupling(a::MatrixCoupling)::SparseMat = vcat(coupling(a.lm), a.C) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_coupling_constraints(a::MatrixCoupling)::Int = n_coupling_constraints(a.lm) + size(a.C, 1) -""" -$(TYPEDSIGNATURES) -""" Accessors.coupling_bounds(a::MatrixCoupling)::Tuple{Vector{Float64},Vector{Float64}} = vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) -""" -$(TYPEDSIGNATURES) - -Make a `MatrixCoupling` out of any compatible model type. -""" function Base.convert( ::Type{MatrixCoupling{M}}, mm::AbstractMetabolicModel; diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/types/wrappers/MaxMinDrivingForceModel.jl index 8c3e8c737..9736db085 100644 --- a/src/types/wrappers/MaxMinDrivingForceModel.jl +++ b/src/types/wrappers/MaxMinDrivingForceModel.jl @@ -7,10 +7,13 @@ thermodynamics highlights kinetic obstacles in central metabolism.", PLoS computational biology, 2014. When [`flux_balance_analysis`](@ref) is called in this type of model, the -max-min driving force algorithm is solved. It returns the Gibbs free energy of -the reactions, the concentrations of metabolites, and the actual maximum minimum -driving force across all reactions in the model. The optimization problem solved -is: +max-min driving force algorithm is solved i.e. the objective of the model is to +find the maximum minimum Gibbs free energy of reaction across all reactions in +the model by changing the metabolite concentrations. The variables for max-min +driving force analysis are the actual maximum minimum driving force of the +model, the log metabolite concentrations, and the gibbs free energy reaction +potentials across each reaction. Reaction fluxes are assumed constant.The +optimization problem solved is: ``` max min -ΔᵣG s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) @@ -78,19 +81,9 @@ end Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner -""" -$(TYPEDSIGNATURES) - -The variables for max-min driving force analysis are the actual maximum minimum -driving force of the model, the log metabolite concentrations, and the gibbs -free energy reaction potentials across each reaction. -""" Accessors.variables(model::MaxMinDrivingForceModel) = ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] -""" -$(TYPEDSIGNATURES) -""" Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + n_reactions(model) @@ -111,18 +104,9 @@ Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) -""" -$(TYPEDSIGNATURES) - -For this kind of the model, the objective is the the max-min-driving force which -is at index 1 in the variables. -""" Accessors.objective(model::MaxMinDrivingForceModel) = [1.0; fill(0.0, n_variables(model) - 1)] -""" -$(TYPEDSIGNATURES) -""" function Accessors.balance(model::MaxMinDrivingForceModel) # proton water balance num_proton_water = length(model.proton_ids) + length(model.water_ids) @@ -148,9 +132,6 @@ function Accessors.balance(model::MaxMinDrivingForceModel) ] end -""" -$(TYPEDSIGNATURES) -""" function Accessors.stoichiometry(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) @@ -197,9 +178,6 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) ] end -""" -$(TYPEDSIGNATURES) -""" function Accessors.bounds(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) @@ -224,9 +202,6 @@ function Accessors.bounds(model::MaxMinDrivingForceModel) return (lbs, ubs) end -""" -$(TYPEDSIGNATURES) -""" function Accessors.coupling(model::MaxMinDrivingForceModel) # only constrain reactions that have thermo data @@ -255,9 +230,6 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) ] end -""" -$(TYPEDSIGNATURES) -""" function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) n = length(Internal.active_reaction_ids(model)) neg_dg_lb = fill(-model.max_dg_bound, n) diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 6da42aa9f..3a2b48a07 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -22,34 +22,37 @@ An enzyme-capacity-constrained model using sMOMENT algorithm, as described by *Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of metabolic models with enzyme constraints" BMC bioinformatics, 2020*. -Use [`make_simplified_enzyme_constrained_model`](@ref) or [`with_simplified_enzyme_constraints`](@ref) to construct the -models. +Use [`make_simplified_enzyme_constrained_model`](@ref) or +[`with_simplified_enzyme_constraints`](@ref) to construct the models. The model is constructed as follows: - stoichiometry of the original model is retained as much as possible, but enzymatic reations are split into forward and reverse parts (marked by a suffix like `...#forward` and `...#reverse`), -- coupling is added to simulate a virtual metabolite "enzyme capacity", which - is consumed by all enzymatic reactions at a rate given by enzyme mass divided - by the corresponding kcat, +- coupling is added to simulate a virtual metabolite "enzyme capacity", which is + consumed by all enzymatic reactions at a rate given by enzyme mass divided by + the corresponding kcat, - the total consumption of the enzyme capacity is constrained to a fixed maximum. -The `SimplifiedEnzymeConstrainedModel` structure contains a worked-out representation of the -optimization problem atop a wrapped [`AbstractMetabolicModel`](@ref), in particular the -separation of certain reactions into unidirectional forward and reverse parts, -an "enzyme capacity" required for each reaction, and the value of the maximum -capacity constraint. Original coupling in the inner model is retained. +The `SimplifiedEnzymeConstrainedModel` structure contains a worked-out +representation of the optimization problem atop a wrapped +[`AbstractMetabolicModel`](@ref), in particular the separation of certain +reactions into unidirectional forward and reverse parts (which changes the +stoichiometric matrix), an "enzyme capacity" required for each reaction, and the +value of the maximum capacity constraint. Original coupling in the inner model +is retained. -In the structure, the field `columns` describes the correspondence of stoichiometry -columns to the stoichiometry and data of the internal wrapped model, and -`total_enzyme_capacity` is the total bound on the enzyme capacity consumption -as specified in sMOMENT algorithm. +In the structure, the field `columns` describes the correspondence of +stoichiometry columns to the stoichiometry and data of the internal wrapped +model, and `total_enzyme_capacity` is the total bound on the enzyme capacity +consumption as specified in sMOMENT algorithm. This implementation allows easy access to fluxes from the split reactions (available in `variables(model)`), while the original "simple" reactions from -the wrapped model are retained as [`reactions`](@ref). All additional constraints -are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). +the wrapped model are retained as [`reactions`](@ref). All additional +constraints are implemented using [`coupling`](@ref) and +[`coupling_bounds`](@ref). # Fields $(TYPEDFIELDS) @@ -63,24 +66,12 @@ end Accessors.unwrap_model(model::SimplifiedEnzymeConstrainedModel) = model.inner -""" -$(TYPEDSIGNATURES) - -Return a stoichiometry of the [`SimplifiedEnzymeConstrainedModel`](@ref). The enzymatic reactions -are split into unidirectional forward and reverse ones. -""" Accessors.stoichiometry(model::SimplifiedEnzymeConstrainedModel) = stoichiometry(model.inner) * simplified_enzyme_constrained_column_reactions(model) -""" -$(TYPEDSIGNATURES) -""" Accessors.objective(model::SimplifiedEnzymeConstrainedModel) = simplified_enzyme_constrained_column_reactions(model)' * objective(model.inner) -""" -$(TYPEDSIGNATURES) -""" Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = let inner_reactions = variables(model.inner) [ @@ -91,14 +82,8 @@ Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = ] end -""" -$(TYPEDSIGNATURES) -""" Accessors.n_variables(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) -""" -$(TYPEDSIGNATURES) -""" Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) @@ -127,28 +112,14 @@ Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = reaction_variables_matrix(model), ) # TODO currently inefficient -""" -$(TYPEDSIGNATURES) - -Return the coupling of a [`SimplifiedEnzymeConstrainedModel`](@ref). This -combines the coupling of the wrapped model, coupling for split reactions, and -the coupling for the total enzyme capacity, which is added as a -[`coupling_bounds`](@ref). -""" Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) = vcat( coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model), [col.capacity_required for col in model.columns]', ) -""" -$(TYPEDSIGNATURES) -""" Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = n_coupling_constraints(model.inner) + 1 -""" -$(TYPEDSIGNATURES) -""" Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = let (iclb, icub) = coupling_bounds(model.inner) (vcat(iclb, [0.0]), vcat(icub, [model.total_enzyme_capacity])) From bc0af0b96d875ed52f476a933cbde0f548a941d1 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 11:29:15 +0100 Subject: [PATCH 203/531] remove docstrings for BalancedGrowthCommunityModel --- .../models/BalancedGrowthCommunityModel.jl | 96 ++++--------------- 1 file changed, 18 insertions(+), 78 deletions(-) diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/BalancedGrowthCommunityModel.jl index fb53fe240..7e9ed531c 100644 --- a/src/types/models/BalancedGrowthCommunityModel.jl +++ b/src/types/models/BalancedGrowthCommunityModel.jl @@ -42,6 +42,23 @@ $(TYPEDFIELDS) 3. The objective created by this model is the equal growth rate/balanced growth objective. In short, all biomass metabolites are produced at the same rate. 4. The flux units are `mmol X/gDW_total/h` for some metabolite `X`. + +# Implementation notes +1. All reactions have the `id` of each respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. + Consequently, exchange reactions of the original model will look like + `species1#EX_...`. All exchange environmental reactions have `EX_` as a + prefix followed by the environmental metabolite id. +2. All metabolites have the `id` of each respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The + environmental metabolites have no prefix. +3. All genes have the `id` of the respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. +4. Each bound from the underlying community members is multiplied by the + abundance of that member. +5. This objective is assumed to be the equal growth rate/balanced growth + objective. Consequently, the relation `community_growth * + abundance_species_i = growth_species_i` should hold. """ Base.@kwdef mutable struct BalancedGrowthCommunityModel <: AbstractMetabolicModel "Models making up the community." @@ -53,25 +70,12 @@ Base.@kwdef mutable struct BalancedGrowthCommunityModel <: AbstractMetabolicMode Dict{String,Tuple{Float64,Float64}}() end -""" -$(TYPEDSIGNATURES) - -Return the reactions in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). -All reactions have the `id` of each respective underlying -[`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -Consequently, exchange reactions of the original model will look like -`species1#EX_...`. All exchange environmental reactions have `EX_` as a prefix -followed by the environmental metabolite id. -""" function Accessors.variables(cm::BalancedGrowthCommunityModel) rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] return [rxns; env_exs; cm.objective_id] end -""" -$(TYPEDSIGNATURES) -""" function Accessors.n_variables(cm::BalancedGrowthCommunityModel) num_model_reactions = sum(n_variables(m.model) for m in cm.members) # assume each env metabolite gets an env exchange @@ -79,23 +83,12 @@ function Accessors.n_variables(cm::BalancedGrowthCommunityModel) return num_model_reactions + num_env_metabolites + 1 # add 1 for the community biomass end -""" -$(TYPEDSIGNATURES) - -Return the metabolites in `cm`, which is a -[`BalancedGrowthCommunityModel`](@ref). All metabolites have the `id` of each -respective underlying [`CommunityMember`](@ref) appended as a prefix with the -delimiter `#`. The environmental metabolites have no prefix. -""" function Accessors.metabolites(cm::BalancedGrowthCommunityModel) mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] return [mets; "ENV_" .* get_env_mets(cm)] end -""" -$(TYPEDSIGNATURES) -""" function Accessors.n_metabolites(cm::BalancedGrowthCommunityModel) num_model_reactions = sum(n_metabolites(m.model) for m in cm.members) # assume each env metabolite gets an env exchange @@ -103,36 +96,17 @@ function Accessors.n_metabolites(cm::BalancedGrowthCommunityModel) return num_model_reactions + num_env_metabolites end -""" -$(TYPEDSIGNATURES) - -Return the genes in `cm`, which is a [`BalancedGrowthCommunityModel`](@ref). All -genes have the `id` of the respective underlying [`CommunityMember`](@ref) -appended as a prefix with the delimiter `#`. -""" Accessors.genes(cm::BalancedGrowthCommunityModel) = [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] -""" -$(TYPEDSIGNATURES) -""" Accessors.balance(cm::BalancedGrowthCommunityModel) = [ vcat([balance(m.model) .* m.abundance for m in cm.members]...) spzeros(length(get_env_mets(cm))) ] -""" -$(TYPEDSIGNATURES) -""" Accessors.n_genes(cm::BalancedGrowthCommunityModel) = sum(n_genes(m.model) for m in cm.members) -""" -$(TYPEDSIGNATURES) - -Return the overall stoichiometric matrix for a -[`BalancedGrowthCommunityModel`](@ref), built from the underlying models. -""" function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) env_mets = get_env_mets(cm) @@ -155,14 +129,6 @@ function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) ] end -""" -$(TYPEDSIGNATURES) - -Returns the simple variable bounds on `cm`. Assumes the objective can only go -forward at maximum rate `constants.default_reaction_bound`. Note, each bound -from the underlying community members is multiplied by the abundance of that -member. -""" function Accessors.bounds(cm::BalancedGrowthCommunityModel) models_lbs = vcat([first(bounds(m.model)) .* m.abundance for m in cm.members]...) models_ubs = vcat([last(bounds(m.model)) .* m.abundance for m in cm.members]...) @@ -180,41 +146,21 @@ function Accessors.bounds(cm::BalancedGrowthCommunityModel) ) end -""" -$(TYPEDSIGNATURES) - -Returns the objective of `cm`. This objective is assumed to be the equal growth -rate/balanced growth objective. Consequently, the relation `community_growth * -abundance_species_i = growth_species_i` should hold. -""" function Accessors.objective(cm::BalancedGrowthCommunityModel) vec = spzeros(n_variables(cm)) vec[end] = 1.0 return vec end -""" -$(TYPEDSIGNATURES) -""" function Accessors.coupling(cm::BalancedGrowthCommunityModel) coups = blockdiag([coupling(m.model) for m in cm.members]...) n = n_variables(cm) return [coups spzeros(size(coups, 1), n - size(coups, 2))] end -""" -$(TYPEDSIGNATURES) -""" Accessors.n_coupling_constraints(cm::BalancedGrowthCommunityModel) = sum(n_coupling_constraints(m.model) for m in cm.members) -""" -$(TYPEDSIGNATURES) - -Coupling bounds for a [`BalancedGrowthCommunityModel`](@ref). Note, each bound -from the underlying community members is multiplied by the abundance of that -member. -""" function Accessors.coupling_bounds(cm::BalancedGrowthCommunityModel) lbs = vcat([first(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) ubs = vcat([last(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) @@ -235,18 +181,12 @@ function Accessors.reaction_variables_matrix(cm::BalancedGrowthCommunityModel) blockdiag(rfs, spdiagm(fill(1, nr))) end -""" -$(TYPEDSIGNATURES) -""" Accessors.reactions(cm::BalancedGrowthCommunityModel) = [ vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) ["EX_" * env_met for env_met in get_env_mets(cm)] cm.objective_id ] -""" -$(TYPEDSIGNATURES) -""" Accessors.n_reactions(cm::BalancedGrowthCommunityModel) = sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 @@ -273,7 +213,7 @@ for (func, def) in ( (:metabolite_name, nothing), (:gene_name, nothing), ) - @eval begin # TODO add docstrings somehow + @eval begin Accessors.$func(cm::BalancedGrowthCommunityModel, id::String) = access_community_member(cm, id, $func; default = $def) end From e259c184f733c5ddfdfc650531d6ef3f52e95997 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 12:14:52 +0100 Subject: [PATCH 204/531] slightly improve docstrings for AbstractMetabolicModel --- src/types/accessors/AbstractMetabolicModel.jl | 21 +++++++------------ 1 file changed, 8 insertions(+), 13 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 0fdbdb543..50295caec 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -12,14 +12,15 @@ """ $(TYPEDSIGNATURES) -Return a vector of reaction identifiers in a model. The vector precisely +Return a vector of variable identifiers in a model. The vector precisely corresponds to the columns in [`stoichiometry`](@ref) matrix. -For technical reasons, the "reactions" may sometimes not be true reactions but -various virtual and helper pseudo-reactions that are used in the metabolic -modeling, such as metabolite exchanges, separate forward and reverse reactions, -supplies of enzymatic and genetic material and virtual cell volume, etc. To -simplify the view of the model contents use [`reaction_variables`](@ref). +Usually, variables correspond to reactions. However, for technical reasons, the +reactions may sometimes not be true reactions, but various virtual and helper +pseudo-reactions that are used in the metabolic modeling, such as metabolite +exchanges, separate forward and reverse reactions, supplies of enzymatic and +genetic material and virtual cell volume, etc. To simplify the view of the model +contents use [`reaction_variables`](@ref). """ function variables(a::AbstractMetabolicModel)::Vector{String} missing_impl_error(variables, (a,)) @@ -59,13 +60,7 @@ end """ $(TYPEDSIGNATURES) -Get the sparse stoichiometry matrix of a model. A feasible solution `x` of a -model `m` is defined as satisfying the equations: - -- `stoichiometry(m) * x .== balance(m)` -- `x .>= lbs` -- `y .<= ubs` -- `(lbs, ubs) == bounds(m) +Get the sparse stoichiometry matrix of a model. """ function stoichiometry(a::AbstractMetabolicModel)::SparseMat missing_impl_error(stoichiometry, (a,)) From 0acdf62754cbf61cf2d8d43734400d5584e4da33 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 20:23:04 +0100 Subject: [PATCH 205/531] rather map through molar masses --- src/types/wrappers/EnzymeConstrainedModel.jl | 4 ++-- test/reconstruction/enzyme_constrained.jl | 15 +++++++++++---- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index bddfe2a0f..ca8cbbd67 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -189,10 +189,10 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = $(TYPEDSIGNATURES) Get a mapping of enzyme variables to variables -- for enzyme constrained models, -this is just a direct mapping. +this is proportional to the molar mass of each gene product. """ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = - Dict(gid => Dict(gid => 1.0) for gid in genes(model)) # this is enough for all the semantics to work + Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) # this is enough for all the semantics to work """ $(TYPEDSIGNATURES) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 3702e313f..18f3dcb06 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -60,11 +60,18 @@ atol = TEST_TOLERANCE, ) - prot_mass = sum(ecoli_core_gene_product_masses[gid] * c for (gid, c) in prot_concens) mass_groups = values_dict(:enzyme_group, gm, opt_model) - @test isapprox(prot_mass, total_gene_product_mass, atol = TEST_TOLERANCE) - @test isapprox(prot_mass, mass_groups["uncategorized"], atol = TEST_TOLERANCE) + @test isapprox( + sum(values(prot_concens)), + total_gene_product_mass, + atol = TEST_TOLERANCE, + ) + @test isapprox( + sum(values(prot_concens)), + mass_groups["uncategorized"], + atol = TEST_TOLERANCE, + ) # test enzyme objective growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 @@ -147,7 +154,7 @@ end mass_groups = values_dict(:enzyme_group, gm, opt_model) @test isapprox(rxn_fluxes["r6"], 1.1688888886502442, atol = TEST_TOLERANCE) - @test isapprox(gene_products["g4"], 0.02666666666304931, atol = TEST_TOLERANCE) + @test isapprox(gene_products["g4"], 0.02666666666304931 * 4.0, atol = TEST_TOLERANCE) @test isapprox(mass_groups["uncategorized"], 0.5, atol = TEST_TOLERANCE) @test isapprox(mass_groups["bound2"], 0.04, atol = TEST_TOLERANCE) @test length(genes(gm)) == 4 From 6346ac713f44a961ddf840a9b1e70af56366e4ef Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 17 Feb 2023 13:27:09 +0100 Subject: [PATCH 206/531] Update docstring for clarity --- src/types/wrappers/EnzymeConstrainedModel.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index ca8cbbd67..92e1f9685 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -189,7 +189,9 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = $(TYPEDSIGNATURES) Get a mapping of enzyme variables to variables -- for enzyme constrained models, -this is proportional to the molar mass of each gene product. +this is proportional to the molar mass of each gene product. Effectively the +concentration of each enzyme is then reported in mass units relative to the dry +cell mass. """ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) # this is enough for all the semantics to work From 50f617169ad1e8395cb1abfe807a8b8d905981e1 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 17 Feb 2023 13:28:06 +0100 Subject: [PATCH 207/531] more docstrings --- src/types/wrappers/EnzymeConstrainedModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 92e1f9685..377ff19e2 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -176,7 +176,7 @@ end $(TYPEDSIGNATURES) Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to -the original fluxes in the wrapped model +the original fluxes in the wrapped model. """ Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( @@ -199,7 +199,7 @@ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Get a mapping of enzyme groups to variables. +Get a mapping of enzyme groups to variables. See [`enzyme_variables`](@ref). """ function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) enz_ids = genes(model) From f515349755e619d9ae45340e626df7a49d9920b4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 17 Feb 2023 17:20:29 +0100 Subject: [PATCH 208/531] fix docstrings --- src/types/wrappers/EnzymeConstrainedModel.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index 377ff19e2..290677e5e 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -188,10 +188,8 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = """ $(TYPEDSIGNATURES) -Get a mapping of enzyme variables to variables -- for enzyme constrained models, -this is proportional to the molar mass of each gene product. Effectively the -concentration of each enzyme is then reported in mass units relative to the dry -cell mass. +Get a mapping of enzyme concentration (on a mass basis, i.e. mass enzyme/mass +cell) variables to inner variables. """ Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) # this is enough for all the semantics to work From c79f6a6e0fb0dd352421323cd3e958a2278fc20d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 17 Feb 2023 19:35:09 +0100 Subject: [PATCH 209/531] add plural to change_bound and change_constraint --- src/analysis/modifications/generic.jl | 23 +++++++++++++- src/macros/change_bounds.jl | 11 ++++--- src/reconstruction/MatrixCoupling.jl | 8 ++--- src/reconstruction/MatrixModel.jl | 30 ++++++++++++------- src/reconstruction/ObjectModel.jl | 11 +++++-- test/reconstruction/MatrixCoupling.jl | 12 ++++---- test/reconstruction/MatrixModel.jl | 20 ++++++------- test/reconstruction/ObjectModel.jl | 12 ++++---- test/reconstruction/enzyme_constrained.jl | 4 +-- .../simplified_enzyme_constrained.jl | 4 +-- test/types/BalancedGrowthCommunityModel.jl | 6 +++- 11 files changed, 92 insertions(+), 49 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 249493a52..70187e405 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -15,7 +15,8 @@ constrain_objective_value(tolerance) = """ $(TYPEDSIGNATURES) -Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) of reaction `id` if supplied. +Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) +of reaction `id` if supplied. """ change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin @@ -32,6 +33,26 @@ change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = """ $(TYPEDSIGNATURES) +Change the lower and upper bounds (`lower_bounds` and `upper_bounds` +respectively) of reactions in `ids` if supplied. +""" +change_constraint(ids::Vector{String}; lower_bounds = nothing, upper_bounds = nothing) = + (model, opt_model) -> begin + for id in ids + ind = first(indexin([id], variables(model))) + isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) + set_optmodel_bound!( + ind, + opt_model, + lower_bound = lower_bound, + upper_bound = upper_bound, + ) + end + end + +""" +$(TYPEDSIGNATURES) + Modification that changes the objective function used in a constraint based analysis function. `new_objective` can be a single reaction identifier, or an array of reactions identifiers. diff --git a/src/macros/change_bounds.jl b/src/macros/change_bounds.jl index 8fd45c52a..0d94c764d 100644 --- a/src/macros/change_bounds.jl +++ b/src/macros/change_bounds.jl @@ -18,6 +18,9 @@ macro _change_bounds_fn(model_type, idx_type, args...) throw(DomainError(idx_type, "unsupported index type for change_bound macro")), plural_s, ) + lower_bound_s = Symbol(:lower_bound, plural_s) + upper_bound_s = Symbol(:upper_bound, plural_s) + example_idx = plural ? (idx_type == :Int ? [123, 234] : ["ReactionA", "ReactionC"]) : (idx_type == :Int ? 123 : "\"ReactionB\"") #= unquoting is hard =# @@ -34,8 +37,8 @@ macro _change_bounds_fn(model_type, idx_type, args...) $fname( model::$model_type, $idx_var::$idx_type; - lower_bound =$missing_default, - upper_bound =$missing_default, + lower_bound$(plural_s) =$missing_default, + upper_bound$(plural_s) =$missing_default, ) Change the specified reaction flux bound$(plural_s) in the model @@ -57,8 +60,8 @@ macro _change_bounds_fn(model_type, idx_type, args...) $fname( model::$model_type, $idx_var::$idx_type; - lower_bound = $missing_default, - upper_bound = $missing_default, + $lower_bound_s = $missing_default, + $upper_bound_s = $missing_default, ) = $body ), ), diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 7225e8e96..272d74141 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -264,7 +264,7 @@ end end @_change_bounds_fn MatrixCoupling Int inplace plural begin - change_bounds!(model.lm, rxn_idxs; lower_bound, upper_bound) + change_bounds!(model.lm, rxn_idxs; lower_bounds, upper_bounds) end @_change_bounds_fn MatrixCoupling String inplace begin @@ -272,7 +272,7 @@ end end @_change_bounds_fn MatrixCoupling String inplace plural begin - change_bounds!(model.lm, rxn_ids; lower_bound, upper_bound) + change_bounds!(model.lm, rxn_ids; lower_bounds, upper_bounds) end @_change_bounds_fn MatrixCoupling Int begin @@ -283,7 +283,7 @@ end @_change_bounds_fn MatrixCoupling Int plural begin n = copy(model) - n.lm = change_bounds(model.lm, rxn_idxs; lower_bound, upper_bound) + n.lm = change_bounds(model.lm, rxn_idxs; lower_bounds, upper_bounds) n end @@ -295,7 +295,7 @@ end @_change_bounds_fn MatrixCoupling String plural begin n = copy(model) - n.lm = change_bounds(model.lm, rxn_ids; lower_bound, upper_bound) + n.lm = change_bounds(model.lm, rxn_ids; lower_bounds, upper_bounds) n end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 20ae1931d..a2914254a 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -260,7 +260,7 @@ end end @_change_bounds_fn MatrixModel Int inplace plural begin - for (i, l, u) in zip(rxn_idxs, lower_bound, upper_bound) + for (i, l, u) in zip(rxn_idxs, lower_bounds, upper_bounds) change_bound!(model, i, lower_bound = l, upper_bound = u) end end @@ -269,8 +269,8 @@ end change_bounds( model, [rxn_idx], - lower_bound = [lower_bound], - upper_bound = [upper_bound], + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], ) end @@ -278,7 +278,7 @@ end n = copy(model) n.xl = copy(n.xl) n.xu = copy(n.xu) - change_bounds!(n, rxn_idxs; lower_bound, upper_bound) + change_bounds!(n, rxn_idxs; lower_bounds, upper_bounds) n end @@ -286,8 +286,8 @@ end change_bounds!( model, [rxn_id], - lower_bound = [lower_bound], - upper_bound = [upper_bound], + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], ) end @@ -295,17 +295,27 @@ end change_bounds!( model, Vector{Int}(indexin(rxn_ids, variables(model))); - lower_bound, - upper_bound, + lower_bounds, + upper_bounds, ) end @_change_bounds_fn MatrixModel String begin - change_bounds(model, [rxn_id], lower_bound = [lower_bound], upper_bound = [upper_bound]) + change_bounds( + model, + [rxn_id], + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) end @_change_bounds_fn MatrixModel String plural begin - change_bounds(model, Int.(indexin(rxn_ids, variables(model))); lower_bound, upper_bound) + change_bounds( + model, + Int.(indexin(rxn_ids, variables(model))); + lower_bounds, + upper_bounds, + ) end @_remove_fn reaction MatrixModel Int inplace begin diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index b4098ae95..ca002e474 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -178,13 +178,18 @@ remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) end @_change_bounds_fn ObjectModel String inplace plural begin - for (i, l, u) in zip(rxn_ids, lower_bound, upper_bound) + for (i, l, u) in zip(rxn_ids, lower_bounds, upper_bounds) change_bound!(model, i, lower_bound = l, upper_bound = u) end end @_change_bounds_fn ObjectModel String begin - change_bounds(model, [rxn_id], lower_bound = [lower_bound], upper_bound = [upper_bound]) + change_bounds( + model, + [rxn_id], + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) end @_change_bounds_fn ObjectModel String plural begin @@ -193,7 +198,7 @@ end for i in rxn_ids n.reactions[i] = copy(n.reactions[i]) end - change_bounds!(n, rxn_ids; lower_bound, upper_bound) + change_bounds!(n, rxn_ids; lower_bounds, upper_bounds) return n end diff --git a/test/reconstruction/MatrixCoupling.jl b/test/reconstruction/MatrixCoupling.jl index 554f604f5..5b3de7464 100644 --- a/test/reconstruction/MatrixCoupling.jl +++ b/test/reconstruction/MatrixCoupling.jl @@ -154,7 +154,7 @@ end change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) @test cp.lm.xl[1] == -10 @test cp.lm.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) + change_bounds!(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) @test cp.lm.xl[2] == -12.2 @test cp.lm.xu[1] == 11 @@ -164,8 +164,8 @@ end change_bounds!( cp, ["r1", "r2"]; - lower_bound = [-113, -12.23], - upper_bound = [113, 233.0], + lower_bounds = [-113, -12.23], + upper_bounds = [113, 233.0], ) @test cp.lm.xl[2] == -12.23 @test cp.lm.xu[1] == 113 @@ -174,7 +174,7 @@ end @test new_model.lm.xl[1] == -10 @test new_model.lm.xu[1] == 10 new_model = - change_bounds(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) + change_bounds(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) @test new_model.lm.xl[2] == -12.2 @test new_model.lm.xu[1] == 11 @@ -184,8 +184,8 @@ end new_model = change_bounds( cp, ["r1", "r2"]; - lower_bound = [-113, -12.23], - upper_bound = [113, 233.0], + lower_bounds = [-113, -12.23], + upper_bounds = [113, 233.0], ) @test new_model.lm.xl[2] == -12.23 @test new_model.lm.xu[1] == 113 diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index dfe3725de..480d579b4 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -4,7 +4,7 @@ change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) @test cp.xl[1] == -10 @test cp.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) + change_bounds!(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) @test cp.xl[2] == -12.2 @test cp.xu[1] == 11 @@ -14,16 +14,16 @@ change_bounds!( cp, ["r1", "r2"]; - lower_bound = [-113, -12.23], - upper_bound = [114, 233.0], + lower_bounds = [-113, -12.23], + upper_bounds = [114, 233.0], ) @test cp.xl[2] == -12.23 @test cp.xu[1] == 114 change_bounds!( cp, ["r1", "r2"]; - lower_bound = [-114, nothing], - upper_bound = [nothing, 2333.0], + lower_bounds = [-114, nothing], + upper_bounds = [nothing, 2333.0], ) @test cp.xl[1] == -114 @test cp.xl[2] == -12.23 @@ -34,7 +34,7 @@ @test new_model.xl[1] == -10 @test new_model.xu[1] == 10 new_model = - change_bounds(cp, [1, 2]; lower_bound = [-11, -12.2], upper_bound = [11, 23.0]) + change_bounds(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) @test new_model.xl[2] == -12.2 @test new_model.xu[1] == 11 @@ -44,16 +44,16 @@ new_model = change_bounds( cp, ["r1", "r2"]; - lower_bound = [-113, -12.23], - upper_bound = [113, 1000], + lower_bounds = [-113, -12.23], + upper_bounds = [113, 1000], ) @test new_model.xl[2] == -12.23 @test new_model.xu[1] == 113 new_model = change_bounds( cp, ["r1", "r2"]; - lower_bound = [nothing, -10], - upper_bound = [110, nothing], + lower_bounds = [nothing, -10], + upper_bounds = [110, nothing], ) @test new_model.xl[1] == -114 @test new_model.xl[2] == -10 diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 9c4c05854..60f46c968 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -45,8 +45,8 @@ change_bounds!( model, ["r1", "r2"]; - lower_bound = [-110, -220], - upper_bound = [110.0, 220.0], + lower_bounds = [-110, -220], + upper_bounds = [110.0, 220.0], ) @test model.reactions["r1"].lower_bound == -110 @test model.reactions["r1"].upper_bound == 110 @@ -65,8 +65,8 @@ new_model = change_bounds( model, ["r1", "r2"]; - lower_bound = [-10, -20], - upper_bound = [10.0, 20.0], + lower_bounds = [-10, -20], + upper_bounds = [10.0, 20.0], ) @test new_model.reactions["r1"].lower_bound == -10 @test new_model.reactions["r1"].upper_bound == 10 @@ -76,8 +76,8 @@ new_model = change_bounds( model, ["r1", "r2"]; - lower_bound = [-10, nothing], - upper_bound = [nothing, 20.0], + lower_bounds = [-10, nothing], + upper_bounds = [nothing, 20.0], ) @test new_model.reactions["r1"].lower_bound == -10 @test new_model.reactions["r1"].upper_bound == 110 diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 18f3dcb06..4a4b1e209 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -40,8 +40,8 @@ model |> with_changed_bounds( ["EX_glc__D_e", "GLCpts"]; - lower_bound = [-1000.0, -1.0], - upper_bound = [nothing, 12.0], + lower_bounds = [-1000.0, -1.0], + upper_bounds = [nothing, 12.0], ) |> with_enzyme_constraints(total_gene_product_mass) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index b385d23f8..c78197e67 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -31,8 +31,8 @@ model |> with_changed_bounds( ["EX_glc__D_e", "GLCpts"], - lower_bound = [-1000.0, -1.0], - upper_bound = [nothing, 12.0], + lower_bounds = [-1000.0, -1.0], + upper_bounds = [nothing, 12.0], ) |> with_simplified_enzyme_constraints(total_enzyme_capacity = 100.0) diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 2c175a2d6..6f30b396a 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -303,7 +303,11 @@ end gm = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> - with_changed_bounds(["EX_glc__D_e"]; lower_bound = [-1000.0], upper_bound = [0]) |> + with_changed_bounds( + ["EX_glc__D_e"]; + lower_bounds = [-1000.0], + upper_bounds = [0], + ) |> with_enzyme_constraints( gene_product_mass_group_bound = Dict("uncategorized" => 100.0), ) From 608cd6cf98de9d9a26d630e199b0869acfd36396 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 18 Feb 2023 00:05:40 +0100 Subject: [PATCH 210/531] add simple Result type and implement for FBA --- src/analysis/flux_balance_analysis.jl | 19 ++++++++++-- src/io/show/AbstractResult.jl | 15 ++++++++++ src/solver.jl | 43 ++++++++++++++------------- src/types.jl | 1 + src/types/Result.jl | 9 ++++++ src/types/abstract/AbstractResult.jl | 7 +++++ 6 files changed, 70 insertions(+), 24 deletions(-) create mode 100644 src/io/show/AbstractResult.jl create mode 100644 src/types/Result.jl create mode 100644 src/types/abstract/AbstractResult.jl diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 36974ff8d..ead788bb2 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -14,7 +14,7 @@ flux_balance_analysis_vec( args...; kwargs..., )::Maybe{Vector{Float64}} = - values_vec(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) + values_vec(:reaction, flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -30,7 +30,7 @@ flux_balance_analysis_dict( args...; kwargs..., )::Maybe{Dict{String,Float64}} = - values_dict(:reaction, model, flux_balance_analysis(model, args...; kwargs...)) + values_dict(:reaction, flux_balance_analysis(model, args...; kwargs...)) """ $(TYPEDSIGNATURES) @@ -80,5 +80,18 @@ function flux_balance_analysis( end optimize!(opt_model) - return opt_model + + return Result(model, opt_model) end + +""" +$(TYPEDSIGNATURES) + +A pipeable variant of [`flux_balance_analysis`](@ref). + +# Example +``` +flux_balance_analysis(Tulip.Optimizer) |> values_dict(:reaction) +``` +""" +flux_balance_analysis(args...; kwargs...) = model -> flux_balance_analysis(model, args...; kwargs...) \ No newline at end of file diff --git a/src/io/show/AbstractResult.jl b/src/io/show/AbstractResult.jl new file mode 100644 index 000000000..9beab8e84 --- /dev/null +++ b/src/io/show/AbstractResult.jl @@ -0,0 +1,15 @@ +""" +$(TYPEDSIGNATURES) + +Pretty printing of a [`AbstractResult`](@ref) and more. +""" +function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractResult) + print( + io, + """ + Result with fields `model` and `opt_model`. + """, + ) +end + +# $(typeof(m))(#= $(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites =#)\n diff --git a/src/solver.jl b/src/solver.jl index 25faef3d9..a83ccb5ba 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -131,15 +131,11 @@ From the optimized model, returns a vector of values for the selected values_vec(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) ``` """ -function values_vec( - semantics::Val{Semantics}, - model::AbstractMetabolicModel, - opt_model, -) where {Semantics} +function values_vec(semantics::Val{Semantics}, res::AbstractResult) where {Semantics} sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (_, _, _, sem_varmtx) = sem - is_solved(opt_model) ? sem_varmtx(model)' * value.(opt_model[:x]) : nothing + is_solved(res.opt_model) ? sem_varmtx(res.model)' * value.(res.opt_model[:x]) : nothing end """ @@ -152,8 +148,19 @@ Convenience variant of [`values_vec`](@ref). values_vec(:reaction, model, flux_balance_analysis(model, ...)) # in order of reactions(model) ``` """ -values_vec(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = - values_vec(Val(semantics), model, opt_model) +values_vec(semantics::Symbol, res::AbstractResult) = values_vec(Val(semantics), res) + +""" +$(TYPEDSIGNATURES) + +A pipeable variant of the convenience variant of [`values_vec`](@ref). + +# Example +``` +flux_balance_analysis(model, ...) |> values_vec(:reaction) +``` +""" +values_vec(semantics::Symbol) = res -> values_vec(Val(semantics), res) """ $(TYPEDSIGNATURES) @@ -164,19 +171,15 @@ solved values for the selected `semantics`. If the model did not solve, returns # Example ``` -values_dict(Val(:reaction), model, flux_balance_analysis(model, ...)) +values_dict(Val(:reaction), flux_balance_analysis(model, ...)) ``` """ -function values_dict( - semantics::Val{Semantics}, - model::AbstractMetabolicModel, - opt_model, -) where {Semantics} +function values_dict(semantics::Val{Semantics}, res::AbstractResult) where {Semantics} sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (ids, _, _, sem_varmtx) = sem - is_solved(opt_model) ? Dict(ids(model) .=> sem_varmtx(model)' * value.(opt_model[:x])) : - nothing + is_solved(res.opt_model) ? + Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.opt_model[:x])) : nothing end """ @@ -189,8 +192,7 @@ Convenience variant of [`values_dict`](@ref). values_dict(:reaction, model, flux_balance_analysis(model, ...)) ``` """ -values_dict(semantics::Symbol, model::AbstractMetabolicModel, opt_model) = - values_dict(Val(semantics), model, opt_model) +values_dict(semantics::Symbol, res::AbstractResult) = values_dict(Val(semantics), res) """ $(TYPEDSIGNATURES) @@ -199,11 +201,10 @@ A pipeable variant of the convenience variant of [`values_dict`](@ref). # Example ``` -flux_balance_analysis(model, ...) |> values_dict(:reaction, model) +flux_balance_analysis(model, ...) |> values_dict(:reaction) ``` """ -values_dict(semantics::Symbol, model::AbstractMetabolicModel) = - opt_model -> values_dict(Val(semantics), model, opt_model) +values_dict(semantics::Symbol) = res -> values_dict(Val(semantics), res) @export_locals end diff --git a/src/types.jl b/src/types.jl index eea398a33..84245a008 100644 --- a/src/types.jl +++ b/src/types.jl @@ -71,6 +71,7 @@ end # module Accessors using ..Accessors using ..Internal.Macros using ..Log.Internal: @io_log + using JuMP @inc_dir types @inc_dir types models diff --git a/src/types/Result.jl b/src/types/Result.jl new file mode 100644 index 000000000..1c1d95222 --- /dev/null +++ b/src/types/Result.jl @@ -0,0 +1,9 @@ +""" + mutable struct Result + +A `Result` type stores an [`AbstractMetabolicModel`](@ref) and a `JuMP.Model`. +""" +mutable struct Result{AM<:AbstractMetabolicModel,JM<:JuMP.Model} <: AbstractResult + model::AM + opt_model::JM +end diff --git a/src/types/abstract/AbstractResult.jl b/src/types/abstract/AbstractResult.jl new file mode 100644 index 000000000..e380436a9 --- /dev/null +++ b/src/types/abstract/AbstractResult.jl @@ -0,0 +1,7 @@ +""" + abstract type Result + +A `Result` stores the input and output of analysis functions. This facilitates +piping between analysis functions. +""" +abstract type AbstractResult end From 912ad0ef5068bdf18f3a11b806de37733a30e1b3 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 18 Feb 2023 11:31:44 +0100 Subject: [PATCH 211/531] implement reviews --- src/analysis/modifications/generic.jl | 8 ++++---- src/macros/change_bounds.jl | 4 ++-- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 70187e405..6f1392942 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -36,16 +36,16 @@ $(TYPEDSIGNATURES) Change the lower and upper bounds (`lower_bounds` and `upper_bounds` respectively) of reactions in `ids` if supplied. """ -change_constraint(ids::Vector{String}; lower_bounds = nothing, upper_bounds = nothing) = +change_constraints(ids::Vector{String}; lower_bounds = fill(nothing,length(ids)), upper_bounds = fill(nothing,length(ids))) = (model, opt_model) -> begin - for id in ids + for (id, lb, ub) in zip(ids, lower_bounds, upper_bounds) ind = first(indexin([id], variables(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) set_optmodel_bound!( ind, opt_model, - lower_bound = lower_bound, - upper_bound = upper_bound, + lower_bound = lb, + upper_bound = ub, ) end end diff --git a/src/macros/change_bounds.jl b/src/macros/change_bounds.jl index 0d94c764d..f9e3fe71b 100644 --- a/src/macros/change_bounds.jl +++ b/src/macros/change_bounds.jl @@ -37,8 +37,8 @@ macro _change_bounds_fn(model_type, idx_type, args...) $fname( model::$model_type, $idx_var::$idx_type; - lower_bound$(plural_s) =$missing_default, - upper_bound$(plural_s) =$missing_default, + $lower_bound_s =$missing_default, + $upper_bound_s =$missing_default, ) Change the specified reaction flux bound$(plural_s) in the model From 595dd0499328fa7af57490a80e60c88edbb34d9d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 18 Feb 2023 11:32:39 +0100 Subject: [PATCH 212/531] format --- src/analysis/modifications/generic.jl | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 6f1392942..8281b132f 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -36,17 +36,16 @@ $(TYPEDSIGNATURES) Change the lower and upper bounds (`lower_bounds` and `upper_bounds` respectively) of reactions in `ids` if supplied. """ -change_constraints(ids::Vector{String}; lower_bounds = fill(nothing,length(ids)), upper_bounds = fill(nothing,length(ids))) = +change_constraints( + ids::Vector{String}; + lower_bounds = fill(nothing, length(ids)), + upper_bounds = fill(nothing, length(ids)), +) = (model, opt_model) -> begin for (id, lb, ub) in zip(ids, lower_bounds, upper_bounds) ind = first(indexin([id], variables(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!( - ind, - opt_model, - lower_bound = lb, - upper_bound = ub, - ) + set_optmodel_bound!(ind, opt_model, lower_bound = lb, upper_bound = ub) end end From 0cb25968f804b04a9c5c781893c3c9fd7a86b8d5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 16 Feb 2023 20:23:04 +0100 Subject: [PATCH 213/531] rather map through molar masses --- src/reconstruction/enzyme_constrained.jl | 49 +++++++------------ src/reconstruction/pipes/enzymes.jl | 4 +- .../simplified_enzyme_constrained.jl | 2 +- test/reconstruction/constrained_allocation.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 4 +- 5 files changed, 25 insertions(+), 36 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index fc38096b3..e34ed9a8f 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -6,15 +6,10 @@ given by GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for details). Multiple capacity constraints can be placed on the model using the kwargs. -# Arguments -- a `model` that implements the accessors `gene_product_molar_mass`, - `reaction_isozymes`, `gene_product_lower_bound`, `gene_product_upper_bound`. +Parameters `gene_product_mass_group` and `gene_product_mass_group_bound` specify the groups of gene products and the respective total limit of total mass of the gene products for each group. Gene products not listed in any gene product mass group are ignored. + +For simplicity in many use cases, specifying the `total_enzyme_capacity` argument overrides the above arguments by specifying a single group called `uncategorized` of all gene products, with the corresponding mass bound. - `gene_product_mass_group` is a dict that returns a vector of gene IDs - associated with each a named capacity constraint. By default, all gene - products belong to group `"uncategorized"`, which is the behavior of original - GECKO. -- `gene_product_mass_group_bound` is a dict that returns the capacity - limitation for a given named capacity constraint. # Example ``` @@ -29,20 +24,30 @@ ecmodel = make_enzyme_constrained_model( "total" => 0.5, ), ) -``` +ecmodel2 = make_enzyme_constrained_model( + model; + total_enzyme_capacity = 0.5 +) +``` # Notes Reactions with no turnover number data, or non-enzymatic reactions that should -be ignored, must have `nothing` in the `gene_associations` field of the +be ignored, *must* have `nothing` in the `gene_associations` field of the associated reaction. """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; - gene_product_mass_group::Dict{String,Vector{String}} = Dict( - "uncategorized" => genes(model), - ), - gene_product_mass_group_bound::Dict{String,Float64} = Dict("uncategorized" => 0.5), + gene_product_mass_group::Maybe{Dict{String,Vector{String}}} = nothing, + gene_product_mass_group_bound::Maybe{Dict{String,Float64}} = nothing, + total_enzyme_capacity::Maybe{Float64} = nothing, ) + if !isnothing(total_enzyme_capacity) + gene_product_mass_group = Dict("uncategorized" => genes(model)) + gene_product_mass_group_bound = Dict("uncategorized" => total_enzyme_capacity) + end + isnothing(gene_product_mass_group) && throw(ArgumentError("missing mass group specification")) + isnothing(gene_product_mass_group_bound) && throw(ArgumentError("missing mass group bounds")) + gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) gpmm_(gid) = gene_product_molar_mass(model, gid) @@ -165,19 +170,3 @@ function make_enzyme_constrained_model( model, ) end - -""" -$(TYPEDSIGNATURES) - -A convenience wrapper around [`make_enzyme_constrained_model`](@ref) that -enforces a global enzyme capacity limitation across all genes in the model with -`total_capacity_limitation` being the bound. -""" -make_enzyme_constrained_model( - model::AbstractMetabolicModel, - total_capacity_limitation::Float64, -) = make_enzyme_constrained_model( - model; - gene_product_mass_group = Dict("uncategorized" => genes(model)), - gene_product_mass_group_bound = Dict("uncategorized" => total_capacity_limitation), -) diff --git a/src/reconstruction/pipes/enzymes.jl b/src/reconstruction/pipes/enzymes.jl index 947558265..fefb2f6f0 100644 --- a/src/reconstruction/pipes/enzymes.jl +++ b/src/reconstruction/pipes/enzymes.jl @@ -5,8 +5,8 @@ Specifies a model variant which adds extra semantics of the sMOMENT algorithm, giving a [`SimplifiedEnzymeConstrainedModel`](@ref). The arguments are forwarded to [`make_simplified_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). """ -with_simplified_enzyme_constraints(; kwargs...) = - model -> make_simplified_enzyme_constrained_model(model; kwargs...) +with_simplified_enzyme_constraints(args...; kwargs...) = + model -> make_simplified_enzyme_constrained_model(model, args...; kwargs...) """ $(TYPEDSIGNATURES) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 06d1d7f4a..07dad3c02 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -20,7 +20,7 @@ associated reaction. """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; - total_enzyme_capacity::Float64, + total_enzyme_capacity::Float64 = 0.5, ) # helper function to rank the isozymes by relative speed speed_enzyme(model, isozyme) = diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 252c5c970..66cdaf044 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -71,7 +71,7 @@ # test inplace variant add_virtualribosome!(m, "r6", 0.2) - cam = make_simplified_enzyme_constrained_model(m; total_enzyme_capacity = 0.5) + cam = m |> with_simplified_enzyme_constraints(total_enzyme_capacity = 0.5) @test coupling(cam)[1, 7] == 5.0 diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 4a4b1e209..d4d2b7d60 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -27,7 +27,7 @@ end end - total_gene_product_mass = 100.0 + total_enzyme_capacity = 100.0 # set gene product bounds for gid in genes(model) @@ -43,7 +43,7 @@ lower_bounds = [-1000.0, -1.0], upper_bounds = [nothing, 12.0], ) |> - with_enzyme_constraints(total_gene_product_mass) + with_enzyme_constraints(; total_enzyme_capacity) opt_model = flux_balance_analysis( gm, From 346c64c7b1a05501c1ff8e68546f742a8e1c6c27 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 18 Feb 2023 11:37:45 +0100 Subject: [PATCH 214/531] rename kwarg to be even more consistent --- .../14_simplified_enzyme_constrained.jl | 2 +- src/reconstruction/enzyme_constrained.jl | 36 ++++++++++++------- .../simplified_enzyme_constrained.jl | 6 ++-- .../SimplifiedEnzymeConstrainedModel.jl | 6 ++-- test/reconstruction/constrained_allocation.jl | 7 ++-- test/reconstruction/enzyme_constrained.jl | 6 ++-- .../simplified_enzyme_constrained.jl | 2 +- test/types/BalancedGrowthCommunityModel.jl | 4 +-- 8 files changed, 40 insertions(+), 29 deletions(-) diff --git a/docs/src/examples/14_simplified_enzyme_constrained.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl index 99484c3a4..6a61e7e3a 100644 --- a/docs/src/examples/14_simplified_enzyme_constrained.jl +++ b/docs/src/examples/14_simplified_enzyme_constrained.jl @@ -63,7 +63,7 @@ simplified_enzyme_constrained_model = model |> with_simplified_enzyme_constraints( reaction_isozyme = rxn_isozymes, gene_product_molar_mass = gene_product_masses, - total_enzyme_capacity = 50.0, + total_gene_product_mass_bound = 50.0, ) # (You could alternatively use the [`make_simplified_enzyme_constrained_model`](@ref) to create the diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index e34ed9a8f..f1cc563b0 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -1,15 +1,20 @@ """ $(TYPEDSIGNATURES) -Wrap a model into a [`EnzymeConstrainedModel`](@ref), following the structure -given by GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for -details). Multiple capacity constraints can be placed on the model using the +Wrap a model into an [`EnzymeConstrainedModel`](@ref), following the structure +given by the GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation +for details). Multiple capacity constraints can be placed on the model using the kwargs. -Parameters `gene_product_mass_group` and `gene_product_mass_group_bound` specify the groups of gene products and the respective total limit of total mass of the gene products for each group. Gene products not listed in any gene product mass group are ignored. +Parameters `gene_product_mass_group` and `gene_product_mass_group_bound` specify +the groups of gene products and the respective total capacity limit of the gene +products for each group. Gene products not listed in any gene product mass group +are ignored. -For simplicity in many use cases, specifying the `total_enzyme_capacity` argument overrides the above arguments by specifying a single group called `uncategorized` of all gene products, with the corresponding mass bound. -- `gene_product_mass_group` is a dict that returns a vector of gene IDs +For simplicity, in many use cases specifying the `total_gene_product_mass_bound` +argument overrides the above arguments by specifying a single group called +`uncategorized` of all gene products, with the corresponding mass capacity +bound. # Example ``` @@ -27,26 +32,31 @@ ecmodel = make_enzyme_constrained_model( ecmodel2 = make_enzyme_constrained_model( model; - total_enzyme_capacity = 0.5 -) + total_gene_product_mass_bound = 0.5 +) ``` # Notes Reactions with no turnover number data, or non-enzymatic reactions that should be ignored, *must* have `nothing` in the `gene_associations` field of the associated reaction. + +The capacity bound """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; gene_product_mass_group::Maybe{Dict{String,Vector{String}}} = nothing, gene_product_mass_group_bound::Maybe{Dict{String,Float64}} = nothing, - total_enzyme_capacity::Maybe{Float64} = nothing, + total_gene_product_mass_bound::Maybe{Float64} = nothing, ) - if !isnothing(total_enzyme_capacity) + if !isnothing(total_gene_product_mass_bound) gene_product_mass_group = Dict("uncategorized" => genes(model)) - gene_product_mass_group_bound = Dict("uncategorized" => total_enzyme_capacity) + gene_product_mass_group_bound = + Dict("uncategorized" => total_gene_product_mass_bound) end - isnothing(gene_product_mass_group) && throw(ArgumentError("missing mass group specification")) - isnothing(gene_product_mass_group_bound) && throw(ArgumentError("missing mass group bounds")) + isnothing(gene_product_mass_group) && + throw(ArgumentError("missing mass group specification")) + isnothing(gene_product_mass_group_bound) && + throw(ArgumentError("missing mass group bounds")) gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 07dad3c02..957e25993 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -7,7 +7,7 @@ Construct a model with a structure given by sMOMENT algorithm; returns a # Arguments - a `model` that implements the accessors `gene_product_molar_mass`, `reaction_isozymes`. -- `total_enzyme_capacity` is the maximum "enzyme capacity" in the model. +- `total_gene_product_mass_bound` is the maximum "enzyme capacity" in the model. # Notes The SMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple @@ -20,7 +20,7 @@ associated reaction. """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; - total_enzyme_capacity::Float64 = 0.5, + total_gene_product_mass_bound::Float64 = 0.5, ) # helper function to rank the isozymes by relative speed speed_enzyme(model, isozyme) = @@ -88,5 +88,5 @@ function make_simplified_enzyme_constrained_model( end end - return SimplifiedEnzymeConstrainedModel(columns, total_enzyme_capacity, model) + return SimplifiedEnzymeConstrainedModel(columns, total_gene_product_mass_bound, model) end diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 3a2b48a07..683949ba2 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -45,7 +45,7 @@ is retained. In the structure, the field `columns` describes the correspondence of stoichiometry columns to the stoichiometry and data of the internal wrapped -model, and `total_enzyme_capacity` is the total bound on the enzyme capacity +model, and `total_gene_product_mass_bound` is the total bound on the enzyme capacity consumption as specified in sMOMENT algorithm. This implementation allows easy access to fluxes from the split reactions @@ -59,7 +59,7 @@ $(TYPEDFIELDS) """ struct SimplifiedEnzymeConstrainedModel <: AbstractModelWrapper columns::Vector{_SimplifiedEnzymeConstrainedColumn} - total_enzyme_capacity::Float64 + total_gene_product_mass_bound::Float64 inner::AbstractMetabolicModel end @@ -122,5 +122,5 @@ Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = let (iclb, icub) = coupling_bounds(model.inner) - (vcat(iclb, [0.0]), vcat(icub, [model.total_enzyme_capacity])) + (vcat(iclb, [0.0]), vcat(icub, [model.total_gene_product_mass_bound])) end diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 66cdaf044..0b03b4413 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -57,7 +57,10 @@ @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 - cam = make_simplified_enzyme_constrained_model(ribomodel; total_enzyme_capacity = 0.5) + cam = make_simplified_enzyme_constrained_model( + ribomodel; + total_gene_product_mass_bound = 0.5, + ) @test coupling(cam)[1, 7] == 5.0 @@ -71,7 +74,7 @@ # test inplace variant add_virtualribosome!(m, "r6", 0.2) - cam = m |> with_simplified_enzyme_constraints(total_enzyme_capacity = 0.5) + cam = m |> with_simplified_enzyme_constraints(total_gene_product_mass_bound = 0.5) @test coupling(cam)[1, 7] == 5.0 diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index d4d2b7d60..f321f766a 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -27,7 +27,7 @@ end end - total_enzyme_capacity = 100.0 + total_gene_product_mass_bound = 100.0 # set gene product bounds for gid in genes(model) @@ -43,7 +43,7 @@ lower_bounds = [-1000.0, -1.0], upper_bounds = [nothing, 12.0], ) |> - with_enzyme_constraints(; total_enzyme_capacity) + with_enzyme_constraints(; total_gene_product_mass_bound) opt_model = flux_balance_analysis( gm, @@ -64,7 +64,7 @@ @test isapprox( sum(values(prot_concens)), - total_gene_product_mass, + total_gene_product_mass_bound, atol = TEST_TOLERANCE, ) @test isapprox( diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index c78197e67..5d729a35b 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -34,7 +34,7 @@ lower_bounds = [-1000.0, -1.0], upper_bounds = [nothing, 12.0], ) |> - with_simplified_enzyme_constraints(total_enzyme_capacity = 100.0) + with_simplified_enzyme_constraints(total_gene_product_mass_bound = 100.0) rxn_fluxes = flux_balance_analysis_dict( simplified_enzyme_constrained_model, diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 6f30b396a..4869e8fbe 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -308,9 +308,7 @@ end lower_bounds = [-1000.0], upper_bounds = [0], ) |> - with_enzyme_constraints( - gene_product_mass_group_bound = Dict("uncategorized" => 100.0), - ) + with_enzyme_constraints(total_gene_product_mass_bound = 100.0) ex_rxns = find_exchange_reaction_ids(ecoli) From 68f46f335cc615588c796ec522e4797abdc43fda Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 18 Feb 2023 13:00:15 +0100 Subject: [PATCH 215/531] improve docstrings --- src/reconstruction/enzyme_constrained.jl | 25 +++++++----------- .../simplified_enzyme_constrained.jl | 26 +++++++++---------- src/types/wrappers/EnzymeConstrainedModel.jl | 7 +++++ .../SimplifiedEnzymeConstrainedModel.jl | 10 +++++-- 4 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index f1cc563b0..15abb76cb 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -1,20 +1,19 @@ """ $(TYPEDSIGNATURES) -Wrap a model into an [`EnzymeConstrainedModel`](@ref), following the structure +Wrap a `model` into an [`EnzymeConstrainedModel`](@ref), following the structure given by the GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation -for details). Multiple capacity constraints can be placed on the model using the -kwargs. +for details). Multiple mass constraint groups can be placed on the model using +the keyword arguments. Parameters `gene_product_mass_group` and `gene_product_mass_group_bound` specify -the groups of gene products and the respective total capacity limit of the gene -products for each group. Gene products not listed in any gene product mass group -are ignored. +the groups of gene products, and the respective total mass limit for each group. +Gene products that are not listed in any gene product mass group are ignored. -For simplicity, in many use cases specifying the `total_gene_product_mass_bound` -argument overrides the above arguments by specifying a single group called -`uncategorized` of all gene products, with the corresponding mass capacity -bound. +For simplicity, specifying the `total_gene_product_mass_bound` argument +overrides the above arguments by internally specifying a single group called +`uncategorized` of all gene products, and acts like the maximum "enzyme +capacity" in the model. # Example ``` @@ -35,12 +34,6 @@ ecmodel2 = make_enzyme_constrained_model( total_gene_product_mass_bound = 0.5 ) ``` -# Notes -Reactions with no turnover number data, or non-enzymatic reactions that should -be ignored, *must* have `nothing` in the `gene_associations` field of the -associated reaction. - -The capacity bound """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 957e25993..cc31f3f4e 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -1,22 +1,20 @@ """ $(TYPEDSIGNATURES) -Construct a model with a structure given by sMOMENT algorithm; returns a +Wrap a `model` with a structure given by sMOMENT algorithm; returns a [`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). +The sMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple +isozymes are present in `model`, the "fastest" isozyme will be used. This is +determined based on maximum kcat (forward or backward) divided by mass of the +isozyme. The `total_gene_product_mass_bound` is the maximum "enzyme capacity" in +the model. -# Arguments -- a `model` that implements the accessors `gene_product_molar_mass`, - `reaction_isozymes`. -- `total_gene_product_mass_bound` is the maximum "enzyme capacity" in the model. - -# Notes -The SMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple -isozymes are present the "fastest" isozyme will be used. This is determined -based on maximum kcat (forward or backward) divided by mass of the isozyme. - -Reactions with no turnover number data, or non-enzymatic reactions that should -be ignored, must have `nothing` in the `gene_associations` field of the -associated reaction. +# Example +``` +ecmodel = make_simplified_enzyme_constrained_model( + model; + total_gene_product_mass_bound = 0.5 +) """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/types/wrappers/EnzymeConstrainedModel.jl index fa9d4d120..6dba7e3b8 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/types/wrappers/EnzymeConstrainedModel.jl @@ -74,6 +74,13 @@ but retains the original "simple" reactions accessible by [`reactions`](@ref). The related constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). +To implement this wrapper for a model, the accessors +[`reaction_isozymes`](@ref), [`gene_product_lower_bound`](@ref), +[`gene_product_upper_bound](@ref), [`gene_product_molar_mass`](@ref), need to be +available. Additionally, the model needs to associate [`Isozyme`](@ref)s with +reactions. Reactions without enzymes, or those that should be ignored need to +return `nothing` when [`reaction_isozymes`](@ref) is called on them. + # Fields $(TYPEDFIELDS) """ diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl index 683949ba2..0859f73e0 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -45,8 +45,8 @@ is retained. In the structure, the field `columns` describes the correspondence of stoichiometry columns to the stoichiometry and data of the internal wrapped -model, and `total_gene_product_mass_bound` is the total bound on the enzyme capacity -consumption as specified in sMOMENT algorithm. +model, and `total_gene_product_mass_bound` is the total bound on the enzyme +capacity consumption as specified in sMOMENT algorithm. This implementation allows easy access to fluxes from the split reactions (available in `variables(model)`), while the original "simple" reactions from @@ -54,6 +54,12 @@ the wrapped model are retained as [`reactions`](@ref). All additional constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). +To implement this wrapper for a model, the accessors [`reaction_isozymes`](@ref) +and [`gene_product_molar_mass`](@ref), need to be available. Additionally, the +model needs to associate [`Isozyme`](@ref)s with reactions. Reactions without +enzymes, or those that should be ignored need to return `nothing` when +[`reaction_isozymes`](@ref) is called on them. + # Fields $(TYPEDFIELDS) """ From 58700cd6715dd35185d68b861725782133fdc249 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 12:57:06 +0100 Subject: [PATCH 216/531] annihilate `get_optmodel_bounds` --- src/solver.jl | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index a83ccb5ba..a6b15cd2d 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -81,18 +81,6 @@ end """ $(TYPEDSIGNATURES) -Returns vectors of the lower and upper bounds of `opt_model` constraints, where -`opt_model` is a JuMP model constructed by e.g. -[`make_optimization_model`](@ref) or [`flux_balance_analysis`](@ref). -""" -get_optmodel_bounds(opt_model) = ( - [-normalized_rhs(lb) for lb in opt_model[:lbs]], - [normalized_rhs(ub) for ub in opt_model[:ubs]], -) - -""" -$(TYPEDSIGNATURES) - Helper function to set the bounds of a variable in the model. Internally calls `set_normalized_rhs` from JuMP. If the bounds are set to `nothing`, they will not be changed. From 49d36a9364bbe297a95efd3a9b6e733d9eb72bc4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 14:57:45 +0100 Subject: [PATCH 217/531] normalize ModelWithResult, remove the Val semantics --- src/analysis/variability_analysis.jl | 17 +++-------- src/io/show/AbstractResult.jl | 15 --------- src/solver.jl | 40 +++++------------------- src/types.jl | 1 - src/types/Result.jl | 44 ++++++++++++++++++++++++--- src/types/abstract/AbstractResult.jl | 7 ----- src/types/accessors/bits/semantics.jl | 15 ++++----- 7 files changed, 59 insertions(+), 80 deletions(-) delete mode 100644 src/io/show/AbstractResult.jl delete mode 100644 src/types/abstract/AbstractResult.jl diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 3eb7608fc..c69d04338 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -33,7 +33,7 @@ function flux_variability_analysis( kwargs..., ) variability_analysis( - Val(:reaction), + :reaction, model, optimizer; ids = reaction_ids, @@ -45,27 +45,18 @@ end """ $(TYPEDSIGNATURES) -Run the variability analysis over a selected semantics defined by a symbol, -such as `:reaction`. All other arguments are forwarded. -""" -variability_analysis(semantics::Symbol, args...; kwargs...) = - variability_analysis(Val(semantics), args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - A variability analysis over a selected semantics, picking up only objects specified by IDs or indexes from the selected semantics. For semantics -`Val(:reaction)`, this is equivalent to [`flux_variability_analysis`](@ref). +`:reaction`, this is equivalent to [`flux_variability_analysis`](@ref). """ function variability_analysis( - semantics::Val{Semantics}, + semantics::Symbol, model::AbstractMetabolicModel, optimizer; ids::Maybe{Vector{String}} = nothing, indexes::Maybe{Vector{Int}} = nothing, kwargs..., -) where {Semantics} +) sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (sem_ids, n_ids, _, sem_varmtx) = sem diff --git a/src/io/show/AbstractResult.jl b/src/io/show/AbstractResult.jl deleted file mode 100644 index 9beab8e84..000000000 --- a/src/io/show/AbstractResult.jl +++ /dev/null @@ -1,15 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Pretty printing of a [`AbstractResult`](@ref) and more. -""" -function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractResult) - print( - io, - """ - Result with fields `model` and `opt_model`. - """, - ) -end - -# $(typeof(m))(#= $(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites =#)\n diff --git a/src/solver.jl b/src/solver.jl index a6b15cd2d..58aa98d0b 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -119,28 +119,16 @@ From the optimized model, returns a vector of values for the selected values_vec(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) ``` """ -function values_vec(semantics::Val{Semantics}, res::AbstractResult) where {Semantics} +function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (_, _, _, sem_varmtx) = sem - is_solved(res.opt_model) ? sem_varmtx(res.model)' * value.(res.opt_model[:x]) : nothing + is_solved(res.result) ? sem_varmtx(res.model)' * value.(res.result[:x]) : nothing end """ $(TYPEDSIGNATURES) -Convenience variant of [`values_vec`](@ref). - -# Example -``` -values_vec(:reaction, model, flux_balance_analysis(model, ...)) # in order of reactions(model) -``` -""" -values_vec(semantics::Symbol, res::AbstractResult) = values_vec(Val(semantics), res) - -""" -$(TYPEDSIGNATURES) - A pipeable variant of the convenience variant of [`values_vec`](@ref). # Example @@ -148,7 +136,7 @@ A pipeable variant of the convenience variant of [`values_vec`](@ref). flux_balance_analysis(model, ...) |> values_vec(:reaction) ``` """ -values_vec(semantics::Symbol) = res -> values_vec(Val(semantics), res) +values_vec(semantics::Symbol) = res -> values_vec(semantics, res) """ $(TYPEDSIGNATURES) @@ -159,32 +147,20 @@ solved values for the selected `semantics`. If the model did not solve, returns # Example ``` -values_dict(Val(:reaction), flux_balance_analysis(model, ...)) +values_dict(:reaction, flux_balance_analysis(model, ...)) ``` """ -function values_dict(semantics::Val{Semantics}, res::AbstractResult) where {Semantics} +function values_dict(semantics::Symbol, res::ModelWithResult{<:Model}) sem = Accessors.Internal.get_semantics(semantics) isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) (ids, _, _, sem_varmtx) = sem - is_solved(res.opt_model) ? - Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.opt_model[:x])) : nothing + is_solved(res.result) ? + Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.result[:x])) : nothing end """ $(TYPEDSIGNATURES) -Convenience variant of [`values_dict`](@ref). - -# Example -``` -values_dict(:reaction, model, flux_balance_analysis(model, ...)) -``` -""" -values_dict(semantics::Symbol, res::AbstractResult) = values_dict(Val(semantics), res) - -""" -$(TYPEDSIGNATURES) - A pipeable variant of the convenience variant of [`values_dict`](@ref). # Example @@ -192,7 +168,7 @@ A pipeable variant of the convenience variant of [`values_dict`](@ref). flux_balance_analysis(model, ...) |> values_dict(:reaction) ``` """ -values_dict(semantics::Symbol) = res -> values_dict(Val(semantics), res) +values_dict(semantics::Symbol) = res -> values_dict(semantics, res) @export_locals end diff --git a/src/types.jl b/src/types.jl index 84245a008..eea398a33 100644 --- a/src/types.jl +++ b/src/types.jl @@ -71,7 +71,6 @@ end # module Accessors using ..Accessors using ..Internal.Macros using ..Log.Internal: @io_log - using JuMP @inc_dir types @inc_dir types models diff --git a/src/types/Result.jl b/src/types/Result.jl index 1c1d95222..fcaf6cc9b 100644 --- a/src/types/Result.jl +++ b/src/types/Result.jl @@ -1,9 +1,43 @@ """ - mutable struct Result +$(TYPEDEF) -A `Result` type stores an [`AbstractMetabolicModel`](@ref) and a `JuMP.Model`. +A simple storage type for analysis result that is accompanied with the original +model. This vastly simplifies piping; e.g., instead of having to specify the +model in each part of the pipeline as such: +``` +flux_balance_analysis(m, optimizer) |> values_vec(m) +``` +...we can do: +``` +flux_balance_analysis(m, optimizer) |> values_vec +``` + +Optionally you may take out "just" the result value by piping through +[`result`](@ref), in this case obtaining a vector: +``` +... |> values_vec |> result +``` + +This additionally enables some situations where carrying the model around +manually would be hard or require multiple piping steps, such as: +``` +model |> with_some_reactions(...) |> + with_enzyme_constraints(...) |> + flux_balance_analysis(optimizer) |> + values_dict |> + result +""" +struct ModelWithResult{T} + model::AbstractMetabolicModel + result::T +end + +""" +$(TYPEDSIGNATURES) + +Pipeable shortcut for extracting the result value out of +[`ModelWithResult`](@ref). """ -mutable struct Result{AM<:AbstractMetabolicModel,JM<:JuMP.Model} <: AbstractResult - model::AM - opt_model::JM +function result(x::ModelWithResult{T})::T where T + x.result end diff --git a/src/types/abstract/AbstractResult.jl b/src/types/abstract/AbstractResult.jl deleted file mode 100644 index e380436a9..000000000 --- a/src/types/abstract/AbstractResult.jl +++ /dev/null @@ -1,7 +0,0 @@ -""" - abstract type Result - -A `Result` stores the input and output of analysis functions. This facilitates -piping between analysis functions. -""" -abstract type AbstractResult end diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index bae805d8e..23884d5f9 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -53,14 +53,15 @@ Get a tuple of functions that work with the given semantics, or `nothing` if the semantics doesn't exist. """ function get_semantics( - ::Val{Semantics}, -)::Types.Maybe{Tuple{Function,Function,Function,Function}} where {Semantics} - if Semantics in variable_semantics + semantics::Symbol, +)::Types.Maybe{Tuple{Function,Function,Function,Function}} + # TODO store the functions instead + if semantics in variable_semantics return ( - Base.eval(Accessors, Symbol(Semantics, :s)), - Base.eval(Accessors, Symbol(:n_, Semantics, :s)), - Base.eval(Accessors, Symbol(Semantics, :_variables)), - Base.eval(Accessors, Symbol(Semantics, :_variables_matrix)), + Base.eval(Accessors, Symbol(semantics, :s)), + Base.eval(Accessors, Symbol(:n_, semantics, :s)), + Base.eval(Accessors, Symbol(semantics, :_variables)), + Base.eval(Accessors, Symbol(semantics, :_variables_matrix)), ) end end From 5d3f96a62d5d4b6b6f97c6fe1157630a8839d97c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 15:25:20 +0100 Subject: [PATCH 218/531] ModelWithResult FBA --- src/analysis/flux_balance_analysis.jl | 54 +++------------------------ src/types/Result.jl | 2 +- 2 files changed, 7 insertions(+), 49 deletions(-) diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index ead788bb2..c64a369c6 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -1,40 +1,6 @@ """ $(TYPEDSIGNATURES) -A variant of FBA that returns a vector of fluxes in the same order as reactions -of the model, if the solution is found. - -Arguments are passed to [`flux_balance_analysis`](@ref). - -This function is kept for backwards compatibility, use [`values_vec`](@ref) -instead. -""" -flux_balance_analysis_vec( - model::AbstractMetabolicModel, - args...; - kwargs..., -)::Maybe{Vector{Float64}} = - values_vec(:reaction, flux_balance_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - -A variant of FBA that returns a dictionary assigning fluxes to reactions, if -the solution is found. Arguments are passed to [`flux_balance_analysis`](@ref). - -This function is kept for backwards compatibility, use [`values_dict`](@ref) -instead. -""" -flux_balance_analysis_dict( - model::AbstractMetabolicModel, - args...; - kwargs..., -)::Maybe{Dict{String,Float64}} = - values_dict(:reaction, flux_balance_analysis(model, args...; kwargs...)) - -""" -$(TYPEDSIGNATURES) - Run flux balance analysis (FBA) on the `model` optionally specifying `modifications` to the problem. Basically, FBA solves this optimization problem: ``` @@ -67,12 +33,7 @@ modified_solution = flux_balance_analysis(model, GLPK.optimizer; modifications=[change_objective(biomass_reaction_id)]) ``` """ -function flux_balance_analysis( - model::M, - optimizer; - modifications = [], -) where {M<:AbstractMetabolicModel} - +function flux_balance_analysis(model::AbstractMetabolicModel, optimizer; modifications = []) opt_model = make_optimization_model(model, optimizer) for mod in modifications @@ -81,17 +42,14 @@ function flux_balance_analysis( optimize!(opt_model) - return Result(model, opt_model) + ModelWithResult(model, opt_model) end + """ $(TYPEDSIGNATURES) -A pipeable variant of [`flux_balance_analysis`](@ref). - -# Example -``` -flux_balance_analysis(Tulip.Optimizer) |> values_dict(:reaction) -``` +Pipe-able variant of [`flux_balance_analysis`](@ref). """ -flux_balance_analysis(args...; kwargs...) = model -> flux_balance_analysis(model, args...; kwargs...) \ No newline at end of file +flux_balance_analysis(optimizer; modifications = []) = + model -> flux_balance_analysis(model, optimizer; modifications) diff --git a/src/types/Result.jl b/src/types/Result.jl index fcaf6cc9b..62025b87e 100644 --- a/src/types/Result.jl +++ b/src/types/Result.jl @@ -38,6 +38,6 @@ $(TYPEDSIGNATURES) Pipeable shortcut for extracting the result value out of [`ModelWithResult`](@ref). """ -function result(x::ModelWithResult{T})::T where T +function result(x::ModelWithResult{T})::T where {T} x.result end From 7d95b7b0b68d9dfcb51ca62cffc9197ed478de42 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 15:59:18 +0100 Subject: [PATCH 219/531] simplify semantics retrieval --- src/analysis/variability_analysis.jl | 4 +--- src/solver.jl | 8 ++------ src/types/accessors/bits/semantics.jl | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 9 deletions(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index c69d04338..536d06d6e 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -57,9 +57,7 @@ function variability_analysis( indexes::Maybe{Vector{Int}} = nothing, kwargs..., ) - sem = Accessors.Internal.get_semantics(semantics) - isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) - (sem_ids, n_ids, _, sem_varmtx) = sem + (sem_ids, n_ids, _, sem_varmtx) = Accessors.Internal.semantics(semantics) if isnothing(indexes) idxs = if isnothing(ids) diff --git a/src/solver.jl b/src/solver.jl index 58aa98d0b..935ad3cdb 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -120,9 +120,7 @@ values_vec(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order ``` """ function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) - sem = Accessors.Internal.get_semantics(semantics) - isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) - (_, _, _, sem_varmtx) = sem + (_, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) is_solved(res.result) ? sem_varmtx(res.model)' * value.(res.result[:x]) : nothing end @@ -151,9 +149,7 @@ values_dict(:reaction, flux_balance_analysis(model, ...)) ``` """ function values_dict(semantics::Symbol, res::ModelWithResult{<:Model}) - sem = Accessors.Internal.get_semantics(semantics) - isnothing(sem) && throw(DomainError(semantics, "Unknown semantics")) - (ids, _, _, sem_varmtx) = sem + (ids, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) is_solved(res.result) ? Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.result[:x])) : nothing end diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 23884d5f9..201cc893c 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -69,6 +69,20 @@ end """ $(TYPEDSIGNATURES) +Like [`get_semantics`](@ref) but throws a `DomainError` if the semantics is not +available. +""" +function semantics( + semantics::Symbol, +)::Types.Maybe{Tuple{Function,Function,Function,Function}} + res = get_semantics(semantics) + isnothing(res) && throw(DomainError(semantics, "unknown semantics")) + res +end + +""" +$(TYPEDSIGNATURES) + Inject a new functionality for variable semantics defined by `sym` into `themodule` (which should ideally be COBREXA.Accessors). From db830aec55c84f101e3e8261605719d140259fea Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 16:11:25 +0100 Subject: [PATCH 220/531] improve solver interface --- src/solver.jl | 60 +++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 51 insertions(+), 9 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 935ad3cdb..3cca5a682 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -3,7 +3,8 @@ module Solver Interface of COBREXA to JuMP solvers; mainly recreation of the -`AbstractMetabolicModel`s into JuMP optimization models. +`AbstractMetabolicModel`s as JuMP optimization models, and retrieval of solved +values. # Exports $(EXPORTS) @@ -56,6 +57,7 @@ function make_optimization_model( end return optimization_model + #TODO what about ModelWithResult right from this point? ;D end """ @@ -105,18 +107,41 @@ Returns the current objective value of a model, if solved. solved_objective_value(flux_balance_analysis(model, ...)) ``` """ -solved_objective_value(opt_model)::Maybe{Float64} = +solved_objective_value(opt_model::Model)::Maybe{Float64} = is_solved(opt_model) ? objective_value(opt_model) : nothing """ $(TYPEDSIGNATURES) -From the optimized model, returns a vector of values for the selected -`semantics`. If the model did not solve, returns `nothing`. +Pipeable variant of [`solved_objective_value`](@ref). +""" +solved_objective_value(x::ModelWithResult{<:Model}) = + ModelWithResult(x.model, solved_objective_value(x.result)) + +""" +$(TYPEDSIGNATURES) + +Return a vector of all variable values from the solved model, in the same order +given by [`variables`](@ref). # Example ``` -values_vec(Val(:reaction), model, flux_balance_analysis(model, ...)) # in order of reactions(model) +flux_balance_analysis(model, ...) |> values_vec +``` +""" +function values_vec(res::ModelWithResult{<:Model}) + is_solved(res.result) ? value.(res.result[:x]) : nothing +end + +""" +$(TYPEDSIGNATURES) + +Return a vector of all semantic variable values in the model, in the order +given by the corresponding semantics. + +# Example +``` +values_vec(:reaction, flux_balance_analysis(model, ...)) ``` """ function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) @@ -127,14 +152,30 @@ end """ $(TYPEDSIGNATURES) -A pipeable variant of the convenience variant of [`values_vec`](@ref). +A pipeable variant of [`values_vec`](@ref). # Example ``` flux_balance_analysis(model, ...) |> values_vec(:reaction) ``` """ -values_vec(semantics::Symbol) = res -> values_vec(semantics, res) +values_vec(semantics::Symbol) = + (res::ModelWithResult{<:Model}) -> values_vec(semantics, res) + +""" +$(TYPEDSIGNATURES) + +Return a dictionary of all variable values from the solved model mapped +to their IDs. + +# Example +``` +flux_balance_analysis(model, ...) |> values_dict +``` +""" +function values_dict(res::ModelWithResult{<:Model}) + is_solved(res.result) ? Dict(variables(res.model) .=> value.(res.result[:x])) : nothing +end """ $(TYPEDSIGNATURES) @@ -157,14 +198,15 @@ end """ $(TYPEDSIGNATURES) -A pipeable variant of the convenience variant of [`values_dict`](@ref). +A pipeable variant of [`values_dict`](@ref). # Example ``` flux_balance_analysis(model, ...) |> values_dict(:reaction) ``` """ -values_dict(semantics::Symbol) = res -> values_dict(semantics, res) +values_dict(semantics::Symbol) = + (res::ModelWithResult{<:Model}) -> values_dict(semantics, res) @export_locals end From 06036a3b20e31bf02913cfcbb08142aae2ecbbd2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 16:16:16 +0100 Subject: [PATCH 221/531] FluxVariablityAnalysisWithResult --- src/analysis/variability_analysis.jl | 56 ++++++++++++++++------------ 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 536d06d6e..5150005f2 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -145,28 +145,31 @@ function variability_analysis( flux_vector = [directions[:, i] for i = 1:size(directions, 2)] - return screen_optmodel_modifications( + ModelWithResult( model, - optimizer; - common_modifications = vcat( - modifications, - [ - (model, opt_model) -> begin - Z[1] > -Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] >= Z[1] - ) - Z[2] < Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] <= Z[2] - ) - end, - ], + screen_optmodel_modifications( + model, + optimizer; + common_modifications = vcat( + modifications, + [ + (model, opt_model) -> begin + Z[1] > -Inf && @constraint( + opt_model, + objective(model)' * opt_model[:x] >= Z[1] + ) + Z[2] < Inf && @constraint( + opt_model, + objective(model)' * opt_model[:x] <= Z[2] + ) + end, + ], + ), + args = tuple.([flux_vector flux_vector], [MIN_SENSE MAX_SENSE]), + analysis = (_, opt_model, flux, sense) -> + _max_variability_flux(opt_model, flux, sense, ret), + workers = workers, ), - args = tuple.([flux_vector flux_vector], [MIN_SENSE MAX_SENSE]), - analysis = (_, opt_model, flux, sense) -> - _max_variability_flux(opt_model, flux, sense, ret), - workers = workers, ) end @@ -193,16 +196,20 @@ mins, maxs = flux_variability_analysis_dict( """ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer; kwargs...) # TODO generalize this (requires smart analysis results) - vs = flux_variability_analysis( + res = flux_variability_analysis( model, optimizer; kwargs..., ret = sol -> values_vec(:reaction, model, sol), ) - flxs = reactions(model) - dicts = zip.(Ref(flxs), vs) + flxs = reactions(res.model) + dicts = zip.(Ref(flxs), res.result) - return (Dict(flxs .=> Dict.(dicts[:, 1])), Dict(flxs .=> Dict.(dicts[:, 2]))) + ModelWithResult( + res.model, + Dict(flxs .=> Dict.(dicts[:, 1])), + Dict(flxs .=> Dict.(dicts[:, 2])), + ) end """ @@ -214,5 +221,6 @@ function _max_variability_flux(opt_model, flux, sense, ret) @objective(opt_model, sense, sum(flux .* opt_model[:x])) optimize!(opt_model) + # TODO should this get ModelWithResult ? is_solved(opt_model) ? ret(opt_model) : nothing end From 3b0a4763b2c0e9be9234ce7bbf7362ff75f78b81 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 16:27:45 +0100 Subject: [PATCH 222/531] do not re-eval the semantics symbols to functions everytime (this might be actually efficient) --- src/types/accessors/bits/semantics.jl | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 201cc893c..1a9bc609c 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -44,7 +44,7 @@ function make_mapping_dict( ) end -const variable_semantics = Symbol[] +const variable_semantics = Dict{Symbol}() """ $(TYPEDSIGNATURES) @@ -55,15 +55,7 @@ the semantics doesn't exist. function get_semantics( semantics::Symbol, )::Types.Maybe{Tuple{Function,Function,Function,Function}} - # TODO store the functions instead - if semantics in variable_semantics - return ( - Base.eval(Accessors, Symbol(semantics, :s)), - Base.eval(Accessors, Symbol(:n_, semantics, :s)), - Base.eval(Accessors, Symbol(semantics, :_variables)), - Base.eval(Accessors, Symbol(semantics, :_variables_matrix)), - ) - end + get(variable_semantics, semantics, nothing) end """ @@ -97,13 +89,12 @@ function make_variable_semantics( name::String, example::String, ) - sym in themodule.Internal.variable_semantics && return + haskey(themodule.Internal.variable_semantics, sym) && return plural = Symbol(sym, :s) count = Symbol(:n_, plural) mapping = Symbol(sym, :_variables) mapping_mtx = Symbol(sym, :_variables_matrix) - push!(themodule.Internal.variable_semantics, sym) pluralfn = Expr( :macrocall, @@ -192,9 +183,12 @@ safety reasons, this is never automatically inherited by wrappers. end), ) - code = Expr(:block, pluralfn, countfn, mappingfn, mtxfn) - - Base.eval(themodule, code) + themodule.Internal.variable_semantics[sym] = ( + Base.eval(themodule, pluralfn), + Base.eval(themodule, countfn), + Base.eval(themodule, mappingfn), + Base.eval(themodule, mtxfn), + ) end """ From a1fdf195cb83f31e0b49adfcf69be7bbab2078bc Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 19 Feb 2023 23:36:41 +0100 Subject: [PATCH 223/531] a little amount of fixes --- src/analysis/minimize_metabolic_adjustment.jl | 2 +- .../parsimonious_flux_balance_analysis.jl | 44 ++----------- src/analysis/screening.jl | 2 +- src/analysis/variability_analysis.jl | 18 ++--- src/reconstruction/ObjectModel.jl | 22 +------ src/solver.jl | 23 ++++--- src/types/accessors/bits/semantics.jl | 21 +++--- test/analysis/flux_balance_analysis.jl | 66 ++++++++++--------- test/analysis/knockouts.jl | 46 ++++++------- test/analysis/loopless.jl | 11 ++-- test/analysis/max_min_driving_force.jl | 40 ++++++----- .../analysis/minimize_metabolic_adjustment.jl | 2 +- .../parsimonious_flux_balance_analysis.jl | 19 +++--- test/analysis/screening.jl | 14 ++-- test/analysis/variability_analysis.jl | 50 +++++++------- test/reconstruction/constrained_allocation.jl | 45 ++++++------- test/reconstruction/enzyme_constrained.jl | 20 +++--- .../simplified_enzyme_constrained.jl | 11 ++-- test/types/BalancedGrowthCommunityModel.jl | 6 +- test/types/FluxSummary.jl | 11 ++-- test/types/FluxVariabilitySummary.jl | 13 ++-- test/utils/ObjectModel.jl | 13 ++-- test/utils/fluxes.jl | 11 ++-- test/utils/reaction.jl | 11 ++-- 24 files changed, 242 insertions(+), 279 deletions(-) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 502eeb33e..9008cbadc 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -23,7 +23,7 @@ reference. # Example ``` model = load_model("e_coli_core.json") -reference_flux = flux_balance_analysis_vec(model, Gurobi.Optimizer) +reference_flux = flux_balance_analysis(model, Gurobi.Optimizer) |> value_vec optmodel = minimize_metabolic_adjustment( model, reference_flux, diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index cfffeb412..fe341386e 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -43,8 +43,7 @@ if something went wrong). # Example ``` model = load_model("e_coli_core.json") -optmodel = parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) -value.(solution[:x]) # extract the flux from the optimizer +parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec ``` """ function parsimonious_flux_balance_analysis( @@ -55,8 +54,8 @@ function parsimonious_flux_balance_analysis( relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], ) # Run FBA - opt_model = flux_balance_analysis(model, optimizer; modifications = modifications) - is_solved(opt_model) || return opt_model # FBA failed + opt_model = flux_balance_analysis(model, optimizer; modifications) |> result + is_solved(opt_model) || return ModelWithResult(model, opt_model) # FBA failed # get the objective Z = objective_value(opt_model) @@ -83,40 +82,5 @@ function parsimonious_flux_balance_analysis( unregister(opt_model, :pfba_constraint) end - return opt_model + return ModelWithResult(model, opt_model) end - -""" -$(TYPEDSIGNATURES) - -Perform parsimonious flux balance analysis on `model` using `optimizer`. -Returns a vector of fluxes in the same order as the reactions in `model`. -Arguments are forwarded to [`parsimonious_flux_balance_analysis`](@ref) -internally. - -This function is kept for backwards compatibility, use [`values_vec`](@ref) -instead. -""" -parsimonious_flux_balance_analysis_vec(model::AbstractMetabolicModel, args...; kwargs...) = - values_vec( - :reaction, - model, - parsimonious_flux_balance_analysis(model, args...; kwargs...), - ) - -""" -$(TYPEDSIGNATURES) - -Perform parsimonious flux balance analysis on `model` using `optimizer`. -Returns a dictionary mapping the reaction IDs to fluxes. Arguments are -forwarded to [`parsimonious_flux_balance_analysis`](@ref) internally. - -This function is kept for backwards compatibility, use [`values_dict`](@ref) -instead. -""" -parsimonious_flux_balance_analysis_dict(model::AbstractMetabolicModel, args...; kwargs...) = - values_dict( - :reaction, - model, - parsimonious_flux_balance_analysis(model, args...; kwargs...), - ) diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl index 043b54ab5..7e3707401 100644 --- a/src/analysis/screening.jl +++ b/src/analysis/screening.jl @@ -98,7 +98,7 @@ screen(m, [reverse_reaction(5)], [reverse_reaction(3), reverse_reaction(6)] ], - analysis = mod -> flux_balance_analysis_vec(mod, GLPK.Optimizer)) + analysis = mod -> flux_balance_analysis(mod, GLPK.Optimizer) |> value_vec) ``` """ screen(args...; kwargs...) = diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 5150005f2..9ea5555ce 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -136,12 +136,13 @@ function variability_analysis( ) end - Z = bounds( - isnothing(optimal_objective_value) ? - objective_value( - flux_balance_analysis(model, optimizer; modifications = modifications), - ) : optimal_objective_value, - ) + if isnothing(optimal_objective_value) + optimal_objective_value = + flux_balance_analysis(model, optimizer; modifications = modifications) |> + solved_objective_value + end + isnothing(optimal_objective_value) && error("model has no feasible solution for FVA") + Z = bounds(optimal_objective_value) flux_vector = [directions[:, i] for i = 1:size(directions, 2)] @@ -200,15 +201,14 @@ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer model, optimizer; kwargs..., - ret = sol -> values_vec(:reaction, model, sol), + ret = sol -> values_vec(:reaction, ModelWithResult(model, sol)), ) flxs = reactions(res.model) dicts = zip.(Ref(flxs), res.result) ModelWithResult( res.model, - Dict(flxs .=> Dict.(dicts[:, 1])), - Dict(flxs .=> Dict.(dicts[:, 2])), + (Dict(flxs .=> Dict.(dicts[:, 1])), Dict(flxs .=> Dict.(dicts[:, 2]))), ) end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index b4098ae95..c67bb55db 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -385,29 +385,11 @@ function add_virtualribosome( virtualribosome_id = "virtualribosome", ) m = copy(model) - - # unidirectional biomass m.reactions = copy(model.reactions) m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) - m.reactions[biomass_rxn_id].lower_bound = 0.0 - m.reactions[biomass_rxn_id].upper_bound = constants.default_reaction_bound - - # add ribosome kinetics - if !isnothing(model.reactions[biomass_rxn_id].gene_associations) - m.reactions[biomass_rxn_id].gene_associations = - copy(model.reactions[biomass_rxn_id].gene_associations) - end - m.reactions[biomass_rxn_id].gene_associations = [ - Isozyme( - kcat_forward = weight, - kcat_backward = 0.0, - gene_product_stoichiometry = Dict(virtualribosome_id => 1.0), - ), - ] - - # add ribosome gene m.genes = copy(model.genes) - m.genes[virtualribosome_id] = Gene(id = virtualribosome_id, product_molar_mass = 1.0) + + add_virtualribosome!(m, biomass_rxn_id, weight; virtualribosome_id) m end diff --git a/src/solver.jl b/src/solver.jl index 3cca5a682..bf0ae31c2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -67,7 +67,15 @@ Return `true` if `opt_model` solved successfully (solution is optimal or locally optimal). Return `false` if any other termination status is reached. Termination status is defined in the documentation of `JuMP`. """ -is_solved(opt_model) = termination_status(opt_model) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +is_solved(opt_model::Model) = + termination_status(opt_model) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] + +""" +$(TYPEDSIGNATURES) + +Variant of is_solved that works with [`ModelWithResult`](@ref). +""" +is_solved(r::ModelWithResult{<:Model}) = is_solved(r.result) """ $(TYPEDSIGNATURES) @@ -101,11 +109,6 @@ end $(TYPEDSIGNATURES) Returns the current objective value of a model, if solved. - -# Example -``` -solved_objective_value(flux_balance_analysis(model, ...)) -``` """ solved_objective_value(opt_model::Model)::Maybe{Float64} = is_solved(opt_model) ? objective_value(opt_model) : nothing @@ -114,9 +117,13 @@ solved_objective_value(opt_model::Model)::Maybe{Float64} = $(TYPEDSIGNATURES) Pipeable variant of [`solved_objective_value`](@ref). + +# Example +``` +flux_balance_analysis(model, ...) |> solved_objective_value +``` """ -solved_objective_value(x::ModelWithResult{<:Model}) = - ModelWithResult(x.model, solved_objective_value(x.result)) +solved_objective_value(x::ModelWithResult{<:Model}) = solved_objective_value(x.result) """ $(TYPEDSIGNATURES) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 1a9bc609c..a617a4ceb 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -44,7 +44,9 @@ function make_mapping_dict( ) end -const variable_semantics = Dict{Symbol}() +const Semantic = Tuple{Function,Function,Function,Function} + +const variable_semantics = Dict{Symbol,Semantic}() """ $(TYPEDSIGNATURES) @@ -52,9 +54,7 @@ $(TYPEDSIGNATURES) Get a tuple of functions that work with the given semantics, or `nothing` if the semantics doesn't exist. """ -function get_semantics( - semantics::Symbol, -)::Types.Maybe{Tuple{Function,Function,Function,Function}} +function get_semantics(semantics::Symbol)::Types.Maybe{Semantic} get(variable_semantics, semantics, nothing) end @@ -64,9 +64,7 @@ $(TYPEDSIGNATURES) Like [`get_semantics`](@ref) but throws a `DomainError` if the semantics is not available. """ -function semantics( - semantics::Symbol, -)::Types.Maybe{Tuple{Function,Function,Function,Function}} +function semantics(semantics::Symbol)::Types.Maybe{Semantic} res = get_semantics(semantics) isnothing(res) && throw(DomainError(semantics, "unknown semantics")) res @@ -183,12 +181,9 @@ safety reasons, this is never automatically inherited by wrappers. end), ) - themodule.Internal.variable_semantics[sym] = ( - Base.eval(themodule, pluralfn), - Base.eval(themodule, countfn), - Base.eval(themodule, mappingfn), - Base.eval(themodule, mtxfn), - ) + Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn]) + themodule.Internal.variable_semantics[sym] = + Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx)) end """ diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index 4ccc211dc..ed40b316d 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -1,13 +1,13 @@ @testset "Flux balance analysis with MatrixModel" begin cp = test_simpleLP() - lp = flux_balance_analysis(cp, Tulip.Optimizer) + lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result @test termination_status(lp) == MOI.OPTIMAL sol = JuMP.value.(lp[:x]) @test sol ≈ [1.0, 2.0] # test the maximization of the objective cp = test_simpleLP2() - lp = flux_balance_analysis(cp, Tulip.Optimizer) + lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result @test termination_status(lp) == MOI.OPTIMAL sol = JuMP.value.(lp[:x]) @test sol ≈ [-1.0, 2.0] @@ -16,16 +16,16 @@ cp = load_model(MatrixModel, model_paths["iJR904.mat"]) expected_optimum = 0.9219480950504393 - lp = flux_balance_analysis(cp, Tulip.Optimizer) + lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result @test termination_status(lp) == MOI.OPTIMAL sol = JuMP.value.(lp[:x]) @test isapprox(objective_value(lp), expected_optimum, atol = TEST_TOLERANCE) @test isapprox(cp.c' * sol, expected_optimum, atol = TEST_TOLERANCE) # test the "nicer output" variants - fluxes_vec = flux_balance_analysis_vec(cp, Tulip.Optimizer) + fluxes_vec = flux_balance_analysis(cp, Tulip.Optimizer) |> values_vec @test all(fluxes_vec .== sol) - fluxes_dict = flux_balance_analysis_dict(cp, Tulip.Optimizer) + fluxes_dict = flux_balance_analysis(cp, Tulip.Optimizer) |> values_dict rxns = variables(cp) @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) end @@ -34,16 +34,18 @@ end model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - ], - ) + sol = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [ + change_objective("BIOMASS_Ecoli_core_w_GAM"), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + change_sense(MAX_SENSE), + change_optimizer_attribute("IPM_IterationsLimit", 110), + ], + ) |> values_dict + @test isapprox( sol["BIOMASS_Ecoli_core_w_GAM"], 1.0572509997013568, @@ -52,33 +54,35 @@ end pfl_frac = 0.8 biomass_frac = 0.2 - sol_multi = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective( - ["BIOMASS_Ecoli_core_w_GAM", "PFL"]; - weights = [biomass_frac, pfl_frac], - ), - ], - ) + sol_multi = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [ + change_objective( + ["BIOMASS_Ecoli_core_w_GAM", "PFL"]; + weights = [biomass_frac, pfl_frac], + ), + ], + ) |> values_dict + @test isapprox( biomass_frac * sol_multi["BIOMASS_Ecoli_core_w_GAM"] + pfl_frac * sol_multi["PFL"], 31.999999998962604, atol = TEST_TOLERANCE, ) - @test_throws DomainError flux_balance_analysis_dict( + @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; modifications = [change_constraint("gbbrsh"; lower_bound = -12, upper_bound = -12)], - ) - @test_throws DomainError flux_balance_analysis_dict( + ) |> values_dict + @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; modifications = [change_objective("gbbrsh")], - ) - @test_throws DomainError flux_balance_analysis_dict( + ) |> values_dict + @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; modifications = [change_objective(["BIOMASS_Ecoli_core_w_GAM"; "gbbrsh"])], @@ -114,6 +118,6 @@ end cmodel = MatrixModelWithCoupling(model, C, clb, cub) # construct - dc = flux_balance_analysis_dict(cmodel, Tulip.Optimizer) + dc = flux_balance_analysis(cmodel, Tulip.Optimizer) |> values_dict @test isapprox(dc["BIOMASS_Ecoli_core_w_GAM"], 0.665585699298256, atol = TEST_TOLERANCE) end diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index 5dc12f8ce..c7b691691 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -115,34 +115,36 @@ end load_model(model_paths["e_coli_core.json"]), #then on JSONModel with the same contents ] - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - knockout(["b0978", "b0734"]), # knockouts out cytbd - ], - ) + sol = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [ + change_objective("BIOMASS_Ecoli_core_w_GAM"), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + change_sense(MAX_SENSE), + change_optimizer_attribute("IPM_IterationsLimit", 110), + knockout(["b0978", "b0734"]), # knockouts out cytbd + ], + ) |> values_dict @test isapprox( sol["BIOMASS_Ecoli_core_w_GAM"], 0.2725811189335953, atol = TEST_TOLERANCE, ) - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), - knockout("b2779"), # knockouts out enolase - ], - ) + sol = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [ + change_objective("BIOMASS_Ecoli_core_w_GAM"), + change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + change_sense(MAX_SENSE), + change_optimizer_attribute("IPM_IterationsLimit", 110), + knockout("b2779"), # knockouts out enolase + ], + ) |> values_dict @test isapprox(sol["BIOMASS_Ecoli_core_w_GAM"], 0.0, atol = TEST_TOLERANCE) end end diff --git a/test/analysis/loopless.jl b/test/analysis/loopless.jl index e54cab549..55d8cdf99 100644 --- a/test/analysis/loopless.jl +++ b/test/analysis/loopless.jl @@ -2,11 +2,12 @@ model = load_model(model_paths["e_coli_core.json"]) - sol = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) + sol = + flux_balance_analysis( + model, + GLPK.Optimizer; + modifications = [add_loopless_constraints()], + ) |> values_dict @test isapprox( sol["BIOMASS_Ecoli_core_w_GAM"], diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 4d638a701..646b55529 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -2,11 +2,12 @@ model = load_model(model_paths["e_coli_core.json"]) - flux_solution = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) + flux_solution = + flux_balance_analysis( + model, + GLPK.Optimizer; + modifications = [add_loopless_constraints()], + ) |> values_dict mmdfm = make_max_min_driving_force_model( model; @@ -24,30 +25,27 @@ ignore_reaction_ids = ["H2Ot"], ) - opt_model = flux_balance_analysis( + x = flux_balance_analysis( mmdfm, Tulip.Optimizer; modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) # get mmdf - @test isapprox( - solved_objective_value(opt_model), - 1.7661155558545698, - atol = TEST_TOLERANCE, - ) + @test isapprox(x |> solved_objective_value, 1.7661155558545698, atol = TEST_TOLERANCE) # values_dict(:reaction, mmdfm, opt_model) # TODO throw missing semantics error - @test length(values_dict(:metabolite_log_concentration, mmdfm, opt_model)) == 72 - @test length(values_dict(:gibbs_free_energy_reaction, mmdfm, opt_model)) == 95 - - sols = variability_analysis( - Val(:gibbs_free_energy_reaction), - mmdfm, - Tulip.Optimizer; - bounds = gamma_bounds(0.9), - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) + @test length(x |> values_dict(:metabolite_log_concentration)) == 72 + @test length(x |> values_dict(:gibbs_free_energy_reaction)) == 95 + + sols = + variability_analysis( + :gibbs_free_energy_reaction, + mmdfm, + Tulip.Optimizer; + bounds = gamma_bounds(0.9), + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) |> result pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reactions(mmdfm))) @test isapprox(sols[pyk_idx, 2], -1.5895040002691128; atol = TEST_TOLERANCE) diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl index 2661c70b1..e064f939b 100644 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ b/test/analysis/minimize_metabolic_adjustment.jl @@ -9,7 +9,7 @@ sol, Clarabel.Optimizer; modifications = [silence], - ) |> values_dict(:reaction, model) + ) |> values_dict(:reaction) @test isapprox(moma["biomass1"], 0.07692307692307691, atol = QP_TEST_TOLERANCE) end diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 511008572..eb9320730 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -1,15 +1,16 @@ @testset "Parsimonious flux balance analysis with ObjectModel" begin model = test_toyModel() - d = parsimonious_flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - change_constraint("EX_m1(e)", lower_bound = -10.0), - change_optimizer_attribute("IPM_IterationsLimit", 500), - ], - qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], - ) + d = + parsimonious_flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [ + change_constraint("EX_m1(e)", lower_bound = -10.0), + change_optimizer_attribute("IPM_IterationsLimit", 500), + ], + qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], + ) |> values_dict # The used optimizer doesn't really converge to the same answer everytime # here, we therefore tolerate a wide range of results. diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl index accdee175..f2ea28353 100644 --- a/test/analysis/screening.jl +++ b/test/analysis/screening.jl @@ -37,7 +37,8 @@ @test screen_variants( m, [[quad_rxn(i)] for i = 1:3], - m -> Analysis.flux_balance_analysis_vec(m, Tulip.Optimizer); + m -> + Analysis.flux_balance_analysis(m, Tulip.Optimizer) |> COBREXA.Solver.values_vec; workers = W, ) == [ [250.0, -250.0, -1000.0, 250.0, 1000.0, 250.0, 250.0], @@ -48,11 +49,12 @@ # test solver modifications @test screen( m; - analysis = (m, sense) -> Analysis.flux_balance_analysis_vec( - m, - Tulip.Optimizer; - modifications = [change_sense(sense)], - ), + analysis = (m, sense) -> + Analysis.flux_balance_analysis( + m, + Tulip.Optimizer; + modifications = [change_sense(sense)], + ) |> COBREXA.Solver.values_vec, args = [(MIN_SENSE,), (MAX_SENSE,)], ) == [ [-500.0, -500.0, -1000.0, 500.0, 1000.0, -500.0, -500.0], diff --git a/test/analysis/variability_analysis.jl b/test/analysis/variability_analysis.jl index baedc3282..2eceadca9 100644 --- a/test/analysis/variability_analysis.jl +++ b/test/analysis/variability_analysis.jl @@ -1,7 +1,7 @@ @testset "Flux variability analysis" begin cp = test_simpleLP() optimizer = Tulip.Optimizer - fluxes = flux_variability_analysis(cp, optimizer) + fluxes = flux_variability_analysis(cp, optimizer) |> result @test size(fluxes) == (2, 2) @test fluxes ≈ [ @@ -9,10 +9,10 @@ 2.0 2.0 ] - rates = variability_analysis(cp, optimizer) + rates = variability_analysis(cp, optimizer) |> result @test fluxes == rates - fluxes = flux_variability_analysis(cp, optimizer, reaction_indexes = [2]) + fluxes = flux_variability_analysis(cp, optimizer, reaction_indexes = [2]) |> result @test size(fluxes) == (1, 2) @test isapprox(fluxes, [2 2], atol = TEST_TOLERANCE) @@ -27,7 +27,7 @@ ["r$x" for x = 1:3], ["m1"], ) - fluxes = flux_variability_analysis(cp, optimizer) + fluxes = flux_variability_analysis(cp, optimizer) |> result @test isapprox( fluxes, [ @@ -37,7 +37,7 @@ ], atol = TEST_TOLERANCE, ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = gamma_bounds(0.5)) + fluxes = flux_variability_analysis(cp, optimizer; bounds = gamma_bounds(0.5)) |> result @test isapprox( fluxes, [ @@ -47,7 +47,7 @@ ], atol = TEST_TOLERANCE, ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = _ -> (0, Inf)) + fluxes = flux_variability_analysis(cp, optimizer; bounds = _ -> (0, Inf)) |> result @test isapprox( fluxes, [ @@ -58,7 +58,9 @@ atol = TEST_TOLERANCE, ) - @test isempty(flux_variability_analysis(cp, Tulip.Optimizer, reaction_ids = String[])) + @test isempty( + flux_variability_analysis(cp, Tulip.Optimizer, reaction_ids = String[]) |> result, + ) @test_throws DomainError flux_variability_analysis( cp, Tulip.Optimizer, @@ -74,12 +76,13 @@ end @testset "Parallel FVA" begin cp = test_simpleLP() - fluxes = flux_variability_analysis( - cp, - Tulip.Optimizer; - workers = W, - reaction_indexes = [1, 2], - ) + fluxes = + flux_variability_analysis( + cp, + Tulip.Optimizer; + workers = W, + reaction_indexes = [1, 2], + ) |> result @test isapprox( fluxes, [ @@ -92,16 +95,17 @@ end @testset "Flux variability analysis with ObjectModel" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - mins, maxs = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), - change_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), - ], - ) + mins, maxs = + flux_variability_analysis_dict( + model, + Tulip.Optimizer; + bounds = objective_bounds(0.99), + modifications = [ + change_optimizer_attribute("IPM_IterationsLimit", 500), + change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), + change_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), + ], + ) |> result @test isapprox(maxs["EX_ac_e"]["EX_ac_e"], 8.5185494, atol = TEST_TOLERANCE) @test isapprox(mins["EX_ac_e"]["EX_ac_e"], 7.4483887, atol = TEST_TOLERANCE) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 252c5c970..7a8377f35 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -1,10 +1,6 @@ @testset "Constrained allocation FBA" begin m = ObjectModel() - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") add_reactions!( m, @@ -18,13 +14,6 @@ ], ) - gs = [ - Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) - Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) - Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) - Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) - ] - m.reactions["r3"].gene_associations = [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] m.reactions["r4"].gene_associations = [ @@ -47,8 +36,15 @@ ] m.objective = Dict("r6" => 1.0) - add_genes!(m, gs) - add_metabolites!(m, [m1, m2, m3, m4]) + add_genes!( + m, + [ + Gene(id = "g$i", product_upper_bound = 10.0, product_molar_mass = float(i)) for + i = 1:4 + ], + ) + + add_metabolites!(m, [Metabolite("m$i") for i = 1:4]) ribomodel = m |> with_virtualribosome("r6", 0.2) @@ -61,12 +57,12 @@ @test coupling(cam)[1, 7] == 5.0 - rxn_fluxes = flux_balance_analysis_dict( - cam, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - + rxn_fluxes = + flux_balance_analysis( + cam, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) |> values_dict(:reaction) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) # test inplace variant @@ -75,11 +71,12 @@ @test coupling(cam)[1, 7] == 5.0 - rxn_fluxes = flux_balance_analysis_dict( - cam, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) + rxn_fluxes = + flux_balance_analysis( + cam, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) |> values_dict(:reaction) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) # test with_isozyme functions diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 18f3dcb06..7d759a0e1 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -45,14 +45,14 @@ ) |> with_enzyme_constraints(total_gene_product_mass) - opt_model = flux_balance_analysis( + res = flux_balance_analysis( gm, Tulip.Optimizer; modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = values_dict(:reaction, gm, opt_model) - prot_concens = values_dict(:enzyme, gm, opt_model) + rxn_fluxes = values_dict(:reaction, res) + prot_concens = values_dict(:enzyme, res) @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], @@ -60,7 +60,7 @@ atol = TEST_TOLERANCE, ) - mass_groups = values_dict(:enzyme_group, gm, opt_model) + mass_groups = values_dict(:enzyme_group, res) @test isapprox( sum(values(prot_concens)), @@ -75,7 +75,7 @@ # test enzyme objective growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 - opt_model = flux_balance_analysis( + res = flux_balance_analysis( gm, Tulip.Optimizer; modifications = [ @@ -84,7 +84,7 @@ change_optimizer_attribute("IPM_IterationsLimit", 1000), ], ) - mass_groups_min = values_dict(:enzyme_group, gm, opt_model) + mass_groups_min = values_dict(:enzyme_group, res) @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] end @@ -143,15 +143,15 @@ end gene_product_mass_group_bound = Dict("uncategorized" => 0.5, "bound2" => 0.04), ) - opt_model = flux_balance_analysis( + res = flux_balance_analysis( gm, Tulip.Optimizer; modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = values_dict(:reaction, gm, opt_model) - gene_products = values_dict(:enzyme, gm, opt_model) - mass_groups = values_dict(:enzyme_group, gm, opt_model) + rxn_fluxes = values_dict(:reaction, res) + gene_products = values_dict(:enzyme, res) + mass_groups = values_dict(:enzyme_group, res) @test isapprox(rxn_fluxes["r6"], 1.1688888886502442, atol = TEST_TOLERANCE) @test isapprox(gene_products["g4"], 0.02666666666304931 * 4.0, atol = TEST_TOLERANCE) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index b385d23f8..82257b676 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -36,11 +36,12 @@ ) |> with_simplified_enzyme_constraints(total_enzyme_capacity = 100.0) - rxn_fluxes = flux_balance_analysis_dict( - simplified_enzyme_constrained_model, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) + rxn_fluxes = + flux_balance_analysis( + simplified_enzyme_constrained_model, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) |> values_dict @test isapprox( rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 2c175a2d6..89f946cba 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -235,7 +235,7 @@ end env_met_flux_bounds = Dict("glc__D_e" => (-10, 10)), ) - d = flux_balance_analysis_dict(cm, Tulip.Optimizer) + d = flux_balance_analysis(cm, Tulip.Optimizer) |> values_dict @test isapprox(d[cm.objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) @@ -330,12 +330,12 @@ end cm = BalancedGrowthCommunityModel(members = [cm1, cm2]) - opt_model = flux_balance_analysis( + res = flux_balance_analysis( cm, Tulip.Optimizer; modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - f_d = values_dict(:reaction, cm, opt_model) + f_d = values_dict(:reaction, res) @test isapprox(f_d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) end diff --git a/test/types/FluxSummary.jl b/test/types/FluxSummary.jl index 1f9601f5e..ca66f7b19 100644 --- a/test/types/FluxSummary.jl +++ b/test/types/FluxSummary.jl @@ -1,11 +1,12 @@ @testset "Flux summary" begin model = load_model(model_paths["e_coli_core.json"]) - sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 200)], - ) + sol = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 200)], + ) |> values_dict fr = flux_summary(sol; keep_unbounded = true, large_flux_bound = 25) diff --git a/test/types/FluxVariabilitySummary.jl b/test/types/FluxVariabilitySummary.jl index 1ed04b8c8..469023f12 100644 --- a/test/types/FluxVariabilitySummary.jl +++ b/test/types/FluxVariabilitySummary.jl @@ -1,12 +1,13 @@ @testset "Flux variability summary" begin model = load_model(model_paths["e_coli_core.json"]) - sol = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.90), - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 2000)], - ) + sol = + flux_variability_analysis_dict( + model, + Tulip.Optimizer; + bounds = objective_bounds(0.90), + modifications = [change_optimizer_attribute("IPM_IterationsLimit", 2000)], + ) |> result fr = flux_variability_summary(sol) @test isapprox( diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl index f620a00f5..490d2a585 100644 --- a/test/utils/ObjectModel.jl +++ b/test/utils/ObjectModel.jl @@ -2,13 +2,14 @@ model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # FBA - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) + fluxes = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + ) |> values_dict - # bounds setting + # bounds setting # TODO why is this here? cbm = make_optimization_model(model, Tulip.Optimizer) ubs = cbm[:ubs] lbs = cbm[:lbs] diff --git a/test/utils/fluxes.jl b/test/utils/fluxes.jl index ad3906138..b5c821b5c 100644 --- a/test/utils/fluxes.jl +++ b/test/utils/fluxes.jl @@ -1,11 +1,12 @@ @testset "Flux utilities" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) + fluxes = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + ) |> values_dict consuming, producing = metabolite_fluxes(model, fluxes) @test isapprox(consuming["atp_c"]["PFK"], -7.47738; atol = TEST_TOLERANCE) diff --git a/test/utils/reaction.jl b/test/utils/reaction.jl index 149f1ea43..6ac2a5898 100644 --- a/test/utils/reaction.jl +++ b/test/utils/reaction.jl @@ -2,11 +2,12 @@ model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # FBA - fluxes = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], - ) + fluxes = + flux_balance_analysis( + model, + Tulip.Optimizer; + modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + ) |> values_dict # test if reaction is balanced @test reaction_mass_balanced(model, "PFL") From 00960881ef442be132de6f4708c9cd7c5b91bc24 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 20 Feb 2023 11:05:53 +0100 Subject: [PATCH 224/531] avoid regression in JuliaFormatter --- .github/workflows/pr-format.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr-format.yml b/.github/workflows/pr-format.yml index 98b3bbbeb..b23cbc932 100644 --- a/.github/workflows/pr-format.yml +++ b/.github/workflows/pr-format.yml @@ -20,7 +20,7 @@ jobs: gh pr checkout ${{ github.event.issue.number }} - name: Install JuliaFormatter and format run: | - julia --color=yes -e 'import Pkg; Pkg.add("JuliaFormatter")' + julia --color=yes -e 'import Pkg; Pkg.add(name="JuliaFormatter",version="1.0.22")' julia --color=yes -e 'using JuliaFormatter; format(".")' - name: Remove trailing whitespace run: | From 701f629b9c74a5376d2ca824297662f9a1be5299 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 13:45:41 +0100 Subject: [PATCH 225/531] add gene change bounds functions --- src/reconstruction/ObjectModel.jl | 90 ++++++++++++++++++++++++++++- src/reconstruction/pipes/generic.jl | 18 ++++++ 2 files changed, 106 insertions(+), 2 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 6c518c5f1..99750f503 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -195,8 +195,11 @@ end @_change_bounds_fn ObjectModel String plural begin n = copy(model) n.reactions = copy(model.reactions) - for i in rxn_ids - n.reactions[i] = copy(n.reactions[i]) + for rid in rxn_ids + n.reactions[rid] = copy(model.reactions[rid]) + for field in fieldnames(model.reactions[rid]) + setfield!(n.reactions[rid], field, getfield(model.reactions[rid], field)) + end end change_bounds!(n, rxn_ids; lower_bounds, upper_bounds) return n @@ -475,3 +478,86 @@ function add_isozymes( m end + +""" +$(TYPEDSIGNATURES) + +Changes the `product_lower_bound` or `product_upper_bound` for the +[`Gene`][(ref)s listed in `gids` in the `model`, in place. +""" +function change_gene_product_bounds!( + model::ObjectModel, + gids::Vector{String}; + lower_bounds = fill(nothing, length(gids)), + upper_bounds = fill(nothing, length(gids)), +) + for (i, gid) in enumerate(gids) + + isnothing(lower_bounds[i]) || begin + (model.genes[gid].product_lower_bound = lower_bounds[i]) + end + + isnothing(upper_bounds[i]) || begin + (model.genes[gid].product_upper_bound = upper_bounds[i]) + end + end +end + +""" +$(TYPEDSIGNATURES) + +Changes the `product_lower_bound` or `product_upper_bound` for the +[`Gene`][(ref) `gid` in the `model`, in place. +""" +function change_gene_product_bound!( + model::ObjectModel, + gid::String; + lower_bound = nothing, + upper_bound = nothing, +) + change_gene_product_bounds!(model, [gid]; lower_bounds = [lower_bound], upper_bounds = [upper_bound]) +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`change_gene_product_bounds!`](@ref) that does not modify the +original model, but makes a shallow copy with the modification included. +""" +function change_gene_product_bounds( + model::ObjectModel, + gids::Vector{String}; + lower_bounds = fill(nothing, length(gids)), + upper_bounds = fill(nothing, length(gids)), +) + m = copy(model) + m.genes = copy(model.genes) + for gid in gids + m.genes[gid] = copy(model.genes[gid]) + for field in fieldnames(typeof(m.genes[gid])) + setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) + end + end + change_gene_product_bounds!(m, gids; lower_bounds, upper_bounds) + m +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`change_gene_product_bound!`](@ref) that does not modify the +original model, but makes a shallow copy with the modification included. +""" +function change_gene_product_bound( + model::ObjectModel, + gid::String; + lower_bound = nothing, + upper_bound = nothing, +) + change_gene_product_bounds( + model, + [gid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) +end diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index 54e632e04..d71e90954 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -103,3 +103,21 @@ Forwards arguments to [`add_biomass_metabolite`](@ref). """ with_added_biomass_metabolite(args...; kwargs...) = m -> add_biomass_metabolite(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with the bounds changed for the gene product. Forwards +the arguments to [`change_gene_product_bound`](@ref). +""" +with_changed_gene_product_bound(args...; kwargs...) = + m -> change_gene_product_bound(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Plural version of [`with_changed_gene_product_bound`](@ref), calls +[`change_gene_product_bounds`](@ref) internally. +""" +with_changed_gene_product_bounds(args...; kwargs...) = + m -> change_gene_product_bounds(m, args...; kwargs...) From ac177db1b79ad6d698269faedf896df87755654a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 13:56:17 +0100 Subject: [PATCH 226/531] fix change_bound and extend change_objective --- src/reconstruction/ObjectModel.jl | 58 +++++++++++++++++++++++++---- src/reconstruction/pipes/generic.jl | 8 ++++ 2 files changed, 58 insertions(+), 8 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 99750f503..6c30e19fe 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -170,7 +170,6 @@ remove_gene!(model, "g1") remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) - @_change_bounds_fn ObjectModel String inplace begin isnothing(lower_bound) || (model.reactions[rxn_id].lower_bound = lower_bound) isnothing(upper_bound) || (model.reactions[rxn_id].upper_bound = upper_bound) @@ -197,7 +196,7 @@ end n.reactions = copy(model.reactions) for rid in rxn_ids n.reactions[rid] = copy(model.reactions[rid]) - for field in fieldnames(model.reactions[rid]) + for field in fieldnames(typeof(model.reactions[rid])) setfield!(n.reactions[rid], field, getfield(model.reactions[rid], field)) end end @@ -263,8 +262,9 @@ end """ $(TYPEDSIGNATURES) -Change the objective for `model` to reaction(s) with `rxn_ids`, optionally specifying their `weights`. By default, -assume equal weights. If no objective exists in model, sets objective. +Change the objective for `model` to reaction(s) with `rxn_ids`, optionally +specifying their `weights`. By default, assume equal weights. If no objective +exists in model, sets objective. """ function change_objective!( model::ObjectModel, @@ -277,7 +277,44 @@ function change_objective!( nothing end -change_objective!(model::ObjectModel, rxn_id::String) = change_objective!(model, [rxn_id]) +""" +$(TYPEDSIGNATURES) + +Variant of [`change_objective!`](@ref) that sets a single `rxn_id` as the +objective weight with `weight` (defaults to 1.0). +""" +change_objective!(model::ObjectModel, rxn_id::String; weight::Float64 = 1.0) = + change_objective!(model, [rxn_id]; weights = [weight]) + +""" +$(TYPEDSIGNATURES) + +Variant of [`change_objective!`](@ref) that does not modify the original model, +but makes a shallow copy with the modification included. +""" +function change_objective( + model::ObjectModel, + rxn_ids::Vector{String}; + weights = ones(length(rxn_ids)), +) + m = copy(model) + m.objective = copy(model.objective) + change_objective!(m, rxn_ids; weights) + m +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`change_objective!`](@ref) that does not modify the original model, +but makes a shallow copy with the modification included. +""" +function change_objective(model::ObjectModel, rxn_id::String; weight::Float64 = 1.0) + m = copy(model) + m.objective = copy(model.objective) + change_objective!(m, rxn_id; weight) + m +end """ $(TYPEDSIGNATURES) @@ -485,14 +522,14 @@ $(TYPEDSIGNATURES) Changes the `product_lower_bound` or `product_upper_bound` for the [`Gene`][(ref)s listed in `gids` in the `model`, in place. """ -function change_gene_product_bounds!( +function change_gene_product_bounds!( model::ObjectModel, gids::Vector{String}; lower_bounds = fill(nothing, length(gids)), upper_bounds = fill(nothing, length(gids)), ) for (i, gid) in enumerate(gids) - + isnothing(lower_bounds[i]) || begin (model.genes[gid].product_lower_bound = lower_bounds[i]) end @@ -515,7 +552,12 @@ function change_gene_product_bound!( lower_bound = nothing, upper_bound = nothing, ) - change_gene_product_bounds!(model, [gid]; lower_bounds = [lower_bound], upper_bounds = [upper_bound]) + change_gene_product_bounds!( + model, + [gid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) end """ diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index d71e90954..f93176fc0 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -121,3 +121,11 @@ Plural version of [`with_changed_gene_product_bound`](@ref), calls """ with_changed_gene_product_bounds(args...; kwargs...) = m -> change_gene_product_bounds(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant with the objective reaction(s) changed. Forwards the +arguments to [`change_objective`](@ref). +""" +with_changed_objective(args...; kwargs...) = m -> change_objective(m, args...; kwargs...) From 203903de1b89e34fbcaa563388f622d85ed49891 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 15:11:52 +0100 Subject: [PATCH 227/531] remove const from reexport --- src/moduletools.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moduletools.jl b/src/moduletools.jl index 8717eec9d..add1ea2f4 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -50,7 +50,7 @@ macro reexport(mods...) Base.isexported($modulename, sym) || continue typeof($(Expr(:., modulename, :sym))) == Module && continue sym in [:eval, :include] && continue - @eval const $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) + @eval $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) @eval export $(Expr(:$, :sym)) end end) From df1b708bb450513cd14130d04d631ee80c9b2397 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 15:34:11 +0100 Subject: [PATCH 228/531] add tests --- test/reconstruction/ObjectModel.jl | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 60f46c968..25ba32234 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -84,6 +84,13 @@ @test new_model.reactions["r2"].lower_bound == -220 @test new_model.reactions["r2"].upper_bound == 20 + ### objective + change_objective!(model, "r2") + @test model.objective["r2"] == 1.0 + + new_model = change_objective(model, "r1"; weight = 2.0) + @test new_model.objective["r1"] == 2.0 + ### reactions add_reactions!(model, [r3, r4]) @test length(model.reactions) == 4 @@ -123,7 +130,12 @@ remove_gene!(model, "g1") @test length(model.genes) == 4 - ### objective - change_objective!(model, "r2") - @test model.objective["r2"] == 1.0 + # change gene + change_gene_product_bound!(model, "g1"; lower_bound = -10, upper_bound = 10) + @test model.genes["g1"].product_lower_bound == -10.0 + @test model.genes["g1"].product_upper_bound == 10.0 + + new_model = change_gene_product_bound(model, "g2"; lower_bound = -10, upper_bound = 10) + @test new_model.genes["g2"].product_lower_bound == -10.0 + @test new_model.genes["g2"].product_upper_bound == 10.0 end From ea0ce570c4d84e13f7950deac1683d2c903c8fed Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 15:50:10 +0100 Subject: [PATCH 229/531] revert const in reexport --- src/moduletools.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/moduletools.jl b/src/moduletools.jl index add1ea2f4..8717eec9d 100644 --- a/src/moduletools.jl +++ b/src/moduletools.jl @@ -50,7 +50,7 @@ macro reexport(mods...) Base.isexported($modulename, sym) || continue typeof($(Expr(:., modulename, :sym))) == Module && continue sym in [:eval, :include] && continue - @eval $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) + @eval const $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) @eval export $(Expr(:$, :sym)) end end) From 1f36b40b23d4d77e1eebac123c1c4a448397f685 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 16:58:32 +0100 Subject: [PATCH 230/531] add tests for add --- src/analysis/modifications/generic.jl | 6 +- src/analysis/modifications/optimizer.jl | 6 +- src/reconstruction/ObjectModel.jl | 362 ++++++++++++++---------- src/reconstruction/pipes/generic.jl | 85 +++--- test/reconstruction/ObjectModel.jl | 6 + 5 files changed, 283 insertions(+), 182 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 8281b132f..71e542e92 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -18,7 +18,7 @@ $(TYPEDSIGNATURES) Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) of reaction `id` if supplied. """ -change_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = +modify_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin ind = first(indexin([id], variables(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) @@ -36,7 +36,7 @@ $(TYPEDSIGNATURES) Change the lower and upper bounds (`lower_bounds` and `upper_bounds` respectively) of reactions in `ids` if supplied. """ -change_constraints( +modify_constraints( ids::Vector{String}; lower_bounds = fill(nothing, length(ids)), upper_bounds = fill(nothing, length(ids)), @@ -59,7 +59,7 @@ array of reactions identifiers. Optionally, the objective can be weighted by a vector of `weights`, and a optimization `sense` can be set to either `MAX_SENSE` or `MIN_SENSE`. """ -change_objective( +modify_objective( new_objective::Union{String,Vector{String}}; weights = [], sense = MAX_SENSE, diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer.jl index 41c51bdf8..67038eca2 100644 --- a/src/analysis/modifications/optimizer.jl +++ b/src/analysis/modifications/optimizer.jl @@ -8,7 +8,7 @@ Possible arguments are `MAX_SENSE` and `MIN_SENSE`. If you want to change the objective and sense at the same time, use [`change_objective`](@ref) instead to do both at once. """ -change_sense(objective_sense) = +modify_sense(objective_sense) = (_, opt_model) -> set_objective_sense(opt_model, objective_sense) """ @@ -20,7 +20,7 @@ This may be used to try different approaches for reaching the optimum, and in problems that may require different optimizers for different parts, such as the [`parsimonious_flux_balance_analysis`](@ref). """ -change_optimizer(optimizer) = (_, opt_model) -> set_optimizer(opt_model, optimizer) +modify_optimizer(optimizer) = (_, opt_model) -> set_optimizer(opt_model, optimizer) """ $(TYPEDSIGNATURES) @@ -29,7 +29,7 @@ Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer to the JuMP documentation and the documentation of the specific optimizer for usable keys and values. """ -change_optimizer_attribute(attribute_key, value) = +modify_optimizer_attribute(attribute_key, value) = (_, opt_model) -> set_optimizer_attribute(opt_model, attribute_key, value) """ diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 6c30e19fe..a42eb5975 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -1,13 +1,19 @@ +# Add and remove reactions + """ $(TYPEDSIGNATURES) Add `rxns` to `model` based on reaction `id`. """ function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) + rxn_ids = collect(keys(model.reactions)) + idxs = filter(!isnothing, indexin([r.id for r in rxns], rxn_ids)) + isempty(idxs) || + throw(ArgumentError("Duplicated reaction IDs in model: $(rxn_ids[idxs])")) + for rxn in rxns model.reactions[rxn.id] = rxn end - nothing end """ @@ -30,7 +36,7 @@ function add_reactions(model::ObjectModel, rxns::Vector{Reaction}) m.reactions[rxn.id] = rxn end - return m + m end """ @@ -40,16 +46,47 @@ Add `rxn` to `model`, and return a shallow copied version of the model. """ add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) +@_remove_fn reaction ObjectModel String inplace begin + if !(reaction_id in variables(model)) + @models_log @info "Reaction $reaction_id not found in model." + else + delete!(model.reactions, reaction_id) + end + nothing +end + +@_remove_fn reaction ObjectModel String inplace plural begin + remove_reaction!.(Ref(model), reaction_ids) + nothing +end + +@_remove_fn reaction ObjectModel String begin + remove_reactions(model, [reaction_id]) +end + +@_remove_fn reaction ObjectModel String plural begin + n = copy(model) + n.reactions = copy(model.reactions) + remove_reactions!(n, reaction_ids) + return n +end + +# Add and remove metabolites + """ $(TYPEDSIGNATURES) Add `mets` to `model` based on metabolite `id`. """ function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) + met_ids = collect(keys(model.metabolites)) + idxs = filter(!isnothing, indexin([m.id for m in mets], met_ids)) + isempty(idxs) || + throw(ArgumentError("Duplicated metabolite IDs in model: $(met_ids[idxs])")) + for met in mets model.metabolites[met.id] = met end - nothing end """ @@ -72,7 +109,7 @@ function add_metabolites(model::ObjectModel, mets::Vector{Metabolite}) m.metabolites[met.id] = met end - return m + m end """ @@ -82,16 +119,51 @@ Add `met` to `model` and return a shallow copied version of the model. """ add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) +@_remove_fn metabolite ObjectModel String inplace begin + remove_metabolites!(model, [metabolite_id]) +end + +@_remove_fn metabolite ObjectModel String inplace plural begin + !all(in.(metabolite_ids, Ref(metabolites(model)))) && + @models_log @info "Some metabolites not found in model." + remove_reactions!( + model, + [ + rid for (rid, rn) in model.reactions if + any(haskey.(Ref(rn.metabolites), metabolite_ids)) + ], + ) + delete!.(Ref(model.metabolites), metabolite_ids) + nothing +end + +@_remove_fn metabolite ObjectModel String begin + remove_metabolites(model, [metabolite_id]) +end + +@_remove_fn metabolite ObjectModel String plural begin + n = copy(model) + n.reactions = copy(model.reactions) + n.metabolites = copy(model.metabolites) + remove_metabolites!(n, metabolite_ids) + return n +end + +# Add and remove genes + """ $(TYPEDSIGNATURES) Add `genes` to `model` based on gene `id`. """ function add_genes!(model::ObjectModel, genes::Vector{Gene}) + gene_ids = collect(keys(model.genes)) + idxs = filter(!isnothing, indexin([g.id for g in genes], gene_ids)) + isempty(idxs) || throw(ArgumentError("Duplicated gene IDs in model: $(gene_ids[idxs])")) + for gene in genes model.genes[gene.id] = gene end - nothing end """ @@ -114,7 +186,7 @@ function add_genes(model::ObjectModel, genes::Vector{Gene}) m.genes[gn.id] = gn end - return m + m end """ @@ -170,6 +242,8 @@ remove_gene!(model, "g1") remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) +# Change reaction bounds + @_change_bounds_fn ObjectModel String inplace begin isnothing(lower_bound) || (model.reactions[rxn_id].lower_bound = lower_bound) isnothing(upper_bound) || (model.reactions[rxn_id].upper_bound = upper_bound) @@ -204,61 +278,98 @@ end return n end -@_remove_fn reaction ObjectModel String inplace begin - if !(reaction_id in variables(model)) - @models_log @info "Reaction $reaction_id not found in model." - else - delete!(model.reactions, reaction_id) - end - nothing -end +# Change gene product bounds -@_remove_fn reaction ObjectModel String inplace plural begin - remove_reaction!.(Ref(model), reaction_ids) - nothing -end +""" +$(TYPEDSIGNATURES) -@_remove_fn reaction ObjectModel String begin - remove_reactions(model, [reaction_id]) -end +Changes the `product_lower_bound` or `product_upper_bound` for the +[`Gene`][(ref)s listed in `gids` in the `model`, in place. +""" +function change_gene_product_bounds!( + model::ObjectModel, + gids::Vector{String}; + lower_bounds = fill(nothing, length(gids)), + upper_bounds = fill(nothing, length(gids)), +) + for (i, gid) in enumerate(gids) -@_remove_fn reaction ObjectModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - remove_reactions!(n, reaction_ids) - return n -end + isnothing(lower_bounds[i]) || begin + (model.genes[gid].product_lower_bound = lower_bounds[i]) + end -@_remove_fn metabolite ObjectModel String inplace begin - remove_metabolites!(model, [metabolite_id]) + isnothing(upper_bounds[i]) || begin + (model.genes[gid].product_upper_bound = upper_bounds[i]) + end + end end -@_remove_fn metabolite ObjectModel String inplace plural begin - !all(in.(metabolite_ids, Ref(metabolites(model)))) && - @models_log @info "Some metabolites not found in model." - remove_reactions!( +""" +$(TYPEDSIGNATURES) + +Changes the `product_lower_bound` or `product_upper_bound` for the +[`Gene`][(ref) `gid` in the `model`, in place. +""" +function change_gene_product_bound!( + model::ObjectModel, + gid::String; + lower_bound = nothing, + upper_bound = nothing, +) + change_gene_product_bounds!( model, - [ - rid for (rid, rn) in model.reactions if - any(haskey.(Ref(rn.metabolites), metabolite_ids)) - ], + [gid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], ) - delete!.(Ref(model.metabolites), metabolite_ids) - nothing end -@_remove_fn metabolite ObjectModel String begin - remove_metabolites(model, [metabolite_id]) +""" +$(TYPEDSIGNATURES) + +Variant of [`change_gene_product_bounds!`](@ref) that does not modify the +original model, but makes a shallow copy with the modification included. +""" +function change_gene_product_bounds( + model::ObjectModel, + gids::Vector{String}; + lower_bounds = fill(nothing, length(gids)), + upper_bounds = fill(nothing, length(gids)), +) + m = copy(model) + m.genes = copy(model.genes) + for gid in gids + m.genes[gid] = copy(model.genes[gid]) + for field in fieldnames(typeof(m.genes[gid])) + setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) + end + end + change_gene_product_bounds!(m, gids; lower_bounds, upper_bounds) + m end -@_remove_fn metabolite ObjectModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - n.metabolites = copy(model.metabolites) - remove_metabolites!(n, metabolite_ids) - return n +""" +$(TYPEDSIGNATURES) + +Variant of [`change_gene_product_bound!`](@ref) that does not modify the +original model, but makes a shallow copy with the modification included. +""" +function change_gene_product_bound( + model::ObjectModel, + gid::String; + lower_bound = nothing, + upper_bound = nothing, +) + change_gene_product_bounds( + model, + [gid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) end +# Change objective + """ $(TYPEDSIGNATURES) @@ -316,12 +427,14 @@ function change_objective(model::ObjectModel, rxn_id::String; weight::Float64 = m end +# Add and remove biomass metabolite + """ $(TYPEDSIGNATURES) Add a biomass metabolite called `biomass_metabolite_id` with stoichiometry 1 to the biomass reaction, called `biomass_rxn_id` in `model`. Changes the model in -place. +place. Does not check if the model already has a biomass metabolite. """ function add_biomass_metabolite!( model::ObjectModel, @@ -336,7 +449,8 @@ end $(TYPEDSIGNATURES) Variant of [`add_biomass_metabolite!`](@ref) that does not modify the original -model, but makes a shallow copy with the modification included. +model, but makes a shallow copy with the modification included. Does not check +if the model already has a biomass metabolite. """ function add_biomass_metabolite( model::ObjectModel, @@ -359,6 +473,47 @@ end """ $(TYPEDSIGNATURES) +Remove a biomass metabolite called `biomass_metabolite_id` from +the biomass reaction, called `biomass_rxn_id` in `model`. +""" +function remove_biomass_metabolite!( + model::ObjectModel, + biomass_rxn_id::String; + biomass_metabolite_id = "biomass", +) + delete!(model.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) + remove_metabolite!(model, biomass_metabolite_id) +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`remove_biomass_metabolite!`](@ref) that does not modify the original +model, but makes a shallow copy with the modification included. +""" +function remove_biomass_metabolite( + model::ObjectModel, + biomass_rxn_id::String; + biomass_metabolite_id = "biomass", +) + m = copy(model) + m.metabolites = copy(m.metabolites) + remove_metabolite!(m, biomass_metabolite_id) + + m.reactions = copy(model.reactions) + m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) + m.reactions[biomass_rxn_id].metabolites = + copy(model.reactions[biomass_rxn_id].metabolites) + delete!(m.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) + + m +end + +# Add virtual ribosome for constrained allocation applications + +""" +$(TYPEDSIGNATURES) + To `biomass_rxn_id` in `model`, add a pseudo-isozyme that approximates the effect ribosome synthesis has on growth. @@ -439,27 +594,34 @@ function add_virtualribosome( m end +# Add, change, remove isozymes + """ $(TYPEDSIGNATURES) -Add `isozymes` to `rxn_id` in `model`. Overwrites the currently stored isozymes. Assumes genes -are already in `model`. +Add `isozymes` to `rxn_id` in `model`. """ add_isozymes!(model::ObjectModel, rxn_id::String, isozymes::Vector{Isozyme}) = - model.reactions[rxn_id].gene_associations = isozymes + add_isozymes!(model, [rxn_id], [isozymes]) """ $(TYPEDSIGNATURES) -For each pair of `isozymes` and `rxn_id` in `isozymes_vector` and -`rxn_id_vector`, call [`add_isozymes!`](@ref) to add the isozymes to the -`model`. +For each reaction in `rxn_ids`, add the corresponding isozymes in +`isozymes_vector` to `model`. """ function add_isozymes!( model::ObjectModel, - rxn_id_vector::Vector{String}, + rxn_ids::Vector{String}, isozymes_vector::Vector{Vector{Isozyme}}, ) + rxn_ids = keys(model.reactions) + idxs = filter(!isnothing, indexin(rxns, rxn_ids)) + isempty(idxs) || + throw(ArgumentError("Duplicated reaction IDs in model: $(rxn_ids[idxs])")) + isnothing(model.reactions[rxn_id].gene_associations) || + throw(ArgumentError("$rxn_id already has isozymes.")) + for (rid, isozymes) in zip(rxn_id_vector, isozymes_vector) add_isozymes!(model, rid, isozymes) end @@ -515,91 +677,3 @@ function add_isozymes( m end - -""" -$(TYPEDSIGNATURES) - -Changes the `product_lower_bound` or `product_upper_bound` for the -[`Gene`][(ref)s listed in `gids` in the `model`, in place. -""" -function change_gene_product_bounds!( - model::ObjectModel, - gids::Vector{String}; - lower_bounds = fill(nothing, length(gids)), - upper_bounds = fill(nothing, length(gids)), -) - for (i, gid) in enumerate(gids) - - isnothing(lower_bounds[i]) || begin - (model.genes[gid].product_lower_bound = lower_bounds[i]) - end - - isnothing(upper_bounds[i]) || begin - (model.genes[gid].product_upper_bound = upper_bounds[i]) - end - end -end - -""" -$(TYPEDSIGNATURES) - -Changes the `product_lower_bound` or `product_upper_bound` for the -[`Gene`][(ref) `gid` in the `model`, in place. -""" -function change_gene_product_bound!( - model::ObjectModel, - gid::String; - lower_bound = nothing, - upper_bound = nothing, -) - change_gene_product_bounds!( - model, - [gid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_gene_product_bounds!`](@ref) that does not modify the -original model, but makes a shallow copy with the modification included. -""" -function change_gene_product_bounds( - model::ObjectModel, - gids::Vector{String}; - lower_bounds = fill(nothing, length(gids)), - upper_bounds = fill(nothing, length(gids)), -) - m = copy(model) - m.genes = copy(model.genes) - for gid in gids - m.genes[gid] = copy(model.genes[gid]) - for field in fieldnames(typeof(m.genes[gid])) - setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) - end - end - change_gene_product_bounds!(m, gids; lower_bounds, upper_bounds) - m -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_gene_product_bound!`](@ref) that does not modify the -original model, but makes a shallow copy with the modification included. -""" -function change_gene_product_bound( - model::ObjectModel, - gid::String; - lower_bound = nothing, - upper_bound = nothing, -) - change_gene_product_bounds( - model, - [gid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index f93176fc0..1146d1a92 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -1,3 +1,5 @@ +# Reactions + """ $(TYPEDSIGNATURES) @@ -17,6 +19,24 @@ with_added_reaction(args...; kwargs...) = m -> add_reaction(m, args...; kwargs.. """ $(TYPEDSIGNATURES) +Specifies a model variant without a certain reaction. Forwards arguments to +[`remove_reaction`](@ref). +""" +with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Plural version of [`with_removed_reaction`](@ref), calls +[`remove_reactions`](@ref) internally. +""" +with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) + +# Metabolites + +""" +$(TYPEDSIGNATURES) + Specifies a model variant with metabolites added. Forwards the arguments to [`add_metabolites`](@ref). """ @@ -33,76 +53,75 @@ with_added_metabolite(args...; kwargs...) = m -> add_metabolite(m, args...; kwar """ $(TYPEDSIGNATURES) -Specifies a model variant with genes added. Forwards the arguments to -[`add_genes`](@ref). +Specifies a model variant without a certain metabolite. Forwards arguments to +[`remove_metabolite`](@ref). """ -with_added_genes(args...; kwargs...) = m -> add_genes(m, args...; kwargs...) +with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) """ $(TYPEDSIGNATURES) -Specifies a model variant with an added gene. Forwards the arguments to -[`add_gene`](@ref). +Plural version of [`with_removed_metabolite`](@ref), calls +[`remove_metabolites`](@ref) internally. """ -with_added_gene(args...; kwargs...) = m -> add_gene(m, args...; kwargs...) +with_removed_metabolites(args...; kwargs...) = + m -> remove_metabolites(m, args...; kwargs...) + +# Genes """ $(TYPEDSIGNATURES) -Specifies a model variant that has a new bound set. Forwards arguments to -[`change_bound`](@ref). Intended for usage with [`screen`](@ref). +Specifies a model variant with genes added. Forwards the arguments to +[`add_genes`](@ref). """ -with_changed_bound(args...; kwargs...) = m -> change_bound(m, args...; kwargs...) +with_added_genes(args...; kwargs...) = m -> add_genes(m, args...; kwargs...) """ $(TYPEDSIGNATURES) -Specifies a model variant that has new bounds set. Forwards arguments to -[`change_bounds`](@ref). Intended for usage with [`screen`](@ref). +Specifies a model variant with an added gene. Forwards the arguments to +[`add_gene`](@ref). """ -with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs...) +with_added_gene(args...; kwargs...) = m -> add_gene(m, args...; kwargs...) -""" -$(TYPEDSIGNATURES) -Specifies a model variant without a certain metabolite. Forwards arguments to -[`remove_metabolite`](@ref). -""" -with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) +# Biomass """ $(TYPEDSIGNATURES) -Plural version of [`with_removed_metabolite`](@ref), calls -[`remove_metabolites`](@ref) internally. +Specifies a model variant that adds a biomass metabolite to the biomass +reaction. Forwards arguments to [`add_biomass_metabolite`](@ref). """ -with_removed_metabolites(args...; kwargs...) = - m -> remove_metabolites(m, args...; kwargs...) +with_added_biomass_metabolite(args...; kwargs...) = + m -> add_biomass_metabolite(m, args...; kwargs...) """ $(TYPEDSIGNATURES) -Specifies a model variant without a certain reaction. Forwards arguments to -[`remove_reaction`](@ref). +Specifies a model variant that removes a biomass metabolite from the biomass +reaction. Forwards arguments to [`remove_biomass_metabolite`](@ref). """ -with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) +with_removed_biomass_metabolite(args...; kwargs...) = + m -> remove_biomass_metabolite(m, args...; kwargs...) + +# Bounds """ $(TYPEDSIGNATURES) -Plural version of [`with_removed_reaction`](@ref), calls -[`remove_reactions`](@ref) internally. +Specifies a model variant that has a new bound set. Forwards arguments to +[`change_bound`](@ref). Intended for usage with [`screen`](@ref). """ -with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) +with_changed_bound(args...; kwargs...) = m -> change_bound(m, args...; kwargs...) """ $(TYPEDSIGNATURES) -Species a model variant that adds a biomass metabolite to the biomass reaction. -Forwards arguments to [`add_biomass_metabolite`](@ref). +Plural variant of [`with_changed_bound`](@ref). """ -with_added_biomass_metabolite(args...; kwargs...) = - m -> add_biomass_metabolite(m, args...; kwargs...) +with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs...) """ $(TYPEDSIGNATURES) @@ -122,6 +141,8 @@ Plural version of [`with_changed_gene_product_bound`](@ref), calls with_changed_gene_product_bounds(args...; kwargs...) = m -> change_gene_product_bounds(m, args...; kwargs...) +# Objective + """ $(TYPEDSIGNATURES) diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 25ba32234..1b54b63d0 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -104,6 +104,8 @@ remove_reaction!(model, "r1") @test length(model.reactions) == 2 + @test_throws ArgumentError add_reaction!(model, r3) + ### metabolites add_metabolites!(model, [m5, m6]) @test length(model.metabolites) == 6 @@ -117,6 +119,8 @@ remove_metabolite!(model, "m1") @test length(model.metabolites) == 4 + @test_throws ArgumentError add_metabolite!(model, m2) + ### genes add_genes!(model, [g5, g6]) @test length(model.genes) == 6 @@ -130,6 +134,8 @@ remove_gene!(model, "g1") @test length(model.genes) == 4 + @test_throws ArgumentError add_gene!(model, g7) + # change gene change_gene_product_bound!(model, "g1"; lower_bound = -10, upper_bound = 10) @test model.genes["g1"].product_lower_bound == -10.0 From 511525d678fad8d3297df81759575517c18ea7c9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 19:51:50 +0100 Subject: [PATCH 231/531] streamlined objectmodel recon --- src/misc/check_keys.jl | 19 +++++++ src/reconstruction.jl | 2 +- src/reconstruction/ObjectModel.jl | 90 +++++++++++++----------------- test/reconstruction/ObjectModel.jl | 11 ++-- 4 files changed, 65 insertions(+), 57 deletions(-) create mode 100644 src/misc/check_keys.jl diff --git a/src/misc/check_keys.jl b/src/misc/check_keys.jl new file mode 100644 index 000000000..c3fe196a4 --- /dev/null +++ b/src/misc/check_keys.jl @@ -0,0 +1,19 @@ +""" +Throw an ArgumentError if model.field has any keys in xs::Union{Reaction, Metabolite, Gene}. +""" +function throw_argerror_if_key_found(model, field, xs) + _ids = collect(keys(getfield(model, field))) + ids = [x.id for x in xs if x.id in _ids] + isempty(ids) || throw(ArgumentError("Duplicated $field IDs already present in model: $ids")) + nothing +end + +""" +Throw an ArgumentError if model.field does not have any keys in xs. +""" +function throw_argerror_if_key_missing(model, field, xs::Vector{String}) + _ids = collect(keys(getfield(model, field))) + ids = [x for x in xs if !(x in _ids)] + isempty(ids) || throw(ArgumentError("Missing $field IDs in model: $ids")) + nothing +end diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 2783ee712..011675647 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -13,7 +13,7 @@ using ..ModuleTools @dse using ..Accessors -using ..Internal: constants +using ..Internal: constants, throw_argerror_if_key_found, throw_argerror_if_key_missing using ..Internal.Macros using ..Log.Internal using ..Types diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index a42eb5975..ab0682c3f 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -3,14 +3,11 @@ """ $(TYPEDSIGNATURES) -Add `rxns` to `model` based on reaction `id`. +Add `rxns` to `model` based on reaction `id` if the `id` is not already in the +model. """ function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) - rxn_ids = collect(keys(model.reactions)) - idxs = filter(!isnothing, indexin([r.id for r in rxns], rxn_ids)) - isempty(idxs) || - throw(ArgumentError("Duplicated reaction IDs in model: $(rxn_ids[idxs])")) - + throw_argerror_if_key_found(model, :reactions, rxns) for rxn in rxns model.reactions[rxn.id] = rxn end @@ -19,44 +16,40 @@ end """ $(TYPEDSIGNATURES) -Add `rxn` to `model` based on reaction `id`. +Add `rxn` to `model` based on reaction `id` if the `id` is not already in the +model """ add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) """ $(TYPEDSIGNATURES) -Add `rxns` to `model` and return a shallow copied version of the model. +Add `rxns` to `model` and return a shallow copied version of the model. Only +adds the `rxns` if their IDs are not already present in the model. """ function add_reactions(model::ObjectModel, rxns::Vector{Reaction}) m = copy(model) - m.reactions = copy(m.reactions) - for rxn in rxns - m.reactions[rxn.id] = rxn - end - + add_reactions!(m, rxns) m end """ $(TYPEDSIGNATURES) -Add `rxn` to `model`, and return a shallow copied version of the model. +Add `rxn` to `model`, and return a shallow copied version of the model. Only +adds the `rxn` if its ID is not already present in the model. """ add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) @_remove_fn reaction ObjectModel String inplace begin - if !(reaction_id in variables(model)) - @models_log @info "Reaction $reaction_id not found in model." - else - delete!(model.reactions, reaction_id) - end + remove_reactions!(model, [reaction_id]) nothing end @_remove_fn reaction ObjectModel String inplace plural begin - remove_reaction!.(Ref(model), reaction_ids) + throw_argerror_if_key_missing(model, :reactions, reaction_ids) + delete!.(Ref(model.reactions), reaction_ids) nothing end @@ -76,14 +69,11 @@ end """ $(TYPEDSIGNATURES) -Add `mets` to `model` based on metabolite `id`. +Add `mets` to `model` based on metabolite `id` if the `id` is not already in the +model. """ function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) - met_ids = collect(keys(model.metabolites)) - idxs = filter(!isnothing, indexin([m.id for m in mets], met_ids)) - isempty(idxs) || - throw(ArgumentError("Duplicated metabolite IDs in model: $(met_ids[idxs])")) - + throw_argerror_if_key_found(model, :metabolites, mets) for met in mets model.metabolites[met.id] = met end @@ -92,30 +82,29 @@ end """ $(TYPEDSIGNATURES) -Add `met` to `model` based on metabolite `id`. +Add `met` to `model` based on metabolite `id` if the `id` is not already in the +model. """ add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met]) """ $(TYPEDSIGNATURES) -Add `mets` to `model` and return a shallow copied version of the model. +Add `mets` to `model` and return a shallow copied version of the model. Only +adds the `mets` if their IDs are not already present in the model. """ function add_metabolites(model::ObjectModel, mets::Vector{Metabolite}) m = copy(model) - m.metabolites = copy(m.metabolites) - for met in mets - m.metabolites[met.id] = met - end - + add_metabolites!(m, mets) m end """ $(TYPEDSIGNATURES) -Add `met` to `model` and return a shallow copied version of the model. +Add `met` to `model` and return a shallow copied version of the model. Only +adds the `met` if its ID is not already present in the model. """ add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) @@ -124,8 +113,7 @@ add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [me end @_remove_fn metabolite ObjectModel String inplace plural begin - !all(in.(metabolite_ids, Ref(metabolites(model)))) && - @models_log @info "Some metabolites not found in model." + throw_argerror_if_key_missing(model, :metabolites, metabolite_ids) remove_reactions!( model, [ @@ -154,13 +142,11 @@ end """ $(TYPEDSIGNATURES) -Add `genes` to `model` based on gene `id`. +Add `genes` to `model` based on gene `id` if the `id` is not already in the +model. """ function add_genes!(model::ObjectModel, genes::Vector{Gene}) - gene_ids = collect(keys(model.genes)) - idxs = filter(!isnothing, indexin([g.id for g in genes], gene_ids)) - isempty(idxs) || throw(ArgumentError("Duplicated gene IDs in model: $(gene_ids[idxs])")) - + throw_argerror_if_key_found(model, :genes, genes) for gene in genes model.genes[gene.id] = gene end @@ -169,30 +155,29 @@ end """ $(TYPEDSIGNATURES) -Add `gene` to `model` based on gene `id`. +Add `gene` to `model` based on gene `id` if the `id` is not already in the +model. """ add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) """ $(TYPEDSIGNATURES) -Add `gns` to `model` and return a shallow copied version of the model. +Add `genes` to `model` and return a shallow copied version of the model. Only +adds the `genes` if their IDs are not already present in the model. """ function add_genes(model::ObjectModel, genes::Vector{Gene}) m = copy(model) - m.genes = copy(m.genes) - for gn in genes - m.genes[gn.id] = gn - end - + add_genes!(m, genes) m end """ $(TYPEDSIGNATURES) -Add `gene` to `model` and return a shallow copied version of the model. +Add `gene` to `model` and return a shallow copied version of the model. Only +adds the `gene` if its ID is not already present in the model. """ add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) @@ -211,7 +196,8 @@ function remove_genes!( model::ObjectModel, gids::Vector{String}; knockout_reactions::Bool = false, -) +) + throw_argerror_if_key_missing(model, :genes, gids) if knockout_reactions rm_reactions = String[] for (rid, r) in model.reactions @@ -222,9 +208,9 @@ function remove_genes!( push!(rm_reactions, rid) end end - pop!.(Ref(model.reactions), rm_reactions) + delete!.(Ref(model.reactions), rm_reactions) end - pop!.(Ref(model.genes), gids) + delete!.(Ref(model.genes), gids) nothing end diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 1b54b63d0..85cac04cf 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -105,6 +105,7 @@ @test length(model.reactions) == 2 @test_throws ArgumentError add_reaction!(model, r3) + @test_throws ArgumentError remove_reaction!(model, "abc") ### metabolites add_metabolites!(model, [m5, m6]) @@ -120,6 +121,7 @@ @test length(model.metabolites) == 4 @test_throws ArgumentError add_metabolite!(model, m2) + @test_throws ArgumentError remove_metabolite!(model, "abc") ### genes add_genes!(model, [g5, g6]) @@ -134,12 +136,13 @@ remove_gene!(model, "g1") @test length(model.genes) == 4 - @test_throws ArgumentError add_gene!(model, g7) + @test_throws ArgumentError add_gene!(model, g7) + @test_throws ArgumentError remove_gene!(model, "abc") # change gene - change_gene_product_bound!(model, "g1"; lower_bound = -10, upper_bound = 10) - @test model.genes["g1"].product_lower_bound == -10.0 - @test model.genes["g1"].product_upper_bound == 10.0 + change_gene_product_bound!(model, "g3"; lower_bound = -10, upper_bound = 10) + @test model.genes["g3"].product_lower_bound == -10.0 + @test model.genes["g3"].product_upper_bound == 10.0 new_model = change_gene_product_bound(model, "g2"; lower_bound = -10, upper_bound = 10) @test new_model.genes["g2"].product_lower_bound == -10.0 From a3097e03008b9542998843f0176721eeddb7549e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 19:55:42 +0100 Subject: [PATCH 232/531] sed modify --- docs/src/concepts/1_screen.md | 2 +- docs/src/concepts/2_modifications.md | 4 +- docs/src/examples/04_core_model.jl | 6 +- docs/src/examples/05b_fba_mods.jl | 10 +- docs/src/examples/06_fva.jl | 8 +- docs/src/examples/07_restricting_reactions.jl | 6 +- docs/src/examples/08_pfba.jl | 12 +- docs/src/examples/11_growth.jl | 2 +- src/analysis/flux_balance_analysis.jl | 6 +- src/analysis/minimize_metabolic_adjustment.jl | 2 +- src/analysis/screening.jl | 2 +- src/analysis/variability_analysis.jl | 6 +- src/misc/check_keys.jl | 12 +- src/reconstruction.jl | 6 +- src/reconstruction/ObjectModel.jl | 308 ++++++++++-------- test/analysis/flux_balance_analysis.jl | 16 +- test/analysis/knockouts.jl | 16 +- test/analysis/max_min_driving_force.jl | 4 +- .../parsimonious_flux_balance_analysis.jl | 4 +- test/analysis/screening.jl | 2 +- test/analysis/variability_analysis.jl | 6 +- test/reconstruction/ObjectModel.jl | 6 +- test/reconstruction/constrained_allocation.jl | 4 +- test/reconstruction/enzyme_constrained.jl | 10 +- .../simplified_enzyme_constrained.jl | 2 +- test/types/BalancedGrowthCommunityModel.jl | 2 +- test/types/FluxSummary.jl | 2 +- test/types/FluxVariabilitySummary.jl | 2 +- test/utils/ObjectModel.jl | 2 +- test/utils/fluxes.jl | 2 +- test/utils/reaction.jl | 2 +- 31 files changed, 262 insertions(+), 212 deletions(-) diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md index 60bdc182c..a00204092 100644 --- a/docs/src/concepts/1_screen.md +++ b/docs/src/concepts/1_screen.md @@ -244,7 +244,7 @@ screen(m, analysis = (m,a) -> # `args` elements get passed as the extra parameter here flux_balance_analysis_vec(m, Tulip.Optimizer; - modifications=[change_optimizer_attribute("IPM_IterationsLimit", a)], + modifications=[modify_optimizer_attribute("IPM_IterationsLimit", a)], ), ) ``` diff --git a/docs/src/concepts/2_modifications.md b/docs/src/concepts/2_modifications.md index 16f4322b4..6ab03090f 100644 --- a/docs/src/concepts/2_modifications.md +++ b/docs/src/concepts/2_modifications.md @@ -10,8 +10,8 @@ list callbacks that do the changes to the prepared optimization model. The callbacks available in COBREXA.jl include functions that may help with tuning the optimizer, or change the raw values in the linear model, such as: -- [`change_constraint`](@ref) and [`change_objective`](@ref) -- [`change_sense`](@ref), [`change_optimizer`](@ref), [`change_optimizer_attribute`](@ref) +- [`modify_constraint`](@ref) and [`change_objective`](@ref) +- [`modify_sense`](@ref), [`modify_optimizer`](@ref), [`modify_optimizer_attribute`](@ref) - [`silence`](@ref) - [`knockout`](@ref) - [`add_loopless_constraints`](@ref) diff --git a/docs/src/examples/04_core_model.jl b/docs/src/examples/04_core_model.jl index 2c2610051..0254c1624 100644 --- a/docs/src/examples/04_core_model.jl +++ b/docs/src/examples/04_core_model.jl @@ -28,9 +28,9 @@ dict_sol = flux_balance_analysis_dict( model, Tulip.Optimizer; modifications = [ - change_objective("R_BIOMASS_Ecoli_core_w_GAM"), - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - change_constraint("R_EX_o2_e"; lb = 0, ub = 0), + modify_objective("R_BIOMASS_Ecoli_core_w_GAM"), + modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), + modify_constraint("R_EX_o2_e"; lb = 0, ub = 0), ], ) diff --git a/docs/src/examples/05b_fba_mods.jl b/docs/src/examples/05b_fba_mods.jl index 8beff7a04..28602ff4f 100644 --- a/docs/src/examples/05b_fba_mods.jl +++ b/docs/src/examples/05b_fba_mods.jl @@ -28,11 +28,11 @@ fluxes = flux_balance_analysis_dict( model, GLPK.Optimizer; modifications = [ # modifications are applied in order - change_objective("R_BIOMASS_Ecoli_core_w_GAM"), # maximize production - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix an exchange rate + modify_objective("R_BIOMASS_Ecoli_core_w_GAM"), # maximize production + modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix an exchange rate knockout(["b0978", "b0734"]), # knock out two genes - change_optimizer(Tulip.Optimizer), # ignore the above optimizer and switch to Tulip - change_optimizer_attribute("IPM_IterationsLimit", 1000), # customize Tulip - change_sense(JuMP.MAX_SENSE), # explicitly tell Tulip to maximize the objective + modify_optimizer(Tulip.Optimizer), # ignore the above optimizer and switch to Tulip + modify_optimizer_attribute("IPM_IterationsLimit", 1000), # customize Tulip + modify_sense(JuMP.MAX_SENSE), # explicitly tell Tulip to maximize the objective ], ) diff --git a/docs/src/examples/06_fva.jl b/docs/src/examples/06_fva.jl index c729726f8..db90d96db 100644 --- a/docs/src/examples/06_fva.jl +++ b/docs/src/examples/06_fva.jl @@ -35,8 +35,8 @@ min_fluxes, max_fluxes = flux_variability_analysis_dict( GLPK.Optimizer; bounds = objective_bounds(0.99), modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), + modify_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), + modify_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), ], ) @@ -79,8 +79,8 @@ vs = flux_variability_analysis( GLPK.Optimizer; bounds = objective_bounds(0.50), # objective can vary by up to 50% of the optimum modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - change_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), + modify_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), + modify_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), ], ret = optimized_model -> ( COBREXA.JuMP.objective_value(optimized_model), diff --git a/docs/src/examples/07_restricting_reactions.jl b/docs/src/examples/07_restricting_reactions.jl index ee31cad6b..8c68e5c92 100644 --- a/docs/src/examples/07_restricting_reactions.jl +++ b/docs/src/examples/07_restricting_reactions.jl @@ -20,7 +20,7 @@ model = load_model(ObjectModel, "e_coli_core.json") # (or, alternatively, a pipeable "variant" version # [`with_changed_bound`](@ref)). # -# Alternatively, you could utilize [`change_constraint`](@ref) as a +# Alternatively, you could utilize [`modify_constraint`](@ref) as a # modification that acts directly on the JuMP optimization model. That may be # useful if you first apply some kind of complicated constraint scheme # modification, such as [`add_loopless_constraints`](@ref). @@ -39,7 +39,7 @@ flux1 = flux_balance_analysis_vec( flux2 = flux_balance_analysis_vec( model, GLPK.Optimizer, - modifications = [change_constraint("FBA", lb = 0.0, ub = 0.0)], + modifications = [modify_constraint("FBA", lb = 0.0, ub = 0.0)], ); # The solutions should not differ a lot: @@ -55,7 +55,7 @@ original_flux = flux_balance_analysis_dict(model, GLPK.Optimizer); restricted_flux = flux_balance_analysis_dict( model, GLPK.Optimizer, - modifications = [change_constraint("EX_o2_e", lb = -0.1, ub = 0.0)], + modifications = [modify_constraint("EX_o2_e", lb = -0.1, ub = 0.0)], ); # The growth in the restricted case is, expectably, lower than the original one: diff --git a/docs/src/examples/08_pfba.jl b/docs/src/examples/08_pfba.jl index c10fbfd02..f8ea24bd5 100644 --- a/docs/src/examples/08_pfba.jl +++ b/docs/src/examples/08_pfba.jl @@ -35,15 +35,15 @@ model = load_model("e_coli_core.xml") # Running of basic pFBA is perfectly analogous to running of [FBA](05a_fba.md) # and other analyses. We add several modifications that improve the solution # (using functions [`silence`](@ref), and -# [`change_optimizer_attribute`](@ref)), and fix the glucose exchange (using -# [`change_constraint`](@ref)) in order to get a more reasonable result: +# [`modify_optimizer_attribute`](@ref)), and fix the glucose exchange (using +# [`modify_constraint`](@ref)) in order to get a more reasonable result: fluxes = parsimonious_flux_balance_analysis_dict( model, Clarabel.Optimizer; modifications = [ silence, # optionally silence the optimizer (Clarabel is very verbose by default) - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix glucose consumption rate + modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix glucose consumption rate ], ) @@ -64,11 +64,11 @@ flux_vector = parsimonious_flux_balance_analysis_vec( model, Tulip.Optimizer; # start with Tulip modifications = [ - change_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - change_optimizer_attribute("IPM_IterationsLimit", 500), # we may change Tulip-specific attributes here + modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), + modify_optimizer_attribute("IPM_IterationsLimit", 500), # we may change Tulip-specific attributes here ], qp_modifications = [ - change_optimizer(Clarabel.Optimizer), # now switch to Clarabel (Tulip wouldn't be able to finish the computation) + modify_optimizer(Clarabel.Optimizer), # now switch to Clarabel (Tulip wouldn't be able to finish the computation) silence, # and make it quiet. ], ) diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl index a61f0dba8..f2c87620c 100644 --- a/docs/src/examples/11_growth.jl +++ b/docs/src/examples/11_growth.jl @@ -122,7 +122,7 @@ model_with_bounded_production = change_bound(model, biomass, lower_bound = 0.1) minimal_intake_production = flux_balance_analysis_dict( model_with_bounded_production, GLPK.Optimizer, - modifications = [change_objective(exchanges)], + modifications = [modify_objective(exchanges)], ); # Metabolite "cost" data may be supplemented using the `weights` argument of diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index c64a369c6..0a00990fd 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -16,8 +16,8 @@ The `optimizer` must be set to a `JuMP`-compatible optimizer, such as `GLPK.Optimizer` or `Tulip.Optimizer` Optionally, you may specify one or more modifications to be applied to the -model before the analysis, such as [`change_optimizer_attribute`](@ref), -[`change_objective`](@ref), and [`change_sense`](@ref). +model before the analysis, such as [`modify_optimizer_attribute`](@ref), +[`change_objective`](@ref), and [`modify_sense`](@ref). Returns an optimized `JuMP` model. @@ -30,7 +30,7 @@ value.(solution[:x]) # extract flux steady state from the optimizer biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") modified_solution = flux_balance_analysis(model, GLPK.optimizer; - modifications=[change_objective(biomass_reaction_id)]) + modifications=[modify_objective(biomass_reaction_id)]) ``` """ function flux_balance_analysis(model::AbstractMetabolicModel, optimizer; modifications = []) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 9008cbadc..0ed216113 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -28,7 +28,7 @@ optmodel = minimize_metabolic_adjustment( model, reference_flux, Gurobi.Optimizer; - modifications = [change_constraint("PFL"; lower_bound=0, upper_bound=0)], # find flux of mutant that is closest to the wild type (reference) model + modifications = [modify_constraint("PFL"; lower_bound=0, upper_bound=0)], # find flux of mutant that is closest to the wild type (reference) model ) value.(solution[:x]) # extract the flux from the optimizer ``` diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl index 7e3707401..ce0f4fa47 100644 --- a/src/analysis/screening.jl +++ b/src/analysis/screening.jl @@ -222,7 +222,7 @@ Both the modification functions (in vectors) and the analysis function here have 2 base parameters (as opposed to 1 with [`screen`](@ref)): first is the `model` (carried through as-is), second is the prepared JuMP optimization model that may be modified and acted upon. As an example, you can use modification -[`change_constraint`](@ref) and analysis [`screen_optimize_objective`](@ref). +[`modify_constraint`](@ref) and analysis [`screen_optimize_objective`](@ref). Note: This function is a thin argument-handling wrapper around [`_screen_optmodel_modifications_impl`](@ref). diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 9ea5555ce..820c601cf 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -188,9 +188,9 @@ mins, maxs = flux_variability_analysis_dict( Tulip.Optimizer; bounds = objective_bounds(0.99), modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), - change_constraint("EX_o2_e"; lower_bound = 0, upper_bound = 0), + modify_optimizer_attribute("IPM_IterationsLimit", 500), + modify_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), + modify_constraint("EX_o2_e"; lower_bound = 0, upper_bound = 0), ], ) ``` diff --git a/src/misc/check_keys.jl b/src/misc/check_keys.jl index c3fe196a4..68130c2ad 100644 --- a/src/misc/check_keys.jl +++ b/src/misc/check_keys.jl @@ -4,7 +4,8 @@ Throw an ArgumentError if model.field has any keys in xs::Union{Reaction, Metabo function throw_argerror_if_key_found(model, field, xs) _ids = collect(keys(getfield(model, field))) ids = [x.id for x in xs if x.id in _ids] - isempty(ids) || throw(ArgumentError("Duplicated $field IDs already present in model: $ids")) + isempty(ids) || + throw(ArgumentError("Duplicated $field IDs already present in model: $ids")) nothing end @@ -17,3 +18,12 @@ function throw_argerror_if_key_missing(model, field, xs::Vector{String}) isempty(ids) || throw(ArgumentError("Missing $field IDs in model: $ids")) nothing end + +""" +Throw and ArgumentError if rxn_ids have isozymes associated with them. +""" +function throw_argerror_if_isozymes_found(model, rxn_ids) + ids = filter(!isnothing, r.gene_associations for r in model.reactions[rxn_ids]) + isempty(ids) || throw(ArgumentError("Isozymes already assign to reactions: $ids")) + nothing +end diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 011675647..96559feab 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -13,7 +13,11 @@ using ..ModuleTools @dse using ..Accessors -using ..Internal: constants, throw_argerror_if_key_found, throw_argerror_if_key_missing +using ..Internal: + constants, + throw_argerror_if_key_found, + throw_argerror_if_key_missing, + throw_argerror_if_isozymes_found using ..Internal.Macros using ..Log.Internal using ..Types diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index ab0682c3f..422252229 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -3,8 +3,7 @@ """ $(TYPEDSIGNATURES) -Add `rxns` to `model` based on reaction `id` if the `id` is not already in the -model. +Plural variant of [`add_reaction!`](@ref). """ function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) throw_argerror_if_key_found(model, :reactions, rxns) @@ -17,15 +16,14 @@ end $(TYPEDSIGNATURES) Add `rxn` to `model` based on reaction `id` if the `id` is not already in the -model +model. """ add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) """ $(TYPEDSIGNATURES) -Add `rxns` to `model` and return a shallow copied version of the model. Only -adds the `rxns` if their IDs are not already present in the model. +Plural variant of [`add_reaction`](@ref). """ function add_reactions(model::ObjectModel, rxns::Vector{Reaction}) m = copy(model) @@ -37,24 +35,20 @@ end """ $(TYPEDSIGNATURES) -Add `rxn` to `model`, and return a shallow copied version of the model. Only -adds the `rxn` if its ID is not already present in the model. +Return a shallow copied version of `model` with `rxn` added if it's ID is not +already present in the model. """ add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) -@_remove_fn reaction ObjectModel String inplace begin - remove_reactions!(model, [reaction_id]) - nothing -end - @_remove_fn reaction ObjectModel String inplace plural begin throw_argerror_if_key_missing(model, :reactions, reaction_ids) delete!.(Ref(model.reactions), reaction_ids) nothing end -@_remove_fn reaction ObjectModel String begin - remove_reactions(model, [reaction_id]) +@_remove_fn reaction ObjectModel String inplace begin + remove_reactions!(model, [reaction_id]) + nothing end @_remove_fn reaction ObjectModel String plural begin @@ -64,13 +58,16 @@ end return n end +@_remove_fn reaction ObjectModel String begin + remove_reactions(model, [reaction_id]) +end + # Add and remove metabolites """ $(TYPEDSIGNATURES) -Add `mets` to `model` based on metabolite `id` if the `id` is not already in the -model. +Plural variant of [`add_metabolite!`](@ref). """ function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) throw_argerror_if_key_found(model, :metabolites, mets) @@ -90,8 +87,7 @@ add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [ """ $(TYPEDSIGNATURES) -Add `mets` to `model` and return a shallow copied version of the model. Only -adds the `mets` if their IDs are not already present in the model. +Plural variant of [`add_metabolite`](@ref). """ function add_metabolites(model::ObjectModel, mets::Vector{Metabolite}) m = copy(model) @@ -103,15 +99,11 @@ end """ $(TYPEDSIGNATURES) -Add `met` to `model` and return a shallow copied version of the model. Only -adds the `met` if its ID is not already present in the model. +Return a shallow copied version of the `model` with `met` added. Only adds +`met` if its ID is not already present in the model. """ add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) -@_remove_fn metabolite ObjectModel String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - @_remove_fn metabolite ObjectModel String inplace plural begin throw_argerror_if_key_missing(model, :metabolites, metabolite_ids) remove_reactions!( @@ -125,8 +117,8 @@ end nothing end -@_remove_fn metabolite ObjectModel String begin - remove_metabolites(model, [metabolite_id]) +@_remove_fn metabolite ObjectModel String inplace begin + remove_metabolites!(model, [metabolite_id]) end @_remove_fn metabolite ObjectModel String plural begin @@ -137,13 +129,16 @@ end return n end +@_remove_fn metabolite ObjectModel String begin + remove_metabolites(model, [metabolite_id]) +end + # Add and remove genes """ $(TYPEDSIGNATURES) -Add `genes` to `model` based on gene `id` if the `id` is not already in the -model. +Plural variant of [`add_gene!`](@ref). """ function add_genes!(model::ObjectModel, genes::Vector{Gene}) throw_argerror_if_key_found(model, :genes, genes) @@ -163,8 +158,7 @@ add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) """ $(TYPEDSIGNATURES) -Add `genes` to `model` and return a shallow copied version of the model. Only -adds the `genes` if their IDs are not already present in the model. +Plural variant of [`add_gene`](@ref). """ function add_genes(model::ObjectModel, genes::Vector{Gene}) m = copy(model) @@ -176,27 +170,21 @@ end """ $(TYPEDSIGNATURES) -Add `gene` to `model` and return a shallow copied version of the model. Only -adds the `gene` if its ID is not already present in the model. +Return a shallow copied version of the `model` with added `gene`. Only adds the +`gene` if its ID is not already present in the model. """ add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) """ $(TYPEDSIGNATURES) -Remove all genes with `ids` from `model`. If `knockout_reactions` is true, then also -constrain reactions that require the genes to function to carry zero flux. - -# Example -``` -remove_genes!(model, ["g1", "g2"]) -``` +Plural variant of [`remove_gene!`](@ref). """ function remove_genes!( model::ObjectModel, gids::Vector{String}; knockout_reactions::Bool = false, -) +) throw_argerror_if_key_missing(model, :genes, gids) if knockout_reactions rm_reactions = String[] @@ -219,11 +207,6 @@ $(TYPEDSIGNATURES) Remove gene with `id` from `model`. If `knockout_reactions` is true, then also constrain reactions that require the genes to function to carry zero flux. - -# Example -``` -remove_gene!(model, "g1") -``` """ remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) @@ -231,14 +214,19 @@ remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) # Change reaction bounds @_change_bounds_fn ObjectModel String inplace begin - isnothing(lower_bound) || (model.reactions[rxn_id].lower_bound = lower_bound) - isnothing(upper_bound) || (model.reactions[rxn_id].upper_bound = upper_bound) - nothing + change_bounds!( + model, + [rxn_id]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], + ) end @_change_bounds_fn ObjectModel String inplace plural begin - for (i, l, u) in zip(rxn_ids, lower_bounds, upper_bounds) - change_bound!(model, i, lower_bound = l, upper_bound = u) + throw_argerror_if_key_missing(model, :reactions, rxn_ids) + for (rxn_id, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) + isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) + isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) end end @@ -252,16 +240,18 @@ end end @_change_bounds_fn ObjectModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - for rid in rxn_ids - n.reactions[rid] = copy(model.reactions[rid]) + throw_argerror_if_key_missing(model, :reactions, rxn_ids) + m = copy(model) + m.reactions = copy(model.reactions) + for (rid, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) + m.reactions[rid] = copy(model.reactions[rid]) for field in fieldnames(typeof(model.reactions[rid])) - setfield!(n.reactions[rid], field, getfield(model.reactions[rid], field)) + setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) end + isnothing(lower) || (m.reactions[rxn_id].lower_bound = lower) + isnothing(upper) || (m.reactions[rxn_id].upper_bound = upper) end - change_bounds!(n, rxn_ids; lower_bounds, upper_bounds) - return n + return m end # Change gene product bounds @@ -269,8 +259,7 @@ end """ $(TYPEDSIGNATURES) -Changes the `product_lower_bound` or `product_upper_bound` for the -[`Gene`][(ref)s listed in `gids` in the `model`, in place. +Plural variant of [`change_gene_product_bound!`](@ref). """ function change_gene_product_bounds!( model::ObjectModel, @@ -278,15 +267,10 @@ function change_gene_product_bounds!( lower_bounds = fill(nothing, length(gids)), upper_bounds = fill(nothing, length(gids)), ) - for (i, gid) in enumerate(gids) - - isnothing(lower_bounds[i]) || begin - (model.genes[gid].product_lower_bound = lower_bounds[i]) - end - - isnothing(upper_bounds[i]) || begin - (model.genes[gid].product_upper_bound = upper_bounds[i]) - end + throw_argerror_if_key_missing(model, :genes, gids) + for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) + isnothing(lower) || (model.genes[gid].product_lower_bound = lower) + isnothing(upper) || (model.genes[gid].product_upper_bound = upper) end end @@ -294,7 +278,8 @@ end $(TYPEDSIGNATURES) Changes the `product_lower_bound` or `product_upper_bound` for the -[`Gene`][(ref) `gid` in the `model`, in place. +[`Gene`][(ref) `gid` in the `model`, in place. If either `lower_bound` or +`upper_bound` is `nothing`, then that bound is not changed. """ function change_gene_product_bound!( model::ObjectModel, @@ -313,8 +298,7 @@ end """ $(TYPEDSIGNATURES) -Variant of [`change_gene_product_bounds!`](@ref) that does not modify the -original model, but makes a shallow copy with the modification included. +Plural variant of [`change_gene_product_bound`](@ref). """ function change_gene_product_bounds( model::ObjectModel, @@ -322,15 +306,17 @@ function change_gene_product_bounds( lower_bounds = fill(nothing, length(gids)), upper_bounds = fill(nothing, length(gids)), ) + throw_argerror_if_key_missing(model, :genes, gids) m = copy(model) m.genes = copy(model.genes) - for gid in gids + for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) m.genes[gid] = copy(model.genes[gid]) - for field in fieldnames(typeof(m.genes[gid])) + for field in fieldnames(typeof(model.genes[gid])) setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) end + isnothing(lower) || (model.genes[gid].product_lower_bound = lower) + isnothing(upper) || (model.genes[gid].product_upper_bound = upper) end - change_gene_product_bounds!(m, gids; lower_bounds, upper_bounds) m end @@ -400,6 +386,7 @@ function change_objective( m end + """ $(TYPEDSIGNATURES) @@ -427,6 +414,8 @@ function add_biomass_metabolite!( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) model.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 add_metabolite!(model, Metabolite(biomass_metabolite_id)) end @@ -443,6 +432,9 @@ function add_biomass_metabolite( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) + m = copy(model) m.metabolites = copy(m.metabolites) m.metabolites[biomass_metabolite_id] = Metabolite(biomass_metabolite_id) @@ -467,6 +459,11 @@ function remove_biomass_metabolite!( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) + haskey(model.reaction[biomass_rxn_id], biomass_metabolite_id) || + throw(ArgumentError("$biomass_metabolite_id not found in $biomass_rxn_id.")) + delete!(model.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) remove_metabolite!(model, biomass_metabolite_id) end @@ -482,9 +479,14 @@ function remove_biomass_metabolite( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) + haskey(model.reaction[biomass_rxn_id], biomass_metabolite_id) || + throw(ArgumentError("$biomass_metabolite_id not found in $biomass_rxn_id.")) + m = copy(model) m.metabolites = copy(m.metabolites) - remove_metabolite!(m, biomass_metabolite_id) + delete!(m.metabolites, biomass_metabolite_id) m.reactions = copy(model.reactions) m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) @@ -495,13 +497,13 @@ function remove_biomass_metabolite( m end -# Add virtual ribosome for constrained allocation applications +# Add virtual ribosome (assume no model already has one) """ $(TYPEDSIGNATURES) -To `biomass_rxn_id` in `model`, add a pseudo-isozyme that approximates the -effect ribosome synthesis has on growth. +To `biomass_rxn_id` in `model`, add a pseudo-isozyme and associated gene that +approximates the effect ribosome synthesis has on growth. # Bacterial growth law models Numerous experimental studies have shown that during steady state growth the @@ -516,21 +518,16 @@ over" for biosynthetic enzymes. See Mori, Matteo, et al. "Constrained allocation flux balance analysis." PLoS computational biology 12.6 (2016) for more details. # Implementation -By adding a protein cost to biomass synthesis this effect can be simulated. The -parameter `weight` needs to be estimated from data, but acts like a turnover -number. Lower `weight` means more ribosome is required for growth (`ribosome = -growth/weight`). The molar mass of the ribosome is `1`. - -# Note -1. This modifications makes the underlying biomass reaction unidirectional. -2. The `virtualribosome_id` defaults to `virtualribosome` and must be manually included - in any capacity bound later used in enzyme constrained models. -3. This modification also adds a pseudogene called `virtualribosome_id`. - -The pseudo-isozyme acts like a regular gene product, +This modification makes the underlying biomass reaction unidirectional. The +`virtualribosome_id` defaults to `virtualribosome`, and corresponds to a +pseudogene called `virtualribosome_id`. The parameter `weight` needs to be +estimated from data, but acts like a turnover number. Lower `weight` means more +ribosome is required for growth (`ribosome = growth/weight`). The molar mass of +the ribosome is `1`. The pseudo-isozyme acts like a regular gene product, ``` ribosome = weight * biomass_flux ``` +when simulating enzyme constrained models. """ function add_virtualribosome!( model::ObjectModel, @@ -538,6 +535,13 @@ function add_virtualribosome!( weight::Float64; virtualribosome_id = "virtualribosome", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) + isnothing(model.reactions[biomass_rxn_id]) || + throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) + haskey(model.genes, virtualribosome_id) || + throw(ArgumentError("$virtualribosome_id already found in model.")) + # ensure unidirectional model.reactions[biomass_rxn_id].lower_bound = 0.0 model.reactions[biomass_rxn_id].upper_bound = constants.default_reaction_bound @@ -570,6 +574,13 @@ function add_virtualribosome( weight::Float64; virtualribosome_id = "virtualribosome", ) + haskey(model.reactions, biomass_rxn_id) || + throw(ArgumentError("$biomass_rxn_id not found in model.")) + isnothing(model.reactions[biomass_rxn_id]) || + throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) + haskey(model.genes, virtualribosome_id) || + throw(ArgumentError("$virtualribosome_id already found in model.")) + m = copy(model) m.reactions = copy(model.reactions) m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) @@ -580,37 +591,59 @@ function add_virtualribosome( m end -# Add, change, remove isozymes +# Add, remove isozymes (no change because the order isozymes may appear in is not constant across models) """ $(TYPEDSIGNATURES) -Add `isozymes` to `rxn_id` in `model`. +Plural variant of [`add_isozymes!`](@ref). """ -add_isozymes!(model::ObjectModel, rxn_id::String, isozymes::Vector{Isozyme}) = - add_isozymes!(model, [rxn_id], [isozymes]) +function add_isozymes!( + model::ObjectModel, + rids::Vector{String}, + isozymes_vector::Vector{Vector{Isozyme}}, +) + throw_argerror_if_key_missing(model, :reactions, rids) + throw_argerror_if_isozymes_found(model, rids) + + for (rid, isozymes) in zip(rids, isozymes_vector) + model.reactions[rid].gene_associations = isozymes + end +end """ $(TYPEDSIGNATURES) -For each reaction in `rxn_ids`, add the corresponding isozymes in -`isozymes_vector` to `model`. +Add `isozymes` to `rid` in `model`. Only allowed if `rid` does not have isozymes assigned to it. """ -function add_isozymes!( +add_isozymes!(model::ObjectModel, rid::String, isozymes::Vector{Isozyme}) = + add_isozymes!(model, [rid], [isozymes]) + +""" +$(TYPEDSIGNATURES) + +Plural variant of [`add_isozymes`](@ref). +""" +function add_isozymes( model::ObjectModel, - rxn_ids::Vector{String}, + rids::Vector{String}, isozymes_vector::Vector{Vector{Isozyme}}, ) - rxn_ids = keys(model.reactions) - idxs = filter(!isnothing, indexin(rxns, rxn_ids)) - isempty(idxs) || - throw(ArgumentError("Duplicated reaction IDs in model: $(rxn_ids[idxs])")) - isnothing(model.reactions[rxn_id].gene_associations) || - throw(ArgumentError("$rxn_id already has isozymes.")) - - for (rid, isozymes) in zip(rxn_id_vector, isozymes_vector) - add_isozymes!(model, rid, isozymes) + throw_argerror_if_key_missing(model, :reactions, rids) + throw_argerror_if_isozymes_found(model, rids) + + m = copy(model) + m.reactions = copy(model.reactions) + + for (rid, isozymes) in zip(rids, isozymes_vector) + m.reactions[rid] = copy(model.reactions[rid]) + if !isnothing(model.reactions[rid].gene_associations) + m.reactions[rid].gene_associations = + copy(model.reactions[rid].gene_associations) + end + m.reactions[rid].gene_associations = isozymes end + m end """ @@ -619,47 +652,50 @@ $(TYPEDSIGNATURES) Variant of [`add_isozymes!`](@ref) that returns a copied model instead of modifying the input. """ -function add_isozymes(model::ObjectModel, rxn_id::String, isozymes::Vector{Isozyme}) +add_isozymes(model::ObjectModel, rid::String, isozymes::Vector{Isozyme}) = + add_isozymes(model, [rid], [isozymes]) - m = copy(model) - m.reactions = copy(model.reactions) +""" +$(TYPEDSIGNATURES) - m.reactions[rxn_id] = copy(model.reactions[rxn_id]) - if !isnothing(model.reactions[rxn_id].gene_associations) - m.reactions[rxn_id].gene_associations = - copy(model.reactions[rxn_id].gene_associations) +Plural variant of [`remove_isozyme!`](@ref). +""" +function remove_isozymes!(model::ObjectModel, rids::Vector{String}) + throw_argerror_if_key_missing(model, :reactions, rids) + for rid in rids + model.reactions[rid].gene_associations = nothing end - m.reactions[rxn_id].gene_associations = isozymes - - m end """ $(TYPEDSIGNATURES) -For each pair of `isozymes` and `rxn_id` in `isozymes_vector` and -`rxn_id_vector`, call [`add_isozymes`](@ref) to add the isozymes to the -`model`. +Remove all isozymes from `rid` in `model`. """ -function add_isozymes( - model::ObjectModel, - rxn_id_vector::Vector{String}, - isozymes_vector::Vector{Vector{Isozyme}}, -) +remove_isozyme!(model::ObjectModel, rid::String) = remove_isozymes!(model, [rid]) - m = copy(model) - m.reactions = copy(model.reactions) +""" +$(TYPEDSIGNATURES) - for (rxn_id, isozymes) in zip(rxn_id_vector, isozymes_vector) +Plural variant of [`remove_isozyme`](@ref). +""" +function remove_isozymes!(model::ObjectModel, rids::Vector{String}) + throw_argerror_if_key_missing(model, :reactions, rids) - m.reactions[rxn_id] = copy(model.reactions[rxn_id]) - if !isnothing(model.reactions[rxn_id].gene_associations) - m.reactions[rxn_id].gene_associations = - copy(model.reactions[rxn_id].gene_associations) + m = copy(model) + m.reactions = copy(model.reactions) + for rid in rids + m.reactions[rid] = copy(model.reactions[rid]) + for field in fieldnames(typeof(model.reactions[rid])) + setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) end - m.reactions[rxn_id].gene_associations = isozymes - + model.reactions[rid].gene_associations = nothing end - - m end + +""" +$(TYPEDSIGNATURES) + +Return a shallow copy of `model` with all isozymes of reaction `rid` removed. +""" +remove_isozyme(model::ObjectModel, rid::String) = remove_isozymes(model, [rid]) diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index ed40b316d..25a22a7b7 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -39,10 +39,10 @@ end model, Tulip.Optimizer; modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), + modify_objective("BIOMASS_Ecoli_core_w_GAM"), + modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + modify_sense(MAX_SENSE), + modify_optimizer_attribute("IPM_IterationsLimit", 110), ], ) |> values_dict @@ -59,7 +59,7 @@ end model, Tulip.Optimizer; modifications = [ - change_objective( + modify_objective( ["BIOMASS_Ecoli_core_w_GAM", "PFL"]; weights = [biomass_frac, pfl_frac], ), @@ -75,17 +75,17 @@ end @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_constraint("gbbrsh"; lower_bound = -12, upper_bound = -12)], + modifications = [modify_objective("gbbrsh"; lower_bound = -12, upper_bound = -12)], ) |> values_dict @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_objective("gbbrsh")], + modifications = [modify_objective("gbbrsh")], ) |> values_dict @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_objective(["BIOMASS_Ecoli_core_w_GAM"; "gbbrsh"])], + modifications = [modify_objective(["BIOMASS_Ecoli_core_w_GAM"; "gbbrsh"])], ) end diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl index c7b691691..c4bdd322a 100644 --- a/test/analysis/knockouts.jl +++ b/test/analysis/knockouts.jl @@ -120,10 +120,10 @@ end model, Tulip.Optimizer; modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), + modify_objective("BIOMASS_Ecoli_core_w_GAM"), + modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + modify_sense(MAX_SENSE), + modify_optimizer_attribute("IPM_IterationsLimit", 110), knockout(["b0978", "b0734"]), # knockouts out cytbd ], ) |> values_dict @@ -138,10 +138,10 @@ end model, Tulip.Optimizer; modifications = [ - change_objective("BIOMASS_Ecoli_core_w_GAM"), - change_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - change_sense(MAX_SENSE), - change_optimizer_attribute("IPM_IterationsLimit", 110), + modify_objective("BIOMASS_Ecoli_core_w_GAM"), + modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), + modify_sense(MAX_SENSE), + modify_optimizer_attribute("IPM_IterationsLimit", 110), knockout("b2779"), # knockouts out enolase ], ) |> values_dict diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 646b55529..1e087a262 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -28,7 +28,7 @@ x = flux_balance_analysis( mmdfm, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) # get mmdf @@ -44,7 +44,7 @@ mmdfm, Tulip.Optimizer; bounds = gamma_bounds(0.9), - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> result pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reactions(mmdfm))) diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index eb9320730..077f7a394 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -6,8 +6,8 @@ model, Tulip.Optimizer; modifications = [ - change_constraint("EX_m1(e)", lower_bound = -10.0), - change_optimizer_attribute("IPM_IterationsLimit", 500), + modify_constraint("EX_m1(e)", lower_bound = -10.0), + modify_optimizer_attribute("IPM_IterationsLimit", 500), ], qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], ) |> values_dict diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl index f2ea28353..54f5a14c1 100644 --- a/test/analysis/screening.jl +++ b/test/analysis/screening.jl @@ -53,7 +53,7 @@ Analysis.flux_balance_analysis( m, Tulip.Optimizer; - modifications = [change_sense(sense)], + modifications = [modify_sense(sense)], ) |> COBREXA.Solver.values_vec, args = [(MIN_SENSE,), (MAX_SENSE,)], ) == [ diff --git a/test/analysis/variability_analysis.jl b/test/analysis/variability_analysis.jl index 2eceadca9..758431df7 100644 --- a/test/analysis/variability_analysis.jl +++ b/test/analysis/variability_analysis.jl @@ -101,9 +101,9 @@ end Tulip.Optimizer; bounds = objective_bounds(0.99), modifications = [ - change_optimizer_attribute("IPM_IterationsLimit", 500), - change_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), - change_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), + modify_optimizer_attribute("IPM_IterationsLimit", 500), + modify_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), + modify_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), ], ) |> result diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 85cac04cf..f7c8764a8 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -120,8 +120,8 @@ remove_metabolite!(model, "m1") @test length(model.metabolites) == 4 - @test_throws ArgumentError add_metabolite!(model, m2) - @test_throws ArgumentError remove_metabolite!(model, "abc") + @test_throws ArgumentError add_metabolite!(model, m2) + @test_throws ArgumentError remove_metabolite!(model, "abc") ### genes add_genes!(model, [g5, g6]) @@ -138,7 +138,7 @@ @test_throws ArgumentError add_gene!(model, g7) @test_throws ArgumentError remove_gene!(model, "abc") - + # change gene change_gene_product_bound!(model, "g3"; lower_bound = -10, upper_bound = 10) @test model.genes["g3"].product_lower_bound == -10.0 diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index f9e6a60b8..1cf8fcf30 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -64,7 +64,7 @@ flux_balance_analysis( cam, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> values_dict(:reaction) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) @@ -78,7 +78,7 @@ flux_balance_analysis( cam, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> values_dict(:reaction) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index c54d69148..bc237f5ac 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -48,7 +48,7 @@ res = flux_balance_analysis( gm, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) rxn_fluxes = values_dict(:reaction, res) @@ -79,9 +79,9 @@ gm, Tulip.Optimizer; modifications = [ - change_objective(genes(gm); weights = [], sense = MIN_SENSE), - change_constraint("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb), - change_optimizer_attribute("IPM_IterationsLimit", 1000), + modify_objective(genes(gm); weights = [], sense = MIN_SENSE), + modify_constraint("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb), + modify_optimizer_attribute("IPM_IterationsLimit", 1000), ], ) mass_groups_min = values_dict(:enzyme_group, res) @@ -146,7 +146,7 @@ end res = flux_balance_analysis( gm, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) rxn_fluxes = values_dict(:reaction, res) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index 756221ab0..f4b094e7d 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -40,7 +40,7 @@ flux_balance_analysis( simplified_enzyme_constrained_model, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> values_dict @test isapprox( diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/BalancedGrowthCommunityModel.jl index 4612d525d..161706681 100644 --- a/test/types/BalancedGrowthCommunityModel.jl +++ b/test/types/BalancedGrowthCommunityModel.jl @@ -335,7 +335,7 @@ end res = flux_balance_analysis( cm, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) f_d = values_dict(:reaction, res) diff --git a/test/types/FluxSummary.jl b/test/types/FluxSummary.jl index ca66f7b19..f0e6bfdd8 100644 --- a/test/types/FluxSummary.jl +++ b/test/types/FluxSummary.jl @@ -5,7 +5,7 @@ flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 200)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 200)], ) |> values_dict fr = flux_summary(sol; keep_unbounded = true, large_flux_bound = 25) diff --git a/test/types/FluxVariabilitySummary.jl b/test/types/FluxVariabilitySummary.jl index 469023f12..7cc2e003c 100644 --- a/test/types/FluxVariabilitySummary.jl +++ b/test/types/FluxVariabilitySummary.jl @@ -6,7 +6,7 @@ model, Tulip.Optimizer; bounds = objective_bounds(0.90), - modifications = [change_optimizer_attribute("IPM_IterationsLimit", 2000)], + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], ) |> result fr = flux_variability_summary(sol) diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl index 490d2a585..7a3c73666 100644 --- a/test/utils/ObjectModel.jl +++ b/test/utils/ObjectModel.jl @@ -6,7 +6,7 @@ flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], ) |> values_dict # bounds setting # TODO why is this here? diff --git a/test/utils/fluxes.jl b/test/utils/fluxes.jl index b5c821b5c..effc4e82d 100644 --- a/test/utils/fluxes.jl +++ b/test/utils/fluxes.jl @@ -5,7 +5,7 @@ flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], ) |> values_dict consuming, producing = metabolite_fluxes(model, fluxes) diff --git a/test/utils/reaction.jl b/test/utils/reaction.jl index 6ac2a5898..8a49715a6 100644 --- a/test/utils/reaction.jl +++ b/test/utils/reaction.jl @@ -6,7 +6,7 @@ flux_balance_analysis( model, Tulip.Optimizer; - modifications = [change_objective("BIOMASS_Ecoli_core_w_GAM")], + modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], ) |> values_dict # test if reaction is balanced From 8a44bb7de4c6e9dfd51c2033fed7dd94073584d7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 22:34:32 +0100 Subject: [PATCH 233/531] fix basic errors --- src/reconstruction/ObjectModel.jl | 59 +++++++++++++++++++++++++++--- test/reconstruction/ObjectModel.jl | 1 + 2 files changed, 55 insertions(+), 5 deletions(-) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index 422252229..e1c66ce49 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -211,6 +211,55 @@ constrain reactions that require the genes to function to carry zero flux. remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) +""" +$(TYPEDSIGNATURES) + +Plural variant of [`remove_gene`](@ref). +""" +function remove_genes( + model::ObjectModel, + gids::Vector{String}; + knockout_reactions::Bool = false, +) + throw_argerror_if_key_missing(model, :genes, gids) + + m = copy(model) + m.genes = copy(model.genes) + + if knockout_reactions + + rm_reactions = String[] + for (rid, r) in model.reactions + if !isnothing(r.gene_associations) && all( + any(in.(gids, Ref(conjunction))) for + conjunction in reaction_gene_associations(model, rid) + ) + push!(rm_reactions, rid) + end + end + + m.reactions = copy(reactions) + delete!.(Ref(m.reactions), rm_reactions) + end + + delete!.(Ref(m.genes), gids) + + nothing +end + +""" +$(TYPEDSIGNATURES) + +Return a shallow copy of `model` with `gid` remove from `model`. If +`knockout_reactions` is true, then also constrain reactions that require the +genes to function to carry zero flux. +""" +remove_genes( + model::ObjectModel, + gid::String; + knockout_reactions::Bool = false, +) = remove_genes(model, [gid]; knockout_reactions) + # Change reaction bounds @_change_bounds_fn ObjectModel String inplace begin @@ -248,8 +297,8 @@ end for field in fieldnames(typeof(model.reactions[rid])) setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) end - isnothing(lower) || (m.reactions[rxn_id].lower_bound = lower) - isnothing(upper) || (m.reactions[rxn_id].upper_bound = upper) + isnothing(lower) || (m.reactions[rid].lower_bound = lower) + isnothing(upper) || (m.reactions[rid].upper_bound = upper) end return m end @@ -314,8 +363,8 @@ function change_gene_product_bounds( for field in fieldnames(typeof(model.genes[gid])) setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) end - isnothing(lower) || (model.genes[gid].product_lower_bound = lower) - isnothing(upper) || (model.genes[gid].product_upper_bound = upper) + isnothing(lower) || (m.genes[gid].product_lower_bound = lower) + isnothing(upper) || (m.genes[gid].product_upper_bound = upper) end m end @@ -679,7 +728,7 @@ $(TYPEDSIGNATURES) Plural variant of [`remove_isozyme`](@ref). """ -function remove_isozymes!(model::ObjectModel, rids::Vector{String}) +function remove_isozymes(model::ObjectModel, rids::Vector{String}) throw_argerror_if_key_missing(model, :reactions, rids) m = copy(model) diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index f7c8764a8..be49b2716 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -147,4 +147,5 @@ new_model = change_gene_product_bound(model, "g2"; lower_bound = -10, upper_bound = 10) @test new_model.genes["g2"].product_lower_bound == -10.0 @test new_model.genes["g2"].product_upper_bound == 10.0 + @test model.genes["g2"].product_lower_bound == 0.0 end From 56c73c8cc15f70cc43e561377e440402a2a6cd75 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 19 Feb 2023 22:42:13 +0100 Subject: [PATCH 234/531] add pipes and re-arrange --- src/misc/check_keys.jl | 2 +- src/reconstruction/ObjectModel.jl | 39 ++++++----- src/reconstruction/pipes/enzymes.jl | 19 +----- src/reconstruction/pipes/generic.jl | 68 ++++++++++++++++--- test/analysis/flux_balance_analysis.jl | 4 +- .../parsimonious_flux_balance_analysis.jl | 2 +- test/reconstruction/ObjectModel.jl | 37 ++++++++++ test/reconstruction/constrained_allocation.jl | 23 ++----- 8 files changed, 124 insertions(+), 70 deletions(-) diff --git a/src/misc/check_keys.jl b/src/misc/check_keys.jl index 68130c2ad..991d9ec62 100644 --- a/src/misc/check_keys.jl +++ b/src/misc/check_keys.jl @@ -23,7 +23,7 @@ end Throw and ArgumentError if rxn_ids have isozymes associated with them. """ function throw_argerror_if_isozymes_found(model, rxn_ids) - ids = filter(!isnothing, r.gene_associations for r in model.reactions[rxn_ids]) + ids = [rid for rid in rxn_ids if !isnothing(model.reactions[rid].gene_associations)] isempty(ids) || throw(ArgumentError("Isozymes already assign to reactions: $ids")) nothing end diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index e1c66ce49..cec5837b1 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -216,18 +216,18 @@ $(TYPEDSIGNATURES) Plural variant of [`remove_gene`](@ref). """ -function remove_genes( +function remove_genes( model::ObjectModel, gids::Vector{String}; knockout_reactions::Bool = false, ) throw_argerror_if_key_missing(model, :genes, gids) - + m = copy(model) m.genes = copy(model.genes) if knockout_reactions - + rm_reactions = String[] for (rid, r) in model.reactions if !isnothing(r.gene_associations) && all( @@ -243,8 +243,8 @@ function remove_genes( end delete!.(Ref(m.genes), gids) - - nothing + + m end """ @@ -254,11 +254,8 @@ Return a shallow copy of `model` with `gid` remove from `model`. If `knockout_reactions` is true, then also constrain reactions that require the genes to function to carry zero flux. """ -remove_genes( - model::ObjectModel, - gid::String; - knockout_reactions::Bool = false, -) = remove_genes(model, [gid]; knockout_reactions) +remove_gene(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = + remove_genes(model, [gid]; knockout_reactions) # Change reaction bounds @@ -389,7 +386,7 @@ function change_gene_product_bound( ) end -# Change objective +# Change objective """ $(TYPEDSIGNATURES) @@ -501,7 +498,7 @@ end $(TYPEDSIGNATURES) Remove a biomass metabolite called `biomass_metabolite_id` from -the biomass reaction, called `biomass_rxn_id` in `model`. +the biomass reaction, called `biomass_rxn_id` in `model`. """ function remove_biomass_metabolite!( model::ObjectModel, @@ -586,9 +583,9 @@ function add_virtualribosome!( ) haskey(model.reactions, biomass_rxn_id) || throw(ArgumentError("$biomass_rxn_id not found in model.")) - isnothing(model.reactions[biomass_rxn_id]) || + isnothing(model.reactions[biomass_rxn_id].gene_associations) || throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) - haskey(model.genes, virtualribosome_id) || + haskey(model.genes, virtualribosome_id) && throw(ArgumentError("$virtualribosome_id already found in model.")) # ensure unidirectional @@ -625,9 +622,9 @@ function add_virtualribosome( ) haskey(model.reactions, biomass_rxn_id) || throw(ArgumentError("$biomass_rxn_id not found in model.")) - isnothing(model.reactions[biomass_rxn_id]) || + isnothing(model.reactions[biomass_rxn_id].gene_associations) || throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) - haskey(model.genes, virtualribosome_id) || + haskey(model.genes, virtualribosome_id) && throw(ArgumentError("$virtualribosome_id already found in model.")) m = copy(model) @@ -719,9 +716,10 @@ end """ $(TYPEDSIGNATURES) -Remove all isozymes from `rid` in `model`. +Remove all isozymes from `rid` in `model`. Note, this function removes all +isozymes from `rid`. """ -remove_isozyme!(model::ObjectModel, rid::String) = remove_isozymes!(model, [rid]) +remove_isozymes!(model::ObjectModel, rid::String) = remove_isozymes!(model, [rid]) """ $(TYPEDSIGNATURES) @@ -738,8 +736,9 @@ function remove_isozymes(model::ObjectModel, rids::Vector{String}) for field in fieldnames(typeof(model.reactions[rid])) setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) end - model.reactions[rid].gene_associations = nothing + m.reactions[rid].gene_associations = nothing end + m end """ @@ -747,4 +746,4 @@ $(TYPEDSIGNATURES) Return a shallow copy of `model` with all isozymes of reaction `rid` removed. """ -remove_isozyme(model::ObjectModel, rid::String) = remove_isozymes(model, [rid]) +remove_isozymes(model::ObjectModel, rid::String) = remove_isozymes(model, [rid]) diff --git a/src/reconstruction/pipes/enzymes.jl b/src/reconstruction/pipes/enzymes.jl index fefb2f6f0..72f94591c 100644 --- a/src/reconstruction/pipes/enzymes.jl +++ b/src/reconstruction/pipes/enzymes.jl @@ -1,3 +1,5 @@ +# constructors for enzyme constrained models + """ $(TYPEDSIGNATURES) @@ -17,20 +19,3 @@ giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to """ with_enzyme_constraints(args...; kwargs...) = model -> make_enzyme_constrained_model(model, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that adds a virtualribosome to a model. Args and kwargs -are forwarded to [`add_virtualribosome`](@ref). -""" -with_virtualribosome(args...; kwargs...) = - model -> add_virtualribosome(model, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that overwrites the current isozymes associated with the -model through calling [`add_isozymes`](@ref). -""" -with_isozymes(args...; kwargs...) = model -> add_isozymes(model, args...; kwargs...) diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl index 1146d1a92..0126de49c 100644 --- a/src/reconstruction/pipes/generic.jl +++ b/src/reconstruction/pipes/generic.jl @@ -1,4 +1,4 @@ -# Reactions +# Reactions """ $(TYPEDSIGNATURES) @@ -86,25 +86,21 @@ Specifies a model variant with an added gene. Forwards the arguments to with_added_gene(args...; kwargs...) = m -> add_gene(m, args...; kwargs...) -# Biomass - """ $(TYPEDSIGNATURES) -Specifies a model variant that adds a biomass metabolite to the biomass -reaction. Forwards arguments to [`add_biomass_metabolite`](@ref). +Specifies a model variant with removed genes. Forwards the arguments to +[`remove_genes`](@ref). """ -with_added_biomass_metabolite(args...; kwargs...) = - m -> add_biomass_metabolite(m, args...; kwargs...) +with_removed_genes(args...; kwargs...) = m -> remove_genes(m, args...; kwargs...) """ $(TYPEDSIGNATURES) -Specifies a model variant that removes a biomass metabolite from the biomass -reaction. Forwards arguments to [`remove_biomass_metabolite`](@ref). +Specifies a model variant with a gene removed. Forwards the arguments to +[`remove_gene`](@ref). """ -with_removed_biomass_metabolite(args...; kwargs...) = - m -> remove_biomass_metabolite(m, args...; kwargs...) +with_removed_gene(args...; kwargs...) = m -> remove_gene(m, args...; kwargs...) # Bounds @@ -150,3 +146,53 @@ Specifies a model variant with the objective reaction(s) changed. Forwards the arguments to [`change_objective`](@ref). """ with_changed_objective(args...; kwargs...) = m -> change_objective(m, args...; kwargs...) + +# Biomass + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that adds a biomass metabolite to the biomass +reaction. Forwards arguments to [`add_biomass_metabolite`](@ref). +""" +with_added_biomass_metabolite(args...; kwargs...) = + m -> add_biomass_metabolite(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that removes a biomass metabolite from the biomass +reaction. Forwards arguments to [`remove_biomass_metabolite`](@ref). +""" +with_removed_biomass_metabolite(args...; kwargs...) = + m -> remove_biomass_metabolite(m, args...; kwargs...) + +# Virtual ribosome + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that adds a virtualribosome to a model. Args and kwargs +are forwarded to [`add_virtualribosome`](@ref). +""" +with_virtualribosome(args...; kwargs...) = + model -> add_virtualribosome(model, args...; kwargs...) + +# Isozymes + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that adds isozymes to the model through calling +[`add_isozyme`](@ref). +""" +with_added_isozymes(args...; kwargs...) = model -> add_isozymes(model, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant that removes isozymes to the model through calling +[`remove_isozymes`](@ref). +""" +with_removed_isozymes(args...; kwargs...) = + model -> remove_isozymes(model, args...; kwargs...) diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index 25a22a7b7..630db3877 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -75,13 +75,15 @@ end @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; - modifications = [modify_objective("gbbrsh"; lower_bound = -12, upper_bound = -12)], + modifications = [modify_constraint("gbbrsh"; lower_bound = -12, upper_bound = -12)], ) |> values_dict + @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; modifications = [modify_objective("gbbrsh")], ) |> values_dict + @test_throws DomainError flux_balance_analysis( model, Tulip.Optimizer; diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 077f7a394..edbbc54aa 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -9,7 +9,7 @@ modify_constraint("EX_m1(e)", lower_bound = -10.0), modify_optimizer_attribute("IPM_IterationsLimit", 500), ], - qp_modifications = [change_optimizer(Clarabel.Optimizer), silence], + qp_modifications = [modify_optimizer(Clarabel.Optimizer), silence], ) |> values_dict # The used optimizer doesn't really converge to the same answer everytime diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index be49b2716..dfa9b4b6a 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -7,6 +7,7 @@ m5 = Metabolite("m5") m6 = Metabolite("m6") m7 = Metabolite("m7") + mtest = Metabolite("mtest") g1 = Gene("g1") g2 = Gene("g2") @@ -16,6 +17,7 @@ g5 = Gene("g5") g6 = Gene("g6") g7 = Gene("g7") + gtest = Gene("gtest") r1 = ReactionForward("r1", Dict(m1.id => -1.0, m2.id => 1.0)) r2 = ReactionBidirectional("r2", Dict(m2.id => -2.0, m3.id => 1.0)) @@ -23,6 +25,7 @@ r3 = ReactionBackward("r3", Dict(m1.id => -1.0, m4.id => 2.0)) r4 = ReactionBackward("r4", Dict(m1.id => -5.0, m4.id => 2.0)) r5 = ReactionBackward("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0)) + rtest = ReactionForward("rtest", Dict(m1.id => -1.0, m2.id => 1.0)) rxns = [r1, r2] @@ -104,6 +107,14 @@ remove_reaction!(model, "r1") @test length(model.reactions) == 2 + new_model = model |> with_added_reaction(rtest) + @test haskey(new_model.reactions, "rtest") + @test !haskey(model.reactions, "rtest") + + new_model2 = new_model |> with_removed_reaction("rtest") + @test !haskey(new_model2.reactions, "rtest") + @test haskey(new_model.reactions, "rtest") + @test_throws ArgumentError add_reaction!(model, r3) @test_throws ArgumentError remove_reaction!(model, "abc") @@ -120,6 +131,14 @@ remove_metabolite!(model, "m1") @test length(model.metabolites) == 4 + new_model = model |> with_added_metabolite(mtest) + @test haskey(new_model.metabolites, "mtest") + @test !haskey(model.metabolites, "mtest") + + new_model2 = new_model |> with_removed_metabolite("mtest") + @test !haskey(new_model2.metabolites, "mtest") + @test haskey(new_model.metabolites, "mtest") + @test_throws ArgumentError add_metabolite!(model, m2) @test_throws ArgumentError remove_metabolite!(model, "abc") @@ -136,6 +155,14 @@ remove_gene!(model, "g1") @test length(model.genes) == 4 + new_model = model |> with_added_gene(gtest) + @test haskey(new_model.genes, "gtest") + @test !haskey(model.genes, "gtest") + + new_model2 = new_model |> with_removed_gene("gtest") + @test !haskey(new_model2.genes, "gtest") + @test haskey(new_model.genes, "gtest") + @test_throws ArgumentError add_gene!(model, g7) @test_throws ArgumentError remove_gene!(model, "abc") @@ -148,4 +175,14 @@ @test new_model.genes["g2"].product_lower_bound == -10.0 @test new_model.genes["g2"].product_upper_bound == 10.0 @test model.genes["g2"].product_lower_bound == 0.0 + + # isozymes + isos = [Isozyme(["g1"])] + new_model = model |> with_removed_isozymes("r2") + @test isnothing(new_model.reactions["r2"].gene_associations) + @test !isnothing(model.reactions["r2"].gene_associations) + + new_model2 = new_model |> with_added_isozymes("r2", isos) + @test !isnothing(new_model2.reactions["r2"].gene_associations) + @test isnothing(new_model.reactions["r2"].gene_associations) end diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 1cf8fcf30..7113f8099 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -46,13 +46,14 @@ add_metabolites!(m, [Metabolite("m$i") for i = 1:4]) - ribomodel = m |> with_virtualribosome("r6", 0.2) + @test_throws ArgumentError (m |> with_virtualribosome("r6", 0.2)) + + ribomodel = m |> with_removed_isozymes("r6") |> with_virtualribosome("r6", 0.2) @test haskey(ribomodel.genes, "virtualribosome") @test first(ribomodel.reactions["r6"].gene_associations).kcat_forward == 0.2 @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 - cam = make_simplified_enzyme_constrained_model( ribomodel; total_gene_product_mass_bound = 0.5, @@ -69,6 +70,7 @@ @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) # test inplace variant + remove_isozymes!(m, "r6") add_virtualribosome!(m, "r6", 0.2) cam = m |> with_simplified_enzyme_constraints(total_gene_product_mass_bound = 0.5) @@ -81,21 +83,4 @@ modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> values_dict(:reaction) @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) - - # test with_isozyme functions - iso1 = Isozyme(["g1"]; kcat_forward = 200.0, kcat_backward = 300.0) - iso2 = Isozyme(["g2"]; kcat_forward = 100.0, kcat_backward = 500.0) - m2 = m |> with_isozymes(["r3", "r4"], [[iso1], [iso2]]) - @test first(m2.reactions["r3"].gene_associations).kcat_backward == 300.0 - @test first(m2.reactions["r4"].gene_associations).kcat_backward == 500.0 - @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 - - m2 = m |> with_isozymes("r3", [iso2]) - @test first(m2.reactions["r3"].gene_associations).kcat_backward == 500.0 - @test first(m.reactions["r3"].gene_associations).kcat_backward == 1.0 - - add_isozymes!(m, ["r3", "r4"], [[iso1], [iso2]]) - @test first(m.reactions["r3"].gene_associations).kcat_backward == 300.0 - @test first(m.reactions["r4"].gene_associations).kcat_backward == 500.0 - end From 06700319f356742e0fe1110b338981183d46b8f9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 13:48:04 +0100 Subject: [PATCH 235/531] remove more code duplications and rename checks --- src/misc/check_keys.jl | 69 ++++++++++++---- src/reconstruction.jl | 10 ++- src/reconstruction/ObjectModel.jl | 78 +++++++++---------- test/reconstruction/ObjectModel.jl | 12 +-- test/reconstruction/constrained_allocation.jl | 2 +- 5 files changed, 107 insertions(+), 64 deletions(-) diff --git a/src/misc/check_keys.jl b/src/misc/check_keys.jl index 991d9ec62..f73388b4d 100644 --- a/src/misc/check_keys.jl +++ b/src/misc/check_keys.jl @@ -1,29 +1,72 @@ """ -Throw an ArgumentError if model.field has any keys in xs::Union{Reaction, Metabolite, Gene}. +Throw a DomainError if `model.field` has any keys in the ID field of +xs::Union{Reaction, Metabolite, Gene}. """ -function throw_argerror_if_key_found(model, field, xs) - _ids = collect(keys(getfield(model, field))) - ids = [x.id for x in xs if x.id in _ids] +function check_arg_keys_exists(model, field, xs) + d = getfield(model, field) + ids = [x.id for x in xs if haskey(d, x.id)] isempty(ids) || - throw(ArgumentError("Duplicated $field IDs already present in model: $ids")) + throw(DomainError("Duplicated $field IDs already present in model: $ids")) nothing end """ -Throw an ArgumentError if model.field does not have any keys in xs. +Throw a DomainError if `model.field` does not have any keys in `xs`. """ -function throw_argerror_if_key_missing(model, field, xs::Vector{String}) - _ids = collect(keys(getfield(model, field))) - ids = [x for x in xs if !(x in _ids)] - isempty(ids) || throw(ArgumentError("Missing $field IDs in model: $ids")) +function check_arg_keys_missing(model, field, xs::Vector{String}) + d = getfield(model, field) + ids = [x for x in xs if !haskey(d, x)] + isempty(ids) || throw(DomainError(ids, " $field IDs not found in model.")) nothing end """ -Throw and ArgumentError if rxn_ids have isozymes associated with them. +Throw a DomainError if rxn_ids have isozymes associated with them. """ -function throw_argerror_if_isozymes_found(model, rxn_ids) +function check_has_isozymes(model, rxn_ids) ids = [rid for rid in rxn_ids if !isnothing(model.reactions[rid].gene_associations)] - isempty(ids) || throw(ArgumentError("Isozymes already assign to reactions: $ids")) + isempty(ids) || throw(DomainError(ids, " reactions already have isozymes.")) + nothing +end + +""" +Throw a DomainError if the `biomass_rxn_id` is not in `model_reactions`. +""" +function check_has_biomass_rxn_id(model_reactions, biomass_rxn_id) + haskey(model_reactions, biomass_rxn_id) || + throw(DomainError(biomass_rxn_id, " not found in model.")) + nothing +end + +""" +Throw a DomainError if the `biomass_rxn_id` in `model_reactions` has any +isozymes assigned to it. +""" +function check_biomass_rxn_has_isozymes(model_reactions, biomass_rxn_id) + isnothing(model_reactions[biomass_rxn_id].gene_associations) || + throw(DomainError(biomass_rxn_id, " already has isozymes associated to it.")) + nothing +end + +""" +Throw a DomainError if `virtualribosome_id` is already in the `model_genes`. +""" +function check_has_virtualribosome(model_genes, virtualribosome_id) + haskey(model_genes, virtualribosome_id) && + throw(DomainError(virtualribosome_id, " already found in model.")) + nothing +end + +""" +Throw a DomainError if `biomass_rxn_id` in `model_reactions` already has a +`biomass_metabolite_id`. +""" +function check_has_biomass_rxn_biomas_metabolite( + model_reactions, + biomass_rxn_id, + biomass_metabolite_id, +) + haskey(model_reactions[biomass_rxn_id], biomass_metabolite_id) || + throw(DomainError(biomass_metabolite_id, " not found in $biomass_rxn_id.")) nothing end diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 96559feab..3f3abf1bb 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -15,9 +15,13 @@ using ..ModuleTools using ..Accessors using ..Internal: constants, - throw_argerror_if_key_found, - throw_argerror_if_key_missing, - throw_argerror_if_isozymes_found + check_arg_keys_exists, + check_arg_keys_missing, + check_has_isozymes, + check_has_biomass_rxn_id, + check_biomass_rxn_has_isozymes, + check_has_virtualribosome, + check_has_biomass_rxn_biomas_metabolite using ..Internal.Macros using ..Log.Internal using ..Types diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl index cec5837b1..c8aeb08bb 100644 --- a/src/reconstruction/ObjectModel.jl +++ b/src/reconstruction/ObjectModel.jl @@ -6,7 +6,7 @@ $(TYPEDSIGNATURES) Plural variant of [`add_reaction!`](@ref). """ function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) - throw_argerror_if_key_found(model, :reactions, rxns) + check_arg_keys_exists(model, :reactions, rxns) for rxn in rxns model.reactions[rxn.id] = rxn end @@ -41,7 +41,7 @@ already present in the model. add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) @_remove_fn reaction ObjectModel String inplace plural begin - throw_argerror_if_key_missing(model, :reactions, reaction_ids) + check_arg_keys_missing(model, :reactions, reaction_ids) delete!.(Ref(model.reactions), reaction_ids) nothing end @@ -70,7 +70,7 @@ $(TYPEDSIGNATURES) Plural variant of [`add_metabolite!`](@ref). """ function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) - throw_argerror_if_key_found(model, :metabolites, mets) + check_arg_keys_exists(model, :metabolites, mets) for met in mets model.metabolites[met.id] = met end @@ -105,7 +105,7 @@ Return a shallow copied version of the `model` with `met` added. Only adds add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) @_remove_fn metabolite ObjectModel String inplace plural begin - throw_argerror_if_key_missing(model, :metabolites, metabolite_ids) + check_arg_keys_missing(model, :metabolites, metabolite_ids) remove_reactions!( model, [ @@ -141,7 +141,7 @@ $(TYPEDSIGNATURES) Plural variant of [`add_gene!`](@ref). """ function add_genes!(model::ObjectModel, genes::Vector{Gene}) - throw_argerror_if_key_found(model, :genes, genes) + check_arg_keys_exists(model, :genes, genes) for gene in genes model.genes[gene.id] = gene end @@ -185,7 +185,7 @@ function remove_genes!( gids::Vector{String}; knockout_reactions::Bool = false, ) - throw_argerror_if_key_missing(model, :genes, gids) + check_arg_keys_missing(model, :genes, gids) if knockout_reactions rm_reactions = String[] for (rid, r) in model.reactions @@ -221,7 +221,7 @@ function remove_genes( gids::Vector{String}; knockout_reactions::Bool = false, ) - throw_argerror_if_key_missing(model, :genes, gids) + check_arg_keys_missing(model, :genes, gids) m = copy(model) m.genes = copy(model.genes) @@ -269,7 +269,7 @@ remove_gene(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = end @_change_bounds_fn ObjectModel String inplace plural begin - throw_argerror_if_key_missing(model, :reactions, rxn_ids) + check_arg_keys_missing(model, :reactions, rxn_ids) for (rxn_id, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) @@ -286,7 +286,7 @@ end end @_change_bounds_fn ObjectModel String plural begin - throw_argerror_if_key_missing(model, :reactions, rxn_ids) + check_arg_keys_missing(model, :reactions, rxn_ids) m = copy(model) m.reactions = copy(model.reactions) for (rid, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) @@ -313,7 +313,7 @@ function change_gene_product_bounds!( lower_bounds = fill(nothing, length(gids)), upper_bounds = fill(nothing, length(gids)), ) - throw_argerror_if_key_missing(model, :genes, gids) + check_arg_keys_missing(model, :genes, gids) for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) isnothing(lower) || (model.genes[gid].product_lower_bound = lower) isnothing(upper) || (model.genes[gid].product_upper_bound = upper) @@ -352,7 +352,7 @@ function change_gene_product_bounds( lower_bounds = fill(nothing, length(gids)), upper_bounds = fill(nothing, length(gids)), ) - throw_argerror_if_key_missing(model, :genes, gids) + check_arg_keys_missing(model, :genes, gids) m = copy(model) m.genes = copy(model.genes) for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) @@ -460,8 +460,7 @@ function add_biomass_metabolite!( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) model.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 add_metabolite!(model, Metabolite(biomass_metabolite_id)) end @@ -478,8 +477,7 @@ function add_biomass_metabolite( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) m = copy(model) m.metabolites = copy(m.metabolites) @@ -505,10 +503,12 @@ function remove_biomass_metabolite!( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) - haskey(model.reaction[biomass_rxn_id], biomass_metabolite_id) || - throw(ArgumentError("$biomass_metabolite_id not found in $biomass_rxn_id.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) + check_has_biomass_rxn_biomas_metabolite( + model.reactions, + biomass_rxn_id, + biomass_metabolite_id, + ) delete!(model.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) remove_metabolite!(model, biomass_metabolite_id) @@ -525,10 +525,12 @@ function remove_biomass_metabolite( biomass_rxn_id::String; biomass_metabolite_id = "biomass", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) - haskey(model.reaction[biomass_rxn_id], biomass_metabolite_id) || - throw(ArgumentError("$biomass_metabolite_id not found in $biomass_rxn_id.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) + check_has_biomass_rxn_biomas_metabolite( + model.reactions, + biomass_rxn_id, + biomass_metabolite_id, + ) m = copy(model) m.metabolites = copy(m.metabolites) @@ -581,12 +583,9 @@ function add_virtualribosome!( weight::Float64; virtualribosome_id = "virtualribosome", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) - isnothing(model.reactions[biomass_rxn_id].gene_associations) || - throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) - haskey(model.genes, virtualribosome_id) && - throw(ArgumentError("$virtualribosome_id already found in model.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) + check_biomass_rxn_has_isozymes(model.reactions, biomass_rxn_id) + check_has_virtualribosome(model.genes, virtualribosome_id) # ensure unidirectional model.reactions[biomass_rxn_id].lower_bound = 0.0 @@ -620,12 +619,9 @@ function add_virtualribosome( weight::Float64; virtualribosome_id = "virtualribosome", ) - haskey(model.reactions, biomass_rxn_id) || - throw(ArgumentError("$biomass_rxn_id not found in model.")) - isnothing(model.reactions[biomass_rxn_id].gene_associations) || - throw(ArgumentError("$biomass_rxn_id already has isozymes associated to it.")) - haskey(model.genes, virtualribosome_id) && - throw(ArgumentError("$virtualribosome_id already found in model.")) + check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) + check_biomass_rxn_has_isozymes(model.reactions, biomass_rxn_id) + check_has_virtualribosome(model.genes, virtualribosome_id) m = copy(model) m.reactions = copy(model.reactions) @@ -649,8 +645,8 @@ function add_isozymes!( rids::Vector{String}, isozymes_vector::Vector{Vector{Isozyme}}, ) - throw_argerror_if_key_missing(model, :reactions, rids) - throw_argerror_if_isozymes_found(model, rids) + check_arg_keys_missing(model, :reactions, rids) + check_has_isozymes(model, rids) for (rid, isozymes) in zip(rids, isozymes_vector) model.reactions[rid].gene_associations = isozymes @@ -675,8 +671,8 @@ function add_isozymes( rids::Vector{String}, isozymes_vector::Vector{Vector{Isozyme}}, ) - throw_argerror_if_key_missing(model, :reactions, rids) - throw_argerror_if_isozymes_found(model, rids) + check_arg_keys_missing(model, :reactions, rids) + check_has_isozymes(model, rids) m = copy(model) m.reactions = copy(model.reactions) @@ -707,7 +703,7 @@ $(TYPEDSIGNATURES) Plural variant of [`remove_isozyme!`](@ref). """ function remove_isozymes!(model::ObjectModel, rids::Vector{String}) - throw_argerror_if_key_missing(model, :reactions, rids) + check_arg_keys_missing(model, :reactions, rids) for rid in rids model.reactions[rid].gene_associations = nothing end @@ -727,7 +723,7 @@ $(TYPEDSIGNATURES) Plural variant of [`remove_isozyme`](@ref). """ function remove_isozymes(model::ObjectModel, rids::Vector{String}) - throw_argerror_if_key_missing(model, :reactions, rids) + check_arg_keys_missing(model, :reactions, rids) m = copy(model) m.reactions = copy(model.reactions) diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index dfa9b4b6a..ac2c2fd2b 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -115,8 +115,8 @@ @test !haskey(new_model2.reactions, "rtest") @test haskey(new_model.reactions, "rtest") - @test_throws ArgumentError add_reaction!(model, r3) - @test_throws ArgumentError remove_reaction!(model, "abc") + @test_throws DomainError add_reaction!(model, r3) + @test_throws DomainError remove_reaction!(model, "abc") ### metabolites add_metabolites!(model, [m5, m6]) @@ -139,8 +139,8 @@ @test !haskey(new_model2.metabolites, "mtest") @test haskey(new_model.metabolites, "mtest") - @test_throws ArgumentError add_metabolite!(model, m2) - @test_throws ArgumentError remove_metabolite!(model, "abc") + @test_throws DomainError add_metabolite!(model, m2) + @test_throws DomainError remove_metabolite!(model, "abc") ### genes add_genes!(model, [g5, g6]) @@ -163,8 +163,8 @@ @test !haskey(new_model2.genes, "gtest") @test haskey(new_model.genes, "gtest") - @test_throws ArgumentError add_gene!(model, g7) - @test_throws ArgumentError remove_gene!(model, "abc") + @test_throws DomainError add_gene!(model, g7) + @test_throws DomainError remove_gene!(model, "abc") # change gene change_gene_product_bound!(model, "g3"; lower_bound = -10, upper_bound = 10) diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index 7113f8099..b0b4971d1 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -46,7 +46,7 @@ add_metabolites!(m, [Metabolite("m$i") for i = 1:4]) - @test_throws ArgumentError (m |> with_virtualribosome("r6", 0.2)) + @test_throws DomainError (m |> with_virtualribosome("r6", 0.2)) ribomodel = m |> with_removed_isozymes("r6") |> with_virtualribosome("r6", 0.2) From e0ccc03e25459da31de5c4a2d9394b90ab2942e7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 12 Feb 2023 22:57:05 +0100 Subject: [PATCH 236/531] add basic pipe-able pfba --- src/analysis/flux_balance_analysis.jl | 1 - src/reconstruction/pipes/parsimonious.jl | 9 ++++ src/types/wrappers/ParsimoniousModel.jl | 48 +++++++++++++++++++ .../parsimonious_flux_balance_analysis.jl | 12 +++++ test/reconstruction/enzyme_constrained.jl | 29 ++++++++++- 5 files changed, 97 insertions(+), 2 deletions(-) create mode 100644 src/reconstruction/pipes/parsimonious.jl create mode 100644 src/types/wrappers/ParsimoniousModel.jl diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 0a00990fd..f0611fe22 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -45,7 +45,6 @@ function flux_balance_analysis(model::AbstractMetabolicModel, optimizer; modific ModelWithResult(model, opt_model) end - """ $(TYPEDSIGNATURES) diff --git a/src/reconstruction/pipes/parsimonious.jl b/src/reconstruction/pipes/parsimonious.jl new file mode 100644 index 000000000..5fea8ed26 --- /dev/null +++ b/src/reconstruction/pipes/parsimonious.jl @@ -0,0 +1,9 @@ +""" +$(TYPEDSIGNATURES) + +Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper. +""" +with_parsimonious_solution(semantics::Vector{Symbol}) = + model -> ParsimoniousModel(model, semantics) + +with_parsimonious_solution(semantic::Symbol) = with_parsimonious_solution([semantic]) diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/types/wrappers/ParsimoniousModel.jl new file mode 100644 index 000000000..d9f95a36b --- /dev/null +++ b/src/types/wrappers/ParsimoniousModel.jl @@ -0,0 +1,48 @@ + +""" +$(TYPEDEF) + +A wrapper that adds a quadratic objective that minimizes the sum of a set of +squared variables. If the bounds of the growth rate of the model are fixed, this +corresponds to finding a parsimonious solution (i.e. pFBA). + +This is used to implement [`parsimonious_flux_balance_analysis`](@ref). + +# Example +``` +model |> with_changed_bound("biomass", lower_bound = 0.1) |> with_parsimonious_solution(:enzymes) |> flux_balance_analysis(Clarabel.Optimizer) +``` +""" +struct ParsimoniousModel <: AbstractModelWrapper + inner::AbstractMetabolicModel + var_ids::Vector{String} +end + +function ParsimoniousModel(model::AbstractMetabolicModel, semantics::Vector{Symbol}) + var_ids = vcat([@eval $(Symbol(sem, :s))($(model)) for sem in semantics]...) + ParsimoniousModel(model, var_ids) +end + +ParsimoniousModel(model::AbstractMetabolicModel, semantic::Symbol) = + ParsimoniousModel(model, [semantic]) + +Accessors.unwrap_model(m::ParsimoniousModel) = m.inner + +""" +$(TYPEDSIGNATURES) + +Return a negative, uniformly weighted, quadratic-only objective representing the +squared sum of `model.var_ids`. +""" +function Accessors.objective(model::ParsimoniousModel)::SparseMat + obj = spzeros(n_variables(model), n_variables(model) + 1) # + 1 for QP solver formulation + + idxs = indexin(model.var_ids, variables(model)) + j = findfirst(isnothing, idxs) + isnothing(j) || throw(DomainError(model.var_ids[j], "No variable with this ID.")) + + for i in idxs + obj[i, i] = -1.0 # negative because objective will be maximized + end + obj +end diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index edbbc54aa..31cd6ef7e 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -15,4 +15,16 @@ # The used optimizer doesn't really converge to the same answer everytime # here, we therefore tolerate a wide range of results. @test isapprox(d["biomass1"], 10.0, atol = QP_TEST_TOLERANCE) + + sol = + model |> + with_changed_bound("biomass1", lower_bound = 10.0) |> + with_parsimonious_solution(:reaction) |> + flux_balance_analysis(Clarabel.Optimizer) + + @test isapprox( + values_dict(:reaction, model, sol)["biomass1"], + 10.0, + atol = QP_TEST_TOLERANCE, + ) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index bc237f5ac..ca03b4e8e 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -73,8 +73,9 @@ atol = TEST_TOLERANCE, ) - # test enzyme objective + # test pFBA growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 + res = flux_balance_analysis( gm, Tulip.Optimizer; @@ -86,6 +87,32 @@ ) mass_groups_min = values_dict(:enzyme_group, res) @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] + + gm_changed_bound = + model |> + with_changed_bound("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb) |> + with_enzyme_constraints( + gene_product_mass_group_bound = Dict( + "uncategorized" => total_gene_product_mass, + ), + ) + + pfba_sol = + gm_changed_bound |> + with_parsimonious_solution(:enzyme) |> + flux_balance_analysis(Clarabel.Optimizer) + + @test isapprox( + values_dict(:reaction, gm_changed_bound, pfba_sol)["BIOMASS_Ecoli_core_w_GAM"], + 0.7315450597991255, + atol = QP_TEST_TOLERANCE, + ) + + @test isapprox( + values_dict(:enzyme_group, gm_changed_bound, pfba_sol)["uncategorized"], + 91.425, + atol = QP_TEST_TOLERANCE, + ) end @testset "GECKO small model" begin From 637da14ef51c56d73598b451d563346cee9f96f4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 16:59:37 +0100 Subject: [PATCH 237/531] Add test and rebase with Result struct --- src/analysis/modifications/generic.jl | 4 +++- src/reconstruction/pipes/parsimonious.jl | 18 +++++++++++++- src/types/wrappers/ParsimoniousModel.jl | 14 +++++++++-- .../parsimonious_flux_balance_analysis.jl | 14 +++++------ test/reconstruction/enzyme_constrained.jl | 24 ++++++++----------- 5 files changed, 49 insertions(+), 25 deletions(-) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 71e542e92..5936276fc 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -57,7 +57,9 @@ analysis function. `new_objective` can be a single reaction identifier, or an array of reactions identifiers. Optionally, the objective can be weighted by a vector of `weights`, and a -optimization `sense` can be set to either `MAX_SENSE` or `MIN_SENSE`. +optimization `sense` can be set to either `MAX_SENSE` or `MIN_SENSE`. Note, the +`sense` argument is a JuMP constant, thus you need to import JUMP for the name +to be available. """ modify_objective( new_objective::Union{String,Vector{String}}; diff --git a/src/reconstruction/pipes/parsimonious.jl b/src/reconstruction/pipes/parsimonious.jl index 5fea8ed26..7f8e39fdf 100644 --- a/src/reconstruction/pipes/parsimonious.jl +++ b/src/reconstruction/pipes/parsimonious.jl @@ -1,9 +1,25 @@ """ $(TYPEDSIGNATURES) -Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper. +Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the +solution of variables specified by `semantics`. """ with_parsimonious_solution(semantics::Vector{Symbol}) = model -> ParsimoniousModel(model, semantics) +""" +$(TYPEDSIGNATURES) + +Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the +solution of variables specified by `semantic`. +""" with_parsimonious_solution(semantic::Symbol) = with_parsimonious_solution([semantic]) + +""" +$(TYPEDSIGNATURES) + +Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the +solution of variables specified by `var_ids`. +""" +with_parsimonious_solution(var_ids::Vector{String}) = + model -> ParsimoniousModel(model, var_ids) diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/types/wrappers/ParsimoniousModel.jl index d9f95a36b..c75b8d440 100644 --- a/src/types/wrappers/ParsimoniousModel.jl +++ b/src/types/wrappers/ParsimoniousModel.jl @@ -4,13 +4,18 @@ $(TYPEDEF) A wrapper that adds a quadratic objective that minimizes the sum of a set of squared variables. If the bounds of the growth rate of the model are fixed, this -corresponds to finding a parsimonious solution (i.e. pFBA). +corresponds to finding a parsimonious solution (i.e. pFBA). Note, the sense of +the optimization problem should be `MAX_SENSE`, because internally the objective +is negated. This is used to implement [`parsimonious_flux_balance_analysis`](@ref). # Example ``` -model |> with_changed_bound("biomass", lower_bound = 0.1) |> with_parsimonious_solution(:enzymes) |> flux_balance_analysis(Clarabel.Optimizer) +res = model |> + with_changed_bound("biomass", lower_bound = 0.1) |> + with_parsimonious_solution(:enzymes) |> + flux_balance_analysis(Clarabel.Optimizer) ``` """ struct ParsimoniousModel <: AbstractModelWrapper @@ -46,3 +51,8 @@ function Accessors.objective(model::ParsimoniousModel)::SparseMat end obj end + +# need to manually inherit these +Accessors.enzyme_variables(model::ParsimoniousModel) = enzyme_variables(model.inner) +Accessors.enzyme_group_variables(model::ParsimoniousModel) = + enzyme_group_variables(model.inner) diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 31cd6ef7e..a32ed5b31 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -16,15 +16,15 @@ # here, we therefore tolerate a wide range of results. @test isapprox(d["biomass1"], 10.0, atol = QP_TEST_TOLERANCE) - sol = + d2 = model |> with_changed_bound("biomass1", lower_bound = 10.0) |> with_parsimonious_solution(:reaction) |> - flux_balance_analysis(Clarabel.Optimizer) + flux_balance_analysis(Clarabel.Optimizer) |> + values_dict - @test isapprox( - values_dict(:reaction, model, sol)["biomass1"], - 10.0, - atol = QP_TEST_TOLERANCE, - ) + @test all(isapprox(d[k], d2[k], atol = QP_TEST_TOLERANCE) for k in keys(d2)) + + Q = objective(model |> with_parsimonious_solution(:reaction)) + @test all(Q[i, i] == -1 for i = 1:7) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index ca03b4e8e..3e869a187 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -75,7 +75,7 @@ # test pFBA growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 - + res = flux_balance_analysis( gm, Tulip.Optimizer; @@ -88,29 +88,25 @@ mass_groups_min = values_dict(:enzyme_group, res) @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] - gm_changed_bound = + res2 = model |> with_changed_bound("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb) |> - with_enzyme_constraints( - gene_product_mass_group_bound = Dict( - "uncategorized" => total_gene_product_mass, - ), - ) - - pfba_sol = - gm_changed_bound |> + with_enzyme_constraints(; total_gene_product_mass_bound) |> with_parsimonious_solution(:enzyme) |> - flux_balance_analysis(Clarabel.Optimizer) + flux_balance_analysis( + Clarabel.Optimizer; + modifications = [modify_optimizer_attribute("max_iter", 1000)], + ) @test isapprox( - values_dict(:reaction, gm_changed_bound, pfba_sol)["BIOMASS_Ecoli_core_w_GAM"], + values_dict(:reaction, res2)["BIOMASS_Ecoli_core_w_GAM"], 0.7315450597991255, atol = QP_TEST_TOLERANCE, ) @test isapprox( - values_dict(:enzyme_group, gm_changed_bound, pfba_sol)["uncategorized"], - 91.425, + values_dict(:enzyme_group, res2)["uncategorized"], + 91.4275211, atol = QP_TEST_TOLERANCE, ) end From dabbe840a130f41693307836f448b90df5a42d05 Mon Sep 17 00:00:00 2001 From: stelmo Date: Mon, 20 Feb 2023 16:13:43 +0000 Subject: [PATCH 238/531] automatic formatting triggered by @stelmo on PR #747 --- src/types/wrappers/ParsimoniousModel.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/types/wrappers/ParsimoniousModel.jl index c75b8d440..f8acd03de 100644 --- a/src/types/wrappers/ParsimoniousModel.jl +++ b/src/types/wrappers/ParsimoniousModel.jl @@ -12,9 +12,9 @@ This is used to implement [`parsimonious_flux_balance_analysis`](@ref). # Example ``` -res = model |> - with_changed_bound("biomass", lower_bound = 0.1) |> - with_parsimonious_solution(:enzymes) |> +res = model |> + with_changed_bound("biomass", lower_bound = 0.1) |> + with_parsimonious_solution(:enzymes) |> flux_balance_analysis(Clarabel.Optimizer) ``` """ @@ -37,10 +37,10 @@ Accessors.unwrap_model(m::ParsimoniousModel) = m.inner $(TYPEDSIGNATURES) Return a negative, uniformly weighted, quadratic-only objective representing the -squared sum of `model.var_ids`. +squared sum of `model.var_ids`. """ function Accessors.objective(model::ParsimoniousModel)::SparseMat - obj = spzeros(n_variables(model), n_variables(model) + 1) # + 1 for QP solver formulation + obj = spzeros(n_variables(model), n_variables(model) + 1) # + 1 for QP solver formulation idxs = indexin(model.var_ids, variables(model)) j = findfirst(isnothing, idxs) From bd6a35d1f5a4bb8bf39f5ffcd96f7a5e71822b92 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 17:19:19 +0100 Subject: [PATCH 239/531] remove eval --- src/types/wrappers/ParsimoniousModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/types/wrappers/ParsimoniousModel.jl index f8acd03de..b6f4ffe64 100644 --- a/src/types/wrappers/ParsimoniousModel.jl +++ b/src/types/wrappers/ParsimoniousModel.jl @@ -24,7 +24,7 @@ struct ParsimoniousModel <: AbstractModelWrapper end function ParsimoniousModel(model::AbstractMetabolicModel, semantics::Vector{Symbol}) - var_ids = vcat([@eval $(Symbol(sem, :s))($(model)) for sem in semantics]...) + var_ids = vcat([first(Accessors.Internal.semantics(sem))(model) for sem in semantics]...) ParsimoniousModel(model, var_ids) end From f916bfbd30c079b988fef2973d6159c512cba346 Mon Sep 17 00:00:00 2001 From: stelmo Date: Mon, 20 Feb 2023 16:21:14 +0000 Subject: [PATCH 240/531] automatic formatting triggered by @stelmo on PR #747 --- src/types/wrappers/ParsimoniousModel.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/types/wrappers/ParsimoniousModel.jl index b6f4ffe64..e3e8f5e34 100644 --- a/src/types/wrappers/ParsimoniousModel.jl +++ b/src/types/wrappers/ParsimoniousModel.jl @@ -24,7 +24,8 @@ struct ParsimoniousModel <: AbstractModelWrapper end function ParsimoniousModel(model::AbstractMetabolicModel, semantics::Vector{Symbol}) - var_ids = vcat([first(Accessors.Internal.semantics(sem))(model) for sem in semantics]...) + var_ids = + vcat([first(Accessors.Internal.semantics(sem))(model) for sem in semantics]...) ParsimoniousModel(model, var_ids) end From 1139170145b04222a45778a8e837af172544ef87 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 17:55:38 +0100 Subject: [PATCH 241/531] rename files --- ...lancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} | 0 ...lancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} | 0 ...lancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} | 0 ...lancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} | 0 4 files changed, 0 insertions(+), 0 deletions(-) rename src/types/misc/{BalancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} (100%) rename src/types/models/{BalancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} (100%) rename src/utils/{BalancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} (100%) rename test/types/{BalancedGrowthCommunityModel.jl => EqualGrowthCommunityModel.jl} (100%) diff --git a/src/types/misc/BalancedGrowthCommunityModel.jl b/src/types/misc/EqualGrowthCommunityModel.jl similarity index 100% rename from src/types/misc/BalancedGrowthCommunityModel.jl rename to src/types/misc/EqualGrowthCommunityModel.jl diff --git a/src/types/models/BalancedGrowthCommunityModel.jl b/src/types/models/EqualGrowthCommunityModel.jl similarity index 100% rename from src/types/models/BalancedGrowthCommunityModel.jl rename to src/types/models/EqualGrowthCommunityModel.jl diff --git a/src/utils/BalancedGrowthCommunityModel.jl b/src/utils/EqualGrowthCommunityModel.jl similarity index 100% rename from src/utils/BalancedGrowthCommunityModel.jl rename to src/utils/EqualGrowthCommunityModel.jl diff --git a/test/types/BalancedGrowthCommunityModel.jl b/test/types/EqualGrowthCommunityModel.jl similarity index 100% rename from test/types/BalancedGrowthCommunityModel.jl rename to test/types/EqualGrowthCommunityModel.jl From 4fb403e94e605b9695559af39e5a856129301bac Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 18:36:25 +0100 Subject: [PATCH 242/531] partially implement --- src/io/show/BalancedGrowthCommunityModel.jl | 2 +- src/types/misc/EqualGrowthCommunityModel.jl | 4 +- src/types/models/EqualGrowthCommunityModel.jl | 111 ++++--- src/utils/EqualGrowthCommunityModel.jl | 4 +- test/reconstruction/ObjectModel.jl | 7 + test/types/EqualGrowthCommunityModel.jl | 280 +++++++++--------- 6 files changed, 201 insertions(+), 207 deletions(-) diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl index 02e6c3d84..2174c3e85 100644 --- a/src/io/show/BalancedGrowthCommunityModel.jl +++ b/src/io/show/BalancedGrowthCommunityModel.jl @@ -5,7 +5,7 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) ) end -function Base.show(io::Base.IO, ::MIME"text/plain", cm::BalancedGrowthCommunityModel) +function Base.show(io::Base.IO, ::MIME"text/plain", cm::EqualGrowthCommunityModel) println( io, "A balanced growth community model comprised of $(length(cm.members)) underlying models.", diff --git a/src/types/misc/EqualGrowthCommunityModel.jl b/src/types/misc/EqualGrowthCommunityModel.jl index 0cbc81dfa..8e378ab84 100644 --- a/src/types/misc/EqualGrowthCommunityModel.jl +++ b/src/types/misc/EqualGrowthCommunityModel.jl @@ -12,7 +12,7 @@ $(TYPEDSIGNATURES) A helper function to get the unique environmental metabolites. """ -get_env_mets(cm::BalancedGrowthCommunityModel) = +get_env_mets(cm::EqualGrowthCommunityModel) = unique(vcat([get_exchange_mets(m) for m in cm.members]...)) """ @@ -40,7 +40,7 @@ A helper function to find the index of the appropriate model. Assumes each `id` is delimited by `#` that separates the model ID prefix and the original id. """ function access_community_member( - cm::BalancedGrowthCommunityModel, + cm::EqualGrowthCommunityModel, id::String, accessor::Function; default = nothing, diff --git a/src/types/models/EqualGrowthCommunityModel.jl b/src/types/models/EqualGrowthCommunityModel.jl index 7e9ed531c..d3a368e18 100644 --- a/src/types/models/EqualGrowthCommunityModel.jl +++ b/src/types/models/EqualGrowthCommunityModel.jl @@ -2,7 +2,7 @@ $(TYPEDEF) A standardized structure used to package models that can easily be combined into -a [`BalancedGrowthCommunityModel`](@ref). +an [`EqualGrowthCommunityModel`](@ref). # Fields $(TYPEDFIELDS) @@ -10,10 +10,8 @@ $(TYPEDFIELDS) # Assumptions 1. Exchange reactions, *all* of which are idenitified in `exchange_reaction_ids`, have the form: `A[external] ⟷ ∅` where `A` is a metabolite. No other - exchanges are allowed. -2. The biomass reaction of a model produces a biomass metabolite called - `biomass_metabolite_id` with stoichiometry 1. -3. There is only one biomass reaction in the model. + exchanges are allowed. This is not checked, but only assumed. +2. There is only one biomass reaction in the model. """ Base.@kwdef mutable struct CommunityMember "Name of model appended to intracellular reactions and metabolites." @@ -24,8 +22,8 @@ Base.@kwdef mutable struct CommunityMember model::AbstractMetabolicModel "List of all exchange reactions in model." exchange_reaction_ids::Vector{String} - "ID of biomass metabolite." - biomass_metabolite_id::String + "ID of biomass reaction." + biomass_reaction_id::String end """ @@ -40,8 +38,7 @@ $(TYPEDFIELDS) 2. It is assumed that the same namespace is used to identify unique exchanged metabolites. 3. The objective created by this model is the equal growth rate/balanced growth - objective. In short, all biomass metabolites are produced at the same rate. -4. The flux units are `mmol X/gDW_total/h` for some metabolite `X`. + objective. # Implementation notes 1. All reactions have the `id` of each respective underlying @@ -54,13 +51,8 @@ $(TYPEDFIELDS) environmental metabolites have no prefix. 3. All genes have the `id` of the respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -4. Each bound from the underlying community members is multiplied by the - abundance of that member. -5. This objective is assumed to be the equal growth rate/balanced growth - objective. Consequently, the relation `community_growth * - abundance_species_i = growth_species_i` should hold. """ -Base.@kwdef mutable struct BalancedGrowthCommunityModel <: AbstractMetabolicModel +Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractMetabolicModel "Models making up the community." members::Vector{CommunityMember} "Name of the objective" @@ -70,68 +62,73 @@ Base.@kwdef mutable struct BalancedGrowthCommunityModel <: AbstractMetabolicMode Dict{String,Tuple{Float64,Float64}}() end -function Accessors.variables(cm::BalancedGrowthCommunityModel) +function Accessors.variables(cm::EqualGrowthCommunityModel) rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] return [rxns; env_exs; cm.objective_id] end -function Accessors.n_variables(cm::BalancedGrowthCommunityModel) +function Accessors.n_variables(cm::EqualGrowthCommunityModel) num_model_reactions = sum(n_variables(m.model) for m in cm.members) # assume each env metabolite gets an env exchange num_env_metabolites = length(get_env_mets(cm)) return num_model_reactions + num_env_metabolites + 1 # add 1 for the community biomass end -function Accessors.metabolites(cm::BalancedGrowthCommunityModel) - mets = - [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] - return [mets; "ENV_" .* get_env_mets(cm)] +function Accessors.metabolites(cm::EqualGrowthCommunityModel) + mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] + biomass_constraints = [m.id for m in cm.members] + return [mets; "ENV_" .* get_env_mets(cm); biomass_constraints] end -function Accessors.n_metabolites(cm::BalancedGrowthCommunityModel) - num_model_reactions = sum(n_metabolites(m.model) for m in cm.members) +function Accessors.n_metabolites(cm::EqualGrowthCommunityModel) + num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) # assume each env metabolite gets an env exchange num_env_metabolites = length(get_env_mets(cm)) - return num_model_reactions + num_env_metabolites + return num_model_constraints + num_env_metabolites + length(cm.members) end -Accessors.genes(cm::BalancedGrowthCommunityModel) = +Accessors.genes(cm::EqualGrowthCommunityModel) = [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] -Accessors.balance(cm::BalancedGrowthCommunityModel) = [ - vcat([balance(m.model) .* m.abundance for m in cm.members]...) +Accessors.n_genes(cm::EqualGrowthCommunityModel) = + sum(n_genes(m.model) for m in cm.members) + +Accessors.balance(cm::EqualGrowthCommunityModel) = [ + vcat([balance(m.model) for m in cm.members]...) spzeros(length(get_env_mets(cm))) + spzeros(length(cm.members)) ] -Accessors.n_genes(cm::BalancedGrowthCommunityModel) = - sum(n_genes(m.model) for m in cm.members) - -function Accessors.stoichiometry(cm::BalancedGrowthCommunityModel) - env_mets = get_env_mets(cm) +function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) + env_met_ids = get_env_mets(cm) model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) + model_env = spzeros(size(model_S, 1), length(env_met_ids)) + obj1 = spzeros(size(model_env, 1)) - zero_rxns = spzeros(size(model_S, 1), length(env_mets)) - obj_rxn = spzeros(size(model_S, 1)) - obj_rxn[indexin( - [add_community_prefix(m, m.biomass_metabolite_id) for m in cm.members], - metabolites(cm), - )] .= [-m.abundance for m in cm.members] # fix units of biomass - - env_met_rows = spzeros(length(env_mets), size(model_S, 2)) - env_met_rows = hcat([env_ex_matrix(m, env_mets) for m in cm.members]...) - zero_objs = spzeros(length(env_mets)) + env_rows = hcat([env_ex_matrix(m, env_met_ids) .* m.abundance for m in cm.members]...) + obj2 = spzeros(size(env_rows, 1)) + obj_rows = spzeros(length(cm.members), size(model_S, 2)) + for i in 1:length(cm.members) + m = cm.members[i] + j = first(indexin([add_community_prefix(m, m.biomass_reaction_id)], variables(cm))) + obj_rows[i, j] = 1.0 + end + env_cols = spzeros(length(cm.members), length(env_met_ids)) + obj3 = -ones(length(cm.members)) + return [ - model_S zero_rxns obj_rxn - env_met_rows -I zero_objs + model_S model_env obj1 + env_rows -I obj2 + obj_rows env_cols obj3 ] end -function Accessors.bounds(cm::BalancedGrowthCommunityModel) - models_lbs = vcat([first(bounds(m.model)) .* m.abundance for m in cm.members]...) - models_ubs = vcat([last(bounds(m.model)) .* m.abundance for m in cm.members]...) +function Accessors.bounds(cm::EqualGrowthCommunityModel) + models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) + models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) env_lbs = [ first(get(cm.env_met_flux_bounds, met_id, -constants.default_reaction_bound)) for met_id in get_env_mets(cm) @@ -146,24 +143,24 @@ function Accessors.bounds(cm::BalancedGrowthCommunityModel) ) end -function Accessors.objective(cm::BalancedGrowthCommunityModel) +function Accessors.objective(cm::EqualGrowthCommunityModel) vec = spzeros(n_variables(cm)) vec[end] = 1.0 return vec end -function Accessors.coupling(cm::BalancedGrowthCommunityModel) +function Accessors.coupling(cm::EqualGrowthCommunityModel) coups = blockdiag([coupling(m.model) for m in cm.members]...) n = n_variables(cm) return [coups spzeros(size(coups, 1), n - size(coups, 2))] end -Accessors.n_coupling_constraints(cm::BalancedGrowthCommunityModel) = +Accessors.n_coupling_constraints(cm::EqualGrowthCommunityModel) = sum(n_coupling_constraints(m.model) for m in cm.members) -function Accessors.coupling_bounds(cm::BalancedGrowthCommunityModel) - lbs = vcat([first(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) - ubs = vcat([last(coupling_bounds(m.model)) .* m.abundance for m in cm.members]...) +function Accessors.coupling_bounds(cm::EqualGrowthCommunityModel) + lbs = vcat([first(coupling_bounds(m.model)) for m in cm.members]...) + ubs = vcat([last(coupling_bounds(m.model)) for m in cm.members]...) return (lbs, ubs) end @@ -174,20 +171,20 @@ Returns a matrix, which when multipled by the solution of a constraints based problem, yields the semantically meaningful fluxes that correspond to [`reactions`](@ref). """ -function Accessors.reaction_variables_matrix(cm::BalancedGrowthCommunityModel) +function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) # TODO add the non-matrix form! rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) nr = length(get_env_mets(cm)) + 1 # env ex + obj blockdiag(rfs, spdiagm(fill(1, nr))) end -Accessors.reactions(cm::BalancedGrowthCommunityModel) = [ +Accessors.reactions(cm::EqualGrowthCommunityModel) = [ vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) ["EX_" * env_met for env_met in get_env_mets(cm)] cm.objective_id ] -Accessors.n_reactions(cm::BalancedGrowthCommunityModel) = +Accessors.n_reactions(cm::EqualGrowthCommunityModel) = sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 #= @@ -214,7 +211,7 @@ for (func, def) in ( (:gene_name, nothing), ) @eval begin - Accessors.$func(cm::BalancedGrowthCommunityModel, id::String) = + Accessors.$func(cm::EqualGrowthCommunityModel, id::String) = access_community_member(cm, id, $func; default = $def) end end diff --git a/src/utils/EqualGrowthCommunityModel.jl b/src/utils/EqualGrowthCommunityModel.jl index d52d12c3a..3e731d8bf 100644 --- a/src/utils/EqualGrowthCommunityModel.jl +++ b/src/utils/EqualGrowthCommunityModel.jl @@ -6,7 +6,7 @@ a solved optimization model built from the `community_model`. Removes the `community_member` prefix in the string ids of the returned dictionary. """ get_community_member_solution( - community_model::BalancedGrowthCommunityModel, + community_model::EqualGrowthCommunityModel, opt_model, community_member::CommunityMember, ) = @@ -25,7 +25,7 @@ $(TYPEDSIGNATURES) Extract the environmental exchanges from `opt_model`, which is a solved optimization model built from the `community_model`. """ -get_environmental_exchanges(community_model::BalancedGrowthCommunityModel, opt_model) = +get_environmental_exchanges(community_model::EqualGrowthCommunityModel, opt_model) = is_solved(opt_model) ? Dict( rid => val for (rid, val) in zip( diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index ac2c2fd2b..92dc48caf 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -185,4 +185,11 @@ new_model2 = new_model |> with_added_isozymes("r2", isos) @test !isnothing(new_model2.reactions["r2"].gene_associations) @test isnothing(new_model.reactions["r2"].gene_associations) + + # test added biomass metabolite + # new_model = model |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") + # @test "biomass" in metabolites(modded_ecoli) + # @test !("biomass" in metabolites(ecoli)) + # @test haskey(modded_ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") + # @test !haskey(ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") end diff --git a/test/types/EqualGrowthCommunityModel.jl b/test/types/EqualGrowthCommunityModel.jl index 161706681..dfce48907 100644 --- a/test/types/EqualGrowthCommunityModel.jl +++ b/test/types/EqualGrowthCommunityModel.jl @@ -1,4 +1,5 @@ -@testset "BalancedGrowthCommunityModel: simple model" begin +@testset "EqualGrowthCommunityModel: simple model" begin + m1 = ObjectModel() add_metabolites!( m1, @@ -7,7 +8,6 @@ Metabolite("B"), Metabolite("Ae"), Metabolite("Be"), - Metabolite("X1"), ], ) add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) @@ -16,7 +16,7 @@ [ ReactionBidirectional("EX_A", Dict("Ae" => -1)), ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), - ReactionBidirectional("r2", Dict("A" => -1, "B" => 1, "X1" => 1)), + ReactionForward("r2", Dict("A" => -1, "B" => 1)), # bm1 ReactionForward("r3", Dict("B" => -1, "Be" => 1)), ReactionForward("EX_B", Dict("Be" => -1)), ], @@ -30,7 +30,6 @@ Metabolite("A"), Metabolite("C"), Metabolite("Ce"), - Metabolite("X2"), ], ) add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) @@ -41,7 +40,7 @@ ReactionForward("EX_C", Dict("Ce" => -1)), ReactionBidirectional("EX_A", Dict("Ae" => -1)), ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), - ReactionBidirectional("r2", Dict("A" => -1, "C" => 1, "X2" => 1)), + ReactionForward("r2", Dict("A" => -1, "C" => 1)), #bm2 ], ) @@ -50,7 +49,7 @@ abundance = 0.2, model = m1, exchange_reaction_ids = ["EX_A", "EX_B"], - biomass_metabolite_id = "X1", + biomass_reaction_id = "r2", ) @test contains(sprint(show, MIME("text/plain"), cm1), "community member") @@ -59,10 +58,10 @@ abundance = 0.8, model = m2, exchange_reaction_ids = ["EX_A", "EX_C"], - biomass_metabolite_id = "X2", + biomass_reaction_id = "r2", ) - cm = BalancedGrowthCommunityModel( + cm = EqualGrowthCommunityModel( members = [cm1, cm2], env_met_flux_bounds = Dict("Ae" => (-10, 10)), ) @@ -88,125 +87,124 @@ ], ) - @test issetequal( - metabolites(cm), - [ - "m1#A" - "m1#B" - "m1#Ae" - "m1#Be" - "m1#X1" - "m2#Ae" - "m2#A" - "m2#C" - "m2#Ce" - "m2#X2" - "ENV_Ae" - "ENV_Be" - "ENV_Ce" - ], - ) - - @test issetequal( - genes(cm), - [ - "m1#g1" - "m1#g2" - "m1#g3" - "m1#g4" - "m2#g1" - "m2#g2" - "m2#g3" - "m2#g4" - ], - ) - - @test n_variables(cm) == 14 - @test n_metabolites(cm) == 13 - @test n_genes(cm) == 8 - - @test all( - stoichiometry(cm) .== [ - 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 -0.8 - 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 -1.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 - ], - ) - - lbs, ubs = bounds(cm) - @test all( - lbs .== [ - -200.0 - -200.0 - -200.0 - 0.0 - 0.0 - 0.0 - 0.0 - -800.0 - -800.0 - -800.0 - -10.0 - -1000.0 - -1000.0 - 0.0 - ], - ) - @test all( - ubs .== [ - 200.0 - 200.0 - 200.0 - 200.0 - 200.0 - 800.0 - 800.0 - 800.0 - 800.0 - 800.0 - 10.0 - 1000.0 - 1000.0 - 1000.0 - ], - ) - - @test all(objective(cm) .== [ - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 1.0 - ]) - - @test n_coupling_constraints(cm) == 0 - @test isempty(coupling(cm)) - @test all(isempty.(coupling_bounds(cm))) + # @test issetequal( + # metabolites(cm), + # [ + # "m1#A" + # "m1#B" + # "m1#Ae" + # "m1#Be" + # "m1#X1" + # "m2#Ae" + # "m2#A" + # "m2#C" + # "m2#Ce" + # "m2#X2" + # "ENV_Ae" + # "ENV_Be" + # "ENV_Ce" + # ], + # ) + + # @test issetequal( + # genes(cm), + # [ + # "m1#g1" + # "m1#g2" + # "m1#g3" + # "m1#g4" + # "m2#g1" + # "m2#g2" + # "m2#g3" + # "m2#g4" + # ], + # ) + + # @test n_variables(cm) == 14 + # @test n_metabolites(cm) == 13 + # @test n_genes(cm) == 8 + + # @test all( + # stoichiometry(cm) .== [ + # 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + # -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 + # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 -0.8 + # 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 -1.0 0.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 + # 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 + # ], + # ) + + # lbs, ubs = bounds(cm) + # @test all( + # lbs .== [ + # -200.0 + # -200.0 + # -200.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # -800.0 + # -800.0 + # -800.0 + # -10.0 + # -1000.0 + # -1000.0 + # 0.0 + # ], + # ) + # @test all( + # ubs .== [ + # 200.0 + # 200.0 + # 200.0 + # 200.0 + # 200.0 + # 800.0 + # 800.0 + # 800.0 + # 800.0 + # 800.0 + # 10.0 + # 1000.0 + # 1000.0 + # 1000.0 + # ], + # ) + + # @test all(objective(cm) .== [ + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 0.0 + # 1.0 + # ]) + + # @test n_coupling_constraints(cm) == 0 + # @test isempty(coupling(cm)) + # @test all(isempty.(coupling_bounds(cm))) end -@testset "BalancedGrowthCommunityModel: e coli core" begin - ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) +@testset "EqualGrowthCommunityModel: e coli core" begin - add_biomass_metabolite!(ecoli, "BIOMASS_Ecoli_core_w_GAM") + ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) ecoli.reactions["EX_glc__D_e"].lower_bound = -1000 @@ -220,17 +218,17 @@ end abundance = a1, model = ecoli, exchange_reaction_ids = ex_rxns, - biomass_metabolite_id = "biomass", + biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) cm2 = CommunityMember( id = "ecoli2", abundance = a2, model = ecoli, exchange_reaction_ids = ex_rxns, - biomass_metabolite_id = "biomass", + biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) - cm = BalancedGrowthCommunityModel( + cm = EqualGrowthCommunityModel( members = [cm1, cm2], env_met_flux_bounds = Dict("glc__D_e" => (-10, 10)), ) @@ -239,39 +237,33 @@ end @test isapprox(d[cm.objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) + # test if growth rates are the same @test isapprox( d["ecoli1#BIOMASS_Ecoli_core_w_GAM"], - a1 * d[cm.objective_id], + d[cm.objective_id], atol = TEST_TOLERANCE, ) @test isapprox( d["ecoli2#BIOMASS_Ecoli_core_w_GAM"], - a2 * d[cm.objective_id], + d[cm.objective_id], atol = TEST_TOLERANCE, ) @test isapprox( d["EX_glc__D_e"], - d["ecoli1#EX_glc__D_e"] + d["ecoli2#EX_glc__D_e"], + a1 * d["ecoli1#EX_glc__D_e"] + a2 * d["ecoli2#EX_glc__D_e"], atol = TEST_TOLERANCE, ) - # test if model can be converted to another type - om = convert(ObjectModel, cm) - @test n_variables(om) == n_variables(cm) + # # test if model can be converted to another type + # om = convert(ObjectModel, cm) + # @test n_variables(om) == n_variables(cm) end -@testset "BalancedGrowthCommunityModel: enzyme constrained e coli" begin +@testset "EqualGrowthCommunityModel: enzyme constrained e coli" begin ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) - # test added biomass metabolite - modded_ecoli = ecoli |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") - @test "biomass" in metabolites(modded_ecoli) - @test !("biomass" in metabolites(ecoli)) - @test haskey(modded_ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") - @test !haskey(ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") - # add molar masses to gene products for gid in genes(ecoli) ecoli.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) @@ -300,9 +292,7 @@ end end end - gm = - ecoli |> - with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") |> + gm = ecoli |> with_changed_bounds( ["EX_glc__D_e"]; lower_bounds = [-1000.0], @@ -330,7 +320,7 @@ end biomass_metabolite_id = "biomass", ) - cm = BalancedGrowthCommunityModel(members = [cm1, cm2]) + cm = EqualGrowthCommunityModel(members = [cm1, cm2]) res = flux_balance_analysis( cm, From 10bf410019c61e1a2cfba7ca98aa64d80c08d144 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 12:07:44 +0100 Subject: [PATCH 243/531] implement community model structure --- ...wthCommunityModel.jl => CommunityModel.jl} | 0 src/types/models/CommunityModel.jl | 202 ++++++++++++++++++ 2 files changed, 202 insertions(+) rename src/types/misc/{EqualGrowthCommunityModel.jl => CommunityModel.jl} (100%) create mode 100644 src/types/models/CommunityModel.jl diff --git a/src/types/misc/EqualGrowthCommunityModel.jl b/src/types/misc/CommunityModel.jl similarity index 100% rename from src/types/misc/EqualGrowthCommunityModel.jl rename to src/types/misc/CommunityModel.jl diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl new file mode 100644 index 000000000..35e56408c --- /dev/null +++ b/src/types/models/CommunityModel.jl @@ -0,0 +1,202 @@ +""" +$(TYPEDEF) + +A standardized structure used to package models that can easily be combined into +a [`CommunityModel`](@ref). + +# Fields +$(TYPEDFIELDS) + +# Assumptions +1. Exchange reactions, *all* of which are idenitified in `exchange_reaction_ids`, + have the form: `A[external] ⟷ ∅` where `A` is a metabolite. No other + exchanges are allowed. This is not checked, but only assumed. +2. There is only one biomass reaction in the model. +""" +Base.@kwdef mutable struct CommunityMember + "Name of model appended to intracellular reactions and metabolites." + id::String + "Abundance of organism." + abundance::Float64 + "Underlying model." + model::AbstractMetabolicModel + "List of all exchange reactions in model." + exchange_reaction_ids::Vector{String} + "ID of biomass reaction." + biomass_reaction_id::String +end + +""" +$(TYPEDEF) + +A basic structure representing a community model. All `members` are connected to +`environmental_exchange_reactions` if they possess the corresponding exchange +reaction internally. This structure stitches together individual models with +environmental exchange reactions, but does not add any objective. Use the +community wrappers for this. The abundance of each member in the community +weights the environmental exchange balance: +``` +env_ex_met1 = abundance_1 * ex_met1_member_1 + abundance_2 * ex_met_member_2 + ... +``` +Thus, the environmental exchange reactions will have flux units normalized to +total biomass, but the fluxes of each member in the community will be normalized +to its own biomass. + +# Fields +$(TYPEDFIELDS) + +# Assumptions +1. `environmental_exchange_reactions` is a superset of all exchange reactions + contained in each underlying member in the community. +2. Each exchanged metabolite in the underlying models, identified through the + exchange reactions, is associated with an environmental exchange reaction. + +# Implementation notes +1. All reactions have the `id` of each respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. + Consequently, exchange reactions of the original model will look like + `species1#EX_...`. All exchange environmental reactions have `EX_` as a + prefix followed by the environmental metabolite id. +2. All metabolites have the `id` of each respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The + environmental metabolites have no prefix. +3. All genes have the `id` of the respective underlying + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. +""" +Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel + "Models making up the community." + members::Vector{CommunityMember} + environmental_exchange_reactions::Vector{String} +end + +function Accessors.variables(cm::CommunityModel) + rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] + env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] + return [rxns; env_exs] +end + +function Accessors.n_variables(cm::CommunityModel) + num_model_reactions = sum(n_variables(m.model) for m in cm.members) + # assume each env metabolite gets an env exchange + num_env_metabolites = length(get_env_mets(cm)) + return num_model_reactions + num_env_metabolites +end + +function Accessors.metabolites(cm::CommunityModel) + mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] + return [mets; "ENV_" .* get_env_mets(cm)] +end + +function Accessors.n_metabolites(cm::CommunityModel) + num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) + # assume each env metabolite gets an env exchange + num_env_metabolites = length(get_env_mets(cm)) + return num_model_constraints + num_env_metabolites +end + +Accessors.genes(cm::CommunityModel) = + [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] + +Accessors.n_genes(cm::CommunityModel) = + sum(n_genes(m.model) for m in cm.members) + +Accessors.balance(cm::CommunityModel) = [ + vcat([balance(m.model) for m in cm.members]...) + spzeros(length(get_env_mets(cm))) +] + +function Accessors.stoichiometry(cm::CommunityModel) + env_met_ids = get_env_mets(cm) + + model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) + model_env = spzeros(size(model_S, 1), length(env_met_ids)) + + env_rows = hcat([env_ex_matrix(m, env_met_ids) .* m.abundance for m in cm.members]...) + + return [ + model_S model_env + env_rows -I + ] +end + +function Accessors.bounds(cm::CommunityModel) + models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) + models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) + + env_mets = get_env_mets(cm) + env_lbs = fill(-constants.default_reaction_bound, length(env_mets)) + env_ubs = fill(constants.default_reaction_bound, length(env_mets)) + + return ( + [models_lbs; env_lbs], + [models_ubs; env_ubs], + ) +end + +Accessors.objective(cm::CommunityModel) = spzeros(n_variables(cm)) + +function Accessors.coupling(cm::CommunityModel) + coups = blockdiag([coupling(m.model) for m in cm.members]...) + n = n_variables(cm) + return [coups spzeros(size(coups, 1), n - size(coups, 2))] +end + +Accessors.n_coupling_constraints(cm::CommunityModel) = + sum(n_coupling_constraints(m.model) for m in cm.members) + +function Accessors.coupling_bounds(cm::CommunityModel) + lbs = vcat([first(coupling_bounds(m.model)) for m in cm.members]...) + ubs = vcat([last(coupling_bounds(m.model)) for m in cm.members]...) + return (lbs, ubs) +end + +""" +$(TYPEDSIGNATURES) + +Returns a matrix, which when multipled by the solution of a constraints based +problem, yields the semantically meaningful fluxes that correspond to +[`reactions`](@ref). +""" +function Accessors.reaction_variables_matrix(cm::CommunityModel) + # TODO add the non-matrix form! + rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) + nr = length(get_env_mets(cm)) + blockdiag(rfs, spdiagm(fill(1, nr))) +end + +Accessors.reactions(cm::CommunityModel) = [ + vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) + ["EX_" * env_met for env_met in get_env_mets(cm)] +] + +Accessors.n_reactions(cm::CommunityModel) = + sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + +#= +This loops implements the rest of the accssors through access_community_member. +Since most of the environmental reactions are generated programmtically, they +will not have things like annotations etc. For this reason, these methods will +only work if they access something inside the community members. +=# +for (func, def) in ( + (:reaction_gene_associations, nothing), + (:reaction_subsystem, nothing), + (:reaction_stoichiometry, nothing), + (:metabolite_formula, nothing), + (:metabolite_charge, nothing), + (:metabolite_compartment, nothing), + (:reaction_annotations, Dict()), + (:metabolite_annotations, Dict()), + (:gene_annotations, Dict()), + (:reaction_notes, Dict()), + (:metabolite_notes, Dict()), + (:gene_notes, Dict()), + (:reaction_name, nothing), + (:metabolite_name, nothing), + (:gene_name, nothing), +) + @eval begin + Accessors.$func(cm::CommunityModel, id::String) = + access_community_member(cm, id, $func; default = $def) + end +end From 9904077caedfc5eb74b15575a94bc0b1965552ea Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 18:19:48 +0100 Subject: [PATCH 244/531] basic structural changes --- src/reconstruction/CommunityModel.jl | 104 +++++++++ src/reconstruction/pipes/community.jl | 24 ++ src/types/misc/CommunityModel.jl | 37 +-- src/types/models/CommunityModel.jl | 94 ++++---- src/types/models/EqualGrowthCommunityModel.jl | 217 ------------------ .../wrappers/EqualGrowthCommunityModel.jl | 89 +++++++ ...wthCommunityModel.jl => CommunityModel.jl} | 17 +- 7 files changed, 280 insertions(+), 302 deletions(-) create mode 100644 src/reconstruction/CommunityModel.jl create mode 100644 src/reconstruction/pipes/community.jl delete mode 100644 src/types/models/EqualGrowthCommunityModel.jl create mode 100644 src/types/wrappers/EqualGrowthCommunityModel.jl rename test/types/{EqualGrowthCommunityModel.jl => CommunityModel.jl} (96%) diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl new file mode 100644 index 000000000..868ed37a5 --- /dev/null +++ b/src/reconstruction/CommunityModel.jl @@ -0,0 +1,104 @@ +""" +$(TYPEDSIGNATURES) + +Change the abundances of `model` with `new_abundances` in place. +""" +function change_abundances!(model::CommunityModel, new_abundances::Vector{Float64}) + isapprox(sum(new_abundances), 1.0; atol = constants.tolerance) || + throw(ArgumentError("The abundances do not sum to 1.")) + model.abundances .= new_abundances + nothing +end + +""" +$(TYPEDSIGNATURES) + +Return a shallow copy of `model` with the abundances changed. +""" +function change_abundances(model::CommunityModel, new_abundances::Vector{Float64}) + m = copy(model) + m.abundances = copy(model.abundances) + m.abundances .= new_abundances + m +end + +""" +$(TYPEDSIGNATURES) + +Change the environmental bound (environmental exchange) reaction `rid` in +`model`. If the associated bound is `nothing`, then it is not changed. +""" +change_environmental_bound!( + model::CommunityModel, + rid::String; + lower_bound = nothing, + upper_bound = nothing, +) = change_environmental_bounds!( + model, + [rid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], +) + +""" +$(TYPEDSIGNATURES) + +Plural variant of [`change_environmental_bound!`](@ref). +""" +function change_environmental_bounds!( + model::CommunityModel, + rids::Vector{String}; + lower_bounds = fill(nothing, length(rids)), + upper_bounds = fill(nothing, length(rids)), +) + for (rid, lb, ub) in zip(rids, lower_bounds, upper_bounds) + isnothing(lb) || model.environmental_exchange_reactions[rid][2] = lb + isnothing(ub) || model.environmental_exchange_reactions[rid][3] = ub + end +end + +""" +$(TYPEDSIGNATURES) + +Return a shallow copy of `model` with the environmental bound (environmental +exchange) reaction `rid` in `model` changed. If the associated bound is +`nothing`, then it is not changed. +""" +change_environmental_bound( + model::CommunityModel, + rid::String; + lower_bound = nothing, + upper_bound = nothing, +) = change_environmental_bounds( + model, + [rid]; + lower_bounds = [lower_bound], + upper_bounds = [upper_bound], +) + +""" +$(TYPEDSIGNATURES) + +Plural variant of [`change_environmental_bound`](@ref). +""" +function change_environmental_bounds( + model::CommunityModel, + rids::Vector{String}; + lower_bounds = fill(nothing, length(rids)), + upper_bounds = fill(nothing, length(rids)), +) + m = copy(model) + m.environmental_exchange_reactions = copy(model.environmental_exchange_reactions) + for (k, (mid, _lb, _ub)) in model.environmental_exchange_reactions + _idx = indexin([k], rids) + if isnothing(_idx) + m.environmental_exchange_reactions[k] = [mid, _lb, _ub] + else + idx = first(_idx) + lb = isnothing(lower_bounds[idx]) ? _lb : lower_bounds[idx] + ub = isnothing(upper_bounds[idx]) ? _ub : upper_bounds[idx] + m.environmental_exchange_reactions[k] = [mid, lb, ub] + end + end + m +end diff --git a/src/reconstruction/pipes/community.jl b/src/reconstruction/pipes/community.jl new file mode 100644 index 000000000..fca657484 --- /dev/null +++ b/src/reconstruction/pipes/community.jl @@ -0,0 +1,24 @@ +""" +$(TYPEDSIGNATURES) + +Specifies a model variant where the abundances of community members has been +changed. Forwards arguments to [`change_abundances`](@ref). +""" +with_changed_abundances(args...; kwargs...) = m -> change_abundances(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Specifies a model variant where an environmental exchange reaction has its +bounds changed. Calls [`change_environmental_bound`](@ref) internally. +""" +with_changed_environmental_bound(args...; kwargs...) = + m -> change_environmental_bound(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Plural variant of [`with_changed_environmental_bound`](@ref) +""" +with_changed_environmental_bounds(args...; kwargs...) = + m -> change_environmental_bounds(m, args...; kwargs...) diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index 8e378ab84..8137691b4 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -1,34 +1,19 @@ """ $(TYPEDSIGNATURES) -A helper function to get the exchange metabolites in the order of the listed -exchange reactions of a [`CommunityMember`](@ref). -""" -get_exchange_mets(m::CommunityMember) = - [first(keys(reaction_stoichiometry(m.model, r))) for r in m.exchange_reaction_ids] - -""" -$(TYPEDSIGNATURES) - -A helper function to get the unique environmental metabolites. -""" -get_env_mets(cm::EqualGrowthCommunityModel) = - unique(vcat([get_exchange_mets(m) for m in cm.members]...)) - -""" -$(TYPEDSIGNATURES) - A helper function that creates an exchange/environmental variable linking matrix -for community member `m` with `abundance`. +for community member `m`. """ -function env_ex_matrix(m::CommunityMember, env_mets) - mat = spzeros(length(env_mets), size(stoichiometry(m.model), 2)) - for (env_met_idx, env_met) in enumerate(env_mets) - !(env_met in metabolites(m.model)) && continue - rex = first(indexin([env_met], get_exchange_mets(m))) - isnothing(rex) && continue - ex_ridx = first(indexin([m.exchange_reaction_ids[rex]], variables(m.model))) - mat[env_met_idx, ex_ridx] = 1.0 +function env_ex_matrix( + m::CommunityMember, + env_mets::Vector{String}, + env_rxns::Vector{String}, +) + mat = spzeros(length(env_mets), n_variables(m.model)) + for (midx, mid) in enumerate(env_mets) + mid in metabolites(m.model) || continue # does not have this exchange + ridx = first(indexin([env_rxns[midx]], variables(m.model))) # find index of exchange reaction in member + mat[midx, ridx] = 1.0 end return mat end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 35e56408c..26a7be93e 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -8,16 +8,14 @@ a [`CommunityModel`](@ref). $(TYPEDFIELDS) # Assumptions -1. Exchange reactions, *all* of which are idenitified in `exchange_reaction_ids`, - have the form: `A[external] ⟷ ∅` where `A` is a metabolite. No other - exchanges are allowed. This is not checked, but only assumed. +1. Exchange reactions, *all* of which are idenitified in + `exchange_reaction_ids`, have the form: `A[external] ⟷ ∅` where `A` is a + metabolite. No other exchanges are allowed. This is not checked, but assumed. 2. There is only one biomass reaction in the model. """ Base.@kwdef mutable struct CommunityMember "Name of model appended to intracellular reactions and metabolites." id::String - "Abundance of organism." - abundance::Float64 "Underlying model." model::AbstractMetabolicModel "List of all exchange reactions in model." @@ -29,12 +27,16 @@ end """ $(TYPEDEF) -A basic structure representing a community model. All `members` are connected to -`environmental_exchange_reactions` if they possess the corresponding exchange -reaction internally. This structure stitches together individual models with +A basic structure representing a community model. All `members` are connected +through `environmental_exchange_reactions` if they possess the corresponding +exchange reaction internally (in `member.exchange_reaction_ids`). The +`environmental_exchange_reactions` field is a dictionary with keys corresponding +to the associated metabolite, as well as the reaction lower and upper bounds. + +This model structure stitches together individual member models with environmental exchange reactions, but does not add any objective. Use the community wrappers for this. The abundance of each member in the community -weights the environmental exchange balance: +weights the environmental exchange balances: ``` env_ex_met1 = abundance_1 * ex_met1_member_1 + abundance_2 * ex_met_member_2 + ... ``` @@ -45,74 +47,77 @@ to its own biomass. # Fields $(TYPEDFIELDS) -# Assumptions -1. `environmental_exchange_reactions` is a superset of all exchange reactions - contained in each underlying member in the community. -2. Each exchanged metabolite in the underlying models, identified through the - exchange reactions, is associated with an environmental exchange reaction. - # Implementation notes 1. All reactions have the `id` of each respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. Consequently, exchange reactions of the original model will look like - `species1#EX_...`. All exchange environmental reactions have `EX_` as a - prefix followed by the environmental metabolite id. + `species1#EX_...`. 2. All metabolites have the `id` of each respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The - environmental metabolites have no prefix. + [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. 3. All genes have the `id` of the respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. +4. `environmental_exchange_reactions` is a superset of all exchange reactions + contained in each underlying member in the community. Only these reactions + get joined to each underlying model. """ Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel "Models making up the community." members::Vector{CommunityMember} - environmental_exchange_reactions::Vector{String} + "Abundances of each community member." + abundances::Vector{Float64} + "Environmental exchange reactions ids mapped to their environmental metabolite, and the reaction bounds." + environmental_exchange_reactions::Dict{String,Vector{String,Float64,Float64}} end function Accessors.variables(cm::CommunityModel) rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] - env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] + env_exs = [rid for rid in keys(cm.environmental_exchange_reactions)] return [rxns; env_exs] end function Accessors.n_variables(cm::CommunityModel) num_model_reactions = sum(n_variables(m.model) for m in cm.members) - # assume each env metabolite gets an env exchange - num_env_metabolites = length(get_env_mets(cm)) + num_env_metabolites = length(cm.environmental_exchange_reactions) return num_model_reactions + num_env_metabolites end function Accessors.metabolites(cm::CommunityModel) - mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] - return [mets; "ENV_" .* get_env_mets(cm)] + mets = + [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] + return [mets; "ENV_" .* [mid for (mid, _, _) in cm.environmental_exchange_reactions]] end function Accessors.n_metabolites(cm::CommunityModel) num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) - # assume each env metabolite gets an env exchange - num_env_metabolites = length(get_env_mets(cm)) + num_env_metabolites = length(cm.environmental_exchange_reactions) return num_model_constraints + num_env_metabolites end Accessors.genes(cm::CommunityModel) = [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] -Accessors.n_genes(cm::CommunityModel) = - sum(n_genes(m.model) for m in cm.members) +Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in cm.members) Accessors.balance(cm::CommunityModel) = [ vcat([balance(m.model) for m in cm.members]...) - spzeros(length(get_env_mets(cm))) + spzeros(length(cm.environmental_exchange_reactions)) ] function Accessors.stoichiometry(cm::CommunityModel) - env_met_ids = get_env_mets(cm) model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) model_env = spzeros(size(model_S, 1), length(env_met_ids)) - - env_rows = hcat([env_ex_matrix(m, env_met_ids) .* m.abundance for m in cm.members]...) - + + env_mets = [mid for (mid, _, _) in values(cm.environmental_exchange_reactions)] + env_rxns = keys(cm.environmental_exchange_reactions) + + env_rows = hcat( + [ + env_ex_matrix(m, env_mets, env_rxns) .* a for + (m, a) in zip(cm.members, cm.abundances) + ]..., + ) + return [ model_S model_env env_rows -I @@ -122,15 +127,11 @@ end function Accessors.bounds(cm::CommunityModel) models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) - - env_mets = get_env_mets(cm) - env_lbs = fill(-constants.default_reaction_bound, length(env_mets)) - env_ubs = fill(constants.default_reaction_bound, length(env_mets)) - - return ( - [models_lbs; env_lbs], - [models_ubs; env_ubs], - ) + + env_lbs = [lb for (_, lb, _) in values(cm.environmental_exchange_reactions)] + env_ubs = [ub for (_, _, ub) in values(cm.environmental_exchange_reactions)] + + return ([models_lbs; env_lbs], [models_ubs; env_ubs]) end Accessors.objective(cm::CommunityModel) = spzeros(n_variables(cm)) @@ -160,17 +161,18 @@ problem, yields the semantically meaningful fluxes that correspond to function Accessors.reaction_variables_matrix(cm::CommunityModel) # TODO add the non-matrix form! rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) - nr = length(get_env_mets(cm)) + nr = length(cm.environmental_exchange_reactions) blockdiag(rfs, spdiagm(fill(1, nr))) end Accessors.reactions(cm::CommunityModel) = [ vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) - ["EX_" * env_met for env_met in get_env_mets(cm)] + [rid for rid in keys(cm.environmental_exchange_reactions)] ] Accessors.n_reactions(cm::CommunityModel) = - sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + sum(n_reactions(m.model) for m in cm.members) + + length(cm.environmental_exchange_reactions) #= This loops implements the rest of the accssors through access_community_member. diff --git a/src/types/models/EqualGrowthCommunityModel.jl b/src/types/models/EqualGrowthCommunityModel.jl deleted file mode 100644 index d3a368e18..000000000 --- a/src/types/models/EqualGrowthCommunityModel.jl +++ /dev/null @@ -1,217 +0,0 @@ -""" -$(TYPEDEF) - -A standardized structure used to package models that can easily be combined into -an [`EqualGrowthCommunityModel`](@ref). - -# Fields -$(TYPEDFIELDS) - -# Assumptions -1. Exchange reactions, *all* of which are idenitified in `exchange_reaction_ids`, - have the form: `A[external] ⟷ ∅` where `A` is a metabolite. No other - exchanges are allowed. This is not checked, but only assumed. -2. There is only one biomass reaction in the model. -""" -Base.@kwdef mutable struct CommunityMember - "Name of model appended to intracellular reactions and metabolites." - id::String - "Abundance of organism." - abundance::Float64 - "Underlying model." - model::AbstractMetabolicModel - "List of all exchange reactions in model." - exchange_reaction_ids::Vector{String} - "ID of biomass reaction." - biomass_reaction_id::String -end - -""" -$(TYPEDEF) - -# Fields -$(TYPEDFIELDS) - -# Assumptions -1. Each exchanged metabolite in the underlying models, identified through the - exchange reactions, will get an environmental variable. -2. It is assumed that the same namespace is used to identify unique exchanged - metabolites. -3. The objective created by this model is the equal growth rate/balanced growth - objective. - -# Implementation notes -1. All reactions have the `id` of each respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. - Consequently, exchange reactions of the original model will look like - `species1#EX_...`. All exchange environmental reactions have `EX_` as a - prefix followed by the environmental metabolite id. -2. All metabolites have the `id` of each respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. The - environmental metabolites have no prefix. -3. All genes have the `id` of the respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -""" -Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractMetabolicModel - "Models making up the community." - members::Vector{CommunityMember} - "Name of the objective" - objective_id::String = "equal_growth_rates_biomass_function" - "Bounds enforced on the environmental exchanged metabolites." - env_met_flux_bounds::Dict{String,Tuple{Float64,Float64}} = - Dict{String,Tuple{Float64,Float64}}() -end - -function Accessors.variables(cm::EqualGrowthCommunityModel) - rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] - env_exs = ["EX_" * env_met for env_met in get_env_mets(cm)] - return [rxns; env_exs; cm.objective_id] -end - -function Accessors.n_variables(cm::EqualGrowthCommunityModel) - num_model_reactions = sum(n_variables(m.model) for m in cm.members) - # assume each env metabolite gets an env exchange - num_env_metabolites = length(get_env_mets(cm)) - return num_model_reactions + num_env_metabolites + 1 # add 1 for the community biomass -end - -function Accessors.metabolites(cm::EqualGrowthCommunityModel) - mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] - biomass_constraints = [m.id for m in cm.members] - return [mets; "ENV_" .* get_env_mets(cm); biomass_constraints] -end - -function Accessors.n_metabolites(cm::EqualGrowthCommunityModel) - num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) - # assume each env metabolite gets an env exchange - num_env_metabolites = length(get_env_mets(cm)) - return num_model_constraints + num_env_metabolites + length(cm.members) -end - -Accessors.genes(cm::EqualGrowthCommunityModel) = - [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] - -Accessors.n_genes(cm::EqualGrowthCommunityModel) = - sum(n_genes(m.model) for m in cm.members) - -Accessors.balance(cm::EqualGrowthCommunityModel) = [ - vcat([balance(m.model) for m in cm.members]...) - spzeros(length(get_env_mets(cm))) - spzeros(length(cm.members)) -] - -function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) - env_met_ids = get_env_mets(cm) - - model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) - model_env = spzeros(size(model_S, 1), length(env_met_ids)) - obj1 = spzeros(size(model_env, 1)) - - env_rows = hcat([env_ex_matrix(m, env_met_ids) .* m.abundance for m in cm.members]...) - obj2 = spzeros(size(env_rows, 1)) - - obj_rows = spzeros(length(cm.members), size(model_S, 2)) - for i in 1:length(cm.members) - m = cm.members[i] - j = first(indexin([add_community_prefix(m, m.biomass_reaction_id)], variables(cm))) - obj_rows[i, j] = 1.0 - end - env_cols = spzeros(length(cm.members), length(env_met_ids)) - obj3 = -ones(length(cm.members)) - - return [ - model_S model_env obj1 - env_rows -I obj2 - obj_rows env_cols obj3 - ] -end - -function Accessors.bounds(cm::EqualGrowthCommunityModel) - models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) - models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) - env_lbs = [ - first(get(cm.env_met_flux_bounds, met_id, -constants.default_reaction_bound)) - for met_id in get_env_mets(cm) - ] - env_ubs = [ - last(get(cm.env_met_flux_bounds, met_id, constants.default_reaction_bound)) for - met_id in get_env_mets(cm) - ] - return ( - [models_lbs; env_lbs; 0], - [models_ubs; env_ubs; constants.default_reaction_bound], - ) -end - -function Accessors.objective(cm::EqualGrowthCommunityModel) - vec = spzeros(n_variables(cm)) - vec[end] = 1.0 - return vec -end - -function Accessors.coupling(cm::EqualGrowthCommunityModel) - coups = blockdiag([coupling(m.model) for m in cm.members]...) - n = n_variables(cm) - return [coups spzeros(size(coups, 1), n - size(coups, 2))] -end - -Accessors.n_coupling_constraints(cm::EqualGrowthCommunityModel) = - sum(n_coupling_constraints(m.model) for m in cm.members) - -function Accessors.coupling_bounds(cm::EqualGrowthCommunityModel) - lbs = vcat([first(coupling_bounds(m.model)) for m in cm.members]...) - ubs = vcat([last(coupling_bounds(m.model)) for m in cm.members]...) - return (lbs, ubs) -end - -""" -$(TYPEDSIGNATURES) - -Returns a matrix, which when multipled by the solution of a constraints based -problem, yields the semantically meaningful fluxes that correspond to -[`reactions`](@ref). -""" -function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) - # TODO add the non-matrix form! - rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) - nr = length(get_env_mets(cm)) + 1 # env ex + obj - blockdiag(rfs, spdiagm(fill(1, nr))) -end - -Accessors.reactions(cm::EqualGrowthCommunityModel) = [ - vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) - ["EX_" * env_met for env_met in get_env_mets(cm)] - cm.objective_id -] - -Accessors.n_reactions(cm::EqualGrowthCommunityModel) = - sum(n_reactions(m.model) for m in cm.members) + length(get_env_mets(cm)) + 1 - -#= -This loops implements the rest of the accssors through access_community_member. -Since most of the environmental reactions are generated programmtically, they -will not have things like annotations etc. For this reason, these methods will -only work if they access something inside the community members. -=# -for (func, def) in ( - (:reaction_gene_associations, nothing), - (:reaction_subsystem, nothing), - (:reaction_stoichiometry, nothing), - (:metabolite_formula, nothing), - (:metabolite_charge, nothing), - (:metabolite_compartment, nothing), - (:reaction_annotations, Dict()), - (:metabolite_annotations, Dict()), - (:gene_annotations, Dict()), - (:reaction_notes, Dict()), - (:metabolite_notes, Dict()), - (:gene_notes, Dict()), - (:reaction_name, nothing), - (:metabolite_name, nothing), - (:gene_name, nothing), -) - @eval begin - Accessors.$func(cm::EqualGrowthCommunityModel, id::String) = - access_community_member(cm, id, $func; default = $def) - end -end diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl new file mode 100644 index 000000000..dd60ce414 --- /dev/null +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -0,0 +1,89 @@ +""" +$(TYPEDEF) + +A wrapper around [`CommunityModel`](@ref) that returns a community model where +the growth rates of all members are constrained to be equal to `objective_id`, +which is the community growth rate. The objective of the resultant model is set +to this `objective_id`. + +# Assumptions +1. No biomass metabolite exists (and none are created). + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper + inner::CommunityModel + objective_id::String = "equal_growth_rates_biomass_function" +end + +Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner + +Accessors.variables(cm::EqualGrowthCommunityModel) = [variables(cm.inner); cm.objective_id] + +Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 + +Accessors.metabolites(cm::EqualGrowthCommunityModel) = + [metabolites(cm.inner); [m.id for m in cm.inner.members]] + +Accessors.n_metabolites(cm::EqualGrowthCommunityModel) = + n_metabolites(cm.inner) + length(cm.inner.members) + +Accessors.balance(cm::EqualGrowthCommunityModel) = [ + balance(cm.inner) + spzeros(length(cm.inner.members)) +] + +function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) + + S = stoichiometry(cm.inner) + obj_col = spzeros(size(S, 1)) + + obj_links = spzeros(length(cm.inner.members), size(S, 2)) + for i = 1:length(cm.inner.members) + m = cm.inner.members[i] + j = first( + indexin([add_community_prefix(m, m.biomass_reaction_id)], variables(cm.inner)), + ) + obj_links[i, j] = 1.0 + end + + obj = -ones(length(cm.inner.members)) + + return [ + S obj_col + obj_links obj + ] +end + +function Accessors.bounds(cm::EqualGrowthCommunityModel) + lbs, ubs = bounds(cm.inner) + return ([lbs; 0], [ubs; constants.default_reaction_bound]) +end + +function Accessors.objective(cm::EqualGrowthCommunityModel) + vec = spzeros(n_variables(cm)) # overwrite objective + vec[end] = 1.0 + return vec +end + +Accessors.coupling(cm::EqualGrowthCommunityModel) = + [coupling(cm.inner) spzeros(n_coupling_constraints(cm.inner))] + +""" +$(TYPEDSIGNATURES) + +Returns a matrix, which when multipled by the solution of a constraints based +problem, yields the semantically meaningful fluxes that correspond to +[`reactions`](@ref). +""" +function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) + mtx = reaction_variables_matrix(cm.inner) + u = spzeros(1, 1) + u[1] = 1.0 + blockdiag(mtx, u) +end + +Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.objective_id] + +Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 diff --git a/test/types/EqualGrowthCommunityModel.jl b/test/types/CommunityModel.jl similarity index 96% rename from test/types/EqualGrowthCommunityModel.jl rename to test/types/CommunityModel.jl index dfce48907..331f80b36 100644 --- a/test/types/EqualGrowthCommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -3,12 +3,7 @@ m1 = ObjectModel() add_metabolites!( m1, - [ - Metabolite("A"), - Metabolite("B"), - Metabolite("Ae"), - Metabolite("Be"), - ], + [Metabolite("A"), Metabolite("B"), Metabolite("Ae"), Metabolite("Be")], ) add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) add_reactions!( @@ -25,12 +20,7 @@ m2 = ObjectModel() add_metabolites!( m2, - [ - Metabolite("Ae"), - Metabolite("A"), - Metabolite("C"), - Metabolite("Ce"), - ], + [Metabolite("Ae"), Metabolite("A"), Metabolite("C"), Metabolite("Ce")], ) add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) add_reactions!( @@ -292,7 +282,8 @@ end end end - gm = ecoli |> + gm = + ecoli |> with_changed_bounds( ["EX_glc__D_e"]; lower_bounds = [-1000.0], From da7a65d6911b2ac0d1df0fa131862da9481b42ed Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 18:29:52 +0100 Subject: [PATCH 245/531] fix issues with types and syntax --- src/reconstruction/CommunityModel.jl | 4 ++-- src/types/misc/CommunityModel.jl | 2 +- src/types/models/CommunityModel.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl index 868ed37a5..81f151ccc 100644 --- a/src/reconstruction/CommunityModel.jl +++ b/src/reconstruction/CommunityModel.jl @@ -52,8 +52,8 @@ function change_environmental_bounds!( upper_bounds = fill(nothing, length(rids)), ) for (rid, lb, ub) in zip(rids, lower_bounds, upper_bounds) - isnothing(lb) || model.environmental_exchange_reactions[rid][2] = lb - isnothing(ub) || model.environmental_exchange_reactions[rid][3] = ub + isnothing(lb) || (model.environmental_exchange_reactions[rid][2] = lb) + isnothing(ub) || (model.environmental_exchange_reactions[rid][3] = ub) end end diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index 8137691b4..d87d4f86f 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -25,7 +25,7 @@ A helper function to find the index of the appropriate model. Assumes each `id` is delimited by `#` that separates the model ID prefix and the original id. """ function access_community_member( - cm::EqualGrowthCommunityModel, + cm::CommunityModel, id::String, accessor::Function; default = nothing, diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 26a7be93e..6bf2a0bc9 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -66,7 +66,7 @@ Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel "Abundances of each community member." abundances::Vector{Float64} "Environmental exchange reactions ids mapped to their environmental metabolite, and the reaction bounds." - environmental_exchange_reactions::Dict{String,Vector{String,Float64,Float64}} + environmental_exchange_reactions::Dict{String,Vector{Any}} # TODO make concrete end function Accessors.variables(cm::CommunityModel) From 5b034b3890363513a58c2846ecc92c1610e24488 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 20:32:57 +0100 Subject: [PATCH 246/531] fix structure of CommunityModel and add tests --- src/io/show/BalancedGrowthCommunityModel.jl | 13 -- src/io/show/CommunityModel.jl | 13 ++ src/misc/{check_keys.jl => checkkeys.jl} | 15 ++ src/reconstruction/CommunityModel.jl | 28 ++- src/types/models/CommunityModel.jl | 70 ++++-- test/types/CommunityModel.jl | 241 ++++++++++---------- 6 files changed, 206 insertions(+), 174 deletions(-) delete mode 100644 src/io/show/BalancedGrowthCommunityModel.jl create mode 100644 src/io/show/CommunityModel.jl rename src/misc/{check_keys.jl => checkkeys.jl} (81%) diff --git a/src/io/show/BalancedGrowthCommunityModel.jl b/src/io/show/BalancedGrowthCommunityModel.jl deleted file mode 100644 index 2174c3e85..000000000 --- a/src/io/show/BalancedGrowthCommunityModel.jl +++ /dev/null @@ -1,13 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println( - io, - "A $(typeof(cm.model)) community member with $(n_variables(cm.model)) reactions, $(n_metabolites(cm.model)) metabolites, and abundance $(cm.abundance*100)%.", - ) -end - -function Base.show(io::Base.IO, ::MIME"text/plain", cm::EqualGrowthCommunityModel) - println( - io, - "A balanced growth community model comprised of $(length(cm.members)) underlying models.", - ) -end diff --git a/src/io/show/CommunityModel.jl b/src/io/show/CommunityModel.jl new file mode 100644 index 000000000..a6fa0164b --- /dev/null +++ b/src/io/show/CommunityModel.jl @@ -0,0 +1,13 @@ +function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) + println( + io, + "A community member with $(n_variables(cm.model)) internal variables.", + ) +end + +function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityModel) + println( + io, + "A basic community model comprised of $(length(cm.members)) underlying models.", + ) +end diff --git a/src/misc/check_keys.jl b/src/misc/checkkeys.jl similarity index 81% rename from src/misc/check_keys.jl rename to src/misc/checkkeys.jl index f73388b4d..9435d19c6 100644 --- a/src/misc/check_keys.jl +++ b/src/misc/checkkeys.jl @@ -70,3 +70,18 @@ function check_has_biomass_rxn_biomas_metabolite( throw(DomainError(biomass_metabolite_id, " not found in $biomass_rxn_id.")) nothing end + +""" +Throw a DomainError if some reaction ids `rids` are not found in the +environmental_links of `cm`. Return the indices of `rids` in the environmental +linkage vector. +""" +function check_environmental_ids(cm, rids) + env_rids = [envlink.reaction_id for envlink in cm.environmental_links] + idxs = indexin(rids, env_rids) + any(isnothing.(idxs)) || begin + missing_idxs = findall(isnothing, idxs) + throw(DomainError(rids[missing_idxs], " exchange reaction IDs not found in environmental links.")) + end + idxs +end diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl index 81f151ccc..571a60ab9 100644 --- a/src/reconstruction/CommunityModel.jl +++ b/src/reconstruction/CommunityModel.jl @@ -50,10 +50,11 @@ function change_environmental_bounds!( rids::Vector{String}; lower_bounds = fill(nothing, length(rids)), upper_bounds = fill(nothing, length(rids)), -) - for (rid, lb, ub) in zip(rids, lower_bounds, upper_bounds) - isnothing(lb) || (model.environmental_exchange_reactions[rid][2] = lb) - isnothing(ub) || (model.environmental_exchange_reactions[rid][3] = ub) +) + idxs = check_environmental_ids(cm, rids) + for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) + isnothing(lb) || (model.environmental_links[idx].lower_bound = lb) + isnothing(ub) || (model.environmental_links[idx].upper_bound = ub) end end @@ -87,18 +88,15 @@ function change_environmental_bounds( lower_bounds = fill(nothing, length(rids)), upper_bounds = fill(nothing, length(rids)), ) + idxs = check_environmental_ids(cm, rids) m = copy(model) - m.environmental_exchange_reactions = copy(model.environmental_exchange_reactions) - for (k, (mid, _lb, _ub)) in model.environmental_exchange_reactions - _idx = indexin([k], rids) - if isnothing(_idx) - m.environmental_exchange_reactions[k] = [mid, _lb, _ub] - else - idx = first(_idx) - lb = isnothing(lower_bounds[idx]) ? _lb : lower_bounds[idx] - ub = isnothing(upper_bounds[idx]) ? _ub : upper_bounds[idx] - m.environmental_exchange_reactions[k] = [mid, lb, ub] - end + m.environmental_links = copy(model.environmental_links) + for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) + m.environmental_links[idx] = copy(model.environmental_links[idx]) + m.environmental_links[idx].reaction_id = model.environmental_links[idx].reaction_id + m.environmental_links[idx].metabolite_id = model.environmental_links[idx].metabolite_id + m.environmental_links[idx].lower_bound = isnothing(lb) ? model.environmental_links[idx].lower_bound : lb + m.environmental_links[idx].upper_bound = isnothing(ub) ? model.environmental_links[idx].upper_bound : ub end m end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 6bf2a0bc9..5f2f92e86 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -27,11 +27,34 @@ end """ $(TYPEDEF) +A helper struct used to store the environmental linking information in a +[`CommunityModel`](@ref). The `reaction_id` is an environmental exchange +reaction ID, and `metabolite_id` is the associated metabolite ID. The +`lower_bound` and `upper_bound` are bounds on the flux of the reaction. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct EnvironmentalLink + "Environmental exchange reaction ID." + reaction_id::String + "Environmental metabolite ID." + metabolite_id::String + "Exchange reaction lower bound." + lower_bound::Float64 + "Environmental reaction upper bound." + upper_bound::Float64 +end + +""" +$(TYPEDEF) + A basic structure representing a community model. All `members` are connected -through `environmental_exchange_reactions` if they possess the corresponding -exchange reaction internally (in `member.exchange_reaction_ids`). The -`environmental_exchange_reactions` field is a dictionary with keys corresponding -to the associated metabolite, as well as the reaction lower and upper bounds. +through `environmental_links`, which is a vector of +[`EnvironmentalLink`](@ref)s. If a member model possesses any exchange reaction +in `environmental_links`, then it is connected to the associated environmental +exchange reaction. Only the reactions in `environmental_links` are linked, any +other boundary reaction is not constrained in the community model. This model structure stitches together individual member models with environmental exchange reactions, but does not add any objective. Use the @@ -56,40 +79,40 @@ $(TYPEDFIELDS) [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. 3. All genes have the `id` of the respective underlying [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -4. `environmental_exchange_reactions` is a superset of all exchange reactions - contained in each underlying member in the community. Only these reactions - get joined to each underlying model. +4. `environmental_links` is a superset of all exchange reactions contained in + each underlying member in the community. Only these reactions get joined to + each underlying model. """ Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel "Models making up the community." members::Vector{CommunityMember} "Abundances of each community member." abundances::Vector{Float64} - "Environmental exchange reactions ids mapped to their environmental metabolite, and the reaction bounds." - environmental_exchange_reactions::Dict{String,Vector{Any}} # TODO make concrete + "Environmental exchange to model exchange linking structure." + environmental_links::Vector{EnvironmentalLink} end function Accessors.variables(cm::CommunityModel) rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] - env_exs = [rid for rid in keys(cm.environmental_exchange_reactions)] + env_exs = [envlink.reaction_id for envlink in cm.environmental_links] return [rxns; env_exs] end function Accessors.n_variables(cm::CommunityModel) num_model_reactions = sum(n_variables(m.model) for m in cm.members) - num_env_metabolites = length(cm.environmental_exchange_reactions) + num_env_metabolites = length(cm.environmental_links) return num_model_reactions + num_env_metabolites end function Accessors.metabolites(cm::CommunityModel) mets = [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] - return [mets; "ENV_" .* [mid for (mid, _, _) in cm.environmental_exchange_reactions]] + return [mets; "ENV_" .* [envlink.metabolite_id for envlink in cm.environmental_links]] end function Accessors.n_metabolites(cm::CommunityModel) num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) - num_env_metabolites = length(cm.environmental_exchange_reactions) + num_env_metabolites = length(cm.environmental_links) return num_model_constraints + num_env_metabolites end @@ -100,16 +123,16 @@ Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in cm.members Accessors.balance(cm::CommunityModel) = [ vcat([balance(m.model) for m in cm.members]...) - spzeros(length(cm.environmental_exchange_reactions)) + spzeros(length(cm.environmental_links)) ] function Accessors.stoichiometry(cm::CommunityModel) model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) - model_env = spzeros(size(model_S, 1), length(env_met_ids)) + model_env = spzeros(size(model_S, 1), length(cm.environmental_links)) - env_mets = [mid for (mid, _, _) in values(cm.environmental_exchange_reactions)] - env_rxns = keys(cm.environmental_exchange_reactions) + env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] + env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] env_rows = hcat( [ @@ -118,9 +141,10 @@ function Accessors.stoichiometry(cm::CommunityModel) ]..., ) + env_link = spdiagm(sum(env_rows, dims=2)[:]) return [ model_S model_env - env_rows -I + env_rows -env_link ] end @@ -128,8 +152,8 @@ function Accessors.bounds(cm::CommunityModel) models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) - env_lbs = [lb for (_, lb, _) in values(cm.environmental_exchange_reactions)] - env_ubs = [ub for (_, _, ub) in values(cm.environmental_exchange_reactions)] + env_lbs = [envlink.lower_bound for envlink in cm.environmental_links] + env_ubs = [envlink.upper_bound for envlink in cm.environmental_links] return ([models_lbs; env_lbs], [models_ubs; env_ubs]) end @@ -161,18 +185,18 @@ problem, yields the semantically meaningful fluxes that correspond to function Accessors.reaction_variables_matrix(cm::CommunityModel) # TODO add the non-matrix form! rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) - nr = length(cm.environmental_exchange_reactions) + nr = length(cm.environmental_links) blockdiag(rfs, spdiagm(fill(1, nr))) end Accessors.reactions(cm::CommunityModel) = [ vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) - [rid for rid in keys(cm.environmental_exchange_reactions)] + [envlink.reaction_id for envlink in cm.environmental_links] ] Accessors.n_reactions(cm::CommunityModel) = sum(n_reactions(m.model) for m in cm.members) + - length(cm.environmental_exchange_reactions) + length(cm.environmental_links) #= This loops implements the rest of the accssors through access_community_member. diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 331f80b36..d50a9fe14 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -1,4 +1,4 @@ -@testset "EqualGrowthCommunityModel: simple model" begin +@testset "CommunityModel: structure" begin m1 = ObjectModel() add_metabolites!( @@ -36,7 +36,6 @@ cm1 = CommunityMember( id = "m1", - abundance = 0.2, model = m1, exchange_reaction_ids = ["EX_A", "EX_B"], biomass_reaction_id = "r2", @@ -45,17 +44,21 @@ cm2 = CommunityMember( id = "m2", - abundance = 0.8, model = m2, exchange_reaction_ids = ["EX_A", "EX_C"], biomass_reaction_id = "r2", ) - cm = EqualGrowthCommunityModel( + cm = CommunityModel( members = [cm1, cm2], - env_met_flux_bounds = Dict("Ae" => (-10, 10)), + abundances = [0.2, 0.8], + environmental_links = [ + EnvironmentalLink("EX_A", "Ae", -10.0, 10.0) + EnvironmentalLink("EX_B", "Be", -20.0, 20.0) + EnvironmentalLink("EX_C", "Ce", -30, 30) + ] ) - @test contains(sprint(show, MIME("text/plain"), cm), "balanced growth") + @test contains(sprint(show, MIME("text/plain"), cm), "community model") @test issetequal( variables(cm), @@ -70,126 +73,118 @@ "m2#EX_A" "m2#r1" "m2#r2" - "EX_Ae" - "EX_Be" - "EX_Ce" - "equal_growth_rates_biomass_function" + "EX_A" + "EX_C" + "EX_B" + ], + ) + + @test issetequal( + metabolites(cm), + [ + "m1#A" + "m1#B" + "m1#Ae" + "m1#Be" + "m2#Ae" + "m2#A" + "m2#C" + "m2#Ce" + "ENV_Ae" + "ENV_Be" + "ENV_Ce" + ], + ) + + @test issetequal( + genes(cm), + [ + "m1#g1" + "m1#g2" + "m1#g3" + "m1#g4" + "m2#g1" + "m2#g2" + "m2#g3" + "m2#g4" + ], + ) + + @test n_variables(cm) == 13 + @test n_metabolites(cm) == 11 + @test n_genes(cm) == 8 + + @test all( + stoichiometry(cm) .== [ + 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 -1.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.2 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 0.0 0.0 0.0 -0.8 + ], + ) + + lbs, ubs = bounds(cm) + @test all( + lbs .== [ + -1000.0 + -1000.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + -1000.0 + -1000.0 + 0.0 + -10.0 + -20.0 + -30.0 + ], + ) + @test all( + ubs .== [ + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 1000.0 + 10.0 + 20.0 + 30.0 ], ) - # @test issetequal( - # metabolites(cm), - # [ - # "m1#A" - # "m1#B" - # "m1#Ae" - # "m1#Be" - # "m1#X1" - # "m2#Ae" - # "m2#A" - # "m2#C" - # "m2#Ce" - # "m2#X2" - # "ENV_Ae" - # "ENV_Be" - # "ENV_Ce" - # ], - # ) - - # @test issetequal( - # genes(cm), - # [ - # "m1#g1" - # "m1#g2" - # "m1#g3" - # "m1#g4" - # "m2#g1" - # "m2#g2" - # "m2#g3" - # "m2#g4" - # ], - # ) - - # @test n_variables(cm) == 14 - # @test n_metabolites(cm) == 13 - # @test n_genes(cm) == 8 - - # @test all( - # stoichiometry(cm) .== [ - # 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - # -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 - # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 -0.8 - # 1.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 -1.0 0.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 - # 0.0 0.0 0.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 - # ], - # ) - - # lbs, ubs = bounds(cm) - # @test all( - # lbs .== [ - # -200.0 - # -200.0 - # -200.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # -800.0 - # -800.0 - # -800.0 - # -10.0 - # -1000.0 - # -1000.0 - # 0.0 - # ], - # ) - # @test all( - # ubs .== [ - # 200.0 - # 200.0 - # 200.0 - # 200.0 - # 200.0 - # 800.0 - # 800.0 - # 800.0 - # 800.0 - # 800.0 - # 10.0 - # 1000.0 - # 1000.0 - # 1000.0 - # ], - # ) - - # @test all(objective(cm) .== [ - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 0.0 - # 1.0 - # ]) - - # @test n_coupling_constraints(cm) == 0 - # @test isempty(coupling(cm)) - # @test all(isempty.(coupling_bounds(cm))) + @test all(objective(cm) .== [ + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + ]) + + @test n_coupling_constraints(cm) == 0 + @test isempty(coupling(cm)) + @test all(isempty.(coupling_bounds(cm))) end @testset "EqualGrowthCommunityModel: e coli core" begin From 3cbbe48e4bd3f7486b2705737c982cb96a9f5046 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 21:35:18 +0100 Subject: [PATCH 247/531] fix tests --- src/io/show/CommunityModel.jl | 5 +- src/misc/checkkeys.jl | 18 +- src/reconstruction.jl | 4 +- src/reconstruction/CommunityModel.jl | 27 ++- src/reconstruction/pipes/community.jl | 8 + src/types/misc/CommunityModel.jl | 35 ++++ src/types/models/CommunityModel.jl | 7 +- .../wrappers/EqualGrowthCommunityModel.jl | 14 +- ...wthCommunityModel.jl => CommunityModel.jl} | 0 test/types/CommunityModel.jl | 157 +++++++++++------- 10 files changed, 185 insertions(+), 90 deletions(-) rename src/utils/{EqualGrowthCommunityModel.jl => CommunityModel.jl} (100%) diff --git a/src/io/show/CommunityModel.jl b/src/io/show/CommunityModel.jl index a6fa0164b..9d8371d76 100644 --- a/src/io/show/CommunityModel.jl +++ b/src/io/show/CommunityModel.jl @@ -1,8 +1,5 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println( - io, - "A community member with $(n_variables(cm.model)) internal variables.", - ) + println(io, "A community member with $(n_variables(cm.model)) internal variables.") end function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityModel) diff --git a/src/misc/checkkeys.jl b/src/misc/checkkeys.jl index 9435d19c6..e648af10e 100644 --- a/src/misc/checkkeys.jl +++ b/src/misc/checkkeys.jl @@ -79,9 +79,23 @@ linkage vector. function check_environmental_ids(cm, rids) env_rids = [envlink.reaction_id for envlink in cm.environmental_links] idxs = indexin(rids, env_rids) - any(isnothing.(idxs)) || begin + any(isnothing.(idxs)) && begin missing_idxs = findall(isnothing, idxs) - throw(DomainError(rids[missing_idxs], " exchange reaction IDs not found in environmental links.")) + throw( + DomainError( + rids[missing_idxs], + " exchange reaction IDs not found in environmental links.", + ), + ) end idxs end + +""" +Check if `new_abundances` sums to 1. +""" +function check_abundances(new_abundances) + isapprox(sum(new_abundances), 1.0; atol = constants.tolerance) || + throw(DomainError(new_abundances, "The abundances do not sum to 1.")) + nothing +end \ No newline at end of file diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 3f3abf1bb..aff015463 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -21,7 +21,9 @@ using ..Internal: check_has_biomass_rxn_id, check_biomass_rxn_has_isozymes, check_has_virtualribosome, - check_has_biomass_rxn_biomas_metabolite + check_has_biomass_rxn_biomas_metabolite, + check_environmental_ids, + check_abundances using ..Internal.Macros using ..Log.Internal using ..Types diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl index 571a60ab9..a8a2ee858 100644 --- a/src/reconstruction/CommunityModel.jl +++ b/src/reconstruction/CommunityModel.jl @@ -4,8 +4,7 @@ $(TYPEDSIGNATURES) Change the abundances of `model` with `new_abundances` in place. """ function change_abundances!(model::CommunityModel, new_abundances::Vector{Float64}) - isapprox(sum(new_abundances), 1.0; atol = constants.tolerance) || - throw(ArgumentError("The abundances do not sum to 1.")) + check_abundances(new_abundances) model.abundances .= new_abundances nothing end @@ -16,6 +15,7 @@ $(TYPEDSIGNATURES) Return a shallow copy of `model` with the abundances changed. """ function change_abundances(model::CommunityModel, new_abundances::Vector{Float64}) + check_abundances(new_abundances) m = copy(model) m.abundances = copy(model.abundances) m.abundances .= new_abundances @@ -50,8 +50,8 @@ function change_environmental_bounds!( rids::Vector{String}; lower_bounds = fill(nothing, length(rids)), upper_bounds = fill(nothing, length(rids)), -) - idxs = check_environmental_ids(cm, rids) +) + idxs = check_environmental_ids(model, rids) for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) isnothing(lb) || (model.environmental_links[idx].lower_bound = lb) isnothing(ub) || (model.environmental_links[idx].upper_bound = ub) @@ -88,15 +88,26 @@ function change_environmental_bounds( lower_bounds = fill(nothing, length(rids)), upper_bounds = fill(nothing, length(rids)), ) - idxs = check_environmental_ids(cm, rids) + idxs = check_environmental_ids(model, rids) m = copy(model) m.environmental_links = copy(model.environmental_links) for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) m.environmental_links[idx] = copy(model.environmental_links[idx]) m.environmental_links[idx].reaction_id = model.environmental_links[idx].reaction_id - m.environmental_links[idx].metabolite_id = model.environmental_links[idx].metabolite_id - m.environmental_links[idx].lower_bound = isnothing(lb) ? model.environmental_links[idx].lower_bound : lb - m.environmental_links[idx].upper_bound = isnothing(ub) ? model.environmental_links[idx].upper_bound : ub + m.environmental_links[idx].metabolite_id = + model.environmental_links[idx].metabolite_id + m.environmental_links[idx].lower_bound = + isnothing(lb) ? model.environmental_links[idx].lower_bound : lb + m.environmental_links[idx].upper_bound = + isnothing(ub) ? model.environmental_links[idx].upper_bound : ub end m end + +""" +$(TYPEDSIGNATURES) + +Return an [`EqualGrowthCommunityModel`](@ref) wrapper around `model`, optionally +specifying the `community_objective_id`. +""" +make_EqualGrowthCommunityModel(model::CommunityModel; community_objective_id="equal_growth_rates_biomass_function") = EqualGrowthCommunityModel(model, community_objective_id) \ No newline at end of file diff --git a/src/reconstruction/pipes/community.jl b/src/reconstruction/pipes/community.jl index fca657484..ba491de6c 100644 --- a/src/reconstruction/pipes/community.jl +++ b/src/reconstruction/pipes/community.jl @@ -22,3 +22,11 @@ Plural variant of [`with_changed_environmental_bound`](@ref) """ with_changed_environmental_bounds(args...; kwargs...) = m -> change_environmental_bounds(m, args...; kwargs...) + +""" +$(TYPEDSIGNATURES) + +Species a model variant that wraps a [`CommunityModel`](@ref) into a +[`EqualGrowthCommunityModel`](@ref). Forwards the arguments to the constructor. +""" +with_equal_growth_objective(args...; kwargs...) = m -> make_EqualGrowthCommunityModel(m, args...; kwargs...) \ No newline at end of file diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index d87d4f86f..d762f6b79 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -1,6 +1,41 @@ """ $(TYPEDSIGNATURES) +Shallow copy of a [`CommunityModel`](@ref) +""" +Base.copy(m::CommunityModel) = CommunityModel(; + members = m.members, + abundances = m.abundances, + environmental_links = m.environmental_links, +) + +""" +$(TYPEDSIGNATURES) + +Shallow copy of a [`CommunityModel`](@ref) +""" +Base.copy(m::CommunityMember) = CommunityMember(; + id = m.id, + model = m.id, + exchange_reaction_ids = m.exchange_reaction_ids, + biomass_reaction_id = m.biomass_reaction_id, +) + +""" +$(TYPEDSIGNATURES) + +Shallow copy of a [`EnvironmentalLink`](@ref) +""" +Base.copy(m::EnvironmentalLink) = EnvironmentalLink(; + reaction_id = m.reaction_id, + metabolite_id = m.metabolite_id, + lower_bound = m.lower_bound, + upper_bound = m.upper_bound, +) + +""" +$(TYPEDSIGNATURES) + A helper function that creates an exchange/environmental variable linking matrix for community member `m`. """ diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 5f2f92e86..f9e122f3a 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -43,7 +43,7 @@ Base.@kwdef mutable struct EnvironmentalLink "Exchange reaction lower bound." lower_bound::Float64 "Environmental reaction upper bound." - upper_bound::Float64 + upper_bound::Float64 end """ @@ -141,7 +141,7 @@ function Accessors.stoichiometry(cm::CommunityModel) ]..., ) - env_link = spdiagm(sum(env_rows, dims=2)[:]) + env_link = spdiagm(sum(env_rows, dims = 2)[:]) return [ model_S model_env env_rows -env_link @@ -195,8 +195,7 @@ Accessors.reactions(cm::CommunityModel) = [ ] Accessors.n_reactions(cm::CommunityModel) = - sum(n_reactions(m.model) for m in cm.members) + - length(cm.environmental_links) + sum(n_reactions(m.model) for m in cm.members) + length(cm.environmental_links) #= This loops implements the rest of the accssors through access_community_member. diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index dd60ce414..570ac3c2a 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -2,11 +2,11 @@ $(TYPEDEF) A wrapper around [`CommunityModel`](@ref) that returns a community model where -the growth rates of all members are constrained to be equal to `objective_id`, -which is the community growth rate. The objective of the resultant model is set -to this `objective_id`. +the growth rates of all members are constrained to be equal to +`community_objective_id`, which is the community growth rate. The objective of +the resultant model is set to this `community_objective_id`. -# Assumptions +# Notes 1. No biomass metabolite exists (and none are created). # Fields @@ -14,12 +14,12 @@ $(TYPEDFIELDS) """ Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper inner::CommunityModel - objective_id::String = "equal_growth_rates_biomass_function" + community_objective_id::String end Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner -Accessors.variables(cm::EqualGrowthCommunityModel) = [variables(cm.inner); cm.objective_id] +Accessors.variables(cm::EqualGrowthCommunityModel) = [variables(cm.inner); cm.community_objective_id] Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 @@ -84,6 +84,6 @@ function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) blockdiag(mtx, u) end -Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.objective_id] +Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.community_objective_id] Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 diff --git a/src/utils/EqualGrowthCommunityModel.jl b/src/utils/CommunityModel.jl similarity index 100% rename from src/utils/EqualGrowthCommunityModel.jl rename to src/utils/CommunityModel.jl diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index d50a9fe14..1734690ab 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -52,11 +52,11 @@ cm = CommunityModel( members = [cm1, cm2], abundances = [0.2, 0.8], - environmental_links = [ + environmental_links = [ EnvironmentalLink("EX_A", "Ae", -10.0, 10.0) EnvironmentalLink("EX_B", "Be", -20.0, 20.0) EnvironmentalLink("EX_C", "Ce", -30, 30) - ] + ], ) @test contains(sprint(show, MIME("text/plain"), cm), "community model") @@ -116,38 +116,36 @@ @test all( stoichiometry(cm) .== [ - 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 -1.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.2 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 0.0 0.0 0.0 -0.8 + 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 + 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 -1.0 0.0 0.0 + 0.0 0.0 0.0 0.0 0.2 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 0.0 + 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 0.0 0.0 0.0 -0.8 ], ) lbs, ubs = bounds(cm) - @test all( - lbs .== [ + @test all(lbs .== [ -1000.0 -1000.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 + 0.0 + 0.0 + 0.0 + 0.0 + 0.0 -1000.0 -1000.0 - 0.0 - -10.0 - -20.0 - -30.0 - ], - ) + 0.0 + -10.0 + -20.0 + -30.0 + ]) @test all( ubs .== [ 1000.0 @@ -160,9 +158,9 @@ 1000.0 1000.0 1000.0 - 10.0 - 20.0 - 30.0 + 10.0 + 20.0 + 30.0 ], ) @@ -190,48 +188,67 @@ end @testset "EqualGrowthCommunityModel: e coli core" begin ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) - ecoli.reactions["EX_glc__D_e"].lower_bound = -1000 - ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) - - a1 = 0.2 # abundance species 1 - a2 = 0.8 # abundance species 2 - + cm1 = CommunityMember( id = "ecoli1", - abundance = a1, model = ecoli, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) cm2 = CommunityMember( id = "ecoli2", - abundance = a2, model = ecoli, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) - cm = EqualGrowthCommunityModel( + ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) + ex_mids = [first(keys(reaction_stoichiometry(ecoli, rid))) for rid in ex_rxns] + ex_lbs = [ecoli.reactions[rid].lower_bound for rid in ex_rxns] + ex_ubs = [ecoli.reactions[rid].upper_bound for rid in ex_rxns] + + a1 = 0.1 # abundance species 1 + a2 = 0.8 # abundance species 2 + + cm = CommunityModel( members = [cm1, cm2], - env_met_flux_bounds = Dict("glc__D_e" => (-10, 10)), + abundances = [a1, a2], + environmental_links = [ + EnvironmentalLink(rid, mid, lb, ub) for + (rid, mid, lb, ub) in zip(ex_rxns, ex_mids, ex_lbs, ex_ubs) + ], ) + a1 = 0.2 + change_abundances!(cm, [a1, 0.8]) + @test cm.abundances[1] == 0.2 + + @test_throws DomainError cm |> with_changed_abundances([0.1, 0.2]) + + @test_throws DomainError cm |> with_changed_environmental_bound("abc"; lower_bound = -10, upper_bound = 10) + + cm3 = cm |> with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -10, upper_bound = 10) + change_environmental_bound!(cm3, "EX_ac_e"; upper_bound = 100) - d = flux_balance_analysis(cm, Tulip.Optimizer) |> values_dict + @test cm3.environmental_links[1].upper_bound == 100 + @test cm3.environmental_links[9].lower_bound == -10 - @test isapprox(d[cm.objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) + eqcm = cm3 |> with_equal_growth_objective() + d = flux_balance_analysis(eqcm, Tulip.Optimizer) |> values_dict + + @test isapprox(d[eqcm.community_objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) # test if growth rates are the same @test isapprox( d["ecoli1#BIOMASS_Ecoli_core_w_GAM"], - d[cm.objective_id], + d[eqcm.community_objective_id], atol = TEST_TOLERANCE, ) @test isapprox( d["ecoli2#BIOMASS_Ecoli_core_w_GAM"], - d[cm.objective_id], + d[eqcm.community_objective_id], atol = TEST_TOLERANCE, ) @@ -241,9 +258,9 @@ end atol = TEST_TOLERANCE, ) - # # test if model can be converted to another type - # om = convert(ObjectModel, cm) - # @test n_variables(om) == n_variables(cm) + # test if model can be converted to another type + om = convert(ObjectModel, cm) + @test n_variables(om) == n_variables(cm) end @testset "EqualGrowthCommunityModel: enzyme constrained e coli" begin @@ -276,44 +293,56 @@ end ecoli.reactions[rid].gene_associations = nothing end end + + ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) gm = ecoli |> - with_changed_bounds( - ["EX_glc__D_e"]; - lower_bounds = [-1000.0], - upper_bounds = [0], + with_changed_bound( + "EX_glc__D_e"; + lower_bound = -1000.0, + upper_bound = 0, ) |> with_enzyme_constraints(total_gene_product_mass_bound = 100.0) - ex_rxns = find_exchange_reaction_ids(ecoli) - - a1 = 0.2 # abundance species 1 - a2 = 0.8 # abundance species 2 - cm1 = CommunityMember( id = "ecoli1", - abundance = a1, model = gm, exchange_reaction_ids = ex_rxns, - biomass_metabolite_id = "biomass", + biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) cm2 = CommunityMember( id = "ecoli2", - abundance = a2, model = gm, exchange_reaction_ids = ex_rxns, - biomass_metabolite_id = "biomass", + biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) - cm = EqualGrowthCommunityModel(members = [cm1, cm2]) + a1 = 0.2 # abundance species 1 + a2 = 0.8 # abundance species 2 + + ex_mids = [first(keys(reaction_stoichiometry(ecoli, rid))) for rid in ex_rxns] + ex_lbs = [ecoli.reactions[rid].lower_bound for rid in ex_rxns] + ex_ubs = [ecoli.reactions[rid].upper_bound for rid in ex_rxns] + + cm = CommunityModel( + members = [cm1, cm2], + abundances = [a1, a2], + environmental_links = [ + EnvironmentalLink(rid, mid, lb, ub) for + (rid, mid, lb, ub) in zip(ex_rxns, ex_mids, ex_lbs, ex_ubs) + ], + ) + + eqgr = cm |> with_changed_environmental_bound("EX_glc__D_e"; lower_bound=-1000.0) |> with_equal_growth_objective() res = flux_balance_analysis( - cm, + eqgr, Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], + ); f_d = values_dict(:reaction, res) - @test isapprox(f_d[cm.objective_id], 0.9210848582802592, atol = TEST_TOLERANCE) + + @test isapprox(f_d[eqgr.community_objective_id], 0.9210836692534606, atol = TEST_TOLERANCE) end From a097a27601c0a3530183fac35714b52ecd243969 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 21:35:35 +0100 Subject: [PATCH 248/531] format --- src/misc/checkkeys.jl | 2 +- src/reconstruction/CommunityModel.jl | 5 ++- src/reconstruction/pipes/community.jl | 3 +- .../wrappers/EqualGrowthCommunityModel.jl | 6 ++- test/types/CommunityModel.jl | 43 ++++++++++++------- 5 files changed, 39 insertions(+), 20 deletions(-) diff --git a/src/misc/checkkeys.jl b/src/misc/checkkeys.jl index e648af10e..64b260126 100644 --- a/src/misc/checkkeys.jl +++ b/src/misc/checkkeys.jl @@ -98,4 +98,4 @@ function check_abundances(new_abundances) isapprox(sum(new_abundances), 1.0; atol = constants.tolerance) || throw(DomainError(new_abundances, "The abundances do not sum to 1.")) nothing -end \ No newline at end of file +end diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl index a8a2ee858..371384edb 100644 --- a/src/reconstruction/CommunityModel.jl +++ b/src/reconstruction/CommunityModel.jl @@ -110,4 +110,7 @@ $(TYPEDSIGNATURES) Return an [`EqualGrowthCommunityModel`](@ref) wrapper around `model`, optionally specifying the `community_objective_id`. """ -make_EqualGrowthCommunityModel(model::CommunityModel; community_objective_id="equal_growth_rates_biomass_function") = EqualGrowthCommunityModel(model, community_objective_id) \ No newline at end of file +make_EqualGrowthCommunityModel( + model::CommunityModel; + community_objective_id = "equal_growth_rates_biomass_function", +) = EqualGrowthCommunityModel(model, community_objective_id) diff --git a/src/reconstruction/pipes/community.jl b/src/reconstruction/pipes/community.jl index ba491de6c..afd8d4d79 100644 --- a/src/reconstruction/pipes/community.jl +++ b/src/reconstruction/pipes/community.jl @@ -29,4 +29,5 @@ $(TYPEDSIGNATURES) Species a model variant that wraps a [`CommunityModel`](@ref) into a [`EqualGrowthCommunityModel`](@ref). Forwards the arguments to the constructor. """ -with_equal_growth_objective(args...; kwargs...) = m -> make_EqualGrowthCommunityModel(m, args...; kwargs...) \ No newline at end of file +with_equal_growth_objective(args...; kwargs...) = + m -> make_EqualGrowthCommunityModel(m, args...; kwargs...) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 570ac3c2a..a82d7cc92 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -19,7 +19,8 @@ end Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner -Accessors.variables(cm::EqualGrowthCommunityModel) = [variables(cm.inner); cm.community_objective_id] +Accessors.variables(cm::EqualGrowthCommunityModel) = + [variables(cm.inner); cm.community_objective_id] Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 @@ -84,6 +85,7 @@ function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) blockdiag(mtx, u) end -Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.community_objective_id] +Accessors.reactions(cm::EqualGrowthCommunityModel) = + [reactions(cm.inner); cm.community_objective_id] Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 1734690ab..d8a3d03ee 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -190,7 +190,7 @@ end ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) ecoli.reactions["EX_glc__D_e"].lower_bound = -1000 ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) - + cm1 = CommunityMember( id = "ecoli1", model = ecoli, @@ -223,12 +223,18 @@ end a1 = 0.2 change_abundances!(cm, [a1, 0.8]) @test cm.abundances[1] == 0.2 - + @test_throws DomainError cm |> with_changed_abundances([0.1, 0.2]) - @test_throws DomainError cm |> with_changed_environmental_bound("abc"; lower_bound = -10, upper_bound = 10) + @test_throws DomainError cm |> with_changed_environmental_bound( + "abc"; + lower_bound = -10, + upper_bound = 10, + ) - cm3 = cm |> with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -10, upper_bound = 10) + cm3 = + cm |> + with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -10, upper_bound = 10) change_environmental_bound!(cm3, "EX_ac_e"; upper_bound = 100) @test cm3.environmental_links[1].upper_bound == 100 @@ -237,7 +243,11 @@ end eqcm = cm3 |> with_equal_growth_objective() d = flux_balance_analysis(eqcm, Tulip.Optimizer) |> values_dict - @test isapprox(d[eqcm.community_objective_id], 0.8739215069521299, atol = TEST_TOLERANCE) + @test isapprox( + d[eqcm.community_objective_id], + 0.8739215069521299, + atol = TEST_TOLERANCE, + ) # test if growth rates are the same @test isapprox( @@ -293,16 +303,12 @@ end ecoli.reactions[rid].gene_associations = nothing end end - + ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) gm = ecoli |> - with_changed_bound( - "EX_glc__D_e"; - lower_bound = -1000.0, - upper_bound = 0, - ) |> + with_changed_bound("EX_glc__D_e"; lower_bound = -1000.0, upper_bound = 0) |> with_enzyme_constraints(total_gene_product_mass_bound = 100.0) cm1 = CommunityMember( @@ -333,16 +339,23 @@ end (rid, mid, lb, ub) in zip(ex_rxns, ex_mids, ex_lbs, ex_ubs) ], ) - - eqgr = cm |> with_changed_environmental_bound("EX_glc__D_e"; lower_bound=-1000.0) |> with_equal_growth_objective() + + eqgr = + cm |> + with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -1000.0) |> + with_equal_growth_objective() res = flux_balance_analysis( eqgr, Tulip.Optimizer; modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], - ); + ) f_d = values_dict(:reaction, res) - @test isapprox(f_d[eqgr.community_objective_id], 0.9210836692534606, atol = TEST_TOLERANCE) + @test isapprox( + f_d[eqgr.community_objective_id], + 0.9210836692534606, + atol = TEST_TOLERANCE, + ) end From 913d8dd684d054aaed2f4de7594ab42134b0732c Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 22:28:20 +0100 Subject: [PATCH 249/531] implement semantics on community --- src/types/accessors/AbstractMetabolicModel.jl | 10 ++++ src/types/models/CommunityModel.jl | 11 +++- .../wrappers/EqualGrowthCommunityModel.jl | 10 ++++ src/utils/CommunityModel.jl | 59 +++++++++++-------- test/types/CommunityModel.jl | 8 +++ 5 files changed, 74 insertions(+), 24 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 50295caec..9034b1568 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -155,6 +155,16 @@ negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. """ ) +@make_variable_semantics( + :environmental_reaction, + "Environmental reaction", + """ +Community models are composed of member models as well as environmental exchange +reactions. This semantic grouping represents the environmental exchange +reactions. +""" +) + """ $(TYPEDSIGNATURES) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index f9e122f3a..e2583a6c0 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -183,7 +183,6 @@ problem, yields the semantically meaningful fluxes that correspond to [`reactions`](@ref). """ function Accessors.reaction_variables_matrix(cm::CommunityModel) - # TODO add the non-matrix form! rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) nr = length(cm.environmental_links) blockdiag(rfs, spdiagm(fill(1, nr))) @@ -197,6 +196,16 @@ Accessors.reactions(cm::CommunityModel) = [ Accessors.n_reactions(cm::CommunityModel) = sum(n_reactions(m.model) for m in cm.members) + length(cm.environmental_links) +""" +$(TYPEDSIGNATURES) + +Environmental reaction mapping to model variables. +""" +Accessors.environmental_reaction_variables(model::CommunityModel) = Dict( + rid => Dict(rid => 1.0) for + rid in [envlink.reaction_id for envlink in model.environmental_links] +) + #= This loops implements the rest of the accssors through access_community_member. Since most of the environmental reactions are generated programmtically, they diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index a82d7cc92..c504841cb 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -89,3 +89,13 @@ Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.community_objective_id] Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 + +""" +$(TYPEDSIGNATURES) + +Environmental reaction mapping to model variables. +""" +Accessors.environmental_reaction_variables(model::EqualGrowthCommunityModel) = Dict( + rid => Dict(rid => 1.0) for + rid in [envlink.reaction_id for envlink in model.inner.environmental_links] +) diff --git a/src/utils/CommunityModel.jl b/src/utils/CommunityModel.jl index 3e731d8bf..a2e137c57 100644 --- a/src/utils/CommunityModel.jl +++ b/src/utils/CommunityModel.jl @@ -1,35 +1,48 @@ """ $(TYPEDSIGNATURES) -Extract the solution of a specific `community_member` from `opt_model`, which is -a solved optimization model built from the `community_model`. Removes the -`community_member` prefix in the string ids of the returned dictionary. +Extract the semantic solution of the variables for a specific `community_member` +from `res`. Removes the `community_member` prefix in the string ids of the +returned dictionary. """ -get_community_member_solution( - community_model::EqualGrowthCommunityModel, - opt_model, +function values_community_member_dict( + semantics::Symbol, + res::ModelWithResult{<:Model}, community_member::CommunityMember, -) = - is_solved(opt_model) ? - Dict( - string(last(split(rid, community_member.id * "#"))) => val for (rid, val) in zip( - reactions(community_model), - reaction_variables_matrix(community_model)' * value.(opt_model[:x]), - ) if startswith(rid, community_member.id * "#") - ) : nothing +) + + is_solved(res) || return nothing + + val_d = values_community_member_dict(res, community_member) + (sem_ids, _, sem_vard, _) = Accessors.Internal.semantics(semantics) + + ids = sem_ids(community_member.model) + + Dict( + id => sum(v * val_d[k] for (k, v) in sem_vard(community_member.model)[id]) for + id in ids + ) +end """ $(TYPEDSIGNATURES) -Extract the environmental exchanges from `opt_model`, which is a solved -optimization model built from the `community_model`. +Extract the solution of the variables for a specific `community_member` from +`res`. Removes the `community_member` prefix in the string ids of the returned +dictionary. """ -get_environmental_exchanges(community_model::EqualGrowthCommunityModel, opt_model) = - is_solved(opt_model) ? +function values_community_member_dict( + res::ModelWithResult{<:Model}, + community_member::CommunityMember, +) + is_solved(res) || return nothing + cm = res.model + opt_model = res.result + Dict( - rid => val for (rid, val) in zip( - reactions(community_model), - reaction_variables_matrix(community_model)' * value.(opt_model[:x]), - ) if !any(startswith(rid, cm.id * "#") for cm in community_model.members) - ) : nothing + string(last(split(vid, community_member.id * "#"))) => value(opt_model[:x][k]) for + (k, vid) in enumerate(variables(cm)) if startswith(vid, community_member.id * "#") + ) +end + diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index d8a3d03ee..2d69923f7 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -358,4 +358,12 @@ end 0.9210836692534606, atol = TEST_TOLERANCE, ) + + # test convenience operators + f_env = values_dict(:environmental_reaction, res) + @test f_env["EX_o2_e"] == f_d["EX_o2_e"] # this should(?) be non-variable + + @test values_community_member_dict(res, cm1)["EX_glc__D_e"] == f_d["ecoli1#EX_glc__D_e"] + + @test values_community_member_dict(:enzyme, res, cm2)["b4301"] == values_dict(res)["ecoli2#b4301"]*gene_product_molar_mass(cm2.model.inner, "b4301") end From ab57afafe385520f82f977f0f0ccd58d2119aebc Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 22 Feb 2023 23:46:59 +0100 Subject: [PATCH 250/531] add abundance modification --- src/analysis.jl | 2 +- src/analysis/modifications/community.jl | 48 +++++++++++++++++++++++++ src/types/misc/CommunityModel.jl | 38 +++++++++++++++++++- src/types/models/CommunityModel.jl | 12 ++----- test/types/CommunityModel.jl | 30 +++++++++++++++- 5 files changed, 117 insertions(+), 13 deletions(-) create mode 100644 src/analysis/modifications/community.jl diff --git a/src/analysis.jl b/src/analysis.jl index 5e9fbd0f4..ab64961fa 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -61,7 +61,7 @@ end @inject Analysis.Modifications begin using ...Accessors using ...Analysis - using ...Internal: constants + using ...Internal: constants, env_ex_matrix, check_abundances using ...Solver using ...Types diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl new file mode 100644 index 000000000..6c22078de --- /dev/null +++ b/src/analysis/modifications/community.jl @@ -0,0 +1,48 @@ +""" +$(TYPEDSIGNATURES) + +Change the abundances of the community model within the solver. Only +[`CommunityModel`](@ref) and [`EqualGrowthCommunityModel`](@ref) are supported +at this time. +""" +modify_abundances(new_abundances::Vector{Float64}) = + (model, opt_model) -> begin + #= + Only support these community models because the location of the + environmental balances are known. + =# + model isa CommunityModel || + model isa EqualGrowthCommunityModel || + throw( + ArgumentError( + "Only CommunityModel and EqualGrowthCommunityModel are supported at this time.", + ), + ) + + check_abundances(new_abundances) # TODO consider removing: too pedantic + + env_rows = model isa CommunityModel ? env_ex_matrix(model, new_abundances) : env_ex_matrix(model.inner, new_abundances) + env_link = spdiagm(sum(env_rows, dims = 2)[:]) + + n_vars = n_variables(model) + n_env_vars = length(model.environmental_links) + n_cons = length(opt_model[:mb]) + n_objs = model isa CommunityModel ? 0 : length(model.inner.members) + + row_offset = model isa CommunityModel ? n_cons - n_env_vars : n_cons - n_env_vars - n_objs + + # fix abundance coefficients of species exchanges + for (i, j, v) in zip(findnz(env_rows)...) + ii = i + row_offset + set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][j], v) + end + + column_offset = model isa CommunityModel ? n_vars - n_env_vars : n_vars - n_env_vars - 1 + + # fix total abundance to link exchange + for (i, j, v) in zip(findnz(env_link)...) + jj = j + column_offset + ii = i + row_offset + set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][jj], -v) + end + end diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index d762f6b79..116a729c9 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -39,7 +39,7 @@ $(TYPEDSIGNATURES) A helper function that creates an exchange/environmental variable linking matrix for community member `m`. """ -function env_ex_matrix( +function env_ex_member_matrix( m::CommunityMember, env_mets::Vector{String}, env_rxns::Vector{String}, @@ -56,6 +56,42 @@ end """ $(TYPEDSIGNATURES) +A helper function that creates the entire exchange/environmental variable +linking matrix for a community model. +""" +function env_ex_matrix(cm) + env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] + env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] + + hcat( + [ + env_ex_member_matrix(m, env_mets, env_rxns) .* a for + (m, a) in zip(cm.members, cm.abundances) + ]..., + ) +end + +""" +$(TYPEDSIGNATURES) + +Variant of [`env_ex_matrix`](@ref) that takes an explicit abundance matrix (used +in solver modifications.) +""" +function env_ex_matrix(cm, abundances) + env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] + env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] + + hcat( + [ + env_ex_member_matrix(m, env_mets, env_rxns) .* a for + (m, a) in zip(cm.members, abundances) + ]..., + ) +end + +""" +$(TYPEDSIGNATURES) + A helper function to find the index of the appropriate model. Assumes each `id` is delimited by `#` that separates the model ID prefix and the original id. """ diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index e2583a6c0..9be59f1b7 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -131,17 +131,9 @@ function Accessors.stoichiometry(cm::CommunityModel) model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) model_env = spzeros(size(model_S, 1), length(cm.environmental_links)) - env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] - env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] - - env_rows = hcat( - [ - env_ex_matrix(m, env_mets, env_rxns) .* a for - (m, a) in zip(cm.members, cm.abundances) - ]..., - ) - + env_rows = env_ex_matrix(cm) env_link = spdiagm(sum(env_rows, dims = 2)[:]) + return [ model_S model_env env_rows -env_link diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 2d69923f7..a9bff7bfc 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -183,6 +183,32 @@ @test n_coupling_constraints(cm) == 0 @test isempty(coupling(cm)) @test all(isempty.(coupling_bounds(cm))) + + # test modification for community model + res = flux_balance_analysis(cm, Tulip.Optimizer); + mb = res.result[:mb] + x = res.result[:x] + @test normalized_coefficient(mb[9], x[1]) == 0.2 + @test normalized_coefficient(mb[11], x[13]) == -0.8 + + res2 = flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.5, 0.5])]); + mb = res2.result[:mb] + x = res2.result[:x] + @test normalized_coefficient(mb[9], x[1]) == 0.5 + @test normalized_coefficient(mb[11], x[13]) == -0.5 + + @test_throws ArgumentError flux_balance_analysis(m1, Tulip.Optimizer; modifications=[modify_abundances([0.5, 0.5])]) + @test_throws DomainError flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.3, 0.5])]) + + # test modification for EqualGrowthCommunityModel + eqgr = cm |> with_equal_growth_objective() + + res3 = flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.3, 0.7])]); + mb = res3.result[:mb] + x = res3.result[:x] + @test normalized_coefficient(mb[10], x[5]) == 0.3 + @test normalized_coefficient(mb[10], x[12]) == -0.3 + end @testset "EqualGrowthCommunityModel: e coli core" begin @@ -365,5 +391,7 @@ end @test values_community_member_dict(res, cm1)["EX_glc__D_e"] == f_d["ecoli1#EX_glc__D_e"] - @test values_community_member_dict(:enzyme, res, cm2)["b4301"] == values_dict(res)["ecoli2#b4301"]*gene_product_molar_mass(cm2.model.inner, "b4301") + @test values_community_member_dict(:enzyme, res, cm2)["b4301"] == + values_dict(res)["ecoli2#b4301"] * + gene_product_molar_mass(cm2.model.inner, "b4301") end From 264da596af6fceffacb32e27c65e08ef9728196e Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 22 Feb 2023 22:49:28 +0000 Subject: [PATCH 251/531] automatic formatting triggered by @stelmo on PR #764 --- src/analysis/modifications/community.jl | 16 ++++++---- src/types/models/CommunityModel.jl | 2 +- .../wrappers/EqualGrowthCommunityModel.jl | 2 +- src/utils/CommunityModel.jl | 1 - test/types/CommunityModel.jl | 30 ++++++++++++++----- 5 files changed, 35 insertions(+), 16 deletions(-) diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl index 6c22078de..820ae449a 100644 --- a/src/analysis/modifications/community.jl +++ b/src/analysis/modifications/community.jl @@ -10,7 +10,7 @@ modify_abundances(new_abundances::Vector{Float64}) = #= Only support these community models because the location of the environmental balances are known. - =# + =# model isa CommunityModel || model isa EqualGrowthCommunityModel || throw( @@ -18,10 +18,12 @@ modify_abundances(new_abundances::Vector{Float64}) = "Only CommunityModel and EqualGrowthCommunityModel are supported at this time.", ), ) - + check_abundances(new_abundances) # TODO consider removing: too pedantic - - env_rows = model isa CommunityModel ? env_ex_matrix(model, new_abundances) : env_ex_matrix(model.inner, new_abundances) + + env_rows = + model isa CommunityModel ? env_ex_matrix(model, new_abundances) : + env_ex_matrix(model.inner, new_abundances) env_link = spdiagm(sum(env_rows, dims = 2)[:]) n_vars = n_variables(model) @@ -29,7 +31,8 @@ modify_abundances(new_abundances::Vector{Float64}) = n_cons = length(opt_model[:mb]) n_objs = model isa CommunityModel ? 0 : length(model.inner.members) - row_offset = model isa CommunityModel ? n_cons - n_env_vars : n_cons - n_env_vars - n_objs + row_offset = + model isa CommunityModel ? n_cons - n_env_vars : n_cons - n_env_vars - n_objs # fix abundance coefficients of species exchanges for (i, j, v) in zip(findnz(env_rows)...) @@ -37,7 +40,8 @@ modify_abundances(new_abundances::Vector{Float64}) = set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][j], v) end - column_offset = model isa CommunityModel ? n_vars - n_env_vars : n_vars - n_env_vars - 1 + column_offset = + model isa CommunityModel ? n_vars - n_env_vars : n_vars - n_env_vars - 1 # fix total abundance to link exchange for (i, j, v) in zip(findnz(env_link)...) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 9be59f1b7..beedc3602 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -133,7 +133,7 @@ function Accessors.stoichiometry(cm::CommunityModel) env_rows = env_ex_matrix(cm) env_link = spdiagm(sum(env_rows, dims = 2)[:]) - + return [ model_S model_env env_rows -env_link diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index c504841cb..e3b882856 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -4,7 +4,7 @@ $(TYPEDEF) A wrapper around [`CommunityModel`](@ref) that returns a community model where the growth rates of all members are constrained to be equal to `community_objective_id`, which is the community growth rate. The objective of -the resultant model is set to this `community_objective_id`. +the resultant model is set to this `community_objective_id`. # Notes 1. No biomass metabolite exists (and none are created). diff --git a/src/utils/CommunityModel.jl b/src/utils/CommunityModel.jl index a2e137c57..eb4cd556d 100644 --- a/src/utils/CommunityModel.jl +++ b/src/utils/CommunityModel.jl @@ -45,4 +45,3 @@ function values_community_member_dict( (k, vid) in enumerate(variables(cm)) if startswith(vid, community_member.id * "#") ) end - diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index a9bff7bfc..d958f9aed 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -185,25 +185,41 @@ @test all(isempty.(coupling_bounds(cm))) # test modification for community model - res = flux_balance_analysis(cm, Tulip.Optimizer); + res = flux_balance_analysis(cm, Tulip.Optimizer) mb = res.result[:mb] - x = res.result[:x] + x = res.result[:x] @test normalized_coefficient(mb[9], x[1]) == 0.2 @test normalized_coefficient(mb[11], x[13]) == -0.8 - res2 = flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.5, 0.5])]); + res2 = flux_balance_analysis( + cm, + Tulip.Optimizer; + modifications = [modify_abundances([0.5, 0.5])], + ) mb = res2.result[:mb] - x = res2.result[:x] + x = res2.result[:x] @test normalized_coefficient(mb[9], x[1]) == 0.5 @test normalized_coefficient(mb[11], x[13]) == -0.5 - @test_throws ArgumentError flux_balance_analysis(m1, Tulip.Optimizer; modifications=[modify_abundances([0.5, 0.5])]) - @test_throws DomainError flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.3, 0.5])]) + @test_throws ArgumentError flux_balance_analysis( + m1, + Tulip.Optimizer; + modifications = [modify_abundances([0.5, 0.5])], + ) + @test_throws DomainError flux_balance_analysis( + cm, + Tulip.Optimizer; + modifications = [modify_abundances([0.3, 0.5])], + ) # test modification for EqualGrowthCommunityModel eqgr = cm |> with_equal_growth_objective() - res3 = flux_balance_analysis(cm, Tulip.Optimizer; modifications=[modify_abundances([0.3, 0.7])]); + res3 = flux_balance_analysis( + cm, + Tulip.Optimizer; + modifications = [modify_abundances([0.3, 0.7])], + ) mb = res3.result[:mb] x = res3.result[:x] @test normalized_coefficient(mb[10], x[5]) == 0.3 From 98bb03d0d808043fd7a2a48f06529c42ac7d42f0 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 23 Feb 2023 19:52:21 +0100 Subject: [PATCH 252/531] implement reviews --- src/reconstruction/CommunityModel.jl | 11 -- src/reconstruction/pipes/community.jl | 2 +- src/types.jl | 1 + src/types/accessors/AbstractMetabolicModel.jl | 4 +- src/types/misc/CommunityModel.jl | 54 +++++--- src/types/models/CommunityModel.jl | 126 ++++++++++++------ .../wrappers/EqualGrowthCommunityModel.jl | 52 ++++---- src/utils/CommunityModel.jl | 47 ------- test/types/CommunityModel.jl | 39 +++--- 9 files changed, 172 insertions(+), 164 deletions(-) delete mode 100644 src/utils/CommunityModel.jl diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl index 371384edb..1cc1eeabb 100644 --- a/src/reconstruction/CommunityModel.jl +++ b/src/reconstruction/CommunityModel.jl @@ -103,14 +103,3 @@ function change_environmental_bounds( end m end - -""" -$(TYPEDSIGNATURES) - -Return an [`EqualGrowthCommunityModel`](@ref) wrapper around `model`, optionally -specifying the `community_objective_id`. -""" -make_EqualGrowthCommunityModel( - model::CommunityModel; - community_objective_id = "equal_growth_rates_biomass_function", -) = EqualGrowthCommunityModel(model, community_objective_id) diff --git a/src/reconstruction/pipes/community.jl b/src/reconstruction/pipes/community.jl index afd8d4d79..4de0402df 100644 --- a/src/reconstruction/pipes/community.jl +++ b/src/reconstruction/pipes/community.jl @@ -30,4 +30,4 @@ Species a model variant that wraps a [`CommunityModel`](@ref) into a [`EqualGrowthCommunityModel`](@ref). Forwards the arguments to the constructor. """ with_equal_growth_objective(args...; kwargs...) = - m -> make_EqualGrowthCommunityModel(m, args...; kwargs...) + m -> EqualGrowthCommunityModel(args...; inner = m, kwargs...) diff --git a/src/types.jl b/src/types.jl index eea398a33..65d809b70 100644 --- a/src/types.jl +++ b/src/types.jl @@ -88,6 +88,7 @@ end using SBML using SparseArrays import PikaParser as PP + using OrderedCollections @inc_dir types misc @export_locals diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 9034b1568..6d5c240b7 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -156,8 +156,8 @@ negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. ) @make_variable_semantics( - :environmental_reaction, - "Environmental reaction", + :environmental_exchange, + "Environmental exchange reaction", """ Community models are composed of member models as well as environmental exchange reactions. This semantic grouping represents the environmental exchange diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index 116a729c9..a45784385 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -44,13 +44,17 @@ function env_ex_member_matrix( env_mets::Vector{String}, env_rxns::Vector{String}, ) - mat = spzeros(length(env_mets), n_variables(m.model)) - for (midx, mid) in enumerate(env_mets) - mid in metabolites(m.model) || continue # does not have this exchange - ridx = first(indexin([env_rxns[midx]], variables(m.model))) # find index of exchange reaction in member - mat[midx, ridx] = 1.0 - end - return mat + idxs = [ + (i, j) for + (i, j) in enumerate(indexin(env_rxns, variables(m.model))) if !isnothing(j) + ] + sparse( + first.(idxs), + last.(idxs), + ones(length(idxs)), + length(env_mets), + n_variables(m.model), + ) end """ @@ -66,7 +70,7 @@ function env_ex_matrix(cm) hcat( [ env_ex_member_matrix(m, env_mets, env_rxns) .* a for - (m, a) in zip(cm.members, cm.abundances) + (m, a) in zip(values(cm.members), cm.abundances) ]..., ) end @@ -84,7 +88,7 @@ function env_ex_matrix(cm, abundances) hcat( [ env_ex_member_matrix(m, env_mets, env_rxns) .* a for - (m, a) in zip(cm.members, abundances) + (m, a) in zip(values(cm.members), abundances) ]..., ) end @@ -97,20 +101,36 @@ is delimited by `#` that separates the model ID prefix and the original id. """ function access_community_member( cm::CommunityModel, - id::String, + delim_id::String, accessor::Function; + delim = "#", default = nothing, ) - id_split = split(id, "#") - idx = findfirst(startswith(first(id_split)), m.id for m in cm.members) - isnothing(idx) && return default # can only access inside community member - accessor(cm.members[idx].model, string(last(id_split))) -end + modelid_nameid = string.(split(delim_id, delim)) + length(modelid_nameid) == 1 && return default # accessor default + modelid = first(modelid_nameid) + nameid = last(modelid_nameid) + + accessor(cm.members[modelid].model, nameid) +end """ $(TYPEDSIGNATURES) -A helper function to add the id of the community member as a prefix to some string. +A helper function to build the `names_lookup` dictionary for a +[`CommunityModel`](@ref). """ -add_community_prefix(m::CommunityMember, str::String; delim = "#") = m.id * delim * str +function build_community_name_lookup( + members::OrderedDict{String,CommunityMember}; + delim = "#", +) + accessors = [variables, reactions, metabolites, genes] + Dict( + id => Dict( + Symbol(accessor) => + Dict(k => id * delim * k for k in accessor(member.model)) for + accessor in accessors + ) for (id, member) in members + ) +end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index beedc3602..1ab97c9f5 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -14,8 +14,6 @@ $(TYPEDFIELDS) 2. There is only one biomass reaction in the model. """ Base.@kwdef mutable struct CommunityMember - "Name of model appended to intracellular reactions and metabolites." - id::String "Underlying model." model::AbstractMetabolicModel "List of all exchange reactions in model." @@ -50,11 +48,12 @@ end $(TYPEDEF) A basic structure representing a community model. All `members` are connected -through `environmental_links`, which is a vector of -[`EnvironmentalLink`](@ref)s. If a member model possesses any exchange reaction -in `environmental_links`, then it is connected to the associated environmental -exchange reaction. Only the reactions in `environmental_links` are linked, any -other boundary reaction is not constrained in the community model. +through `environmental_links`. The `members` is an `OrderedDict` mapping the +member ID to a [`CommunityMember`](@ref). The `environmental_links` is a vector +of [`EnvironmentalLink`](@ref)s. If a member model possesses any exchange +reaction in `environmental_links`, then it is connected to the associated +environmental exchange reaction. Only the reactions in `environmental_links` are +linked, any other boundary reaction is not constrained in the community model. This model structure stitches together individual member models with environmental exchange reactions, but does not add any objective. Use the @@ -84,51 +83,59 @@ $(TYPEDFIELDS) each underlying model. """ Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel - "Models making up the community." - members::Vector{CommunityMember} + "Models making up the community (ID => model)." + members::OrderedDict{String,CommunityMember} "Abundances of each community member." abundances::Vector{Float64} "Environmental exchange to model exchange linking structure." environmental_links::Vector{EnvironmentalLink} + "A lookup table mapping: model IDs => accessor symbol => names => community names." + name_lookup::Dict{String,Dict{Symbol,Dict{String,String}}} = + build_community_name_lookup(members) end function Accessors.variables(cm::CommunityModel) - rxns = [add_community_prefix(m, rid) for m in cm.members for rid in variables(m.model)] + rxns = [ + cm.name_lookup[id][:variables][vid] for (id, m) in cm.members for + vid in variables(m.model) + ] env_exs = [envlink.reaction_id for envlink in cm.environmental_links] return [rxns; env_exs] end function Accessors.n_variables(cm::CommunityModel) - num_model_reactions = sum(n_variables(m.model) for m in cm.members) + num_model_reactions = sum(n_variables(m.model) for m in values(cm.members)) num_env_metabolites = length(cm.environmental_links) return num_model_reactions + num_env_metabolites end function Accessors.metabolites(cm::CommunityModel) - mets = - [add_community_prefix(m, mid) for m in cm.members for mid in metabolites(m.model)] + mets = [ + cm.name_lookup[id][:metabolites][mid] for (id, m) in cm.members for + mid in metabolites(m.model) + ] return [mets; "ENV_" .* [envlink.metabolite_id for envlink in cm.environmental_links]] end function Accessors.n_metabolites(cm::CommunityModel) - num_model_constraints = sum(n_metabolites(m.model) for m in cm.members) + num_model_constraints = sum(n_metabolites(m.model) for m in values(cm.members)) num_env_metabolites = length(cm.environmental_links) return num_model_constraints + num_env_metabolites end Accessors.genes(cm::CommunityModel) = - [add_community_prefix(m, gid) for m in cm.members for gid in genes(m.model)] + [cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in genes(m.model)] -Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in cm.members) +Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in values(cm.members)) Accessors.balance(cm::CommunityModel) = [ - vcat([balance(m.model) for m in cm.members]...) + vcat([balance(m.model) for m in values(cm.members)]...) spzeros(length(cm.environmental_links)) ] function Accessors.stoichiometry(cm::CommunityModel) - model_S = blockdiag([stoichiometry(m.model) for m in cm.members]...) + model_S = blockdiag([stoichiometry(m.model) for m in values(cm.members)]...) model_env = spzeros(size(model_S, 1), length(cm.environmental_links)) env_rows = env_ex_matrix(cm) @@ -141,8 +148,8 @@ function Accessors.stoichiometry(cm::CommunityModel) end function Accessors.bounds(cm::CommunityModel) - models_lbs = vcat([first(bounds(m.model)) for m in cm.members]...) - models_ubs = vcat([last(bounds(m.model)) for m in cm.members]...) + models_lbs = vcat([first(bounds(m.model)) for m in values(cm.members)]...) + models_ubs = vcat([last(bounds(m.model)) for m in values(cm.members)]...) env_lbs = [envlink.lower_bound for envlink in cm.environmental_links] env_ubs = [envlink.upper_bound for envlink in cm.environmental_links] @@ -153,53 +160,92 @@ end Accessors.objective(cm::CommunityModel) = spzeros(n_variables(cm)) function Accessors.coupling(cm::CommunityModel) - coups = blockdiag([coupling(m.model) for m in cm.members]...) + coups = blockdiag([coupling(m.model) for m in values(cm.members)]...) n = n_variables(cm) return [coups spzeros(size(coups, 1), n - size(coups, 2))] end Accessors.n_coupling_constraints(cm::CommunityModel) = - sum(n_coupling_constraints(m.model) for m in cm.members) + sum(n_coupling_constraints(m.model) for m in values(cm.members)) function Accessors.coupling_bounds(cm::CommunityModel) - lbs = vcat([first(coupling_bounds(m.model)) for m in cm.members]...) - ubs = vcat([last(coupling_bounds(m.model)) for m in cm.members]...) + lbs = vcat([first(coupling_bounds(m.model)) for m in values(cm.members)]...) + ubs = vcat([last(coupling_bounds(m.model)) for m in values(cm.members)]...) return (lbs, ubs) end -""" -$(TYPEDSIGNATURES) - -Returns a matrix, which when multipled by the solution of a constraints based -problem, yields the semantically meaningful fluxes that correspond to -[`reactions`](@ref). -""" -function Accessors.reaction_variables_matrix(cm::CommunityModel) - rfs = blockdiag([reaction_variables_matrix(m.model) for m in cm.members]...) - nr = length(cm.environmental_links) - blockdiag(rfs, spdiagm(fill(1, nr))) +function Accessors.reaction_variables(model::CommunityModel) + nlu_r(id, x) = model.name_lookup[id][:reactions][x] + nlu_v(id, x) = model.name_lookup[id][:variables][x] + r_v = Dict{String,Dict{String,Float64}}() + for (id, m) in model.members + r_v_m = reaction_variables(m.model) + for (k, v) in r_v_m + r_v[nlu_r(id, k)] = Dict(nlu_v(id, kk) => vv for (kk, vv) in v) + end + end + r_v end Accessors.reactions(cm::CommunityModel) = [ - vcat([add_community_prefix.(Ref(m), reactions(m.model)) for m in cm.members]...) + vcat( + [ + [cm.name_lookup[id][:reactions][rid] for rid in reactions(m.model)] for + (id, m) in cm.members + ]..., + ) [envlink.reaction_id for envlink in cm.environmental_links] ] Accessors.n_reactions(cm::CommunityModel) = - sum(n_reactions(m.model) for m in cm.members) + length(cm.environmental_links) + sum(n_reactions(m.model) for m in values(cm.members)) + length(cm.environmental_links) """ $(TYPEDSIGNATURES) -Environmental reaction mapping to model variables. +Environmental exchange reaction mapping to model variables. """ -Accessors.environmental_reaction_variables(model::CommunityModel) = Dict( +Accessors.environmental_exchange_variables(model::CommunityModel) = Dict( rid => Dict(rid => 1.0) for rid in [envlink.reaction_id for envlink in model.environmental_links] ) +""" +$(TYPEDSIGNATURES) + +Environmental exchange reaction mapping to model variables. +""" +function Accessors.enzyme_variables(model::CommunityModel) + nlu(id, x) = model.name_lookup[id][:genes][x] + e_v = Dict{String,Dict{String,Float64}}() + for (id, m) in model.members + e_v_m = enzyme_variables(m.model) + for (k, v) in e_v_m + e_v[nlu(id, k)] = Dict(nlu(id, kk) => vv for (kk, vv) in v) + end + end + e_v +end + +""" +$(TYPEDSIGNATURES) + +Get a mapping of enzyme groups to variables. See [`enzyme_variables`](@ref). +""" +function Accessors.enzyme_group_variables(model::CommunityModel) + nlu(id, x) = model.name_lookup[id][:genes][x] + e_g_v = Dict{String,Dict{String,Float64}}() + for (id, m) in model.members + e_g_v_m = enzyme_group_variables(m.model) + for (k, v) in e_g_v_m + e_g_v[id*"#"*k] = Dict(nlu(id, kk) => vv for (kk, vv) in v) + end + end + e_g_v +end + #= -This loops implements the rest of the accssors through access_community_member. +This loops implements the rest of the accessors through access_community_member. Since most of the environmental reactions are generated programmtically, they will not have things like annotations etc. For this reason, these methods will only work if they access something inside the community members. diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index e3b882856..b0ef79ef8 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -14,7 +14,7 @@ $(TYPEDFIELDS) """ Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper inner::CommunityModel - community_objective_id::String + community_objective_id::String = "community_biomass" end Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner @@ -40,14 +40,19 @@ function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) S = stoichiometry(cm.inner) obj_col = spzeros(size(S, 1)) - obj_links = spzeros(length(cm.inner.members), size(S, 2)) - for i = 1:length(cm.inner.members) - m = cm.inner.members[i] - j = first( - indexin([add_community_prefix(m, m.biomass_reaction_id)], variables(cm.inner)), - ) - obj_links[i, j] = 1.0 - end + biomass_ids = [ + cm.inner.name_lookup[id][:variables][m.biomass_reaction_id] for + (id, m) in cm.inner.members + ] + biomass_idxs = indexin(biomass_ids, variables(cm.inner)) + + obj_links = sparse( + 1:length(biomass_idxs), + biomass_idxs, + ones(length(biomass_idxs)), + length(cm.inner.members), + size(S, 2), + ) obj = -ones(length(cm.inner.members)) @@ -71,18 +76,11 @@ end Accessors.coupling(cm::EqualGrowthCommunityModel) = [coupling(cm.inner) spzeros(n_coupling_constraints(cm.inner))] -""" -$(TYPEDSIGNATURES) -Returns a matrix, which when multipled by the solution of a constraints based -problem, yields the semantically meaningful fluxes that correspond to -[`reactions`](@ref). -""" -function Accessors.reaction_variables_matrix(cm::EqualGrowthCommunityModel) - mtx = reaction_variables_matrix(cm.inner) - u = spzeros(1, 1) - u[1] = 1.0 - blockdiag(mtx, u) +function Accessors.reaction_variables(cm::EqualGrowthCommunityModel) + r_v = reaction_variables(cm.inner) + r_v[cm.community_objective_id] = Dict(cm.community_objective_id => 1.0) + r_v end Accessors.reactions(cm::EqualGrowthCommunityModel) = @@ -90,12 +88,10 @@ Accessors.reactions(cm::EqualGrowthCommunityModel) = Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 -""" -$(TYPEDSIGNATURES) +Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = + environmental_exchange_variables(model.inner) -Environmental reaction mapping to model variables. -""" -Accessors.environmental_reaction_variables(model::EqualGrowthCommunityModel) = Dict( - rid => Dict(rid => 1.0) for - rid in [envlink.reaction_id for envlink in model.inner.environmental_links] -) +Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) + +Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = + enzyme_group_variables(model.inner) diff --git a/src/utils/CommunityModel.jl b/src/utils/CommunityModel.jl deleted file mode 100644 index eb4cd556d..000000000 --- a/src/utils/CommunityModel.jl +++ /dev/null @@ -1,47 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Extract the semantic solution of the variables for a specific `community_member` -from `res`. Removes the `community_member` prefix in the string ids of the -returned dictionary. -""" -function values_community_member_dict( - semantics::Symbol, - res::ModelWithResult{<:Model}, - community_member::CommunityMember, -) - - is_solved(res) || return nothing - - val_d = values_community_member_dict(res, community_member) - - (sem_ids, _, sem_vard, _) = Accessors.Internal.semantics(semantics) - - ids = sem_ids(community_member.model) - - Dict( - id => sum(v * val_d[k] for (k, v) in sem_vard(community_member.model)[id]) for - id in ids - ) -end - -""" -$(TYPEDSIGNATURES) - -Extract the solution of the variables for a specific `community_member` from -`res`. Removes the `community_member` prefix in the string ids of the returned -dictionary. -""" -function values_community_member_dict( - res::ModelWithResult{<:Model}, - community_member::CommunityMember, -) - is_solved(res) || return nothing - cm = res.model - opt_model = res.result - - Dict( - string(last(split(vid, community_member.id * "#"))) => value(opt_model[:x][k]) for - (k, vid) in enumerate(variables(cm)) if startswith(vid, community_member.id * "#") - ) -end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index d958f9aed..7d1a90c48 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -35,7 +35,6 @@ ) cm1 = CommunityMember( - id = "m1", model = m1, exchange_reaction_ids = ["EX_A", "EX_B"], biomass_reaction_id = "r2", @@ -43,14 +42,13 @@ @test contains(sprint(show, MIME("text/plain"), cm1), "community member") cm2 = CommunityMember( - id = "m2", model = m2, exchange_reaction_ids = ["EX_A", "EX_C"], biomass_reaction_id = "r2", ) cm = CommunityModel( - members = [cm1, cm2], + members = OrderedDict("m1" => cm1, "m2" => cm2), abundances = [0.2, 0.8], environmental_links = [ EnvironmentalLink("EX_A", "Ae", -10.0, 10.0) @@ -224,7 +222,6 @@ x = res3.result[:x] @test normalized_coefficient(mb[10], x[5]) == 0.3 @test normalized_coefficient(mb[10], x[12]) == -0.3 - end @testset "EqualGrowthCommunityModel: e coli core" begin @@ -234,13 +231,11 @@ end ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) cm1 = CommunityMember( - id = "ecoli1", model = ecoli, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) cm2 = CommunityMember( - id = "ecoli2", model = ecoli, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", @@ -255,7 +250,7 @@ end a2 = 0.8 # abundance species 2 cm = CommunityModel( - members = [cm1, cm2], + members = OrderedDict("ecoli1" => cm1, "ecoli2" => cm2), abundances = [a1, a2], environmental_links = [ EnvironmentalLink(rid, mid, lb, ub) for @@ -354,13 +349,11 @@ end with_enzyme_constraints(total_gene_product_mass_bound = 100.0) cm1 = CommunityMember( - id = "ecoli1", model = gm, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", ) cm2 = CommunityMember( - id = "ecoli2", model = gm, exchange_reaction_ids = ex_rxns, biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", @@ -374,7 +367,7 @@ end ex_ubs = [ecoli.reactions[rid].upper_bound for rid in ex_rxns] cm = CommunityModel( - members = [cm1, cm2], + members = OrderedDict("ecoli1" => cm1, "ecoli2" => cm2), abundances = [a1, a2], environmental_links = [ EnvironmentalLink(rid, mid, lb, ub) for @@ -393,21 +386,31 @@ end modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], ) - f_d = values_dict(:reaction, res) + f_a = values_dict(res) + @test length(f_a) == 665 + f_r = values_dict(:reaction, res) @test isapprox( - f_d[eqgr.community_objective_id], + f_r[eqgr.community_objective_id], 0.9210836692534606, atol = TEST_TOLERANCE, ) # test convenience operators - f_env = values_dict(:environmental_reaction, res) - @test f_env["EX_o2_e"] == f_d["EX_o2_e"] # this should(?) be non-variable + f_env = values_dict(:environmental_exchange, res) + @test isapprox( + f_env["EX_o2_e"], + a1 * f_d["ecoli1#EX_o2_e"] + a2 * f_d["ecoli2#EX_o2_e"]; + atol = TEST_TOLERANCE, + ) - @test values_community_member_dict(res, cm1)["EX_glc__D_e"] == f_d["ecoli1#EX_glc__D_e"] + f_e = values_dict(:enzyme, res) + @test isapprox( + sum(v for (k, v) in f_e if startswith(k, "ecoli1")), + 100.0; + atol = TEST_TOLERANCE, + ) - @test values_community_member_dict(:enzyme, res, cm2)["b4301"] == - values_dict(res)["ecoli2#b4301"] * - gene_product_molar_mass(cm2.model.inner, "b4301") + f_g = values_dict(:enzyme_group, res) + @test isapprox(f_g["ecoli2#uncategorized"], 100.0; atol = TEST_TOLERANCE) end From 7bf7029ce6b2e8d3cb087c5c1e2e33ccf1b6eafe Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 23 Feb 2023 20:04:37 +0100 Subject: [PATCH 253/531] fix missing variable --- test/types/CommunityModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 7d1a90c48..9804d4787 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -400,7 +400,7 @@ end f_env = values_dict(:environmental_exchange, res) @test isapprox( f_env["EX_o2_e"], - a1 * f_d["ecoli1#EX_o2_e"] + a2 * f_d["ecoli2#EX_o2_e"]; + a1 * f_r["ecoli1#EX_o2_e"] + a2 * f_r["ecoli2#EX_o2_e"]; atol = TEST_TOLERANCE, ) From c86a6ee7c532af550f207c65b1341a306398b503 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 24 Feb 2023 11:01:55 +0100 Subject: [PATCH 254/531] implement reviews and format --- src/analysis.jl | 2 +- src/analysis/modifications/community.jl | 5 +++-- src/misc/checkkeys.jl | 2 +- src/types/misc/CommunityModel.jl | 14 +++++++------- src/types/models/CommunityModel.jl | 2 +- test/reconstruction/ObjectModel.jl | 14 +++++++++----- 6 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/analysis.jl b/src/analysis.jl index ab64961fa..846db67a0 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -61,7 +61,7 @@ end @inject Analysis.Modifications begin using ...Accessors using ...Analysis - using ...Internal: constants, env_ex_matrix, check_abundances + using ...Internal: constants, environment_exchange_stoichiometry, check_abundances using ...Solver using ...Types diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl index 820ae449a..dd4cbd476 100644 --- a/src/analysis/modifications/community.jl +++ b/src/analysis/modifications/community.jl @@ -22,8 +22,9 @@ modify_abundances(new_abundances::Vector{Float64}) = check_abundances(new_abundances) # TODO consider removing: too pedantic env_rows = - model isa CommunityModel ? env_ex_matrix(model, new_abundances) : - env_ex_matrix(model.inner, new_abundances) + model isa CommunityModel ? + environment_exchange_stoichiometry(model, new_abundances) : + environment_exchange_stoichiometry(model.inner, new_abundances) env_link = spdiagm(sum(env_rows, dims = 2)[:]) n_vars = n_variables(model) diff --git a/src/misc/checkkeys.jl b/src/misc/checkkeys.jl index 64b260126..8d992ff92 100644 --- a/src/misc/checkkeys.jl +++ b/src/misc/checkkeys.jl @@ -66,7 +66,7 @@ function check_has_biomass_rxn_biomas_metabolite( biomass_rxn_id, biomass_metabolite_id, ) - haskey(model_reactions[biomass_rxn_id], biomass_metabolite_id) || + haskey(model_reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) || throw(DomainError(biomass_metabolite_id, " not found in $biomass_rxn_id.")) nothing end diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index a45784385..6e119eeb5 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -39,7 +39,7 @@ $(TYPEDSIGNATURES) A helper function that creates an exchange/environmental variable linking matrix for community member `m`. """ -function env_ex_member_matrix( +function environment_exchange_stoichiometry( m::CommunityMember, env_mets::Vector{String}, env_rxns::Vector{String}, @@ -63,13 +63,13 @@ $(TYPEDSIGNATURES) A helper function that creates the entire exchange/environmental variable linking matrix for a community model. """ -function env_ex_matrix(cm) +function environment_exchange_stoichiometry(cm::CommunityModel) env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] hcat( [ - env_ex_member_matrix(m, env_mets, env_rxns) .* a for + environment_exchange_stoichiometry(m, env_mets, env_rxns) .* a for (m, a) in zip(values(cm.members), cm.abundances) ]..., ) @@ -78,16 +78,16 @@ end """ $(TYPEDSIGNATURES) -Variant of [`env_ex_matrix`](@ref) that takes an explicit abundance matrix (used +Variant of [`environment_exchange_stoichiometry`](@ref) that takes an explicit abundance matrix (used in solver modifications.) """ -function env_ex_matrix(cm, abundances) +function environment_exchange_stoichiometry(cm::CommunityModel, abundances) env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] hcat( [ - env_ex_member_matrix(m, env_mets, env_rxns) .* a for + environment_exchange_stoichiometry(m, env_mets, env_rxns) .* a for (m, a) in zip(values(cm.members), abundances) ]..., ) @@ -110,7 +110,7 @@ function access_community_member( length(modelid_nameid) == 1 && return default # accessor default modelid = first(modelid_nameid) - nameid = last(modelid_nameid) + nameid = modelid_nameid[2] # TODO deal with delimiters better accessor(cm.members[modelid].model, nameid) end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 1ab97c9f5..abb0fba02 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -138,7 +138,7 @@ function Accessors.stoichiometry(cm::CommunityModel) model_S = blockdiag([stoichiometry(m.model) for m in values(cm.members)]...) model_env = spzeros(size(model_S, 1), length(cm.environmental_links)) - env_rows = env_ex_matrix(cm) + env_rows = environment_exchange_stoichiometry(cm) env_link = spdiagm(sum(env_rows, dims = 2)[:]) return [ diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 92dc48caf..668d6029e 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -187,9 +187,13 @@ @test isnothing(new_model.reactions["r2"].gene_associations) # test added biomass metabolite - # new_model = model |> with_added_biomass_metabolite("BIOMASS_Ecoli_core_w_GAM") - # @test "biomass" in metabolites(modded_ecoli) - # @test !("biomass" in metabolites(ecoli)) - # @test haskey(modded_ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") - # @test !haskey(ecoli.reactions["BIOMASS_Ecoli_core_w_GAM"].metabolites, "biomass") + new_model = model |> with_added_biomass_metabolite("r2") + @test "biomass" in metabolites(new_model) + @test !("biomass" in metabolites(model)) + @test haskey(new_model.reactions["r2"].metabolites, "biomass") + @test !haskey(model.reactions["r2"].metabolites, "biomass") + + new_model2 = new_model |> with_removed_biomass_metabolite("r2") + @test !("biomass" in metabolites(new_model2)) + @test !haskey(new_model2.reactions["r2"].metabolites, "biomass") end From b958956a6569399b4525bf3767ead9cfd795811b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 24 Feb 2023 11:21:32 +0100 Subject: [PATCH 255/531] add basic show for ModelWithResult --- src/io/show/ModelWithResult.jl | 5 +++++ test/types/ModelWithResult.jl | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 src/io/show/ModelWithResult.jl create mode 100644 test/types/ModelWithResult.jl diff --git a/src/io/show/ModelWithResult.jl b/src/io/show/ModelWithResult.jl new file mode 100644 index 000000000..d5c5092d4 --- /dev/null +++ b/src/io/show/ModelWithResult.jl @@ -0,0 +1,5 @@ +function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult) + println(io, "A ModelWithResult composed of:") + println(io, "model: $(typeof(res.model))") + println(io, "result: $(typeof(res.result))") +end diff --git a/test/types/ModelWithResult.jl b/test/types/ModelWithResult.jl new file mode 100644 index 000000000..44df13df5 --- /dev/null +++ b/test/types/ModelWithResult.jl @@ -0,0 +1,5 @@ +@testset "ModelWithResult" begin + model = load_model(ObjectModel, model_paths["e_coli_core.json"]) + res = flux_balance_analysis(model, Tulip.Optimizer) + @test contains(sprint(show, MIME("text/plain"), res), "ModelWithResult composed of") +end From 63742843c2025207b38ff9e06885ae9512fb4807 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 24 Feb 2023 11:38:58 +0100 Subject: [PATCH 256/531] Update src/io/show/ModelWithResult.jl Co-authored-by: Mirek Kratochvil --- src/io/show/ModelWithResult.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/io/show/ModelWithResult.jl b/src/io/show/ModelWithResult.jl index d5c5092d4..842fd6711 100644 --- a/src/io/show/ModelWithResult.jl +++ b/src/io/show/ModelWithResult.jl @@ -1,5 +1,3 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult) - println(io, "A ModelWithResult composed of:") - println(io, "model: $(typeof(res.model))") - println(io, "result: $(typeof(res.result))") +function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult{T}) where T + println(io, "ModelWithResult{$T}($(res.model), ...)") end From c44792e4a665b2a8668dc5c09f3470f5829fcf30 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 24 Feb 2023 11:49:07 +0100 Subject: [PATCH 257/531] fix tests --- src/io/show/ModelWithResult.jl | 4 ++-- test/types/ModelWithResult.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/io/show/ModelWithResult.jl b/src/io/show/ModelWithResult.jl index 842fd6711..bf5c0cf43 100644 --- a/src/io/show/ModelWithResult.jl +++ b/src/io/show/ModelWithResult.jl @@ -1,3 +1,3 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult{T}) where T - println(io, "ModelWithResult{$T}($(res.model), ...)") +function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult{T}) where {T} + println(io, "ModelWithResult{$T}($(typeof(res.model)), ...)") end diff --git a/test/types/ModelWithResult.jl b/test/types/ModelWithResult.jl index 44df13df5..6ee2a274e 100644 --- a/test/types/ModelWithResult.jl +++ b/test/types/ModelWithResult.jl @@ -1,5 +1,5 @@ @testset "ModelWithResult" begin model = load_model(ObjectModel, model_paths["e_coli_core.json"]) res = flux_balance_analysis(model, Tulip.Optimizer) - @test contains(sprint(show, MIME("text/plain"), res), "ModelWithResult composed of") + @test contains(sprint(show, MIME("text/plain"), res), "ModelWithResult") end From 384609a69b8c936021ecff5725618a9f7f2b5447 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 27 Feb 2023 15:40:50 +0100 Subject: [PATCH 258/531] split wrappers off to a separate module --- src/COBREXA.jl | 2 + src/analysis.jl | 2 +- src/reconstruction.jl | 2 + src/reconstruction/enzyme_constrained.jl | 13 ++--- .../simplified_enzyme_constrained.jl | 8 +-- src/types.jl | 1 - src/utils.jl | 2 + src/wrappers.jl | 45 ++++++++++++++++ .../wrappers/EnzymeConstrainedModel.jl | 37 +------------- src/{types => }/wrappers/MatrixCoupling.jl | 0 .../wrappers/MaxMinDrivingForceModel.jl | 0 .../wrappers/MinimizeAdjustment.jl | 0 src/{types => }/wrappers/ParsimoniousModel.jl | 0 .../SimplifiedEnzymeConstrainedModel.jl | 18 +------ src/wrappers/bits/enzyme_constrained.jl | 51 +++++++++++++++++++ src/{types => wrappers}/misc/MatrixModel.jl | 0 .../misc/enzyme_constrained.jl | 0 .../misc/mmdf.jl} | 0 .../misc/simplified_enzyme_constrained.jl | 0 .../parsimonious_flux_balance_analysis.jl | 2 +- 20 files changed, 118 insertions(+), 65 deletions(-) create mode 100644 src/wrappers.jl rename src/{types => }/wrappers/EnzymeConstrainedModel.jl (90%) rename src/{types => }/wrappers/MatrixCoupling.jl (100%) rename src/{types => }/wrappers/MaxMinDrivingForceModel.jl (100%) rename src/{types => }/wrappers/MinimizeAdjustment.jl (100%) rename src/{types => }/wrappers/ParsimoniousModel.jl (100%) rename src/{types => }/wrappers/SimplifiedEnzymeConstrainedModel.jl (88%) create mode 100644 src/wrappers/bits/enzyme_constrained.jl rename src/{types => wrappers}/misc/MatrixModel.jl (100%) rename src/{types => wrappers}/misc/enzyme_constrained.jl (100%) rename src/{types/misc/thermodynamic.jl => wrappers/misc/mmdf.jl} (100%) rename src/{types => wrappers}/misc/simplified_enzyme_constrained.jl (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 936af8ed4..075f8c015 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -44,6 +44,7 @@ using .ModuleTools # start loading individual user-facing modules @inc types +@inc wrappers @inc io @inc solver @@ -61,6 +62,7 @@ module Everything @reexport Reconstruction Pipes @reexport Solver @reexport Types + @reexport Wrappers @reexport Utils end diff --git a/src/analysis.jl b/src/analysis.jl index 5e9fbd0f4..dfe5f8f7c 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -20,11 +20,11 @@ using ..ModuleTools @dse using ..Accessors +using ..Wrappers using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types -using ..Types: _EnzymeConstrainedReactionColumn, _SimplifiedEnzymeConstrainedColumn using Distributed using DistributedData diff --git a/src/reconstruction.jl b/src/reconstruction.jl index 3f3abf1bb..2648bc89e 100644 --- a/src/reconstruction.jl +++ b/src/reconstruction.jl @@ -13,6 +13,7 @@ using ..ModuleTools @dse using ..Accessors +using ..Wrappers using ..Internal: constants, check_arg_keys_exists, @@ -52,6 +53,7 @@ end @inject Reconstruction.Pipes begin using ..Reconstruction using ..Types + using ..Wrappers @inc_dir reconstruction pipes @export_locals diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 15abb76cb..ee35d3487 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -1,3 +1,4 @@ +# TODO move this to wrappers """ $(TYPEDSIGNATURES) @@ -55,7 +56,7 @@ function make_enzyme_constrained_model( gpmm_(gid) = gene_product_molar_mass(model, gid) - columns = Vector{Types._EnzymeConstrainedReactionColumn}() + columns = Vector{Wrappers.Internal.EnzymeConstrainedReactionColumn}() coupling_row_reaction = Int[] coupling_row_gene_product = Int[] @@ -72,7 +73,7 @@ function make_enzyme_constrained_model( if isnothing(isozymes) push!( columns, - Types._EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], []), + Wrappers.Internal.EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], []), ) continue end @@ -119,7 +120,7 @@ function make_enzyme_constrained_model( # make a new column push!( columns, - Types._EnzymeConstrainedReactionColumn( + Wrappers.Internal.EnzymeConstrainedReactionColumn( i, iidx, dir, @@ -146,13 +147,13 @@ function make_enzyme_constrained_model( end end end - coupling_row_mass_group = Vector{Types._EnzymeConstrainedCapacity}() + coupling_row_mass_group = Vector{Wrappers.Internal.EnzymeConstrainedCapacity}() for (grp, gs) in mg_gid_lookup idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] mms = gpmm_.(gs) push!( coupling_row_mass_group, - Types._EnzymeConstrainedCapacity( + Wrappers.Internal.EnzymeConstrainedCapacity( grp, idxs, mms, @@ -163,7 +164,7 @@ function make_enzyme_constrained_model( EnzymeConstrainedModel( [ - Types.enzyme_constrained_column_reactions(columns, model)' * objective(model) + Wrappers.Internal.enzyme_constrained_column_reactions(columns, model)' * objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index cc31f3f4e..c55ee922c 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -34,7 +34,7 @@ function make_simplified_enzyme_constrained_model( argmax(isozyme -> speed_enzyme(model, isozyme), isozymes) end - columns = Vector{Types._SimplifiedEnzymeConstrainedColumn}() + columns = Vector{Wrappers.Internal.SimplifiedEnzymeConstrainedColumn}() (lbs, ubs) = bounds(model) rids = variables(model) @@ -47,7 +47,7 @@ function make_simplified_enzyme_constrained_model( # non-enzymatic reaction (or a totally ignored one) push!( columns, - Types._SimplifiedEnzymeConstrainedColumn(i, 0, lbs[i], ubs[i], 0), + Wrappers.Internal.SimplifiedEnzymeConstrainedColumn(i, 0, lbs[i], ubs[i], 0), ) continue end @@ -61,7 +61,7 @@ function make_simplified_enzyme_constrained_model( # reaction can run in reverse push!( columns, - Types._SimplifiedEnzymeConstrainedColumn( + Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( i, -1, max(-ubs[i], 0), @@ -75,7 +75,7 @@ function make_simplified_enzyme_constrained_model( # reaction can run forward push!( columns, - Types._SimplifiedEnzymeConstrainedColumn( + Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( i, 1, max(lbs[i], 0), diff --git a/src/types.jl b/src/types.jl index eea398a33..213bbc146 100644 --- a/src/types.jl +++ b/src/types.jl @@ -74,7 +74,6 @@ end # module Accessors @inc_dir types @inc_dir types models - @inc_dir types wrappers @export_locals end diff --git a/src/utils.jl b/src/utils.jl index 233a7d3e7..7db09e936 100644 --- a/src/utils.jl +++ b/src/utils.jl @@ -16,6 +16,7 @@ using ..ModuleTools using ..Types using ..Accessors +using ..Wrappers using ..Internal.Identifiers using ..Internal: constants using ..Internal.Macros @@ -36,3 +37,4 @@ end @inject Types import ...Utils @inject Analysis using ...Utils: objective_bounds @inject Analysis.Modifications using ...Utils: is_boundary +@inject Wrappers using ..Utils diff --git a/src/wrappers.jl b/src/wrappers.jl new file mode 100644 index 000000000..beed94f45 --- /dev/null +++ b/src/wrappers.jl @@ -0,0 +1,45 @@ + +""" + module Wrappers + +All "layered" modifications of the models and their types are grouped in this module. + +# Exports +$(EXPORTS) +""" +module Wrappers +using ..ModuleTools +@dse + +using ..Types +using ..Accessors +using ..Internal.Macros +using ..Internal: constants + +using LinearAlgebra +using SparseArrays + +module Internal + using ..ModuleTools + @dse + using ..Wrappers + using ..Types + using ..Accessors + + using SparseArrays + + @inc_dir wrappers bits + @export_locals +end # module Internal + +using .Internal + +@inc_dir wrappers + +@export_locals +end # module Wrappers + +@inject Wrappers.Internal begin + @inc_dir wrappers misc + @export_locals # again +end diff --git a/src/types/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl similarity index 90% rename from src/types/wrappers/EnzymeConstrainedModel.jl rename to src/wrappers/EnzymeConstrainedModel.jl index 6dba7e3b8..eeb7c6134 100644 --- a/src/types/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -1,36 +1,3 @@ -""" -$(TYPEDEF) - -A helper type for describing the contents of [`EnzymeConstrainedModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct _EnzymeConstrainedReactionColumn - reaction_idx::Int - isozyme_idx::Int - direction::Int - reaction_coupling_row::Int - lb::Float64 - ub::Float64 - gene_product_coupling::Vector{Tuple{Int,Float64}} -end - -""" -$(TYPEDEF) - -A helper struct that contains the gene product capacity terms organized by -the grouping type, e.g. metabolic or membrane groups etc. - -# Fields -$(TYPEDFIELDS) -""" -struct _EnzymeConstrainedCapacity - group_id::String - gene_product_idxs::Vector{Int} - gene_product_molar_masses::Vector{Float64} - group_upper_bound::Float64 -end """ $(TYPEDEF) @@ -86,10 +53,10 @@ $(TYPEDFIELDS) """ struct EnzymeConstrainedModel <: AbstractModelWrapper objective::SparseVec - columns::Vector{_EnzymeConstrainedReactionColumn} + columns::Vector{EnzymeConstrainedReactionColumn} coupling_row_reaction::Vector{Int} coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} - coupling_row_mass_group::Vector{_EnzymeConstrainedCapacity} + coupling_row_mass_group::Vector{EnzymeConstrainedCapacity} inner::AbstractMetabolicModel end diff --git a/src/types/wrappers/MatrixCoupling.jl b/src/wrappers/MatrixCoupling.jl similarity index 100% rename from src/types/wrappers/MatrixCoupling.jl rename to src/wrappers/MatrixCoupling.jl diff --git a/src/types/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl similarity index 100% rename from src/types/wrappers/MaxMinDrivingForceModel.jl rename to src/wrappers/MaxMinDrivingForceModel.jl diff --git a/src/types/wrappers/MinimizeAdjustment.jl b/src/wrappers/MinimizeAdjustment.jl similarity index 100% rename from src/types/wrappers/MinimizeAdjustment.jl rename to src/wrappers/MinimizeAdjustment.jl diff --git a/src/types/wrappers/ParsimoniousModel.jl b/src/wrappers/ParsimoniousModel.jl similarity index 100% rename from src/types/wrappers/ParsimoniousModel.jl rename to src/wrappers/ParsimoniousModel.jl diff --git a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl similarity index 88% rename from src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl rename to src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 0859f73e0..7f027f30c 100644 --- a/src/types/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -2,22 +2,6 @@ """ $(TYPEDEF) -A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct _SimplifiedEnzymeConstrainedColumn - reaction_idx::Int # number of the corresponding reaction in the inner model - direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part - lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) - ub::Float64 - capacity_required::Float64 # must be 0 for bidirectional reactions (if direction==0) -end - -""" -$(TYPEDEF) - An enzyme-capacity-constrained model using sMOMENT algorithm, as described by *Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of metabolic models with enzyme constraints" BMC bioinformatics, 2020*. @@ -64,7 +48,7 @@ enzymes, or those that should be ignored need to return `nothing` when $(TYPEDFIELDS) """ struct SimplifiedEnzymeConstrainedModel <: AbstractModelWrapper - columns::Vector{_SimplifiedEnzymeConstrainedColumn} + columns::Vector{SimplifiedEnzymeConstrainedColumn} total_gene_product_mass_bound::Float64 inner::AbstractMetabolicModel diff --git a/src/wrappers/bits/enzyme_constrained.jl b/src/wrappers/bits/enzyme_constrained.jl new file mode 100644 index 000000000..1223cfbf8 --- /dev/null +++ b/src/wrappers/bits/enzyme_constrained.jl @@ -0,0 +1,51 @@ + +""" +$(TYPEDEF) + +A helper type that describes the contents of +[`SimplifiedEnzymeConstrainedModel`](@ref)s. + +# Fields +$(TYPEDFIELDS) +""" +struct SimplifiedEnzymeConstrainedColumn + reaction_idx::Int # number of the corresponding reaction in the inner model + direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part + lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) + ub::Float64 + capacity_required::Float64 # must be 0 for bidirectional reactions (if direction==0) +end + +""" +$(TYPEDEF) + +A helper type for describing the contents of [`EnzymeConstrainedModel`](@ref)s. + +# Fields +$(TYPEDFIELDS) +""" +struct EnzymeConstrainedReactionColumn + reaction_idx::Int + isozyme_idx::Int + direction::Int + reaction_coupling_row::Int + lb::Float64 + ub::Float64 + gene_product_coupling::Vector{Tuple{Int,Float64}} +end + +""" +$(TYPEDEF) + +A helper struct that contains the gene product capacity terms organized by +the grouping type, e.g. metabolic or membrane groups etc. + +# Fields +$(TYPEDFIELDS) +""" +struct EnzymeConstrainedCapacity + group_id::String + gene_product_idxs::Vector{Int} + gene_product_molar_masses::Vector{Float64} + group_upper_bound::Float64 +end diff --git a/src/types/misc/MatrixModel.jl b/src/wrappers/misc/MatrixModel.jl similarity index 100% rename from src/types/misc/MatrixModel.jl rename to src/wrappers/misc/MatrixModel.jl diff --git a/src/types/misc/enzyme_constrained.jl b/src/wrappers/misc/enzyme_constrained.jl similarity index 100% rename from src/types/misc/enzyme_constrained.jl rename to src/wrappers/misc/enzyme_constrained.jl diff --git a/src/types/misc/thermodynamic.jl b/src/wrappers/misc/mmdf.jl similarity index 100% rename from src/types/misc/thermodynamic.jl rename to src/wrappers/misc/mmdf.jl diff --git a/src/types/misc/simplified_enzyme_constrained.jl b/src/wrappers/misc/simplified_enzyme_constrained.jl similarity index 100% rename from src/types/misc/simplified_enzyme_constrained.jl rename to src/wrappers/misc/simplified_enzyme_constrained.jl diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index a32ed5b31..8c51995ec 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -20,7 +20,7 @@ model |> with_changed_bound("biomass1", lower_bound = 10.0) |> with_parsimonious_solution(:reaction) |> - flux_balance_analysis(Clarabel.Optimizer) |> + flux_balance_analysis(Clarabel.Optimizer, modifications=[silence]) |> values_dict @test all(isapprox(d[k], d2[k], atol = QP_TEST_TOLERANCE) for k in keys(d2)) From ae70f56058ccb7ec24f131d42cfde2e4c99744bd Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 28 Feb 2023 08:43:00 +0100 Subject: [PATCH 259/531] format --- src/reconstruction/enzyme_constrained.jl | 13 +++++++++++-- src/reconstruction/simplified_enzyme_constrained.jl | 8 +++++++- test/analysis/parsimonious_flux_balance_analysis.jl | 2 +- 3 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index ee35d3487..ccad749de 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -73,7 +73,15 @@ function make_enzyme_constrained_model( if isnothing(isozymes) push!( columns, - Wrappers.Internal.EnzymeConstrainedReactionColumn(i, 0, 0, 0, lbs[i], ubs[i], []), + Wrappers.Internal.EnzymeConstrainedReactionColumn( + i, + 0, + 0, + 0, + lbs[i], + ubs[i], + [], + ), ) continue end @@ -164,7 +172,8 @@ function make_enzyme_constrained_model( EnzymeConstrainedModel( [ - Wrappers.Internal.enzyme_constrained_column_reactions(columns, model)' * objective(model) + Wrappers.Internal.enzyme_constrained_column_reactions(columns, model)' * + objective(model) spzeros(length(coupling_row_gene_product)) ], columns, diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index c55ee922c..85cc90104 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -47,7 +47,13 @@ function make_simplified_enzyme_constrained_model( # non-enzymatic reaction (or a totally ignored one) push!( columns, - Wrappers.Internal.SimplifiedEnzymeConstrainedColumn(i, 0, lbs[i], ubs[i], 0), + Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( + i, + 0, + lbs[i], + ubs[i], + 0, + ), ) continue end diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 8c51995ec..355d8a587 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -20,7 +20,7 @@ model |> with_changed_bound("biomass1", lower_bound = 10.0) |> with_parsimonious_solution(:reaction) |> - flux_balance_analysis(Clarabel.Optimizer, modifications=[silence]) |> + flux_balance_analysis(Clarabel.Optimizer, modifications = [silence]) |> values_dict @test all(isapprox(d[k], d2[k], atol = QP_TEST_TOLERANCE) for k in keys(d2)) From 6ddf5eea5f0fafa9669624da7e0e72f8f5d43ec9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Mar 2023 11:51:04 +0100 Subject: [PATCH 260/531] unify a few quadratic-distance wrappers --- src/analysis/minimize_metabolic_adjustment.jl | 3 +- .../pipes/minimize_adjustment.jl | 8 -- src/utils/quadratic_objective.jl | 10 -- src/wrappers/MinimizeAdjustment.jl | 30 ------ src/wrappers/MinimizeDistance.jl | 95 +++++++++++++++++++ 5 files changed, 97 insertions(+), 49 deletions(-) delete mode 100644 src/reconstruction/pipes/minimize_adjustment.jl delete mode 100644 src/utils/quadratic_objective.jl delete mode 100644 src/wrappers/MinimizeAdjustment.jl create mode 100644 src/wrappers/MinimizeDistance.jl diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 0ed216113..52aa16ab6 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -16,6 +16,7 @@ Metabolic Networks, Proceedings of the National Academy of Sciences, 2002" for more details. Additional arguments are passed to [`flux_balance_analysis`](@ref). +See [`minimize_solution_distance`](@ref) for implementation details. Returns an optimized model that contains the feasible flux nearest to the reference. @@ -39,7 +40,7 @@ minimize_metabolic_adjustment_analysis( optimizer; kwargs..., ) = flux_balance_analysis( - model |> Reconstruction.Pipes.minimize_adjustment(reference_flux), + model |> minimize_solution_distance(reference_flux), optimizer; kwargs..., ) diff --git a/src/reconstruction/pipes/minimize_adjustment.jl b/src/reconstruction/pipes/minimize_adjustment.jl deleted file mode 100644 index d1a6128bb..000000000 --- a/src/reconstruction/pipes/minimize_adjustment.jl +++ /dev/null @@ -1,8 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Pipe-able version of the [`MinimizeAdjustmentModel`](@ref) wrapper. -""" -minimize_adjustment(reference_flux::Vector{Float64}) = - model -> MinimizeAdjustmentModel(model, reference_flux) diff --git a/src/utils/quadratic_objective.jl b/src/utils/quadratic_objective.jl deleted file mode 100644 index 81af9ef36..000000000 --- a/src/utils/quadratic_objective.jl +++ /dev/null @@ -1,10 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Produce a matrix for [`objective`](@ref) so that the model minimizes the -squared distance from the `center` variable assignment. Length of `center` -should be the same as number of variables in the model. -""" -negative_squared_distance_objective(center::Vector{Float64}) = - [spdiagm(fill(-0.5, length(center))) center] diff --git a/src/wrappers/MinimizeAdjustment.jl b/src/wrappers/MinimizeAdjustment.jl deleted file mode 100644 index 01c9661c6..000000000 --- a/src/wrappers/MinimizeAdjustment.jl +++ /dev/null @@ -1,30 +0,0 @@ - -""" -$(TYPEDEF) - -A wrapper that adds a quadratic objective that minimizes total squared error -("adjustment") from a certain reference variable assignment ("reference flux"). - -Length of `reference_assignment` must be the same as the the number of -variables in the inner model. - -This is used to implement [`minimize_metabolic_adjustment`](@ref). - -# Example -``` -m = load_model("e_coli_core.xml") -adjusted_model = m |> MinimizeAdjustmentModel(fill(0.1, n_variables(m))) -``` -""" -struct MinimizeAdjustmentModel <: AbstractModelWrapper - inner::AbstractMetabolicModel - reference_assignment::Vector{Float64} -end -#TODO: sparse MinimizeAdjustmentModel with dict/sparseVec -#TODO: MinimizeReactionAdjustmentModel? -#TODO: MinimizeEnzymeAdjustmentModel? - -Accessors.unwrap_model(m::MinimizeAdjustmentModel) = m.inner - -Accessors.objective(m::MinimizeAdjustmentModel)::SparseMat = - Utils.negative_squared_distance_objective(m.reference_assignment) diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl new file mode 100644 index 000000000..891d87841 --- /dev/null +++ b/src/wrappers/MinimizeDistance.jl @@ -0,0 +1,95 @@ + +""" +$(TYPEDEF) + +A wrapper for setting an Euclidean-distance-from-a-reference-point-minimizing +objective on solution variables. +""" +struct MinimizeSolutionDistance <: AbstractModelWrapper + center::Vector{Float64} + inner::AbstractMetabolicModel +end + +Accessors.unwrap_model(m::MinimizeSolutionDistance) = m.inner + +Accessors.objective(m::MinimizeSolutionDistance) = + [spdiagm(fill(-0.5, length(m.center))) m.center] + +""" +$(TYPEDSIGNATURES) + +Set a quadratic objective that minimizes the solution distance from a selected +point. Use [`minimize_semantic_distance`](@ref) or +[`minimize_projected_distance`](@ref) for more fine-grained and weighted +variants of the same concept. Internally powered by +[`MinimizeSolutionDistance`](@ref). +""" +minimize_solution_distance(center::Vector{Float64}) = + model -> MinimizeSolutionDistance(center, model) + +""" +$(TYPEDEF) + +A wrapper that sets an objective that minimizes Euclidean distance from a given +point in a semantic. +""" +struct MinimizeSemanticDistance <: AbstractModelWrapper + semantic::Symbol + center::Vector{Float64} + inner::AbstractMetabolicModel +end + +Accessors.unwrap_model(m::MinimizeSemanticDistance) = m.inner + +Accessors.objective(m::MinimizeSemanticDistance) = + let + Sem = variable_semantic_mtx(m.semantic, m.inner) + Sem' * + [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * + [Sem zeros(size(Sem, 1)); zeros(size(Sem, 2))' 1.0] + end + +""" +$(TYPEDSIGNATURES) + +Set a quadratic objective that minimizes the solution distance from a selected +point in a space defined by a given semantic. Use +[`minimize_projected_distance`](@ref) for more fine-grained and weighted +variant, and [`minimize_solution_distance`](@ref) for working directly upon +variables. Internally powered by [`MinimizeSemanticDistance`](@ref). +""" +minimize_semantic_distance(semantic::Symbol, center::Vector{Float64}) = + model -> MinimizeSolutionDistance(semantic, center, model) + +""" +$(TYPEDEF) + +A wrapper that sets an objective that minimizes Euclidean distance from a given +point in a space defined by a projection matrix. +""" +struct MinimizeProjectedDistance <: AbstractModelWrapper + proj::SparseMat + center::Vector{Float64} + inner::AbstractMetabolicModel +end + +Accessors.unwrap_model(m::MinimizeProjectedDistance) = m.inner + +Accessors.objective(m::MinimizeProjectedDistance) = + let + m.proj' * + [spdiagm(fill(-0.5, length(m.center))) m.center] * + [m.proj zeros(size(R, 1)); zeros(size(R, 2))' 1.0] + end + +""" +$(TYPEDSIGNATURES) + +Set a quadratic objective that minimizes the solution distance from a selected +point in a space defined by a custom projection matrix. See +[`minimize_solution_distance`](@ref) and [`minimize_semantic_distance`](@ref) +for simpler variants. Internally powered by +[`MinimizeProjectedDistance`](@ref). +""" +minimize_projected_distance(proj::SparseMat, center::Vector{Float64}) = + model -> MinimizeProjectedDistance(proj, center, model) From 65a5ab7e54aec87afe6decca0a531591f18c3da6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 2 Mar 2023 11:55:57 +0100 Subject: [PATCH 261/531] sssssh clarabel --- test/reconstruction/enzyme_constrained.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 3e869a187..9795f2782 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -95,7 +95,7 @@ with_parsimonious_solution(:enzyme) |> flux_balance_analysis( Clarabel.Optimizer; - modifications = [modify_optimizer_attribute("max_iter", 1000)], + modifications = [modify_optimizer_attribute("max_iter", 1000), silence], ) @test isapprox( From ac77f2af5c85ccedf84f2d87dc92686dacaba054 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 3 Mar 2023 00:24:27 +0100 Subject: [PATCH 262/531] make kwarg plural --- docs/src/examples/15_enzyme_constrained.jl | 4 ++-- src/reconstruction/enzyme_constrained.jl | 22 +++++++++++----------- test/reconstruction/enzyme_constrained.jl | 4 ++-- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index 4888b43e8..ae0b595e9 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -62,8 +62,8 @@ enzyme_constrained_model = reaction_isozymes = rxn_isozymes, gene_product_bounds, gene_product_molar_mass = gene_product_masses, - gene_product_mass_group = _ -> "uncategorized", # all products belong to the same "uncategorized" category - gene_product_mass_group_bound = _ -> 100.0, # the total limit of mass in the single category + gene_product_mass_groups = _ -> "uncategorized", # all products belong to the same "uncategorized" category + gene_product_mass_group_bounds = _ -> 100.0, # the total limit of mass in the single category ) # (Alternatively, you may use [`make_enzyme_constrained_model`](@ref), which does the same diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 15abb76cb..0f50eb79a 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -6,7 +6,7 @@ given by the GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation for details). Multiple mass constraint groups can be placed on the model using the keyword arguments. -Parameters `gene_product_mass_group` and `gene_product_mass_group_bound` specify +Parameters `gene_product_mass_groups` and `gene_product_mass_group_bounds` specify the groups of gene products, and the respective total mass limit for each group. Gene products that are not listed in any gene product mass group are ignored. @@ -19,11 +19,11 @@ capacity" in the model. ``` ecmodel = make_enzyme_constrained_model( model; - gene_product_mass_group = Dict( + gene_product_mass_groups = Dict( "membrane" => ["e1", "e2"], "total" => ["e1", "e2", "e3"], ), - gene_product_mass_group_bound = Dict( + gene_product_mass_group_bounds = Dict( "membrane" => 0.2, "total" => 0.5, ), @@ -37,18 +37,18 @@ ecmodel2 = make_enzyme_constrained_model( """ function make_enzyme_constrained_model( model::AbstractMetabolicModel; - gene_product_mass_group::Maybe{Dict{String,Vector{String}}} = nothing, - gene_product_mass_group_bound::Maybe{Dict{String,Float64}} = nothing, + gene_product_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, + gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, total_gene_product_mass_bound::Maybe{Float64} = nothing, ) if !isnothing(total_gene_product_mass_bound) - gene_product_mass_group = Dict("uncategorized" => genes(model)) - gene_product_mass_group_bound = + gene_product_mass_groups = Dict("uncategorized" => genes(model)) + gene_product_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) end - isnothing(gene_product_mass_group) && + isnothing(gene_product_mass_groups) && throw(ArgumentError("missing mass group specification")) - isnothing(gene_product_mass_group_bound) && + isnothing(gene_product_mass_group_bounds) && throw(ArgumentError("missing mass group bounds")) gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) @@ -137,7 +137,7 @@ function make_enzyme_constrained_model( # prepare enzyme capacity constraints mg_gid_lookup = Dict{String,Vector{String}}() for gid in gids[coupling_row_gene_product] - for (mg, mg_gids) in gene_product_mass_group # each gid can belong to multiple mass groups + for (mg, mg_gids) in gene_product_mass_groups # each gid can belong to multiple mass groups gid ∉ mg_gids && continue if haskey(mg_gid_lookup, mg) push!(mg_gid_lookup[mg], gid) @@ -156,7 +156,7 @@ function make_enzyme_constrained_model( grp, idxs, mms, - gene_product_mass_group_bound[grp], + gene_product_mass_group_bounds[grp], ), ) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index bc237f5ac..b1c816142 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -139,8 +139,8 @@ end gm = make_enzyme_constrained_model( m; - gene_product_mass_group = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), - gene_product_mass_group_bound = Dict("uncategorized" => 0.5, "bound2" => 0.04), + gene_product_mass_groups = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), + gene_product_mass_group_bounds = Dict("uncategorized" => 0.5, "bound2" => 0.04), ) res = flux_balance_analysis( From f543e161968537180ddc17cbb738d290e1e711ab Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 3 Mar 2023 00:26:28 +0100 Subject: [PATCH 263/531] rename geneassociation file to Isozyme --- src/types/{GeneAssociations.jl => Isozyme.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/types/{GeneAssociations.jl => Isozyme.jl} (100%) diff --git a/src/types/GeneAssociations.jl b/src/types/Isozyme.jl similarity index 100% rename from src/types/GeneAssociations.jl rename to src/types/Isozyme.jl From ec6ab919edf029f9b15769f252a7c92aa2a65fd6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 11:21:23 +0100 Subject: [PATCH 264/531] convert parsimonious wrappers to the other QP ones --- .../parsimonious_flux_balance_analysis.jl | 10 ++- src/reconstruction/pipes/parsimonious.jl | 25 ------- src/wrappers/MinimizeDistance.jl | 66 +++++++++++++++---- src/wrappers/ParsimoniousModel.jl | 59 ----------------- .../parsimonious_flux_balance_analysis.jl | 6 +- test/reconstruction/enzyme_constrained.jl | 2 +- 6 files changed, 65 insertions(+), 103 deletions(-) delete mode 100644 src/reconstruction/pipes/parsimonious.jl delete mode 100644 src/wrappers/ParsimoniousModel.jl diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index fe341386e..a975d7b38 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -33,13 +33,21 @@ using the callback in `qp_modifications`, which are applied after the FBA. See the documentation of [`flux_balance_analysis`](@ref) for usage examples of modifications. -Thhe optimum relaxation sequence can be specified in `relax` parameter, it +The optimum relaxation sequence can be specified in `relax` parameter, it defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original bound. Returns an optimized model that contains the pFBA solution (or an unsolved model if something went wrong). +# Performance + +This implementation attempts to save time by executing all pFBA steps on a +single instance of the optimization model problem, trading off possible +flexibility. For slightly less performant but much more flexible use, one can +construct parsimonious models directly using +[`with_parsimonious_objective`](@ref). + # Example ``` model = load_model("e_coli_core.json") diff --git a/src/reconstruction/pipes/parsimonious.jl b/src/reconstruction/pipes/parsimonious.jl deleted file mode 100644 index 7f8e39fdf..000000000 --- a/src/reconstruction/pipes/parsimonious.jl +++ /dev/null @@ -1,25 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the -solution of variables specified by `semantics`. -""" -with_parsimonious_solution(semantics::Vector{Symbol}) = - model -> ParsimoniousModel(model, semantics) - -""" -$(TYPEDSIGNATURES) - -Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the -solution of variables specified by `semantic`. -""" -with_parsimonious_solution(semantic::Symbol) = with_parsimonious_solution([semantic]) - -""" -$(TYPEDSIGNATURES) - -Pipe-able version of the [`ParsimoniousModel`](@ref) wrapper that minimizes the -solution of variables specified by `var_ids`. -""" -with_parsimonious_solution(var_ids::Vector{String}) = - model -> ParsimoniousModel(model, var_ids) diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl index 891d87841..97a609f68 100644 --- a/src/wrappers/MinimizeDistance.jl +++ b/src/wrappers/MinimizeDistance.jl @@ -25,41 +25,69 @@ variants of the same concept. Internally powered by [`MinimizeSolutionDistance`](@ref). """ minimize_solution_distance(center::Vector{Float64}) = - model -> MinimizeSolutionDistance(center, model) + model::AbstractMetabolicModel -> MinimizeSolutionDistance(center, model) + +""" +$(TYPEDSIGNATURES) + +Set an objective that finds a solution of minimal norm. Typically, it is +necessary to also add more constraints to the objective that prevent optimality +of the trivial zero solution. + +This can be used to implement [`parsimonious_flux_balance_analysis`](@ref) in a +flexible way that fits into larger model systems. +""" +with_parsimonious_objective() = + model::AbstractMetabolicModel -> + MinimizeSolutionDistance(zeros(n_variables(model)), model) """ $(TYPEDEF) A wrapper that sets an objective that minimizes Euclidean distance from a given -point in a semantic. +point in a semantics. """ struct MinimizeSemanticDistance <: AbstractModelWrapper - semantic::Symbol + semantics::Symbol center::Vector{Float64} inner::AbstractMetabolicModel end Accessors.unwrap_model(m::MinimizeSemanticDistance) = m.inner -Accessors.objective(m::MinimizeSemanticDistance) = - let - Sem = variable_semantic_mtx(m.semantic, m.inner) - Sem' * - [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * - [Sem zeros(size(Sem, 1)); zeros(size(Sem, 2))' 1.0] - end +function Accessors.objective(m::MinimizeSemanticDistance) + (_, _, _, smtx) = Accessors.Internal.semantics(m.semantics) + Sem = smtx(m.inner) + + return Sem' * + [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * + [Sem zeros(size(Sem, 1)); zeros(size(Sem, 2))' 1.0] +end """ $(TYPEDSIGNATURES) Set a quadratic objective that minimizes the solution distance from a selected -point in a space defined by a given semantic. Use +point in a space defined by a given semantics. Use [`minimize_projected_distance`](@ref) for more fine-grained and weighted variant, and [`minimize_solution_distance`](@ref) for working directly upon variables. Internally powered by [`MinimizeSemanticDistance`](@ref). """ -minimize_semantic_distance(semantic::Symbol, center::Vector{Float64}) = - model -> MinimizeSolutionDistance(semantic, center, model) +minimize_semantic_distance(semantics::Symbol, center::Vector{Float64}) = + model::AbstractMetabolicModel -> MinimizeSolutionDistance(semantics, center, model) + +""" +$(TYPEDSIGNATURES) + +Set an objective that finds a solution of minimal norm in a given semantics. +This can be used to implement various realistic variants of +[`parsimonious_flux_balance_analysis`](@ref). +""" +with_parsimonious_objective(semantics::Symbol) = + model::AbstractMetabolicModel -> let + (_, n_sem, _, _) = Accessors.Internal.semantics(semantics) + MinimizeSemanticDistance(semantics, zeros(n_sem(model)), model) + end """ $(TYPEDEF) @@ -92,4 +120,14 @@ for simpler variants. Internally powered by [`MinimizeProjectedDistance`](@ref). """ minimize_projected_distance(proj::SparseMat, center::Vector{Float64}) = - model -> MinimizeProjectedDistance(proj, center, model) + model::AbstractMetabolicModel -> MinimizeProjectedDistance(proj, center, model) + +""" +$(TYPEDSIGNATURES) + +Set a quadratic objective that minimizes the norm in a given projection of +model variables. +""" +with_parsimonious_objective(proj::SparseMat) = + model::AbstractMetabolicModel -> + MinimizeProjectedDistance(proj, zeros(size(proj, 1)), model) diff --git a/src/wrappers/ParsimoniousModel.jl b/src/wrappers/ParsimoniousModel.jl deleted file mode 100644 index e3e8f5e34..000000000 --- a/src/wrappers/ParsimoniousModel.jl +++ /dev/null @@ -1,59 +0,0 @@ - -""" -$(TYPEDEF) - -A wrapper that adds a quadratic objective that minimizes the sum of a set of -squared variables. If the bounds of the growth rate of the model are fixed, this -corresponds to finding a parsimonious solution (i.e. pFBA). Note, the sense of -the optimization problem should be `MAX_SENSE`, because internally the objective -is negated. - -This is used to implement [`parsimonious_flux_balance_analysis`](@ref). - -# Example -``` -res = model |> - with_changed_bound("biomass", lower_bound = 0.1) |> - with_parsimonious_solution(:enzymes) |> - flux_balance_analysis(Clarabel.Optimizer) -``` -""" -struct ParsimoniousModel <: AbstractModelWrapper - inner::AbstractMetabolicModel - var_ids::Vector{String} -end - -function ParsimoniousModel(model::AbstractMetabolicModel, semantics::Vector{Symbol}) - var_ids = - vcat([first(Accessors.Internal.semantics(sem))(model) for sem in semantics]...) - ParsimoniousModel(model, var_ids) -end - -ParsimoniousModel(model::AbstractMetabolicModel, semantic::Symbol) = - ParsimoniousModel(model, [semantic]) - -Accessors.unwrap_model(m::ParsimoniousModel) = m.inner - -""" -$(TYPEDSIGNATURES) - -Return a negative, uniformly weighted, quadratic-only objective representing the -squared sum of `model.var_ids`. -""" -function Accessors.objective(model::ParsimoniousModel)::SparseMat - obj = spzeros(n_variables(model), n_variables(model) + 1) # + 1 for QP solver formulation - - idxs = indexin(model.var_ids, variables(model)) - j = findfirst(isnothing, idxs) - isnothing(j) || throw(DomainError(model.var_ids[j], "No variable with this ID.")) - - for i in idxs - obj[i, i] = -1.0 # negative because objective will be maximized - end - obj -end - -# need to manually inherit these -Accessors.enzyme_variables(model::ParsimoniousModel) = enzyme_variables(model.inner) -Accessors.enzyme_group_variables(model::ParsimoniousModel) = - enzyme_group_variables(model.inner) diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl index 355d8a587..19e4fd5fa 100644 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ b/test/analysis/parsimonious_flux_balance_analysis.jl @@ -19,12 +19,12 @@ d2 = model |> with_changed_bound("biomass1", lower_bound = 10.0) |> - with_parsimonious_solution(:reaction) |> + with_parsimonious_objective(:reaction) |> flux_balance_analysis(Clarabel.Optimizer, modifications = [silence]) |> values_dict @test all(isapprox(d[k], d2[k], atol = QP_TEST_TOLERANCE) for k in keys(d2)) - Q = objective(model |> with_parsimonious_solution(:reaction)) - @test all(Q[i, i] == -1 for i = 1:7) + Q = objective(model |> with_parsimonious_objective(:reaction)) + @test all(diag(Q) .== -0.5) end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 9795f2782..81aede0f0 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -92,7 +92,7 @@ model |> with_changed_bound("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb) |> with_enzyme_constraints(; total_gene_product_mass_bound) |> - with_parsimonious_solution(:enzyme) |> + with_parsimonious_objective(:enzyme) |> flux_balance_analysis( Clarabel.Optimizer; modifications = [modify_optimizer_attribute("max_iter", 1000), silence], From 18cc8a036ca2a7d30433d593638575898af31160 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 11:23:31 +0100 Subject: [PATCH 265/531] fix types --- src/types/accessors/bits/semantics.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index a617a4ceb..be768007c 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -64,7 +64,7 @@ $(TYPEDSIGNATURES) Like [`get_semantics`](@ref) but throws a `DomainError` if the semantics is not available. """ -function semantics(semantics::Symbol)::Types.Maybe{Semantic} +function semantics(semantics::Symbol)::Semantic res = get_semantics(semantics) isnothing(res) && throw(DomainError(semantics, "unknown semantics")) res From c142c9eabac4ef839793d12520533a74156eb6ef Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 13:33:26 +0100 Subject: [PATCH 266/531] more fixen --- src/types/accessors/ModelWrapper.jl | 2 +- src/types/accessors/bits/semantics.jl | 18 ++++++++++++++++++ src/wrappers/MinimizeDistance.jl | 4 ++-- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index b0935dc0f..2c687f616 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -15,7 +15,7 @@ end # [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective reactions n_reactions reaction_variables coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index be768007c..2a3ab87c4 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -182,6 +182,24 @@ safety reasons, this is never automatically inherited by wrappers. ) Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn]) + + Base.eval( + themodule, + :(function $mapping(w::AbstractModelWrapper)::Dict{String,Dict{String,Float64}} + $mapping(unwrap_model(w)) + end), + ) + + # TODO here we would normally also overload the matrix function, but that + # one will break once anyone touches variables of the models (which is + # common). We should have a macro like @model_does_not_modify_variable_set + # that adds the overloads. Or perhaps AbstractModelWrapperWithSameVariables? + # + # The same probably goes for other semantics; + # AbstractModelWrapperThatOnlyTouchesSemantics(...) ? (Which has an + # alternative in forcing people to overload all semantic functions in all + # cases of adding semantics, which might actually be the right way.) + themodule.Internal.variable_semantics[sym] = Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx)) end diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl index 97a609f68..599abb86e 100644 --- a/src/wrappers/MinimizeDistance.jl +++ b/src/wrappers/MinimizeDistance.jl @@ -59,9 +59,9 @@ function Accessors.objective(m::MinimizeSemanticDistance) (_, _, _, smtx) = Accessors.Internal.semantics(m.semantics) Sem = smtx(m.inner) - return Sem' * + return Sem * [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * - [Sem zeros(size(Sem, 1)); zeros(size(Sem, 2))' 1.0] + [Sem' zeros(size(Sem, 2)); zeros(size(Sem, 1))' 1.0] end """ From c76d6d6092b27254ebaf7f37c2b7efb26cafa271 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 13:38:22 +0100 Subject: [PATCH 267/531] fix infinite recursion --- src/wrappers/EnzymeConstrainedModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index eeb7c6134..87c13417c 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -125,7 +125,7 @@ the original fluxes in the wrapped model. Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - reactions(model), + reactions(model.inner), reaction_variables_matrix(model), ) # TODO currently inefficient From a078cd931cdaf7d9314e413a2fbf9e4d1cde8363 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 13:47:01 +0100 Subject: [PATCH 268/531] more fixes --- src/wrappers/SimplifiedEnzymeConstrainedModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 7f027f30c..589e7be51 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -98,7 +98,7 @@ wrapped model. Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - semantics(model), + reactions(model.inner), reaction_variables_matrix(model), ) # TODO currently inefficient From ae696d6c29d6605424a52df6dd691a42bde90ca5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 3 Mar 2023 21:35:33 +0100 Subject: [PATCH 269/531] enforce proper overloading for semantics, fix some wrappers --- src/types/accessors/bits/semantics.jl | 14 +++++++---- src/wrappers/EnzymeConstrainedModel.jl | 32 +++++++------------------ src/wrappers/MaxMinDrivingForceModel.jl | 17 ++++++------- 3 files changed, 26 insertions(+), 37 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 2a3ab87c4..7a3d7077f 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -113,9 +113,7 @@ definition of the correspondence of $name and model variables. """, ), :(function $plural(a::AbstractMetabolicModel)::Vector{String} - x = collect(keys($mapping(a))) - sort!(x) - x + String[] end), ) @@ -133,7 +131,7 @@ vector returned by [`$plural`]. """, ), :(function $count(a::AbstractMetabolicModel)::Int - length($mapping(a)) + 0 end), ) @@ -183,6 +181,14 @@ safety reasons, this is never automatically inherited by wrappers. Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn]) + Base.eval(themodule, :(function $plural(w::AbstractModelWrapper)::Vector{String} + $plural(unwrap_model(w)) + end)) + + Base.eval(themodule, :(function $count(w::AbstractModelWrapper)::Int + $count(unwrap_model(w)) + end)) + Base.eval( themodule, :(function $mapping(w::AbstractModelWrapper)::Dict{String,Dict{String,Float64}} diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 87c13417c..81c9581b3 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -101,12 +101,6 @@ function Accessors.bounds(model::EnzymeConstrainedModel) (lbs, ubs) end -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to -the original fluxes in the wrapped model (as a matrix). -""" function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) rxnmat = enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) @@ -116,33 +110,25 @@ function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) ] end -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in [`EnzymeConstrainedModel`](@ref) to -the original fluxes in the wrapped model. -""" Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - reactions(model.inner), + reactions(model), reaction_variables_matrix(model), ) # TODO currently inefficient -""" -$(TYPEDSIGNATURES) +Accessors.enzymes(model::EnzymeConstrainedModel) = genes(model) + +Accessors.n_enzymes(model::EnzymeConstrainedModel) = n_genes(model) -Get a mapping of enzyme concentration (on a mass basis, i.e. mass enzyme/mass -cell) variables to inner variables. -""" Accessors.enzyme_variables(model::EnzymeConstrainedModel) = - Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) # this is enough for all the semantics to work + Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) -""" -$(TYPEDSIGNATURES) +Accessors.enzyme_groups(model::EnzymeConstrainedModel) = + [grp.group_id for grp in model.coupling_row_mass_group] +Accessors.n_enzyme_groups(model::EnzymeConstrainedModel) = + length(model.coupling_row_mass_group) -Get a mapping of enzyme groups to variables. See [`enzyme_variables`](@ref). -""" function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) enz_ids = genes(model) Dict( diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index 9736db085..d7931a118 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -87,19 +87,16 @@ Accessors.variables(model::MaxMinDrivingForceModel) = Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + n_reactions(model) -""" -$(TYPEDSIGNATURES) - -Log metabolite concentration mapping to model variables. -""" +Accessors.metabolite_log_concentrations(model::MaxMinDrivingForceModel) = + "log " .* metabolites(model) +Accessors.n_metabolite_log_concentrations(model::MaxMinDrivingForceModel) = + n_metabolites(model) Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) -""" -$(TYPEDSIGNATURES) - -Gibbs free energy of reaction mapping to model variables. -""" +Accessors.gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = + "ΔG " .* reactions(model) +Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = n_reactions(model) Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) From 67bfb175e40fbc0d8d264efcffc7207769364351 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 5 Mar 2023 19:05:21 +0100 Subject: [PATCH 270/531] add new test and adjust for mass parsimony --- test/reconstruction/enzyme_constrained.jl | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 81aede0f0..a0feda927 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -106,7 +106,7 @@ @test isapprox( values_dict(:enzyme_group, res2)["uncategorized"], - 91.4275211, + 89.35338, atol = QP_TEST_TOLERANCE, ) end @@ -182,4 +182,23 @@ end @test isapprox(mass_groups["bound2"], 0.04, atol = TEST_TOLERANCE) @test length(genes(gm)) == 4 @test length(genes(gm.inner)) == 4 + + # new pfba test + growth_lb = rxn_fluxes["r6"] * 0.9 + + mqp = + m |> + with_changed_bound("r6", lower_bound = growth_lb) |> + with_enzyme_constraints(; + gene_product_mass_group = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), + gene_product_mass_group_bound = Dict("uncategorized" => 0.5, "bound2" => 0.04), + ) |> + with_parsimonious_objective(:enzyme) + + @test all(stoichiometry(mqp) .== stoichiometry(gm)) + @test all(coupling(mqp) .== coupling(gm)) + + # this QP minimizes the squared sum of enzyme masses in g/gDW! + Q = sparse([9, 10, 11, 12], [9, 10, 11, 12], [-0.5, -2.0, -8.0, -4.5], 12, 13) + @test all(objective(mqp) .== Q) end From 3bb0cb26eb199dbd6793e63bb85198a3d1b92214 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 20 Feb 2023 17:55:38 +0100 Subject: [PATCH 271/531] extend simplified enzyme model --- src/analysis.jl | 1 + .../simplified_enzyme_constrained.jl | 62 ++++++++++-- src/wrappers/EqualGrowthCommunityModel.jl | 97 +++++++++++++++++++ .../SimplifiedEnzymeConstrainedModel.jl | 79 +++++++++++---- test/reconstruction/constrained_allocation.jl | 8 +- .../simplified_enzyme_constrained.jl | 73 +++++++++++++- 6 files changed, 281 insertions(+), 39 deletions(-) create mode 100644 src/wrappers/EqualGrowthCommunityModel.jl diff --git a/src/analysis.jl b/src/analysis.jl index 590d2e6f2..3c55109b3 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -25,6 +25,7 @@ using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types +using ..Types: _EnzymeConstrainedReactionColumn, SimplifiedEnzymeConstrainedColumn using Distributed using DistributedData diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 85cc90104..8f1633ca1 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -1,25 +1,59 @@ """ $(TYPEDSIGNATURES) -Wrap a `model` with a structure given by sMOMENT algorithm; returns a +Wrap a `model` with a structure given by sMOMENT algorithm. Returns a [`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). The sMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple isozymes are present in `model`, the "fastest" isozyme will be used. This is determined based on maximum kcat (forward or backward) divided by mass of the -isozyme. The `total_gene_product_mass_bound` is the maximum "enzyme capacity" in -the model. +isozyme. Multiple enzyme capacity constraint can be placed on the model using +the keyword arguments. + +Parameters `reaction_mass_groups` and `reaction_mass_group_bounds` specify +groups of reactions (by their IDs), and their respective total mass limit. +Reactions that are not listed in any reaction mass group are ignored (likewise +if they don't have isozymes). + +For simplicity, specifying the `total_reaction_mass_bound` argument overrides +the above arguments by internally specifying a single group on all reactions +that acts like a maximum "enzyme capacity" in the model. # Example ``` ecmodel = make_simplified_enzyme_constrained_model( model; - total_gene_product_mass_bound = 0.5 + reaction_mass_groups = Dict( + "membrane" => ["r1", "r2"], + "total" => ["r1", "r2", "r3"], + ), + reaction_mass_group_bounds = Dict( + "membrane" => 0.2, + "total" => 0.5, + ), +) + +ecmodel2 = make_simplified_enzyme_constrained_model( + model; + total_reaction_mass_bound = 0.5 ) +``` """ function make_simplified_enzyme_constrained_model( model::AbstractMetabolicModel; - total_gene_product_mass_bound::Float64 = 0.5, + total_reaction_mass_bound::Maybe{Float64} = nothing, + reaction_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, + reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, ) + # fix kwarg inputs + if !isnothing(total_reaction_mass_bound) + reaction_mass_groups = Dict("uncategorized" => variables(model)) # TODO should be reactions + reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) + end + isnothing(reaction_mass_groups) && + throw(ArgumentError("missing reaction mass group specification")) + isnothing(reaction_mass_group_bounds) && + throw(ArgumentError("missing reaction mass group bounds")) + # helper function to rank the isozymes by relative speed speed_enzyme(model, isozyme) = max(isozyme.kcat_forward, isozyme.kcat_backward) / sum( @@ -36,10 +70,13 @@ function make_simplified_enzyme_constrained_model( columns = Vector{Wrappers.Internal.SimplifiedEnzymeConstrainedColumn}() - (lbs, ubs) = bounds(model) - rids = variables(model) + bound_ids = keys(reaction_mass_group_bounds) + total_reaction_mass_bounds = collect(values(reaction_mass_group_bounds)) - for i = 1:n_variables(model) + (lbs, ubs) = bounds(model) # TODO need a reaction_bounds accessor for full generality + rids = variables(model) # TODO needs to be reactions + + for i = 1:n_variables(model) # TODO this should be reactions isozyme = ris_(model, rids[i]) @@ -63,6 +100,11 @@ function make_simplified_enzyme_constrained_model( (gid, ps) in isozyme.gene_product_stoichiometry ) + bidxs = [ + idx for + (idx, bid) in enumerate(bound_ids) if rids[i] in reaction_mass_groups[bid] + ] + if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance # reaction can run in reverse push!( @@ -73,6 +115,7 @@ function make_simplified_enzyme_constrained_model( max(-ubs[i], 0), -lbs[i], mw / isozyme.kcat_backward, + bidxs, ), ) end @@ -87,10 +130,11 @@ function make_simplified_enzyme_constrained_model( max(lbs[i], 0), ubs[i], mw / isozyme.kcat_forward, + bidxs, ), ) end end - return SimplifiedEnzymeConstrainedModel(columns, total_gene_product_mass_bound, model) + return SimplifiedEnzymeConstrainedModel(columns, total_reaction_mass_bounds, model) end diff --git a/src/wrappers/EqualGrowthCommunityModel.jl b/src/wrappers/EqualGrowthCommunityModel.jl new file mode 100644 index 000000000..b0ef79ef8 --- /dev/null +++ b/src/wrappers/EqualGrowthCommunityModel.jl @@ -0,0 +1,97 @@ +""" +$(TYPEDEF) + +A wrapper around [`CommunityModel`](@ref) that returns a community model where +the growth rates of all members are constrained to be equal to +`community_objective_id`, which is the community growth rate. The objective of +the resultant model is set to this `community_objective_id`. + +# Notes +1. No biomass metabolite exists (and none are created). + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper + inner::CommunityModel + community_objective_id::String = "community_biomass" +end + +Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner + +Accessors.variables(cm::EqualGrowthCommunityModel) = + [variables(cm.inner); cm.community_objective_id] + +Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 + +Accessors.metabolites(cm::EqualGrowthCommunityModel) = + [metabolites(cm.inner); [m.id for m in cm.inner.members]] + +Accessors.n_metabolites(cm::EqualGrowthCommunityModel) = + n_metabolites(cm.inner) + length(cm.inner.members) + +Accessors.balance(cm::EqualGrowthCommunityModel) = [ + balance(cm.inner) + spzeros(length(cm.inner.members)) +] + +function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) + + S = stoichiometry(cm.inner) + obj_col = spzeros(size(S, 1)) + + biomass_ids = [ + cm.inner.name_lookup[id][:variables][m.biomass_reaction_id] for + (id, m) in cm.inner.members + ] + biomass_idxs = indexin(biomass_ids, variables(cm.inner)) + + obj_links = sparse( + 1:length(biomass_idxs), + biomass_idxs, + ones(length(biomass_idxs)), + length(cm.inner.members), + size(S, 2), + ) + + obj = -ones(length(cm.inner.members)) + + return [ + S obj_col + obj_links obj + ] +end + +function Accessors.bounds(cm::EqualGrowthCommunityModel) + lbs, ubs = bounds(cm.inner) + return ([lbs; 0], [ubs; constants.default_reaction_bound]) +end + +function Accessors.objective(cm::EqualGrowthCommunityModel) + vec = spzeros(n_variables(cm)) # overwrite objective + vec[end] = 1.0 + return vec +end + +Accessors.coupling(cm::EqualGrowthCommunityModel) = + [coupling(cm.inner) spzeros(n_coupling_constraints(cm.inner))] + + +function Accessors.reaction_variables(cm::EqualGrowthCommunityModel) + r_v = reaction_variables(cm.inner) + r_v[cm.community_objective_id] = Dict(cm.community_objective_id => 1.0) + r_v +end + +Accessors.reactions(cm::EqualGrowthCommunityModel) = + [reactions(cm.inner); cm.community_objective_id] + +Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 + +Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = + environmental_exchange_variables(model.inner) + +Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) + +Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = + enzyme_group_variables(model.inner) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 589e7be51..2cef0191f 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -2,6 +2,23 @@ """ $(TYPEDEF) +A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. + +# Fields +$(TYPEDFIELDS) +""" +struct SimplifiedEnzymeConstrainedColumn + reaction_idx::Int # number of the corresponding reaction in the inner model + direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part + lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) + ub::Float64 + capacity_contribution::Float64 # must be 0 for bidirectional reactions (if direction==0) + capacity_bound_idxs::Vector{Int64} # index of associated bound(s) +end + +""" +$(TYPEDEF) + An enzyme-capacity-constrained model using sMOMENT algorithm, as described by *Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of metabolic models with enzyme constraints" BMC bioinformatics, 2020*. @@ -13,24 +30,24 @@ The model is constructed as follows: - stoichiometry of the original model is retained as much as possible, but enzymatic reations are split into forward and reverse parts (marked by a suffix like `...#forward` and `...#reverse`), -- coupling is added to simulate a virtual metabolite "enzyme capacity", which is - consumed by all enzymatic reactions at a rate given by enzyme mass divided by - the corresponding kcat, -- the total consumption of the enzyme capacity is constrained to a fixed - maximum. +- coupling is added to simulate lumped virtual metabolites that act like enzyme + capacities. These are consumed by enzymatic reactions at a rate given by + enzyme mass divided by the corresponding kcat, +- the total consumption of the enzyme capacity bounds is constrained to be less + than some fixed values. The `SimplifiedEnzymeConstrainedModel` structure contains a worked-out representation of the optimization problem atop a wrapped -[`AbstractMetabolicModel`](@ref), in particular the separation of certain -reactions into unidirectional forward and reverse parts (which changes the -stoichiometric matrix), an "enzyme capacity" required for each reaction, and the -value of the maximum capacity constraint. Original coupling in the inner model -is retained. +[`AbstractMetabolicModel`](@ref). The internal representation of the model +splits reactions into unidirectional forward and reverse parts (which changes +the stoichiometric matrix). In the structure, the field `columns` describes the correspondence of -stoichiometry columns to the stoichiometry and data of the internal wrapped -model, and `total_gene_product_mass_bound` is the total bound on the enzyme -capacity consumption as specified in sMOMENT algorithm. +stoichiometry columns to the stoichiometry, and data of the internal wrapped +model. Multiple capacity bounds may be added through +`total_reaction_mass_bounds`. These bounds are connected to the model through +`columns`. Since this algorithm is reaction centered, no enzymes directly appear +in the formulation. This implementation allows easy access to fluxes from the split reactions (available in `variables(model)`), while the original "simple" reactions from @@ -49,7 +66,7 @@ $(TYPEDFIELDS) """ struct SimplifiedEnzymeConstrainedModel <: AbstractModelWrapper columns::Vector{SimplifiedEnzymeConstrainedColumn} - total_gene_product_mass_bound::Float64 + total_reaction_mass_bounds::Vector{Float64} inner::AbstractMetabolicModel end @@ -102,15 +119,37 @@ Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = reaction_variables_matrix(model), ) # TODO currently inefficient -Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) = vcat( - coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model), - [col.capacity_required for col in model.columns]', -) +function Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) + inner_coupling = + coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model) + + I = Int64[] + J = Int64[] + V = Float64[] + for (col_idx, col) in enumerate(model.columns) + for row_idx in col.capacity_bound_idxs + push!(J, col_idx) + push!(I, row_idx) + push!(V, col.capacity_contribution) + end + end + + capacity_coupling = + sparse(I, J, V, length(model.total_reaction_mass_bounds), length(model.columns)) + + return [ + inner_coupling + capacity_coupling + ] +end Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = - n_coupling_constraints(model.inner) + 1 + n_coupling_constraints(model.inner) + length(model.total_reaction_mass_bounds) Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = let (iclb, icub) = coupling_bounds(model.inner) - (vcat(iclb, [0.0]), vcat(icub, [model.total_gene_product_mass_bound])) + ( + vcat(iclb, zeros(length(model.total_reaction_mass_bounds))), + vcat(icub, model.total_reaction_mass_bounds), + ) end diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl index b0b4971d1..e721be06c 100644 --- a/test/reconstruction/constrained_allocation.jl +++ b/test/reconstruction/constrained_allocation.jl @@ -54,10 +54,8 @@ @test first(ribomodel.reactions["r6"].gene_associations).kcat_forward == 0.2 @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 - cam = make_simplified_enzyme_constrained_model( - ribomodel; - total_gene_product_mass_bound = 0.5, - ) + cam = + make_simplified_enzyme_constrained_model(ribomodel; total_reaction_mass_bound = 0.5) @test coupling(cam)[1, 7] == 5.0 @@ -72,7 +70,7 @@ # test inplace variant remove_isozymes!(m, "r6") add_virtualribosome!(m, "r6", 0.2) - cam = m |> with_simplified_enzyme_constraints(total_gene_product_mass_bound = 0.5) + cam = m |> with_simplified_enzyme_constraints(total_reaction_mass_bound = 0.5) @test coupling(cam)[1, 7] == 5.0 diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index f4b094e7d..a0b22fd63 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -34,7 +34,7 @@ lower_bounds = [-1000.0, -1.0], upper_bounds = [nothing, 12.0], ) |> - with_simplified_enzyme_constraints(total_gene_product_mass_bound = 100.0) + with_simplified_enzyme_constraints(total_reaction_mass_bound = 100.0) rxn_fluxes = flux_balance_analysis( @@ -43,9 +43,72 @@ modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> values_dict - @test isapprox( - rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8907347602586123, - atol = TEST_TOLERANCE, + @test isapprox(rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], 0.890731, atol = TEST_TOLERANCE) +end + +@testset "Small SMOMENT" begin + + m = ObjectModel() + m1 = Metabolite("m1") + m2 = Metabolite("m2") + m3 = Metabolite("m3") + m4 = Metabolite("m4") + + add_reactions!( + m, + [ + ReactionForward("r1", Dict("m1" => 1)), + ReactionForward("r2", Dict("m2" => 1)), + ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), + ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), + ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), + ReactionForward("r6", Dict("m4" => -1)), + ], ) + + gs = [ + Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) + Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) + Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) + Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) + ] + + m.reactions["r3"].gene_associations = + [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] + m.reactions["r4"].gene_associations = + [Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0)] + m.reactions["r5"].gene_associations = [ + Isozyme(; + gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), + kcat_forward = 2.0, + kcat_backward = 2.0, + ), + ] + m.objective = Dict("r6" => 1.0) + + add_genes!(m, gs) + add_metabolites!(m, [m1, m2, m3, m4]) + + sm = make_simplified_enzyme_constrained_model( + m; + reaction_mass_groups = Dict("b1" => ["r3", "r4"], "b2" => ["r5", "r4"]), + reaction_mass_group_bounds = Dict("b2" => 0.5, "b1" => 0.2), + ) + + stoichiometry(sm) + + cpling = sum(coupling(sm), dims = 2) + @test 1.5 in cpling && 11.5 in cpling + + lbs, ubs = coupling_bounds(sm) + @test all(lbs .== 0.0) + @test 0.5 in ubs && 0.2 in ubs + + res = flux_balance_analysis( + sm, + Tulip.Optimizer; + modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + + @test isapprox(solved_objective_value(res), 0.21212120975836252; atol = TEST_TOLERANCE) end From 628461fca6bce7464213143aea5678cf1a32289f Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 5 Mar 2023 20:32:28 +0100 Subject: [PATCH 272/531] fix merge error and missing semantics --- src/analysis.jl | 3 +- .../simplified_enzyme_constrained.jl | 1 + src/types.jl | 1 + src/types/models/CommunityModel.jl | 24 +++-- .../wrappers/EqualGrowthCommunityModel.jl | 14 +++ src/wrappers/EqualGrowthCommunityModel.jl | 97 ------------------- .../SimplifiedEnzymeConstrainedModel.jl | 18 ---- src/wrappers/bits/enzyme_constrained.jl | 6 +- src/wrappers/misc/enzyme_constrained.jl | 24 +++++ .../misc/simplified_enzyme_constrained.jl | 24 ----- test/reconstruction/enzyme_constrained.jl | 7 +- 11 files changed, 66 insertions(+), 153 deletions(-) delete mode 100644 src/wrappers/EqualGrowthCommunityModel.jl delete mode 100644 src/wrappers/misc/simplified_enzyme_constrained.jl diff --git a/src/analysis.jl b/src/analysis.jl index 3c55109b3..67b522c4b 100644 --- a/src/analysis.jl +++ b/src/analysis.jl @@ -25,7 +25,8 @@ using ..Internal: constants using ..Log.Internal: @models_log using ..Solver using ..Types -using ..Types: _EnzymeConstrainedReactionColumn, SimplifiedEnzymeConstrainedColumn +using ..Wrappers.Internal: + EnzymeConstrainedReactionColumn, SimplifiedEnzymeConstrainedColumn using Distributed using DistributedData diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 8f1633ca1..911b7fa54 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -90,6 +90,7 @@ function make_simplified_enzyme_constrained_model( lbs[i], ubs[i], 0, + Int64[], ), ) continue diff --git a/src/types.jl b/src/types.jl index f7c213643..9e1244b06 100644 --- a/src/types.jl +++ b/src/types.jl @@ -74,6 +74,7 @@ end # module Accessors @inc_dir types @inc_dir types models + @inc_dir types wrappers # TODO find a home for EqualGrowthCommunityModel @export_locals end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index abb0fba02..f35d8455c 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -200,21 +200,18 @@ Accessors.reactions(cm::CommunityModel) = [ Accessors.n_reactions(cm::CommunityModel) = sum(n_reactions(m.model) for m in values(cm.members)) + length(cm.environmental_links) -""" -$(TYPEDSIGNATURES) -Environmental exchange reaction mapping to model variables. -""" Accessors.environmental_exchange_variables(model::CommunityModel) = Dict( rid => Dict(rid => 1.0) for rid in [envlink.reaction_id for envlink in model.environmental_links] ) -""" -$(TYPEDSIGNATURES) +Accessors.environmental_exchanges(model::CommunityModel) = + [envlink.reaction_id for envlink in model.environmental_links] + +Accessors.n_environmental_exchanges(model::CommunityModel) = + length(model.environmental_links) -Environmental exchange reaction mapping to model variables. -""" function Accessors.enzyme_variables(model::CommunityModel) nlu(id, x) = model.name_lookup[id][:genes][x] e_v = Dict{String,Dict{String,Float64}}() @@ -227,6 +224,11 @@ function Accessors.enzyme_variables(model::CommunityModel) e_v end +Accessors.enzymes(cm::CommunityModel) = + [cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in enzymes(m.model)] + +Accessors.n_enzymes(cm::CommunityModel) = sum(n_enzymes(m.model) for m in cm.members) + """ $(TYPEDSIGNATURES) @@ -244,6 +246,12 @@ function Accessors.enzyme_group_variables(model::CommunityModel) e_g_v end +Accessors.enzyme_groups(cm::CommunityModel) = + [id * "#" * k for (id, m) in cm.members for k in enzyme_groups(m.model)] + +Accessors.n_enzyme_groups(cm::CommunityModel) = + sum(n_enzyme_groups(m.model) for m in cm.members) + #= This loops implements the rest of the accessors through access_community_member. Since most of the environmental reactions are generated programmtically, they diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index b0ef79ef8..6b68fa895 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -91,7 +91,21 @@ Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = environmental_exchange_variables(model.inner) +Accessors.environmental_exchanges(model::EqualGrowthCommunityModel) = + environmental_exchanges(model.inner) + +Accessors.n_environmental_exchanges(model::EqualGrowthCommunityModel) = + n_environmental_exchanges(model.inner) + Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) +Accessors.enzymes(model::EqualGrowthCommunityModel) = enzymes(model.inner) + +Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) + Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = enzyme_group_variables(model.inner) + +Accessors.enzymes(model::EqualGrowthCommunityModel) = enzymes(model.inner) + +Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) diff --git a/src/wrappers/EqualGrowthCommunityModel.jl b/src/wrappers/EqualGrowthCommunityModel.jl deleted file mode 100644 index b0ef79ef8..000000000 --- a/src/wrappers/EqualGrowthCommunityModel.jl +++ /dev/null @@ -1,97 +0,0 @@ -""" -$(TYPEDEF) - -A wrapper around [`CommunityModel`](@ref) that returns a community model where -the growth rates of all members are constrained to be equal to -`community_objective_id`, which is the community growth rate. The objective of -the resultant model is set to this `community_objective_id`. - -# Notes -1. No biomass metabolite exists (and none are created). - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper - inner::CommunityModel - community_objective_id::String = "community_biomass" -end - -Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner - -Accessors.variables(cm::EqualGrowthCommunityModel) = - [variables(cm.inner); cm.community_objective_id] - -Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 - -Accessors.metabolites(cm::EqualGrowthCommunityModel) = - [metabolites(cm.inner); [m.id for m in cm.inner.members]] - -Accessors.n_metabolites(cm::EqualGrowthCommunityModel) = - n_metabolites(cm.inner) + length(cm.inner.members) - -Accessors.balance(cm::EqualGrowthCommunityModel) = [ - balance(cm.inner) - spzeros(length(cm.inner.members)) -] - -function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) - - S = stoichiometry(cm.inner) - obj_col = spzeros(size(S, 1)) - - biomass_ids = [ - cm.inner.name_lookup[id][:variables][m.biomass_reaction_id] for - (id, m) in cm.inner.members - ] - biomass_idxs = indexin(biomass_ids, variables(cm.inner)) - - obj_links = sparse( - 1:length(biomass_idxs), - biomass_idxs, - ones(length(biomass_idxs)), - length(cm.inner.members), - size(S, 2), - ) - - obj = -ones(length(cm.inner.members)) - - return [ - S obj_col - obj_links obj - ] -end - -function Accessors.bounds(cm::EqualGrowthCommunityModel) - lbs, ubs = bounds(cm.inner) - return ([lbs; 0], [ubs; constants.default_reaction_bound]) -end - -function Accessors.objective(cm::EqualGrowthCommunityModel) - vec = spzeros(n_variables(cm)) # overwrite objective - vec[end] = 1.0 - return vec -end - -Accessors.coupling(cm::EqualGrowthCommunityModel) = - [coupling(cm.inner) spzeros(n_coupling_constraints(cm.inner))] - - -function Accessors.reaction_variables(cm::EqualGrowthCommunityModel) - r_v = reaction_variables(cm.inner) - r_v[cm.community_objective_id] = Dict(cm.community_objective_id => 1.0) - r_v -end - -Accessors.reactions(cm::EqualGrowthCommunityModel) = - [reactions(cm.inner); cm.community_objective_id] - -Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 - -Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = - environmental_exchange_variables(model.inner) - -Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) - -Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = - enzyme_group_variables(model.inner) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 2cef0191f..fbd6c1ae4 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -1,21 +1,3 @@ - -""" -$(TYPEDEF) - -A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct SimplifiedEnzymeConstrainedColumn - reaction_idx::Int # number of the corresponding reaction in the inner model - direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part - lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) - ub::Float64 - capacity_contribution::Float64 # must be 0 for bidirectional reactions (if direction==0) - capacity_bound_idxs::Vector{Int64} # index of associated bound(s) -end - """ $(TYPEDEF) diff --git a/src/wrappers/bits/enzyme_constrained.jl b/src/wrappers/bits/enzyme_constrained.jl index 1223cfbf8..e76921de2 100644 --- a/src/wrappers/bits/enzyme_constrained.jl +++ b/src/wrappers/bits/enzyme_constrained.jl @@ -2,8 +2,7 @@ """ $(TYPEDEF) -A helper type that describes the contents of -[`SimplifiedEnzymeConstrainedModel`](@ref)s. +A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. # Fields $(TYPEDFIELDS) @@ -13,7 +12,8 @@ struct SimplifiedEnzymeConstrainedColumn direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) ub::Float64 - capacity_required::Float64 # must be 0 for bidirectional reactions (if direction==0) + capacity_contribution::Float64 # must be 0 for bidirectional reactions (if direction==0) + capacity_bound_idxs::Vector{Int64} # index of associated bound(s) end """ diff --git a/src/wrappers/misc/enzyme_constrained.jl b/src/wrappers/misc/enzyme_constrained.jl index c745e2d9a..120af97bb 100644 --- a/src/wrappers/misc/enzyme_constrained.jl +++ b/src/wrappers/misc/enzyme_constrained.jl @@ -91,3 +91,27 @@ function enzyme_constrained_mass_group_coupling(model::EnzymeConstrainedModel) n_genes(model), ) end + +""" +$(TYPEDSIGNATURES) + +Internal helper for systematically naming reactions in [`SimplifiedEnzymeConstrainedModel`](@ref). +""" +simplified_enzyme_constrained_reaction_name(original_name::String, direction::Int) = + direction == 0 ? original_name : + direction > 0 ? "$original_name#forward" : "$original_name#reverse" + +""" +$(TYPEDSIGNATURES) + +Retrieve a utility mapping between reactions and split reactions; rows +correspond to "original" reactions, columns correspond to "split" reactions. +""" +simplified_enzyme_constrained_column_reactions(model::SimplifiedEnzymeConstrainedModel) = + sparse( + [col.reaction_idx for col in model.columns], + 1:length(model.columns), + [col.direction >= 0 ? 1 : -1 for col in model.columns], + n_variables(model.inner), + length(model.columns), + ) diff --git a/src/wrappers/misc/simplified_enzyme_constrained.jl b/src/wrappers/misc/simplified_enzyme_constrained.jl deleted file mode 100644 index 6d5711be0..000000000 --- a/src/wrappers/misc/simplified_enzyme_constrained.jl +++ /dev/null @@ -1,24 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`SimplifiedEnzymeConstrainedModel`](@ref). -""" -simplified_enzyme_constrained_reaction_name(original_name::String, direction::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward" : "$original_name#reverse" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -simplified_enzyme_constrained_column_reactions(model::SimplifiedEnzymeConstrainedModel) = - sparse( - [col.reaction_idx for col in model.columns], - 1:length(model.columns), - [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_variables(model.inner), - length(model.columns), - ) diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 0f2d0470a..83a791c47 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -190,8 +190,11 @@ end m |> with_changed_bound("r6", lower_bound = growth_lb) |> with_enzyme_constraints(; - gene_product_mass_group = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), - gene_product_mass_group_bound = Dict("uncategorized" => 0.5, "bound2" => 0.04), + gene_product_mass_groups = Dict( + "uncategorized" => genes(m), + "bound2" => ["g3"], + ), + gene_product_mass_group_bounds = Dict("uncategorized" => 0.5, "bound2" => 0.04), ) |> with_parsimonious_objective(:enzyme) From 32cd88b38c433c5c730e7983e47a00b029950924 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 6 Mar 2023 13:08:39 +0100 Subject: [PATCH 273/531] fix accessors --- src/types/wrappers/EqualGrowthCommunityModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 6b68fa895..6e2b83d64 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -106,6 +106,6 @@ Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = enzyme_group_variables(model.inner) -Accessors.enzymes(model::EqualGrowthCommunityModel) = enzymes(model.inner) +Accessors.enzyme_groups(model::EqualGrowthCommunityModel) = enzyme_groups(model.inner) -Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) +Accessors.n_enzyme_groups(model::EqualGrowthCommunityModel) = n_enzyme_groups(model.inner) From d35667e7f5c1dd51734793bbacdbfae13ba78ebd Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 30 Apr 2023 14:00:17 +0200 Subject: [PATCH 274/531] Semantics are now a cool friendly nice structure --- src/analysis/variability_analysis.jl | 10 ++++---- src/solver.jl | 9 ++++---- src/types/accessors/bits/semantics.jl | 33 +++++++++++++++++++++++---- src/wrappers/MinimizeDistance.jl | 14 ++++++------ 4 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 820c601cf..728843e89 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -57,27 +57,27 @@ function variability_analysis( indexes::Maybe{Vector{Int}} = nothing, kwargs..., ) - (sem_ids, n_ids, _, sem_varmtx) = Accessors.Internal.semantics(semantics) + s = Accessors.Internal.semantics(semantics) if isnothing(indexes) idxs = if isnothing(ids) - collect(1:n_ids(model)) + collect(1:s.count(model)) else - indexin(ids, sem_ids(model)) + indexin(ids, s.ids(model)) end any(isnothing.(idxs)) && throw(DomainError(ids[isnothing.(idxs)], "Unknown IDs specified")) indexes = Int.(idxs) end - if any((indexes .< 1) .| (indexes .> n_ids(model))) + if any((indexes .< 1) .| (indexes .> s.count(model))) throw(DomainError(indexes, "Index out of range")) end variability_analysis( model, optimizer; - directions = sem_varmtx(model)[:, indexes], + directions = s.mapping_matrix(model)[:, indexes], kwargs..., ) end diff --git a/src/solver.jl b/src/solver.jl index bf0ae31c2..a96493108 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -152,8 +152,8 @@ values_vec(:reaction, flux_balance_analysis(model, ...)) ``` """ function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) - (_, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) - is_solved(res.result) ? sem_varmtx(res.model)' * value.(res.result[:x]) : nothing + s = Accessors.Internal.semantics(semantics) + is_solved(res.result) ? s.mapping_matrix(res.model)' * value.(res.result[:x]) : nothing end """ @@ -197,9 +197,10 @@ values_dict(:reaction, flux_balance_analysis(model, ...)) ``` """ function values_dict(semantics::Symbol, res::ModelWithResult{<:Model}) - (ids, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) + s = Accessors.Internal.semantics(semantics) is_solved(res.result) ? - Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.result[:x])) : nothing + Dict(s.ids(res.model) .=> s.mapping_matrix(res.model)' * value.(res.result[:x])) : + nothing end """ diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 7a3d7077f..6dbcf3834 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -9,6 +9,7 @@ function make_mapping_mtx( semantics::Vector{String}, var_sem_val::Dict{String,Dict{String,Float64}}, )::Types.SparseMat + # TODO: move this to general utils or so rowidx = Dict(vars .=> 1:length(vars)) colidx = Dict(semantics .=> 1:length(semantics)) n = sum(length.(values(var_sem_val))) @@ -38,15 +39,37 @@ function make_mapping_dict( semantics::Vector{String}, mtx::Types.SparseMat, )::Dict{String,Dict{String,Float64}} + # TODO: move this to general utils Dict( sid => Dict(vars[vidx] => val for (vidx, val) in zip(findnz(mtx[:, sidx])...)) for (sidx, sid) in enumerate(semantics) ) end -const Semantic = Tuple{Function,Function,Function,Function} +""" +$(TYPEDEF) + +A structure of function that stores implementation of a given variable +semantic. Use [`semantics`](@ref) for lookup. If you want to create and +register new semantics, use [`@make_variable_semantics`](@ref). +""" +Base.@kwdef struct Semantics + # TODO: move this to types? + "Returns a vector identifiers that describe views in the given semantics" + ids::Function + + """Returns the size of the vector of the identifiers, in a possibly more + efficient way than measuring the length of the ID vector.""" + count::Function + + """Returns a mapping of semantic values to variables IDs in the model.""" + mapping::Function + + """Same as `mapping` but returns a matrix, which is possibly more efficient in certain cases.""" + mapping_matrix::Function +end -const variable_semantics = Dict{Symbol,Semantic}() +const variable_semantics = Dict{Symbol,Semantics}() """ $(TYPEDSIGNATURES) @@ -54,7 +77,7 @@ $(TYPEDSIGNATURES) Get a tuple of functions that work with the given semantics, or `nothing` if the semantics doesn't exist. """ -function get_semantics(semantics::Symbol)::Types.Maybe{Semantic} +function get_semantics(semantics::Symbol)::Types.Maybe{Semantics} get(variable_semantics, semantics, nothing) end @@ -64,7 +87,7 @@ $(TYPEDSIGNATURES) Like [`get_semantics`](@ref) but throws a `DomainError` if the semantics is not available. """ -function semantics(semantics::Symbol)::Semantic +function semantics(semantics::Symbol)::Semantics res = get_semantics(semantics) isnothing(res) && throw(DomainError(semantics, "unknown semantics")) res @@ -207,7 +230,7 @@ safety reasons, this is never automatically inherited by wrappers. # cases of adding semantics, which might actually be the right way.) themodule.Internal.variable_semantics[sym] = - Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx)) + Semantics(Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx))...) end """ diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl index 599abb86e..399fe1db1 100644 --- a/src/wrappers/MinimizeDistance.jl +++ b/src/wrappers/MinimizeDistance.jl @@ -56,12 +56,12 @@ end Accessors.unwrap_model(m::MinimizeSemanticDistance) = m.inner function Accessors.objective(m::MinimizeSemanticDistance) - (_, _, _, smtx) = Accessors.Internal.semantics(m.semantics) - Sem = smtx(m.inner) + s = Accessors.Internal.semantics(m.semantics) + M = s.mapping_matrix(m.inner) - return Sem * - [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * - [Sem' zeros(size(Sem, 2)); zeros(size(Sem, 1))' 1.0] + return M * + [spdiagm(fill(-0.5, size(M, 2))) m.center] * + [M' zeros(size(M, 2)); zeros(size(M, 1))' 1.0] end """ @@ -85,8 +85,8 @@ This can be used to implement various realistic variants of """ with_parsimonious_objective(semantics::Symbol) = model::AbstractMetabolicModel -> let - (_, n_sem, _, _) = Accessors.Internal.semantics(semantics) - MinimizeSemanticDistance(semantics, zeros(n_sem(model)), model) + s = Accessors.Internal.semantics(semantics) + MinimizeSemanticDistance(semantics, zeros(s.count(model)), model) end """ From 1ff215f9c3d2c6995e31714563d36d911586aff2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 10:20:43 +0200 Subject: [PATCH 275/531] add a framework for bounds over the semantics --- src/types/accessors/bits/semantics.jl | 69 +++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 9 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 6dbcf3834..f2f44056f 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -55,18 +55,36 @@ register new semantics, use [`@make_variable_semantics`](@ref). """ Base.@kwdef struct Semantics # TODO: move this to types? - "Returns a vector identifiers that describe views in the given semantics" + """ + Returns a vector identifiers that describe views in the given semantics. + """ ids::Function - """Returns the size of the vector of the identifiers, in a possibly more - efficient way than measuring the length of the ID vector.""" + """ + Returns the size of the vector of the identifiers, in a possibly more + efficient way than measuring the length of the ID vector. + """ count::Function - """Returns a mapping of semantic values to variables IDs in the model.""" + """ + Returns a mapping of semantic values to variables IDs in the model. + """ mapping::Function - """Same as `mapping` but returns a matrix, which is possibly more efficient in certain cases.""" + """ + Same as `mapping` but returns a matrix (with variables in rows and the + semantic values in columns), which is possibly more efficient or handy in + specific cases. + """ mapping_matrix::Function + + """ + Returns either `nothing` if the semantics does not have specific + constraints associated, or a vector of floating-point values that represent + equality constraints on semantic values, or a tuple of 2 vectors that + represent lower and upper bounds on semantic values. + """ + bounds::Function end const variable_semantics = Dict{Symbol,Semantics}() @@ -116,6 +134,7 @@ function make_variable_semantics( count = Symbol(:n_, plural) mapping = Symbol(sym, :_variables) mapping_mtx = Symbol(sym, :_variables_matrix) + bounds = Symbol(sym, :_bounds) pluralfn = Expr( :macrocall, @@ -202,8 +221,28 @@ safety reasons, this is never automatically inherited by wrappers. end), ) - Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn]) + boundsfn = Expr( + :macrocall, + Symbol("@doc"), + source, + Expr( + :string, + :TYPEDSIGNATURES, + """ + +Bounds for $name described by the model. Either returns `nothing` if there are +no bounds, or a vector of floats with equality bounds, or a tuple of 2 vectors +with lower and upper bounds. +""", + ), + :(function $bounds(a::AbstractMetabolicModel)::SparseMat + nothing + end), + ) + + Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn, boundsfn]) + # extend the AbstractModelWrapper Base.eval(themodule, :(function $plural(w::AbstractModelWrapper)::Vector{String} $plural(unwrap_model(w)) end)) @@ -219,8 +258,19 @@ safety reasons, this is never automatically inherited by wrappers. end), ) + Base.eval( + themodule, + :( + function $bounds( + w::AbstractModelWrapper, + )::Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}} + $bounds(unwrap_model(w)) + end + ), + ) + # TODO here we would normally also overload the matrix function, but that - # one will break once anyone touches variables of the models (which is + # one will break once anyone touches variables of the models (which is quite # common). We should have a macro like @model_does_not_modify_variable_set # that adds the overloads. Or perhaps AbstractModelWrapperWithSameVariables? # @@ -229,8 +279,9 @@ safety reasons, this is never automatically inherited by wrappers. # alternative in forcing people to overload all semantic functions in all # cases of adding semantics, which might actually be the right way.) - themodule.Internal.variable_semantics[sym] = - Semantics(Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx))...) + themodule.Internal.variable_semantics[sym] = Semantics( + Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx, bounds))..., + ) end """ From 54a07c5cdfa0ad6e983335342c35c2542619d597 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 10:54:04 +0200 Subject: [PATCH 276/531] start unifying the semantics naming (eradicate "plural") --- src/types/accessors/AbstractMetabolicModel.jl | 5 ++++ src/types/accessors/bits/semantics.jl | 29 +++++++++---------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 6d5c240b7..9f7dd79ef 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -116,6 +116,11 @@ flux, such as with separate bidirectional reactions. """ ) +""" +Shortcut for writing [`reaction_ids`](@ref). +""" +const reactions = reaction_ids + @make_variable_semantics( :enzyme, "enzyme supplies", diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index f2f44056f..f707a5099 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -130,13 +130,13 @@ function make_variable_semantics( ) haskey(themodule.Internal.variable_semantics, sym) && return - plural = Symbol(sym, :s) - count = Symbol(:n_, plural) + ids = Symbol(sym, :_ids) + count = Symbol(sym, :_count) mapping = Symbol(sym, :_variables) mapping_mtx = Symbol(sym, :_variables_matrix) bounds = Symbol(sym, :_bounds) - pluralfn = Expr( + idsfn = Expr( :macrocall, Symbol("@doc"), source, @@ -154,7 +154,7 @@ model. See the documentation of [`$mapping`](@ref) for closer definition of the correspondence of $name and model variables. """, ), - :(function $plural(a::AbstractMetabolicModel)::Vector{String} + :(function $ids(a::AbstractMetabolicModel)::Vector{String} String[] end), ) @@ -169,7 +169,7 @@ definition of the correspondence of $name and model variables. """ Count of $name that the model describes, should be equal to the length of -vector returned by [`$plural`]. +vector returned by [`$ids`]. """, ), :(function $count(a::AbstractMetabolicModel)::Int @@ -189,7 +189,7 @@ vector returned by [`$plural`]. Bipartite mapping of $name described by the model to the actual variables in the model. Returns a dictionary of $name assigned to the variable IDs and their linear coefficients. See the documentation of -[`$plural`](@ref) for semantics. +[`$ids`](@ref) for semantics. To improve the performance, you may want to use [`$mapping_mtx`](@ref). """, @@ -217,7 +217,7 @@ safety reasons, this is never automatically inherited by wrappers. """, ), :(function $mapping_mtx(a::AbstractMetabolicModel)::SparseMat - make_mapping_mtx(variables(a), $plural(a), $mapping(a)) + make_mapping_mtx(variables(a), $ids(a), $mapping(a)) end), ) @@ -240,11 +240,11 @@ with lower and upper bounds. end), ) - Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn, boundsfn]) + Base.eval.(Ref(themodule), [idsfn, countfn, mappingfn, mtxfn, boundsfn]) # extend the AbstractModelWrapper - Base.eval(themodule, :(function $plural(w::AbstractModelWrapper)::Vector{String} - $plural(unwrap_model(w)) + Base.eval(themodule, :(function $ids(w::AbstractModelWrapper)::Vector{String} + $ids(unwrap_model(w)) end)) Base.eval(themodule, :(function $count(w::AbstractModelWrapper)::Int @@ -279,9 +279,8 @@ with lower and upper bounds. # alternative in forcing people to overload all semantic functions in all # cases of adding semantics, which might actually be the right way.) - themodule.Internal.variable_semantics[sym] = Semantics( - Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx, bounds))..., - ) + themodule.Internal.variable_semantics[sym] = + Semantics(Base.eval.(Ref(themodule), (ids, count, mapping, mapping_mtx, bounds))...) end """ @@ -305,8 +304,8 @@ reactions; this macro declares precisely the same about the model type. macro all_variables_are_reactions(mt) m = esc(mt) quote - $Accessors.reactions(model::$m) = $Accessors.variables(model) - $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) + $Accessors.reaction_ids(model::$m) = $Accessors.variables(model) + $Accessors.reaction_count(model::$m) = $Accessors.n_variables(model) $Accessors.reaction_variables(model::$m) = Dict(var => Dict(var => 1.0) for var in $Accessors.variables(model)) $Accessors.reaction_variables_matrix(model::$m) = From aff6ad38eb065798a2f51e4b6ebb78bd437309f9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 10:55:08 +0200 Subject: [PATCH 277/531] replace n_reactions with reaction_count --- docs/src/concepts/3_custom_models.md | 12 ++++++------ docs/src/concepts/4_wrappers.md | 4 ++-- src/io/show/AbstractMetabolicModel.jl | 2 +- src/types/models/CommunityModel.jl | 5 +++-- src/types/models/SBMLModel.jl | 4 ++-- src/types/wrappers/EqualGrowthCommunityModel.jl | 2 +- src/wrappers/MaxMinDrivingForceModel.jl | 9 +++++---- test/io/mat.jl | 2 +- test/io/sbml.jl | 2 +- 9 files changed, 22 insertions(+), 20 deletions(-) diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index 418e788d3..a6836a717 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -49,10 +49,10 @@ end First, define the reactions and metabolites: ```julia -COBREXA.n_reactions(m::CircularModel) = m.size +COBREXA.reaction_count(m::CircularModel) = m.size COBREXA.n_metabolites(m::CircularModel) = m.size -COBREXA.reactions(m::CircularModel) = ["rxn$i" for i in 1:n_reactions(m)] +COBREXA.reactions(m::CircularModel) = ["rxn$i" for i in 1:reaction_count(m)] COBREXA.metabolites(m::CircularModel) = ["met$i" for i in 1:n_metabolites(m)] ``` @@ -63,18 +63,18 @@ We can continue with the actual linear model properties: ```julia function COBREXA.objective(m::CircularModel) - c = spzeros(n_reactions(m)) + c = spzeros(reaction_count(m)) c[1] = 1 #optimize the first reaction return c end COBREXA.bounds(m::CircularModel) = ( - zeros(n_reactions(m)), # lower bounds - ones(n_reactions(m)), # upper bounds + zeros(reaction_count(m)), # lower bounds + ones(reaction_count(m)), # upper bounds ) function COBREXA.stoichiometry(m::CircularModel) - nr = n_reactions(m) + nr = reaction_count(m) stoi(i,j) = i == j ? 1.0 : (i % nr + 1) == j ? -1.0 : diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index e59e600da..aec3f899f 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -106,7 +106,7 @@ modifying the reaction list, stoichiometry, and bounds: ```julia COBREXA.unwrap_model(x::LeakyModel) = x.mdl -COBREXA.n_reactions(x::LeakyModel) = n_reactions(x.mdl) + 1 +COBREXA.reaction_count(x::LeakyModel) = reaction_count(x.mdl) + 1 COBREXA.reactions(x::LeakyModel) = [reactions(x.mdl); "The Leak"] COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] function COBREXA.bounds(x::LeakyModel) @@ -120,7 +120,7 @@ accessors that depend on correct sizes of the model items. ```julia COBREXA.objective(x::LeakyModel) = [objective(x.mdl); 0] -COBREXA.reaction_flux(x::LeakyModel) = [reaction_flux(x.mdl); zeros(1, n_reactions(x.mdl))] +COBREXA.reaction_flux(x::LeakyModel) = [reaction_flux(x.mdl); zeros(1, reaction_count(x.mdl))] COBREXA.coupling(x::LeakyModel) = [coupling(x.mdl) zeros(n_coupling_constraints(x.mdl))] ``` (Among other, we modified the [`reaction_flux`](@ref) so that all analysis diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index 88f1217c1..587b543f9 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -6,6 +6,6 @@ Pretty printing of everything metabolic-modelish. function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) print( io, - "$(typeof(m))(#= $(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites =#)", + "$(typeof(m))(#= $(reaction_count(m)) reactions, $(n_metabolites(m)) metabolites =#)", ) end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index f35d8455c..cc6c147b2 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -197,8 +197,9 @@ Accessors.reactions(cm::CommunityModel) = [ [envlink.reaction_id for envlink in cm.environmental_links] ] -Accessors.n_reactions(cm::CommunityModel) = - sum(n_reactions(m.model) for m in values(cm.members)) + length(cm.environmental_links) +Accessors.reaction_count(cm::CommunityModel) = + sum(reaction_count(m.model) for m in values(cm.members)) + + length(cm.environmental_links) Accessors.environmental_exchange_variables(model::CommunityModel) = Dict( diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index a4933e6b0..0814ab236 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -78,7 +78,7 @@ function Accessors.stoichiometry(model::SBMLModel)::SparseMat push!(Vals, isnothing(sr.stoichiometry) ? 1.0 : sr.stoichiometry) end end - return sparse(Rows, Cols, Vals, n_metabolites(model), n_reactions(model)) + return sparse(Rows, Cols, Vals, n_metabolites(model), reaction_count(model)) end function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} @@ -137,7 +137,7 @@ end Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) function Accessors.objective(model::SBMLModel)::SparseVec - res = sparsevec([], [], n_reactions(model)) + res = sparsevec([], [], reaction_count(model)) objective = get(model.sbml.objectives, model.active_objective, nothing) if isnothing(objective) && length(model.sbml.objectives) == 1 diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 6e2b83d64..a3bd51a8d 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -86,7 +86,7 @@ end Accessors.reactions(cm::EqualGrowthCommunityModel) = [reactions(cm.inner); cm.community_objective_id] -Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 +Accessors.reaction_count(cm::EqualGrowthCommunityModel) = reaction_count(cm.inner) + 1 Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = environmental_exchange_variables(model.inner) diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index d7931a118..a9cfd0059 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -85,7 +85,7 @@ Accessors.variables(model::MaxMinDrivingForceModel) = ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] Accessors.n_variables(model::MaxMinDrivingForceModel) = - 1 + n_metabolites(model) + n_reactions(model) + 1 + n_metabolites(model) + reaction_count(model) Accessors.metabolite_log_concentrations(model::MaxMinDrivingForceModel) = "log " .* metabolites(model) @@ -96,7 +96,8 @@ Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) Accessors.gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = "ΔG " .* reactions(model) -Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = n_reactions(model) +Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = + reaction_count(model) Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) @@ -165,7 +166,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) dgrs = spdiagm(ones(length(reactions(model)))) S = stoichiometry(model.inner) stoich_mat = -(model.R * model.T) * S' - dg_mat = [spzeros(n_reactions(model)) stoich_mat dgrs] + dg_mat = [spzeros(reaction_count(model)) stoich_mat dgrs] return [ proton_water_mat @@ -206,7 +207,7 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) idxs = Int.(indexin(active_rids, reactions(model))) # thermodynamic sign should correspond to the fluxes - flux_signs = spzeros(length(idxs), n_reactions(model)) + flux_signs = spzeros(length(idxs), reaction_count(model)) for (i, j) in enumerate(idxs) flux_signs[i, j] = sign(model.flux_solution[reactions(model)[j]]) end diff --git a/test/io/mat.jl b/test/io/mat.jl index cd0beed82..b7e90efd7 100644 --- a/test/io/mat.jl +++ b/test/io/mat.jl @@ -17,6 +17,6 @@ end @testset "Import yeast-GEM (mat)" begin m = load_model(ObjectModel, model_paths["yeast-GEM.mat"]) @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 + @test reaction_count(m) == 4063 @test n_genes(m) == 1160 end diff --git a/test/io/sbml.jl b/test/io/sbml.jl index c9e9abd0c..87d88bda8 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -28,6 +28,6 @@ end @testset "Import yeast-GEM (sbml)" begin m = load_model(ObjectModel, model_paths["yeast-GEM.xml"]) @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 + @test reaction_count(m) == 4063 @test n_genes(m) == 1160 end From 159746bb111e49cbfb291ed6cb88efc7e2cbf6c7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 10:59:42 +0200 Subject: [PATCH 278/531] prefer using newer reaction_ids --- docs/src/concepts/3_custom_models.md | 2 +- docs/src/concepts/4_wrappers.md | 2 +- docs/src/quickstart.md | 4 ++-- src/analysis/variability_analysis.jl | 2 +- src/types/misc/ObjectModel.jl | 4 ++-- src/types/models/CommunityModel.jl | 4 ++-- src/types/wrappers/EqualGrowthCommunityModel.jl | 4 ++-- src/wrappers/EnzymeConstrainedModel.jl | 2 +- src/wrappers/MaxMinDrivingForceModel.jl | 14 +++++++------- src/wrappers/SimplifiedEnzymeConstrainedModel.jl | 2 +- src/wrappers/misc/mmdf.jl | 4 ++-- test/io/sbml.jl | 2 +- test/reconstruction/enzyme_constrained.jl | 2 +- .../simplified_enzyme_constrained.jl | 2 +- test/types/CommunityModel.jl | 2 +- test/types/MatrixModel.jl | 4 ++-- 16 files changed, 28 insertions(+), 28 deletions(-) diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index a6836a717..50a1ad45c 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -52,7 +52,7 @@ First, define the reactions and metabolites: COBREXA.reaction_count(m::CircularModel) = m.size COBREXA.n_metabolites(m::CircularModel) = m.size -COBREXA.reactions(m::CircularModel) = ["rxn$i" for i in 1:reaction_count(m)] +COBREXA.reaction_ids(m::CircularModel) = ["rxn$i" for i in 1:reaction_count(m)] COBREXA.metabolites(m::CircularModel) = ["met$i" for i in 1:n_metabolites(m)] ``` diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index aec3f899f..c9aa3eb17 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -107,7 +107,7 @@ modifying the reaction list, stoichiometry, and bounds: ```julia COBREXA.unwrap_model(x::LeakyModel) = x.mdl COBREXA.reaction_count(x::LeakyModel) = reaction_count(x.mdl) + 1 -COBREXA.reactions(x::LeakyModel) = [reactions(x.mdl); "The Leak"] +COBREXA.reaction_ids(x::LeakyModel) = [reaction_ids(x.mdl); "The Leak"] COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] function COBREXA.bounds(x::LeakyModel) (l, u) = bounds(x.mdl) diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md index bffc50d9a..823c27c85 100644 --- a/docs/src/quickstart.md +++ b/docs/src/quickstart.md @@ -114,7 +114,7 @@ res = screen(m, variants=[ # create one variant for each reaction in the model, with that reaction knocked out [with_changed_bound(reaction_id, lower_bound =0.0, upper_bound =0.0)] - for reaction_id in reactions(m) + for reaction_id in reaction_ids(m) ], analysis = model -> begin # we need to check if the optimizer even found a feasible solution, @@ -130,7 +130,7 @@ res = screen(m, In result, you should get a long list of the biomass production for each reaction knockout. Let's decorate it with reaction names: ```julia -Dict(reactions(m) .=> res) +Dict(reaction_ids(m) .=> res) ``` ...which should output an easily accessible dictionary with all the objective values named, giving a quick overview of which reactions are critical for the diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 728843e89..c9a449abc 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -203,7 +203,7 @@ function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer kwargs..., ret = sol -> values_vec(:reaction, ModelWithResult(model, sol)), ) - flxs = reactions(res.model) + flxs = reaction_ids(res.model) dicts = zip.(Ref(flxs), res.result) ModelWithResult( diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl index 3b3ee06de..a1b761462 100644 --- a/src/types/misc/ObjectModel.jl +++ b/src/types/misc/ObjectModel.jl @@ -57,7 +57,7 @@ Return the lower bounds for all reactions in `model`. Order matches that of the reaction IDs returned by [`reactions`](@ref). """ lower_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].lower_bound for rxn in reactions(model)] + [model.reactions[rxn].lower_bound for rxn in reaction_ids(model)] """ $(TYPEDSIGNATURES) @@ -66,4 +66,4 @@ Return the upper bounds for all reactions in `model`. Order matches that of the reaction IDs returned in [`reactions`](@ref). """ upper_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].upper_bound for rxn in reactions(model)] + [model.reactions[rxn].upper_bound for rxn in reaction_ids(model)] diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index cc6c147b2..59076d96a 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -187,10 +187,10 @@ function Accessors.reaction_variables(model::CommunityModel) r_v end -Accessors.reactions(cm::CommunityModel) = [ +Accessors.reaction_ids(cm::CommunityModel) = [ vcat( [ - [cm.name_lookup[id][:reactions][rid] for rid in reactions(m.model)] for + [cm.name_lookup[id][:reactions][rid] for rid in reaction_ids(m.model)] for (id, m) in cm.members ]..., ) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index a3bd51a8d..edeaf437f 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -83,8 +83,8 @@ function Accessors.reaction_variables(cm::EqualGrowthCommunityModel) r_v end -Accessors.reactions(cm::EqualGrowthCommunityModel) = - [reactions(cm.inner); cm.community_objective_id] +Accessors.reaction_ids(cm::EqualGrowthCommunityModel) = + [reaction_ids(cm.inner); cm.community_objective_id] Accessors.reaction_count(cm::EqualGrowthCommunityModel) = reaction_count(cm.inner) + 1 diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 81c9581b3..fa3b0eb76 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -113,7 +113,7 @@ end Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - reactions(model), + reaction_ids(model), reaction_variables_matrix(model), ) # TODO currently inefficient diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index a9cfd0059..fe50cde2b 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -82,7 +82,7 @@ end Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner Accessors.variables(model::MaxMinDrivingForceModel) = - ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] + ["mmdf"; "log " .* metabolites(model); "ΔG " .* reaction_ids(model)] Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + reaction_count(model) @@ -95,11 +95,11 @@ Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) Accessors.gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = - "ΔG " .* reactions(model) + "ΔG " .* reaction_ids(model) Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = reaction_count(model) Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = - Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) + Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reaction_ids(model)) Accessors.objective(model::MaxMinDrivingForceModel) = @@ -119,7 +119,7 @@ function Accessors.balance(model::MaxMinDrivingForceModel) # give dummy dG0 for reactions that don't have data dg0s = [ get(model.reaction_standard_gibbs_free_energies, rid, 0.0) for - rid in reactions(model) + rid in reaction_ids(model) ] return [ @@ -163,7 +163,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) end # add ΔG relationships - dgrs = spdiagm(ones(length(reactions(model)))) + dgrs = spdiagm(ones(length(reaction_ids(model)))) S = stoichiometry(model.inner) stoich_mat = -(model.R * model.T) * S' dg_mat = [spzeros(reaction_count(model)) stoich_mat dgrs] @@ -204,12 +204,12 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) # only constrain reactions that have thermo data active_rids = Internal.active_reaction_ids(model) - idxs = Int.(indexin(active_rids, reactions(model))) + idxs = Int.(indexin(active_rids, reaction_ids(model))) # thermodynamic sign should correspond to the fluxes flux_signs = spzeros(length(idxs), reaction_count(model)) for (i, j) in enumerate(idxs) - flux_signs[i, j] = sign(model.flux_solution[reactions(model)[j]]) + flux_signs[i, j] = sign(model.flux_solution[reaction_ids(model)[j]]) end neg_dg_mat = [ diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index fbd6c1ae4..706dc6cb6 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -97,7 +97,7 @@ wrapped model. Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( variables(model), - reactions(model.inner), + reaction_ids(model.inner), reaction_variables_matrix(model), ) # TODO currently inefficient diff --git a/src/wrappers/misc/mmdf.jl b/src/wrappers/misc/mmdf.jl index 86c3b7c50..fc0c92c04 100644 --- a/src/wrappers/misc/mmdf.jl +++ b/src/wrappers/misc/mmdf.jl @@ -11,7 +11,7 @@ active_reaction_ids(model::MaxMinDrivingForceModel) = filter( abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > model.small_flux_tol && !(rid in model.ignore_reaction_ids), - reactions(model), + reaction_ids(model), ) """ @@ -20,4 +20,4 @@ $(TYPEDSIGNATURES) Helper function that returns the unmangled variable IDs. """ original_variables(model::MaxMinDrivingForceModel) = - ["mmdf"; metabolites(model); reactions(model)] + ["mmdf"; metabolites(model); reaction_ids(model)] diff --git a/test/io/sbml.jl b/test/io/sbml.jl index 87d88bda8..a3508830b 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -11,7 +11,7 @@ @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) @test metabolites(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] - @test reactions(m)[1:3] == ["R_ACALD", "R_ACALDt", "R_ACKr"] + @test reaction_ids(m)[1:3] == ["R_ACALD", "R_ACALDt", "R_ACKr"] cm = convert(MatrixModelWithCoupling, sbmlm) @test n_coupling_constraints(cm) == 0 diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl index 83a791c47..46e269919 100644 --- a/test/reconstruction/enzyme_constrained.jl +++ b/test/reconstruction/enzyme_constrained.jl @@ -8,7 +8,7 @@ model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) # update isozymes with kinetic information - for rid in reactions(model) + for rid in reaction_ids(model) if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(model, rid)) diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl index a0b22fd63..481d5c126 100644 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ b/test/reconstruction/simplified_enzyme_constrained.jl @@ -8,7 +8,7 @@ model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) # update isozymes with kinetic information - for rid in reactions(model) + for rid in reaction_ids(model) if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(model, rid)) diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 9804d4787..6495d9eaa 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -322,7 +322,7 @@ end ecoli.genes["s0001"].product_upper_bound = 10.0 # update isozymes with kinetic information - for rid in reactions(ecoli) + for rid in reaction_ids(ecoli) if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr newisozymes = Isozyme[] for (i, grr) in enumerate(reaction_gene_associations(ecoli, rid)) diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index c6ddfcbec..6ca8c2d46 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -14,6 +14,6 @@ end @test Set(variables(cm)) == Set(variables(sm)) @test Set(variables(cm)) == Set(variables(cm2)) - @test sort(sort.(reaction_gene_associations(sm, reactions(sm)[1]))) == - sort(sort.(reaction_gene_associations(cm, reactions(sm)[1]))) + @test sort(sort.(reaction_gene_associations(sm, reaction_ids(sm)[1]))) == + sort(sort.(reaction_gene_associations(cm, reaction_ids(sm)[1]))) end From f2b964fb767ec847fc97ec5fa7f9166472ed1dbc Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:07:02 +0200 Subject: [PATCH 279/531] rename enzyme accessors --- src/types/models/CommunityModel.jl | 7 ++++--- src/types/wrappers/EqualGrowthCommunityModel.jl | 4 ++-- src/wrappers/EnzymeConstrainedModel.jl | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 59076d96a..8129790f9 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -225,10 +225,11 @@ function Accessors.enzyme_variables(model::CommunityModel) e_v end -Accessors.enzymes(cm::CommunityModel) = - [cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in enzymes(m.model)] +Accessors.enzyme_ids(cm::CommunityModel) = [ + cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in enzyme_ids(m.model) +] -Accessors.n_enzymes(cm::CommunityModel) = sum(n_enzymes(m.model) for m in cm.members) +Accessors.enzyme_count(cm::CommunityModel) = sum(enzyme_count(m.model) for m in cm.members) """ $(TYPEDSIGNATURES) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index edeaf437f..f9abe7a1b 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -99,9 +99,9 @@ Accessors.n_environmental_exchanges(model::EqualGrowthCommunityModel) = Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) -Accessors.enzymes(model::EqualGrowthCommunityModel) = enzymes(model.inner) +Accessors.enzyme_ids(model::EqualGrowthCommunityModel) = enzyme_ids(model.inner) -Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) +Accessors.enzyme_count(model::EqualGrowthCommunityModel) = enzyme_count(model.inner) Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = enzyme_group_variables(model.inner) diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index fa3b0eb76..479e50967 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -117,9 +117,9 @@ Accessors.reaction_variables(model::EnzymeConstrainedModel) = reaction_variables_matrix(model), ) # TODO currently inefficient -Accessors.enzymes(model::EnzymeConstrainedModel) = genes(model) +Accessors.enzyme_ids(model::EnzymeConstrainedModel) = genes(model) -Accessors.n_enzymes(model::EnzymeConstrainedModel) = n_genes(model) +Accessors.enzyme_count(model::EnzymeConstrainedModel) = n_genes(model) Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) From fdd2b8fad957c53789d6f8a70f9accf31ef61aae Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:07:54 +0200 Subject: [PATCH 280/531] rename enzyme groups accessors --- src/types/models/CommunityModel.jl | 8 ++++---- src/types/wrappers/EqualGrowthCommunityModel.jl | 5 +++-- src/wrappers/EnzymeConstrainedModel.jl | 4 ++-- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 8129790f9..8abd47168 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -248,11 +248,11 @@ function Accessors.enzyme_group_variables(model::CommunityModel) e_g_v end -Accessors.enzyme_groups(cm::CommunityModel) = - [id * "#" * k for (id, m) in cm.members for k in enzyme_groups(m.model)] +Accessors.enzyme_group_ids(cm::CommunityModel) = + [id * "#" * k for (id, m) in cm.members for k in enzyme_group_ids(m.model)] -Accessors.n_enzyme_groups(cm::CommunityModel) = - sum(n_enzyme_groups(m.model) for m in cm.members) +Accessors.enzyme_group_count(cm::CommunityModel) = + sum(enzyme_group_count(m.model) for m in cm.members) #= This loops implements the rest of the accessors through access_community_member. diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index f9abe7a1b..831415202 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -106,6 +106,7 @@ Accessors.enzyme_count(model::EqualGrowthCommunityModel) = enzyme_count(model.in Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = enzyme_group_variables(model.inner) -Accessors.enzyme_groups(model::EqualGrowthCommunityModel) = enzyme_groups(model.inner) +Accessors.enzyme_group_ids(model::EqualGrowthCommunityModel) = enzyme_group_ids(model.inner) -Accessors.n_enzyme_groups(model::EqualGrowthCommunityModel) = n_enzyme_groups(model.inner) +Accessors.enzyme_group_count(model::EqualGrowthCommunityModel) = + enzyme_group_count(model.inner) diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 479e50967..aced3e1bf 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -124,9 +124,9 @@ Accessors.enzyme_count(model::EnzymeConstrainedModel) = n_genes(model) Accessors.enzyme_variables(model::EnzymeConstrainedModel) = Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) -Accessors.enzyme_groups(model::EnzymeConstrainedModel) = +Accessors.enzyme_group_ids(model::EnzymeConstrainedModel) = [grp.group_id for grp in model.coupling_row_mass_group] -Accessors.n_enzyme_groups(model::EnzymeConstrainedModel) = +Accessors.enzyme_group_count(model::EnzymeConstrainedModel) = length(model.coupling_row_mass_group) function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) From c6aeeeac274c5d7fe570ccb2dc912d6406dbbe90 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:10:45 +0200 Subject: [PATCH 281/531] rename logcentration accessors --- src/wrappers/MaxMinDrivingForceModel.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index fe50cde2b..90f7bb4c6 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -87,9 +87,9 @@ Accessors.variables(model::MaxMinDrivingForceModel) = Accessors.n_variables(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + reaction_count(model) -Accessors.metabolite_log_concentrations(model::MaxMinDrivingForceModel) = +Accessors.metabolite_log_concentration_ids(model::MaxMinDrivingForceModel) = "log " .* metabolites(model) -Accessors.n_metabolite_log_concentrations(model::MaxMinDrivingForceModel) = +Accessors.metabolite_log_concentration_count(model::MaxMinDrivingForceModel) = n_metabolites(model) Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) From 545585b883f236c5c3a74cde651c925139de32e1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:12:09 +0200 Subject: [PATCH 282/531] rename gibbs free energy accessors --- src/wrappers/MaxMinDrivingForceModel.jl | 4 ++-- test/analysis/max_min_driving_force.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index 90f7bb4c6..7c2b35e1b 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -94,9 +94,9 @@ Accessors.metabolite_log_concentration_count(model::MaxMinDrivingForceModel) = Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) -Accessors.gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = +Accessors.gibbs_free_energy_reaction_ids(model::MaxMinDrivingForceModel) = "ΔG " .* reaction_ids(model) -Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = +Accessors.gibbs_free_energy_reaction_count(model::MaxMinDrivingForceModel) = reaction_count(model) Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reaction_ids(model)) diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 1e087a262..97a9ed5ad 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -47,6 +47,6 @@ modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> result - pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reactions(mmdfm))) + pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reaction_ids(mmdfm))) @test isapprox(sols[pyk_idx, 2], -1.5895040002691128; atol = TEST_TOLERANCE) end From c6bd4a52cd53985569e79c6a5fe24e7c4a130203 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:14:35 +0200 Subject: [PATCH 283/531] clean up Gibbs free energy naming (the previous forced pluralization didn't turn out very good) --- src/types/accessors/AbstractMetabolicModel.jl | 4 ++-- src/wrappers/MaxMinDrivingForceModel.jl | 7 +++---- test/analysis/max_min_driving_force.jl | 6 +++--- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 9f7dd79ef..f0f6ca74b 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -152,8 +152,8 @@ to make thermodynamic calculations easier. ) @make_variable_semantics( - :gibbs_free_energy_reaction, - "Gibbs free energy of reaction", + :gibbs_free_energy, + "Gibbs free energies", """ Some thermodynamic models need to ensure that the ΔG of each reaction is negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index 7c2b35e1b..881bd6831 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -94,11 +94,10 @@ Accessors.metabolite_log_concentration_count(model::MaxMinDrivingForceModel) = Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) -Accessors.gibbs_free_energy_reaction_ids(model::MaxMinDrivingForceModel) = +Accessors.gibbs_free_energy_ids(model::MaxMinDrivingForceModel) = "ΔG " .* reaction_ids(model) -Accessors.gibbs_free_energy_reaction_count(model::MaxMinDrivingForceModel) = - reaction_count(model) -Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = +Accessors.gibbs_free_energy_count(model::MaxMinDrivingForceModel) = reaction_count(model) +Accessors.gibbs_free_energy_variables(model::MaxMinDrivingForceModel) = Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reaction_ids(model)) diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl index 97a9ed5ad..9b542cfda 100644 --- a/test/analysis/max_min_driving_force.jl +++ b/test/analysis/max_min_driving_force.jl @@ -36,17 +36,17 @@ # values_dict(:reaction, mmdfm, opt_model) # TODO throw missing semantics error @test length(x |> values_dict(:metabolite_log_concentration)) == 72 - @test length(x |> values_dict(:gibbs_free_energy_reaction)) == 95 + @test length(x |> values_dict(:gibbs_free_energy)) == 95 sols = variability_analysis( - :gibbs_free_energy_reaction, + :gibbs_free_energy, mmdfm, Tulip.Optimizer; bounds = gamma_bounds(0.9), modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], ) |> result - pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reaction_ids(mmdfm))) + pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_ids(mmdfm))) @test isapprox(sols[pyk_idx, 2], -1.5895040002691128; atol = TEST_TOLERANCE) end From 210cab19079de8a777ba673b55a764768011fe68 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:15:15 +0200 Subject: [PATCH 284/531] a little extra naming cleanup (mainly docstring contents) --- src/types/accessors/AbstractMetabolicModel.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index f0f6ca74b..7b777b434 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -133,7 +133,7 @@ these values. @make_variable_semantics( :enzyme_group, - "enzyme group", + "enzyme groups", """ Certain model types use enzymes to catalyze reactions. These enzymes typically have capacity limitations (e.g. membrane or cytosol density constraints). Enzyme @@ -143,7 +143,7 @@ groups collect these sets of enzymes for convenient analysis. @make_variable_semantics( :metabolite_log_concentration, - "metabolite log concentration", + "metabolite log-concentrations", """ Certain model types use metabolite concentrations instead of reaction fluxes are variables. This semantic grouping uses the log (base e) metabolite concentration @@ -162,7 +162,7 @@ negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. @make_variable_semantics( :environmental_exchange, - "Environmental exchange reaction", + "environmental exchanges", """ Community models are composed of member models as well as environmental exchange reactions. This semantic grouping represents the environmental exchange From c7241909ebb0cdf673796589bafdb63018660250 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:17:09 +0200 Subject: [PATCH 285/531] rename environmental exchange accessors --- src/types/models/CommunityModel.jl | 4 ++-- src/types/wrappers/EqualGrowthCommunityModel.jl | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 8abd47168..903badb30 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -207,10 +207,10 @@ Accessors.environmental_exchange_variables(model::CommunityModel) = Dict( rid in [envlink.reaction_id for envlink in model.environmental_links] ) -Accessors.environmental_exchanges(model::CommunityModel) = +Accessors.environmental_exchange_ids(model::CommunityModel) = [envlink.reaction_id for envlink in model.environmental_links] -Accessors.n_environmental_exchanges(model::CommunityModel) = +Accessors.environmental_exchange_count(model::CommunityModel) = length(model.environmental_links) function Accessors.enzyme_variables(model::CommunityModel) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 831415202..ff9ad04a4 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -91,11 +91,11 @@ Accessors.reaction_count(cm::EqualGrowthCommunityModel) = reaction_count(cm.inne Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = environmental_exchange_variables(model.inner) -Accessors.environmental_exchanges(model::EqualGrowthCommunityModel) = - environmental_exchanges(model.inner) +Accessors.environmental_exchange_ids(model::EqualGrowthCommunityModel) = + environmental_exchange_ids(model.inner) -Accessors.n_environmental_exchanges(model::EqualGrowthCommunityModel) = - n_environmental_exchanges(model.inner) +Accessors.environmental_exchange_count(model::EqualGrowthCommunityModel) = + environmental_exchange_count(model.inner) Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) From 0288d472fce607d0d8d975474c20d785e11da716 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 11:28:48 +0200 Subject: [PATCH 286/531] reduce accidental metaprogramming in community models --- src/types/misc/CommunityModel.jl | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index 6e119eeb5..2fd4bfc04 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -125,12 +125,16 @@ function build_community_name_lookup( members::OrderedDict{String,CommunityMember}; delim = "#", ) - accessors = [variables, reactions, metabolites, genes] + accessors = [ + :variables => variables, + :reactions => reaction_ids, + :metabolites => metabolites, + :genes => genes, + ] Dict( id => Dict( - Symbol(accessor) => - Dict(k => id * delim * k for k in accessor(member.model)) for - accessor in accessors + accessorname => Dict(k => id * delim * k for k in accessor(member.model)) + for (accessorname, accessor) in accessors ) for (id, member) in members ) end From 826eaae3c945a0d3b791f7331609e403a72767db Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 12:28:26 +0200 Subject: [PATCH 287/531] rename variable accessors to hold with the rest of _ids and _count logic --- docs/src/examples/04_standardmodel.jl | 2 +- docs/src/examples/06_fva.jl | 2 +- docs/src/examples/11_growth.jl | 2 +- .../14_simplified_enzyme_constrained.jl | 2 +- docs/src/examples/15_enzyme_constrained.jl | 2 +- docs/src/examples/16_hit_and_run.jl | 2 +- src/analysis/envelopes.jl | 4 +- src/analysis/minimize_metabolic_adjustment.jl | 2 +- src/analysis/modifications/community.jl | 2 +- src/analysis/modifications/generic.jl | 13 ++++--- src/analysis/modifications/knockout.jl | 2 +- src/analysis/modifications/loopless.jl | 2 +- .../gapfill_minimum_reactions.jl | 4 +- src/analysis/sampling/affine_hit_and_run.jl | 4 +- src/analysis/sampling/warmup_variability.jl | 6 +-- src/analysis/variability_analysis.jl | 4 +- src/io/h5.jl | 2 +- src/io/show/CommunityModel.jl | 2 +- src/reconstruction/MatrixCoupling.jl | 26 ++++++------- src/reconstruction/MatrixModel.jl | 16 ++++---- src/reconstruction/enzyme_constrained.jl | 4 +- .../simplified_enzyme_constrained.jl | 6 +-- src/solver.jl | 3 +- src/types/accessors/AbstractMetabolicModel.jl | 23 ++++++----- src/types/accessors/bits/semantics.jl | 12 +++--- src/types/misc/CommunityModel.jl | 4 +- src/types/models/CommunityModel.jl | 12 +++--- src/types/models/HDF5Model.jl | 4 +- src/types/models/JSONModel.jl | 10 ++--- src/types/models/MATModel.jl | 32 ++++++++-------- src/types/models/MatrixModel.jl | 6 +-- src/types/models/ObjectModel.jl | 12 +++--- src/types/models/SBMLModel.jl | 4 +- .../wrappers/EqualGrowthCommunityModel.jl | 10 ++--- src/utils/fluxes.jl | 4 +- src/utils/looks_like.jl | 20 +++++----- src/wrappers/EnzymeConstrainedModel.jl | 10 ++--- src/wrappers/MatrixCoupling.jl | 9 ++++- src/wrappers/MaxMinDrivingForceModel.jl | 16 ++++---- src/wrappers/MinimizeDistance.jl | 2 +- .../SimplifiedEnzymeConstrainedModel.jl | 10 ++--- src/wrappers/misc/enzyme_constrained.jl | 4 +- test/analysis/envelopes.jl | 4 +- test/analysis/flux_balance_analysis.jl | 6 +-- .../analysis/minimize_metabolic_adjustment.jl | 2 +- test/analysis/sampling/affine_hit_and_run.jl | 4 +- test/analysis/sampling/warmup_variability.jl | 2 +- test/io/h5.jl | 8 ++-- test/io/io.jl | 16 ++++---- test/io/json.jl | 2 +- test/io/sbml.jl | 2 +- test/reconstruction/MatrixCoupling.jl | 4 +- test/reconstruction/MatrixModel.jl | 8 ++-- test/reconstruction/SerializedModel.jl | 4 +- test/types/CommunityModel.jl | 6 +-- test/types/JSONModel.jl | 4 +- test/types/MATModel.jl | 4 +- test/types/MatrixCoupling.jl | 8 ++-- test/types/MatrixModel.jl | 4 +- test/types/ObjectModel.jl | 10 ++--- test/types/SBMLModel.jl | 8 ++-- test/types/abstract/AbstractMetabolicModel.jl | 2 +- test/utils/MatrixModel.jl | 2 +- test/utils/ObjectModel.jl | 6 +-- test/utils/Serialized.jl | 4 +- test/utils/looks_like.jl | 38 +++++++++---------- 66 files changed, 244 insertions(+), 232 deletions(-) diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl index aac2f756a..28c2c1baf 100644 --- a/docs/src/examples/04_standardmodel.jl +++ b/docs/src/examples/04_standardmodel.jl @@ -79,7 +79,7 @@ model.genes[random_gene_id] random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] model.metabolites[random_metabolite_id] # -random_reaction_id = variables(model)[rand(1:n_variables(model))] +random_reaction_id = variable_ids(model)[rand(1:variable_count(model))] model.reactions[random_reaction_id] # `ObjectModel` can be used to build your own metabolic model or modify an diff --git a/docs/src/examples/06_fva.jl b/docs/src/examples/06_fva.jl index db90d96db..0d0f4cd5d 100644 --- a/docs/src/examples/06_fva.jl +++ b/docs/src/examples/06_fva.jl @@ -71,7 +71,7 @@ flux_variability_summary((min_fluxes, max_fluxes)) # biomass "growth" along with the minimized/maximized reaction flux. # First, find the index of biomass reaction in all reactions -biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], variables(model))) +biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], variable_ids(model))) # Now run the FVA: vs = flux_variability_analysis( diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl index f2c87620c..c022acf8a 100644 --- a/docs/src/examples/11_growth.jl +++ b/docs/src/examples/11_growth.jl @@ -58,7 +58,7 @@ flux_summary( # The effect of all nutrients on the metabolism can be scanned using [`screen`](@ref). The [`change_bound`](@ref) function is, for this purpose, packed in a variant specified [`with_changed_bound`](@ref): -exchanges = filter(looks_like_exchange_reaction, variables(model)) +exchanges = filter(looks_like_exchange_reaction, variable_ids(model)) exchanges .=> screen( model, diff --git a/docs/src/examples/14_simplified_enzyme_constrained.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl index 6a61e7e3a..6bfc3fa00 100644 --- a/docs/src/examples/14_simplified_enzyme_constrained.jl +++ b/docs/src/examples/14_simplified_enzyme_constrained.jl @@ -37,7 +37,7 @@ rxns = filter( !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && !isnothing(reaction_gene_associations(model, x)), - variables(model), + variable_ids(model), ) # The information about each enzyme and its capabilities is stored in an diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl index ae0b595e9..70ea5a04d 100644 --- a/docs/src/examples/15_enzyme_constrained.jl +++ b/docs/src/examples/15_enzyme_constrained.jl @@ -35,7 +35,7 @@ rxns = filter( !looks_like_biomass_reaction(x) && !looks_like_exchange_reaction(x) && !isnothing(reaction_gene_associations(model, x)), - variables(model), + variable_ids(model), ) # The main difference from sMOMENT comes from allowing multiple isozymes per diff --git a/docs/src/examples/16_hit_and_run.jl b/docs/src/examples/16_hit_and_run.jl index b147ed2cf..569417f50 100644 --- a/docs/src/examples/16_hit_and_run.jl +++ b/docs/src/examples/16_hit_and_run.jl @@ -64,7 +64,7 @@ samples = affine_hit_and_run(model, warmup_points, sample_iters = 201:210, chain using CairoMakie -o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], variables(model)) +o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], variable_ids(model)) scatter( samples[o2, :], diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl index 5606ae3a9..1cc68a7bf 100644 --- a/src/analysis/envelopes.jl +++ b/src/analysis/envelopes.jl @@ -6,7 +6,7 @@ Version of [`envelope_lattice`](@ref) that works on string reaction IDs instead of integer indexes. """ envelope_lattice(model::AbstractMetabolicModel, rids::Vector{String}; kwargs...) = - envelope_lattice(model, Vector{Int}(indexin(rids, variables(model))); kwargs...) + envelope_lattice(model, Vector{Int}(indexin(rids, variable_ids(model))); kwargs...) """ $(TYPEDSIGNATURES) @@ -39,7 +39,7 @@ objective_envelope( kwargs..., ) = objective_envelope( model, - Vector{Int}(indexin(rids, variables(model))), + Vector{Int}(indexin(rids, variable_ids(model))), args...; kwargs..., ) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl index 52aa16ab6..c16c07c4f 100644 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ b/src/analysis/minimize_metabolic_adjustment.jl @@ -3,7 +3,7 @@ $(TYPEDSIGNATURES) Run minimization of metabolic adjustment (MOMA) on `model` with respect to `reference_flux`, which is a vector of fluxes in the order of -`variables(model)`. MOMA finds the shortest Euclidian distance between +`variable_ids(model)`. MOMA finds the shortest Euclidian distance between `reference_flux` and `model` with `modifications`: ``` min Σᵢ (xᵢ - flux_refᵢ)² diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl index dd4cbd476..8fbfb73a7 100644 --- a/src/analysis/modifications/community.jl +++ b/src/analysis/modifications/community.jl @@ -27,7 +27,7 @@ modify_abundances(new_abundances::Vector{Float64}) = environment_exchange_stoichiometry(model.inner, new_abundances) env_link = spdiagm(sum(env_rows, dims = 2)[:]) - n_vars = n_variables(model) + n_vars = variable_count(model) n_env_vars = length(model.environmental_links) n_cons = length(opt_model[:mb]) n_objs = model isa CommunityModel ? 0 : length(model.inner.members) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl index 5936276fc..26361af09 100644 --- a/src/analysis/modifications/generic.jl +++ b/src/analysis/modifications/generic.jl @@ -20,7 +20,7 @@ of reaction `id` if supplied. """ modify_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = (model, opt_model) -> begin - ind = first(indexin([id], variables(model))) + ind = first(indexin([id], variable_ids(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) set_optmodel_bound!( ind, @@ -43,7 +43,7 @@ modify_constraints( ) = (model, opt_model) -> begin for (id, lb, ub) in zip(ids, lower_bounds, upper_bounds) - ind = first(indexin([id], variables(model))) + ind = first(indexin([id], variable_ids(model))) isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) set_optmodel_bound!(ind, opt_model, lower_bound = lb, upper_bound = ub) end @@ -70,10 +70,11 @@ modify_objective( # Construct objective_indices array if typeof(new_objective) == String - objective_indices = indexin([new_objective], variables(model)) + objective_indices = indexin([new_objective], variable_ids(model)) else - objective_indices = - [first(indexin([rxnid], variables(model))) for rxnid in new_objective] + objective_indices = [ + first(indexin([rxnid], variable_ids(model))) for rxnid in new_objective + ] end any(isnothing.(objective_indices)) && throw( @@ -81,7 +82,7 @@ modify_objective( ) # Initialize weights - opt_weights = spzeros(n_variables(model)) + opt_weights = spzeros(variable_count(model)) isempty(weights) && (weights = ones(length(objective_indices))) # equal weights diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl index 6612ecd24..8ae23ba95 100644 --- a/src/analysis/modifications/knockout.jl +++ b/src/analysis/modifications/knockout.jl @@ -36,7 +36,7 @@ other models. function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) #TODO this should preferably work on reactions. Make it a wrapper. KOs = Set(gene_ids) - for (ridx, rid) in enumerate(variables(model)) + for (ridx, rid) in enumerate(variable_ids(model)) if eval_reaction_gene_association(model, rid, falses = KOs) == false # also tests for nothing! set_optmodel_bound!(ridx, opt_model, lower_bound = 0, upper_bound = 0) end diff --git a/src/analysis/modifications/loopless.jl b/src/analysis/modifications/loopless.jl index 8c17e57a4..89f85d753 100644 --- a/src/analysis/modifications/loopless.jl +++ b/src/analysis/modifications/loopless.jl @@ -28,7 +28,7 @@ add_loopless_constraints(; (model, opt_model) -> begin internal_rxn_idxs = [ - ridx for (ridx, rid) in enumerate(variables(model)) if + ridx for (ridx, rid) in enumerate(variable_ids(model)) if !is_boundary(reaction_stoichiometry(model, rid)) ] diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl index f2cfd0c5f..76e88a776 100644 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ b/src/analysis/reconstruction/gapfill_minimum_reactions.jl @@ -60,7 +60,7 @@ function gapfill_minimum_reactions( # stoichiometry extended_stoichiometry = [[ stoichiometry(model) - spzeros(length(univs.new_mids), n_variables(model)) + spzeros(length(univs.new_mids), variable_count(model)) ] univs.stoichiometry] # make the model anew (we can't really use make_optimization_model because @@ -69,7 +69,7 @@ function gapfill_minimum_reactions( # tiny temporary wrapper for this. # keep this in sync with src/base/solver.jl, except for adding balances. opt_model = Model(optimizer) - @variable(opt_model, x[1:n_variables(model)]) + @variable(opt_model, x[1:variable_count(model)]) xl, xu = bounds(model) @constraint(opt_model, lbs, xl .<= x) @constraint(opt_model, ubs, x .<= xu) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index 072ff0d09..b78afbf93 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -38,13 +38,13 @@ function affine_hit_and_run( chains = length(workers), seed = rand(Int), ) - @assert size(warmup_points, 1) == n_variables(m) + @assert size(warmup_points, 1) == variable_count(m) lbs, ubs = bounds(m) C = coupling(m) cl, cu = coupling_bounds(m) if isnothing(C) - C = zeros(0, n_variables(m)) + C = zeros(0, variable_count(m)) cl = zeros(0) cu = zeros(0) end diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl index 9c6b8c3f0..50ab4e198 100644 --- a/src/analysis/sampling/warmup_variability.jl +++ b/src/analysis/sampling/warmup_variability.jl @@ -12,7 +12,7 @@ function warmup_from_variability( seed = rand(Int); kwargs..., ) - nr = n_variables(model) + nr = variable_count(model) n_points > 2 * nr && throw( DomainError( @@ -48,8 +48,8 @@ single column in the result. function warmup_from_variability( model::AbstractMetabolicModel, optimizer, - min_reactions::AbstractVector{Int} = 1:n_variables(model), - max_reactions::AbstractVector{Int} = 1:n_variables(model); + min_reactions::AbstractVector{Int} = 1:variable_count(model), + max_reactions::AbstractVector{Int} = 1:variable_count(model); modifications = [], workers::Vector{Int} = [myid()], )::Matrix{Float64} diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index c9a449abc..99dce7e5c 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -120,14 +120,14 @@ reason. function variability_analysis( model::AbstractMetabolicModel, optimizer; - directions::SparseMat = spdiagm(fill(1.0, n_variables(model))), + directions::SparseMat = spdiagm(fill(1.0, variable_count(model))), modifications = [], workers = [myid()], optimal_objective_value = nothing, bounds = z -> (z, Inf), ret = objective_value, ) - if size(directions, 1) != n_variables(model) + if size(directions, 1) != variable_count(model) throw( DomainError( size(directions, 1), diff --git a/src/io/h5.jl b/src/io/h5.jl index b71e05817..2048a6ecd 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -21,7 +21,7 @@ disk storage, writing the data to disk (using this function) is the only way to make new HDF5 models. """ function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Model - rxns = variables(model) + rxns = variable_ids(model) rxnp = sortperm(rxns) mets = metabolites(model) metp = sortperm(mets) diff --git a/src/io/show/CommunityModel.jl b/src/io/show/CommunityModel.jl index 9d8371d76..581cef2e2 100644 --- a/src/io/show/CommunityModel.jl +++ b/src/io/show/CommunityModel.jl @@ -1,5 +1,5 @@ function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println(io, "A community member with $(n_variables(cm.model)) internal variables.") + println(io, "A community member with $(variable_count(cm.model)) internal variables.") end function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityModel) diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 272d74141..24d00c3ef 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -16,7 +16,7 @@ function add_reactions( new_lm = add_reactions(m.lm, s, b, c, xl, xu, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), variable_count(new_lm) - variable_count(m.lm))), m.cl, m.cu, ) @@ -50,7 +50,7 @@ function add_reactions( ) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), variable_count(new_lm) - variable_count(m.lm))), m.cl, m.cu, ) @@ -71,7 +71,7 @@ function add_reactions( new_lm = add_reactions(m.lm, Sp, b, c, xl, xu, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), variable_count(new_lm) - variable_count(m.lm))), m.cl, m.cu, ) @@ -90,7 +90,7 @@ function add_reactions( new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) return MatrixModelWithCoupling( new_lm, - hcat(m1.C, spzeros(size(m1.C, 1), n_variables(new_lm) - n_variables(m1.lm))), + hcat(m1.C, spzeros(size(m1.C, 1), variable_count(new_lm) - variable_count(m1.lm))), m1.cl, m1.cu, ) @@ -123,7 +123,7 @@ function add_reactions( ) return MatrixModelWithCoupling( new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), + hcat(m.C, spzeros(size(m.C, 1), variable_count(new_lm) - variable_count(m.lm))), m.cl, m.cu, ) @@ -182,7 +182,7 @@ function add_coupling_constraints!( all([length(cu), length(cl)] .== size(C, 1)) || throw(DimensionMismatch("mismatched numbers of constraints")) - size(C, 2) == n_variables(m) || + size(C, 2) == variable_count(m) || throw(DimensionMismatch("mismatched number of reactions")) m.C = vcat(m.C, sparse(C)) @@ -304,9 +304,9 @@ end end @_remove_fn reaction MatrixCoupling Int inplace plural begin - orig_rxns = variables(model.lm) + orig_rxns = variable_ids(model.lm) remove_reactions!(model.lm, reaction_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] + model.C = model.C[:, in.(orig_rxns, Ref(Set(variable_ids(model.lm))))] nothing end @@ -317,7 +317,7 @@ end @_remove_fn reaction MatrixCoupling Int plural begin n = copy(model) n.lm = remove_reactions(n.lm, reaction_idxs) - n.C = n.C[:, in.(variables(model.lm), Ref(Set(variables(n.lm))))] + n.C = n.C[:, in.(variable_ids(model.lm), Ref(Set(variable_ids(n.lm))))] return n end @@ -326,7 +326,7 @@ end end @_remove_fn reaction MatrixCoupling String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) + remove_reactions!(model, Int.(indexin(reaction_ids, variable_ids(model)))) end @_remove_fn reaction MatrixCoupling String begin @@ -334,7 +334,7 @@ end end @_remove_fn reaction MatrixCoupling String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) + remove_reactions(model, Int.(indexin(reaction_ids, variable_ids(model)))) end @_remove_fn metabolite MatrixCoupling Int inplace begin @@ -342,9 +342,9 @@ end end @_remove_fn metabolite MatrixCoupling Int plural inplace begin - orig_rxns = variables(model.lm) + orig_rxns = variable_ids(model.lm) model.lm = remove_metabolites(model.lm, metabolite_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] + model.C = model.C[:, in.(orig_rxns, Ref(Set(variable_ids(model.lm))))] nothing end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index a2914254a..1a661cdf6 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -180,7 +180,7 @@ function add_reactions( new_mets = vcat(m.mets, mets[new_metabolites]) - zero_block = spzeros(length(new_metabolites), n_variables(m)) + zero_block = spzeros(length(new_metabolites), variable_count(m)) ext_s = vcat(sparse(m.S), zero_block) mapping = [findfirst(isequal(met), new_mets) for met in mets] @@ -294,7 +294,7 @@ end @_change_bounds_fn MatrixModel String inplace plural begin change_bounds!( model, - Vector{Int}(indexin(rxn_ids, variables(model))); + Vector{Int}(indexin(rxn_ids, variable_ids(model))); lower_bounds, upper_bounds, ) @@ -312,7 +312,7 @@ end @_change_bounds_fn MatrixModel String plural begin change_bounds( model, - Int.(indexin(rxn_ids, variables(model))); + Int.(indexin(rxn_ids, variable_ids(model))); lower_bounds, upper_bounds, ) @@ -323,7 +323,7 @@ end end @_remove_fn reaction MatrixModel Int inplace plural begin - mask = .!in.(1:n_variables(model), Ref(reaction_idxs)) + mask = .!in.(1:variable_count(model), Ref(reaction_idxs)) model.S = model.S[:, mask] model.c = model.c[mask] model.xl = model.xl[mask] @@ -347,7 +347,7 @@ end end @_remove_fn reaction MatrixModel String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) + remove_reactions!(model, Int.(indexin(reaction_ids, variable_ids(model)))) end @_remove_fn reaction MatrixModel String begin @@ -355,7 +355,7 @@ end end @_remove_fn reaction MatrixModel String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) + remove_reactions(model, Int.(indexin(reaction_ids, variable_ids(model)))) end @_remove_fn metabolite MatrixModel Int inplace begin @@ -366,7 +366,7 @@ end remove_reactions!( model, [ - ridx for ridx = 1:n_variables(model) if + ridx for ridx = 1:variable_count(model) if any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) ], ) @@ -438,7 +438,7 @@ function change_objective!( rxn_ids::Vector{String}; weights = ones(length(rxn_ids)), ) - idxs = indexin(rxn_ids, variables(model)) + idxs = indexin(rxn_ids, variable_ids(model)) any(isnothing(idx) for idx in idxs) && throw(DomainError(rxn_ids, "Some reaction ids not found in the model")) change_objective!(model, Int.(idxs); weights) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index b1c5407a4..8804c7470 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -62,12 +62,12 @@ function make_enzyme_constrained_model( gids = genes(model) (lbs, ubs) = bounds(model) - rids = variables(model) + rids = variable_ids(model) gene_name_lookup = Dict(gids .=> 1:length(gids)) gene_row_lookup = Dict{Int,Int}() - for i = 1:n_variables(model) + for i = 1:variable_count(model) isozymes = reaction_isozymes(model, rids[i]) if isnothing(isozymes) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 911b7fa54..a23c31c6f 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -46,7 +46,7 @@ function make_simplified_enzyme_constrained_model( ) # fix kwarg inputs if !isnothing(total_reaction_mass_bound) - reaction_mass_groups = Dict("uncategorized" => variables(model)) # TODO should be reactions + reaction_mass_groups = Dict("uncategorized" => variable_ids(model)) # TODO should be reactions reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) end isnothing(reaction_mass_groups) && @@ -74,9 +74,9 @@ function make_simplified_enzyme_constrained_model( total_reaction_mass_bounds = collect(values(reaction_mass_group_bounds)) (lbs, ubs) = bounds(model) # TODO need a reaction_bounds accessor for full generality - rids = variables(model) # TODO needs to be reactions + rids = variable_ids(model) # TODO needs to be reactions - for i = 1:n_variables(model) # TODO this should be reactions + for i = 1:variable_count(model) # TODO this should be reactions isozyme = ris_(model, rids[i]) diff --git a/src/solver.jl b/src/solver.jl index a96493108..aa8c0a3eb 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -181,7 +181,8 @@ flux_balance_analysis(model, ...) |> values_dict ``` """ function values_dict(res::ModelWithResult{<:Model}) - is_solved(res.result) ? Dict(variables(res.model) .=> value.(res.result[:x])) : nothing + is_solved(res.result) ? Dict(variable_ids(res.model) .=> value.(res.result[:x])) : + nothing end """ diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 7b777b434..8aefbb090 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -22,17 +22,22 @@ exchanges, separate forward and reverse reactions, supplies of enzymatic and genetic material and virtual cell volume, etc. To simplify the view of the model contents use [`reaction_variables`](@ref). """ -function variables(a::AbstractMetabolicModel)::Vector{String} +function variable_ids(a::AbstractMetabolicModel)::Vector{String} missing_impl_error(variables, (a,)) end +""" +Shortcut for writing [`variable_ids`](@ref). +""" +const variables = variable_ids + """ $(TYPEDSIGNATURES) Get the number of reactions in a model. """ -function n_variables(a::AbstractMetabolicModel)::Int - length(variables(a)) +function variable_count(a::AbstractMetabolicModel)::Int + length(variable_ids(a)) end """ @@ -164,9 +169,9 @@ negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. :environmental_exchange, "environmental exchanges", """ -Community models are composed of member models as well as environmental exchange -reactions. This semantic grouping represents the environmental exchange -reactions. +Community models are composed of member models as well as environmental +exchange reactions. This semantic grouping represents the environmental +exchange reactions. """ ) @@ -177,7 +182,7 @@ Get a matrix of coupling constraint definitions of a model. By default, there is no coupling in the models. """ function coupling(a::AbstractMetabolicModel)::SparseMat - return spzeros(0, n_variables(a)) + return spzeros(0, variable_count(a)) end """ @@ -299,8 +304,8 @@ function reaction_stoichiometry( )::Dict{String,Float64} mets = metabolites(m) Dict( - mets[k] => v for - (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], variables(m)))])...) + mets[k] => v for (k, v) in + zip(findnz(stoichiometry(m)[:, first(indexin([rid], variable_ids(m)))])...) ) end diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index f707a5099..fe1076222 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -217,7 +217,7 @@ safety reasons, this is never automatically inherited by wrappers. """, ), :(function $mapping_mtx(a::AbstractMetabolicModel)::SparseMat - make_mapping_mtx(variables(a), $ids(a), $mapping(a)) + make_mapping_mtx(variable_ids(a), $ids(a), $mapping(a)) end), ) @@ -242,7 +242,7 @@ with lower and upper bounds. Base.eval.(Ref(themodule), [idsfn, countfn, mappingfn, mtxfn, boundsfn]) - # extend the AbstractModelWrapper + # extend the AbstractModelWrapper defaults Base.eval(themodule, :(function $ids(w::AbstractModelWrapper)::Vector{String} $ids(unwrap_model(w)) end)) @@ -304,11 +304,11 @@ reactions; this macro declares precisely the same about the model type. macro all_variables_are_reactions(mt) m = esc(mt) quote - $Accessors.reaction_ids(model::$m) = $Accessors.variables(model) - $Accessors.reaction_count(model::$m) = $Accessors.n_variables(model) + $Accessors.reaction_ids(model::$m) = $Accessors.variable_ids(model) + $Accessors.reaction_count(model::$m) = $Accessors.variable_count(model) $Accessors.reaction_variables(model::$m) = - Dict(var => Dict(var => 1.0) for var in $Accessors.variables(model)) + Dict(var => Dict(var => 1.0) for var in $Accessors.variable_ids(model)) $Accessors.reaction_variables_matrix(model::$m) = - $SparseArrays.spdiagm(fill(1.0, $Accessors.n_variables(model))) + $SparseArrays.spdiagm(fill(1.0, $Accessors.variable_count(model))) end end diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl index 2fd4bfc04..123fae212 100644 --- a/src/types/misc/CommunityModel.jl +++ b/src/types/misc/CommunityModel.jl @@ -46,14 +46,14 @@ function environment_exchange_stoichiometry( ) idxs = [ (i, j) for - (i, j) in enumerate(indexin(env_rxns, variables(m.model))) if !isnothing(j) + (i, j) in enumerate(indexin(env_rxns, variable_ids(m.model))) if !isnothing(j) ] sparse( first.(idxs), last.(idxs), ones(length(idxs)), length(env_mets), - n_variables(m.model), + variable_count(m.model), ) end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 903badb30..430641663 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -94,17 +94,17 @@ Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel build_community_name_lookup(members) end -function Accessors.variables(cm::CommunityModel) +function Accessors.variable_ids(cm::CommunityModel) rxns = [ cm.name_lookup[id][:variables][vid] for (id, m) in cm.members for - vid in variables(m.model) + vid in variable_ids(m.model) ] env_exs = [envlink.reaction_id for envlink in cm.environmental_links] return [rxns; env_exs] end -function Accessors.n_variables(cm::CommunityModel) - num_model_reactions = sum(n_variables(m.model) for m in values(cm.members)) +function Accessors.variable_count(cm::CommunityModel) + num_model_reactions = sum(variable_count(m.model) for m in values(cm.members)) num_env_metabolites = length(cm.environmental_links) return num_model_reactions + num_env_metabolites end @@ -157,11 +157,11 @@ function Accessors.bounds(cm::CommunityModel) return ([models_lbs; env_lbs], [models_ubs; env_ubs]) end -Accessors.objective(cm::CommunityModel) = spzeros(n_variables(cm)) +Accessors.objective(cm::CommunityModel) = spzeros(variable_count(cm)) function Accessors.coupling(cm::CommunityModel) coups = blockdiag([coupling(m.model) for m in values(cm.members)]...) - n = n_variables(cm) + n = variable_count(cm) return [coups spzeros(size(coups, 1), n - size(coups, 2))] end diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index 3fb8a20d3..69df3ed4e 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -33,12 +33,12 @@ function Accessors.precache!(model::HDF5Model)::Nothing nothing end -function Accessors.n_variables(model::HDF5Model)::Int +function Accessors.variable_count(model::HDF5Model)::Int precache!(model) length(model.h5["reactions"]) end -function Accessors.variables(model::HDF5Model)::Vector{String} +function Accessors.variable_ids(model::HDF5Model)::Vector{String} precache!(model) # TODO is there any reasonable method to mmap strings from HDF5? read(model.h5["reactions"]) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 32393c252..fc406037b 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -18,7 +18,7 @@ changes in `json` invalidate the cache. ```` model = load_json_model("some_model.json") model.json # see the actual underlying JSON -variables(model) # see the list of reactions +variable_ids(model) # see the list of reactions ```` # Fields @@ -71,13 +71,13 @@ end _parse_notes(x)::Notes = _parse_annotations(x) -Accessors.n_variables(model::JSONModel) = length(model.rxns) +Accessors.variable_count(model::JSONModel) = length(model.rxns) Accessors.n_metabolites(model::JSONModel) = length(model.mets) Accessors.n_genes(model::JSONModel) = length(model.genes) -Accessors.variables(model::JSONModel) = +Accessors.variable_ids(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] Accessors.metabolites(model::JSONModel) = @@ -89,7 +89,7 @@ Accessors.genes(model::JSONModel) = Accessors.Internal.@all_variables_are_reactions JSONModel function Accessors.stoichiometry(model::JSONModel) - rxn_ids = variables(model) + rxn_ids = variable_ids(model) met_ids = metabolites(model) n_entries = 0 @@ -204,7 +204,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) return mm end - rxn_ids = variables(mm) + rxn_ids = variable_ids(mm) met_ids = metabolites(mm) gene_ids = genes(mm) S = stoichiometry(mm) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 68b3be4b3..d512f069d 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -11,13 +11,13 @@ struct MATModel <: AbstractMetabolicModel end Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) -Accessors.n_variables(m::MATModel)::Int = size(m.mat["S"], 2) +Accessors.variable_count(m::MATModel)::Int = size(m.mat["S"], 2) -function Accessors.variables(m::MATModel)::Vector{String} +function Accessors.variable_ids(m::MATModel)::Vector{String} if haskey(m.mat, "rxns") - reshape(m.mat["rxns"], n_variables(m)) + reshape(m.mat["rxns"], variable_count(m)) else - "rxn" .* string.(1:n_variables(m)) + "rxn" .* string.(1:variable_count(m)) end end @@ -39,8 +39,8 @@ end Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) Accessors.bounds(m::MATModel) = ( - reshape(get(m.mat, "lb", fill(-Inf, n_variables(m), 1)), n_variables(m)), - reshape(get(m.mat, "ub", fill(Inf, n_variables(m), 1)), n_variables(m)), + reshape(get(m.mat, "lb", fill(-Inf, variable_count(m), 1)), variable_count(m)), + reshape(get(m.mat, "ub", fill(Inf, variable_count(m), 1)), variable_count(m)), ) function Accessors.balance(m::MATModel) @@ -52,18 +52,18 @@ function Accessors.balance(m::MATModel) end Accessors.objective(m::MATModel) = - sparse(reshape(get(m.mat, "c", zeros(n_variables(m), 1)), n_variables(m))) + sparse(reshape(get(m.mat, "c", zeros(variable_count(m), 1)), variable_count(m))) Accessors.coupling(m::MATModel) = - _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_variables(m)+1:end, :]) : - sparse(get(m.mat, "C", zeros(0, n_variables(m)))) + _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][variable_count(m)+1:end, :]) : + sparse(get(m.mat, "C", zeros(0, variable_count(m)))) function Accessors.coupling_bounds(m::MATModel) nc = n_coupling_constraints(m) if _mat_has_squashed_coupling(m.mat) ( sparse(fill(-Inf, nc)), - sparse(reshape(m.mat["b"], length(m.mat["b"]))[n_variables(m)+1:end]), + sparse(reshape(m.mat["b"], length(m.mat["b"]))[variable_count(m)+1:end]), ) else ( @@ -80,7 +80,7 @@ end function Accessors.reaction_gene_associations(m::MATModel, rid::String) if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), variables(m))] + grr = m.mat["grRules"][findfirst(==(rid), variable_ids(m))] typeof(grr) == String ? parse_grr(grr) : nothing else nothing @@ -89,7 +89,7 @@ end function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwargs...) if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), variables(m))] + grr = m.mat["grRules"][findfirst(==(rid), variable_ids(m))] typeof(grr) == String ? eval_grr(parse_grr_to_sbml(grr); kwargs...) : nothing else nothing @@ -134,7 +134,7 @@ function Accessors.reaction_stoichiometry(m::MATModel, ridx::Int)::Dict{String,F end Accessors.reaction_name(m::MATModel, rid::String) = maybemap( - x -> x[findfirst(==(rid), variables(m))], + x -> x[findfirst(==(rid), variable_ids(m))], gets(m.mat, nothing, constants.keynames.rxnnames), ) @@ -157,12 +157,12 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) lb, ub = bounds(m) cl, cu = coupling_bounds(m) - nr = n_variables(m) + nr = variable_count(m) nm = n_metabolites(m) return MATModel( Dict( "S" => stoichiometry(m), - "rxns" => variables(m), + "rxns" => variable_ids(m), "mets" => metabolites(m), "lb" => Vector(lb), "ub" => Vector(ub), @@ -177,7 +177,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) "", maybemap.( x -> unparse_grr(String, x), - reaction_gene_associations.(Ref(m), variables(m)), + reaction_gene_associations.(Ref(m), variable_ids(m)), ), ), "metFormulas" => diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index ab03ae587..8286999f5 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -47,7 +47,7 @@ mutable struct MatrixModel <: AbstractMetabolicModel end end -Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns +Accessors.variable_ids(a::MatrixModel)::Vector{String} = a.rxns Accessors.Internal.@all_variables_are_reactions MatrixModel @@ -102,10 +102,10 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode objective(m), xl, xu, - variables(m), + variable_ids(m), metabolites(m), Vector{Maybe{GeneAssociationsDNF}}([ - reaction_gene_associations(m, id) for id in variables(m) + reaction_gene_associations(m, id) for id in variable_ids(m) ]), ) end diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 5847313c6..381b79bab 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -54,9 +54,9 @@ end # AbstractMetabolicModel interface follows -Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) +Accessors.variable_ids(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) -Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) +Accessors.variable_count(model::ObjectModel)::Int = length(model.reactions) Accessors.Internal.@all_variables_are_reactions ObjectModel @@ -84,7 +84,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat sizehint!(SV, n_entries) # establish the ordering - rxns = variables(model) + rxns = variable_ids(model) met_idx = Dict(mid => i for (i, mid) in enumerate(metabolites(model))) # fill the matrix entries @@ -101,7 +101,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat push!(SV, coeff) end end - return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) + return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), variable_count(model)) end Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = @@ -188,7 +188,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) gids = genes(model) metids = metabolites(model) - rxnids = variables(model) + rxnids = variable_ids(model) for gid in gids modelgenes[gid] = Gene( @@ -214,7 +214,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) S = stoichiometry(model) lbs, ubs = bounds(model) obj_idxs, obj_vals = findnz(objective(model)) - modelobjective = Dict(k => v for (k, v) in zip(variables(model)[obj_idxs], obj_vals)) + modelobjective = Dict(k => v for (k, v) in zip(variable_ids(model)[obj_idxs], obj_vals)) for (i, rid) in enumerate(rxnids) rmets = Dict{String,Float64}() for (j, stoich) in zip(findnz(S[:, i])...) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 0814ab236..0d9cc1b4a 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -38,7 +38,7 @@ function SBMLModel(sbml::SBML.Model, active_objective::String = "") ) end -Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids +Accessors.variable_ids(model::SBMLModel)::Vector{String} = model.reaction_ids Accessors.Internal.@all_variables_are_reactions SBMLModel @@ -280,7 +280,7 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) end mets = metabolites(mm) - rxns = variables(mm) + rxns = variable_ids(mm) stoi = stoichiometry(mm) (lbs, ubs) = bounds(mm) comps = default.("compartment", metabolite_compartment.(Ref(mm), mets)) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index ff9ad04a4..0ae333971 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -19,10 +19,10 @@ end Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner -Accessors.variables(cm::EqualGrowthCommunityModel) = - [variables(cm.inner); cm.community_objective_id] +Accessors.variable_ids(cm::EqualGrowthCommunityModel) = + [variable_ids(cm.inner); cm.community_objective_id] -Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 +Accessors.variable_count(cm::EqualGrowthCommunityModel) = variable_count(cm.inner) + 1 Accessors.metabolites(cm::EqualGrowthCommunityModel) = [metabolites(cm.inner); [m.id for m in cm.inner.members]] @@ -44,7 +44,7 @@ function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) cm.inner.name_lookup[id][:variables][m.biomass_reaction_id] for (id, m) in cm.inner.members ] - biomass_idxs = indexin(biomass_ids, variables(cm.inner)) + biomass_idxs = indexin(biomass_ids, variable_ids(cm.inner)) obj_links = sparse( 1:length(biomass_idxs), @@ -68,7 +68,7 @@ function Accessors.bounds(cm::EqualGrowthCommunityModel) end function Accessors.objective(cm::EqualGrowthCommunityModel) - vec = spzeros(n_variables(cm)) # overwrite objective + vec = spzeros(variable_count(cm)) # overwrite objective vec[end] = 1.0 return vec end diff --git a/src/utils/fluxes.jl b/src/utils/fluxes.jl index 1c840a9aa..b95dc8643 100644 --- a/src/utils/fluxes.jl +++ b/src/utils/fluxes.jl @@ -6,7 +6,7 @@ produce them, given the flux distribution supplied in `flux_dict`. """ function metabolite_fluxes(model::AbstractMetabolicModel, flux_dict::Dict{String,Float64}) S = stoichiometry(model) - rids = variables(model) + rids = variable_ids(model) mids = metabolites(model) producing = Dict{String,Dict{String,Float64}}() @@ -52,7 +52,7 @@ atom_fluxes(model, fluxes)["C"] ``` """ function atom_fluxes(model::AbstractMetabolicModel, reaction_fluxes::Dict{String,Float64}) - rids = variables(model) + rids = variable_ids(model) atom_flux = Dict{String,Float64}() for (ridx, rid) in enumerate(rids) haskey(reaction_fluxes, rid) || continue diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index f6402f346..ce4fce0d3 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -17,13 +17,13 @@ alternative. # Example ``` -findall(looks_like_exchange_reaction, variables(model)) # returns indices -filter(looks_like_exchange_reaction, variables(model)) # returns Strings +findall(looks_like_exchange_reaction, variable_ids(model)) # returns indices +filter(looks_like_exchange_reaction, variable_ids(model)) # returns Strings # to use the optional arguments you need to expand the function's arguments # using an anonymous function -findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns indices -filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns Strings +findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variable_ids(model)) # returns indices +filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variable_ids(model)) # returns Strings ``` """ function looks_like_exchange_reaction( @@ -43,7 +43,7 @@ Shortcut for finding exchange reaction indexes in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ find_exchange_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_exchange_reaction(id; kwargs...), variables(m)) + findall(id -> looks_like_exchange_reaction(id; kwargs...), variable_ids(m)) """ $(TYPEDSIGNATURES) @@ -52,7 +52,7 @@ Shortcut for finding exchange reaction identifiers in a model; arguments are forwarded to [`looks_like_exchange_reaction`](@ref). """ find_exchange_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_exchange_reaction(id, kwargs...), variables(m)) + filter(id -> looks_like_exchange_reaction(id, kwargs...), variable_ids(m)) """ $(TYPEDSIGNATURES) @@ -70,8 +70,8 @@ alternative. # Example ``` -filter(looks_like_biomass_reaction, variables(model)) # returns strings -findall(looks_like_biomass_reaction, variables(model)) # returns indices +filter(looks_like_biomass_reaction, variable_ids(model)) # returns strings +findall(looks_like_biomass_reaction, variable_ids(model)) # returns indices ``` """ function looks_like_biomass_reaction( @@ -91,7 +91,7 @@ Shortcut for finding biomass reaction indexes in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ find_biomass_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) + findall(id -> looks_like_biomass_reaction(id; kwargs...), variable_ids(m)) """ $(TYPEDSIGNATURES) @@ -100,7 +100,7 @@ Shortcut for finding biomass reaction identifiers in a model; arguments are forwarded to [`looks_like_biomass_reaction`](@ref). """ find_biomass_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) + filter(id -> looks_like_biomass_reaction(id; kwargs...), variable_ids(m)) """ $(TYPEDSIGNATURES) diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index aced3e1bf..8115a1fa0 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -36,7 +36,7 @@ not in [`coupling`](@ref)), and `inner`, which is the original wrapped model. The `objective` of the model includes also the extra columns for individual genes, as held by `coupling_row_gene_product`. -Implementation exposes the split reactions (available as `variables(model)`), +Implementation exposes the split reactions (available as `variable_ids(model)`), but retains the original "simple" reactions accessible by [`reactions`](@ref). The related constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). @@ -74,8 +74,8 @@ end Accessors.objective(model::EnzymeConstrainedModel) = model.objective -function Accessors.variables(model::EnzymeConstrainedModel) - inner_reactions = variables(model.inner) +function Accessors.variable_ids(model::EnzymeConstrainedModel) + inner_reactions = variable_ids(model.inner) mangled_reactions = [ enzyme_constrained_reaction_name( inner_reactions[col.reaction_idx], @@ -86,7 +86,7 @@ function Accessors.variables(model::EnzymeConstrainedModel) [mangled_reactions; genes(model)] end -Accessors.n_variables(model::EnzymeConstrainedModel) = +Accessors.variable_count(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) function Accessors.bounds(model::EnzymeConstrainedModel) @@ -112,7 +112,7 @@ end Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( - variables(model), + variable_ids(model), reaction_ids(model), reaction_variables_matrix(model), ) # TODO currently inefficient diff --git a/src/wrappers/MatrixCoupling.jl b/src/wrappers/MatrixCoupling.jl index ac982ab58..a8bc595d6 100644 --- a/src/wrappers/MatrixCoupling.jl +++ b/src/wrappers/MatrixCoupling.jl @@ -25,7 +25,7 @@ mutable struct MatrixCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetab ) where {M<:AbstractMetabolicModel} length(cu) == length(cl) || throw(DimensionMismatch("`cl` and `cu` need to have the same size")) - size(C) == (length(cu), n_variables(lm)) || + size(C) == (length(cu), variable_count(lm)) || throw(DimensionMismatch("wrong dimensions of `C`")) new{M}(lm, sparse(C), collect(cl), collect(cu)) @@ -55,7 +55,12 @@ function Base.convert( (cl, cu) = coupling_bounds(mm) MatrixCoupling(convert(M, mm), coupling(mm), cl, cu) else - MatrixCoupling(convert(M, mm), spzeros(0, n_variables(mm)), spzeros(0), spzeros(0)) + MatrixCoupling( + convert(M, mm), + spzeros(0, variable_count(mm)), + spzeros(0), + spzeros(0), + ) end end diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index 881bd6831..4eda007b1 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -81,10 +81,10 @@ end Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner -Accessors.variables(model::MaxMinDrivingForceModel) = +Accessors.variable_ids(model::MaxMinDrivingForceModel) = ["mmdf"; "log " .* metabolites(model); "ΔG " .* reaction_ids(model)] -Accessors.n_variables(model::MaxMinDrivingForceModel) = +Accessors.variable_count(model::MaxMinDrivingForceModel) = 1 + n_metabolites(model) + reaction_count(model) Accessors.metabolite_log_concentration_ids(model::MaxMinDrivingForceModel) = @@ -102,7 +102,7 @@ Accessors.gibbs_free_energy_variables(model::MaxMinDrivingForceModel) = Accessors.objective(model::MaxMinDrivingForceModel) = - [1.0; fill(0.0, n_variables(model) - 1)] + [1.0; fill(0.0, variable_count(model) - 1)] function Accessors.balance(model::MaxMinDrivingForceModel) # proton water balance @@ -134,7 +134,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) # set proton and water equality constraints num_proton_water = length(model.proton_ids) + length(model.water_ids) - proton_water_mat = spzeros(num_proton_water, n_variables(model)) + proton_water_mat = spzeros(num_proton_water, variable_count(model)) idxs = indexin([model.proton_ids; model.water_ids], var_ids) for (i, j) in enumerate(idxs) isnothing(j) && throw(error("Water or proton ID not found in model.")) @@ -142,7 +142,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) end # constant concentration constraints - const_conc_mat = spzeros(length(model.constant_concentrations), n_variables(model)) + const_conc_mat = spzeros(length(model.constant_concentrations), variable_count(model)) ids = collect(keys(model.constant_concentrations)) idxs = indexin(ids, var_ids) for (i, j) in enumerate(idxs) @@ -152,7 +152,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) end # add the relative bounds - const_ratio_mat = spzeros(length(model.concentration_ratios), n_variables(model)) + const_ratio_mat = spzeros(length(model.concentration_ratios), variable_count(model)) for (i, (mid1, mid2)) in enumerate(keys(model.concentration_ratios)) idxs = indexin([mid1, mid2], var_ids) any(isnothing.(idxs)) && @@ -178,8 +178,8 @@ end function Accessors.bounds(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) - lbs = fill(-model.max_dg_bound, n_variables(model)) - ubs = fill(model.max_dg_bound, n_variables(model)) + lbs = fill(-model.max_dg_bound, variable_count(model)) + ubs = fill(model.max_dg_bound, variable_count(model)) # mmdf must be positive for problem to be feasible (it is defined as -ΔG) lbs[1] = 0.0 diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl index 399fe1db1..1d938ae06 100644 --- a/src/wrappers/MinimizeDistance.jl +++ b/src/wrappers/MinimizeDistance.jl @@ -39,7 +39,7 @@ flexible way that fits into larger model systems. """ with_parsimonious_objective() = model::AbstractMetabolicModel -> - MinimizeSolutionDistance(zeros(n_variables(model)), model) + MinimizeSolutionDistance(zeros(variable_count(model)), model) """ $(TYPEDEF) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 706dc6cb6..5b3eb620b 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -32,7 +32,7 @@ model. Multiple capacity bounds may be added through in the formulation. This implementation allows easy access to fluxes from the split reactions -(available in `variables(model)`), while the original "simple" reactions from +(available in `variable_ids(model)`), while the original "simple" reactions from the wrapped model are retained as [`reactions`](@ref). All additional constraints are implemented using [`coupling`](@ref) and [`coupling_bounds`](@ref). @@ -61,8 +61,8 @@ Accessors.stoichiometry(model::SimplifiedEnzymeConstrainedModel) = Accessors.objective(model::SimplifiedEnzymeConstrainedModel) = simplified_enzyme_constrained_column_reactions(model)' * objective(model.inner) -Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = - let inner_reactions = variables(model.inner) +Accessors.variable_ids(model::SimplifiedEnzymeConstrainedModel) = + let inner_reactions = variable_ids(model.inner) [ simplified_enzyme_constrained_reaction_name( inner_reactions[col.reaction_idx], @@ -71,7 +71,7 @@ Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = ] end -Accessors.n_variables(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) +Accessors.variable_count(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) @@ -96,7 +96,7 @@ wrapped model. """ Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( - variables(model), + variable_ids(model), reaction_ids(model.inner), reaction_variables_matrix(model), ) # TODO currently inefficient diff --git a/src/wrappers/misc/enzyme_constrained.jl b/src/wrappers/misc/enzyme_constrained.jl index 120af97bb..01a5eb2de 100644 --- a/src/wrappers/misc/enzyme_constrained.jl +++ b/src/wrappers/misc/enzyme_constrained.jl @@ -27,7 +27,7 @@ enzyme_constrained_column_reactions(columns, inner) = sparse( [col.reaction_idx for col in columns], 1:length(columns), [col.direction >= 0 ? 1 : -1 for col in columns], - n_variables(inner), + variable_count(inner), length(columns), ) @@ -112,6 +112,6 @@ simplified_enzyme_constrained_column_reactions(model::SimplifiedEnzymeConstraine [col.reaction_idx for col in model.columns], 1:length(model.columns), [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_variables(model.inner), + variable_count(model.inner), length(model.columns), ) diff --git a/test/analysis/envelopes.jl b/test/analysis/envelopes.jl index 9a5d8d746..7c8d748c5 100644 --- a/test/analysis/envelopes.jl +++ b/test/analysis/envelopes.jl @@ -6,12 +6,12 @@ lat = collect.(envelope_lattice(m, rxns; samples = 3)) @test lat == [[-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0]] - @test lat == collect.(envelope_lattice(m, variables(m)[rxns]; samples = 3)) + @test lat == collect.(envelope_lattice(m, variable_ids(m)[rxns]; samples = 3)) vals = objective_envelope( m, - variables(m)[rxns], + variable_ids(m)[rxns], Tulip.Optimizer; lattice_args = (samples = 3, ranges = [(-5, 0), (-5, 0), (-5, 5)]), workers = W, diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl index 630db3877..50fc0c4d7 100644 --- a/test/analysis/flux_balance_analysis.jl +++ b/test/analysis/flux_balance_analysis.jl @@ -26,7 +26,7 @@ fluxes_vec = flux_balance_analysis(cp, Tulip.Optimizer) |> values_vec @test all(fluxes_vec .== sol) fluxes_dict = flux_balance_analysis(cp, Tulip.Optimizer) |> values_dict - rxns = variables(cp) + rxns = variable_ids(cp) @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) end @@ -102,8 +102,8 @@ end γ = 40 # construct coupling bounds - nr = n_variables(model) - biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], variables(model))) + nr = variable_count(model) + biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], variable_ids(model))) Cf = sparse(1.0I, nr, nr) Cf[:, biomass_index] .= -γ diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl index e064f939b..6ebd8ac7d 100644 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ b/test/analysis/minimize_metabolic_adjustment.jl @@ -1,7 +1,7 @@ @testset "MOMA" begin model = test_toyModel() - sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in variables(model)] + sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in variable_ids(model)] moma = minimize_metabolic_adjustment_analysis( diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl index e31ec13a0..949fe6466 100644 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ b/test/analysis/sampling/affine_hit_and_run.jl @@ -2,9 +2,9 @@ model = load_model(model_paths["e_coli_core.json"]) - cm = MatrixCoupling(model, zeros(1, n_variables(model)), [17.0], [19.0]) + cm = MatrixCoupling(model, zeros(1, variable_count(model)), [17.0], [19.0]) - pfk, tala = indexin(["PFK", "TALA"], variables(cm)) + pfk, tala = indexin(["PFK", "TALA"], variable_ids(cm)) cm.C[:, [pfk, tala]] .= 1.0 warmup = warmup_from_variability(cm, Tulip.Optimizer; workers = W) diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl index a329267ff..4b690e764 100644 --- a/test/analysis/sampling/warmup_variability.jl +++ b/test/analysis/sampling/warmup_variability.jl @@ -9,7 +9,7 @@ workers = W, ) - idx = first(indexin([rid], variables(model))) + idx = first(indexin([rid], variable_ids(model))) @test size(pts) == (95, 100) @test all(pts[idx, :] .>= -2) @test all(pts[idx, :] .<= 2) diff --git a/test/io/h5.jl b/test/io/h5.jl index b368cc674..fd87b8f65 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -12,15 +12,15 @@ @test !isnothing(h5.h5) # briefly test that the loading is okay - @test n_variables(model) == n_variables(h5) + @test variable_count(model) == variable_count(h5) @test n_metabolites(model) == n_metabolites(h5) - @test issetequal(variables(model), variables(h5)) + @test issetequal(variable_ids(model), variable_ids(h5)) @test issetequal(metabolites(model), metabolites(h5)) @test issorted(metabolites(h5)) - @test issorted(variables(h5)) + @test issorted(variable_ids(h5)) @test size(stoichiometry(model)) == size(stoichiometry(h5)) @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) - rxnp = sortperm(variables(model)) + rxnp = sortperm(variable_ids(model)) @test bounds(model)[1][rxnp] == bounds(h5)[1] @test bounds(model)[2][rxnp] == bounds(h5)[2] @test objective(model)[rxnp] == objective(h5) diff --git a/test/io/io.jl b/test/io/io.jl index 479562301..fb4b66750 100644 --- a/test/io/io.jl +++ b/test/io/io.jl @@ -2,24 +2,24 @@ sbmlmodel = load_model(model_paths["iJO1366.xml"]) @test sbmlmodel isa SBMLModel - @test n_variables(sbmlmodel) == 2583 + @test variable_count(sbmlmodel) == 2583 matlabmodel = load_model(model_paths["iJO1366.mat"]) @test matlabmodel isa MATModel - @test n_variables(matlabmodel) == 2583 + @test variable_count(matlabmodel) == 2583 jsonmodel = load_model(model_paths["iJO1366.json"]) @test jsonmodel isa JSONModel - @test n_variables(jsonmodel) == 2583 + @test variable_count(jsonmodel) == 2583 - @test Set(lowercase.(variables(sbmlmodel))) == - Set("r_" .* lowercase.(variables(matlabmodel))) - @test Set(lowercase.(variables(sbmlmodel))) == - Set("r_" .* lowercase.(variables(jsonmodel))) + @test Set(lowercase.(variable_ids(sbmlmodel))) == + Set("r_" .* lowercase.(variable_ids(matlabmodel))) + @test Set(lowercase.(variable_ids(sbmlmodel))) == + Set("r_" .* lowercase.(variable_ids(jsonmodel))) # specifically test parsing of gene-reaction associations in Recon reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) - @test n_variables(reconmodel) == 10600 + @test variable_count(reconmodel) == 10600 recon_grrs = [ r.gene_associations for (i, r) in reconmodel.reactions if !isnothing(r.gene_associations) diff --git a/test/io/json.jl b/test/io/json.jl index 90f3cad5c..a10961286 100644 --- a/test/io/json.jl +++ b/test/io/json.jl @@ -4,7 +4,7 @@ stdmodel = convert(ObjectModel, jsonmodel) # test if same reaction ids - @test issetequal(variables(jsonmodel), variables(stdmodel)) + @test issetequal(variable_ids(jsonmodel), variable_ids(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) # not the best tests since it is possible that error could cancel each other out: diff --git a/test/io/sbml.jl b/test/io/sbml.jl index a3508830b..e72f3255d 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -4,7 +4,7 @@ m = convert(MatrixModel, sbmlm) @test size(stoichiometry(sbmlm)) == (92, 95) - @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_variables(sbmlm)) + @test size(stoichiometry(m)) == (n_metabolites(sbmlm), variable_count(sbmlm)) @test length(m.S.nzval) == 380 @test length.(bounds(sbmlm)) == (95, 95) @test length.(bounds(m)) == (95, 95) diff --git a/test/reconstruction/MatrixCoupling.jl b/test/reconstruction/MatrixCoupling.jl index 5b3de7464..96d93a623 100644 --- a/test/reconstruction/MatrixCoupling.jl +++ b/test/reconstruction/MatrixCoupling.jl @@ -25,7 +25,7 @@ cp = test_coupledLP() n_c = n_coupling_constraints(cp) new_cp = remove_coupling_constraints(cp, 1) - @test size(coupling(cp)) == (n_c, n_variables(cp)) + @test size(coupling(cp)) == (n_c, variable_count(cp)) @test n_c - 1 == n_coupling_constraints(new_cp) @test n_coupling_constraints(cp) == n_c new_cp = remove_coupling_constraints(cp, [1, 2]) @@ -117,7 +117,7 @@ end @testset "Remove reactions" begin cp = convert(MatrixModelWithCoupling, test_LP()) - cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_variables(cp)), -1.0, 1.0) + cp = add_coupling_constraints(cp, 1.0 .* collect(1:variable_count(cp)), -1.0, 1.0) new_cp = remove_reactions(cp, [3, 2]) @test new_cp isa MatrixModelWithCoupling diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index 480d579b4..59baeaaf9 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -105,7 +105,7 @@ end 1.0, check_consistency = true, ) - @test n_variables(cp) + 1 == n_variables(new_cp) + @test variable_count(cp) + 1 == variable_count(new_cp) (new_cp, new_reactions, new_mets) = add_reactions( cp, @@ -118,7 +118,7 @@ end ["m1", "m2", "m3", "m6"], check_consistency = true, ) - @test n_variables(cp) == n_variables(new_cp) + @test variable_count(cp) == variable_count(new_cp) @test n_metabolites(cp) + 1 == n_metabolites(new_cp) end @@ -187,7 +187,7 @@ end # proper subset of existing metabolites cp = test_LP() new_cp = add_reactions(cp, [-1.0], zeros(1), 1.0, 0.0, 1.0, "r4", ["m1"]) - @test n_variables(cp) + 1 == n_variables(new_cp) + @test variable_count(cp) + 1 == variable_count(new_cp) @test_throws DimensionMismatch add_reactions( cp, @@ -228,7 +228,7 @@ end @test objective(modLp) == objective(lp)[2:3] @test bounds(modLp)[1] == bounds(lp)[1][2:3] @test bounds(modLp)[2] == bounds(lp)[2][2:3] - @test variables(modLp) == variables(lp)[2:3] + @test variable_ids(modLp) == variable_ids(lp)[2:3] @test metabolites(modLp) == metabolites(lp) end diff --git a/test/reconstruction/SerializedModel.jl b/test/reconstruction/SerializedModel.jl index 21d41e1ff..419816a53 100644 --- a/test/reconstruction/SerializedModel.jl +++ b/test/reconstruction/SerializedModel.jl @@ -8,8 +8,8 @@ @test typeof(m2) == typeof(m) sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = remove_reaction(sm, variables(m)[3]) + m2 = remove_reaction(sm, variable_ids(m)[3]) @test typeof(m2) == typeof(m) - @test !(variables(m)[3] in variables(m2)) + @test !(variable_ids(m)[3] in variable_ids(m2)) end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 6495d9eaa..1ccbea11f 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -59,7 +59,7 @@ @test contains(sprint(show, MIME("text/plain"), cm), "community model") @test issetequal( - variables(cm), + variable_ids(cm), [ "m1#EX_A" "m1#r1" @@ -108,7 +108,7 @@ ], ) - @test n_variables(cm) == 13 + @test variable_count(cm) == 13 @test n_metabolites(cm) == 11 @test n_genes(cm) == 8 @@ -307,7 +307,7 @@ end # test if model can be converted to another type om = convert(ObjectModel, cm) - @test n_variables(om) == n_variables(cm) + @test variable_count(om) == variable_count(cm) end @testset "EqualGrowthCommunityModel: enzyme constrained e coli" begin diff --git a/test/types/JSONModel.jl b/test/types/JSONModel.jl index 97b8ff05e..f94c0451f 100644 --- a/test/types/JSONModel.jl +++ b/test/types/JSONModel.jl @@ -5,8 +5,8 @@ sm = convert(ObjectModel, jm) jm2 = convert(JSONModel, sm) - @test Set(variables(jm)) == Set(variables(sm)) - @test Set(variables(jm)) == Set(variables(jm2)) + @test Set(variable_ids(jm)) == Set(variable_ids(sm)) + @test Set(variable_ids(jm)) == Set(variable_ids(jm2)) end @testset "JSONModel generic interface" begin diff --git a/test/types/MATModel.jl b/test/types/MATModel.jl index 4bd536a7c..2b4cd06c0 100644 --- a/test/types/MATModel.jl +++ b/test/types/MATModel.jl @@ -6,8 +6,8 @@ sm = convert(ObjectModel, mm) mm2 = convert(MATModel, sm) - @test Set(variables(mm)) == Set(variables(sm)) - @test Set(variables(mm)) == Set(variables(mm2)) + @test Set(variable_ids(mm)) == Set(variable_ids(sm)) + @test Set(variable_ids(mm)) == Set(variable_ids(mm2)) end @testset "MATModel generic interface" begin diff --git a/test/types/MatrixCoupling.jl b/test/types/MatrixCoupling.jl index c2b816fd8..2932a6630 100644 --- a/test/types/MatrixCoupling.jl +++ b/test/types/MatrixCoupling.jl @@ -12,9 +12,9 @@ end sm = convert(ObjectModel, cm) cm2 = convert(MatrixModelWithCoupling, sm) - @test Set(variables(cm)) == Set(variables(sm)) - @test Set(variables(cm)) == Set(variables(cm2)) + @test Set(variable_ids(cm)) == Set(variable_ids(sm)) + @test Set(variable_ids(cm)) == Set(variable_ids(cm2)) - @test reaction_gene_associations(sm, variables(sm)[1]) == - reaction_gene_associations(cm, variables(sm)[1]) + @test reaction_gene_associations(sm, variable_ids(sm)[1]) == + reaction_gene_associations(cm, variable_ids(sm)[1]) end diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl index 6ca8c2d46..344de6f8b 100644 --- a/test/types/MatrixModel.jl +++ b/test/types/MatrixModel.jl @@ -11,8 +11,8 @@ end sm = convert(ObjectModel, cm) cm2 = convert(MatrixModel, sm) - @test Set(variables(cm)) == Set(variables(sm)) - @test Set(variables(cm)) == Set(variables(cm2)) + @test Set(variable_ids(cm)) == Set(variable_ids(sm)) + @test Set(variable_ids(cm)) == Set(variable_ids(cm2)) @test sort(sort.(reaction_gene_associations(sm, reaction_ids(sm)[1]))) == sort(sort.(reaction_gene_associations(cm, reaction_ids(sm)[1]))) diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index f1528a1c3..8709a5b68 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -46,10 +46,10 @@ @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") - @test "r1" in variables(model) + @test "r1" in variable_ids(model) @test "m4" in metabolites(model) @test "g2" in genes(model) - @test n_variables(model) == 4 + @test variable_count(model) == 4 @test n_metabolites(model) == 4 @test n_genes(model) == 3 @@ -132,7 +132,7 @@ jsonmodel = convert(JSONModel, model) stdmodel = convert(ObjectModel, jsonmodel) - @test issetequal(variables(jsonmodel), variables(stdmodel)) + @test issetequal(variable_ids(jsonmodel), variable_ids(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) jlbs, jubs = bounds(jsonmodel) @@ -141,8 +141,8 @@ @test issetequal(jubs, subs) jS = stoichiometry(jsonmodel) sS = stoichiometry(stdmodel) - j_r1_index = findfirst(x -> x == "r1", variables(jsonmodel)) - s_r1_index = findfirst(x -> x == "r1", variables(stdmodel)) + j_r1_index = findfirst(x -> x == "r1", variable_ids(jsonmodel)) + s_r1_index = findfirst(x -> x == "r1", variable_ids(stdmodel)) j_m1_index = findfirst(x -> x == "m1", metabolites(jsonmodel)) j_m2_index = findfirst(x -> x == "m2", metabolites(jsonmodel)) s_m1_index = findfirst(x -> x == "m1", metabolites(stdmodel)) diff --git a/test/types/SBMLModel.jl b/test/types/SBMLModel.jl index 1cb52cb59..17a7b7d5c 100644 --- a/test/types/SBMLModel.jl +++ b/test/types/SBMLModel.jl @@ -4,8 +4,8 @@ sm = convert(ObjectModel, sbmlm) sbmlm2 = convert(SBMLModel, sm) - @test Set(variables(sbmlm)) == Set(variables(sbmlm2)) - @test Set(variables(sbmlm)) == Set(variables(sm)) + @test Set(variable_ids(sbmlm)) == Set(variable_ids(sbmlm2)) + @test Set(variable_ids(sbmlm)) == Set(variable_ids(sm)) @test Set(metabolites(sbmlm)) == Set(metabolites(sbmlm2)) sp(x) = x.species @test all([ @@ -15,7 +15,7 @@ ) && issetequal( sp.(sbmlm.sbml.reactions[i].products), sp.(sbmlm2.sbml.reactions[i].products), - ) for i in variables(sbmlm2) + ) for i in variable_ids(sbmlm2) ]) st(x) = isnothing(x.stoichiometry) ? 1.0 : x.stoichiometry @test all([ @@ -25,7 +25,7 @@ ) && issetequal( st.(sbmlm.sbml.reactions[i].products), st.(sbmlm2.sbml.reactions[i].products), - ) for i in variables(sbmlm2) + ) for i in variable_ids(sbmlm2) ]) end diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index 714e9494d..f87f15366 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -4,7 +4,7 @@ struct FakeModel <: AbstractMetabolicModel end @testset "Base abstract model methods require proper minimal implementation" begin - @test_throws MethodError variables(123) + @test_throws MethodError variable_ids(123) x = FakeModel(123) for m in [variables, metabolites, stoichiometry, bounds, objective] @test_throws MethodError m(x) diff --git a/test/utils/MatrixModel.jl b/test/utils/MatrixModel.jl index d808700c1..94d72d4f1 100644 --- a/test/utils/MatrixModel.jl +++ b/test/utils/MatrixModel.jl @@ -1,6 +1,6 @@ @testset "MatrixModel utilities" begin cp = test_LP() - @test n_variables(cp) == 3 + @test variable_count(cp) == 3 @test n_metabolites(cp) == 4 @test n_coupling_constraints(cp) == 0 diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl index 7a3c73666..cbfb3acb1 100644 --- a/test/utils/ObjectModel.jl +++ b/test/utils/ObjectModel.jl @@ -13,9 +13,9 @@ cbm = make_optimization_model(model, Tulip.Optimizer) ubs = cbm[:ubs] lbs = cbm[:lbs] - glucose_index = first(indexin(["EX_glc__D_e"], variables(model))) - o2_index = first(indexin(["EX_o2_e"], variables(model))) - atpm_index = first(indexin(["ATPM"], variables(model))) + glucose_index = first(indexin(["EX_glc__D_e"], variable_ids(model))) + o2_index = first(indexin(["EX_o2_e"], variable_ids(model))) + atpm_index = first(indexin(["ATPM"], variable_ids(model))) set_optmodel_bound!(glucose_index, cbm; upper_bound = -1.0, lower_bound = -1.0) @test normalized_rhs(ubs[glucose_index]) == -1.0 @test normalized_rhs(lbs[glucose_index]) == 1.0 diff --git a/test/utils/Serialized.jl b/test/utils/Serialized.jl index 32d325362..052a26c1e 100644 --- a/test/utils/Serialized.jl +++ b/test/utils/Serialized.jl @@ -14,8 +14,8 @@ @test sm2.m == nothing # nothing is cached here @test isequal(m, deserialize(tmpfile("toy2.serialized"))) # it was written as-is @test issetequal( - variables(convert(ObjectModel, sm)), - variables(convert(ObjectModel, sm2)), + variable_ids(convert(ObjectModel, sm)), + variable_ids(convert(ObjectModel, sm2)), ) sm.m = nothing @test issetequal( diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index e3aa8f8f8..184ad4238 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -1,9 +1,9 @@ @testset "Looks like in MatrixModel, detailed test" begin cp = test_LP() - @test isempty(filter(looks_like_exchange_reaction, variables(cp))) + @test isempty(filter(looks_like_exchange_reaction, variable_ids(cp))) cp = test_simpleLP() - @test isempty(filter(looks_like_exchange_reaction, variables(cp))) + @test isempty(filter(looks_like_exchange_reaction, variable_ids(cp))) cp = MatrixModel( [-1.0 -1 -2; 0 -1 0; 0 0 0], @@ -14,7 +14,7 @@ ["EX_m1"; "r2"; "r3"], ["m1"; "m2"; "m3"], ) - @test filter(looks_like_exchange_reaction, variables(cp)) == ["EX_m1"] + @test filter(looks_like_exchange_reaction, variable_ids(cp)) == ["EX_m1"] cp = MatrixModel( [-1.0 0 0; 0 0 -1; 0 -1 0], @@ -25,55 +25,55 @@ ["EX_m1"; "Exch_m3"; "Ex_m2"], ["m1"; "m2"; "m3_e"], ) - @test filter(looks_like_exchange_reaction, variables(cp)) == + @test filter(looks_like_exchange_reaction, variable_ids(cp)) == ["EX_m1", "Exch_m3", "Ex_m2"] @test filter( x -> looks_like_exchange_reaction(x; exchange_prefixes = ["Exch_"]), - variables(cp), + variable_ids(cp), ) == ["Exch_m3"] # this is originally the "toyModel1.mat" cp = test_toyModel() - @test filter(looks_like_exchange_reaction, variables(cp)) == + @test filter(looks_like_exchange_reaction, variable_ids(cp)) == ["EX_m1(e)", "EX_m3(e)", "EX_biomass(e)"] @test filter( x -> looks_like_exchange_reaction(x; exclude_biomass = true), - variables(cp), + variable_ids(cp), ) == ["EX_m1(e)", "EX_m3(e)"] @test filter(looks_like_extracellular_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"] - @test filter(looks_like_biomass_reaction, variables(cp)) == + @test filter(looks_like_biomass_reaction, variable_ids(cp)) == ["EX_biomass(e)", "biomass1"] @test filter( x -> looks_like_biomass_reaction(x; exclude_exchanges = true), - variables(cp), + variable_ids(cp), ) == ["biomass1"] end @testset "Looks like functions, basic" begin model = load_model(model_paths["e_coli_core.json"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = load_model(model_paths["e_coli_core.xml"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = load_model(model_paths["e_coli_core.mat"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = convert(ObjectModel, model) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 model = convert(MatrixModelWithCoupling, model) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 + @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 + @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 end From 2c789295068bd716d86d1b0f336644b4eb8dbb04 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 12:30:17 +0200 Subject: [PATCH 288/531] test that shortcuts are shortcuts --- test/types/abstract/AbstractMetabolicModel.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index f87f15366..d3ae3c225 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -10,3 +10,10 @@ end @test_throws MethodError m(x) end end + + +@testset "ID shortcuts are identictical with the ID-generating functions" begin + @test variables === variable_ids + @test reactions === reaction_ids + # TODO don't forget about metabolites later +end From f5f5bad073d05575ea81c75246d531da1a7a570b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 12:56:23 +0200 Subject: [PATCH 289/531] reorganize the make_optimization_model a tiny bit --- src/solver.jl | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index aa8c0a3eb..f12c8ce95 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -33,21 +33,29 @@ function make_optimization_model( precache!(model) - m, n = size(stoichiometry(model)) - xl, xu = bounds(model) - optimization_model = Model(optimizer) + + # make the variables + n = variable_count(model) @variable(optimization_model, x[1:n]) + + # bound the variables + xl, xu = bounds(model) + @constraint(optimization_model, lbs, xl .<= x) # lower bounds + @constraint(optimization_model, ubs, x .<= xu) # upper bounds + + # add the objective let obj = objective(model) if obj isa AbstractVector + # linear objective case @objective(optimization_model, sense, obj' * x) else + # quadratic objective case @objective(optimization_model, sense, x' * obj * [x; 1]) end end + @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance - @constraint(optimization_model, lbs, xl .<= x) # lower bounds - @constraint(optimization_model, ubs, x .<= xu) # upper bounds C = coupling(model) # empty if no coupling isempty(C) || begin From 499e576c31e44e0642e89c371ebe119758f3abea Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 14:08:29 +0200 Subject: [PATCH 290/531] build the bounds from bounded semantics --- src/solver.jl | 33 ++++++++++++++++++++++++++- src/types/accessors/bits/semantics.jl | 9 ++++++++ 2 files changed, 41 insertions(+), 1 deletion(-) diff --git a/src/solver.jl b/src/solver.jl index f12c8ce95..1251ecb6a 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -44,7 +44,7 @@ function make_optimization_model( @constraint(optimization_model, lbs, xl .<= x) # lower bounds @constraint(optimization_model, ubs, x .<= xu) # upper bounds - # add the objective + # mark the objective let obj = objective(model) if obj isa AbstractVector # linear objective case @@ -55,6 +55,37 @@ function make_optimization_model( end end + # go over the semantics and add bounds if there are any + for (semname, sem) in Accessors.Internal.get_semantics() + bounds = sem.bounds(model) + if isnothing(bounds) + continue + elseif bounds isa Vector{Float64} + # equality bounds + c = @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) + set_name.(c, "$(semname)_eqs") + elseif bounds isa Tuple{Vector{Float64},Vector{Float64}} + # lower/upper interval bounds + slb, sub = bounds + smtx = sem.mapping_matrix(model) + c = @constraint(optimization_model, slb .<= smtx * x) + set_name.(c, "$(semname)_lbs") + c = @constraint(optimization_model, smtx * x .<= sub) + set_name.(c, "$(semname)_ubs") + else + # if the bounds returned something weird, complain loudly. + throw( + TypeError( + :make_optimization_model, + "conversion of $(typeof(model)) bounds", + typeof(bounds), + Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}}, + ), + ) + end + end + + # make stoichiometry balanced @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance C = coupling(model) # empty if no coupling diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index fe1076222..1f74a046c 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -92,6 +92,15 @@ const variable_semantics = Dict{Symbol,Semantics}() """ $(TYPEDSIGNATURES) +Get all available semantics in a symbol-indexed dictionary. +""" +function get_semantics()::Dict{Symbol,Semantics} + variable_semantics +end + +""" +$(TYPEDSIGNATURES) + Get a tuple of functions that work with the given semantics, or `nothing` if the semantics doesn't exist. """ From ffe980150f476c241078e8e5520fac713ff867d4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 14:08:54 +0200 Subject: [PATCH 291/531] fixup: cosmetic fix of solver reorganization --- src/solver.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 1251ecb6a..5e011e80e 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -88,15 +88,16 @@ function make_optimization_model( # make stoichiometry balanced @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance - C = coupling(model) # empty if no coupling - isempty(C) || begin + # add coupling constraints + C = coupling(model) + if !isempty(C) cl, cu = coupling_bounds(model) @constraint(optimization_model, c_lbs, cl .<= C * x) # coupling lower bounds @constraint(optimization_model, c_ubs, C * x .<= cu) # coupling upper bounds end return optimization_model - #TODO what about ModelWithResult right from this point? ;D + #TODO so well, what about having ModelWithResult right from this point? ;D end """ From d9630f316a3cbe73960d7ad632c5eed27f3a9485 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 14:09:07 +0200 Subject: [PATCH 292/531] fixup: return type constraint on default semantic bounds accessor --- src/types/accessors/bits/semantics.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 1f74a046c..8d67c9dec 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -244,9 +244,13 @@ no bounds, or a vector of floats with equality bounds, or a tuple of 2 vectors with lower and upper bounds. """, ), - :(function $bounds(a::AbstractMetabolicModel)::SparseMat - nothing - end), + :( + function $bounds( + a::AbstractMetabolicModel, + )::Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}} + nothing + end + ), ) Base.eval.(Ref(themodule), [idsfn, countfn, mappingfn, mtxfn, boundsfn]) From 97df9a9a4d292164a6e5a7fd2627ccc5a59572d0 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 14:53:30 +0200 Subject: [PATCH 293/531] clarify bounds as variable bounds --- docs/src/concepts/3_custom_models.md | 4 ++-- docs/src/concepts/4_wrappers.md | 10 ++++---- src/analysis/envelopes.jl | 2 +- .../gapfill_minimum_reactions.jl | 2 +- src/analysis/sampling/affine_hit_and_run.jl | 2 +- src/analysis/variability_analysis.jl | 4 ++-- src/io/h5.jl | 2 +- src/reconstruction/enzyme_constrained.jl | 2 +- .../simplified_enzyme_constrained.jl | 2 +- src/solver.jl | 4 ++-- src/types/abstract/AbstractMetabolicModel.jl | 10 ++++---- src/types/accessors/AbstractMetabolicModel.jl | 23 +++++++++++-------- src/types/models/CommunityModel.jl | 6 ++--- src/types/models/HDF5Model.jl | 2 +- src/types/models/JSONModel.jl | 4 ++-- src/types/models/MATModel.jl | 4 ++-- src/types/models/MatrixModel.jl | 5 ++-- src/types/models/ObjectModel.jl | 4 ++-- src/types/models/SBMLModel.jl | 4 ++-- .../wrappers/EqualGrowthCommunityModel.jl | 4 ++-- src/wrappers/EnzymeConstrainedModel.jl | 4 ++-- src/wrappers/MaxMinDrivingForceModel.jl | 2 +- .../SimplifiedEnzymeConstrainedModel.jl | 2 +- test/analysis/sampling/affine_hit_and_run.jl | 2 +- test/io/h5.jl | 4 ++-- test/io/json.jl | 4 ++-- test/io/sbml.jl | 4 ++-- test/reconstruction/MatrixModel.jl | 4 ++-- test/types/CommunityModel.jl | 2 +- test/types/ObjectModel.jl | 6 ++--- test/types/abstract/AbstractMetabolicModel.jl | 1 + 31 files changed, 71 insertions(+), 64 deletions(-) diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index 50a1ad45c..ba24cb0a8 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -19,7 +19,7 @@ that work over the abstract type [`AbstractMetabolicModel`](@ref). To use your d structure in a model, you just make it a subtype of [`AbstractMetabolicModel`](@ref) and overload the required accessors. The accessors are functions that extract some relevant information, such as [`stoichiometry`](@ref) and -[`bounds`](@ref), returning a fixed simple data type that can be further used +[`variable_bounds`](@ref), returning a fixed simple data type that can be further used by COBREXA. You may see a complete list of accessors [here](../functions.md#Base-Types). @@ -68,7 +68,7 @@ function COBREXA.objective(m::CircularModel) return c end -COBREXA.bounds(m::CircularModel) = ( +COBREXA.variable_bounds(m::CircularModel) = ( zeros(reaction_count(m)), # lower bounds ones(reaction_count(m)), # upper bounds ) diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index c9aa3eb17..9b60f062e 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -52,7 +52,7 @@ flux_balance_analysis_vec(m, GLPK.Optimizer) ``` To modify the functionality, we simply add specific methods for accessors that -we want modified, such as [`bounds`](@ref), [`stoichiometry`](@ref) and +we want modified, such as [`variable_bounds`](@ref), [`stoichiometry`](@ref) and [`objective`](@ref). We demonstrate that on several examples below. ## Example 1: Slower model @@ -73,8 +73,8 @@ wrapped model, and modify them in a certain way. ```julia COBREXA.unwrap_model(x::RateChangedModel) = x.mdl -function COBREXA.bounds(x::RateChangedModel) - (l, u) = bounds(x.mdl) # extract the original bounds +function COBREXA.variable_bounds(x::RateChangedModel) + (l, u) = variable_bounds(x.mdl) # extract the original bounds return (l .* x.factor, u .* x.factor) # return customized bounds end ``` @@ -109,8 +109,8 @@ COBREXA.unwrap_model(x::LeakyModel) = x.mdl COBREXA.reaction_count(x::LeakyModel) = reaction_count(x.mdl) + 1 COBREXA.reaction_ids(x::LeakyModel) = [reaction_ids(x.mdl); "The Leak"] COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] -function COBREXA.bounds(x::LeakyModel) - (l, u) = bounds(x.mdl) +function COBREXA.variable_bounds(x::LeakyModel) + (l, u) = variable_bounds(x.mdl) return ([l; x.leak_rate], [u; x.leak_rate]) end ``` diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl index 1cc68a7bf..0eb0ad333 100644 --- a/src/analysis/envelopes.jl +++ b/src/analysis/envelopes.jl @@ -19,7 +19,7 @@ envelope_lattice( model::AbstractMetabolicModel, ridxs::Vector{Int}; samples = 10, - ranges = collect(zip(bounds(model)...))[ridxs], + ranges = collect(zip(variable_bounds(model)...))[ridxs], reaction_samples = fill(samples, length(ridxs)), ) = ( lb .+ (ub - lb) .* ((1:s) .- 1) ./ max(s - 1, 1) for diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl index 76e88a776..c0b6ef5ce 100644 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ b/src/analysis/reconstruction/gapfill_minimum_reactions.jl @@ -70,7 +70,7 @@ function gapfill_minimum_reactions( # keep this in sync with src/base/solver.jl, except for adding balances. opt_model = Model(optimizer) @variable(opt_model, x[1:variable_count(model)]) - xl, xu = bounds(model) + xl, xu = variable_bounds(model) @constraint(opt_model, lbs, xl .<= x) @constraint(opt_model, ubs, x .<= xu) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index b78afbf93..590c39335 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -40,7 +40,7 @@ function affine_hit_and_run( ) @assert size(warmup_points, 1) == variable_count(m) - lbs, ubs = bounds(m) + lbs, ubs = variable_bounds(m) C = coupling(m) cl, cu = coupling_bounds(m) if isnothing(C) diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 99dce7e5c..10f1dbf94 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -7,8 +7,8 @@ each reaction flux `f` described by [`reactions`](@ref): min,max fᵀxᵢ s.t. S x = b xₗ ≤ x ≤ xᵤ - cᵀx ≥ bounds(Z₀)[1] - cᵀx ≤ bounds(Z₀)[2] + cᵀx ≥ variable_bounds(Z₀)[1] + cᵀx ≤ variable_bounds(Z₀)[2] ``` where Z₀:= cᵀx₀ is the objective value of an optimal solution of the associated FBA problem (see [`flux_balance_analysis`](@ref) for a related analysis, also diff --git a/src/io/h5.jl b/src/io/h5.jl index 2048a6ecd..553ab2d30 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -31,7 +31,7 @@ function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Mo h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) - let (lbs, ubs) = bounds(model) + let (lbs, ubs) = variable_bounds(model) write(f, "lower_bounds", lbs[rxnp]) write(f, "upper_bounds", ubs[rxnp]) end diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 8804c7470..935975bc7 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -61,7 +61,7 @@ function make_enzyme_constrained_model( coupling_row_gene_product = Int[] gids = genes(model) - (lbs, ubs) = bounds(model) + (lbs, ubs) = variable_bounds(model) rids = variable_ids(model) gene_name_lookup = Dict(gids .=> 1:length(gids)) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index a23c31c6f..e69c95f6e 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -73,7 +73,7 @@ function make_simplified_enzyme_constrained_model( bound_ids = keys(reaction_mass_group_bounds) total_reaction_mass_bounds = collect(values(reaction_mass_group_bounds)) - (lbs, ubs) = bounds(model) # TODO need a reaction_bounds accessor for full generality + (lbs, ubs) = variable_bounds(model) # TODO need a reaction_bounds accessor for full generality rids = variable_ids(model) # TODO needs to be reactions for i = 1:variable_count(model) # TODO this should be reactions diff --git a/src/solver.jl b/src/solver.jl index 5e011e80e..8df095333 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -40,7 +40,7 @@ function make_optimization_model( @variable(optimization_model, x[1:n]) # bound the variables - xl, xu = bounds(model) + xl, xu = variable_bounds(model) @constraint(optimization_model, lbs, xl .<= x) # lower bounds @constraint(optimization_model, ubs, x .<= xu) # upper bounds @@ -169,7 +169,7 @@ solved_objective_value(x::ModelWithResult{<:Model}) = solved_objective_value(x.r $(TYPEDSIGNATURES) Return a vector of all variable values from the solved model, in the same order -given by [`variables`](@ref). +given by [`variable_ids`](@ref). # Example ``` diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl index 53f26f6cf..dd33cb164 100644 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ b/src/types/abstract/AbstractMetabolicModel.jl @@ -5,11 +5,11 @@ A helper supertype of everything usable as a linear-like model for COBREXA functions. -If you want your model type to work with COBREXA, add the `AbstractMetabolicModel` as -its supertype, and implement the accessor functions. Accessors -[`variables`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), -[`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not -mandatory and default to safe "empty" values. +If you want your model type to work with COBREXA, add the +`AbstractMetabolicModel` as its supertype, and implement the accessor +functions. Accessors [`variable_ids`](@ref), [`metabolites`](@ref), +[`stoichiometry`](@ref), [`variable_bounds`](@ref) and [`objective`](@ref) must +be implemented; others are not mandatory and default to safe "empty" values. """ abstract type AbstractMetabolicModel end diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 8aefbb090..f29db2fd3 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -43,6 +43,20 @@ end """ $(TYPEDSIGNATURES) +Get the lower and upper solution bounds of a model. +""" +function variable_bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} + missing_impl_error(bounds, (a,)) +end + +""" +Shortcut for writing [`variable_bounds`](@ref). +""" +const bounds = variable_bounds + +""" +$(TYPEDSIGNATURES) + Return a vector of metabolite identifiers in a model. The vector precisely corresponds to the rows in [`stoichiometry`](@ref) matrix. @@ -74,15 +88,6 @@ end """ $(TYPEDSIGNATURES) -Get the lower and upper solution bounds of a model. -""" -function bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - missing_impl_error(bounds, (a,)) -end - -""" -$(TYPEDSIGNATURES) - Get the sparse balance vector of a model. """ function balance(a::AbstractMetabolicModel)::SparseVec diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 430641663..0a0549106 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -147,9 +147,9 @@ function Accessors.stoichiometry(cm::CommunityModel) ] end -function Accessors.bounds(cm::CommunityModel) - models_lbs = vcat([first(bounds(m.model)) for m in values(cm.members)]...) - models_ubs = vcat([last(bounds(m.model)) for m in values(cm.members)]...) +function Accessors.variable_bounds(cm::CommunityModel) + models_lbs = vcat([first(variable_bounds(m.model)) for m in values(cm.members)]...) + models_ubs = vcat([last(variable_bounds(m.model)) for m in values(cm.members)]...) env_lbs = [envlink.lower_bound for envlink in cm.environmental_links] env_ubs = [envlink.upper_bound for envlink in cm.environmental_links] diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index 69df3ed4e..65a020186 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -61,7 +61,7 @@ function Accessors.stoichiometry(model::HDF5Model)::SparseMat h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end -function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} +function Accessors.variable_bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} precache!(model) (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) end diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index fc406037b..1f49b6b84 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -124,7 +124,7 @@ function Accessors.stoichiometry(model::JSONModel) return SparseArrays.sparse(MI, RI, SV, length(met_ids), length(rxn_ids)) end -Accessors.bounds(model::JSONModel) = ( +Accessors.variable_bounds(model::JSONModel) = ( [get(rxn, "lower_bound", -constants.default_reaction_bound) for rxn in model.rxns], [get(rxn, "upper_bound", constants.default_reaction_bound) for rxn in model.rxns], ) @@ -208,7 +208,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) met_ids = metabolites(mm) gene_ids = genes(mm) S = stoichiometry(mm) - lbs, ubs = bounds(mm) + lbs, ubs = variable_bounds(mm) ocs = objective(mm) json = Dict{String,Any}() diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index d512f069d..5494a06d0 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -38,7 +38,7 @@ end Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) -Accessors.bounds(m::MATModel) = ( +Accessors.variable_bounds(m::MATModel) = ( reshape(get(m.mat, "lb", fill(-Inf, variable_count(m), 1)), variable_count(m)), reshape(get(m.mat, "ub", fill(Inf, variable_count(m), 1)), variable_count(m)), ) @@ -155,7 +155,7 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) return m end - lb, ub = bounds(m) + lb, ub = variable_bounds(m) cl, cu = coupling_bounds(m) nr = variable_count(m) nm = n_metabolites(m) diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 8286999f5..82547b7b0 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -55,7 +55,8 @@ Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S -Accessors.bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) +Accessors.variable_bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = + (a.xl, a.xu) Accessors.balance(a::MatrixModel)::SparseVec = a.b @@ -95,7 +96,7 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode return m end - (xl, xu) = bounds(m) + (xl, xu) = variable_bounds(m) MatrixModel( stoichiometry(m), balance(m), diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 381b79bab..eb89b18be 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -104,7 +104,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), variable_count(model)) end -Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = +Accessors.variable_bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) @@ -212,7 +212,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) end S = stoichiometry(model) - lbs, ubs = bounds(model) + lbs, ubs = variable_bounds(model) obj_idxs, obj_vals = findnz(objective(model)) modelobjective = Dict(k => v for (k, v) in zip(variable_ids(model)[obj_idxs], obj_vals)) for (i, rid) in enumerate(rxnids) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 0d9cc1b4a..03ef7d5e5 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -81,7 +81,7 @@ function Accessors.stoichiometry(model::SBMLModel)::SparseMat return sparse(Rows, Cols, Vals, n_metabolites(model), reaction_count(model)) end -function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} +function Accessors.variable_bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} # There are multiple ways in SBML to specify a lower/upper bound. There are # the "global" model bounds that we completely ignore now because no one # uses them. In reaction, you can specify the bounds using "LOWER_BOUND" @@ -282,7 +282,7 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) mets = metabolites(mm) rxns = variable_ids(mm) stoi = stoichiometry(mm) - (lbs, ubs) = bounds(mm) + (lbs, ubs) = variable_bounds(mm) comps = default.("compartment", metabolite_compartment.(Ref(mm), mets)) compss = Set(comps) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 0ae333971..30d0076d6 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -62,8 +62,8 @@ function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) ] end -function Accessors.bounds(cm::EqualGrowthCommunityModel) - lbs, ubs = bounds(cm.inner) +function Accessors.variable_bounds(cm::EqualGrowthCommunityModel) + lbs, ubs = variable_bounds(cm.inner) return ([lbs; 0], [ubs; constants.default_reaction_bound]) end diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 8115a1fa0..4143c1bbe 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -89,7 +89,7 @@ end Accessors.variable_count(model::EnzymeConstrainedModel) = length(model.columns) + n_genes(model) -function Accessors.bounds(model::EnzymeConstrainedModel) +function Accessors.variable_bounds(model::EnzymeConstrainedModel) lbs = [ [col.lb for col in model.columns] [lb for (_, (lb, _)) in model.coupling_row_gene_product] @@ -157,7 +157,7 @@ Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = function Accessors.coupling_bounds(model::EnzymeConstrainedModel) (iclb, icub) = coupling_bounds(model.inner) - (ilb, iub) = bounds(model.inner) + (ilb, iub) = variable_bounds(model.inner) return ( vcat( iclb, diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index 4eda007b1..e0a52e8eb 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -175,7 +175,7 @@ function Accessors.stoichiometry(model::MaxMinDrivingForceModel) ] end -function Accessors.bounds(model::MaxMinDrivingForceModel) +function Accessors.variable_bounds(model::MaxMinDrivingForceModel) var_ids = Internal.original_variables(model) lbs = fill(-model.max_dg_bound, variable_count(model)) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 5b3eb620b..6dc812199 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -73,7 +73,7 @@ Accessors.variable_ids(model::SimplifiedEnzymeConstrainedModel) = Accessors.variable_count(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) -Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = +Accessors.variable_bounds(model::SimplifiedEnzymeConstrainedModel) = ([col.lb for col in model.columns], [col.ub for col in model.columns]) """ diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl index 949fe6466..be9cb2b39 100644 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ b/test/analysis/sampling/affine_hit_and_run.jl @@ -20,7 +20,7 @@ @test size(samples, 1) == size(warmup, 1) @test size(samples, 2) == size(warmup, 2) * 3 * length(W) - lbs, ubs = bounds(model) + lbs, ubs = variable_bounds(model) @test all(samples .>= lbs) @test all(samples .<= ubs) @test all(cm.C * samples .>= cm.cl) diff --git a/test/io/h5.jl b/test/io/h5.jl index fd87b8f65..59f26028b 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -21,8 +21,8 @@ @test size(stoichiometry(model)) == size(stoichiometry(h5)) @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) rxnp = sortperm(variable_ids(model)) - @test bounds(model)[1][rxnp] == bounds(h5)[1] - @test bounds(model)[2][rxnp] == bounds(h5)[2] + @test variable_bounds(model)[1][rxnp] == variable_bounds(h5)[1] + @test variable_bounds(model)[2][rxnp] == variable_bounds(h5)[2] @test objective(model)[rxnp] == objective(h5) @test all(iszero, balance(h5)) diff --git a/test/io/json.jl b/test/io/json.jl index a10961286..3014a9516 100644 --- a/test/io/json.jl +++ b/test/io/json.jl @@ -9,8 +9,8 @@ @test issetequal(genes(jsonmodel), genes(stdmodel)) # not the best tests since it is possible that error could cancel each other out: @test sum(stoichiometry(jsonmodel)) == sum(stoichiometry(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(jsonmodel) + jlbs, jubs = variable_bounds(jsonmodel) + slbs, subs = variable_bounds(jsonmodel) @test sum(jlbs) == sum(slbs) @test sum(jubs) == sum(subs) end diff --git a/test/io/sbml.jl b/test/io/sbml.jl index e72f3255d..a4c7a2d2a 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -6,8 +6,8 @@ @test size(stoichiometry(sbmlm)) == (92, 95) @test size(stoichiometry(m)) == (n_metabolites(sbmlm), variable_count(sbmlm)) @test length(m.S.nzval) == 380 - @test length.(bounds(sbmlm)) == (95, 95) - @test length.(bounds(m)) == (95, 95) + @test length.(variable_bounds(sbmlm)) == (95, 95) + @test length.(variable_bounds(m)) == (95, 95) @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) @test metabolites(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index 59baeaaf9..e4bcaf5b8 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -226,8 +226,8 @@ end @test stoichiometry(modLp) == stoichiometry(lp)[:, 2:3] @test balance(modLp) == balance(lp) @test objective(modLp) == objective(lp)[2:3] - @test bounds(modLp)[1] == bounds(lp)[1][2:3] - @test bounds(modLp)[2] == bounds(lp)[2][2:3] + @test variable_bounds(modLp)[1] == variable_bounds(lp)[1][2:3] + @test variable_bounds(modLp)[2] == variable_bounds(lp)[2][2:3] @test variable_ids(modLp) == variable_ids(lp)[2:3] @test metabolites(modLp) == metabolites(lp) end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index 1ccbea11f..e46014c35 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -128,7 +128,7 @@ ], ) - lbs, ubs = bounds(cm) + lbs, ubs = variable_bounds(cm) @test all(lbs .== [ -1000.0 -1000.0 diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index 8709a5b68..f17870eca 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -74,7 +74,7 @@ ub_test[2] = 0.0 ub_test[3] = 1000.0 ub_test[4] = 1000.0 - lbs, ubs = bounds(model) + lbs, ubs = variable_bounds(model) @test lb_test == lbs @test ub_test == ubs @@ -135,8 +135,8 @@ @test issetequal(variable_ids(jsonmodel), variable_ids(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(stdmodel) + jlbs, jubs = variable_bounds(jsonmodel) + slbs, subs = variable_bounds(stdmodel) @test issetequal(jlbs, slbs) @test issetequal(jubs, subs) jS = stoichiometry(jsonmodel) diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index d3ae3c225..c5efe760a 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -14,6 +14,7 @@ end @testset "ID shortcuts are identictical with the ID-generating functions" begin @test variables === variable_ids + @test bounds === variable_bounds @test reactions === reaction_ids # TODO don't forget about metabolites later end From 6272230eeaecd3fd6f5d5a556b378680676390c8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 15:50:26 +0200 Subject: [PATCH 294/531] fix the typecheck of the returned bounds --- src/solver.jl | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 8df095333..fecd0e70e 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -60,11 +60,11 @@ function make_optimization_model( bounds = sem.bounds(model) if isnothing(bounds) continue - elseif bounds isa Vector{Float64} + elseif typeof(bounds) <: AbstractVector{Float64} # equality bounds c = @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) set_name.(c, "$(semname)_eqs") - elseif bounds isa Tuple{Vector{Float64},Vector{Float64}} + elseif typeof(bounds) <: Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}} # lower/upper interval bounds slb, sub = bounds smtx = sem.mapping_matrix(model) @@ -78,8 +78,12 @@ function make_optimization_model( TypeError( :make_optimization_model, "conversion of $(typeof(model)) bounds", + Union{ + Nothing, + <:AbstractVector{Float64}, + Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}}, + }, typeof(bounds), - Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}}, ), ) end From cb45c4a7f602d084d5e6fa8574f8721ff6e80d8c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 15:11:29 +0200 Subject: [PATCH 295/531] the cut (probably needs fixes at this point) --- docs/src/concepts/3_custom_models.md | 4 +- docs/src/concepts/4_wrappers.md | 2 +- docs/src/examples/02_convert_save.jl | 2 +- docs/src/examples/04_standardmodel.jl | 2 +- .../gapfill_minimum_reactions.jl | 4 +- src/io/h5.jl | 4 +- src/io/show/AbstractMetabolicModel.jl | 2 +- src/reconstruction/MatrixCoupling.jl | 4 +- src/reconstruction/MatrixModel.jl | 10 +-- src/solver.jl | 3 - src/types/accessors/AbstractMetabolicModel.jl | 66 +++++++------------ src/types/models/CommunityModel.jl | 12 ++-- src/types/models/HDF5Model.jl | 6 +- src/types/models/JSONModel.jl | 8 +-- src/types/models/MATModel.jl | 39 ++++++----- src/types/models/MatrixModel.jl | 8 +-- src/types/models/ObjectModel.jl | 14 ++-- src/types/models/SBMLModel.jl | 8 +-- .../wrappers/EqualGrowthCommunityModel.jl | 12 ++-- src/utils/fluxes.jl | 2 +- src/utils/looks_like.jl | 8 +-- src/wrappers/EnzymeConstrainedModel.jl | 12 ++-- src/wrappers/MaxMinDrivingForceModel.jl | 20 +++--- src/wrappers/misc/mmdf.jl | 2 +- test/io/h5.jl | 8 +-- test/io/json.jl | 2 +- test/io/mat.jl | 2 +- test/io/sbml.jl | 6 +- test/reconstruction/MatrixModel.jl | 21 +++--- test/reconstruction/ObjectModel.jl | 6 +- test/types/CommunityModel.jl | 4 +- test/types/ObjectModel.jl | 16 ++--- test/types/SBMLModel.jl | 2 +- test/types/abstract/AbstractMetabolicModel.jl | 4 +- test/utils/MatrixModel.jl | 2 +- test/utils/Serialized.jl | 4 +- test/utils/looks_like.jl | 13 ++-- 37 files changed, 166 insertions(+), 178 deletions(-) diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md index ba24cb0a8..12ddf0d60 100644 --- a/docs/src/concepts/3_custom_models.md +++ b/docs/src/concepts/3_custom_models.md @@ -50,10 +50,10 @@ First, define the reactions and metabolites: ```julia COBREXA.reaction_count(m::CircularModel) = m.size -COBREXA.n_metabolites(m::CircularModel) = m.size +COBREXA.metabolite_count(m::CircularModel) = m.size COBREXA.reaction_ids(m::CircularModel) = ["rxn$i" for i in 1:reaction_count(m)] -COBREXA.metabolites(m::CircularModel) = ["met$i" for i in 1:n_metabolites(m)] +COBREXA.metabolite_ids(m::CircularModel) = ["met$i" for i in 1:metabolite_count(m)] ``` It is useful to re-use the already defined functions, as that improves the code diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md index 9b60f062e..4fc52fefa 100644 --- a/docs/src/concepts/4_wrappers.md +++ b/docs/src/concepts/4_wrappers.md @@ -108,7 +108,7 @@ modifying the reaction list, stoichiometry, and bounds: COBREXA.unwrap_model(x::LeakyModel) = x.mdl COBREXA.reaction_count(x::LeakyModel) = reaction_count(x.mdl) + 1 COBREXA.reaction_ids(x::LeakyModel) = [reaction_ids(x.mdl); "The Leak"] -COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] +COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolite_ids(x.mdl)]] function COBREXA.variable_bounds(x::LeakyModel) (l, u) = variable_bounds(x.mdl) return ([l; x.leak_rate], [u; x.leak_rate]) diff --git a/docs/src/examples/02_convert_save.jl b/docs/src/examples/02_convert_save.jl index 564a38acd..99fa22e91 100644 --- a/docs/src/examples/02_convert_save.jl +++ b/docs/src/examples/02_convert_save.jl @@ -41,7 +41,7 @@ open(f -> serialize(f, sm), "myModel.stdmodel", "w") # The models can then be loaded back using `deserialize`: sm2 = deserialize("myModel.stdmodel") -issetequal(metabolites(sm), metabolites(sm2)) +issetequal(metabolite_ids(sm), metabolite_ids(sm2)) # This form of loading operation is usually pretty quick: t = @elapsed deserialize("myModel.stdmodel") diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl index 28c2c1baf..7a28689e2 100644 --- a/docs/src/examples/04_standardmodel.jl +++ b/docs/src/examples/04_standardmodel.jl @@ -76,7 +76,7 @@ model.genes[random_gene_id] # The same idea holds for both metabolites (stored as `Metabolite`s) and # reactions (stored as `Reaction`s). This is demonstrated below. -random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] +random_metabolite_id = metabolite_ids(model)[rand(1:metabolite_count(model))] model.metabolites[random_metabolite_id] # random_reaction_id = variable_ids(model)[rand(1:variable_count(model))] diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl index c0b6ef5ce..714d2d427 100644 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ b/src/analysis/reconstruction/gapfill_minimum_reactions.jl @@ -54,7 +54,7 @@ function gapfill_minimum_reactions( precache!(model) # constraints from universal reactions that can fill gaps - univs = _universal_stoichiometry(universal_reactions, metabolites(model)) + univs = _universal_stoichiometry(universal_reactions, metabolite_ids(model)) # add space for additional metabolites and glue with the universal reaction # stoichiometry @@ -89,7 +89,7 @@ function gapfill_minimum_reactions( @constraint( opt_model, extended_stoichiometry * [x; ux] .== - [balance(model); zeros(length(univs.new_mids))] + [metabolite_bounds(model); zeros(length(univs.new_mids))] ) # objective bounds diff --git a/src/io/h5.jl b/src/io/h5.jl index 553ab2d30..dca31ba9e 100644 --- a/src/io/h5.jl +++ b/src/io/h5.jl @@ -23,12 +23,12 @@ make new HDF5 models. function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Model rxns = variable_ids(model) rxnp = sortperm(rxns) - mets = metabolites(model) + mets = metabolite_ids(model) metp = sortperm(mets) h5open(file_name, "w") do f write(f, "metabolites", mets[metp]) write(f, "reactions", rxns[rxnp]) - h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) + h5_write_sparse(create_group(f, "balance"), metabolite_bounds(model)[metp]) h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) let (lbs, ubs) = variable_bounds(model) diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl index 587b543f9..8c5751651 100644 --- a/src/io/show/AbstractMetabolicModel.jl +++ b/src/io/show/AbstractMetabolicModel.jl @@ -6,6 +6,6 @@ Pretty printing of everything metabolic-modelish. function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) print( io, - "$(typeof(m))(#= $(reaction_count(m)) reactions, $(n_metabolites(m)) metabolites =#)", + "$(typeof(m))(#= $(reaction_count(m)) reactions, $(metabolite_count(m)) metabolites =#)", ) end diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 24d00c3ef..42f10c14f 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -363,7 +363,7 @@ end end @_remove_fn metabolite MatrixCoupling String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) + remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) end @_remove_fn metabolite MatrixCoupling String begin @@ -371,7 +371,7 @@ end end @_remove_fn metabolite MatrixCoupling String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) + remove_metabolites(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) end """ diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 1a661cdf6..2be6fc608 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -12,7 +12,7 @@ function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) ubs = zeros(length(rxns)) for (j, rxn) in enumerate(rxns) req = rxn.metabolites - for (i, v) in zip(indexin(keys(req), metabolites(model)), values(req)) + for (i, v) in zip(indexin(keys(req), metabolite_ids(model)), values(req)) push!(J, j) push!(I, i) push!(V, v) @@ -21,7 +21,7 @@ function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) lbs[j] = rxn.lower_bound ubs[j] = rxn.upper_bound end - Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) + Sadd = sparse(I, J, V, metabolite_count(model), length(rxns)) model.S = [model.S Sadd] model.c = dropzeros([model.c; zeros(length(rxns))]) # does not add an objective info from rxns model.xu = ubs @@ -370,7 +370,7 @@ end any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) ], ) - mask = .!in.(1:n_metabolites(model), Ref(metabolite_idxs)) + mask = .!in.(1:metabolite_count(model), Ref(metabolite_idxs)) model.S = model.S[mask, :] model.b = model.b[mask] model.mets = model.mets[mask] @@ -392,7 +392,7 @@ end end @_remove_fn metabolite MatrixModel String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) + remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) end @_remove_fn metabolite MatrixModel String begin @@ -400,7 +400,7 @@ end end @_remove_fn metabolite MatrixModel String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) + remove_metabolites(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) end """ diff --git a/src/solver.jl b/src/solver.jl index fecd0e70e..bb4732d89 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -89,9 +89,6 @@ function make_optimization_model( end end - # make stoichiometry balanced - @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance - # add coupling constraints C = coupling(model) if !isempty(C) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index f29db2fd3..7336f7f84 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -23,7 +23,7 @@ genetic material and virtual cell volume, etc. To simplify the view of the model contents use [`reaction_variables`](@ref). """ function variable_ids(a::AbstractMetabolicModel)::Vector{String} - missing_impl_error(variables, (a,)) + missing_impl_error(variable_ids, (a,)) end """ @@ -46,7 +46,7 @@ $(TYPEDSIGNATURES) Get the lower and upper solution bounds of a model. """ function variable_bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - missing_impl_error(bounds, (a,)) + missing_impl_error(variable_bounds, (a,)) end """ @@ -57,46 +57,6 @@ const bounds = variable_bounds """ $(TYPEDSIGNATURES) -Return a vector of metabolite identifiers in a model. The vector precisely -corresponds to the rows in [`stoichiometry`](@ref) matrix. - -As with [`variables`](@ref)s, some metabolites in models may be virtual, -representing purely technical equality constraints. -""" -function metabolites(a::AbstractMetabolicModel)::Vector{String} - missing_impl_error(metabolites, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of metabolites in a model. -""" -function n_metabolites(a::AbstractMetabolicModel)::Int - length(metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse stoichiometry matrix of a model. -""" -function stoichiometry(a::AbstractMetabolicModel)::SparseMat - missing_impl_error(stoichiometry, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse balance vector of a model. -""" -function balance(a::AbstractMetabolicModel)::SparseVec - return spzeros(n_metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - Get the linear objective vector or the quadratic objective affine matrix of the model. @@ -131,6 +91,26 @@ Shortcut for writing [`reaction_ids`](@ref). """ const reactions = reaction_ids +@make_variable_semantics( + :metabolite, + "metabolites", + """ +Metabolite values represent the over-time change of abundance of individual +metabolites in the model. To reach a steady state, models typically constraint +these to be zero. +""" +) + +""" +A shortcut for [`metabolite_ids`](@ref). +""" +const metabolites = metabolite_ids + +""" +The usual name of [`metabolite_variables_matrix`](@ref). +""" +const stoichiometry = metabolite_variables_matrix + @make_variable_semantics( :enzyme, "enzyme supplies", @@ -307,7 +287,7 @@ function reaction_stoichiometry( m::AbstractMetabolicModel, rid::String, )::Dict{String,Float64} - mets = metabolites(m) + mets = metabolite_ids(m) Dict( mets[k] => v for (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], variable_ids(m)))])...) diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl index 0a0549106..c81afdf82 100644 --- a/src/types/models/CommunityModel.jl +++ b/src/types/models/CommunityModel.jl @@ -109,16 +109,16 @@ function Accessors.variable_count(cm::CommunityModel) return num_model_reactions + num_env_metabolites end -function Accessors.metabolites(cm::CommunityModel) +function Accessors.metabolite_ids(cm::CommunityModel) mets = [ cm.name_lookup[id][:metabolites][mid] for (id, m) in cm.members for - mid in metabolites(m.model) + mid in metabolite_ids(m.model) ] return [mets; "ENV_" .* [envlink.metabolite_id for envlink in cm.environmental_links]] end -function Accessors.n_metabolites(cm::CommunityModel) - num_model_constraints = sum(n_metabolites(m.model) for m in values(cm.members)) +function Accessors.metabolite_count(cm::CommunityModel) + num_model_constraints = sum(metabolite_count(m.model) for m in values(cm.members)) num_env_metabolites = length(cm.environmental_links) return num_model_constraints + num_env_metabolites end @@ -128,8 +128,8 @@ Accessors.genes(cm::CommunityModel) = Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in values(cm.members)) -Accessors.balance(cm::CommunityModel) = [ - vcat([balance(m.model) for m in values(cm.members)]...) +Accessors.metabolite_bounds(cm::CommunityModel) = [ + vcat([metabolite_bounds(m.model) for m in values(cm.members)]...) spzeros(length(cm.environmental_links)) ] diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index 65a020186..ec4e03b65 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -46,12 +46,12 @@ end Accessors.Internal.@all_variables_are_reactions HDF5Model -function Accessors.n_metabolites(model::HDF5Model)::Int +function Accessors.metabolite_count(model::HDF5Model)::Int precache!(model) length(model.h5["metabolites"]) end -function Accessors.metabolites(model::HDF5Model)::Vector{String} +function Accessors.metabolite_ids(model::HDF5Model)::Vector{String} precache!(model) read(model.h5["metabolites"]) end @@ -66,7 +66,7 @@ function Accessors.variable_bounds(model::HDF5Model)::Tuple{Vector{Float64},Vect (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) end -function Accessors.balance(model::HDF5Model)::SparseVec +function Accessors.metabolite_bounds(model::HDF5Model)::SparseVec precache!(model) h5_read_sparse(SparseVec, model.h5["balance"]) end diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 1f49b6b84..47ab32579 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -73,14 +73,14 @@ _parse_notes(x)::Notes = _parse_annotations(x) Accessors.variable_count(model::JSONModel) = length(model.rxns) -Accessors.n_metabolites(model::JSONModel) = length(model.mets) +Accessors.metabolite_count(model::JSONModel) = length(model.mets) Accessors.n_genes(model::JSONModel) = length(model.genes) Accessors.variable_ids(model::JSONModel) = [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] -Accessors.metabolites(model::JSONModel) = +Accessors.metabolite_ids(model::JSONModel) = [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] Accessors.genes(model::JSONModel) = @@ -90,7 +90,7 @@ Accessors.Internal.@all_variables_are_reactions JSONModel function Accessors.stoichiometry(model::JSONModel) rxn_ids = variable_ids(model) - met_ids = metabolites(model) + met_ids = metabolite_ids(model) n_entries = 0 for r in model.rxns @@ -205,7 +205,7 @@ function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) end rxn_ids = variable_ids(mm) - met_ids = metabolites(mm) + met_ids = metabolite_ids(mm) gene_ids = genes(mm) S = stoichiometry(mm) lbs, ubs = variable_bounds(mm) diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 5494a06d0..1dbaad206 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -10,7 +10,7 @@ struct MATModel <: AbstractMetabolicModel mat::Dict{String,Any} end -Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) +Accessors.metabolite_count(m::MATModel)::Int = size(m.mat["S"], 1) Accessors.variable_count(m::MATModel)::Int = size(m.mat["S"], 2) function Accessors.variable_ids(m::MATModel)::Vector{String} @@ -27,12 +27,12 @@ _mat_has_squashed_coupling(mat) = haskey(mat, "A") && haskey(mat, "b") && length(mat["b"]) == size(mat["A"], 1) -function Accessors.metabolites(m::MATModel)::Vector{String} - nm = n_metabolites(m) +function Accessors.metabolite_ids(m::MATModel)::Vector{String} + nm = metabolite_count(m) if haskey(m.mat, "mets") reshape(m.mat["mets"], length(m.mat["mets"]))[begin:nm] else - "met" .* string.(1:n_metabolites(m)) + "met" .* string.(1:metabolite_count(m)) end end @@ -43,12 +43,12 @@ Accessors.variable_bounds(m::MATModel) = ( reshape(get(m.mat, "ub", fill(Inf, variable_count(m), 1)), variable_count(m)), ) -function Accessors.balance(m::MATModel) - b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) +function Accessors.metabolite_bounds(m::MATModel) + b = get(m.mat, "b", spzeros(metabolite_count(m), 1)) if _mat_has_squashed_coupling(m.mat) - b = b[1:n_metabolites(m), :] + b = b[1:metabolite_count(m), :] end - sparse(reshape(b, n_metabolites(m))) + sparse(reshape(b, metabolite_count(m))) end Accessors.objective(m::MATModel) = @@ -97,13 +97,13 @@ function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwar end Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( - x -> parse_formula(x[findfirst(==(mid), metabolites(m))]), + x -> parse_formula(x[findfirst(==(mid), metabolite_ids(m))]), gets(m.mat, nothing, constants.keynames.metformulas), ) function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} met_charge = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], + x -> x[findfirst(==(mid), metabolite_ids(m))], gets(m.mat, nothing, constants.keynames.metcharges), ) maybemap(Int, isnan(met_charge) ? nothing : met_charge) @@ -111,7 +111,7 @@ end function Accessors.metabolite_compartment(m::MATModel, mid::String) res = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], + x -> x[findfirst(==(mid), metabolite_ids(m))], gets(m.mat, nothing, constants.keynames.metcompartments), ) # if the metabolite is an integer or a (very integerish) float, it is an @@ -139,7 +139,7 @@ Accessors.reaction_name(m::MATModel, rid::String) = maybemap( ) Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], + x -> x[findfirst(==(mid), metabolite_ids(m))], gets(m.mat, nothing, constants.keynames.metnames), ) @@ -158,15 +158,15 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) lb, ub = variable_bounds(m) cl, cu = coupling_bounds(m) nr = variable_count(m) - nm = n_metabolites(m) + nm = metabolite_count(m) return MATModel( Dict( "S" => stoichiometry(m), "rxns" => variable_ids(m), - "mets" => metabolites(m), + "mets" => metabolite_ids(m), "lb" => Vector(lb), "ub" => Vector(ub), - "b" => Vector(balance(m)), + "b" => Vector(metabolite_bounds(m)), "c" => Vector(objective(m)), "C" => coupling(m), "cl" => Vector(cl), @@ -183,11 +183,14 @@ function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) "metFormulas" => default.( "", - maybemap.(unparse_formula, metabolite_formula.(Ref(m), metabolites(m))), + maybemap.( + unparse_formula, + metabolite_formula.(Ref(m), metabolite_ids(m)), + ), ), - "metCharges" => default.(0, metabolite_charge.(Ref(m), metabolites(m))), + "metCharges" => default.(0, metabolite_charge.(Ref(m), metabolite_ids(m))), "metCompartments" => - default.("", metabolite_compartment.(Ref(m), metabolites(m))), + default.("", metabolite_compartment.(Ref(m), metabolite_ids(m))), ), ) end diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 82547b7b0..7290dd599 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -51,14 +51,14 @@ Accessors.variable_ids(a::MatrixModel)::Vector{String} = a.rxns Accessors.Internal.@all_variables_are_reactions MatrixModel -Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets +Accessors.metabolite_ids(a::MatrixModel)::Vector{String} = a.mets Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S Accessors.variable_bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) -Accessors.balance(a::MatrixModel)::SparseVec = a.b +Accessors.metabolite_bounds(a::MatrixModel)::SparseVec = a.b Accessors.objective(a::MatrixModel)::SparseVec = a.c @@ -99,12 +99,12 @@ function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicMode (xl, xu) = variable_bounds(m) MatrixModel( stoichiometry(m), - balance(m), + metabolite_bounds(m), objective(m), xl, xu, variable_ids(m), - metabolites(m), + metabolite_ids(m), Vector{Maybe{GeneAssociationsDNF}}([ reaction_gene_associations(m, id) for id in variable_ids(m) ]), diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index eb89b18be..87fe4f03e 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -60,9 +60,10 @@ Accessors.variable_count(model::ObjectModel)::Int = length(model.reactions) Accessors.Internal.@all_variables_are_reactions ObjectModel -Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) +Accessors.metabolite_ids(model::ObjectModel)::StringVecType = + collect(keys(model.metabolites)) -Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) +Accessors.metabolite_count(model::ObjectModel)::Int = length(model.metabolites) Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) @@ -85,7 +86,7 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat # establish the ordering rxns = variable_ids(model) - met_idx = Dict(mid => i for (i, mid) in enumerate(metabolites(model))) + met_idx = Dict(mid => i for (i, mid) in enumerate(metabolite_ids(model))) # fill the matrix entries for (ridx, rid) in enumerate(rxns) @@ -101,13 +102,14 @@ function Accessors.stoichiometry(model::ObjectModel)::SparseMat push!(SV, coeff) end end - return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), variable_count(model)) + return SparseArrays.sparse(MI, RI, SV, metabolite_count(model), variable_count(model)) end Accessors.variable_bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = (lower_bounds(model), upper_bounds(model)) -Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) +Accessors.metabolite_bounds(model::ObjectModel)::SparseVec = + spzeros(length(model.metabolites)) Accessors.objective(model::ObjectModel)::SparseVec = sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) @@ -187,7 +189,7 @@ function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) modelgenes = OrderedDict{String,Gene}() gids = genes(model) - metids = metabolites(model) + metids = metabolite_ids(model) rxnids = variable_ids(model) for gid in gids diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 03ef7d5e5..4f3e23b89 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -42,7 +42,7 @@ Accessors.variable_ids(model::SBMLModel)::Vector{String} = model.reaction_ids Accessors.Internal.@all_variables_are_reactions SBMLModel -Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids +Accessors.metabolite_ids(model::SBMLModel)::Vector{String} = model.metabolite_ids function Accessors.stoichiometry(model::SBMLModel)::SparseMat @@ -78,7 +78,7 @@ function Accessors.stoichiometry(model::SBMLModel)::SparseMat push!(Vals, isnothing(sr.stoichiometry) ? 1.0 : sr.stoichiometry) end end - return sparse(Rows, Cols, Vals, n_metabolites(model), reaction_count(model)) + return sparse(Rows, Cols, Vals, metabolite_count(model), reaction_count(model)) end function Accessors.variable_bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} @@ -134,7 +134,7 @@ function Accessors.variable_bounds(model::SBMLModel)::Tuple{Vector{Float64},Vect ) end -Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) +Accessors.metabolite_bounds(model::SBMLModel)::SparseVec = spzeros(metabolite_count(model)) function Accessors.objective(model::SBMLModel)::SparseVec res = sparsevec([], [], reaction_count(model)) @@ -279,7 +279,7 @@ function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) return mm end - mets = metabolites(mm) + mets = metabolite_ids(mm) rxns = variable_ids(mm) stoi = stoichiometry(mm) (lbs, ubs) = variable_bounds(mm) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index 30d0076d6..e987ace65 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -24,14 +24,14 @@ Accessors.variable_ids(cm::EqualGrowthCommunityModel) = Accessors.variable_count(cm::EqualGrowthCommunityModel) = variable_count(cm.inner) + 1 -Accessors.metabolites(cm::EqualGrowthCommunityModel) = - [metabolites(cm.inner); [m.id for m in cm.inner.members]] +Accessors.metabolite_ids(cm::EqualGrowthCommunityModel) = + [metabolite_ids(cm.inner); [m.id for m in cm.inner.members]] -Accessors.n_metabolites(cm::EqualGrowthCommunityModel) = - n_metabolites(cm.inner) + length(cm.inner.members) +Accessors.metabolite_count(cm::EqualGrowthCommunityModel) = + metabolite_count(cm.inner) + length(cm.inner.members) -Accessors.balance(cm::EqualGrowthCommunityModel) = [ - balance(cm.inner) +Accessors.metabolite_bounds(cm::EqualGrowthCommunityModel) = [ + metabolite_bounds(cm.inner) spzeros(length(cm.inner.members)) ] diff --git a/src/utils/fluxes.jl b/src/utils/fluxes.jl index b95dc8643..ee097d829 100644 --- a/src/utils/fluxes.jl +++ b/src/utils/fluxes.jl @@ -7,7 +7,7 @@ produce them, given the flux distribution supplied in `flux_dict`. function metabolite_fluxes(model::AbstractMetabolicModel, flux_dict::Dict{String,Float64}) S = stoichiometry(model) rids = variable_ids(model) - mids = metabolites(model) + mids = metabolite_ids(model) producing = Dict{String,Dict{String,Float64}}() consuming = Dict{String,Dict{String,Float64}}() diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index ce4fce0d3..a215776e9 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -111,8 +111,8 @@ metabolite id. # Example ``` -filter(looks_like_extracellular_metabolite, metabolites(model)) # returns strings -findall(looks_like_extracellular_metabolite, metabolites(model)) # returns indices +filter(looks_like_extracellular_metabolite, metabolite_ids(model)) # returns strings +findall(looks_like_extracellular_metabolite, metabolite_ids(model)) # returns indices ``` """ function looks_like_extracellular_metabolite( @@ -129,7 +129,7 @@ Shortcut for finding extracellular metabolite indexes in a model; arguments are forwarded to [`looks_like_extracellular_metabolite`](@ref). """ find_extracellular_metabolites(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) + findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolite_ids(m)) """ $(TYPEDSIGNATURES) @@ -138,7 +138,7 @@ Shortcut for finding extracellular metabolite identifiers in a model; arguments forwarded to [`looks_like_extracellular_metabolite`](@ref). """ find_extracellular_metabolite_ids(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) + findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolite_ids(m)) @_is_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS @_is_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 4143c1bbe..70d165b89 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -172,16 +172,16 @@ function Accessors.coupling_bounds(model::EnzymeConstrainedModel) ) end -Accessors.balance(model::EnzymeConstrainedModel) = - [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] +Accessors.metabolite_bounds(model::EnzymeConstrainedModel) = + [metabolite_bounds(model.inner); spzeros(length(model.coupling_row_gene_product))] Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) Accessors.genes(model::EnzymeConstrainedModel) = genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] -Accessors.metabolites(model::EnzymeConstrainedModel) = - [metabolites(model.inner); genes(model) .* "#enzyme_constrained"] +Accessors.metabolite_ids(model::EnzymeConstrainedModel) = + [metabolite_ids(model.inner); genes(model) .* "#enzyme_constrained"] -Accessors.n_metabolites(model::EnzymeConstrainedModel) = - n_metabolites(model.inner) + n_genes(model) +Accessors.metabolite_count(model::EnzymeConstrainedModel) = + metabolite_count(model.inner) + n_genes(model) diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl index e0a52e8eb..e1ac2ec4a 100644 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ b/src/wrappers/MaxMinDrivingForceModel.jl @@ -82,17 +82,17 @@ end Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner Accessors.variable_ids(model::MaxMinDrivingForceModel) = - ["mmdf"; "log " .* metabolites(model); "ΔG " .* reaction_ids(model)] + ["mmdf"; "log " .* metabolite_ids(model); "ΔG " .* reaction_ids(model)] Accessors.variable_count(model::MaxMinDrivingForceModel) = - 1 + n_metabolites(model) + reaction_count(model) + 1 + metabolite_count(model) + reaction_count(model) Accessors.metabolite_log_concentration_ids(model::MaxMinDrivingForceModel) = - "log " .* metabolites(model) + "log " .* metabolite_ids(model) Accessors.metabolite_log_concentration_count(model::MaxMinDrivingForceModel) = - n_metabolites(model) + metabolite_count(model) Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = - Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) + Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolite_ids(model)) Accessors.gibbs_free_energy_ids(model::MaxMinDrivingForceModel) = "ΔG " .* reaction_ids(model) @@ -104,7 +104,7 @@ Accessors.gibbs_free_energy_variables(model::MaxMinDrivingForceModel) = Accessors.objective(model::MaxMinDrivingForceModel) = [1.0; fill(0.0, variable_count(model) - 1)] -function Accessors.balance(model::MaxMinDrivingForceModel) +function Accessors.metabolite_bounds(model::MaxMinDrivingForceModel) # proton water balance num_proton_water = length(model.proton_ids) + length(model.water_ids) proton_water_vec = spzeros(num_proton_water) @@ -186,8 +186,8 @@ function Accessors.variable_bounds(model::MaxMinDrivingForceModel) ubs[1] = 1000.0 # log concentrations - lbs[2:(1+n_metabolites(model))] .= log(model.concentration_lb) - ubs[2:(1+n_metabolites(model))] .= log(model.concentration_ub) + lbs[2:(1+metabolite_count(model))] .= log(model.concentration_lb) + ubs[2:(1+metabolite_count(model))] .= log(model.concentration_ub) # need to make special adjustments for the constants idxs = indexin([model.proton_ids; model.water_ids], var_ids) @@ -212,12 +212,12 @@ function Accessors.coupling(model::MaxMinDrivingForceModel) end neg_dg_mat = [ - spzeros(length(idxs)) spzeros(length(idxs), n_metabolites(model)) flux_signs + spzeros(length(idxs)) spzeros(length(idxs), metabolite_count(model)) flux_signs ] mmdf_mat = sparse( [ - -ones(length(idxs)) spzeros(length(idxs), n_metabolites(model)) -flux_signs + -ones(length(idxs)) spzeros(length(idxs), metabolite_count(model)) -flux_signs ], ) diff --git a/src/wrappers/misc/mmdf.jl b/src/wrappers/misc/mmdf.jl index fc0c92c04..c74630982 100644 --- a/src/wrappers/misc/mmdf.jl +++ b/src/wrappers/misc/mmdf.jl @@ -20,4 +20,4 @@ $(TYPEDSIGNATURES) Helper function that returns the unmangled variable IDs. """ original_variables(model::MaxMinDrivingForceModel) = - ["mmdf"; metabolites(model); reaction_ids(model)] + ["mmdf"; metabolite_ids(model); reaction_ids(model)] diff --git a/test/io/h5.jl b/test/io/h5.jl index 59f26028b..da687fe65 100644 --- a/test/io/h5.jl +++ b/test/io/h5.jl @@ -13,10 +13,10 @@ # briefly test that the loading is okay @test variable_count(model) == variable_count(h5) - @test n_metabolites(model) == n_metabolites(h5) + @test metabolite_count(model) == metabolite_count(h5) @test issetequal(variable_ids(model), variable_ids(h5)) - @test issetequal(metabolites(model), metabolites(h5)) - @test issorted(metabolites(h5)) + @test issetequal(metabolite_ids(model), metabolite_ids(h5)) + @test issorted(metabolite_ids(h5)) @test issorted(variable_ids(h5)) @test size(stoichiometry(model)) == size(stoichiometry(h5)) @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) @@ -24,7 +24,7 @@ @test variable_bounds(model)[1][rxnp] == variable_bounds(h5)[1] @test variable_bounds(model)[2][rxnp] == variable_bounds(h5)[2] @test objective(model)[rxnp] == objective(h5) - @test all(iszero, balance(h5)) + @test all(iszero, metabolite_bounds(h5)) close(h5) @test isnothing(h5.h5) diff --git a/test/io/json.jl b/test/io/json.jl index 3014a9516..f00faa3b5 100644 --- a/test/io/json.jl +++ b/test/io/json.jl @@ -5,7 +5,7 @@ # test if same reaction ids @test issetequal(variable_ids(jsonmodel), variable_ids(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) + @test issetequal(metabolite_ids(jsonmodel), metabolite_ids(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) # not the best tests since it is possible that error could cancel each other out: @test sum(stoichiometry(jsonmodel)) == sum(stoichiometry(stdmodel)) diff --git a/test/io/mat.jl b/test/io/mat.jl index b7e90efd7..8253fea11 100644 --- a/test/io/mat.jl +++ b/test/io/mat.jl @@ -16,7 +16,7 @@ end @testset "Import yeast-GEM (mat)" begin m = load_model(ObjectModel, model_paths["yeast-GEM.mat"]) - @test n_metabolites(m) == 2744 + @test metabolite_count(m) == 2744 @test reaction_count(m) == 4063 @test n_genes(m) == 1160 end diff --git a/test/io/sbml.jl b/test/io/sbml.jl index a4c7a2d2a..2bde8251a 100644 --- a/test/io/sbml.jl +++ b/test/io/sbml.jl @@ -4,13 +4,13 @@ m = convert(MatrixModel, sbmlm) @test size(stoichiometry(sbmlm)) == (92, 95) - @test size(stoichiometry(m)) == (n_metabolites(sbmlm), variable_count(sbmlm)) + @test size(stoichiometry(m)) == (metabolite_count(sbmlm), variable_count(sbmlm)) @test length(m.S.nzval) == 380 @test length.(variable_bounds(sbmlm)) == (95, 95) @test length.(variable_bounds(m)) == (95, 95) @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) - @test metabolites(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] + @test metabolite_ids(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] @test reaction_ids(m)[1:3] == ["R_ACALD", "R_ACALDt", "R_ACKr"] cm = convert(MatrixModelWithCoupling, sbmlm) @@ -27,7 +27,7 @@ end @testset "Import yeast-GEM (sbml)" begin m = load_model(ObjectModel, model_paths["yeast-GEM.xml"]) - @test n_metabolites(m) == 2744 + @test metabolite_count(m) == 2744 @test reaction_count(m) == 4063 @test n_genes(m) == 1160 end diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl index e4bcaf5b8..c1eaf794d 100644 --- a/test/reconstruction/MatrixModel.jl +++ b/test/reconstruction/MatrixModel.jl @@ -119,7 +119,7 @@ end check_consistency = true, ) @test variable_count(cp) == variable_count(new_cp) - @test n_metabolites(cp) + 1 == n_metabolites(new_cp) + @test metabolite_count(cp) + 1 == metabolite_count(new_cp) end @testset "Add reactions" begin @@ -224,12 +224,12 @@ end modLp = remove_reactions(lp, [4; 1]) @test stoichiometry(modLp) == stoichiometry(lp)[:, 2:3] - @test balance(modLp) == balance(lp) + @test metabolite_bounds(modLp) == metabolite_bounds(lp) @test objective(modLp) == objective(lp)[2:3] @test variable_bounds(modLp)[1] == variable_bounds(lp)[1][2:3] @test variable_bounds(modLp)[2] == variable_bounds(lp)[2][2:3] @test variable_ids(modLp) == variable_ids(lp)[2:3] - @test metabolites(modLp) == metabolites(lp) + @test metabolite_ids(modLp) == metabolite_ids(lp) end @testset "Remove metabolites" begin @@ -237,17 +237,20 @@ end m1 = remove_metabolites(model, ["glc__D_e", "for_c"]) m2 = remove_metabolite(model, "glc__D_e") - m3 = remove_metabolites(model, Int.(indexin(["glc__D_e", "for_c"], metabolites(model)))) - m4 = remove_metabolite(model, first(indexin(["glc__D_e"], metabolites(model)))) + m3 = remove_metabolites( + model, + Int.(indexin(["glc__D_e", "for_c"], metabolite_ids(model))), + ) + m4 = remove_metabolite(model, first(indexin(["glc__D_e"], metabolite_ids(model)))) @test size(stoichiometry(m1)) == (70, 90) @test size(stoichiometry(m2)) == (71, 93) @test size(stoichiometry(m3)) == (70, 90) @test size(stoichiometry(m4)) == (71, 93) - @test all((!in(metabolites(m1))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m2)) - @test all((!in(metabolites(m3))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m4)) + @test all((!in(metabolite_ids(m1))).(["glc__D_e", "for_c"])) + @test !(["glc__D_e"] in metabolite_ids(m2)) + @test all((!in(metabolite_ids(m3))).(["glc__D_e", "for_c"])) + @test !(["glc__D_e"] in metabolite_ids(m4)) end @testset "Core in place modifications" begin diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl index 668d6029e..563b46056 100644 --- a/test/reconstruction/ObjectModel.jl +++ b/test/reconstruction/ObjectModel.jl @@ -188,12 +188,12 @@ # test added biomass metabolite new_model = model |> with_added_biomass_metabolite("r2") - @test "biomass" in metabolites(new_model) - @test !("biomass" in metabolites(model)) + @test "biomass" in metabolite_ids(new_model) + @test !("biomass" in metabolite_ids(model)) @test haskey(new_model.reactions["r2"].metabolites, "biomass") @test !haskey(model.reactions["r2"].metabolites, "biomass") new_model2 = new_model |> with_removed_biomass_metabolite("r2") - @test !("biomass" in metabolites(new_model2)) + @test !("biomass" in metabolite_ids(new_model2)) @test !haskey(new_model2.reactions["r2"].metabolites, "biomass") end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index e46014c35..b677b45f2 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -78,7 +78,7 @@ ) @test issetequal( - metabolites(cm), + metabolite_ids(cm), [ "m1#A" "m1#B" @@ -109,7 +109,7 @@ ) @test variable_count(cm) == 13 - @test n_metabolites(cm) == 11 + @test metabolite_count(cm) == 11 @test n_genes(cm) == 8 @test all( diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl index f17870eca..25f5ec138 100644 --- a/test/types/ObjectModel.jl +++ b/test/types/ObjectModel.jl @@ -47,10 +47,10 @@ @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") @test "r1" in variable_ids(model) - @test "m4" in metabolites(model) + @test "m4" in metabolite_ids(model) @test "g2" in genes(model) @test variable_count(model) == 4 - @test n_metabolites(model) == 4 + @test metabolite_count(model) == 4 @test n_genes(model) == 3 S_test = spzeros(4, 4) @@ -78,7 +78,7 @@ @test lb_test == lbs @test ub_test == ubs - @test balance(model) == spzeros(n_metabolites(model)) + @test metabolite_bounds(model) == spzeros(metabolite_count(model)) obj_test = spzeros(4) obj_test[1] = 1.0 @@ -134,7 +134,7 @@ stdmodel = convert(ObjectModel, jsonmodel) @test issetequal(variable_ids(jsonmodel), variable_ids(stdmodel)) @test issetequal(genes(jsonmodel), genes(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) + @test issetequal(metabolite_ids(jsonmodel), metabolite_ids(stdmodel)) jlbs, jubs = variable_bounds(jsonmodel) slbs, subs = variable_bounds(stdmodel) @test issetequal(jlbs, slbs) @@ -143,10 +143,10 @@ sS = stoichiometry(stdmodel) j_r1_index = findfirst(x -> x == "r1", variable_ids(jsonmodel)) s_r1_index = findfirst(x -> x == "r1", variable_ids(stdmodel)) - j_m1_index = findfirst(x -> x == "m1", metabolites(jsonmodel)) - j_m2_index = findfirst(x -> x == "m2", metabolites(jsonmodel)) - s_m1_index = findfirst(x -> x == "m1", metabolites(stdmodel)) - s_m2_index = findfirst(x -> x == "m2", metabolites(stdmodel)) + j_m1_index = findfirst(x -> x == "m1", metabolite_ids(jsonmodel)) + j_m2_index = findfirst(x -> x == "m2", metabolite_ids(jsonmodel)) + s_m1_index = findfirst(x -> x == "m1", metabolite_ids(stdmodel)) + s_m2_index = findfirst(x -> x == "m2", metabolite_ids(stdmodel)) @test jS[j_m1_index, j_r1_index] == sS[s_m1_index, s_r1_index] @test jS[j_m2_index, j_r1_index] == sS[s_m2_index, s_r1_index] end diff --git a/test/types/SBMLModel.jl b/test/types/SBMLModel.jl index 17a7b7d5c..fe3125f79 100644 --- a/test/types/SBMLModel.jl +++ b/test/types/SBMLModel.jl @@ -6,7 +6,7 @@ @test Set(variable_ids(sbmlm)) == Set(variable_ids(sbmlm2)) @test Set(variable_ids(sbmlm)) == Set(variable_ids(sm)) - @test Set(metabolites(sbmlm)) == Set(metabolites(sbmlm2)) + @test Set(metabolite_ids(sbmlm)) == Set(metabolite_ids(sbmlm2)) sp(x) = x.species @test all([ issetequal( diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index c5efe760a..8ff7cb15c 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -13,8 +13,10 @@ end @testset "ID shortcuts are identictical with the ID-generating functions" begin + # this is better triple-checked to avoid someone stealing the overloads @test variables === variable_ids @test bounds === variable_bounds @test reactions === reaction_ids - # TODO don't forget about metabolites later + @test metabolites === metabolite_ids + @test stoichiometry === metabolite_variables_matrix end diff --git a/test/utils/MatrixModel.jl b/test/utils/MatrixModel.jl index 94d72d4f1..d7357dc15 100644 --- a/test/utils/MatrixModel.jl +++ b/test/utils/MatrixModel.jl @@ -1,7 +1,7 @@ @testset "MatrixModel utilities" begin cp = test_LP() @test variable_count(cp) == 3 - @test n_metabolites(cp) == 4 + @test metabolite_count(cp) == 4 @test n_coupling_constraints(cp) == 0 cp2 = test_LP() diff --git a/test/utils/Serialized.jl b/test/utils/Serialized.jl index 052a26c1e..691c09c0f 100644 --- a/test/utils/Serialized.jl +++ b/test/utils/Serialized.jl @@ -19,7 +19,7 @@ ) sm.m = nothing @test issetequal( - metabolites(convert(MatrixModelWithCoupling, sm)), - metabolites(convert(MatrixModelWithCoupling, sm2)), + metabolite_ids(convert(MatrixModelWithCoupling, sm)), + metabolite_ids(convert(MatrixModelWithCoupling, sm2)), ) end diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index 184ad4238..29454c230 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -41,7 +41,8 @@ x -> looks_like_exchange_reaction(x; exclude_biomass = true), variable_ids(cp), ) == ["EX_m1(e)", "EX_m3(e)"] - @test filter(looks_like_extracellular_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"] + @test filter(looks_like_extracellular_metabolite, metabolite_ids(cp)) == + ["m1[e]", "m3[e]"] @test filter(looks_like_biomass_reaction, variable_ids(cp)) == ["EX_biomass(e)", "biomass1"] @test filter( @@ -54,27 +55,27 @@ end model = load_model(model_paths["e_coli_core.json"]) @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 + @test length(filter(looks_like_extracellular_metabolite, metabolite_ids(model))) == 20 model = load_model(model_paths["e_coli_core.xml"]) @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 + @test length(filter(looks_like_extracellular_metabolite, metabolite_ids(model))) == 20 model = load_model(model_paths["e_coli_core.mat"]) @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 + @test length(filter(looks_like_extracellular_metabolite, metabolite_ids(model))) == 20 model = convert(ObjectModel, model) @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 + @test length(filter(looks_like_extracellular_metabolite, metabolite_ids(model))) == 20 model = convert(MatrixModelWithCoupling, model) @test length(filter(looks_like_exchange_reaction, variable_ids(model))) == 20 @test length(filter(looks_like_biomass_reaction, variable_ids(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 + @test length(filter(looks_like_extracellular_metabolite, metabolite_ids(model))) == 20 end @testset "Ontology usage in is_xxx_reaction" begin From d35c0984121a138773dc3c07b5dbefa42f9bf125 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 15:13:52 +0200 Subject: [PATCH 296/531] fixup: modelwrapper no longer knows about metabolites by default --- src/types/accessors/ModelWrapper.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 2c687f616..5399b0b51 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -15,7 +15,7 @@ end # [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variable_count variable_ids variable_bounds objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes From b7c704f43f45f387b878f67808ee408d69b7a8db Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:23:43 +0200 Subject: [PATCH 297/531] fix optmodel object naming --- src/solver.jl | 19 +++++++++++++------ test/types/CommunityModel.jl | 6 +++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index bb4732d89..b98339622 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -62,16 +62,23 @@ function make_optimization_model( continue elseif typeof(bounds) <: AbstractVector{Float64} # equality bounds - c = @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) - set_name.(c, "$(semname)_eqs") + constraints = + @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) + label = Symbol(semname, :_eqs) + optimization_model[label] = constraints + set_name.(c, "$label") elseif typeof(bounds) <: Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}} # lower/upper interval bounds slb, sub = bounds smtx = sem.mapping_matrix(model) - c = @constraint(optimization_model, slb .<= smtx * x) - set_name.(c, "$(semname)_lbs") - c = @constraint(optimization_model, smtx * x .<= sub) - set_name.(c, "$(semname)_ubs") + constraints = @constraint(optimization_model, slb .<= smtx * x) + label = Symbol(semname, :_lbs) + optimization_model[label] = constraints + set_name.(c, "$label") + constraints = @constraint(optimization_model, smtx * x .<= sub) + label = Symbol(semname, :_ubs) + optimization_model[label] = constraints + set_name.(c, "$label") else # if the bounds returned something weird, complain loudly. throw( diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl index b677b45f2..3524168f4 100644 --- a/test/types/CommunityModel.jl +++ b/test/types/CommunityModel.jl @@ -184,7 +184,7 @@ # test modification for community model res = flux_balance_analysis(cm, Tulip.Optimizer) - mb = res.result[:mb] + mb = res.result[:metabolite_eqs] x = res.result[:x] @test normalized_coefficient(mb[9], x[1]) == 0.2 @test normalized_coefficient(mb[11], x[13]) == -0.8 @@ -194,7 +194,7 @@ Tulip.Optimizer; modifications = [modify_abundances([0.5, 0.5])], ) - mb = res2.result[:mb] + mb = res2.result[:metabolite_eqs] x = res2.result[:x] @test normalized_coefficient(mb[9], x[1]) == 0.5 @test normalized_coefficient(mb[11], x[13]) == -0.5 @@ -218,7 +218,7 @@ Tulip.Optimizer; modifications = [modify_abundances([0.3, 0.7])], ) - mb = res3.result[:mb] + mb = res3.result[:metabolite_eqs] x = res3.result[:x] @test normalized_coefficient(mb[10], x[5]) == 0.3 @test normalized_coefficient(mb[10], x[12]) == -0.3 From 46b8a45921e4e9ba00d5cacf014ef7a05b36b83e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:23:55 +0200 Subject: [PATCH 298/531] type the defaults better --- src/types/accessors/AbstractMetabolicModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl index 7336f7f84..d352a0d85 100644 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ b/src/types/accessors/AbstractMetabolicModel.jl @@ -199,7 +199,7 @@ In SBML, these are usually called "gene products" but we write `genes` for simplicity. """ function genes(a::AbstractMetabolicModel)::Vector{String} - return [] + String[] end """ From 9257e6b68b0b567af947c4cd95deddc800856106 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:24:15 +0200 Subject: [PATCH 299/531] be slightly less brutal in the defaults --- src/types/accessors/bits/semantics.jl | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 8d67c9dec..15269be98 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -182,7 +182,7 @@ vector returned by [`$ids`]. """, ), :(function $count(a::AbstractMetabolicModel)::Int - 0 + length($ids(a)) end), ) @@ -244,13 +244,9 @@ no bounds, or a vector of floats with equality bounds, or a tuple of 2 vectors with lower and upper bounds. """, ), - :( - function $bounds( - a::AbstractMetabolicModel, - )::Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}} - nothing - end - ), + :(function $bounds(a::AbstractMetabolicModel) + $(sym == :metabolite ? :(spzeros($count(a))) : nothing) + end), ) Base.eval.(Ref(themodule), [idsfn, countfn, mappingfn, mtxfn, boundsfn]) @@ -271,16 +267,9 @@ with lower and upper bounds. end), ) - Base.eval( - themodule, - :( - function $bounds( - w::AbstractModelWrapper, - )::Union{Nothing,Vector{Float64},Tuple{Vector{Float64},Vector{Float64}}} - $bounds(unwrap_model(w)) - end - ), - ) + Base.eval(themodule, :(function $bounds(w::AbstractModelWrapper) + $bounds(unwrap_model(w)) + end)) # TODO here we would normally also overload the matrix function, but that # one will break once anyone touches variables of the models (which is quite From 1659366bf201155d243e19fc334334ed970a91cc Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:25:04 +0200 Subject: [PATCH 300/531] fixup: variable for solver names --- src/solver.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index b98339622..2eaf689e3 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -66,7 +66,7 @@ function make_optimization_model( @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) label = Symbol(semname, :_eqs) optimization_model[label] = constraints - set_name.(c, "$label") + set_name.(constraints, "$label") elseif typeof(bounds) <: Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}} # lower/upper interval bounds slb, sub = bounds @@ -74,11 +74,11 @@ function make_optimization_model( constraints = @constraint(optimization_model, slb .<= smtx * x) label = Symbol(semname, :_lbs) optimization_model[label] = constraints - set_name.(c, "$label") + set_name.(constraints, "$label") constraints = @constraint(optimization_model, smtx * x .<= sub) label = Symbol(semname, :_ubs) optimization_model[label] = constraints - set_name.(c, "$label") + set_name.(constraints, "$label") else # if the bounds returned something weird, complain loudly. throw( From af015cef861c2a4e45f05df1bbd39b6f1c473ac3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:31:04 +0200 Subject: [PATCH 301/531] fixup: same --- src/analysis/modifications/community.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl index 8fbfb73a7..ceb63b213 100644 --- a/src/analysis/modifications/community.jl +++ b/src/analysis/modifications/community.jl @@ -29,7 +29,7 @@ modify_abundances(new_abundances::Vector{Float64}) = n_vars = variable_count(model) n_env_vars = length(model.environmental_links) - n_cons = length(opt_model[:mb]) + n_cons = length(opt_model[:metabolite_eqs]) n_objs = model isa CommunityModel ? 0 : length(model.inner.members) row_offset = @@ -38,7 +38,7 @@ modify_abundances(new_abundances::Vector{Float64}) = # fix abundance coefficients of species exchanges for (i, j, v) in zip(findnz(env_rows)...) ii = i + row_offset - set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][j], v) + set_normalized_coefficient(opt_model[:metabolite_eqs][ii], opt_model[:x][j], v) end column_offset = @@ -48,6 +48,6 @@ modify_abundances(new_abundances::Vector{Float64}) = for (i, j, v) in zip(findnz(env_link)...) jj = j + column_offset ii = i + row_offset - set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][jj], -v) + set_normalized_coefficient(opt_model[:metabolite_eqs][ii], opt_model[:x][jj], -v) end end From 9d9f6f1a1a1a2bb7b204dff2d2a5be52bc522f83 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:31:26 +0200 Subject: [PATCH 302/531] todo: fix this later --- src/solver.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/solver.jl b/src/solver.jl index 2eaf689e3..1d21393ca 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -75,6 +75,10 @@ function make_optimization_model( label = Symbol(semname, :_lbs) optimization_model[label] = constraints set_name.(constraints, "$label") + # TODO: this actually uses the semantic matrix transposed, but + # that's right. Fix: transpose all other semantics because having + # the stoichiometry in the "right" way is quite crucial for folks + # being able to reason about stuff. constraints = @constraint(optimization_model, smtx * x .<= sub) label = Symbol(semname, :_ubs) optimization_model[label] = constraints From ae908e91c9b91c53a0d09d03ae3910f0df170337 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 16:32:49 +0200 Subject: [PATCH 303/531] fixup: format head^^ --- src/analysis/modifications/community.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl index ceb63b213..5a83209ae 100644 --- a/src/analysis/modifications/community.jl +++ b/src/analysis/modifications/community.jl @@ -48,6 +48,10 @@ modify_abundances(new_abundances::Vector{Float64}) = for (i, j, v) in zip(findnz(env_link)...) jj = j + column_offset ii = i + row_offset - set_normalized_coefficient(opt_model[:metabolite_eqs][ii], opt_model[:x][jj], -v) + set_normalized_coefficient( + opt_model[:metabolite_eqs][ii], + opt_model[:x][jj], + -v, + ) end end From ed3c8438e534745184ab64dd883818a1dfd516a7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 19:26:38 +0200 Subject: [PATCH 304/531] simplify the constraint labeling --- src/solver.jl | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 1d21393ca..4865476a0 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -35,6 +35,12 @@ function make_optimization_model( optimization_model = Model(optimizer) + function label(semname, suffix, constraints) + l = Symbol(semname, :_, suffix) + optimization_model[l] = constraints + set_name.(constraints, "$l") + end + # make the variables n = variable_count(model) @variable(optimization_model, x[1:n]) @@ -62,27 +68,19 @@ function make_optimization_model( continue elseif typeof(bounds) <: AbstractVector{Float64} # equality bounds - constraints = + label(semname, :eqs, @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) - label = Symbol(semname, :_eqs) - optimization_model[label] = constraints - set_name.(constraints, "$label") + ) elseif typeof(bounds) <: Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}} # lower/upper interval bounds slb, sub = bounds smtx = sem.mapping_matrix(model) - constraints = @constraint(optimization_model, slb .<= smtx * x) - label = Symbol(semname, :_lbs) - optimization_model[label] = constraints - set_name.(constraints, "$label") + label(semname, :lbs, @constraint(optimization_model, slb .<= smtx * x)) + label(semname, :ubs, @constraint(optimization_model, smtx * x .<= sub)) # TODO: this actually uses the semantic matrix transposed, but # that's right. Fix: transpose all other semantics because having # the stoichiometry in the "right" way is quite crucial for folks # being able to reason about stuff. - constraints = @constraint(optimization_model, smtx * x .<= sub) - label = Symbol(semname, :_ubs) - optimization_model[label] = constraints - set_name.(constraints, "$label") else # if the bounds returned something weird, complain loudly. throw( From 9c2d957b2c970ff78866142c6785618a0fa69342 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 19:53:52 +0200 Subject: [PATCH 305/531] fixup: format --- src/solver.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/solver.jl b/src/solver.jl index 4865476a0..ba8ff85a2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -68,7 +68,9 @@ function make_optimization_model( continue elseif typeof(bounds) <: AbstractVector{Float64} # equality bounds - label(semname, :eqs, + label( + semname, + :eqs, @constraint(optimization_model, sem.mapping_matrix(model) * x .== bounds) ) elseif typeof(bounds) <: Tuple{<:AbstractVector{Float64},<:AbstractVector{Float64}} From 4229aaa88c3a3b4e6b3f4c606df50d178fa38c6b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 19:54:12 +0200 Subject: [PATCH 306/531] fixup: do not inherit variable_count --- src/types/accessors/ModelWrapper.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl index 5399b0b51..42dce31dc 100644 --- a/src/types/accessors/ModelWrapper.jl +++ b/src/types/accessors/ModelWrapper.jl @@ -15,7 +15,12 @@ end # [`AbstractMetabolicModel`](@ref). # -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variable_count variable_ids variable_bounds objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations +# TODO make some functionality that allows folks to clearly declare that stuff +# like variable_count, somesemantic_count or somesemantic_variables_matrix did +# not change and can be safely inherited instead of being recreated from the +# more complex data. + +@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variable_ids variable_bounds objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations @inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes From 5217749f55a7a07407049b8d7dc1dcb1db65971b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 19:54:49 +0200 Subject: [PATCH 307/531] at this point models now don't really need to have metabolites --- test/types/abstract/AbstractMetabolicModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl index 8ff7cb15c..b03097e7a 100644 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ b/test/types/abstract/AbstractMetabolicModel.jl @@ -6,7 +6,7 @@ end @testset "Base abstract model methods require proper minimal implementation" begin @test_throws MethodError variable_ids(123) x = FakeModel(123) - for m in [variables, metabolites, stoichiometry, bounds, objective] + for m in [variable_ids, variable_bounds, objective] @test_throws MethodError m(x) end end From f8c9d106a2bb29177796cddede5e93fe96546939 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 May 2023 19:56:34 +0200 Subject: [PATCH 308/531] turn the semantic matrices around We'd get burned at stakes if we had stoichiometry with reactions in rows. --- src/analysis/sampling/affine_hit_and_run.jl | 2 +- src/analysis/variability_analysis.jl | 2 +- src/solver.jl | 4 +- src/types/accessors/bits/semantics.jl | 40 ++++++++++--------- src/wrappers/EnzymeConstrainedModel.jl | 24 +++++------ src/wrappers/MinimizeDistance.jl | 7 ++-- .../SimplifiedEnzymeConstrainedModel.jl | 6 +-- 7 files changed, 45 insertions(+), 40 deletions(-) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl index 590c39335..b06784631 100644 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ b/src/analysis/sampling/affine_hit_and_run.jl @@ -27,7 +27,7 @@ warmup_points = warmup_from_variability(model, GLPK.Optimizer) samples = affine_hit_and_run(model, warmup_points, sample_iters = 101:105) # convert the result to flux (for models where the distinction matters): -fluxes = reaction_variables_matrix(model)' * samples +fluxes = reaction_variables_matrix(model) * samples ``` """ function affine_hit_and_run( diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl index 10f1dbf94..ff121b485 100644 --- a/src/analysis/variability_analysis.jl +++ b/src/analysis/variability_analysis.jl @@ -77,7 +77,7 @@ function variability_analysis( variability_analysis( model, optimizer; - directions = s.mapping_matrix(model)[:, indexes], + directions = (s.mapping_matrix(model)')[:, indexes], kwargs..., ) end diff --git a/src/solver.jl b/src/solver.jl index ba8ff85a2..dc8fe6bed 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -205,7 +205,7 @@ values_vec(:reaction, flux_balance_analysis(model, ...)) """ function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) s = Accessors.Internal.semantics(semantics) - is_solved(res.result) ? s.mapping_matrix(res.model)' * value.(res.result[:x]) : nothing + is_solved(res.result) ? s.mapping_matrix(res.model) * value.(res.result[:x]) : nothing end """ @@ -252,7 +252,7 @@ values_dict(:reaction, flux_balance_analysis(model, ...)) function values_dict(semantics::Symbol, res::ModelWithResult{<:Model}) s = Accessors.Internal.semantics(semantics) is_solved(res.result) ? - Dict(s.ids(res.model) .=> s.mapping_matrix(res.model)' * value.(res.result[:x])) : + Dict(s.ids(res.model) .=> s.mapping_matrix(res.model) * value.(res.result[:x])) : nothing end diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 15269be98..49303839b 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -5,27 +5,27 @@ A helper function to quickly create a sparse matrix from a dictionary that describes it. Reverse of [`make_mapping_dict`](@ref). """ function make_mapping_mtx( - vars::Vector{String}, semantics::Vector{String}, - var_sem_val::Dict{String,Dict{String,Float64}}, + vars::Vector{String}, + sem_var_val::Dict{String,Dict{String,Float64}}, )::Types.SparseMat # TODO: move this to general utils or so - rowidx = Dict(vars .=> 1:length(vars)) - colidx = Dict(semantics .=> 1:length(semantics)) - n = sum(length.(values(var_sem_val))) + rowidx = Dict(semantics .=> 1:length(semantics)) + colidx = Dict(vars .=> 1:length(vars)) + n = sum(length.(values(sem_var_val))) R = Vector{Int}(undef, n) C = Vector{Int}(undef, n) V = Vector{Float64}(undef, n) i = 1 - for (cid, col_val) in var_sem_val - for (rid, val) in col_val + for (rid, var_val) in sem_var_val + for (cid, val) in var_val R[i] = rowidx[rid] C[i] = colidx[cid] V[i] = val i += 1 end end - sparse(R, C, V, length(vars), length(semantics)) + sparse(R, C, V, length(semantics), length(vars)) end """ @@ -35,15 +35,19 @@ A helper function to quickly create a sparse matrix from a dictionary that describes it. Reverse of [`make_mapping_mtx`](@ref). """ function make_mapping_dict( - vars::Vector{String}, semantics::Vector{String}, + vars::Vector{String}, mtx::Types.SparseMat, )::Dict{String,Dict{String,Float64}} - # TODO: move this to general utils - Dict( - sid => Dict(vars[vidx] => val for (vidx, val) in zip(findnz(mtx[:, sidx])...)) for - (sidx, sid) in enumerate(semantics) - ) + x = Dict{String,Dict{String,Float64}}() + for (ridx, cidx, val) in zip(findnz(mtx)...) + if haskey(x, semantics[ridx]) + x[semantics[ridx]][vars[cidx]] = val + else + x[semantics[ridx]] = Dict{String,Float64}(vars[cidx] => val) + end + end + x end """ @@ -67,13 +71,13 @@ Base.@kwdef struct Semantics count::Function """ - Returns a mapping of semantic values to variables IDs in the model. + Returns a mapping of semantic values to variables IDs and their stoichiometry. """ mapping::Function """ - Same as `mapping` but returns a matrix (with variables in rows and the - semantic values in columns), which is possibly more efficient or handy in + Same as `mapping` but returns a matrix (with semantic values in rows and + the variables in columns), which is possibly more efficient or handy in specific cases. """ mapping_matrix::Function @@ -219,7 +223,7 @@ To improve the performance, you may want to use [`$mapping_mtx`](@ref). Bipartite mapping of $name described by the model to the actual variables in the model, described as a sparse matrix mapping with rows -corresponding to model variables and columns corresponding to $name. +corresponding to $name and columns corresponding to model variables. By default, this is derived from [`$mapping`](@ref) in all models. For safety reasons, this is never automatically inherited by wrappers. diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 70d165b89..355c859de 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -103,20 +103,29 @@ end function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) rxnmat = - enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) + reaction_variables_matrix(model.inner) * enzyme_constrained_column_reactions(model) [ rxnmat - spzeros(n_genes(model), size(rxnmat, 2)) + spzeros(size(rxnmat, 1), n_genes(model)) ] end Accessors.reaction_variables(model::EnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( - variable_ids(model), reaction_ids(model), + variable_ids(model), reaction_variables_matrix(model), ) # TODO currently inefficient +Accessors.metabolite_ids(model::EnzymeConstrainedModel) = + [metabolite_ids(model.inner); genes(model) .* "#enzyme_constrained"] + +Accessors.metabolite_count(model::EnzymeConstrainedModel) = + metabolite_count(model.inner) + n_genes(model) + +Accessors.metabolite_bounds(model::EnzymeConstrainedModel) = + [metabolite_bounds(model.inner); spzeros(length(model.coupling_row_gene_product))] + Accessors.enzyme_ids(model::EnzymeConstrainedModel) = genes(model) Accessors.enzyme_count(model::EnzymeConstrainedModel) = n_genes(model) @@ -172,16 +181,7 @@ function Accessors.coupling_bounds(model::EnzymeConstrainedModel) ) end -Accessors.metabolite_bounds(model::EnzymeConstrainedModel) = - [metabolite_bounds(model.inner); spzeros(length(model.coupling_row_gene_product))] - Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) Accessors.genes(model::EnzymeConstrainedModel) = genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] - -Accessors.metabolite_ids(model::EnzymeConstrainedModel) = - [metabolite_ids(model.inner); genes(model) .* "#enzyme_constrained"] - -Accessors.metabolite_count(model::EnzymeConstrainedModel) = - metabolite_count(model.inner) + n_genes(model) diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl index 1d938ae06..838e5673c 100644 --- a/src/wrappers/MinimizeDistance.jl +++ b/src/wrappers/MinimizeDistance.jl @@ -59,9 +59,10 @@ function Accessors.objective(m::MinimizeSemanticDistance) s = Accessors.Internal.semantics(m.semantics) M = s.mapping_matrix(m.inner) - return M * - [spdiagm(fill(-0.5, size(M, 2))) m.center] * - [M' zeros(size(M, 2)); zeros(size(M, 1))' 1.0] + # TODO check the validity of the math here + return M' * + [spdiagm(fill(-0.5, size(M, 1))) m.center] * + [M zeros(size(M, 1)); zeros(size(M, 2))' 1.0] end """ diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl index 6dc812199..7c9110133 100644 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl @@ -84,8 +84,8 @@ Get the mapping of the reaction rates in wrapped model (as a matrix). """ Accessors.reaction_variables_matrix(model::SimplifiedEnzymeConstrainedModel) = - simplified_enzyme_constrained_column_reactions(model)' * - reaction_variables_matrix(model.inner) + reaction_variables_matrix(model.inner) * + simplified_enzyme_constrained_column_reactions(model) #TODO check """ $(TYPEDSIGNATURES) @@ -96,8 +96,8 @@ wrapped model. """ Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = Accessors.Internal.make_mapping_dict( - variable_ids(model), reaction_ids(model.inner), + variable_ids(model), reaction_variables_matrix(model), ) # TODO currently inefficient From d840067f9e39d782a0899ec1265d46eb07eb3c9e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 5 May 2023 14:35:12 +0200 Subject: [PATCH 309/531] remove enzyme arugment footgun --- src/reconstruction/enzyme_constrained.jl | 9 +++++++++ src/reconstruction/simplified_enzyme_constrained.jl | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index b1c5407a4..7a5bcc1d9 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -42,6 +42,15 @@ function make_enzyme_constrained_model( gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, total_gene_product_mass_bound::Maybe{Float64} = nothing, ) + all( + !isnothing, + [ + gene_product_mass_groups, + gene_product_mass_group_bounds, + total_gene_product_mass_bound, + ], + ) && throw(ArgumentError("Too many arguments specified!")) + if !isnothing(total_gene_product_mass_bound) gene_product_mass_groups = Dict("uncategorized" => genes(model)) gene_product_mass_group_bounds = diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 911b7fa54..79929d19b 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -44,6 +44,11 @@ function make_simplified_enzyme_constrained_model( reaction_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, ) + all( + !isnothing, + [reaction_mass_groups, reaction_mass_group_bounds, total_reaction_mass_bound], + ) && throw(ArgumentError("Too many arguments specified!")) + # fix kwarg inputs if !isnothing(total_reaction_mass_bound) reaction_mass_groups = Dict("uncategorized" => variables(model)) # TODO should be reactions From 8a99ef4b016e5b96a6f44570e64297d4c06792b4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 May 2023 12:08:22 +0200 Subject: [PATCH 310/531] fix macro ident clash --- src/reconstruction/MatrixCoupling.jl | 10 ++++++++-- src/reconstruction/MatrixModel.jl | 10 ++++++++-- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl index 42f10c14f..34aef62b8 100644 --- a/src/reconstruction/MatrixCoupling.jl +++ b/src/reconstruction/MatrixCoupling.jl @@ -363,7 +363,10 @@ end end @_remove_fn metabolite MatrixCoupling String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) + remove_metabolites!( + model, + Int.(indexin(metabolite_ids, Accessors.metabolite_ids(model))), + ) end @_remove_fn metabolite MatrixCoupling String begin @@ -371,7 +374,10 @@ end end @_remove_fn metabolite MatrixCoupling String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) + remove_metabolites( + model, + Int.(indexin(metabolite_ids, Accessors.metabolite_ids(model))), + ) end """ diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl index 2be6fc608..141258a3b 100644 --- a/src/reconstruction/MatrixModel.jl +++ b/src/reconstruction/MatrixModel.jl @@ -392,7 +392,10 @@ end end @_remove_fn metabolite MatrixModel String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) + remove_metabolites!( + model, + Int.(indexin(metabolite_ids, Accessors.metabolite_ids(model))), + ) end @_remove_fn metabolite MatrixModel String begin @@ -400,7 +403,10 @@ end end @_remove_fn metabolite MatrixModel String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolite_ids(model)))) + remove_metabolites( + model, + Int.(indexin(metabolite_ids, Accessors.metabolite_ids(model))), + ) end """ From 973a84886b9a902907ccbde193f791666c245793 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 May 2023 12:08:34 +0200 Subject: [PATCH 311/531] add a note for the future --- src/solver.jl | 3 +++ src/types/models/HDF5Model.jl | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/solver.jl b/src/solver.jl index dc8fe6bed..d58169f8c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -62,6 +62,9 @@ function make_optimization_model( end # go over the semantics and add bounds if there are any + # TODO for use in sampling and other things, it would be nice to have + # helper functions to make a complete matrix of equality and interval + # constraints. for (semname, sem) in Accessors.Internal.get_semantics() bounds = sem.bounds(model) if isnothing(bounds) diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index ec4e03b65..ad175d7ad 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -26,6 +26,8 @@ mutable struct HDF5Model <: AbstractMetabolicModel HDF5Model(filename::String) = new(nothing, filename) end +# TODO this might need to store the extra semantics now + function Accessors.precache!(model::HDF5Model)::Nothing if isnothing(model.h5) model.h5 = h5open(model.filename, "r") From f8b36f50cdd0559893f817f7f55caf3c58eccaa3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 May 2023 12:08:59 +0200 Subject: [PATCH 312/531] fix the parameter order --- src/types/accessors/bits/semantics.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl index 49303839b..8cd84189c 100644 --- a/src/types/accessors/bits/semantics.jl +++ b/src/types/accessors/bits/semantics.jl @@ -230,7 +230,7 @@ safety reasons, this is never automatically inherited by wrappers. """, ), :(function $mapping_mtx(a::AbstractMetabolicModel)::SparseMat - make_mapping_mtx(variable_ids(a), $ids(a), $mapping(a)) + make_mapping_mtx($ids(a), variable_ids(a), $mapping(a)) end), ) From 29df8d227e29eac97d3d0306c04f1f9292e587a5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 May 2023 12:09:26 +0200 Subject: [PATCH 313/531] more notes --- src/types/wrappers/EqualGrowthCommunityModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl index e987ace65..47ea64c28 100644 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ b/src/types/wrappers/EqualGrowthCommunityModel.jl @@ -36,7 +36,7 @@ Accessors.metabolite_bounds(cm::EqualGrowthCommunityModel) = [ ] function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) - + # TODO this needs a rework S = stoichiometry(cm.inner) obj_col = spzeros(size(S, 1)) From 9173d692846feb0f04f1a3ee62e1e8205a8efc0f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 May 2023 12:09:40 +0200 Subject: [PATCH 314/531] make models export the metabolite_variable mapping instead of stoichiometry by default --- src/types/models/HDF5Model.jl | 9 ++++- src/types/models/JSONModel.jl | 42 +++++---------------- src/types/models/MATModel.jl | 8 +++- src/types/models/MatrixModel.jl | 8 +++- src/types/models/ObjectModel.jl | 42 +++++---------------- src/types/models/SBMLModel.jl | 52 +++++++++++--------------- src/wrappers/EnzymeConstrainedModel.jl | 30 ++++++++------- 7 files changed, 80 insertions(+), 111 deletions(-) diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl index ad175d7ad..eb733b592 100644 --- a/src/types/models/HDF5Model.jl +++ b/src/types/models/HDF5Model.jl @@ -58,11 +58,18 @@ function Accessors.metabolite_ids(model::HDF5Model)::Vector{String} read(model.h5["metabolites"]) end -function Accessors.stoichiometry(model::HDF5Model)::SparseMat +function Accessors.metabolite_variables_matrix(model::HDF5Model)::SparseMat precache!(model) h5_read_sparse(SparseMat, model.h5["stoichiometry"]) end +Accessors.metabolite_variables(model::HDF5Model)::Dict{String,Dict{String,Float}} = + Accessors.Internal.make_mapping_dict( + metabolite_ids(m), + variable_ids(m), + metabolite_variables_matrix(m), + ) + function Accessors.variable_bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} precache!(model) (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl index 47ab32579..6f57afcc6 100644 --- a/src/types/models/JSONModel.jl +++ b/src/types/models/JSONModel.jl @@ -88,40 +88,18 @@ Accessors.genes(model::JSONModel) = Accessors.Internal.@all_variables_are_reactions JSONModel -function Accessors.stoichiometry(model::JSONModel) - rxn_ids = variable_ids(model) - met_ids = metabolite_ids(model) - - n_entries = 0 - for r in model.rxns - for _ in r["metabolites"] - n_entries += 1 - end - end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - for (i, rid) in enumerate(rxn_ids) - r = model.rxns[model.rxn_index[rid]] - for (mid, coeff) in r["metabolites"] - haskey(model.met_index, mid) || throw( - DomainError( - met_id, - "Unknown metabolite found in stoichiometry of $(rxn_ids[i])", - ), - ) - - push!(MI, model.met_index[mid]) - push!(RI, i) - push!(SV, coeff) +function Accessors.metabolite_variables(model::JSONModel) + x = Dict{String,Dict{String,Float64}}() + for (rid, ridx) in model.rxn_index + for (mid, coeff) in model.rxns[ridx]["metabolites"] + if haskey(x, mid) + x[mid][rid] = coeff + else + x[mid] = Dict{String,Float64}(rid => coeff) + end end end - return SparseArrays.sparse(MI, RI, SV, length(met_ids), length(rxn_ids)) + x end Accessors.variable_bounds(model::JSONModel) = ( diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl index 1dbaad206..c84aafd4a 100644 --- a/src/types/models/MATModel.jl +++ b/src/types/models/MATModel.jl @@ -36,7 +36,13 @@ function Accessors.metabolite_ids(m::MATModel)::Vector{String} end end -Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) +Accessors.metabolite_variables_matrix(m::MATModel) = sparse(m.mat["S"]) + +Accessors.metabolite_variables(m::MATModel) = Accessors.Internal.make_mapping_dict( + metabolite_ids(m), + variable_ids(m), + metabolite_variables_matrix(m), +) Accessors.variable_bounds(m::MATModel) = ( reshape(get(m.mat, "lb", fill(-Inf, variable_count(m), 1)), variable_count(m)), diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl index 7290dd599..eab9916ff 100644 --- a/src/types/models/MatrixModel.jl +++ b/src/types/models/MatrixModel.jl @@ -53,7 +53,13 @@ Accessors.Internal.@all_variables_are_reactions MatrixModel Accessors.metabolite_ids(a::MatrixModel)::Vector{String} = a.mets -Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S +Accessors.metabolite_variables_matrix(a::MatrixModel)::SparseMat = a.S + +Accessors.metabolite_variables(a::MatrixModel) = Accessors.Internal.make_mapping_dict( + metabolite_ids(a), + variable_ids(a), + metabolite_variables_matrix(a), +) Accessors.variable_bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl index 87fe4f03e..15e2eea43 100644 --- a/src/types/models/ObjectModel.jl +++ b/src/types/models/ObjectModel.jl @@ -69,40 +69,18 @@ Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) -function Accessors.stoichiometry(model::ObjectModel)::SparseMat - n_entries = 0 - for (_, r) in model.reactions - for _ in r.metabolites - n_entries += 1 +function Accessors.metabolite_variables(model::ObjectModel) + x = Dict{String,Dict{String,Float64}}() + for (rid, r) in model.reactions + for (mid, coeff) in r.metabolites + if haskey(x, mid) + x[mid][rid] = coeff + else + x[mid] = Dict{String,Float64}(rid => coeff) + end end end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - # establish the ordering - rxns = variable_ids(model) - met_idx = Dict(mid => i for (i, mid) in enumerate(metabolite_ids(model))) - - # fill the matrix entries - for (ridx, rid) in enumerate(rxns) - for (mid, coeff) in model.reactions[rid].metabolites - haskey(met_idx, mid) || throw( - DomainError( - mid, - "Metabolite $(mid) not found in model but occurs in stoichiometry of $(rid)", - ), - ) - push!(MI, met_idx[mid]) - push!(RI, ridx) - push!(SV, coeff) - end - end - return SparseArrays.sparse(MI, RI, SV, metabolite_count(model), variable_count(model)) + x end Accessors.variable_bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 4f3e23b89..5e30b0532 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -44,41 +44,31 @@ Accessors.Internal.@all_variables_are_reactions SBMLModel Accessors.metabolite_ids(model::SBMLModel)::Vector{String} = model.metabolite_ids -function Accessors.stoichiometry(model::SBMLModel)::SparseMat - - # find the vector size for preallocation - nnz = 0 - for (_, r) in model.sbml.reactions - for _ in r.reactants - nnz += 1 - end - for _ in r.products - nnz += 1 +function Accessors.metabolite_variables(model::SBMLModel) + x = Dict{String,Dict{String,Float64}}() + add!(mid, rid, c) = + if haskey(x, mid) + x[mid][rid] = c + else + x[mid] = Dict{String,Float64}(rid => c) end - end - - Rows = Int[] - Cols = Int[] - Vals = Float64[] - sizehint!(Rows, nnz) - sizehint!(Cols, nnz) - sizehint!(Vals, nnz) - - row_idx = Dict(k => i for (i, k) in enumerate(model.metabolite_ids)) - for (ridx, rid) in enumerate(model.reaction_ids) - r = model.sbml.reactions[rid] - for sr in r.reactants - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? -1.0 : -sr.stoichiometry) + for (rid, r) in model.sbml.reactions + for reactant in r.reactants + add!( + reactant.species, + rid, + isnothing(reactant.stoichiometry) ? -1.0 : -reactant.stoichiometry, + ) end - for sr in r.products - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? 1.0 : sr.stoichiometry) + for product in r.products + add!( + product.species, + rid, + isnothing(product.stoichiometry) ? 1.0 : product.stoichiometry, + ) end end - return sparse(Rows, Cols, Vals, metabolite_count(model), reaction_count(model)) + x end function Accessors.variable_bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl index 355c859de..8bde38ee3 100644 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ b/src/wrappers/EnzymeConstrainedModel.jl @@ -63,15 +63,6 @@ end Accessors.unwrap_model(model::EnzymeConstrainedModel) = model.inner -function Accessors.stoichiometry(model::EnzymeConstrainedModel) - irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) - enzS = enzyme_constrained_gene_product_coupling(model) - [ - irrevS spzeros(size(irrevS, 1), size(enzS, 1)) - -enzS I(size(enzS, 1)) - ] -end - Accessors.objective(model::EnzymeConstrainedModel) = model.objective function Accessors.variable_ids(model::EnzymeConstrainedModel) @@ -104,10 +95,7 @@ end function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) rxnmat = reaction_variables_matrix(model.inner) * enzyme_constrained_column_reactions(model) - [ - rxnmat - spzeros(size(rxnmat, 1), n_genes(model)) - ] + hcat(rxnmat, spzeros(size(rxnmat, 1), n_genes(model))) end Accessors.reaction_variables(model::EnzymeConstrainedModel) = @@ -126,6 +114,22 @@ Accessors.metabolite_count(model::EnzymeConstrainedModel) = Accessors.metabolite_bounds(model::EnzymeConstrainedModel) = [metabolite_bounds(model.inner); spzeros(length(model.coupling_row_gene_product))] +function Accessors.metabolite_variables_matrix(model::EnzymeConstrainedModel) + irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) + enzS = enzyme_constrained_gene_product_coupling(model) + [ + irrevS spzeros(size(irrevS, 1), size(enzS, 1)) + -enzS I(size(enzS, 1)) + ] +end + +Accessors.metabolite_variables(model::EnzymeConstrainedModel) = + Accessors.Internal.make_mapping_dict( + metabolite_ids(model), + variable_ids(model), + metabolite_variables_matrix(model), + ) + Accessors.enzyme_ids(model::EnzymeConstrainedModel) = genes(model) Accessors.enzyme_count(model::EnzymeConstrainedModel) = n_genes(model) From b69b7e10e359fd9eb1c50615550b7faca5a68338 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Tue, 9 May 2023 11:15:14 +0200 Subject: [PATCH 315/531] reorder argument conditionals --- src/reconstruction/enzyme_constrained.jl | 18 +++++++------- .../simplified_enzyme_constrained.jl | 24 ++++++++++--------- 2 files changed, 23 insertions(+), 19 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 7a5bcc1d9..4f4a8dce0 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -42,24 +42,26 @@ function make_enzyme_constrained_model( gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, total_gene_product_mass_bound::Maybe{Float64} = nothing, ) - all( + if all( !isnothing, [ gene_product_mass_groups, gene_product_mass_group_bounds, total_gene_product_mass_bound, ], - ) && throw(ArgumentError("Too many arguments specified!")) - - if !isnothing(total_gene_product_mass_bound) + ) + throw(ArgumentError("Too many arguments specified!")) + elseif !isnothing(total_gene_product_mass_bound) && + all(isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) gene_product_mass_groups = Dict("uncategorized" => genes(model)) gene_product_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) + else + isnothing(gene_product_mass_groups) && + throw(ArgumentError("missing mass group specification")) + isnothing(gene_product_mass_group_bounds) && + throw(ArgumentError("missing mass group bounds")) end - isnothing(gene_product_mass_groups) && - throw(ArgumentError("missing mass group specification")) - isnothing(gene_product_mass_group_bounds) && - throw(ArgumentError("missing mass group bounds")) gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 79929d19b..bb833ab41 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -44,20 +44,22 @@ function make_simplified_enzyme_constrained_model( reaction_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, ) - all( + + if all( !isnothing, [reaction_mass_groups, reaction_mass_group_bounds, total_reaction_mass_bound], - ) && throw(ArgumentError("Too many arguments specified!")) - - # fix kwarg inputs - if !isnothing(total_reaction_mass_bound) - reaction_mass_groups = Dict("uncategorized" => variables(model)) # TODO should be reactions - reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) + ) + throw(ArgumentError("Too many arguments specified!")) + elseif !isnothing(total_reaction_mass_bound) && + all(isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) + reaction_mass_groups = Dict("uncategorized" => genes(model)) + reaction_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) + else + isnothing(reaction_mass_groups) && + throw(ArgumentError("missing reaction mass group specification")) + isnothing(reaction_mass_group_bounds) && + throw(ArgumentError("missing reaction mass group bounds")) end - isnothing(reaction_mass_groups) && - throw(ArgumentError("missing reaction mass group specification")) - isnothing(reaction_mass_group_bounds) && - throw(ArgumentError("missing reaction mass group bounds")) # helper function to rank the isozymes by relative speed speed_enzyme(model, isozyme) = From b33f3c91d771758123722e341950501d202140e9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 10 May 2023 13:53:19 +0200 Subject: [PATCH 316/531] fix errors and simplify --- src/reconstruction/enzyme_constrained.jl | 23 +++++++------------ .../simplified_enzyme_constrained.jl | 21 ++++++++--------- 2 files changed, 17 insertions(+), 27 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index 4f4a8dce0..f518f561c 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -42,25 +42,18 @@ function make_enzyme_constrained_model( gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, total_gene_product_mass_bound::Maybe{Float64} = nothing, ) - if all( - !isnothing, - [ - gene_product_mass_groups, - gene_product_mass_group_bounds, - total_gene_product_mass_bound, - ], - ) - throw(ArgumentError("Too many arguments specified!")) - elseif !isnothing(total_gene_product_mass_bound) && - all(isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) + if !isnothing(total_gene_product_mass_bound) && + all(isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) + gene_product_mass_groups = Dict("uncategorized" => genes(model)) gene_product_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) + + elseif isnothing(total_gene_product_mass_bound) && + all(!isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) + # nothing to do else - isnothing(gene_product_mass_groups) && - throw(ArgumentError("missing mass group specification")) - isnothing(gene_product_mass_group_bounds) && - throw(ArgumentError("missing mass group bounds")) + throw(ArgumentError("Arguments misspecified!")) end gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index bb833ab41..afc279bab 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -45,20 +45,17 @@ function make_simplified_enzyme_constrained_model( reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, ) - if all( - !isnothing, - [reaction_mass_groups, reaction_mass_group_bounds, total_reaction_mass_bound], - ) - throw(ArgumentError("Too many arguments specified!")) - elseif !isnothing(total_reaction_mass_bound) && - all(isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) + if !isnothing(total_reaction_mass_bound) && + all(isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) + reaction_mass_groups = Dict("uncategorized" => genes(model)) - reaction_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) + reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) + + elseif isnothing(total_reaction_mass_bound) && + all(!isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) + # nothing to do else - isnothing(reaction_mass_groups) && - throw(ArgumentError("missing reaction mass group specification")) - isnothing(reaction_mass_group_bounds) && - throw(ArgumentError("missing reaction mass group bounds")) + throw(ArgumentError("Arguments misspecified!")) end # helper function to rank the isozymes by relative speed From ae6dbd9ebf771bd8344f2992ef18b67506a9ab4a Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 10 May 2023 14:07:56 +0200 Subject: [PATCH 317/531] structure the conditions as asserts to allow easier invariant checking --- src/reconstruction/enzyme_constrained.jl | 17 ++++++++++------- .../simplified_enzyme_constrained.jl | 17 ++++++++++------- 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl index f518f561c..e7edbe8c0 100644 --- a/src/reconstruction/enzyme_constrained.jl +++ b/src/reconstruction/enzyme_constrained.jl @@ -42,18 +42,21 @@ function make_enzyme_constrained_model( gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, total_gene_product_mass_bound::Maybe{Float64} = nothing, ) - if !isnothing(total_gene_product_mass_bound) && - all(isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) + if !isnothing(total_gene_product_mass_bound) + (isnothing(gene_product_mass_groups) && isnothing(gene_product_mass_groups)) || + throw( + ArgumentError( + "argument values would be overwritten by total_gene_product_mass_bound!", + ), + ) gene_product_mass_groups = Dict("uncategorized" => genes(model)) gene_product_mass_group_bounds = Dict("uncategorized" => total_gene_product_mass_bound) + end - elseif isnothing(total_gene_product_mass_bound) && - all(!isnothing, [gene_product_mass_groups, gene_product_mass_group_bounds]) - # nothing to do - else - throw(ArgumentError("Arguments misspecified!")) + if isnothing(gene_product_mass_groups) || isnothing(gene_product_mass_group_bounds) + throw(ArgumentError("Insufficient mass group specification!")) end gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index afc279bab..af5ffcbf7 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -45,17 +45,20 @@ function make_simplified_enzyme_constrained_model( reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, ) - if !isnothing(total_reaction_mass_bound) && - all(isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) + if !isnothing(total_reaction_mass_bound) + (isnothing(gene_product_mass_groups) && isnothing(gene_product_mass_groups)) || + throw( + ArgumentError( + "argument values would be overwritten by total_gene_product_mass_bound!", + ), + ) reaction_mass_groups = Dict("uncategorized" => genes(model)) reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) + end - elseif isnothing(total_reaction_mass_bound) && - all(!isnothing, [reaction_mass_groups, reaction_mass_group_bounds]) - # nothing to do - else - throw(ArgumentError("Arguments misspecified!")) + if isnothing(reaction_mass_groups) || isnothing(reaction_mass_group_bounds) + throw(ArgumentError("Insufficient mass group specification!")) end # helper function to rank the isozymes by relative speed From c941b2be50977dd35b02efa16173be0566c0b576 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 10 May 2023 16:06:51 +0200 Subject: [PATCH 318/531] fix variable names --- src/reconstruction/simplified_enzyme_constrained.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index af5ffcbf7..5a475e17c 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -46,10 +46,10 @@ function make_simplified_enzyme_constrained_model( ) if !isnothing(total_reaction_mass_bound) - (isnothing(gene_product_mass_groups) && isnothing(gene_product_mass_groups)) || + (isnothing(reaction_mass_groups) && isnothing(reaction_mass_group_bounds)) || throw( ArgumentError( - "argument values would be overwritten by total_gene_product_mass_bound!", + "argument values would be overwritten by total_reaction_mass_bound!", ), ) From d9c9ecbdf1de600f3946b5364cee8210b0de6e45 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 18 May 2023 20:29:01 +0200 Subject: [PATCH 319/531] add more SBOs and isa functions --- src/macros/isa_xxx.jl | 48 +++++++++++++++++++++++++++++++++++ src/misc/identifiers.jl | 29 ++++++++++++++++++++- src/misc/ontology/SBOTerms.jl | 4 +++ src/utils/looks_like.jl | 6 ++++- test/utils/looks_like.jl | 7 +++++ 5 files changed, 92 insertions(+), 2 deletions(-) create mode 100644 src/macros/isa_xxx.jl diff --git a/src/macros/isa_xxx.jl b/src/macros/isa_xxx.jl new file mode 100644 index 000000000..2bde665ad --- /dev/null +++ b/src/macros/isa_xxx.jl @@ -0,0 +1,48 @@ + +""" +@_isa_fn(anno_id, identifier) + +A helper for creating functions like `isa_reaction`, `isa_gene`, `isa_metabolite`. +""" +macro _isa_fn(anno_id, identifiers) + + fname = Symbol(:isa_, anno_id) + annofunc = Symbol(anno_id, :_annotations) + body = quote + begin + anno = $annofunc(model, id) + for key in annotation_keys + if haskey(anno, key) + any(in.($identifiers, Ref(anno[key]))) && return true + end + end + return false + end + end + + docstring = """ + $fname( + model::AbstractMetabolicModel, + id::String; + annotation_keys = ["sbo", "SBO"], + ) + + Check if a $(anno_id) can be identified using the `annotation_keys`. Returns + false if no hits or if no keys are found. + """ + esc( + Expr( + :macrocall, + Symbol("@doc"), + __source__, + docstring, + :( + $fname( + model::AbstractMetabolicModel, + id::String; + annotation_keys = ["sbo", "SBO"], + ) = $body + ), + ), + ) +end diff --git a/src/misc/identifiers.jl b/src/misc/identifiers.jl index 3da59b299..2ca967201 100644 --- a/src/misc/identifiers.jl +++ b/src/misc/identifiers.jl @@ -21,6 +21,13 @@ const TRANSPORT_REACTIONS = [ SBOTerms.TRANSPORT_REACTION, SBOTerms.TRANSCELLULAR_MEMBRANE_INFLUX_REACTION, SBOTerms.TRANSCELLULAR_MEMBRANE_EFFLUX_REACTION, + SBOTerms.TRANSLOCATION_REACTION, + SBOTerms.COTRANSPORT_REACTION, + SBOTerms.ANTIPORTER_REACTION, + SBOTerms.SYMPORTER_REACTION, + SBOTerms.ACTIVE_TRANSPORT, + SBOTerms.PASSIVE_TRANSPORT, + SBOTerms.TRANSPORTER, ] const METABOLIC_REACTIONS = [SBOTerms.BIOCHEMICAL_REACTION] @@ -29,7 +36,7 @@ const BIOMASS_REACTIONS = [SBOTerms.BIOMASS_PRODUCTION] const ATP_MAINTENANCE_REACTIONS = [SBOTerms.ATP_MAINTENANCE] -const PSEUDOREACTIONS = [ +const PSEUDO_REACTIONS = [ SBOTerms.EXCHANGE_REACTION, SBOTerms.DEMAND_REACTION, SBOTerms.BIOMASS_PRODUCTION, @@ -39,4 +46,24 @@ const PSEUDOREACTIONS = [ ] const SPONTANEOUS_REACTIONS = [SBOTerms.SPONTANEOUS_REACTION] + +const METABOLITES = [ + SBOTerms.SIMPLE_CHEMICAL, + SBOTerms.METABOLITE, +] + +const GENES = [ + SBOTerms.GENE, +] + +const REACTIONS = [ + EXCHANGE_REACTIONS; + TRANSPORT_REACTIONS; + METABOLIC_REACTIONS; + BIOMASS_REACTIONS; + ATP_MAINTENANCE_REACTIONS; + PSEUDO_REACTIONS; + SPONTANEOUS_REACTIONS; +] + end diff --git a/src/misc/ontology/SBOTerms.jl b/src/misc/ontology/SBOTerms.jl index f89c14a99..c25dab963 100644 --- a/src/misc/ontology/SBOTerms.jl +++ b/src/misc/ontology/SBOTerms.jl @@ -85,6 +85,10 @@ const ATP_MAINTENANCE = "SBO:0000630" const PSEUDOREACTION = "SBO:0000631" const SINK_REACTION = "SBO:0000632" const SPONTANEOUS_REACTION = "SBO:0000672" +const TRANSLOCATION_REACTION = "SBO:0000185" +const COTRANSPORT_REACTION = "SBO:0000654" +const ANTIPORTER_REACTION = "SBO:0000660" +const SYMPORTER_REACTION = "SBO:0000659" const ACTIVE_TRANSPORT = "SBO:0000657" const PASSIVE_TRANSPORT = "SBO:0000658" diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index f6402f346..f670dba50 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -144,6 +144,10 @@ find_extracellular_metabolite_ids(m::AbstractMetabolicModel; kwargs...) = @_is_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS @_is_reaction_fn "biomass" Identifiers.BIOMASS_REACTIONS @_is_reaction_fn "atp_maintenance" Identifiers.ATP_MAINTENANCE_REACTIONS -@_is_reaction_fn "pseudo" Identifiers.PSEUDOREACTIONS +@_is_reaction_fn "pseudo" Identifiers.PSEUDO_REACTIONS @_is_reaction_fn "metabolic" Identifiers.METABOLIC_REACTIONS @_is_reaction_fn "spontaneous" Identifiers.SPONTANEOUS_REACTIONS + +@_isa_fn "gene" Identifiers.GENES +@_isa_fn "metabolite" Identifiers.METABOLITES +@_isa_fn "reaction" Identifiers.REACTIONS \ No newline at end of file diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index e3aa8f8f8..662234ae6 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -83,4 +83,11 @@ end # macro generated, so only test positive and negative case @test !is_biomass_reaction(model, "PFL") @test is_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") + + @test isa_reaction(model, "PFL") + @test_throws KeyError isa_reaction(model, "atp_c") + @test isa_gene(model, "b2464") + @test_throws KeyError isa_gene(model, "atp_c") + @test isa_metabolite(model, "atp_c") + @test_throws KeyError isa_metabolite(model, "PFL") end From 80a868a317db2e4a99717365076e5fb5c02dda38 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 18 May 2023 20:41:50 +0200 Subject: [PATCH 320/531] remove error throw and return false if id not in class --- src/macros/isa_xxx.jl | 2 ++ test/utils/looks_like.jl | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/macros/isa_xxx.jl b/src/macros/isa_xxx.jl index 2bde665ad..d96f648a6 100644 --- a/src/macros/isa_xxx.jl +++ b/src/macros/isa_xxx.jl @@ -8,8 +8,10 @@ macro _isa_fn(anno_id, identifiers) fname = Symbol(:isa_, anno_id) annofunc = Symbol(anno_id, :_annotations) + accessorfunc = Symbol(anno_id, :s) body = quote begin + id in $accessorfunc(model) || return false anno = $annofunc(model, id) for key in annotation_keys if haskey(anno, key) diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index 662234ae6..bd61c0939 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -85,9 +85,9 @@ end @test is_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") @test isa_reaction(model, "PFL") - @test_throws KeyError isa_reaction(model, "atp_c") + @test !isa_reaction(model, "atp_c") @test isa_gene(model, "b2464") - @test_throws KeyError isa_gene(model, "atp_c") + @test !isa_gene(model, "atp_c") @test isa_metabolite(model, "atp_c") - @test_throws KeyError isa_metabolite(model, "PFL") + @test !isa_metabolite(model, "PFL") end From f8bcd40192df30fad7056bd8b0921cf358b6cfda Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 18 May 2023 21:02:29 +0200 Subject: [PATCH 321/531] rename is_ functions to is_sbo_ --- src/macros/{isa_xxx.jl => is_sbo_xxx.jl} | 16 ++++++------- ...xxx_reaction.jl => is_sbo_xxx_reaction.jl} | 22 +++++++++--------- src/misc/identifiers.jl | 23 ++++++++----------- src/utils/looks_like.jl | 22 +++++++++--------- test/utils/looks_like.jl | 16 ++++++------- 5 files changed, 47 insertions(+), 52 deletions(-) rename src/macros/{isa_xxx.jl => is_sbo_xxx.jl} (66%) rename src/macros/{is_xxx_reaction.jl => is_sbo_xxx_reaction.jl} (55%) diff --git a/src/macros/isa_xxx.jl b/src/macros/is_sbo_xxx.jl similarity index 66% rename from src/macros/isa_xxx.jl rename to src/macros/is_sbo_xxx.jl index d96f648a6..547e5b6c3 100644 --- a/src/macros/isa_xxx.jl +++ b/src/macros/is_sbo_xxx.jl @@ -1,19 +1,19 @@ """ -@_isa_fn(anno_id, identifier) +@_is_sbo_fn(anno_id, identifier) -A helper for creating functions like `isa_reaction`, `isa_gene`, `isa_metabolite`. +A helper for creating functions like `is_sbo_reaction`, `is_sbo_gene`, `is_sbo_metabolite`. """ -macro _isa_fn(anno_id, identifiers) +macro _is_sbo_fn(anno_id, identifiers) - fname = Symbol(:isa_, anno_id) + fname = Symbol(:is_sbo_, anno_id) annofunc = Symbol(anno_id, :_annotations) accessorfunc = Symbol(anno_id, :s) body = quote begin id in $accessorfunc(model) || return false anno = $annofunc(model, id) - for key in annotation_keys + for key in sbo_annotation_keys if haskey(anno, key) any(in.($identifiers, Ref(anno[key]))) && return true end @@ -26,10 +26,10 @@ macro _isa_fn(anno_id, identifiers) $fname( model::AbstractMetabolicModel, id::String; - annotation_keys = ["sbo", "SBO"], + sbo_annotation_keys = ["sbo", "SBO"], ) - Check if a $(anno_id) can be identified using the `annotation_keys`. Returns + Check if a $(anno_id) can be identified using the `sbo_annotation_keys`. Returns false if no hits or if no keys are found. """ esc( @@ -42,7 +42,7 @@ macro _isa_fn(anno_id, identifiers) $fname( model::AbstractMetabolicModel, id::String; - annotation_keys = ["sbo", "SBO"], + sbo_annotation_keys = ["sbo", "SBO"], ) = $body ), ), diff --git a/src/macros/is_xxx_reaction.jl b/src/macros/is_sbo_xxx_reaction.jl similarity index 55% rename from src/macros/is_xxx_reaction.jl rename to src/macros/is_sbo_xxx_reaction.jl index f5ec5118b..bbce4b85e 100644 --- a/src/macros/is_xxx_reaction.jl +++ b/src/macros/is_sbo_xxx_reaction.jl @@ -1,18 +1,19 @@ """ -@_is_reaction_fn(anno_id, identifier) +@_is_sbo_reaction_fn(anno_id, identifier) -A helper for creating functions like `is_exchange_reaction`. +A helper for creating functions like `is_sbo_exchange_reaction`. """ -macro _is_reaction_fn(anno_id, identifiers) +macro _is_sbo_reaction_fn(anno_id, identifiers) - fname = Symbol(:is_, anno_id, :_reaction) + fname = Symbol(:is_sbo_, anno_id, :_reaction) grammar = any(startswith.(anno_id, ["a", "e", "i", "o", "u"])) ? "an" : "a" body = quote begin + reaction_id in reactions(model) || return false anno = reaction_annotations(model, reaction_id) - for key in annotation_keys + for key in sbo_annotation_keys if haskey(anno, key) any(in.($identifiers, Ref(anno[key]))) && return true end @@ -25,13 +26,12 @@ macro _is_reaction_fn(anno_id, identifiers) $fname( model::AbstractMetabolicModel, reaction_id::String; - annotation_keys = ["sbo", "SBO"], + sbo_annotation_keys = ["sbo", "SBO"], ) - Check if a reaction is annotated as $(grammar) $(anno_id) reaction. Uses - `$identifiers` internally, which includes SBO identifiers. In - the reaction annotations, use the keys in `annotation_keys` to look for entries. - Returns false if no hits or if no keys are found. + Check if a reaction is annotated as $(grammar) SBO annotated $(anno_id) + reaction. In the reaction annotations, use the keys in `sbo_annotation_keys` + to look for entries. Returns false if no hits or if no keys are found. """ esc( Expr( @@ -43,7 +43,7 @@ macro _is_reaction_fn(anno_id, identifiers) $fname( model::AbstractMetabolicModel, reaction_id::String; - annotation_keys = ["sbo", "SBO"], + sbo_annotation_keys = ["sbo", "SBO"], ) = $body ), ), diff --git a/src/misc/identifiers.jl b/src/misc/identifiers.jl index 2ca967201..08a0ff99c 100644 --- a/src/misc/identifiers.jl +++ b/src/misc/identifiers.jl @@ -47,23 +47,18 @@ const PSEUDO_REACTIONS = [ const SPONTANEOUS_REACTIONS = [SBOTerms.SPONTANEOUS_REACTION] -const METABOLITES = [ - SBOTerms.SIMPLE_CHEMICAL, - SBOTerms.METABOLITE, -] +const METABOLITES = [SBOTerms.SIMPLE_CHEMICAL, SBOTerms.METABOLITE] -const GENES = [ - SBOTerms.GENE, -] +const GENES = [SBOTerms.GENE] const REACTIONS = [ - EXCHANGE_REACTIONS; - TRANSPORT_REACTIONS; - METABOLIC_REACTIONS; - BIOMASS_REACTIONS; - ATP_MAINTENANCE_REACTIONS; - PSEUDO_REACTIONS; - SPONTANEOUS_REACTIONS; + EXCHANGE_REACTIONS + TRANSPORT_REACTIONS + METABOLIC_REACTIONS + BIOMASS_REACTIONS + ATP_MAINTENANCE_REACTIONS + PSEUDO_REACTIONS + SPONTANEOUS_REACTIONS ] end diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl index f670dba50..c38b6d279 100644 --- a/src/utils/looks_like.jl +++ b/src/utils/looks_like.jl @@ -140,14 +140,14 @@ forwarded to [`looks_like_extracellular_metabolite`](@ref). find_extracellular_metabolite_ids(m::AbstractMetabolicModel; kwargs...) = findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) -@_is_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS -@_is_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS -@_is_reaction_fn "biomass" Identifiers.BIOMASS_REACTIONS -@_is_reaction_fn "atp_maintenance" Identifiers.ATP_MAINTENANCE_REACTIONS -@_is_reaction_fn "pseudo" Identifiers.PSEUDO_REACTIONS -@_is_reaction_fn "metabolic" Identifiers.METABOLIC_REACTIONS -@_is_reaction_fn "spontaneous" Identifiers.SPONTANEOUS_REACTIONS - -@_isa_fn "gene" Identifiers.GENES -@_isa_fn "metabolite" Identifiers.METABOLITES -@_isa_fn "reaction" Identifiers.REACTIONS \ No newline at end of file +@_is_sbo_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS +@_is_sbo_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS +@_is_sbo_reaction_fn "biomass" Identifiers.BIOMASS_REACTIONS +@_is_sbo_reaction_fn "atp_maintenance" Identifiers.ATP_MAINTENANCE_REACTIONS +@_is_sbo_reaction_fn "pseudo" Identifiers.PSEUDO_REACTIONS +@_is_sbo_reaction_fn "metabolic" Identifiers.METABOLIC_REACTIONS +@_is_sbo_reaction_fn "spontaneous" Identifiers.SPONTANEOUS_REACTIONS + +@_is_sbo_fn "gene" Identifiers.GENES +@_is_sbo_fn "metabolite" Identifiers.METABOLITES +@_is_sbo_fn "reaction" Identifiers.REACTIONS diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl index bd61c0939..9b6f0996c 100644 --- a/test/utils/looks_like.jl +++ b/test/utils/looks_like.jl @@ -81,13 +81,13 @@ end model = load_model(ObjectModel, model_paths["e_coli_core.json"]) # macro generated, so only test positive and negative case - @test !is_biomass_reaction(model, "PFL") - @test is_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") + @test !is_sbo_biomass_reaction(model, "PFL") + @test is_sbo_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") - @test isa_reaction(model, "PFL") - @test !isa_reaction(model, "atp_c") - @test isa_gene(model, "b2464") - @test !isa_gene(model, "atp_c") - @test isa_metabolite(model, "atp_c") - @test !isa_metabolite(model, "PFL") + @test is_sbo_reaction(model, "PFL") + @test !is_sbo_reaction(model, "atp_c") + @test is_sbo_gene(model, "b2464") + @test !is_sbo_gene(model, "atp_c") + @test is_sbo_metabolite(model, "atp_c") + @test !is_sbo_metabolite(model, "PFL") end From ad74e9401f7a92f464a86c8afb4fec6ce4ac6f01 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 19 May 2023 20:55:57 +0200 Subject: [PATCH 322/531] reaction duplication is stricter now --- src/utils/Reaction.jl | 67 ++++++++++++++++++++++++------------------ test/types/Reaction.jl | 35 ++++++++-------------- 2 files changed, 51 insertions(+), 51 deletions(-) diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl index 4a59b2b5e..e7001e908 100644 --- a/src/utils/Reaction.jl +++ b/src/utils/Reaction.jl @@ -1,41 +1,50 @@ """ $(TYPEDSIGNATURES) -Check if `rxn` already exists in `rxns` but has another `id`. -If `only_metabolites` is `true` then only the metabolite `id`s are checked. -Otherwise, compares metabolite `id`s and the absolute value of their stoichiometric coefficients to those of `rxn`. -If `rxn` has the same reaction equation as another reaction in `rxns`, the return the `id`. -Otherwise return `nothing`. +Check if `rxn` already exists in `model`, but has another `id`. For a reaction +to be duplicated the substrates, their stoichiometric coefficients, and the +reaction direction needs to be exactly the same as some other reaction in +`model`. Returns a list of reaction `id`s that are duplicates of `rxn`, or an +empty list otherwise. + +# Notes + +This method ignores any reactions in `model` with the same `id` as `rxn`. This +is convenient to identify duplicate reactions. The reaction directions need to +be exactly the same, as in a forward and a reversible reaction are counted as +different reactions for example. See also: [`reaction_mass_balanced`](@ref) """ -function check_duplicate_reaction( - crxn::Reaction, - rxns::OrderedDict{String,Reaction}; - only_metabolites = true, +function reaction_is_duplicated( + model::ObjectModel, # TODO this can be generalized if getting the bounds of reaction with semantics gets finalized + rxn::Reaction, ) - for (k, rxn) in rxns - if rxn.id != crxn.id # skip if same ID - if only_metabolites # only check if metabolites are the same - if issetequal(keys(crxn.metabolites), keys(rxn.metabolites)) - return k - end - else # also check the stoichiometric coefficients - reaction_checker = true - for (kk, vv) in rxn.metabolites # get reaction stoich - if abs(get(crxn.metabolites, kk, 0)) != abs(vv) # if at least one stoich doesn't match - reaction_checker = false - break - end - end - if reaction_checker && - issetequal(keys(crxn.metabolites), keys(rxn.metabolites)) - return k - end - end + duplicated_rxns = String[] + + for rid in reactions(model) + rid == rxn.id && continue + + rs = reaction_stoichiometry(model, rid) + + # metabolite ids the same + issetequal(keys(rxn.metabolites), keys(rs)) || continue + + # stoichiometry the same + dir_correction = sign(rs[first(keys(rs))]) == sign(rxn.metabolites[first(keys(rs))]) ? 1 : -1 + all(dir_correction * rs[mid] == rxn.metabolites[mid] for mid in keys(rs)) || continue + + # directions the same: A -> B forward and B <- A backward are the same + r = model.reactions[rid] + if dir_correction == 1 + sign(r.lower_bound) == sign(rxn.lower_bound) && sign(r.upper_bound) == sign(rxn.upper_bound) && push!(duplicated_rxns, rid) + else + sign(dir_correction * r.lower_bound) == sign(rxn.upper_bound) && + sign(dir_correction * r.upper_bound) == sign(rxn.lower_bound) && push!(duplicated_rxns, rid) end end - return nothing + + return duplicated_rxns end """ diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index ddcce07e2..5b0c5464d 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -6,13 +6,6 @@ m3 = Metabolite("m3") m4 = Metabolite("m4") m5 = Metabolite("m5") - m6 = Metabolite("m6") - m7 = Metabolite("m7") - m8 = Metabolite("m8") - m9 = Metabolite("m9") - m10 = Metabolite("m10") - m11 = Metabolite("m11") - m12 = Metabolite("m12") g1 = Gene("g1") g2 = Gene("g2") @@ -43,25 +36,23 @@ r3 = ReactionForward("r3", Dict(m3.id => -1.0, m4.id => 1.0)) @test r3.lower_bound == 0.0 && r3.upper_bound == 1000.0 - r4 = ReactionBidirectional("r4", Dict(m3.id => -1.0, m4.id => 1.0)) + r4 = ReactionBidirectional("r4", Dict(m4.id => -1.0, m5.id => 1.0)) r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) @test r4.lower_bound == -1000.0 && r4.upper_bound == 1000.0 rd = OrderedDict(r.id => r for r in [r1, r2, r3, r4]) @test issetequal(["r1", "r4"], ambiguously_identified_items(annotation_index(rd))) - id = check_duplicate_reaction(r4, rd) - @test id == "r3" - - r5 = ReactionBidirectional("r5", Dict(m3.id => -11.0, m4.id => 1.0)) - id = check_duplicate_reaction(r5, rd) - @test id == "r3" - - r5 = ReactionBidirectional("r5", Dict(m3.id => -11.0, m4.id => 1.0)) - id = check_duplicate_reaction(r5, rd; only_metabolites = false) - @test isnothing(id) - - r5 = ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)) - id = check_duplicate_reaction(r5, rd; only_metabolites = false) - @test id == "r3" + model = ObjectModel() + add_reactions!(model, [r1, r2, r3, r4]) + add_metabolites!(model, [m1, m2, m3, m4, m5]) + + @test isempty(reaction_is_duplicated(model, r4)) + @test isempty(reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)))) + @test "r3" in reaction_is_duplicated(model, ReactionForward("r5", Dict(m3.id => -1.0, m4.id => 1.0))) + @test "r3" in reaction_is_duplicated(model, ReactionBackward("r5", Dict(m3.id => 1.0, m4.id => -1.0))) + @test isempty(reaction_is_duplicated(model, ReactionBackward("r5", Dict(m3.id => -1.0, m4.id => 1.0)))) + @test "r4" in reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => -1.0, m5.id => 1.0))) + @test "r4" in reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => 1.0, m5.id => -1.0))) + @test isempty(reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => 2.0, m5.id => -1.0)))) end From cb3449bc130626306723ccd634d7e9c0951288e5 Mon Sep 17 00:00:00 2001 From: stelmo Date: Fri, 19 May 2023 19:00:07 +0000 Subject: [PATCH 323/531] automatic formatting triggered by @stelmo on PR #787 --- src/utils/Reaction.jl | 27 ++++++++++++++++----------- test/types/Reaction.jl | 41 ++++++++++++++++++++++++++++++++++------- 2 files changed, 50 insertions(+), 18 deletions(-) diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl index e7001e908..4379dc569 100644 --- a/src/utils/Reaction.jl +++ b/src/utils/Reaction.jl @@ -5,7 +5,7 @@ Check if `rxn` already exists in `model`, but has another `id`. For a reaction to be duplicated the substrates, their stoichiometric coefficients, and the reaction direction needs to be exactly the same as some other reaction in `model`. Returns a list of reaction `id`s that are duplicates of `rxn`, or an -empty list otherwise. +empty list otherwise. # Notes @@ -21,29 +21,34 @@ function reaction_is_duplicated( rxn::Reaction, ) duplicated_rxns = String[] - + for rid in reactions(model) rid == rxn.id && continue rs = reaction_stoichiometry(model, rid) - + # metabolite ids the same issetequal(keys(rxn.metabolites), keys(rs)) || continue - + # stoichiometry the same - dir_correction = sign(rs[first(keys(rs))]) == sign(rxn.metabolites[first(keys(rs))]) ? 1 : -1 - all(dir_correction * rs[mid] == rxn.metabolites[mid] for mid in keys(rs)) || continue - + dir_correction = + sign(rs[first(keys(rs))]) == sign(rxn.metabolites[first(keys(rs))]) ? 1 : -1 + all(dir_correction * rs[mid] == rxn.metabolites[mid] for mid in keys(rs)) || + continue + # directions the same: A -> B forward and B <- A backward are the same r = model.reactions[rid] if dir_correction == 1 - sign(r.lower_bound) == sign(rxn.lower_bound) && sign(r.upper_bound) == sign(rxn.upper_bound) && push!(duplicated_rxns, rid) + sign(r.lower_bound) == sign(rxn.lower_bound) && + sign(r.upper_bound) == sign(rxn.upper_bound) && + push!(duplicated_rxns, rid) else - sign(dir_correction * r.lower_bound) == sign(rxn.upper_bound) && - sign(dir_correction * r.upper_bound) == sign(rxn.lower_bound) && push!(duplicated_rxns, rid) + sign(dir_correction * r.lower_bound) == sign(rxn.upper_bound) && + sign(dir_correction * r.upper_bound) == sign(rxn.lower_bound) && + push!(duplicated_rxns, rid) end end - + return duplicated_rxns end diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl index 5b0c5464d..bb589bbed 100644 --- a/test/types/Reaction.jl +++ b/test/types/Reaction.jl @@ -48,11 +48,38 @@ add_metabolites!(model, [m1, m2, m3, m4, m5]) @test isempty(reaction_is_duplicated(model, r4)) - @test isempty(reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)))) - @test "r3" in reaction_is_duplicated(model, ReactionForward("r5", Dict(m3.id => -1.0, m4.id => 1.0))) - @test "r3" in reaction_is_duplicated(model, ReactionBackward("r5", Dict(m3.id => 1.0, m4.id => -1.0))) - @test isempty(reaction_is_duplicated(model, ReactionBackward("r5", Dict(m3.id => -1.0, m4.id => 1.0)))) - @test "r4" in reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => -1.0, m5.id => 1.0))) - @test "r4" in reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => 1.0, m5.id => -1.0))) - @test isempty(reaction_is_duplicated(model, ReactionBidirectional("r5", Dict(m4.id => 2.0, m5.id => -1.0)))) + @test isempty( + reaction_is_duplicated( + model, + ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)), + ), + ) + @test "r3" in reaction_is_duplicated( + model, + ReactionForward("r5", Dict(m3.id => -1.0, m4.id => 1.0)), + ) + @test "r3" in reaction_is_duplicated( + model, + ReactionBackward("r5", Dict(m3.id => 1.0, m4.id => -1.0)), + ) + @test isempty( + reaction_is_duplicated( + model, + ReactionBackward("r5", Dict(m3.id => -1.0, m4.id => 1.0)), + ), + ) + @test "r4" in reaction_is_duplicated( + model, + ReactionBidirectional("r5", Dict(m4.id => -1.0, m5.id => 1.0)), + ) + @test "r4" in reaction_is_duplicated( + model, + ReactionBidirectional("r5", Dict(m4.id => 1.0, m5.id => -1.0)), + ) + @test isempty( + reaction_is_duplicated( + model, + ReactionBidirectional("r5", Dict(m4.id => 2.0, m5.id => -1.0)), + ), + ) end From e1f06f68f1e2460d3464427762e535a0c119835b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 23 May 2023 12:27:04 +0200 Subject: [PATCH 324/531] Update src/reconstruction/simplified_enzyme_constrained.jl (github-level fix attempt 1) --- src/reconstruction/simplified_enzyme_constrained.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index 5a475e17c..c57085293 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -53,7 +53,7 @@ function make_simplified_enzyme_constrained_model( ), ) - reaction_mass_groups = Dict("uncategorized" => genes(model)) + reaction_mass_groups = Dict("uncategorized" => variables(model)) reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) end From 848528486f61de263857c5150137490354ac5b93 Mon Sep 17 00:00:00 2001 From: exaexa Date: Tue, 23 May 2023 10:32:37 +0000 Subject: [PATCH 325/531] automatic formatting triggered by @exaexa on PR #780 --- src/reconstruction/simplified_enzyme_constrained.jl | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl index c57085293..3cde7ecdb 100644 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ b/src/reconstruction/simplified_enzyme_constrained.jl @@ -46,12 +46,11 @@ function make_simplified_enzyme_constrained_model( ) if !isnothing(total_reaction_mass_bound) - (isnothing(reaction_mass_groups) && isnothing(reaction_mass_group_bounds)) || - throw( - ArgumentError( - "argument values would be overwritten by total_reaction_mass_bound!", - ), - ) + (isnothing(reaction_mass_groups) && isnothing(reaction_mass_group_bounds)) || throw( + ArgumentError( + "argument values would be overwritten by total_reaction_mass_bound!", + ), + ) reaction_mass_groups = Dict("uncategorized" => variables(model)) reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) From d9132baaf6eecb8b3690ca8613d2d0062202a410 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 23 May 2023 11:57:26 +0200 Subject: [PATCH 326/531] cherry-pick the SBML fix to here Original commit: 391b84053c38b7e31c163587804154288a077e4c --- src/types/models/SBMLModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index a4933e6b0..157f80eae 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -137,7 +137,7 @@ end Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) function Accessors.objective(model::SBMLModel)::SparseVec - res = sparsevec([], [], n_reactions(model)) + res = spzeros(n_reactions(model)) objective = get(model.sbml.objectives, model.active_objective, nothing) if isnothing(objective) && length(model.sbml.objectives) == 1 From 6c9ee73a50cb3cb678453abf24d90cb7ba12a88b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 9 Jun 2023 11:48:18 +0200 Subject: [PATCH 327/531] regex match http and https --- src/types/models/SBMLModel.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl index 157f80eae..207cd37be 100644 --- a/src/types/models/SBMLModel.jl +++ b/src/types/models/SBMLModel.jl @@ -182,7 +182,7 @@ Accessors.metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = model.sbml.species[mid].charge function _parse_sbml_identifiers_org_uri(uri::String)::Tuple{String,String} - m = match(r"^http://identifiers.org/([^/]+)/(.*)$", uri) + m = match(r"^https?://identifiers.org/([^/]+)/(.*)$", uri) isnothing(m) ? ("RESOURCE_URI", uri) : (m[1], m[2]) end From 795a7f1dc4bf88cece6e39b0b2dbd0e806849a39 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 14 Sep 2023 23:24:09 +0200 Subject: [PATCH 328/531] migration to ConstraintTrees step 1 --- Project.toml | 20 +-- src/COBREXA.jl | 69 +------- src/analysis.jl | 77 --------- src/internal.jl | 18 --- src/io.jl | 51 ------ src/io/show/AbstractMetabolicModel.jl | 11 -- src/io/show/CommunityModel.jl | 10 -- src/io/show/FluxSummary.jl | 48 ------ src/io/show/FluxVariabilitySummary.jl | 68 -------- src/io/show/ModelWithResult.jl | 3 - src/io/show/Serialized.jl | 12 -- src/io/show/print_kwdef.jl | 69 -------- src/log.jl | 94 ----------- src/macros.jl | 17 -- src/moduletools.jl | 60 ------- src/reconstruction.jl | 64 -------- src/solver.jl | 221 +++++--------------------- src/types.jl | 98 +----------- src/utils.jl | 40 ----- src/wrappers.jl | 45 ------ 20 files changed, 51 insertions(+), 1044 deletions(-) delete mode 100644 src/analysis.jl delete mode 100644 src/internal.jl delete mode 100644 src/io.jl delete mode 100644 src/io/show/AbstractMetabolicModel.jl delete mode 100644 src/io/show/CommunityModel.jl delete mode 100644 src/io/show/FluxSummary.jl delete mode 100644 src/io/show/FluxVariabilitySummary.jl delete mode 100644 src/io/show/ModelWithResult.jl delete mode 100644 src/io/show/Serialized.jl delete mode 100644 src/io/show/print_kwdef.jl delete mode 100644 src/log.jl delete mode 100644 src/macros.jl delete mode 100644 src/moduletools.jl delete mode 100644 src/reconstruction.jl delete mode 100644 src/utils.jl delete mode 100644 src/wrappers.jl diff --git a/Project.toml b/Project.toml index 45b1aeba1..a9f1f3edf 100644 --- a/Project.toml +++ b/Project.toml @@ -4,39 +4,23 @@ authors = ["The developers of COBREXA.jl"] version = "2.0.0" [deps] +ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" -MAT = "23992714-dd62-5051-b70f-ba57cb901cac" -MacroTools = "1914dd2f-81c6-5fcd-8719-6d5c9610ff09" -OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" -PikaParser = "3bbf5609-3e7b-44cd-8549-7c69f321e792" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" -SBML = "e5567a89-2604-4b09-9718-f5f78e97c3bb" -Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" -Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" [compat] Clarabel = "0.3" -DistributedData = "0.1.4, 0.2" +DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" -HDF5 = "0.16" -JSON = "0.21" JuMP = "1" -MAT = "0.10" -MacroTools = "0.5.6" -OrderedCollections = "1.4" -PikaParser = "0.5" -SBML = "1.4.2" StableRNGs = "1.0" -Tulip = "0.7.0, 0.8.0, 0.9.2" julia = "1.5" [extras] diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 075f8c015..0d6f81702 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -1,69 +1,14 @@ """ -``` -\\\\\\\\\\ // // | COBREXA.jl v$(COBREXA.version) - \\\\ \\\\// // | - \\\\ \\/ // | COnstraint-Based Reconstruction - \\\\ // | and EXascale Analysis in Julia - // \\\\ | - // /\\ \\\\ | See documentation and examples at: - // //\\\\ \\\\ | https://lcsb-biocore.github.io/COBREXA.jl -// // \\\\\\\\\\ | -``` - -To start up quickly, install your favorite optimizer, load a metabolic model in -a format such as SBML or JSON, and run a metabolic analysis such as the flux -balance analysis: -``` -import Pkg; Pkg.add("GLPK") -using COBREXA, GLPK -model = load_model("e_coli_core.xml") -x = flux_balance_analysis_dict(model, GLPK.Optimizer) -flux_summary(x) -``` - -A complete overview of the functionality can be found in the documentation. +$(README) """ module COBREXA -import Pkg - -# versioning tools -const _PKG_ROOT_DIR = normpath(joinpath(@__DIR__, "..")) -include_dependency(joinpath(_PKG_ROOT_DIR, "Project.toml")) - -const version = - VersionNumber(Pkg.TOML.parsefile(joinpath(_PKG_ROOT_DIR, "Project.toml"))["version"]) - -# bootstrap the module machinery -include("moduletools.jl") -using .ModuleTools - -# load various internal helpers first -@inc internal -@inc log - -# start loading individual user-facing modules -@inc types -@inc wrappers +using DocStringExtensions -@inc io -@inc solver -@inc analysis -@inc reconstruction -@inc utils +import ConstraintTrees as C +import Distributed as D -module Everything - using ..ModuleTools - @reexport Accessors - @reexport Analysis - @reexport Analysis Modifications - @reexport IO - @reexport Reconstruction - @reexport Reconstruction Pipes - @reexport Solver - @reexport Types - @reexport Wrappers - @reexport Utils -end +include("types.jl") +include("solver.jl") -end # module +end # module COBREXA diff --git a/src/analysis.jl b/src/analysis.jl deleted file mode 100644 index 67b522c4b..000000000 --- a/src/analysis.jl +++ /dev/null @@ -1,77 +0,0 @@ - -""" - module Analysis - -Contains the analysis functions of COBREXA.jl. Typically, these take a -[`AbstractMetabolicModel`](@ref), convert it to the solver represenation and run -various optimization tasks on top of it, such as finding an optimum (e.g. in -[`flux_balance_analysis`](@ref)) or multiple optima (e.g., -[`flux_variability_analysis`](@ref)). - -Functions [`screen`](@ref) and [`screen_optmodel_modifications`](@ref) are -special meta-analyses that apply another specified analysis to multiple -systematically generated versions of the same input model. - -# Exports -$(EXPORTS) -""" -module Analysis -using ..ModuleTools -@dse - -using ..Accessors -using ..Wrappers -using ..Internal: constants -using ..Log.Internal: @models_log -using ..Solver -using ..Types -using ..Wrappers.Internal: - EnzymeConstrainedReactionColumn, SimplifiedEnzymeConstrainedColumn - -using Distributed -using DistributedData -using JuMP -using LinearAlgebra -using Random -using SparseArrays -using StableRNGs - -@inc_dir analysis -@inc_dir analysis sampling -@inc_dir analysis reconstruction - -""" - module Modifications - -Functions that implement well-defined modifications of the optimization model -before solving. These can be used as `modifications` parameter in e.g. -[`flux_balance_analysis`](@ref) and other analysis functions. - -# Exports -$(EXPORTS) -""" -module Modifications -# TODO move this into Solver - using ..ModuleTools - @dse -end - -@export_locals -end - -# this needs to import from Analysis -@inject Analysis.Modifications begin - using ...Accessors - using ...Analysis - using ...Internal: constants, environment_exchange_stoichiometry, check_abundances - using ...Solver - using ...Types - - using JuMP - using LinearAlgebra - using SparseArrays - - @inc_dir analysis modifications - - @export_locals -end diff --git a/src/internal.jl b/src/internal.jl deleted file mode 100644 index 80795c4c0..000000000 --- a/src/internal.jl +++ /dev/null @@ -1,18 +0,0 @@ -""" - module Internal - -Internal COBREXA.jl functionality, mostly constants and macros. - -# Exports -$(EXPORTS) -""" -module Internal -using ..ModuleTools -@dse - -@inc macros -@inc_dir misc ontology -@inc_dir misc - -@export_locals -end diff --git a/src/io.jl b/src/io.jl deleted file mode 100644 index 53e5cb5ad..000000000 --- a/src/io.jl +++ /dev/null @@ -1,51 +0,0 @@ -""" - module IO - -Reading and writing the models from/to storage. - -# Exports -$(EXPORTS) -""" -module IO -using ..ModuleTools -@dse - -using ..Types -using ..Types.Internal: maybemap, unparse_grr -using ..Accessors -using ..Internal: constants -using ..Log.Internal: @io_log - -using JSON, MAT, SBML, HDF5 - -@inc_dir io -@inc_dir io show - -""" - module Internal - -Internal IO helpers. - -# Exports -$(EXPORTS) -""" -module Internal - using ..ModuleTools - @dse -end - -@export_locals -end - -@inject IO.Internal begin - using ..Types - using HDF5 - using SparseArrays - - @inc_dir io misc - - @export_locals -end - -@inject IO using .Internal -@inject Types using ..IO.Internal: h5_read_sparse diff --git a/src/io/show/AbstractMetabolicModel.jl b/src/io/show/AbstractMetabolicModel.jl deleted file mode 100644 index 88f1217c1..000000000 --- a/src/io/show/AbstractMetabolicModel.jl +++ /dev/null @@ -1,11 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Pretty printing of everything metabolic-modelish. -""" -function Base.show(io::Base.IO, ::MIME"text/plain", m::AbstractMetabolicModel) - print( - io, - "$(typeof(m))(#= $(n_reactions(m)) reactions, $(n_metabolites(m)) metabolites =#)", - ) -end diff --git a/src/io/show/CommunityModel.jl b/src/io/show/CommunityModel.jl deleted file mode 100644 index 9d8371d76..000000000 --- a/src/io/show/CommunityModel.jl +++ /dev/null @@ -1,10 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityMember) - println(io, "A community member with $(n_variables(cm.model)) internal variables.") -end - -function Base.show(io::Base.IO, ::MIME"text/plain", cm::CommunityModel) - println( - io, - "A basic community model comprised of $(length(cm.members)) underlying models.", - ) -end diff --git a/src/io/show/FluxSummary.jl b/src/io/show/FluxSummary.jl deleted file mode 100644 index 5aafb6cfb..000000000 --- a/src/io/show/FluxSummary.jl +++ /dev/null @@ -1,48 +0,0 @@ -function _pad_spaces(slen::Int, maxlen::Int) - " "^max(0, maxlen - slen + 1) -end - -_pad_spaces(str::String, maxlen::Int) = _pad_spaces(length(str), maxlen) - -function Base.show(io::Base.IO, ::MIME"text/plain", flux_res::FluxSummary) - longest_biomass_len = - maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) - longest_import_len = maximum(length(k) for k in keys(flux_res.import_fluxes); init = 0) - longest_export_len = maximum(length(k) for k in keys(flux_res.export_fluxes); init = 0) - - if !isempty(flux_res.unbounded_fluxes) - longest_unbounded_len = - maximum([length(k) for k in keys(flux_res.unbounded_fluxes)]) - word_pad = max( - longest_biomass_len, - longest_export_len, - longest_import_len, - longest_unbounded_len, - ) - else - word_pad = max(longest_biomass_len, longest_export_len, longest_import_len) - end - - println(io, "Biomass") - for (k, v) in flux_res.biomass_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - println(io, "Import") - for (k, v) in flux_res.import_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - println(io, "Export") - for (k, v) in flux_res.export_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - - if !isempty(flux_res.unbounded_fluxes) - println(io, "Unbounded") - for (k, v) in flux_res.unbounded_fluxes - println(io, " ", k, ":", _pad_spaces(k, word_pad), round(v, digits = 4)) - end - end - return nothing -end diff --git a/src/io/show/FluxVariabilitySummary.jl b/src/io/show/FluxVariabilitySummary.jl deleted file mode 100644 index 4862ea776..000000000 --- a/src/io/show/FluxVariabilitySummary.jl +++ /dev/null @@ -1,68 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", flux_res::FluxVariabilitySummary) - - longest_biomass_len = - maximum(length(k) for k in keys(flux_res.biomass_fluxes); init = 0) - longest_exchange_len = - maximum(length(k) for k in keys(flux_res.exchange_fluxes); init = 0) - word_pad_len = max(longest_biomass_len, longest_exchange_len) - - longest_biomass_flux_len = maximum( - length(string(round(v[1], digits = 4))) for v in values(flux_res.biomass_fluxes); - init = 0, - ) - longest_exchange_flux_len = max( - maximum( - length(string(round(v[1], digits = 4))) for - v in values(flux_res.exchange_fluxes); - init = 0, - ), - length("Lower bound "), - ) - number_pad_len = max(longest_biomass_flux_len, longest_exchange_flux_len) - - println( - io, - "Biomass", - _pad_spaces(length("Biomass"), word_pad_len + 3), - "Lower bound ", - _pad_spaces("Lower bound ", number_pad_len), - "Upper bound", - ) - for (k, v) in flux_res.biomass_fluxes - lb = isnothing(v[1]) ? " " : string(round(v[1], digits = 4)) - ub = isnothing(v[2]) ? " " : string(round(v[1], digits = 4)) - println( - io, - " ", - k, - ":", - _pad_spaces(k, word_pad_len), - lb, - _pad_spaces(lb, number_pad_len), - ub, - ) - end - - println(io, "Exchange") - ex_ids = collect(keys(flux_res.exchange_fluxes)) - vs = [ - abs(flux_res.exchange_fluxes[k][1]) + abs(flux_res.exchange_fluxes[k][2]) for - k in ex_ids - ] - idxs = sortperm(vs, rev = true) - for k in ex_ids[idxs] - v = flux_res.exchange_fluxes[k] - lb = isnothing(v[1]) ? " " : string(round(v[1], digits = 4)) - ub = isnothing(v[2]) ? " " : string(round(v[2], digits = 4)) - println( - io, - " ", - k, - ":", - _pad_spaces(k, word_pad_len), - lb, - _pad_spaces(lb, number_pad_len), - ub, - ) - end -end diff --git a/src/io/show/ModelWithResult.jl b/src/io/show/ModelWithResult.jl deleted file mode 100644 index bf5c0cf43..000000000 --- a/src/io/show/ModelWithResult.jl +++ /dev/null @@ -1,3 +0,0 @@ -function Base.show(io::Base.IO, ::MIME"text/plain", res::ModelWithResult{T}) where {T} - println(io, "ModelWithResult{$T}($(typeof(res.model)), ...)") -end diff --git a/src/io/show/Serialized.jl b/src/io/show/Serialized.jl deleted file mode 100644 index cf83665cc..000000000 --- a/src/io/show/Serialized.jl +++ /dev/null @@ -1,12 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Show the [`Serialized`](@ref) model without unnecessarily loading it. -""" -function Base.show(io::Base.IO, ::MIME"text/plain", m::Serialized{M}) where {M} - print( - io, - "Serialized{$M} saved in \"$(m.filename)\" ($(isnothing(m.m) ? "not loaded" : "loaded"))", - ) -end diff --git a/src/io/show/print_kwdef.jl b/src/io/show/print_kwdef.jl deleted file mode 100644 index 51a730f44..000000000 --- a/src/io/show/print_kwdef.jl +++ /dev/null @@ -1,69 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Prints out a display-style (multi-line etc.) version of the structure `x` -defined using `Base.@kwdef` to the `io` stream. Falls back to -[`print_kwdef`](@ref) if the IO context has the `:compact` flag set. -""" -function display_kwdef(io::Base.IO, x::T) where {T} - if get(io, :compact, false) - print_kwdef(io, x) - else - print(io, T) - println(io, "(") - for field in fieldnames(T) - print(io, " ") - print(io, field) - print(io, " = ") - show(IOContext(io, :compact => true), getfield(x, field)) - println(io, ",") - end - print(io, ")") - end - nothing -end - -""" -$(TYPEDSIGNATURES) - -Prints out an inline-style (single-line) version of the structure `x` defined -using `Base.@kwdef` to the `io` stream. -""" -function print_kwdef(io::Base.IO, x::T) where {T} - print(io, T) - print(io, "(") - first = true - for field in fieldnames(T) - if first - first = false - else - print(io, ", ") - end - print(io, field) - print(io, "=") - show(IOContext(io, :compact => true), getfield(x, field)) - end - print(io, ")") - nothing -end - -""" -$(TYPEDSIGNATURES) - -Creates overloads of `Base.show` and `Base.print` for a given type. -""" -macro kwdef_printing(t) - :( - begin - Base.show(io::Base.IO, ::MIME"text/plain", x::$t) = display_kwdef(io, x) - Base.show(io::Base.IO, x::$t) = print_kwdef(io, x) - Base.print(io::Base.IO, x::$t) = print_kwdef(io, x) - end - ) -end - -@kwdef_printing Reaction -@kwdef_printing Metabolite -@kwdef_printing Gene -@kwdef_printing Isozyme diff --git a/src/log.jl b/src/log.jl deleted file mode 100644 index 85a801ca3..000000000 --- a/src/log.jl +++ /dev/null @@ -1,94 +0,0 @@ - -""" - module Log - -Logging helpers for COBREXA. - -# Exports -$(EXPORTS) -""" -module Log -using ..ModuleTools -@dse - -""" - module Internal - -Internal helpers for logging. - -# Exports -$(EXPORTS) -""" -module Internal - using ..ModuleTools - @dse - """ - $(TYPEDSIGNATURES) - - This creates a group of functions that allow masking out topic-related logging - actions. A call that goes as follows: - - @make_logging_tag XYZ - - creates the following tools: - - - global variable `XYZ_log_enabled` defaulted to false - - function `log_XYZ` that can be called to turn the logging on/off - - a masking macro `@XYZ_log` that can be prepended to commands that should - only happen if the logging of tag XYZ is enabled. - - The masking macro is then used as follows: - - @XYZ_log @info "This is the extra verbose information you wanted!" a b c - - The user can direct logging with these: - - log_XYZ() - log_XYZ(false) - - `doc` should be a name of the stuff that is being printed if the corresponding - log_XYZ() is enabled -- it is used to create a friendly documentation for the - logging switch. In this case it could say `"X, Y and Z-related messages"`. - """ - macro make_logging_tag(sym::Symbol, doc::String) - enable_flag = Symbol(sym, :_log_enabled) - enable_fun = Symbol(:log_, sym) - log_macro = Symbol(sym, :_log) - # esc() is necessary here because the internal macro processing would - # otherwise bind the variables incorrectly. - esc(:( - begin - $enable_flag = false - - """ - $(string($enable_fun))(enable::Bool=true) - - Enable (default) or disable (by passing `false`) output of $($doc). - """ - $enable_fun(x::Bool = true) = begin - global $enable_flag = x - end - - macro $log_macro(x) - $enable_flag ? x : nothing - end - end - )) - end - - @make_logging_tag models "model-related messages" - @make_logging_tag io "messages and warnings from model input/output" - @make_logging_tag perf "performance-related tracing information" - - @export_locals -end #Internal - -import .Internal: log_models, log_io, log_perf - -#TODO can this be exported automatically? -export log_models -export log_io -export log_perf - -@export_locals -end diff --git a/src/macros.jl b/src/macros.jl deleted file mode 100644 index ab20cae23..000000000 --- a/src/macros.jl +++ /dev/null @@ -1,17 +0,0 @@ - -""" - module Macros - -Internal COBREXA macros. - -# Exports -$(EXPORTS) -""" -module Macros -using ..ModuleTools -@dse - -@inc_dir macros - -@export_locals -end diff --git a/src/moduletools.jl b/src/moduletools.jl deleted file mode 100644 index 8717eec9d..000000000 --- a/src/moduletools.jl +++ /dev/null @@ -1,60 +0,0 @@ -""" - module ModuleTools - -Internal helpers for simplifying the work with COBREXA submodules. - -# Exports -$(EXPORTS) -""" -module ModuleTools -macro inc(path...) - esc(:(include(joinpath(@__DIR__, $(joinpath(String.(path)...) * ".jl"))))) -end - -macro inc_dir(path...) - dir = joinpath(@__DIR__, String.(path)...) - files = filter(endswith(".jl"), readdir(dir; join = true)) - esc(Expr(:block, (:(include($f)) for f in files)...)) -end - -macro dse() - :(using DocStringExtensions) -end -@dse - -macro inject(mod, code) - esc(:(Base.eval($mod, $(Expr(:quote, code))))) -end - -# export everything from the local namespace that seems exportable -# (inspired by JuMP.jl, thanks!) -macro export_locals() - quote - for sym in names(@__MODULE__; all = true, imported = true) - sym in [Symbol(@__MODULE__), :eval, :include] && continue - startswith(string(sym), ['_', '#']) && continue - sym == :Internal && continue - @eval export $(Expr(:$, :sym)) - end - end -end - -# re-export all imported things -# (many thanks to Reexport.jl for inspiration here!) -macro reexport(mods...) - importexpr = Expr(:import, Expr(:., :., :., mods...)) - modulename = foldl((l, r) -> Expr(:., l, QuoteNode(r)), mods) - esc(quote - $importexpr - for sym in names($modulename) - Base.isexported($modulename, sym) || continue - typeof($(Expr(:., modulename, :sym))) == Module && continue - sym in [:eval, :include] && continue - @eval const $(Expr(:$, :sym)) = ($modulename).$(Expr(:$, :sym)) - @eval export $(Expr(:$, :sym)) - end - end) -end - -@export_locals -end diff --git a/src/reconstruction.jl b/src/reconstruction.jl deleted file mode 100644 index 33ce1b55a..000000000 --- a/src/reconstruction.jl +++ /dev/null @@ -1,64 +0,0 @@ - -""" - module Reconstruction - -Reconstruction functionality of COBREXA; mainly functions that construct or -modify the model contents. - -# Exports -$(EXPORTS) -""" -module Reconstruction -using ..ModuleTools -@dse - -using ..Accessors -using ..Wrappers -using ..Internal: - constants, - check_arg_keys_exists, - check_arg_keys_missing, - check_has_isozymes, - check_has_biomass_rxn_id, - check_biomass_rxn_has_isozymes, - check_has_virtualribosome, - check_has_biomass_rxn_biomas_metabolite, - check_environmental_ids, - check_abundances -using ..Internal.Macros -using ..Log.Internal -using ..Types - -using SparseArrays, OrderedCollections -using MacroTools - -@inc_dir reconstruction - -""" - module Pipes - -Functions that create model variants, typically for efficient use in -[`screen`](@ref) and similar functions. - -# Exports -$(EXPORTS) -""" -module Pipes - using ..ModuleTools - @dse -end - -@export_locals -end - -# this needs to import from Reconstruction -@inject Reconstruction.Pipes begin - using ..Reconstruction - using ..Types - using ..Wrappers - @inc_dir reconstruction pipes - - @export_locals -end - -@inject Analysis import ..Reconstruction diff --git a/src/solver.jl b/src/solver.jl index bf0ae31c2..8ec037c37 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -1,219 +1,76 @@ -""" - module Solver - -Interface of COBREXA to JuMP solvers; mainly recreation of the -`AbstractMetabolicModel`s as JuMP optimization models, and retrieval of solved -values. - -# Exports -$(EXPORTS) -""" -module Solver -using ..ModuleTools -@dse - -using ..Types -using ..Accessors -using JuMP +import JuMP as J """ $(TYPEDSIGNATURES) -Convert `AbstractMetabolicModel`s to a JuMP model, place objectives and the equality -constraint. - -Here coupling means inequality constraints coupling multiple variables together. +Construct a JuMP `Model` that describes the precise constraint system into the +JuMP `Model` created for solving in `optimizer`, with a given optional +`objective` and optimization `sense`. """ -function make_optimization_model( - model::AbstractMetabolicModel, - optimizer; - sense = MAX_SENSE, +function J.Model( + constraints::C.ConstraintTree; + objective::Maybe{C.Value} = nothing, + optimizer, + sense = J.MAX_SENSE ) - - precache!(model) - - m, n = size(stoichiometry(model)) - xl, xu = bounds(model) - - optimization_model = Model(optimizer) - @variable(optimization_model, x[1:n]) - let obj = objective(model) - if obj isa AbstractVector - @objective(optimization_model, sense, obj' * x) - else - @objective(optimization_model, sense, x' * obj * [x; 1]) + model = J.Model(optimizer) + J.@variable(model, x[1:C.var_count(cs)]) + isnothing(objective) || J.@objective(model, sense, C.value_product(objective, x)) + function add_constraint(c::C.Constraint) + if c.bound isa Float64 + J.@constraint(model, C.value_product(c.value, x) == c.bound) + elseif c.bound isa C.IntervalBound + val = C.value_product(c.value, x) + isinf(c.bound[1]) || J.@constraint(model, val >= c.bound[1]) + isinf(c.bound[2]) || J.@constraint(model, val <= c.bound[2]) end end - @constraint(optimization_model, mb, stoichiometry(model) * x .== balance(model)) # mass balance - @constraint(optimization_model, lbs, xl .<= x) # lower bounds - @constraint(optimization_model, ubs, x .<= xu) # upper bounds - - C = coupling(model) # empty if no coupling - isempty(C) || begin - cl, cu = coupling_bounds(model) - @constraint(optimization_model, c_lbs, cl .<= C * x) # coupling lower bounds - @constraint(optimization_model, c_ubs, C * x .<= cu) # coupling upper bounds + function add_constraint(c::C.ConstraintTree) + add_constraint.(values(c)) end - - return optimization_model - #TODO what about ModelWithResult right from this point? ;D + add_constraint(cs) + model end """ $(TYPEDSIGNATURES) -Return `true` if `opt_model` solved successfully (solution is optimal or locally -optimal). Return `false` if any other termination status is reached. -Termination status is defined in the documentation of `JuMP`. +Convenience re-export of `optimize!` from JuMP. """ -is_solved(opt_model::Model) = - termination_status(opt_model) in [MOI.OPTIMAL, MOI.LOCALLY_SOLVED] +const optimize! = J.optimize! """ $(TYPEDSIGNATURES) -Variant of is_solved that works with [`ModelWithResult`](@ref). +`true` if `opt_model` solved successfully (solution is optimal or +locally optimal). `false` if any other termination status is reached. """ -is_solved(r::ModelWithResult{<:Model}) = is_solved(r.result) +is_solved(opt_model::J.Model) = + J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] """ $(TYPEDSIGNATURES) -Shortcut for running JuMP `optimize!` on a model and returning the objective -value, if solved. +The optimized objective value of a JuMP model, if solved. """ -function optimize_objective(opt_model)::Maybe{Float64} - optimize!(opt_model) - solved_objective_value(opt_model) -end +optimized_objective_value(opt_model::J.Model)::Maybe{Float64} = + is_solved(opt_model) ? J.objective_value(opt_model) : nothing """ $(TYPEDSIGNATURES) -Helper function to set the bounds of a variable in the model. Internally calls -`set_normalized_rhs` from JuMP. If the bounds are set to `nothing`, they will -not be changed. +The optimized variable assignment of a JuMP model, if solved. """ -function set_optmodel_bound!( - vidx, - opt_model; - lower_bound::Maybe{Real} = nothing, - upper_bound::Maybe{Real} = nothing, -) - isnothing(lower_bound) || set_normalized_rhs(opt_model[:lbs][vidx], -lower_bound) - isnothing(upper_bound) || set_normalized_rhs(opt_model[:ubs][vidx], upper_bound) -end +optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = + is_solved(opt_model) ? J.value.(model[:x]) """ $(TYPEDSIGNATURES) -Returns the current objective value of a model, if solved. -""" -solved_objective_value(opt_model::Model)::Maybe{Float64} = - is_solved(opt_model) ? objective_value(opt_model) : nothing - +Convenience overload for making solution trees out of JuMP models """ -$(TYPEDSIGNATURES) - -Pipeable variant of [`solved_objective_value`](@ref). - -# Example -``` -flux_balance_analysis(model, ...) |> solved_objective_value -``` -""" -solved_objective_value(x::ModelWithResult{<:Model}) = solved_objective_value(x.result) - -""" -$(TYPEDSIGNATURES) - -Return a vector of all variable values from the solved model, in the same order -given by [`variables`](@ref). - -# Example -``` -flux_balance_analysis(model, ...) |> values_vec -``` -""" -function values_vec(res::ModelWithResult{<:Model}) - is_solved(res.result) ? value.(res.result[:x]) : nothing -end - -""" -$(TYPEDSIGNATURES) - -Return a vector of all semantic variable values in the model, in the order -given by the corresponding semantics. - -# Example -``` -values_vec(:reaction, flux_balance_analysis(model, ...)) -``` -""" -function values_vec(semantics::Symbol, res::ModelWithResult{<:Model}) - (_, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) - is_solved(res.result) ? sem_varmtx(res.model)' * value.(res.result[:x]) : nothing -end - -""" -$(TYPEDSIGNATURES) - -A pipeable variant of [`values_vec`](@ref). - -# Example -``` -flux_balance_analysis(model, ...) |> values_vec(:reaction) -``` -""" -values_vec(semantics::Symbol) = - (res::ModelWithResult{<:Model}) -> values_vec(semantics, res) - -""" -$(TYPEDSIGNATURES) - -Return a dictionary of all variable values from the solved model mapped -to their IDs. - -# Example -``` -flux_balance_analysis(model, ...) |> values_dict -``` -""" -function values_dict(res::ModelWithResult{<:Model}) - is_solved(res.result) ? Dict(variables(res.model) .=> value.(res.result[:x])) : nothing -end - -""" -$(TYPEDSIGNATURES) - -From the optimized model, returns a dictionary mapping semantic IDs to their -solved values for the selected `semantics`. If the model did not solve, returns -`nothing`. - -# Example -``` -values_dict(:reaction, flux_balance_analysis(model, ...)) -``` -""" -function values_dict(semantics::Symbol, res::ModelWithResult{<:Model}) - (ids, _, _, sem_varmtx) = Accessors.Internal.semantics(semantics) - is_solved(res.result) ? - Dict(ids(res.model) .=> sem_varmtx(res.model)' * value.(res.result[:x])) : nothing -end - -""" -$(TYPEDSIGNATURES) - -A pipeable variant of [`values_dict`](@ref). - -# Example -``` -flux_balance_analysis(model, ...) |> values_dict(:reaction) -``` -""" -values_dict(semantics::Symbol) = - (res::ModelWithResult{<:Model}) -> values_dict(semantics, res) - -@export_locals +C.solution_tree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = +let vars = optimized_variable_assignment(opt_model) + isnothing(vars) ? nothing : C.solution_tree(c, vars) end diff --git a/src/types.jl b/src/types.jl index 9e1244b06..281c4278a 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,99 +1,7 @@ """ - module Types - -Module with all types, mainly the model types and various typed model contents. - -# Exports -$(EXPORTS) -""" -module Types -using ..ModuleTools -@dse -using ..Internal - -using HDF5 -using JSON -using LinearAlgebra -using MAT -using OrderedCollections -using SBML -using Serialization -using SparseArrays - -@inc_dir types abstract -@export_locals -end # module Types - -""" - module Accessors - -Functions that gather data from various model types in a standardized form. -Overload these if you want COBREXA to work with your own module types. - -# Exports -$(EXPORTS) -""" -module Accessors -using ..ModuleTools -@dse -using ..Types -using ..Internal.Macros - -using SparseArrays + Maybe{X} +Type of optional values. """ - module Internal - -Internal helpers for accessors. - -# Exports -$(EXPORTS) -""" -module Internal - using ..ModuleTools - @dse - import ...Types - import ..Accessors - using SparseArrays - @inc_dir types accessors bits - @export_locals -end - -using .Internal - -@inc_dir types accessors -@export_locals -end # module Accessors - -# the modules depend on each other so we have to inject the stuff like this -@inject Types begin - using ..Accessors - using ..Internal.Macros - using ..Log.Internal: @io_log - - @inc_dir types - @inc_dir types models - @inc_dir types wrappers # TODO find a home for EqualGrowthCommunityModel - - @export_locals -end - -@inject Types.Internal begin - # TODO where is this declared? - using ...Types - using ..Accessors - using ..Log.Internal: @models_log - - using SBML - using SparseArrays - import PikaParser as PP - using OrderedCollections - - @inc_dir types misc - @export_locals -end - -@inject Types begin - using .Internal -end +const Maybe{X} = Union{Nothing,X} diff --git a/src/utils.jl b/src/utils.jl deleted file mode 100644 index 7db09e936..000000000 --- a/src/utils.jl +++ /dev/null @@ -1,40 +0,0 @@ - -# TODO: It might be much much more useful to actually sort out the utils into -# individual namespaces. -""" - module Utils - -Mostly contains various COBREXA functions and helpers, including internal ones. - -# Exports - -$(EXPORTS) -""" -module Utils -using ..ModuleTools -@dse - -using ..Types -using ..Accessors -using ..Wrappers -using ..Internal.Identifiers -using ..Internal: constants -using ..Internal.Macros -using ..Solver - -using HDF5 -using JuMP -using LinearAlgebra -using OrderedCollections -using Serialization -using SparseArrays - -@inc_dir utils - -@export_locals -end - -@inject Types import ...Utils -@inject Analysis using ...Utils: objective_bounds -@inject Analysis.Modifications using ...Utils: is_boundary -@inject Wrappers using ..Utils diff --git a/src/wrappers.jl b/src/wrappers.jl deleted file mode 100644 index beed94f45..000000000 --- a/src/wrappers.jl +++ /dev/null @@ -1,45 +0,0 @@ - -""" - module Wrappers - -All "layered" modifications of the models and their types are grouped in this module. - -# Exports -$(EXPORTS) -""" -module Wrappers -using ..ModuleTools -@dse - -using ..Types -using ..Accessors -using ..Internal.Macros -using ..Internal: constants - -using LinearAlgebra -using SparseArrays - -module Internal - using ..ModuleTools - @dse - using ..Wrappers - using ..Types - using ..Accessors - - using SparseArrays - - @inc_dir wrappers bits - @export_locals -end # module Internal - -using .Internal - -@inc_dir wrappers - -@export_locals -end # module Wrappers - -@inject Wrappers.Internal begin - @inc_dir wrappers misc - @export_locals # again -end From 9987ad5ead8bc3170775369fb5353ccecba1e453 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 14 Sep 2023 23:55:46 +0200 Subject: [PATCH 329/531] more stuff moved to other packages --- src/COBREXA.jl | 2 +- src/builders/core.jl | 29 + src/macros/change_bounds.jl | 69 -- src/macros/is_sbo_xxx.jl | 50 -- src/macros/is_sbo_xxx_reaction.jl | 51 -- src/macros/model_wrapper.jl | 72 -- src/macros/remove_item.jl | 43 - src/macros/serialized.jl | 27 - src/reconstruction/MatrixCoupling.jl | 384 --------- src/reconstruction/MatrixModel.jl | 453 ----------- src/reconstruction/ObjectModel.jl | 745 ------------------ src/reconstruction/SerializedModel.jl | 20 - src/reconstruction/pipes/community.jl | 33 - src/reconstruction/pipes/enzymes.jl | 21 - src/reconstruction/pipes/generic.jl | 198 ----- src/reconstruction/pipes/thermodynamic.jl | 9 - src/types/FluxSummary.jl | 29 - src/types/FluxVariabilitySummary.jl | 24 - src/types/Gene.jl | 26 - src/types/Isozyme.jl | 25 - src/types/Metabolite.jl | 25 - src/types/Reaction.jl | 83 -- src/types/ReactionStatus.jl | 13 - src/types/Result.jl | 43 - src/types/abstract/AbstractMetabolicModel.jl | 69 -- src/types/abstract/Maybe.jl | 7 - src/types/accessors/AbstractMetabolicModel.jl | 486 ------------ src/types/accessors/ModelWrapper.jl | 27 - src/types/accessors/bits/missing_impl.jl | 2 - src/types/accessors/bits/semantics.jl | 241 ------ src/types/misc/CommunityModel.jl | 136 ---- src/types/misc/ObjectModel.jl | 69 -- src/types/misc/chemical_formulas.jl | 26 - src/types/misc/gene_associations.jl | 211 ----- src/types/misc/maybe.jl | 18 - src/types/models/CommunityModel.jl | 282 ------- src/types/models/HDF5Model.jl | 77 -- src/types/models/JSONModel.jl | 265 ------- src/types/models/MATModel.jl | 193 ----- src/types/models/MatrixModel.jl | 111 --- src/types/models/ObjectModel.jl | 249 ------ src/types/models/SBMLModel.jl | 371 --------- src/types/models/Serialized.jl | 31 - .../wrappers/EqualGrowthCommunityModel.jl | 111 --- src/wrappers/MatrixCoupling.jl | 78 -- src/wrappers/misc/MatrixModel.jl | 20 - 46 files changed, 30 insertions(+), 5524 deletions(-) create mode 100644 src/builders/core.jl delete mode 100644 src/macros/change_bounds.jl delete mode 100644 src/macros/is_sbo_xxx.jl delete mode 100644 src/macros/is_sbo_xxx_reaction.jl delete mode 100644 src/macros/model_wrapper.jl delete mode 100644 src/macros/remove_item.jl delete mode 100644 src/macros/serialized.jl delete mode 100644 src/reconstruction/MatrixCoupling.jl delete mode 100644 src/reconstruction/MatrixModel.jl delete mode 100644 src/reconstruction/ObjectModel.jl delete mode 100644 src/reconstruction/SerializedModel.jl delete mode 100644 src/reconstruction/pipes/community.jl delete mode 100644 src/reconstruction/pipes/enzymes.jl delete mode 100644 src/reconstruction/pipes/generic.jl delete mode 100644 src/reconstruction/pipes/thermodynamic.jl delete mode 100644 src/types/FluxSummary.jl delete mode 100644 src/types/FluxVariabilitySummary.jl delete mode 100644 src/types/Gene.jl delete mode 100644 src/types/Isozyme.jl delete mode 100644 src/types/Metabolite.jl delete mode 100644 src/types/Reaction.jl delete mode 100644 src/types/ReactionStatus.jl delete mode 100644 src/types/Result.jl delete mode 100644 src/types/abstract/AbstractMetabolicModel.jl delete mode 100644 src/types/abstract/Maybe.jl delete mode 100644 src/types/accessors/AbstractMetabolicModel.jl delete mode 100644 src/types/accessors/ModelWrapper.jl delete mode 100644 src/types/accessors/bits/missing_impl.jl delete mode 100644 src/types/accessors/bits/semantics.jl delete mode 100644 src/types/misc/CommunityModel.jl delete mode 100644 src/types/misc/ObjectModel.jl delete mode 100644 src/types/misc/chemical_formulas.jl delete mode 100644 src/types/misc/gene_associations.jl delete mode 100644 src/types/misc/maybe.jl delete mode 100644 src/types/models/CommunityModel.jl delete mode 100644 src/types/models/HDF5Model.jl delete mode 100644 src/types/models/JSONModel.jl delete mode 100644 src/types/models/MATModel.jl delete mode 100644 src/types/models/MatrixModel.jl delete mode 100644 src/types/models/ObjectModel.jl delete mode 100644 src/types/models/SBMLModel.jl delete mode 100644 src/types/models/Serialized.jl delete mode 100644 src/types/wrappers/EqualGrowthCommunityModel.jl delete mode 100644 src/wrappers/MatrixCoupling.jl delete mode 100644 src/wrappers/misc/MatrixModel.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 0d6f81702..cacd9f55e 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -6,9 +6,9 @@ module COBREXA using DocStringExtensions import ConstraintTrees as C -import Distributed as D include("types.jl") include("solver.jl") +include("builders/core.jl") #TODO more stuff end # module COBREXA diff --git a/src/builders/core.jl b/src/builders/core.jl new file mode 100644 index 000000000..9c71446b2 --- /dev/null +++ b/src/builders/core.jl @@ -0,0 +1,29 @@ + +import FBCModelInterface as F +import SparseArrays: sparse + +""" +$(TYPEDSIGNATURES) + +Make a constraint tree that models the content of the given instance of +`AbstractFBCModel`. +""" +C.make_constraint_tree(model::F.AbstractFBCModel) = let + rxns = Symbol.(F.reactions(model)), + mets = Symbol.(F.metabolites(model)), + lbs, ubs = F.bounds(model), + stoi = F.stoichiometry(model), + bal = F.balance(model), + obj = F.objective(model) + + # TODO coupling + :fluxes^C.allocate_variables( + keys=rxns, + bounds=zip(lbs, ubs), + ) * + :balance^C.make_constraint_tree( + m => Constraint(value=Value(sparse(row)), bound=b) for + (m, row, b) in zip(mets, eachrow(stoi), bals) + ) * + :objective^C.Constraint(value=C.Value(sparse(obj))) +end diff --git a/src/macros/change_bounds.jl b/src/macros/change_bounds.jl deleted file mode 100644 index f9e3fe71b..000000000 --- a/src/macros/change_bounds.jl +++ /dev/null @@ -1,69 +0,0 @@ -""" - @_change_bounds_fn ModelType IdxType [plural] [inplace] begin ... end - -A helper for creating simple bounds-changing function similar to -[`change_bounds`](@ref). -""" -macro _change_bounds_fn(model_type, idx_type, args...) - body = last(args) - typeof(body) == Expr || throw(DomainError(body, "missing function body")) - plural = :plural in args - plural_s = plural ? "s" : "" - inplace = :inplace in args - fname = Symbol(:change_bound, plural_s, inplace ? "!" : "") - idx_var = Symbol( - :rxn, - idx_type == :Int ? "_idx" : - idx_type == :String ? "_id" : - throw(DomainError(idx_type, "unsupported index type for change_bound macro")), - plural_s, - ) - lower_bound_s = Symbol(:lower_bound, plural_s) - upper_bound_s = Symbol(:upper_bound, plural_s) - - example_idx = - plural ? (idx_type == :Int ? [123, 234] : ["ReactionA", "ReactionC"]) : - (idx_type == :Int ? 123 : "\"ReactionB\"") #= unquoting is hard =# - example_val = plural ? [4.2, 100.1] : 42.3 - missing_default = plural ? :((nothing for _ in $idx_var)) : nothing - - bound_type = Float64 - if plural - idx_type = AbstractVector{eval(idx_type)} - bound_type = AbstractVector{bound_type} - end - - docstring = """ - $fname( - model::$model_type, - $idx_var::$idx_type; - $lower_bound_s =$missing_default, - $upper_bound_s =$missing_default, - ) - - Change the specified reaction flux bound$(plural_s) in the model - $(inplace ? "in-place" : "and return the modified model"). - - # Example - ``` - $(inplace ? "new_model = " : "")$fname(model, $example_idx, lower_bound =$(-0.5 .* example_val), upper_bound =$example_val) - ``` - """ - - esc( - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::$model_type, - $idx_var::$idx_type; - $lower_bound_s = $missing_default, - $upper_bound_s = $missing_default, - ) = $body - ), - ), - ) -end diff --git a/src/macros/is_sbo_xxx.jl b/src/macros/is_sbo_xxx.jl deleted file mode 100644 index 547e5b6c3..000000000 --- a/src/macros/is_sbo_xxx.jl +++ /dev/null @@ -1,50 +0,0 @@ - -""" -@_is_sbo_fn(anno_id, identifier) - -A helper for creating functions like `is_sbo_reaction`, `is_sbo_gene`, `is_sbo_metabolite`. -""" -macro _is_sbo_fn(anno_id, identifiers) - - fname = Symbol(:is_sbo_, anno_id) - annofunc = Symbol(anno_id, :_annotations) - accessorfunc = Symbol(anno_id, :s) - body = quote - begin - id in $accessorfunc(model) || return false - anno = $annofunc(model, id) - for key in sbo_annotation_keys - if haskey(anno, key) - any(in.($identifiers, Ref(anno[key]))) && return true - end - end - return false - end - end - - docstring = """ - $fname( - model::AbstractMetabolicModel, - id::String; - sbo_annotation_keys = ["sbo", "SBO"], - ) - - Check if a $(anno_id) can be identified using the `sbo_annotation_keys`. Returns - false if no hits or if no keys are found. - """ - esc( - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::AbstractMetabolicModel, - id::String; - sbo_annotation_keys = ["sbo", "SBO"], - ) = $body - ), - ), - ) -end diff --git a/src/macros/is_sbo_xxx_reaction.jl b/src/macros/is_sbo_xxx_reaction.jl deleted file mode 100644 index bbce4b85e..000000000 --- a/src/macros/is_sbo_xxx_reaction.jl +++ /dev/null @@ -1,51 +0,0 @@ - -""" -@_is_sbo_reaction_fn(anno_id, identifier) - -A helper for creating functions like `is_sbo_exchange_reaction`. -""" -macro _is_sbo_reaction_fn(anno_id, identifiers) - - fname = Symbol(:is_sbo_, anno_id, :_reaction) - grammar = any(startswith.(anno_id, ["a", "e", "i", "o", "u"])) ? "an" : "a" - - body = quote - begin - reaction_id in reactions(model) || return false - anno = reaction_annotations(model, reaction_id) - for key in sbo_annotation_keys - if haskey(anno, key) - any(in.($identifiers, Ref(anno[key]))) && return true - end - end - return false - end - end - - docstring = """ - $fname( - model::AbstractMetabolicModel, - reaction_id::String; - sbo_annotation_keys = ["sbo", "SBO"], - ) - - Check if a reaction is annotated as $(grammar) SBO annotated $(anno_id) - reaction. In the reaction annotations, use the keys in `sbo_annotation_keys` - to look for entries. Returns false if no hits or if no keys are found. - """ - esc( - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fname( - model::AbstractMetabolicModel, - reaction_id::String; - sbo_annotation_keys = ["sbo", "SBO"], - ) = $body - ), - ), - ) -end diff --git a/src/macros/model_wrapper.jl b/src/macros/model_wrapper.jl deleted file mode 100644 index ca082345c..000000000 --- a/src/macros/model_wrapper.jl +++ /dev/null @@ -1,72 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -A helper backend for [`@inherit_model_methods`](@ref) and -[`@inherit_model_methods_fn`](@ref). -""" -function _inherit_model_methods_impl( - source, - mtype::Symbol, - arglist, - access, - fwdlist, - fns..., -) - Expr( - :block, - ( - begin - header = Expr(:call, fn, :(model::$mtype), arglist.args...) - call = Expr(:call, fn, access(:model), fwdlist.args...) - esc( - Expr( - :macrocall, - Symbol("@doc"), - source, - """ - $header - - Evaluates [`$fn`](@ref) on the model contained in $mtype. - """, - Expr(:(=), header, Expr(:block, source, call)), - ), - ) - end for fn in fns - )..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Generates trivial accessor functions listed in `fns` for a model that is -wrapped in type `mtype` as field `member`. -""" -macro inherit_model_methods(mtype::Symbol, arglist, member::Symbol, fwdlist, fns...) - _inherit_model_methods_impl( - __source__, - mtype, - arglist, - sym -> :($sym.$member), - fwdlist, - fns..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A more generic version of [`@inherit_model_methods`](@ref) that accesses the -"inner" model using an accessor function name. -""" -macro inherit_model_methods_fn(mtype::Symbol, arglist, accessor, fwdlist, fns...) - _inherit_model_methods_impl( - __source__, - mtype, - arglist, - sym -> :($accessor($sym)), - fwdlist, - fns..., - ) -end diff --git a/src/macros/remove_item.jl b/src/macros/remove_item.jl deleted file mode 100644 index 0f000a1a0..000000000 --- a/src/macros/remove_item.jl +++ /dev/null @@ -1,43 +0,0 @@ - -""" - @ _remove_fn objname ModelType IndexType [plural] [inplace] begin ... end - -A helper for creating functions that follow the `remove_objname` template, such -as [`remove_metabolites`](@ref) and [`remove_reaction`](@ref). -""" -macro _remove_fn(objname, model_type, idx_type, args...) - body = last(args) - typeof(body) == Expr || throw(DomainError(body, "missing function body")) - plural = :plural in args - plural_s = plural ? "s" : "" - inplace = :inplace in args - fname = Symbol(:remove_, objname, plural_s, inplace ? "!" : "") - idx_var = Symbol( - objname, - idx_type == :Int ? "_idx" : - idx_type == :String ? "_id" : - throw(DomainError(idx_type, "unsupported index type for _remove_fn macro")), - plural_s, - ) - - if plural - idx_type = AbstractVector{eval(idx_type)} - end - - docstring = """ - $fname(model::$model_type, $idx_var::$idx_type) - - Remove $objname$plural_s from the model of type `$model_type` - $(inplace ? "in-place" : "and return the modified model"). - """ - - esc( - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :($fname(model::$model_type, $idx_var::$idx_type) = $body), - ), - ) -end diff --git a/src/macros/serialized.jl b/src/macros/serialized.jl deleted file mode 100644 index 8b9aac0bf..000000000 --- a/src/macros/serialized.jl +++ /dev/null @@ -1,27 +0,0 @@ -""" - @_serialized_change_unwrap function - -Creates a simple wrapper structure that calls the `function` transparently on -the internal precached model. The internal type is returned (otherwise this -would break the consistency of serialization). -""" -macro _serialized_change_unwrap(fn::Symbol) - docstring = """ - $fn(model::Serialized, ...) - - Calls [`$fn`](@ref) of the internal serialized model type. - Returns the modified un-serialized model. - """ - esc( - Expr( - :macrocall, - Symbol("@doc"), - __source__, - docstring, - :( - $fn(model::Serialized, args...; kwargs...) = - $fn(unwrap_serialized(model), args...; kwargs...) - ), - ), - ) -end diff --git a/src/reconstruction/MatrixCoupling.jl b/src/reconstruction/MatrixCoupling.jl deleted file mode 100644 index 272d74141..000000000 --- a/src/reconstruction/MatrixCoupling.jl +++ /dev/null @@ -1,384 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add reaction(s) to a `MatrixModelWithCoupling` model `m`. - -""" -function add_reactions( - m::MatrixModelWithCoupling, - s::V1, - b::V2, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat; - check_consistency = false, -) where {V1<:VecType,V2<:VecType} - new_lm = add_reactions(m.lm, s, b, c, xl, xu, check_consistency = check_consistency) - return MatrixModelWithCoupling( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -""" -function add_reactions( - m::MatrixModelWithCoupling, - s::V1, - b::V2, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat, - rxn::String, - mets::K; - check_consistency = false, -) where {V1<:VecType,V2<:VecType,K<:StringVecType} - new_lm = add_reactions( - m.lm, - s, - b, - c, - xl, - xu, - rxn, - mets, - check_consistency = check_consistency, - ) - return MatrixModelWithCoupling( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::MatrixModelWithCoupling, - Sp::M, - b::V, - c::V, - xl::V, - xu::V; - check_consistency = false, -) where {M<:MatType,V<:VecType} - new_lm = add_reactions(m.lm, Sp, b, c, xl, xu, check_consistency = check_consistency) - return MatrixModelWithCoupling( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add all reactions from `m2` to `m1`. -""" -function add_reactions( - m1::MatrixModelWithCoupling, - m2::MatrixModel; - check_consistency = false, -) - new_lm = add_reactions(m1.lm, m2, check_consistency = check_consistency) - return MatrixModelWithCoupling( - new_lm, - hcat(m1.C, spzeros(size(m1.C, 1), n_variables(new_lm) - n_variables(m1.lm))), - m1.cl, - m1.cu, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::MatrixModelWithCoupling, - Sp::M, - b::V, - c::V, - xl::V, - xu::V, - rxns::K, - mets::K; - check_consistency = false, -) where {M<:MatType,V<:VecType,K<:StringVecType} - new_lm = add_reactions( - m.lm, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - check_consistency = check_consistency, - ) - return MatrixModelWithCoupling( - new_lm, - hcat(m.C, spzeros(size(m.C, 1), n_variables(new_lm) - n_variables(m.lm))), - m.cl, - m.cu, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add constraints of the following form to MatrixCoupling and return the modified -model. - -The arguments are same as for in-place [`add_coupling_constraints!`](@ref). -""" -function add_coupling_constraints(m::MatrixCoupling, args...) - new_lp = deepcopy(m) - add_coupling_constraints!(new_lp, args...) - return new_lp -end - -""" -$(TYPEDSIGNATURES) - -Add coupling constraints to a plain [`MatrixModel`](@ref) (returns a -[`MatrixModelWithCoupling`](@ref)). -""" -add_coupling_constraints(m::MatrixModel, args...) = MatrixModelWithCoupling(m, args...) - -""" -$(TYPEDSIGNATURES) - -Overload for adding a single coupling constraint. -""" -function add_coupling_constraints!( - m::MatrixCoupling, - c::VecType, - cl::AbstractFloat, - cu::AbstractFloat, -) - return add_coupling_constraints!(m, sparse(reshape(c, (1, length(c)))), [cl], [cu]) -end - -""" -$(TYPEDSIGNATURES) - -In-place add a single coupling constraint in form -``` - cₗ ≤ C x ≤ cᵤ -``` -""" -function add_coupling_constraints!( - m::MatrixCoupling, - C::MatType, - cl::V, - cu::V, -) where {V<:VecType} - - all([length(cu), length(cl)] .== size(C, 1)) || - throw(DimensionMismatch("mismatched numbers of constraints")) - size(C, 2) == n_variables(m) || - throw(DimensionMismatch("mismatched number of reactions")) - - m.C = vcat(m.C, sparse(C)) - m.cl = vcat(m.cl, collect(cl)) - m.cu = vcat(m.cu, collect(cu)) - nothing -end - -""" -$(TYPEDSIGNATURES) - -Remove coupling constraints from the linear model, and return the modified -model. Arguments are the same as for in-place version -[`remove_coupling_constraints!`](@ref). -""" -function remove_coupling_constraints(m::MatrixCoupling, args...) - new_model = deepcopy(m) - remove_coupling_constraints!(new_model, args...) - return new_model -end - -""" -$(TYPEDSIGNATURES) - -Removes a single coupling constraints from a [`MatrixCoupling`](@ref) in-place. -""" -remove_coupling_constraints!(m::MatrixCoupling, constraint::Int) = - remove_coupling_constraints!(m, [constraint]) - - -""" -$(TYPEDSIGNATURES) - -Removes a set of coupling constraints from a [`MatrixCoupling`](@ref) -in-place. -""" -function remove_coupling_constraints!(m::MatrixCoupling, constraints::Vector{Int}) - to_be_kept = filter(!in(constraints), 1:n_coupling_constraints(m)) - m.C = m.C[to_be_kept, :] - m.cl = m.cl[to_be_kept] - m.cu = m.cu[to_be_kept] - nothing -end - -""" -$(TYPEDSIGNATURES) - -Change the lower and/or upper bounds (`cl` and `cu`) for the given list of -coupling constraints. -""" -function change_coupling_bounds!( - model::MatrixCoupling, - constraints::Vector{Int}; - cl::V = Float64[], - cu::V = Float64[], -) where {V<:VecType} - found = (constraints .>= 1) .& (constraints .<= n_coupling_constraints(model)) - red_constraints = constraints[found] - - length(red_constraints) == length(unique(red_constraints)) || - error("`constraints` appears to contain duplicates") - if !isempty(cl) - length(constraints) == length(cl) || - throw(DimensionMismatch("`constraints` size doesn't match with `cl`")) - model.cl[red_constraints] = cl[found] - end - - if !isempty(cu) - length(constraints) == length(cu) || - throw(DimensionMismatch("`constraints` size doesn't match with `cu`")) - model.cu[red_constraints] = cu[found] - end - nothing -end - -# TODO see if some of these can be derived from AbstractModelWrapper -@_change_bounds_fn MatrixCoupling Int inplace begin - change_bound!(model.lm, rxn_idx; lower_bound, upper_bound) -end - -@_change_bounds_fn MatrixCoupling Int inplace plural begin - change_bounds!(model.lm, rxn_idxs; lower_bounds, upper_bounds) -end - -@_change_bounds_fn MatrixCoupling String inplace begin - change_bound!(model.lm, rxn_id; lower_bound, upper_bound) -end - -@_change_bounds_fn MatrixCoupling String inplace plural begin - change_bounds!(model.lm, rxn_ids; lower_bounds, upper_bounds) -end - -@_change_bounds_fn MatrixCoupling Int begin - n = copy(model) - n.lm = change_bound(model.lm, rxn_idx; lower_bound, upper_bound) - n -end - -@_change_bounds_fn MatrixCoupling Int plural begin - n = copy(model) - n.lm = change_bounds(model.lm, rxn_idxs; lower_bounds, upper_bounds) - n -end - -@_change_bounds_fn MatrixCoupling String begin - n = copy(model) - n.lm = change_bound(model.lm, rxn_id; lower_bound, upper_bound) - n -end - -@_change_bounds_fn MatrixCoupling String plural begin - n = copy(model) - n.lm = change_bounds(model.lm, rxn_ids; lower_bounds, upper_bounds) - n -end - -@_remove_fn reaction MatrixCoupling Int inplace begin - remove_reactions!(model, [reaction_idx]) -end - -@_remove_fn reaction MatrixCoupling Int inplace plural begin - orig_rxns = variables(model.lm) - remove_reactions!(model.lm, reaction_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] - nothing -end - -@_remove_fn reaction MatrixCoupling Int begin - remove_reactions(model, [reaction_idx]) -end - -@_remove_fn reaction MatrixCoupling Int plural begin - n = copy(model) - n.lm = remove_reactions(n.lm, reaction_idxs) - n.C = n.C[:, in.(variables(model.lm), Ref(Set(variables(n.lm))))] - return n -end - -@_remove_fn reaction MatrixCoupling String inplace begin - remove_reactions!(model, [reaction_id]) -end - -@_remove_fn reaction MatrixCoupling String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) -end - -@_remove_fn reaction MatrixCoupling String begin - remove_reactions(model, [reaction_id]) -end - -@_remove_fn reaction MatrixCoupling String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) -end - -@_remove_fn metabolite MatrixCoupling Int inplace begin - remove_metabolites!(model, [metabolite_idx]) -end - -@_remove_fn metabolite MatrixCoupling Int plural inplace begin - orig_rxns = variables(model.lm) - model.lm = remove_metabolites(model.lm, metabolite_idxs) - model.C = model.C[:, in.(orig_rxns, Ref(Set(variables(model.lm))))] - nothing -end - -@_remove_fn metabolite MatrixCoupling Int begin - remove_metabolites(model, [metabolite_idx]) -end - -@_remove_fn metabolite MatrixCoupling Int plural begin - n = copy(model) - n.lm = remove_metabolites(n.lm, metabolite_idxs) - return n -end - -@_remove_fn metabolite MatrixCoupling String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite MatrixCoupling String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -@_remove_fn metabolite MatrixCoupling String begin - remove_metabolites(model, [metabolite_id]) -end - -@_remove_fn metabolite MatrixCoupling String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -""" -$(TYPEDSIGNATURES) - -Forwards arguments to [`change_objective!`](@ref) of the internal model. -""" -function change_objective!(model::MatrixCoupling, args...; kwargs...) - change_objective!(model.lm, args...; kwargs...) -end diff --git a/src/reconstruction/MatrixModel.jl b/src/reconstruction/MatrixModel.jl deleted file mode 100644 index a2914254a..000000000 --- a/src/reconstruction/MatrixModel.jl +++ /dev/null @@ -1,453 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add `rxns` to `model` efficiently. The model must already contain the metabolites used by -`rxns` in the model. -""" -function add_reactions!(model::MatrixModel, rxns::Vector{Reaction}) - I = Int64[] # rows - J = Int64[] # cols - V = Float64[] # values - lbs = zeros(length(rxns)) - ubs = zeros(length(rxns)) - for (j, rxn) in enumerate(rxns) - req = rxn.metabolites - for (i, v) in zip(indexin(keys(req), metabolites(model)), values(req)) - push!(J, j) - push!(I, i) - push!(V, v) - end - push!(model.rxns, rxn.id) - lbs[j] = rxn.lower_bound - ubs[j] = rxn.upper_bound - end - Sadd = sparse(I, J, V, n_metabolites(model), length(rxns)) - model.S = [model.S Sadd] - model.c = dropzeros([model.c; zeros(length(rxns))]) # does not add an objective info from rxns - model.xu = ubs - model.xl = lbs - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Add `rxn` to `model`. The model must already contain the metabolites used by -`rxn` in the model. -""" -add_reaction!(model::MatrixModel, rxn::Reaction) = add_reactions!(model, [rxn]) - -""" -$(TYPEDSIGNATURES) - -Add reaction(s) to a `MatrixModel` model `m`. -""" -function add_reactions( - m::MatrixModel, - s::VecType, - b::VecType, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat; - check_consistency = false, -) - return add_reactions( - m, - sparse(reshape(s, (length(s), 1))), - sparse(b), - sparse([c]), - sparse([xl]), - sparse([xu]), - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::MatrixModel, - s::VecType, - b::VecType, - c::AbstractFloat, - xl::AbstractFloat, - xu::AbstractFloat, - rxn::String, - mets::StringVecType; - check_consistency = false, -) - return add_reactions( - m, - sparse(reshape(s, (length(s), 1))), - sparse(b), - sparse([c]), - sparse([xl]), - sparse([xu]), - [rxn], - mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::MatrixModel, - Sp::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType; - check_consistency = false, -) - rxns = ["r$x" for x = length(m.rxns)+1:length(m.rxns)+length(xu)] - mets = ["m$x" for x = length(m.mets)+1:length(m.mets)+size(Sp)[1]] - return add_reactions( - m, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) - -Add all reactions from `m2` to `m1`. -""" -function add_reactions(m1::MatrixModel, m2::MatrixModel; check_consistency = false) - return add_reactions( - m1, - m2.S, - m2.b, - m2.c, - m2.xl, - m2.xu, - m2.rxns, - m2.mets, - check_consistency = check_consistency, - ) -end - -""" -$(TYPEDSIGNATURES) -""" -function add_reactions( - m::MatrixModel, - Sp::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType, - rxns::StringVecType, - mets::StringVecType; - check_consistency = false, -) - Sp = sparse(Sp) - b = sparse(b) - c = sparse(c) - xl = collect(xl) - xu = collect(xu) - - all([length(b), length(mets)] .== size(Sp, 1)) || - throw(DimensionMismatch("inconsistent number of metabolites")) - all(length.([c, xl, xu, rxns]) .== size(Sp, 2)) || - throw(DimensionMismatch("inconsistent number of reactions")) - - new_reactions = findall(Bool[!(rxn in m.rxns) for rxn in rxns]) - new_metabolites = findall(Bool[!(met in m.mets) for met in mets]) - - if check_consistency - (newReactions1, newMetabolites1) = verify_consistency( - m, - Sp, - b, - c, - xl, - xu, - rxns, - mets, - new_reactions, - new_metabolites, - ) - end - - new_mets = vcat(m.mets, mets[new_metabolites]) - - zero_block = spzeros(length(new_metabolites), n_variables(m)) - ext_s = vcat(sparse(m.S), zero_block) - - mapping = [findfirst(isequal(met), new_mets) for met in mets] - (I, J, elements) = findnz(sparse(Sp[:, new_reactions])) - ext_sp = spzeros(length(new_mets), length(new_reactions)) - for (k, i) in enumerate(I) - new_i = mapping[i] - ext_sp[new_i, J[k]] = elements[k] - end - - new_s = hcat(ext_s, ext_sp) - newb = vcat(m.b, b[new_metabolites]) - newc = vcat(m.c, c[new_reactions]) - newxl = vcat(m.xl, xl[new_reactions]) - newxu = vcat(m.xu, xu[new_reactions]) - new_rxns = vcat(m.rxns, rxns[new_reactions]) - new_lp = MatrixModel(new_s, newb, newc, newxl, newxu, new_rxns, new_mets) - - if check_consistency - return (new_lp, new_reactions, new_metabolites) - else - return new_lp - end -end - -""" -$(TYPEDSIGNATURES) - -Check the consistency of given reactions with existing reactions in `m`. - -TODO: work in progress, doesn't return consistency status. -""" -function verify_consistency( - m::MatrixModel, - Sp::M, - b::V, - c::V, - xl::B, - xu::B, - names::K, - mets::K, - new_reactions, - new_metabolites, -) where {M<:MatType,V<:VecType,B<:VecType,K<:StringVecType} - - if !isempty(new_reactions) - statuses = Vector{ReactionStatus}(undef, length(names)) - for (i, name) in enumerate(names) - rxn_index = findfirst(isequal(name), m.rxns) - reaction = Sp[:, i] - stoich_index = findfirst(Bool[reaction == m.S[:, j] for j = 1:size(m.S, 2)]) - if isnothing(rxn_index) & isnothing(stoich_index) - statuses[i] = ReactionStatus(false, 0, "new") - end - - if !isnothing(rxn_index) & isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same name") - end - - if isnothing(rxn_index) & !isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same stoichiometry") - end - - if !isnothing(rxn_index) & !isnothing(stoich_index) - statuses[i] = ReactionStatus(true, 0, "same name, same stoichiometry") - end - end - end - - return (new_reactions, new_metabolites) -end - -@_change_bounds_fn MatrixModel Int inplace begin - isnothing(lower_bound) || (model.xl[rxn_idx] = lower_bound) - isnothing(upper_bound) || (model.xu[rxn_idx] = upper_bound) - nothing -end - -@_change_bounds_fn MatrixModel Int inplace plural begin - for (i, l, u) in zip(rxn_idxs, lower_bounds, upper_bounds) - change_bound!(model, i, lower_bound = l, upper_bound = u) - end -end - -@_change_bounds_fn MatrixModel Int begin - change_bounds( - model, - [rxn_idx], - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -@_change_bounds_fn MatrixModel Int plural begin - n = copy(model) - n.xl = copy(n.xl) - n.xu = copy(n.xu) - change_bounds!(n, rxn_idxs; lower_bounds, upper_bounds) - n -end - -@_change_bounds_fn MatrixModel String inplace begin - change_bounds!( - model, - [rxn_id], - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -@_change_bounds_fn MatrixModel String inplace plural begin - change_bounds!( - model, - Vector{Int}(indexin(rxn_ids, variables(model))); - lower_bounds, - upper_bounds, - ) -end - -@_change_bounds_fn MatrixModel String begin - change_bounds( - model, - [rxn_id], - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -@_change_bounds_fn MatrixModel String plural begin - change_bounds( - model, - Int.(indexin(rxn_ids, variables(model))); - lower_bounds, - upper_bounds, - ) -end - -@_remove_fn reaction MatrixModel Int inplace begin - remove_reactions!(model, [reaction_idx]) -end - -@_remove_fn reaction MatrixModel Int inplace plural begin - mask = .!in.(1:n_variables(model), Ref(reaction_idxs)) - model.S = model.S[:, mask] - model.c = model.c[mask] - model.xl = model.xl[mask] - model.xu = model.xu[mask] - model.rxns = model.rxns[mask] - nothing -end - -@_remove_fn reaction MatrixModel Int begin - remove_reactions(model, [reaction_idx]) -end - -@_remove_fn reaction MatrixModel Int plural begin - n = copy(model) - remove_reactions!(n, reaction_idxs) - return n -end - -@_remove_fn reaction MatrixModel String inplace begin - remove_reactions!(model, [reaction_id]) -end - -@_remove_fn reaction MatrixModel String inplace plural begin - remove_reactions!(model, Int.(indexin(reaction_ids, variables(model)))) -end - -@_remove_fn reaction MatrixModel String begin - remove_reactions(model, [reaction_id]) -end - -@_remove_fn reaction MatrixModel String plural begin - remove_reactions(model, Int.(indexin(reaction_ids, variables(model)))) -end - -@_remove_fn metabolite MatrixModel Int inplace begin - remove_metabolites!(model, [metabolite_idx]) -end - -@_remove_fn metabolite MatrixModel Int plural inplace begin - remove_reactions!( - model, - [ - ridx for ridx = 1:n_variables(model) if - any(in.(findnz(model.S[:, ridx])[1], Ref(metabolite_idxs))) - ], - ) - mask = .!in.(1:n_metabolites(model), Ref(metabolite_idxs)) - model.S = model.S[mask, :] - model.b = model.b[mask] - model.mets = model.mets[mask] - nothing -end - -@_remove_fn metabolite MatrixModel Int begin - remove_metabolites(model, [metabolite_idx]) -end - -@_remove_fn metabolite MatrixModel Int plural begin - n = deepcopy(model) #everything gets changed anyway - remove_metabolites!(n, metabolite_idxs) - return n -end - -@_remove_fn metabolite MatrixModel String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite MatrixModel String inplace plural begin - remove_metabolites!(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -@_remove_fn metabolite MatrixModel String begin - remove_metabolites(model, [metabolite_id]) -end - -@_remove_fn metabolite MatrixModel String plural begin - remove_metabolites(model, Int.(indexin(metabolite_ids, metabolites(model)))) -end - -""" -$(TYPEDSIGNATURES) - -Change the objective to reactions at given indexes, optionally specifying their -`weights` in the same order. By default, all set weights are 1. -""" -function change_objective!( - model::MatrixModel, - rxn_idxs::Vector{Int}; - weights = ones(length(rxn_idxs)), -) - model.c = spzeros(length(model.c)) - model.c[rxn_idxs] .= weights - nothing -end - -""" -$(TYPEDSIGNATURES) - -Change objective function of a MatrixModel to a single `1` at reaction index -`rxn_idx`. -""" -change_objective!(model::MatrixModel, rxn_idx::Int) = change_objective!(model, [rxn_idx]) - -""" -$(TYPEDSIGNATURES) - -Change objective of given reaction IDs, optionally specifying objective -`weights` in the same order as `rxn_ids`. By default, all set weights are 1. -""" -function change_objective!( - model::MatrixModel, - rxn_ids::Vector{String}; - weights = ones(length(rxn_ids)), -) - idxs = indexin(rxn_ids, variables(model)) - any(isnothing(idx) for idx in idxs) && - throw(DomainError(rxn_ids, "Some reaction ids not found in the model")) - change_objective!(model, Int.(idxs); weights) -end - -""" -$(TYPEDSIGNATURES) - -Change objective function of a MatrixModel to a single `1` at the given reaction -ID. -""" -change_objective!(model::MatrixModel, rxn_id::String) = change_objective!(model, [rxn_id]) diff --git a/src/reconstruction/ObjectModel.jl b/src/reconstruction/ObjectModel.jl deleted file mode 100644 index c8aeb08bb..000000000 --- a/src/reconstruction/ObjectModel.jl +++ /dev/null @@ -1,745 +0,0 @@ -# Add and remove reactions - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_reaction!`](@ref). -""" -function add_reactions!(model::ObjectModel, rxns::Vector{Reaction}) - check_arg_keys_exists(model, :reactions, rxns) - for rxn in rxns - model.reactions[rxn.id] = rxn - end -end - -""" -$(TYPEDSIGNATURES) - -Add `rxn` to `model` based on reaction `id` if the `id` is not already in the -model. -""" -add_reaction!(model::ObjectModel, rxn::Reaction) = add_reactions!(model, [rxn]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_reaction`](@ref). -""" -function add_reactions(model::ObjectModel, rxns::Vector{Reaction}) - m = copy(model) - m.reactions = copy(m.reactions) - add_reactions!(m, rxns) - m -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copied version of `model` with `rxn` added if it's ID is not -already present in the model. -""" -add_reaction(model::ObjectModel, rxn::Reaction) = add_reactions(model, [rxn]) - -@_remove_fn reaction ObjectModel String inplace plural begin - check_arg_keys_missing(model, :reactions, reaction_ids) - delete!.(Ref(model.reactions), reaction_ids) - nothing -end - -@_remove_fn reaction ObjectModel String inplace begin - remove_reactions!(model, [reaction_id]) - nothing -end - -@_remove_fn reaction ObjectModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - remove_reactions!(n, reaction_ids) - return n -end - -@_remove_fn reaction ObjectModel String begin - remove_reactions(model, [reaction_id]) -end - -# Add and remove metabolites - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_metabolite!`](@ref). -""" -function add_metabolites!(model::ObjectModel, mets::Vector{Metabolite}) - check_arg_keys_exists(model, :metabolites, mets) - for met in mets - model.metabolites[met.id] = met - end -end - -""" -$(TYPEDSIGNATURES) - -Add `met` to `model` based on metabolite `id` if the `id` is not already in the -model. -""" -add_metabolite!(model::ObjectModel, met::Metabolite) = add_metabolites!(model, [met]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_metabolite`](@ref). -""" -function add_metabolites(model::ObjectModel, mets::Vector{Metabolite}) - m = copy(model) - m.metabolites = copy(m.metabolites) - add_metabolites!(m, mets) - m -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copied version of the `model` with `met` added. Only adds -`met` if its ID is not already present in the model. -""" -add_metabolite(model::ObjectModel, met::Metabolite) = add_metabolites(model, [met]) - -@_remove_fn metabolite ObjectModel String inplace plural begin - check_arg_keys_missing(model, :metabolites, metabolite_ids) - remove_reactions!( - model, - [ - rid for (rid, rn) in model.reactions if - any(haskey.(Ref(rn.metabolites), metabolite_ids)) - ], - ) - delete!.(Ref(model.metabolites), metabolite_ids) - nothing -end - -@_remove_fn metabolite ObjectModel String inplace begin - remove_metabolites!(model, [metabolite_id]) -end - -@_remove_fn metabolite ObjectModel String plural begin - n = copy(model) - n.reactions = copy(model.reactions) - n.metabolites = copy(model.metabolites) - remove_metabolites!(n, metabolite_ids) - return n -end - -@_remove_fn metabolite ObjectModel String begin - remove_metabolites(model, [metabolite_id]) -end - -# Add and remove genes - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_gene!`](@ref). -""" -function add_genes!(model::ObjectModel, genes::Vector{Gene}) - check_arg_keys_exists(model, :genes, genes) - for gene in genes - model.genes[gene.id] = gene - end -end - -""" -$(TYPEDSIGNATURES) - -Add `gene` to `model` based on gene `id` if the `id` is not already in the -model. -""" -add_gene!(model::ObjectModel, gene::Gene) = add_genes!(model, [gene]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_gene`](@ref). -""" -function add_genes(model::ObjectModel, genes::Vector{Gene}) - m = copy(model) - m.genes = copy(m.genes) - add_genes!(m, genes) - m -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copied version of the `model` with added `gene`. Only adds the -`gene` if its ID is not already present in the model. -""" -add_gene(model::ObjectModel, gene::Gene) = add_genes(model, [gene]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`remove_gene!`](@ref). -""" -function remove_genes!( - model::ObjectModel, - gids::Vector{String}; - knockout_reactions::Bool = false, -) - check_arg_keys_missing(model, :genes, gids) - if knockout_reactions - rm_reactions = String[] - for (rid, r) in model.reactions - if !isnothing(r.gene_associations) && all( - any(in.(gids, Ref(conjunction))) for - conjunction in reaction_gene_associations(model, rid) - ) - push!(rm_reactions, rid) - end - end - delete!.(Ref(model.reactions), rm_reactions) - end - delete!.(Ref(model.genes), gids) - nothing -end - -""" -$(TYPEDSIGNATURES) - -Remove gene with `id` from `model`. If `knockout_reactions` is true, then also -constrain reactions that require the genes to function to carry zero flux. -""" -remove_gene!(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = - remove_genes!(model, [gid]; knockout_reactions = knockout_reactions) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`remove_gene`](@ref). -""" -function remove_genes( - model::ObjectModel, - gids::Vector{String}; - knockout_reactions::Bool = false, -) - check_arg_keys_missing(model, :genes, gids) - - m = copy(model) - m.genes = copy(model.genes) - - if knockout_reactions - - rm_reactions = String[] - for (rid, r) in model.reactions - if !isnothing(r.gene_associations) && all( - any(in.(gids, Ref(conjunction))) for - conjunction in reaction_gene_associations(model, rid) - ) - push!(rm_reactions, rid) - end - end - - m.reactions = copy(reactions) - delete!.(Ref(m.reactions), rm_reactions) - end - - delete!.(Ref(m.genes), gids) - - m -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copy of `model` with `gid` remove from `model`. If -`knockout_reactions` is true, then also constrain reactions that require the -genes to function to carry zero flux. -""" -remove_gene(model::ObjectModel, gid::String; knockout_reactions::Bool = false) = - remove_genes(model, [gid]; knockout_reactions) - -# Change reaction bounds - -@_change_bounds_fn ObjectModel String inplace begin - change_bounds!( - model, - [rxn_id]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -@_change_bounds_fn ObjectModel String inplace plural begin - check_arg_keys_missing(model, :reactions, rxn_ids) - for (rxn_id, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) - isnothing(lower) || (model.reactions[rxn_id].lower_bound = lower) - isnothing(upper) || (model.reactions[rxn_id].upper_bound = upper) - end -end - -@_change_bounds_fn ObjectModel String begin - change_bounds( - model, - [rxn_id], - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -@_change_bounds_fn ObjectModel String plural begin - check_arg_keys_missing(model, :reactions, rxn_ids) - m = copy(model) - m.reactions = copy(model.reactions) - for (rid, lower, upper) in zip(rxn_ids, lower_bounds, upper_bounds) - m.reactions[rid] = copy(model.reactions[rid]) - for field in fieldnames(typeof(model.reactions[rid])) - setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) - end - isnothing(lower) || (m.reactions[rid].lower_bound = lower) - isnothing(upper) || (m.reactions[rid].upper_bound = upper) - end - return m -end - -# Change gene product bounds - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`change_gene_product_bound!`](@ref). -""" -function change_gene_product_bounds!( - model::ObjectModel, - gids::Vector{String}; - lower_bounds = fill(nothing, length(gids)), - upper_bounds = fill(nothing, length(gids)), -) - check_arg_keys_missing(model, :genes, gids) - for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) - isnothing(lower) || (model.genes[gid].product_lower_bound = lower) - isnothing(upper) || (model.genes[gid].product_upper_bound = upper) - end -end - -""" -$(TYPEDSIGNATURES) - -Changes the `product_lower_bound` or `product_upper_bound` for the -[`Gene`][(ref) `gid` in the `model`, in place. If either `lower_bound` or -`upper_bound` is `nothing`, then that bound is not changed. -""" -function change_gene_product_bound!( - model::ObjectModel, - gid::String; - lower_bound = nothing, - upper_bound = nothing, -) - change_gene_product_bounds!( - model, - [gid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`change_gene_product_bound`](@ref). -""" -function change_gene_product_bounds( - model::ObjectModel, - gids::Vector{String}; - lower_bounds = fill(nothing, length(gids)), - upper_bounds = fill(nothing, length(gids)), -) - check_arg_keys_missing(model, :genes, gids) - m = copy(model) - m.genes = copy(model.genes) - for (gid, lower, upper) in zip(gids, lower_bounds, upper_bounds) - m.genes[gid] = copy(model.genes[gid]) - for field in fieldnames(typeof(model.genes[gid])) - setfield!(m.genes[gid], field, getfield(model.genes[gid], field)) - end - isnothing(lower) || (m.genes[gid].product_lower_bound = lower) - isnothing(upper) || (m.genes[gid].product_upper_bound = upper) - end - m -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_gene_product_bound!`](@ref) that does not modify the -original model, but makes a shallow copy with the modification included. -""" -function change_gene_product_bound( - model::ObjectModel, - gid::String; - lower_bound = nothing, - upper_bound = nothing, -) - change_gene_product_bounds( - model, - [gid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], - ) -end - -# Change objective - -""" -$(TYPEDSIGNATURES) - -Change the objective for `model` to reaction(s) with `rxn_ids`, optionally -specifying their `weights`. By default, assume equal weights. If no objective -exists in model, sets objective. -""" -function change_objective!( - model::ObjectModel, - rxn_ids::Vector{String}; - weights = ones(length(rxn_ids)), -) - all(!haskey(model.reactions, rid) for rid in rxn_ids) && - throw(DomainError(rxn_ids, "Some reaction ids were not found in model.")) - model.objective = Dict(rxn_ids .=> weights) - nothing -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_objective!`](@ref) that sets a single `rxn_id` as the -objective weight with `weight` (defaults to 1.0). -""" -change_objective!(model::ObjectModel, rxn_id::String; weight::Float64 = 1.0) = - change_objective!(model, [rxn_id]; weights = [weight]) - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_objective!`](@ref) that does not modify the original model, -but makes a shallow copy with the modification included. -""" -function change_objective( - model::ObjectModel, - rxn_ids::Vector{String}; - weights = ones(length(rxn_ids)), -) - m = copy(model) - m.objective = copy(model.objective) - change_objective!(m, rxn_ids; weights) - m -end - - -""" -$(TYPEDSIGNATURES) - -Variant of [`change_objective!`](@ref) that does not modify the original model, -but makes a shallow copy with the modification included. -""" -function change_objective(model::ObjectModel, rxn_id::String; weight::Float64 = 1.0) - m = copy(model) - m.objective = copy(model.objective) - change_objective!(m, rxn_id; weight) - m -end - -# Add and remove biomass metabolite - -""" -$(TYPEDSIGNATURES) - -Add a biomass metabolite called `biomass_metabolite_id` with stoichiometry 1 to -the biomass reaction, called `biomass_rxn_id` in `model`. Changes the model in -place. Does not check if the model already has a biomass metabolite. -""" -function add_biomass_metabolite!( - model::ObjectModel, - biomass_rxn_id::String; - biomass_metabolite_id = "biomass", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - model.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 - add_metabolite!(model, Metabolite(biomass_metabolite_id)) -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_biomass_metabolite!`](@ref) that does not modify the original -model, but makes a shallow copy with the modification included. Does not check -if the model already has a biomass metabolite. -""" -function add_biomass_metabolite( - model::ObjectModel, - biomass_rxn_id::String; - biomass_metabolite_id = "biomass", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - - m = copy(model) - m.metabolites = copy(m.metabolites) - m.metabolites[biomass_metabolite_id] = Metabolite(biomass_metabolite_id) - - m.reactions = copy(model.reactions) - m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) - m.reactions[biomass_rxn_id].metabolites = - copy(model.reactions[biomass_rxn_id].metabolites) - m.reactions[biomass_rxn_id].metabolites[biomass_metabolite_id] = 1.0 - - m -end - -""" -$(TYPEDSIGNATURES) - -Remove a biomass metabolite called `biomass_metabolite_id` from -the biomass reaction, called `biomass_rxn_id` in `model`. -""" -function remove_biomass_metabolite!( - model::ObjectModel, - biomass_rxn_id::String; - biomass_metabolite_id = "biomass", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - check_has_biomass_rxn_biomas_metabolite( - model.reactions, - biomass_rxn_id, - biomass_metabolite_id, - ) - - delete!(model.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) - remove_metabolite!(model, biomass_metabolite_id) -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`remove_biomass_metabolite!`](@ref) that does not modify the original -model, but makes a shallow copy with the modification included. -""" -function remove_biomass_metabolite( - model::ObjectModel, - biomass_rxn_id::String; - biomass_metabolite_id = "biomass", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - check_has_biomass_rxn_biomas_metabolite( - model.reactions, - biomass_rxn_id, - biomass_metabolite_id, - ) - - m = copy(model) - m.metabolites = copy(m.metabolites) - delete!(m.metabolites, biomass_metabolite_id) - - m.reactions = copy(model.reactions) - m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) - m.reactions[biomass_rxn_id].metabolites = - copy(model.reactions[biomass_rxn_id].metabolites) - delete!(m.reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) - - m -end - -# Add virtual ribosome (assume no model already has one) - -""" -$(TYPEDSIGNATURES) - -To `biomass_rxn_id` in `model`, add a pseudo-isozyme and associated gene that -approximates the effect ribosome synthesis has on growth. - -# Bacterial growth law models -Numerous experimental studies have shown that during steady state growth the -cellular density of E. coli (and probably other bacteria) is constant. As a -consequence of this, growth law models typically assume that the total proteome -capacity (mass fraction of protein in the cell) is limited. Further, it has been -shown experimentally that the ribosomal protein content of a bacterial cell -increases with faster growth rate. Ribosomes are used to make proteins (and -themselves), leading to a trade-off: faster growth requires more ribosomes to -make more enzymes to grow, but this reduces the amount of proteome space "left -over" for biosynthetic enzymes. See Mori, Matteo, et al. "Constrained allocation -flux balance analysis." PLoS computational biology 12.6 (2016) for more details. - -# Implementation -This modification makes the underlying biomass reaction unidirectional. The -`virtualribosome_id` defaults to `virtualribosome`, and corresponds to a -pseudogene called `virtualribosome_id`. The parameter `weight` needs to be -estimated from data, but acts like a turnover number. Lower `weight` means more -ribosome is required for growth (`ribosome = growth/weight`). The molar mass of -the ribosome is `1`. The pseudo-isozyme acts like a regular gene product, -``` -ribosome = weight * biomass_flux -``` -when simulating enzyme constrained models. -""" -function add_virtualribosome!( - model::ObjectModel, - biomass_rxn_id::String, - weight::Float64; - virtualribosome_id = "virtualribosome", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - check_biomass_rxn_has_isozymes(model.reactions, biomass_rxn_id) - check_has_virtualribosome(model.genes, virtualribosome_id) - - # ensure unidirectional - model.reactions[biomass_rxn_id].lower_bound = 0.0 - model.reactions[biomass_rxn_id].upper_bound = constants.default_reaction_bound - - # add ribosome kinetics - model.reactions[biomass_rxn_id].gene_associations = [ - Isozyme( - kcat_forward = weight, - kcat_backward = 0.0, - gene_product_stoichiometry = Dict(virtualribosome_id => 1.0), - ), - ] - - # add ribosome gene - model.genes[virtualribosome_id] = - Gene(id = virtualribosome_id, product_molar_mass = 1.0) - - nothing -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_virtualribosome!`](@ref) that does not modify the original -model, but makes a shallow copy with the modification included. -""" -function add_virtualribosome( - model::ObjectModel, - biomass_rxn_id::String, - weight::Float64; - virtualribosome_id = "virtualribosome", -) - check_has_biomass_rxn_id(model.reactions, biomass_rxn_id) - check_biomass_rxn_has_isozymes(model.reactions, biomass_rxn_id) - check_has_virtualribosome(model.genes, virtualribosome_id) - - m = copy(model) - m.reactions = copy(model.reactions) - m.reactions[biomass_rxn_id] = copy(model.reactions[biomass_rxn_id]) - m.genes = copy(model.genes) - - add_virtualribosome!(m, biomass_rxn_id, weight; virtualribosome_id) - - m -end - -# Add, remove isozymes (no change because the order isozymes may appear in is not constant across models) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_isozymes!`](@ref). -""" -function add_isozymes!( - model::ObjectModel, - rids::Vector{String}, - isozymes_vector::Vector{Vector{Isozyme}}, -) - check_arg_keys_missing(model, :reactions, rids) - check_has_isozymes(model, rids) - - for (rid, isozymes) in zip(rids, isozymes_vector) - model.reactions[rid].gene_associations = isozymes - end -end - -""" -$(TYPEDSIGNATURES) - -Add `isozymes` to `rid` in `model`. Only allowed if `rid` does not have isozymes assigned to it. -""" -add_isozymes!(model::ObjectModel, rid::String, isozymes::Vector{Isozyme}) = - add_isozymes!(model, [rid], [isozymes]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`add_isozymes`](@ref). -""" -function add_isozymes( - model::ObjectModel, - rids::Vector{String}, - isozymes_vector::Vector{Vector{Isozyme}}, -) - check_arg_keys_missing(model, :reactions, rids) - check_has_isozymes(model, rids) - - m = copy(model) - m.reactions = copy(model.reactions) - - for (rid, isozymes) in zip(rids, isozymes_vector) - m.reactions[rid] = copy(model.reactions[rid]) - if !isnothing(model.reactions[rid].gene_associations) - m.reactions[rid].gene_associations = - copy(model.reactions[rid].gene_associations) - end - m.reactions[rid].gene_associations = isozymes - end - m -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`add_isozymes!`](@ref) that returns a copied model instead of -modifying the input. -""" -add_isozymes(model::ObjectModel, rid::String, isozymes::Vector{Isozyme}) = - add_isozymes(model, [rid], [isozymes]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`remove_isozyme!`](@ref). -""" -function remove_isozymes!(model::ObjectModel, rids::Vector{String}) - check_arg_keys_missing(model, :reactions, rids) - for rid in rids - model.reactions[rid].gene_associations = nothing - end -end - -""" -$(TYPEDSIGNATURES) - -Remove all isozymes from `rid` in `model`. Note, this function removes all -isozymes from `rid`. -""" -remove_isozymes!(model::ObjectModel, rid::String) = remove_isozymes!(model, [rid]) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`remove_isozyme`](@ref). -""" -function remove_isozymes(model::ObjectModel, rids::Vector{String}) - check_arg_keys_missing(model, :reactions, rids) - - m = copy(model) - m.reactions = copy(model.reactions) - for rid in rids - m.reactions[rid] = copy(model.reactions[rid]) - for field in fieldnames(typeof(model.reactions[rid])) - setfield!(m.reactions[rid], field, getfield(model.reactions[rid], field)) - end - m.reactions[rid].gene_associations = nothing - end - m -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copy of `model` with all isozymes of reaction `rid` removed. -""" -remove_isozymes(model::ObjectModel, rid::String) = remove_isozymes(model, [rid]) diff --git a/src/reconstruction/SerializedModel.jl b/src/reconstruction/SerializedModel.jl deleted file mode 100644 index 8f9d66f2c..000000000 --- a/src/reconstruction/SerializedModel.jl +++ /dev/null @@ -1,20 +0,0 @@ - -# this just generates the necessary wrappers - -@_serialized_change_unwrap add_reactions -@_serialized_change_unwrap change_bound -@_serialized_change_unwrap change_bounds -@_serialized_change_unwrap remove_metabolite -@_serialized_change_unwrap remove_metabolites -@_serialized_change_unwrap remove_reaction -@_serialized_change_unwrap remove_reactions - -""" -$(TYPEDSIGNATURES) - -Returns the model stored in the serialized structure. -""" -function unwrap_serialized(model::Serialized) - precache!(model) - model.m -end diff --git a/src/reconstruction/pipes/community.jl b/src/reconstruction/pipes/community.jl deleted file mode 100644 index 4de0402df..000000000 --- a/src/reconstruction/pipes/community.jl +++ /dev/null @@ -1,33 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Specifies a model variant where the abundances of community members has been -changed. Forwards arguments to [`change_abundances`](@ref). -""" -with_changed_abundances(args...; kwargs...) = m -> change_abundances(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant where an environmental exchange reaction has its -bounds changed. Calls [`change_environmental_bound`](@ref) internally. -""" -with_changed_environmental_bound(args...; kwargs...) = - m -> change_environmental_bound(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`with_changed_environmental_bound`](@ref) -""" -with_changed_environmental_bounds(args...; kwargs...) = - m -> change_environmental_bounds(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Species a model variant that wraps a [`CommunityModel`](@ref) into a -[`EqualGrowthCommunityModel`](@ref). Forwards the arguments to the constructor. -""" -with_equal_growth_objective(args...; kwargs...) = - m -> EqualGrowthCommunityModel(args...; inner = m, kwargs...) diff --git a/src/reconstruction/pipes/enzymes.jl b/src/reconstruction/pipes/enzymes.jl deleted file mode 100644 index 72f94591c..000000000 --- a/src/reconstruction/pipes/enzymes.jl +++ /dev/null @@ -1,21 +0,0 @@ -# constructors for enzyme constrained models - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant which adds extra semantics of the sMOMENT algorithm, -giving a [`SimplifiedEnzymeConstrainedModel`](@ref). The arguments are forwarded to -[`make_simplified_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). -""" -with_simplified_enzyme_constraints(args...; kwargs...) = - model -> make_simplified_enzyme_constrained_model(model, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant which adds extra semantics of the EnzymeConstrained algorithm, -giving a [`EnzymeConstrainedModel`](@ref). The arguments are forwarded to -[`make_enzyme_constrained_model`](@ref). Intended for usage with [`screen`](@ref). -""" -with_enzyme_constraints(args...; kwargs...) = - model -> make_enzyme_constrained_model(model, args...; kwargs...) diff --git a/src/reconstruction/pipes/generic.jl b/src/reconstruction/pipes/generic.jl deleted file mode 100644 index 0126de49c..000000000 --- a/src/reconstruction/pipes/generic.jl +++ /dev/null @@ -1,198 +0,0 @@ -# Reactions - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with reactions added. Forwards the arguments to -[`add_reactions`](@ref). -""" -with_added_reactions(args...; kwargs...) = m -> add_reactions(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with an added reaction. Forwards the arguments to -[`add_reaction`](@ref). -""" -with_added_reaction(args...; kwargs...) = m -> add_reaction(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant without a certain reaction. Forwards arguments to -[`remove_reaction`](@ref). -""" -with_removed_reaction(args...; kwargs...) = m -> remove_reaction(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural version of [`with_removed_reaction`](@ref), calls -[`remove_reactions`](@ref) internally. -""" -with_removed_reactions(args...; kwargs...) = m -> remove_reactions(m, args...; kwargs...) - -# Metabolites - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with metabolites added. Forwards the arguments to -[`add_metabolites`](@ref). -""" -with_added_metabolites(args...; kwargs...) = m -> add_metabolites(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with an added metabolite. Forwards the arguments to -[`add_metabolite`](@ref). -""" -with_added_metabolite(args...; kwargs...) = m -> add_metabolite(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant without a certain metabolite. Forwards arguments to -[`remove_metabolite`](@ref). -""" -with_removed_metabolite(args...; kwargs...) = m -> remove_metabolite(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural version of [`with_removed_metabolite`](@ref), calls -[`remove_metabolites`](@ref) internally. -""" -with_removed_metabolites(args...; kwargs...) = - m -> remove_metabolites(m, args...; kwargs...) - -# Genes - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with genes added. Forwards the arguments to -[`add_genes`](@ref). -""" -with_added_genes(args...; kwargs...) = m -> add_genes(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with an added gene. Forwards the arguments to -[`add_gene`](@ref). -""" -with_added_gene(args...; kwargs...) = m -> add_gene(m, args...; kwargs...) - - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with removed genes. Forwards the arguments to -[`remove_genes`](@ref). -""" -with_removed_genes(args...; kwargs...) = m -> remove_genes(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with a gene removed. Forwards the arguments to -[`remove_gene`](@ref). -""" -with_removed_gene(args...; kwargs...) = m -> remove_gene(m, args...; kwargs...) - -# Bounds - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that has a new bound set. Forwards arguments to -[`change_bound`](@ref). Intended for usage with [`screen`](@ref). -""" -with_changed_bound(args...; kwargs...) = m -> change_bound(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`with_changed_bound`](@ref). -""" -with_changed_bounds(args...; kwargs...) = m -> change_bounds(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with the bounds changed for the gene product. Forwards -the arguments to [`change_gene_product_bound`](@ref). -""" -with_changed_gene_product_bound(args...; kwargs...) = - m -> change_gene_product_bound(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Plural version of [`with_changed_gene_product_bound`](@ref), calls -[`change_gene_product_bounds`](@ref) internally. -""" -with_changed_gene_product_bounds(args...; kwargs...) = - m -> change_gene_product_bounds(m, args...; kwargs...) - -# Objective - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant with the objective reaction(s) changed. Forwards the -arguments to [`change_objective`](@ref). -""" -with_changed_objective(args...; kwargs...) = m -> change_objective(m, args...; kwargs...) - -# Biomass - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that adds a biomass metabolite to the biomass -reaction. Forwards arguments to [`add_biomass_metabolite`](@ref). -""" -with_added_biomass_metabolite(args...; kwargs...) = - m -> add_biomass_metabolite(m, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that removes a biomass metabolite from the biomass -reaction. Forwards arguments to [`remove_biomass_metabolite`](@ref). -""" -with_removed_biomass_metabolite(args...; kwargs...) = - m -> remove_biomass_metabolite(m, args...; kwargs...) - -# Virtual ribosome - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that adds a virtualribosome to a model. Args and kwargs -are forwarded to [`add_virtualribosome`](@ref). -""" -with_virtualribosome(args...; kwargs...) = - model -> add_virtualribosome(model, args...; kwargs...) - -# Isozymes - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that adds isozymes to the model through calling -[`add_isozyme`](@ref). -""" -with_added_isozymes(args...; kwargs...) = model -> add_isozymes(model, args...; kwargs...) - -""" -$(TYPEDSIGNATURES) - -Specifies a model variant that removes isozymes to the model through calling -[`remove_isozymes`](@ref). -""" -with_removed_isozymes(args...; kwargs...) = - model -> remove_isozymes(model, args...; kwargs...) diff --git a/src/reconstruction/pipes/thermodynamic.jl b/src/reconstruction/pipes/thermodynamic.jl deleted file mode 100644 index 348de78fd..000000000 --- a/src/reconstruction/pipes/thermodynamic.jl +++ /dev/null @@ -1,9 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A pipe-able function that specifies a model variant that solves the max-min -driving force problem. Calls [`make_max_min_driving_force_model`](@ref) -internally. -""" -with_max_min_driving_force_analysis(args...; kwargs...) = - m -> make_max_min_driving_force_model(m, args...; kwargs...) diff --git a/src/types/FluxSummary.jl b/src/types/FluxSummary.jl deleted file mode 100644 index e2cddf37c..000000000 --- a/src/types/FluxSummary.jl +++ /dev/null @@ -1,29 +0,0 @@ -""" -$(TYPEDEF) - -A struct used to store summary information about the solution -of a constraint based analysis result. - -# Fields -$(TYPEDFIELDS) -""" -struct FluxSummary - biomass_fluxes::OrderedDict{String,Float64} - import_fluxes::OrderedDict{String,Float64} - export_fluxes::OrderedDict{String,Float64} - unbounded_fluxes::OrderedDict{String,Float64} -end - -""" -$(TYPEDSIGNATURES) - -A default empty constructor for `FluxSummary`. -""" -function FluxSummary() - FluxSummary( - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - OrderedDict{String,Float64}(), - ) -end diff --git a/src/types/FluxVariabilitySummary.jl b/src/types/FluxVariabilitySummary.jl deleted file mode 100644 index a86c7b983..000000000 --- a/src/types/FluxVariabilitySummary.jl +++ /dev/null @@ -1,24 +0,0 @@ -""" -$(TYPEDEF) - -Stores summary information about the result of a flux variability analysis. - -# Fields -$(TYPEDFIELDS) -""" -struct FluxVariabilitySummary - biomass_fluxes::Dict{String,Vector{Maybe{Float64}}} - exchange_fluxes::Dict{String,Vector{Maybe{Float64}}} -end - -""" -$(TYPEDSIGNATURES) - -A default empty constructor for [`FluxVariabilitySummary`](@ref). -""" -function FluxVariabilitySummary() - FluxVariabilitySummary( - Dict{String,Vector{Maybe{Float64}}}(), - Dict{String,Vector{Maybe{Float64}}}(), - ) -end diff --git a/src/types/Gene.jl b/src/types/Gene.jl deleted file mode 100644 index 5d5961e6a..000000000 --- a/src/types/Gene.jl +++ /dev/null @@ -1,26 +0,0 @@ -""" -$(TYPEDEF) - -# Fields -$(TYPEDFIELDS) - -Default constructor requires the `id` keyword to be assigned. -""" -Base.@kwdef mutable struct Gene - id::String - name::Maybe{String} = nothing - sequence::Maybe{String} = nothing - product_molar_mass::Maybe{Float64} = nothing - product_lower_bound::Float64 = 0 - product_upper_bound::Float64 = constants.default_gene_product_bound - notes::Notes = Notes() - annotations::Annotations = Annotations() -end - -""" -$(TYPEDSIGNATURES) - -A convenience constructor for [`Gene`](@ref) taking as first argument the id -of the gene. All other kwargs are forwarded to the type constructor. -""" -Gene(id; kwargs...) = Gene(; id, kwargs...) diff --git a/src/types/Isozyme.jl b/src/types/Isozyme.jl deleted file mode 100644 index 13ec2d555..000000000 --- a/src/types/Isozyme.jl +++ /dev/null @@ -1,25 +0,0 @@ -""" -$(TYPEDEF) - -Information about isozyme composition and annotations justifying the -stoichiometry or turnover numbers. - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct Isozyme - gene_product_stoichiometry::Dict{String,Float64} - kcat_forward::Maybe{Float64} = nothing - kcat_backward::Maybe{Float64} = nothing - annotation::Annotations = Annotations() -end - -""" -$(TYPEDSIGNATURES) - -A convenience constructor for [`Isozyme`](@ref) that takes a string gene -reaction rule and converts it into the appropriate format. Assumes the -`gene_product_stoichiometry` for each subunit is 1. -""" -Isozyme(gids::Vector{String}; kwargs...) = - Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) diff --git a/src/types/Metabolite.jl b/src/types/Metabolite.jl deleted file mode 100644 index 445be768a..000000000 --- a/src/types/Metabolite.jl +++ /dev/null @@ -1,25 +0,0 @@ -""" -$(TYPEDEF) - -# Fields -$(TYPEDFIELDS) - -Default constructor requires the `id` keyword to be assigned. -""" -Base.@kwdef mutable struct Metabolite - id::String - name::Maybe{String} = nothing - formula::Maybe{String} = nothing - charge::Maybe{Int} = nothing - compartment::Maybe{String} = nothing - notes::Notes = Notes() - annotations::Annotations = Annotations() -end - -""" -$(TYPEDSIGNATURES) - -A convenience constructor for [`Metabolite`](@ref) taking as first argument the id -of the metabolite. All other kwargs are forwarded to the type constructor. -""" -Metabolite(id; kwargs...) = Metabolite(; id, kwargs...) diff --git a/src/types/Reaction.jl b/src/types/Reaction.jl deleted file mode 100644 index 4824eb9ed..000000000 --- a/src/types/Reaction.jl +++ /dev/null @@ -1,83 +0,0 @@ -""" -$(TYPEDEF) - -A structure for representing a single reaction in a [`ObjectModel`](@ref). - -# Fields -$(TYPEDFIELDS) - -Default constructor requires the `id` keyword to be assigned. -""" -Base.@kwdef mutable struct Reaction - id::String - name::Maybe{String} = nothing - metabolites::Dict{String,Float64} = Dict{String,Float64}() - lower_bound::Float64 = -constants.default_reaction_bound - upper_bound::Float64 = constants.default_reaction_bound - gene_associations::Maybe{Vector{Isozyme}} = nothing - subsystem::Maybe{String} = nothing - notes::Notes = Notes() - annotations::Annotations = Annotations() -end - -""" -$(TYPEDSIGNATURES) - -A convenience constructor for [`Reaction`](@ref) taking as first argument the id -of the reaction. All other kwargs are forwarded to the type constructor. -""" -Reaction(id; kwargs...) = Reaction(; id, kwargs...) - -""" -$(TYPEDSIGNATURES) - -Convenience constructor for `Reaction` that generates a reaction constrained to -carry flux only in the forward direction relative to the `metabolites`, which is -a dictionary mapping metabolite ids to stoichiometric coefficients. The -`default_bound` is the value taken to mean infinity in the context of constraint -based models, often this is set to a very high flux value like 1000. - -See also: [`Reaction`](@ref), [`ReactionBackward`](@ref), [`ReactionBidirectional`](@ref) -""" -ReactionForward(id::String, metabolites; default_bound = constants.default_reaction_bound) = - Reaction(id; metabolites = metabolites, lower_bound = 0.0, upper_bound = default_bound) - -""" -$(TYPEDSIGNATURES) - -Convenience constructor for `Reaction` that generates a reaction constrained to -carry flux only in the backward direction relative to the `metabolites`, which is -a dictionary mapping metabolite ids to stoichiometric coefficients. The -`default_bound` is the value taken to mean infinity in the context of constraint -based models, often this is set to a very high flux value like 1000. - -See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBidirectional`](@ref) -""" -ReactionBackward( - id::String, - metabolites; - default_bound = constants.default_reaction_bound, -) = Reaction(id; metabolites = metabolites, lower_bound = -default_bound, upper_bound = 0.0) - -""" -$(TYPEDSIGNATURES) - -Convenience constructor for `Reaction` that generates a reaction constrained to -carry flux in both the forward and backward direction relative to the -`metabolites`, which is a dictionary mapping metabolite ids to stoichiometric -coefficients. The `default_bound` is the value taken to mean infinity in the -context of constraint based models, often this is set to a very high flux value -like 1000. - -See also: [`Reaction`](@ref), [`ReactionForward`](@ref), [`ReactionBackward`](@ref) -""" -ReactionBidirectional( - id::String, - metabolites; - default_bound = constants.default_reaction_bound, -) = Reaction( - id; - metabolites = metabolites, - lower_bound = -default_bound, - upper_bound = default_bound, -) diff --git a/src/types/ReactionStatus.jl b/src/types/ReactionStatus.jl deleted file mode 100644 index 90940711b..000000000 --- a/src/types/ReactionStatus.jl +++ /dev/null @@ -1,13 +0,0 @@ -""" -$(TYPEDEF) - -Used for concise reporting of modeling results. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct ReactionStatus - already_present::Bool - index::Int - info::String -end diff --git a/src/types/Result.jl b/src/types/Result.jl deleted file mode 100644 index 62025b87e..000000000 --- a/src/types/Result.jl +++ /dev/null @@ -1,43 +0,0 @@ -""" -$(TYPEDEF) - -A simple storage type for analysis result that is accompanied with the original -model. This vastly simplifies piping; e.g., instead of having to specify the -model in each part of the pipeline as such: -``` -flux_balance_analysis(m, optimizer) |> values_vec(m) -``` -...we can do: -``` -flux_balance_analysis(m, optimizer) |> values_vec -``` - -Optionally you may take out "just" the result value by piping through -[`result`](@ref), in this case obtaining a vector: -``` -... |> values_vec |> result -``` - -This additionally enables some situations where carrying the model around -manually would be hard or require multiple piping steps, such as: -``` -model |> with_some_reactions(...) |> - with_enzyme_constraints(...) |> - flux_balance_analysis(optimizer) |> - values_dict |> - result -""" -struct ModelWithResult{T} - model::AbstractMetabolicModel - result::T -end - -""" -$(TYPEDSIGNATURES) - -Pipeable shortcut for extracting the result value out of -[`ModelWithResult`](@ref). -""" -function result(x::ModelWithResult{T})::T where {T} - x.result -end diff --git a/src/types/abstract/AbstractMetabolicModel.jl b/src/types/abstract/AbstractMetabolicModel.jl deleted file mode 100644 index 53f26f6cf..000000000 --- a/src/types/abstract/AbstractMetabolicModel.jl +++ /dev/null @@ -1,69 +0,0 @@ - -""" - abstract type AbstractMetabolicModel end - -A helper supertype of everything usable as a linear-like model for COBREXA -functions. - -If you want your model type to work with COBREXA, add the `AbstractMetabolicModel` as -its supertype, and implement the accessor functions. Accessors -[`variables`](@ref), [`metabolites`](@ref), [`stoichiometry`](@ref), -[`bounds`](@ref) and [`objective`](@ref) must be implemented; others are not -mandatory and default to safe "empty" values. -""" -abstract type AbstractMetabolicModel end - -""" - abstract type AbstractModelWrapper <: AbstractMetabolicModel end - -A helper supertype of all "wrapper" types that contain precisely one other -[`AbstractMetabolicModel`](@ref). -""" -abstract type AbstractModelWrapper <: AbstractMetabolicModel end - -const SparseMat = SparseMatrixCSC{Float64,Int} -const SparseVec = SparseVector{Float64,Int} -const MatType = AbstractMatrix{Float64} -const VecType = AbstractVector{Float64} -const StringVecType = AbstractVector{String} - -""" - MetaboliteFormula = Dict{String,Int} - -Dictionary of atoms and their abundances in a molecule. -""" -const MetaboliteFormula = Dict{String,Int} - -""" - Annotations = Dict{String,Vector{String}} - -Dictionary used to store (possible multiple) standardized annotations of -something, such as a [`Metabolite`](@ref) and a [`Reaction`](@ref). - -# Example -``` -Annotations("PubChem" => ["CID12345", "CID54321"]) -``` -""" -const Annotations = Dict{String,Vector{String}} - -""" - Notes = Dict{String,Vector{String}} - -Free-form notes about something (e.g. a [`Gene`](@ref)), categorized by -"topic". -""" -const Notes = Dict{String,Vector{String}} - -""" - GeneAssociationsDNF = Vector{Vector{String}} - -Disjunctive normal form of simple gene associations. For example, `[[A, B], -[B]]` represents two isozymes where the first requires both genes `A` and `B`, -while the second isozyme only requires gene `C`. - -This string representation is typically used to represent gene reaction rules, -but does not contain any subunit stoichiometry of kinetic information of the -isozymes. See [`Isozyme`}(@ref) for a more complete structure. -""" -const GeneAssociationsDNF = Vector{Vector{String}} diff --git a/src/types/abstract/Maybe.jl b/src/types/abstract/Maybe.jl deleted file mode 100644 index 8aef5b515..000000000 --- a/src/types/abstract/Maybe.jl +++ /dev/null @@ -1,7 +0,0 @@ - -""" - Maybe{T} = Union{Nothing, T} - -A nice name for a "nullable" type. -""" -const Maybe{T} = Union{Nothing,T} diff --git a/src/types/accessors/AbstractMetabolicModel.jl b/src/types/accessors/AbstractMetabolicModel.jl deleted file mode 100644 index 6d5c240b7..000000000 --- a/src/types/accessors/AbstractMetabolicModel.jl +++ /dev/null @@ -1,486 +0,0 @@ - -# -# IMPORTANT -# -# This file provides a list of "officially supported" accessors that should -# work with all subtypes of [`AbstractMetabolicModel`](@ref). -# -# Keep this synced with the automatically derived methods for -# [`AbstractModelWrapper`](@ref). -# - -""" -$(TYPEDSIGNATURES) - -Return a vector of variable identifiers in a model. The vector precisely -corresponds to the columns in [`stoichiometry`](@ref) matrix. - -Usually, variables correspond to reactions. However, for technical reasons, the -reactions may sometimes not be true reactions, but various virtual and helper -pseudo-reactions that are used in the metabolic modeling, such as metabolite -exchanges, separate forward and reverse reactions, supplies of enzymatic and -genetic material and virtual cell volume, etc. To simplify the view of the model -contents use [`reaction_variables`](@ref). -""" -function variables(a::AbstractMetabolicModel)::Vector{String} - missing_impl_error(variables, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of reactions in a model. -""" -function n_variables(a::AbstractMetabolicModel)::Int - length(variables(a)) -end - -""" -$(TYPEDSIGNATURES) - -Return a vector of metabolite identifiers in a model. The vector precisely -corresponds to the rows in [`stoichiometry`](@ref) matrix. - -As with [`variables`](@ref)s, some metabolites in models may be virtual, -representing purely technical equality constraints. -""" -function metabolites(a::AbstractMetabolicModel)::Vector{String} - missing_impl_error(metabolites, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of metabolites in a model. -""" -function n_metabolites(a::AbstractMetabolicModel)::Int - length(metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse stoichiometry matrix of a model. -""" -function stoichiometry(a::AbstractMetabolicModel)::SparseMat - missing_impl_error(stoichiometry, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the lower and upper solution bounds of a model. -""" -function bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - missing_impl_error(bounds, (a,)) -end - -""" -$(TYPEDSIGNATURES) - -Get the sparse balance vector of a model. -""" -function balance(a::AbstractMetabolicModel)::SparseVec - return spzeros(n_metabolites(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the linear objective vector or the quadratic objective affine matrix of the -model. - -Analysis functions, such as [`flux_balance_analysis`](@ref), are supposed to -maximize `objective' * x` where `x` is a feasible solution of the model (in -case the objective is a sparse vector), or `x' * objective * [x; 1]` (in -case the objective is a sparse matrix). - -The objective matrix is extended by 1 column to allow affine quadratic form -objectives. Symmetry is not required. - -Use [`negative_squared_distance_objective`](@ref) to simplify creation of the -common nearest-feasible-solution objectives. -""" -function objective(a::AbstractMetabolicModel)::Union{SparseVec,SparseMat} - missing_impl_error(objective, (a,)) -end - -@make_variable_semantics( - :reaction, - "reaction fluxes", - """ -Typically, one variable describes one reaction flux; but in specific cases the -variables may have separate meanings (as with enzyme supply reactions and -various helper values), and multiple variables may combine into one reaction -flux, such as with separate bidirectional reactions. -""" -) - -@make_variable_semantics( - :enzyme, - "enzyme supplies", - """ -Certain model types define a supply of enzymes that is typically required to -"run" the reactions; enzyme supplies define variables that are used to model -these values. -""" -) - -@make_variable_semantics( - :enzyme_group, - "enzyme group", - """ -Certain model types use enzymes to catalyze reactions. These enzymes typically -have capacity limitations (e.g. membrane or cytosol density constraints). Enzyme -groups collect these sets of enzymes for convenient analysis. -""" -) - -@make_variable_semantics( - :metabolite_log_concentration, - "metabolite log concentration", - """ -Certain model types use metabolite concentrations instead of reaction fluxes are -variables. This semantic grouping uses the log (base e) metabolite concentration -to make thermodynamic calculations easier. -""" -) - -@make_variable_semantics( - :gibbs_free_energy_reaction, - "Gibbs free energy of reaction", - """ -Some thermodynamic models need to ensure that the ΔG of each reaction is -negative (2nd law of thermodynamics). This semantic grouping represents ΔGᵣ. -""" -) - -@make_variable_semantics( - :environmental_exchange, - "Environmental exchange reaction", - """ -Community models are composed of member models as well as environmental exchange -reactions. This semantic grouping represents the environmental exchange -reactions. -""" -) - -""" -$(TYPEDSIGNATURES) - -Get a matrix of coupling constraint definitions of a model. By default, there -is no coupling in the models. -""" -function coupling(a::AbstractMetabolicModel)::SparseMat - return spzeros(0, n_variables(a)) -end - -""" -$(TYPEDSIGNATURES) - -Get the number of coupling constraints in a model. -""" -function n_coupling_constraints(a::AbstractMetabolicModel)::Int - size(coupling(a), 1) -end - -""" -$(TYPEDSIGNATURES) - -Get the lower and upper bounds for each coupling bound in a model, as specified -by `coupling`. By default, the model does not have any coupling bounds. -""" -function coupling_bounds(a::AbstractMetabolicModel)::Tuple{Vector{Float64},Vector{Float64}} - return (spzeros(0), spzeros(0)) -end - -""" -$(TYPEDSIGNATURES) - -Return identifiers of all genes contained in the model. By default, there are -no genes. - -In SBML, these are usually called "gene products" but we write `genes` for -simplicity. -""" -function genes(a::AbstractMetabolicModel)::Vector{String} - return [] -end - -""" -$(TYPEDSIGNATURES) - -Return the number of genes in the model (as returned by [`genes`](@ref)). If -you just need the number of the genes, this may be much more efficient than -calling [`genes`](@ref) and measuring the array. -""" -function n_genes(a::AbstractMetabolicModel)::Int - return length(genes(a)) -end - -""" -$(TYPEDSIGNATURES) - -Returns the sets of genes that need to be present so that the reaction can work -(technically, a DNF on gene availability, with positive atoms only). - -For simplicity, `nothing` may be returned, meaning that the reaction always -takes place. (in DNF, that would be equivalent to returning `[[]]`.) -""" -function reaction_gene_associations( - a::AbstractMetabolicModel, - reaction_id::String, -)::Maybe{GeneAssociationsDNF} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Directly evaluate the reaction-gene-association from the data available in the -model. The default implementation evaluates the GRR over the DNF formula -available from [`reaction_gene_associations`](@ref), which may be detrimental -in models where the DNF formula gets exceedingly big while it is in fact not -required for the given analysis, such as for calculating the gene knockouts. In -such cases, users may provide overload of -[`eval_reaction_gene_association`](@ref) to skip the DNF conversion. - -The evaluation takes the first set of gene identifiers of `falses` and `trues` -that isn't `nothing` and considers these to be evaluated as such, while all -other identifiers form the complement with the negated evaluation; at least one -must be supplied. -""" -function eval_reaction_gene_association( - a::AbstractMetabolicModel, - reaction_id::String; - falses::Maybe{AbstractSet{String}} = nothing, - trues::Maybe{AbstractSet{String}} = nothing, -) - isnothing(falses) || return Types.Internal.maybemap( - grr -> any(!any(in(falses), clause) for clause in grr), - reaction_gene_associations(a, reaction_id), - ) - - isnothing(trues) || return Types.Internal.maybemap( - grr -> any(all(in(trues), clause) for clause in grr), - reaction_gene_associations(a, reaction_id), - ) - - throw(ArgumentError("at least one of 'falses' and 'trues' must be specified")) -end - -""" -$(TYPEDSIGNATURES) - -Return the subsystem of reaction `reaction_id` in `model` if it is assigned. If not, -return `nothing`. -""" -function reaction_subsystem( - model::AbstractMetabolicModel, - reaction_id::String, -)::Maybe{String} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the stoichiometry of reaction with ID `rid` in the model. The dictionary -maps the metabolite IDs to their stoichiometric coefficients. -""" -function reaction_stoichiometry( - m::AbstractMetabolicModel, - rid::String, -)::Dict{String,Float64} - mets = metabolites(m) - Dict( - mets[k] => v for - (k, v) in zip(findnz(stoichiometry(m)[:, first(indexin([rid], variables(m)))])...) - ) -end - -""" -$(TYPEDSIGNATURES) - -Return the formula of metabolite `metabolite_id` in `model`. -Return `nothing` in case the formula is not known or irrelevant. -""" -function metabolite_formula( - model::AbstractMetabolicModel, - metabolite_id::String, -)::Maybe{MetaboliteFormula} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the charge associated with metabolite `metabolite_id` in `model`. -Returns `nothing` if charge not present. -""" -function metabolite_charge(model::AbstractMetabolicModel, metabolite_id::String)::Maybe{Int} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the compartment of metabolite `metabolite_id` in `model` if it is assigned. If not, -return `nothing`. -""" -function metabolite_compartment( - model::AbstractMetabolicModel, - metabolite_id::String, -)::Maybe{String} - return nothing -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that may help identifying the reaction. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"Reactome" => ["reactomeID123"]`. -""" -function reaction_annotations(a::AbstractMetabolicModel, reaction_id::String)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that may help to reliably identify the metabolite. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"ChEMBL" => ["123"]` or `"PubChem" => ["CID123", "CID654645645"]`. -""" -function metabolite_annotations( - a::AbstractMetabolicModel, - metabolite_id::String, -)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return standardized names that identify the corresponding gene or product. The -dictionary assigns vectors of possible identifiers to identifier system names, -e.g. `"PDB" => ["PROT01"]`. -""" -function gene_annotations(a::AbstractMetabolicModel, gene_id::String)::Annotations - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with reaction `reaction_id` in `model`. -""" -function reaction_notes(model::AbstractMetabolicModel, reaction_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with metabolite `reaction_id` in `model`. -""" -function metabolite_notes(model::AbstractMetabolicModel, metabolite_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with the gene `gene_id` in `model`. -""" -function gene_notes(model::AbstractMetabolicModel, gene_id::String)::Notes - return Dict() -end - -""" -$(TYPEDSIGNATURES) - -Return the name of reaction with ID `rid`. -""" -reaction_name(model::AbstractMetabolicModel, rid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the name of metabolite with ID `mid`. -""" -metabolite_name(model::AbstractMetabolicModel, mid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Return the name of gene with ID `gid`. -""" -gene_name(model::AbstractMetabolicModel, gid::String) = nothing - -""" -$(TYPEDSIGNATURES) - -Do whatever is feasible to get the model into a state that can be read from -as-quickly-as-possible. This may include e.g. generating helper index -structures and loading delayed parts of the model from disk. The model should -be modified "transparently" in-place. Analysis functions call this right before -applying modifications or converting the model to the optimization model using -[`make_optimization_model`](@ref); usually on the same machine where the -optimizers (and, generally, the core analysis algorithms) will run. The calls -are done in a good hope that the performance will be improved. - -By default, it should be safe to do nothing. -""" -function precache!(a::AbstractMetabolicModel)::Nothing - nothing -end - -""" -$(TYPEDSIGNATURES) - -Return the molar mass of translated gene with ID `gid`. -""" -gene_product_molar_mass(model::AbstractMetabolicModel, gid::String)::Maybe{Float64} = - nothing - -""" -$(TYPEDSIGNATURES) - -Return all the [`Isozyme`](@ref)s associated with a `model` and reaction `rid`. -""" -reaction_isozymes(model::AbstractMetabolicModel, rid::String)::Maybe{Vector{Isozyme}} = - nothing - -""" -$(TYPEDSIGNATURES) - -Return the lower bound of the gene product concentration associated with the `model` and gene `gid`. -""" -gene_product_lower_bound(model::AbstractMetabolicModel, gid::String)::Float64 = 0.0 - -""" -$(TYPEDSIGNATURES) - -Return the upper bound of the gene product concentration associated with the `model` and gene `gid`. -""" -gene_product_upper_bound(model::AbstractMetabolicModel, gid::String)::Float64 = - constants.default_gene_product_bound - -""" -$(TYPEDSIGNATURES) - -Return the notes associated with a `model`. At minimum this should include the -model authors, contact information, and DOI of the associated publication. -""" -model_notes(model::AbstractMetabolicModel)::Notes = Dict() - -""" -$(TYPEDSIGNATURES) - -Return the annotations associated with a `model`. Typically, these should be -encoded in MIRIAM format. At minimum it should include the full species name -with relevant identifiers, taxonomy ID, strain ID, and URL to the genome. -""" -model_annotations(model::AbstractMetabolicModel)::Annotations = Dict() diff --git a/src/types/accessors/ModelWrapper.jl b/src/types/accessors/ModelWrapper.jl deleted file mode 100644 index 2c687f616..000000000 --- a/src/types/accessors/ModelWrapper.jl +++ /dev/null @@ -1,27 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -A simple helper to pick a single wrapped model -""" -function unwrap_model(a::AbstractModelWrapper) - missing_impl_error(unwrap_model, (a,)) -end - -# -# IMPORTANT -# -# The list of inherited functions must be synced with the methods available for -# [`AbstractMetabolicModel`](@ref). -# - -@inherit_model_methods_fn AbstractModelWrapper () unwrap_model () variables metabolites stoichiometry bounds balance objective coupling n_coupling_constraints coupling_bounds genes n_genes precache! model_notes model_annotations - -@inherit_model_methods_fn AbstractModelWrapper (rid::String,) unwrap_model (rid,) reaction_gene_associations reaction_subsystem reaction_stoichiometry reaction_annotations reaction_notes reaction_isozymes - -eval_reaction_gene_association(w::AbstractModelWrapper, rid::String; kwargs...) = - eval_reaction_gene_association(unwrap_model(w), rid; kwargs...) - -@inherit_model_methods_fn AbstractModelWrapper (mid::String,) unwrap_model (mid,) metabolite_formula metabolite_charge metabolite_compartment metabolite_annotations metabolite_notes - -@inherit_model_methods_fn AbstractModelWrapper (gid::String,) unwrap_model (gid,) gene_annotations gene_notes gene_product_molar_mass gene_product_lower_bound gene_product_upper_bound diff --git a/src/types/accessors/bits/missing_impl.jl b/src/types/accessors/bits/missing_impl.jl deleted file mode 100644 index e9dbbd4df..000000000 --- a/src/types/accessors/bits/missing_impl.jl +++ /dev/null @@ -1,2 +0,0 @@ - -missing_impl_error(m, a) = throw(MethodError(m, a)) diff --git a/src/types/accessors/bits/semantics.jl b/src/types/accessors/bits/semantics.jl deleted file mode 100644 index 7a3d7077f..000000000 --- a/src/types/accessors/bits/semantics.jl +++ /dev/null @@ -1,241 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A helper function to quickly create a sparse matrix from a dictionary that -describes it. Reverse of [`make_mapping_dict`](@ref). -""" -function make_mapping_mtx( - vars::Vector{String}, - semantics::Vector{String}, - var_sem_val::Dict{String,Dict{String,Float64}}, -)::Types.SparseMat - rowidx = Dict(vars .=> 1:length(vars)) - colidx = Dict(semantics .=> 1:length(semantics)) - n = sum(length.(values(var_sem_val))) - R = Vector{Int}(undef, n) - C = Vector{Int}(undef, n) - V = Vector{Float64}(undef, n) - i = 1 - for (cid, col_val) in var_sem_val - for (rid, val) in col_val - R[i] = rowidx[rid] - C[i] = colidx[cid] - V[i] = val - i += 1 - end - end - sparse(R, C, V, length(vars), length(semantics)) -end - -""" -$(TYPEDSIGNATURES) - -A helper function to quickly create a sparse matrix from a dictionary that -describes it. Reverse of [`make_mapping_mtx`](@ref). -""" -function make_mapping_dict( - vars::Vector{String}, - semantics::Vector{String}, - mtx::Types.SparseMat, -)::Dict{String,Dict{String,Float64}} - Dict( - sid => Dict(vars[vidx] => val for (vidx, val) in zip(findnz(mtx[:, sidx])...)) for - (sidx, sid) in enumerate(semantics) - ) -end - -const Semantic = Tuple{Function,Function,Function,Function} - -const variable_semantics = Dict{Symbol,Semantic}() - -""" -$(TYPEDSIGNATURES) - -Get a tuple of functions that work with the given semantics, or `nothing` if -the semantics doesn't exist. -""" -function get_semantics(semantics::Symbol)::Types.Maybe{Semantic} - get(variable_semantics, semantics, nothing) -end - -""" -$(TYPEDSIGNATURES) - -Like [`get_semantics`](@ref) but throws a `DomainError` if the semantics is not -available. -""" -function semantics(semantics::Symbol)::Semantic - res = get_semantics(semantics) - isnothing(res) && throw(DomainError(semantics, "unknown semantics")) - res -end - -""" -$(TYPEDSIGNATURES) - -Inject a new functionality for variable semantics defined by `sym` into -`themodule` (which should ideally be COBREXA.Accessors). - -`name` is a human readable description of the semantic object. `example` is a -string that closer describes the semantics, which is inserted into the main -semantic-accessing function. -""" -function make_variable_semantics( - themodule::Module, - source, - sym::Symbol, - name::String, - example::String, -) - haskey(themodule.Internal.variable_semantics, sym) && return - - plural = Symbol(sym, :s) - count = Symbol(:n_, plural) - mapping = Symbol(sym, :_variables) - mapping_mtx = Symbol(sym, :_variables_matrix) - - pluralfn = Expr( - :macrocall, - Symbol("@doc"), - source, - Expr( - :string, - :TYPEDSIGNATURES, - """ - -List the semantics for model variables that can be interpreted as $name. - -$example - -Use [`$count`](@ref) to quickly determine the amount of $name in the -model. See the documentation of [`$mapping`](@ref) for closer -definition of the correspondence of $name and model variables. -""", - ), - :(function $plural(a::AbstractMetabolicModel)::Vector{String} - String[] - end), - ) - - countfn = Expr( - :macrocall, - Symbol("@doc"), - source, - Expr( - :string, - :TYPEDSIGNATURES, - """ - -Count of $name that the model describes, should be equal to the length of -vector returned by [`$plural`]. -""", - ), - :(function $count(a::AbstractMetabolicModel)::Int - 0 - end), - ) - - mappingfn = Expr( - :macrocall, - Symbol("@doc"), - source, - Expr( - :string, - :TYPEDSIGNATURES, - """ - -Bipartite mapping of $name described by the model to the actual -variables in the model. Returns a dictionary of $name assigned to the -variable IDs and their linear coefficients. See the documentation of -[`$plural`](@ref) for semantics. - -To improve the performance, you may want to use [`$mapping_mtx`](@ref). -""", - ), - :(function $mapping(a::AbstractMetabolicModel)::Dict{String,Dict{String,Float64}} - Dict() - end), - ) - - mtxfn = Expr( - :macrocall, - Symbol("@doc"), - source, - Expr( - :string, - :TYPEDSIGNATURES, - """ - -Bipartite mapping of $name described by the model to the actual -variables in the model, described as a sparse matrix mapping with rows -corresponding to model variables and columns corresponding to $name. - -By default, this is derived from [`$mapping`](@ref) in all models. For -safety reasons, this is never automatically inherited by wrappers. -""", - ), - :(function $mapping_mtx(a::AbstractMetabolicModel)::SparseMat - make_mapping_mtx(variables(a), $plural(a), $mapping(a)) - end), - ) - - Base.eval.(Ref(themodule), [pluralfn, countfn, mappingfn, mtxfn]) - - Base.eval(themodule, :(function $plural(w::AbstractModelWrapper)::Vector{String} - $plural(unwrap_model(w)) - end)) - - Base.eval(themodule, :(function $count(w::AbstractModelWrapper)::Int - $count(unwrap_model(w)) - end)) - - Base.eval( - themodule, - :(function $mapping(w::AbstractModelWrapper)::Dict{String,Dict{String,Float64}} - $mapping(unwrap_model(w)) - end), - ) - - # TODO here we would normally also overload the matrix function, but that - # one will break once anyone touches variables of the models (which is - # common). We should have a macro like @model_does_not_modify_variable_set - # that adds the overloads. Or perhaps AbstractModelWrapperWithSameVariables? - # - # The same probably goes for other semantics; - # AbstractModelWrapperThatOnlyTouchesSemantics(...) ? (Which has an - # alternative in forcing people to overload all semantic functions in all - # cases of adding semantics, which might actually be the right way.) - - themodule.Internal.variable_semantics[sym] = - Base.eval.(Ref(themodule), (plural, count, mapping, mapping_mtx)) -end - -""" -$(TYPEDSIGNATURES) - -Convenience macro for running [`make_variable_semantics`](@ref). -""" -macro make_variable_semantics(sym, name, example) - src = __source__ - quote - $make_variable_semantics($Accessors, $src, $sym, $name, $example) - end -end - -""" -$(TYPEDSIGNATURES) - -Convenience helper -- many models carry no other variable semantics than the -reactions; this macro declares precisely the same about the model type. -""" -macro all_variables_are_reactions(mt) - m = esc(mt) - quote - $Accessors.reactions(model::$m) = $Accessors.variables(model) - $Accessors.n_reactions(model::$m) = $Accessors.n_variables(model) - $Accessors.reaction_variables(model::$m) = - Dict(var => Dict(var => 1.0) for var in $Accessors.variables(model)) - $Accessors.reaction_variables_matrix(model::$m) = - $SparseArrays.spdiagm(fill(1.0, $Accessors.n_variables(model))) - end -end diff --git a/src/types/misc/CommunityModel.jl b/src/types/misc/CommunityModel.jl deleted file mode 100644 index 6e119eeb5..000000000 --- a/src/types/misc/CommunityModel.jl +++ /dev/null @@ -1,136 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`CommunityModel`](@ref) -""" -Base.copy(m::CommunityModel) = CommunityModel(; - members = m.members, - abundances = m.abundances, - environmental_links = m.environmental_links, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`CommunityModel`](@ref) -""" -Base.copy(m::CommunityMember) = CommunityMember(; - id = m.id, - model = m.id, - exchange_reaction_ids = m.exchange_reaction_ids, - biomass_reaction_id = m.biomass_reaction_id, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`EnvironmentalLink`](@ref) -""" -Base.copy(m::EnvironmentalLink) = EnvironmentalLink(; - reaction_id = m.reaction_id, - metabolite_id = m.metabolite_id, - lower_bound = m.lower_bound, - upper_bound = m.upper_bound, -) - -""" -$(TYPEDSIGNATURES) - -A helper function that creates an exchange/environmental variable linking matrix -for community member `m`. -""" -function environment_exchange_stoichiometry( - m::CommunityMember, - env_mets::Vector{String}, - env_rxns::Vector{String}, -) - idxs = [ - (i, j) for - (i, j) in enumerate(indexin(env_rxns, variables(m.model))) if !isnothing(j) - ] - sparse( - first.(idxs), - last.(idxs), - ones(length(idxs)), - length(env_mets), - n_variables(m.model), - ) -end - -""" -$(TYPEDSIGNATURES) - -A helper function that creates the entire exchange/environmental variable -linking matrix for a community model. -""" -function environment_exchange_stoichiometry(cm::CommunityModel) - env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] - env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] - - hcat( - [ - environment_exchange_stoichiometry(m, env_mets, env_rxns) .* a for - (m, a) in zip(values(cm.members), cm.abundances) - ]..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Variant of [`environment_exchange_stoichiometry`](@ref) that takes an explicit abundance matrix (used -in solver modifications.) -""" -function environment_exchange_stoichiometry(cm::CommunityModel, abundances) - env_mets = [envlink.metabolite_id for envlink in cm.environmental_links] - env_rxns = [envlink.reaction_id for envlink in cm.environmental_links] - - hcat( - [ - environment_exchange_stoichiometry(m, env_mets, env_rxns) .* a for - (m, a) in zip(values(cm.members), abundances) - ]..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A helper function to find the index of the appropriate model. Assumes each `id` -is delimited by `#` that separates the model ID prefix and the original id. -""" -function access_community_member( - cm::CommunityModel, - delim_id::String, - accessor::Function; - delim = "#", - default = nothing, -) - modelid_nameid = string.(split(delim_id, delim)) - length(modelid_nameid) == 1 && return default # accessor default - - modelid = first(modelid_nameid) - nameid = modelid_nameid[2] # TODO deal with delimiters better - - accessor(cm.members[modelid].model, nameid) -end - -""" -$(TYPEDSIGNATURES) - -A helper function to build the `names_lookup` dictionary for a -[`CommunityModel`](@ref). -""" -function build_community_name_lookup( - members::OrderedDict{String,CommunityMember}; - delim = "#", -) - accessors = [variables, reactions, metabolites, genes] - Dict( - id => Dict( - Symbol(accessor) => - Dict(k => id * delim * k for k in accessor(member.model)) for - accessor in accessors - ) for (id, member) in members - ) -end diff --git a/src/types/misc/ObjectModel.jl b/src/types/misc/ObjectModel.jl deleted file mode 100644 index 3b3ee06de..000000000 --- a/src/types/misc/ObjectModel.jl +++ /dev/null @@ -1,69 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`ObjectModel`](@ref) -""" -Base.copy(m::ObjectModel) = ObjectModel( - reactions = m.reactions, - metabolites = m.metabolites, - genes = m.genes, - objective = m.objective, - notes = m.notes, - annotations = m.annotations, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Reaction`](@ref) -""" -Base.copy(r::Reaction) = Reaction( - r.id; - metabolites = r.metabolites, - lower_bound = r.lower_bound, - upper_bound = r.upper_bound, - gene_associations = r.gene_associations, - subsystem = r.subsystem, - notes = r.notes, - annotations = r.annotations, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Metabolite`](@ref) -""" -Base.copy(m::Metabolite) = Metabolite( - m.id; - formula = m.formula, - charge = m.charge, - compartment = m.compartment, - notes = m.notes, - annotations = m.annotations, -) - -""" -$(TYPEDSIGNATURES) - -Shallow copy of a [`Gene`](@ref) -""" -Base.copy(g::Gene) = Gene(g.id; notes = g.notes, annotations = g.annotations) - -""" -$(TYPEDSIGNATURES) - -Return the lower bounds for all reactions in `model`. -Order matches that of the reaction IDs returned by [`reactions`](@ref). -""" -lower_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].lower_bound for rxn in reactions(model)] - -""" -$(TYPEDSIGNATURES) - -Return the upper bounds for all reactions in `model`. -Order matches that of the reaction IDs returned in [`reactions`](@ref). -""" -upper_bounds(model::ObjectModel)::Vector{Float64} = - [model.reactions[rxn].upper_bound for rxn in reactions(model)] diff --git a/src/types/misc/chemical_formulas.jl b/src/types/misc/chemical_formulas.jl deleted file mode 100644 index f4682fc24..000000000 --- a/src/types/misc/chemical_formulas.jl +++ /dev/null @@ -1,26 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Parse a formula in format `C2H6O` into a [`MetaboliteFormula`](@ref), which is -basically a dictionary of atom counts in the molecule. -""" -function parse_formula(f::String)::MetaboliteFormula - res = Dict{String,Int}() - pattern = @r_str "([A-Z][a-z]*)([1-9][0-9]*)?" - - for m in eachmatch(pattern, f) - res[m.captures[1]] = isnothing(m.captures[2]) ? 1 : parse(Int, m.captures[2]) - end - - return res -end - -""" -$(TYPEDSIGNATURES) - -Format [`MetaboliteFormula`](@ref) to `String`. -""" -function unparse_formula(f::MetaboliteFormula)::String - return join(["$elem$n" for (elem, n) in f]) -end diff --git a/src/types/misc/gene_associations.jl b/src/types/misc/gene_associations.jl deleted file mode 100644 index 80c983285..000000000 --- a/src/types/misc/gene_associations.jl +++ /dev/null @@ -1,211 +0,0 @@ - -""" -$(TYPEDSIGNATURES) -Parse `SBML.GeneProductAssociation` structure and convert it to a strictly -positive DNF [`GeneAssociationsDNF`](@ref). Negation (`SBML.GPANot`) is not -supported. -""" -function parse_grr(gpa::SBML.GeneProductAssociation)::GeneAssociationsDNF - - function fold_and(dnfs::Vector{Vector{Vector{String}}})::Vector{Vector{String}} - if isempty(dnfs) - [String[]] - else - unique(unique(String[l; r]) for l in dnfs[1] for r in fold_and(dnfs[2:end])) - end - end - - dnf(x::SBML.GPARef) = [[x.gene_product]] - dnf(x::SBML.GPAOr) = collect(Set(vcat(dnf.(x.terms)...))) - dnf(x::SBML.GPAAnd) = fold_and(dnf.(x.terms)) - dnf(x) = throw( - DomainError( - x, - "unsupported gene product association contents of type $(typeof(x))", - ), - ) - return dnf(gpa) -end - -""" -$(TYPEDSIGNATURES) - -Evaluate the SBML `GeneProductAssociation` in the same way as -[`Accessors.eval_reaction_gene_association`](@ref). -""" -function eval_grr( - gpa::SBML.GeneProductAssociation; - falses::Maybe{AbstractSet{String}} = nothing, - trues::Maybe{AbstractSet{String}} = nothing, -)::Bool - @assert !(isnothing(falses) && isnothing(trues)) "at least one of 'trues' and 'falses' must be defined" - - e(x::SBML.GeneProductAssociation) = - throw(DomainError(x, "evaluating unsupported GeneProductAssociation contents")) - e(x::SBML.GPARef) = - !isnothing(falses) ? !(x.gene_product in falses) : (x.gene_product in trues) - e(x::SBML.GPAAnd) = all(e, x.terms) - e(x::SBML.GPAOr) = any(e, x.terms) - - e(gpa) -end - -""" -$(TYPEDSIGNATURES) - -Convert [`GeneAssociationsDNF`](@ref) to the corresponding `SBML.jl` structure. -""" -function unparse_grr( - ::Type{SBML.GeneProductAssociation}, - x::GeneAssociationsDNF, -)::SBML.GeneProductAssociation - SBML.GPAOr([SBML.GPAAnd([SBML.GPARef(j) for j in i]) for i in x]) -end - -""" -$(TYPEDSIGNATURES) - -Parse a DNF gene association rule in format `(YIL010W and YLR043C) or (YIL010W -and YGR209C)` to `GeneAssociationsDNF`. Also accepts `OR`, `|`, `||`, `AND`, `&`, -and `&&`. - -# Example -``` -julia> parse_grr("(YIL010W and YLR043C) or (YIL010W and YGR209C)") -2-element Array{Array{String,1},1}: - ["YIL010W", "YLR043C"] - ["YIL010W", "YGR209C"] -``` -""" -parse_grr(s::String) = maybemap(parse_grr, parse_grr_to_sbml(s))::Maybe{GeneAssociationsDNF} - -""" -PikaParser grammar for stringy GRR expressions. -""" -const grr_grammar = begin - # characters that typically form the identifiers - isident(x::Char) = - isletter(x) || - isdigit(x) || - x == '_' || - x == '-' || - x == ':' || - x == '.' || - x == '\'' || - x == '[' || - x == ']' || - x == '\x03' # a very ugly exception for badly parsed MAT files - - # scanner helpers - eat(p) = m -> begin - last = 0 - for i in eachindex(m) - p(m[i]) || break - last = i - end - last - end - - # eat one of keywords - kws(w...) = m -> begin - last = eat(isident)(m) - m[begin:last] in w ? last : 0 - end - - PP.make_grammar( - [:expr], - PP.flatten( - Dict( - :space => PP.first(PP.scan(eat(isspace)), PP.epsilon), - :id => PP.scan(eat(isident)), - :orop => - PP.first(PP.tokens("||"), PP.token('|'), PP.scan(kws("OR", "or"))), - :andop => PP.first( - PP.tokens("&&"), - PP.token('&'), - PP.scan(kws("AND", "and")), - ), - :expr => PP.seq(:space, :orexpr, :space, PP.end_of_input), - :orexpr => PP.first( - :or => PP.seq(:andexpr, :space, :orop, :space, :orexpr), - :andexpr, - ), - :andexpr => PP.first( - :and => PP.seq(:baseexpr, :space, :andop, :space, :andexpr), - :baseexpr, - ), - :baseexpr => PP.first( - :id, - :parenexpr => PP.seq( - PP.token('('), - :space, - :orexpr, - :space, - PP.token(')'), - ), - ), - ), - Char, - ), - ) -end - -grr_grammar_open(m, _) = - m.rule == :expr ? Bool[0, 1, 0, 0] : - m.rule == :parenexpr ? Bool[0, 0, 1, 0, 0] : - m.rule in [:or, :and] ? Bool[1, 0, 0, 0, 1] : - m.rule in [:andexpr, :orexpr, :notexpr, :baseexpr] ? Bool[1] : - (false for _ in m.submatches) - -grr_grammar_fold(m, _, subvals) = - m.rule == :id ? SBML.GPARef(m.view) : - m.rule == :and ? SBML.GPAAnd([subvals[1], subvals[5]]) : - m.rule == :or ? SBML.GPAOr([subvals[1], subvals[5]]) : - m.rule == :parenexpr ? subvals[3] : - m.rule == :expr ? subvals[2] : isempty(subvals) ? nothing : subvals[1] - -""" -$(TYPEDSIGNATURES) - -Internal helper for parsing the string GRRs into SBML data structures. More -general than [`parse_grr`](@ref). -""" -function parse_grr_to_sbml(str::String)::Maybe{SBML.GeneProductAssociation} - all(isspace, str) && return nothing - tree = PP.parse_lex(grr_grammar, str) - match = PP.find_match_at!(tree, :expr, 1) - if match > 0 - return PP.traverse_match( - tree, - match, - open = grr_grammar_open, - fold = grr_grammar_fold, - ) - else - throw(DomainError(str, "cannot parse GRR")) - end -end - -""" -$(TYPEDSIGNATURES) - -Converts a nested string gene reaction array back into a gene reaction rule -string. - -# Example -``` -julia> unparse_grr(String, [["YIL010W", "YLR043C"], ["YIL010W", "YGR209C"]]) -"(YIL010W && YLR043C) || (YIL010W && YGR209C)" -``` -""" -function unparse_grr( - ::Type{String}, - grr::GeneAssociationsDNF; - and = " && ", - or = " || ", -)::String - return join(("(" * join(gr, and) * ")" for gr in grr), or) -end - -unparse_grr(::Type{String}, isozymes::AbstractVector{Isozyme}; kwargs...)::String = - unparse_grr(String, [collect(keys(iso.stoichiometry)) for iso in isozymes]; kwargs...) diff --git a/src/types/misc/maybe.jl b/src/types/misc/maybe.jl deleted file mode 100644 index ddb35b6eb..000000000 --- a/src/types/misc/maybe.jl +++ /dev/null @@ -1,18 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Fold the `Maybe{T}` down to `T` by defaulting. -""" -function default(d::T, x::Maybe{T})::T where {T} - isnothing(x) ? d : x -end - -""" -$(TYPEDSIGNATURES) - -Apply a function to `x` only if it is not `nothing`. -""" -function maybemap(f, x::Maybe)::Maybe - isnothing(x) ? nothing : f(x) -end diff --git a/src/types/models/CommunityModel.jl b/src/types/models/CommunityModel.jl deleted file mode 100644 index f35d8455c..000000000 --- a/src/types/models/CommunityModel.jl +++ /dev/null @@ -1,282 +0,0 @@ -""" -$(TYPEDEF) - -A standardized structure used to package models that can easily be combined into -a [`CommunityModel`](@ref). - -# Fields -$(TYPEDFIELDS) - -# Assumptions -1. Exchange reactions, *all* of which are idenitified in - `exchange_reaction_ids`, have the form: `A[external] ⟷ ∅` where `A` is a - metabolite. No other exchanges are allowed. This is not checked, but assumed. -2. There is only one biomass reaction in the model. -""" -Base.@kwdef mutable struct CommunityMember - "Underlying model." - model::AbstractMetabolicModel - "List of all exchange reactions in model." - exchange_reaction_ids::Vector{String} - "ID of biomass reaction." - biomass_reaction_id::String -end - -""" -$(TYPEDEF) - -A helper struct used to store the environmental linking information in a -[`CommunityModel`](@ref). The `reaction_id` is an environmental exchange -reaction ID, and `metabolite_id` is the associated metabolite ID. The -`lower_bound` and `upper_bound` are bounds on the flux of the reaction. - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct EnvironmentalLink - "Environmental exchange reaction ID." - reaction_id::String - "Environmental metabolite ID." - metabolite_id::String - "Exchange reaction lower bound." - lower_bound::Float64 - "Environmental reaction upper bound." - upper_bound::Float64 -end - -""" -$(TYPEDEF) - -A basic structure representing a community model. All `members` are connected -through `environmental_links`. The `members` is an `OrderedDict` mapping the -member ID to a [`CommunityMember`](@ref). The `environmental_links` is a vector -of [`EnvironmentalLink`](@ref)s. If a member model possesses any exchange -reaction in `environmental_links`, then it is connected to the associated -environmental exchange reaction. Only the reactions in `environmental_links` are -linked, any other boundary reaction is not constrained in the community model. - -This model structure stitches together individual member models with -environmental exchange reactions, but does not add any objective. Use the -community wrappers for this. The abundance of each member in the community -weights the environmental exchange balances: -``` -env_ex_met1 = abundance_1 * ex_met1_member_1 + abundance_2 * ex_met_member_2 + ... -``` -Thus, the environmental exchange reactions will have flux units normalized to -total biomass, but the fluxes of each member in the community will be normalized -to its own biomass. - -# Fields -$(TYPEDFIELDS) - -# Implementation notes -1. All reactions have the `id` of each respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. - Consequently, exchange reactions of the original model will look like - `species1#EX_...`. -2. All metabolites have the `id` of each respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -3. All genes have the `id` of the respective underlying - [`CommunityMember`](@ref) appended as a prefix with the delimiter `#`. -4. `environmental_links` is a superset of all exchange reactions contained in - each underlying member in the community. Only these reactions get joined to - each underlying model. -""" -Base.@kwdef mutable struct CommunityModel <: AbstractMetabolicModel - "Models making up the community (ID => model)." - members::OrderedDict{String,CommunityMember} - "Abundances of each community member." - abundances::Vector{Float64} - "Environmental exchange to model exchange linking structure." - environmental_links::Vector{EnvironmentalLink} - "A lookup table mapping: model IDs => accessor symbol => names => community names." - name_lookup::Dict{String,Dict{Symbol,Dict{String,String}}} = - build_community_name_lookup(members) -end - -function Accessors.variables(cm::CommunityModel) - rxns = [ - cm.name_lookup[id][:variables][vid] for (id, m) in cm.members for - vid in variables(m.model) - ] - env_exs = [envlink.reaction_id for envlink in cm.environmental_links] - return [rxns; env_exs] -end - -function Accessors.n_variables(cm::CommunityModel) - num_model_reactions = sum(n_variables(m.model) for m in values(cm.members)) - num_env_metabolites = length(cm.environmental_links) - return num_model_reactions + num_env_metabolites -end - -function Accessors.metabolites(cm::CommunityModel) - mets = [ - cm.name_lookup[id][:metabolites][mid] for (id, m) in cm.members for - mid in metabolites(m.model) - ] - return [mets; "ENV_" .* [envlink.metabolite_id for envlink in cm.environmental_links]] -end - -function Accessors.n_metabolites(cm::CommunityModel) - num_model_constraints = sum(n_metabolites(m.model) for m in values(cm.members)) - num_env_metabolites = length(cm.environmental_links) - return num_model_constraints + num_env_metabolites -end - -Accessors.genes(cm::CommunityModel) = - [cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in genes(m.model)] - -Accessors.n_genes(cm::CommunityModel) = sum(n_genes(m.model) for m in values(cm.members)) - -Accessors.balance(cm::CommunityModel) = [ - vcat([balance(m.model) for m in values(cm.members)]...) - spzeros(length(cm.environmental_links)) -] - -function Accessors.stoichiometry(cm::CommunityModel) - - model_S = blockdiag([stoichiometry(m.model) for m in values(cm.members)]...) - model_env = spzeros(size(model_S, 1), length(cm.environmental_links)) - - env_rows = environment_exchange_stoichiometry(cm) - env_link = spdiagm(sum(env_rows, dims = 2)[:]) - - return [ - model_S model_env - env_rows -env_link - ] -end - -function Accessors.bounds(cm::CommunityModel) - models_lbs = vcat([first(bounds(m.model)) for m in values(cm.members)]...) - models_ubs = vcat([last(bounds(m.model)) for m in values(cm.members)]...) - - env_lbs = [envlink.lower_bound for envlink in cm.environmental_links] - env_ubs = [envlink.upper_bound for envlink in cm.environmental_links] - - return ([models_lbs; env_lbs], [models_ubs; env_ubs]) -end - -Accessors.objective(cm::CommunityModel) = spzeros(n_variables(cm)) - -function Accessors.coupling(cm::CommunityModel) - coups = blockdiag([coupling(m.model) for m in values(cm.members)]...) - n = n_variables(cm) - return [coups spzeros(size(coups, 1), n - size(coups, 2))] -end - -Accessors.n_coupling_constraints(cm::CommunityModel) = - sum(n_coupling_constraints(m.model) for m in values(cm.members)) - -function Accessors.coupling_bounds(cm::CommunityModel) - lbs = vcat([first(coupling_bounds(m.model)) for m in values(cm.members)]...) - ubs = vcat([last(coupling_bounds(m.model)) for m in values(cm.members)]...) - return (lbs, ubs) -end - -function Accessors.reaction_variables(model::CommunityModel) - nlu_r(id, x) = model.name_lookup[id][:reactions][x] - nlu_v(id, x) = model.name_lookup[id][:variables][x] - r_v = Dict{String,Dict{String,Float64}}() - for (id, m) in model.members - r_v_m = reaction_variables(m.model) - for (k, v) in r_v_m - r_v[nlu_r(id, k)] = Dict(nlu_v(id, kk) => vv for (kk, vv) in v) - end - end - r_v -end - -Accessors.reactions(cm::CommunityModel) = [ - vcat( - [ - [cm.name_lookup[id][:reactions][rid] for rid in reactions(m.model)] for - (id, m) in cm.members - ]..., - ) - [envlink.reaction_id for envlink in cm.environmental_links] -] - -Accessors.n_reactions(cm::CommunityModel) = - sum(n_reactions(m.model) for m in values(cm.members)) + length(cm.environmental_links) - - -Accessors.environmental_exchange_variables(model::CommunityModel) = Dict( - rid => Dict(rid => 1.0) for - rid in [envlink.reaction_id for envlink in model.environmental_links] -) - -Accessors.environmental_exchanges(model::CommunityModel) = - [envlink.reaction_id for envlink in model.environmental_links] - -Accessors.n_environmental_exchanges(model::CommunityModel) = - length(model.environmental_links) - -function Accessors.enzyme_variables(model::CommunityModel) - nlu(id, x) = model.name_lookup[id][:genes][x] - e_v = Dict{String,Dict{String,Float64}}() - for (id, m) in model.members - e_v_m = enzyme_variables(m.model) - for (k, v) in e_v_m - e_v[nlu(id, k)] = Dict(nlu(id, kk) => vv for (kk, vv) in v) - end - end - e_v -end - -Accessors.enzymes(cm::CommunityModel) = - [cm.name_lookup[id][:genes][gid] for (id, m) in cm.members for gid in enzymes(m.model)] - -Accessors.n_enzymes(cm::CommunityModel) = sum(n_enzymes(m.model) for m in cm.members) - -""" -$(TYPEDSIGNATURES) - -Get a mapping of enzyme groups to variables. See [`enzyme_variables`](@ref). -""" -function Accessors.enzyme_group_variables(model::CommunityModel) - nlu(id, x) = model.name_lookup[id][:genes][x] - e_g_v = Dict{String,Dict{String,Float64}}() - for (id, m) in model.members - e_g_v_m = enzyme_group_variables(m.model) - for (k, v) in e_g_v_m - e_g_v[id*"#"*k] = Dict(nlu(id, kk) => vv for (kk, vv) in v) - end - end - e_g_v -end - -Accessors.enzyme_groups(cm::CommunityModel) = - [id * "#" * k for (id, m) in cm.members for k in enzyme_groups(m.model)] - -Accessors.n_enzyme_groups(cm::CommunityModel) = - sum(n_enzyme_groups(m.model) for m in cm.members) - -#= -This loops implements the rest of the accessors through access_community_member. -Since most of the environmental reactions are generated programmtically, they -will not have things like annotations etc. For this reason, these methods will -only work if they access something inside the community members. -=# -for (func, def) in ( - (:reaction_gene_associations, nothing), - (:reaction_subsystem, nothing), - (:reaction_stoichiometry, nothing), - (:metabolite_formula, nothing), - (:metabolite_charge, nothing), - (:metabolite_compartment, nothing), - (:reaction_annotations, Dict()), - (:metabolite_annotations, Dict()), - (:gene_annotations, Dict()), - (:reaction_notes, Dict()), - (:metabolite_notes, Dict()), - (:gene_notes, Dict()), - (:reaction_name, nothing), - (:metabolite_name, nothing), - (:gene_name, nothing), -) - @eval begin - Accessors.$func(cm::CommunityModel, id::String) = - access_community_member(cm, id, $func; default = $def) - end -end diff --git a/src/types/models/HDF5Model.jl b/src/types/models/HDF5Model.jl deleted file mode 100644 index 3fb8a20d3..000000000 --- a/src/types/models/HDF5Model.jl +++ /dev/null @@ -1,77 +0,0 @@ - -""" -$(TYPEDEF) - -A model that is stored in HDF5 format. The model data is never really pulled -into memory, but instead mmap'ed as directly as possible into the Julia -structures. This makes reading the `HDF5Model`s extremely fast, at the same -time the (uncached) `HDF5Model`s can be sent around efficiently among -distributed nodes just like [`Serialized`](@ref) models, provided the nodes -share a common storage. - -All HDF5Models must have the backing disk storage. To create one, use -[`save_h5_model`](@ref) or [`save_model`](@ref) with `.h5` file extension. To -create a temporary model that behaves like a model "in memory", save it to a -temporary file. For related reasons, you can not use `convert` models to -`HDF5Model` format, because the conversion would impliy having the model saved -somewhere. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct HDF5Model <: AbstractMetabolicModel - h5::Maybe{HDF5.File} - filename::String - - HDF5Model(filename::String) = new(nothing, filename) -end - -function Accessors.precache!(model::HDF5Model)::Nothing - if isnothing(model.h5) - model.h5 = h5open(model.filename, "r") - end - nothing -end - -function Accessors.n_variables(model::HDF5Model)::Int - precache!(model) - length(model.h5["reactions"]) -end - -function Accessors.variables(model::HDF5Model)::Vector{String} - precache!(model) - # TODO is there any reasonable method to mmap strings from HDF5? - read(model.h5["reactions"]) -end - -Accessors.Internal.@all_variables_are_reactions HDF5Model - -function Accessors.n_metabolites(model::HDF5Model)::Int - precache!(model) - length(model.h5["metabolites"]) -end - -function Accessors.metabolites(model::HDF5Model)::Vector{String} - precache!(model) - read(model.h5["metabolites"]) -end - -function Accessors.stoichiometry(model::HDF5Model)::SparseMat - precache!(model) - h5_read_sparse(SparseMat, model.h5["stoichiometry"]) -end - -function Accessors.bounds(model::HDF5Model)::Tuple{Vector{Float64},Vector{Float64}} - precache!(model) - (HDF5.readmmap(model.h5["lower_bounds"]), HDF5.readmmap(model.h5["upper_bounds"])) -end - -function Accessors.balance(model::HDF5Model)::SparseVec - precache!(model) - h5_read_sparse(SparseVec, model.h5["balance"]) -end - -function Accessors.objective(model::HDF5Model)::SparseVec - precache!(model) - h5_read_sparse(SparseVec, model.h5["objective"]) -end diff --git a/src/types/models/JSONModel.jl b/src/types/models/JSONModel.jl deleted file mode 100644 index 32393c252..000000000 --- a/src/types/models/JSONModel.jl +++ /dev/null @@ -1,265 +0,0 @@ -""" -$(TYPEDEF) - -A struct used to store the contents of a JSON model, i.e. a model read from a -file ending with `.json`. These model files typically store all the model data -in arrays of JSON objects (represented in Julia as vectors of dictionaries). - -Usually, not all of the fields of the input JSON can be easily represented when -converting to other models, care should be taken to avoid losing information. - -Direct work with the `json` structure is not very efficient; the model -structure therefore caches some of the internal structure in the extra fields. -The single-parameter [`JSONModel`](@ref) constructor creates these caches -correctly from the `json`. The model structure is designed as read-only, and -changes in `json` invalidate the cache. - -# Example -```` -model = load_json_model("some_model.json") -model.json # see the actual underlying JSON -variables(model) # see the list of reactions -```` - -# Fields -$(TYPEDFIELDS) -""" -struct JSONModel <: AbstractMetabolicModel - json::Dict{String,Any} - rxn_index::Dict{String,Int} - rxns::Vector{Any} - met_index::Dict{String,Int} - mets::Vector{Any} - gene_index::Dict{String,Int} - genes::Vector{Any} -end - -_json_rxn_name(r, i) = string(get(r, "id", "rxn$i")) -_json_met_name(m, i) = string(get(m, "id", "met$i")) -_json_gene_name(g, i) = string(get(g, "id", "gene$i")) - -JSONModel(json::Dict{String,Any}) = begin - rkey = guesskey(keys(json), constants.keynames.rxns) - isnothing(rkey) && throw(DomainError(keys(json), "JSON model has no reaction keys")) - rs = json[rkey] - - mkey = guesskey(keys(json), constants.keynames.mets) - ms = json[mkey] - isnothing(mkey) && throw(DomainError(keys(json), "JSON model has no metabolite keys")) - - gkey = guesskey(keys(json), constants.keynames.genes) - gs = isnothing(gkey) ? [] : json[gkey] - - JSONModel( - json, - Dict(_json_rxn_name(r, i) => i for (i, r) in enumerate(rs)), - rs, - Dict(_json_met_name(m, i) => i for (i, m) in enumerate(ms)), - ms, - Dict(_json_gene_name(g, i) => i for (i, g) in enumerate(gs)), - gs, - ) -end - -function _parse_annotations(x)::Annotations - Annotations([k => if typeof(v) == String - [v] - else - convert(Vector{String}, v) - end for (k, v) in x]) -end - -_parse_notes(x)::Notes = _parse_annotations(x) - -Accessors.n_variables(model::JSONModel) = length(model.rxns) - -Accessors.n_metabolites(model::JSONModel) = length(model.mets) - -Accessors.n_genes(model::JSONModel) = length(model.genes) - -Accessors.variables(model::JSONModel) = - [_json_rxn_name(r, i) for (i, r) in enumerate(model.rxns)] - -Accessors.metabolites(model::JSONModel) = - [_json_met_name(m, i) for (i, m) in enumerate(model.mets)] - -Accessors.genes(model::JSONModel) = - [_json_gene_name(g, i) for (i, g) in enumerate(model.genes)] - -Accessors.Internal.@all_variables_are_reactions JSONModel - -function Accessors.stoichiometry(model::JSONModel) - rxn_ids = variables(model) - met_ids = metabolites(model) - - n_entries = 0 - for r in model.rxns - for _ in r["metabolites"] - n_entries += 1 - end - end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - for (i, rid) in enumerate(rxn_ids) - r = model.rxns[model.rxn_index[rid]] - for (mid, coeff) in r["metabolites"] - haskey(model.met_index, mid) || throw( - DomainError( - met_id, - "Unknown metabolite found in stoichiometry of $(rxn_ids[i])", - ), - ) - - push!(MI, model.met_index[mid]) - push!(RI, i) - push!(SV, coeff) - end - end - return SparseArrays.sparse(MI, RI, SV, length(met_ids), length(rxn_ids)) -end - -Accessors.bounds(model::JSONModel) = ( - [get(rxn, "lower_bound", -constants.default_reaction_bound) for rxn in model.rxns], - [get(rxn, "upper_bound", constants.default_reaction_bound) for rxn in model.rxns], -) - -Accessors.objective(model::JSONModel) = - sparse([float(get(rxn, "objective_coefficient", 0.0)) for rxn in model.rxns]) - -Accessors.reaction_gene_associations(model::JSONModel, rid::String) = maybemap( - parse_grr, - get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), -) - -Accessors.eval_reaction_gene_association(model::JSONModel, rid::String; kwargs...) = - maybemap( - x -> eval_grr(x; kwargs...), - maybemap( - parse_grr_to_sbml, - get(model.rxns[model.rxn_index[rid]], "gene_reaction_rule", nothing), - ), - ) - -Accessors.reaction_subsystem(model::JSONModel, rid::String) = - get(model.rxns[model.rxn_index[rid]], "subsystem", nothing) - -Accessors.metabolite_formula(model::JSONModel, mid::String) = - maybemap(parse_formula, get(model.mets[model.met_index[mid]], "formula", nothing)) - -Accessors.metabolite_charge(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "charge", 0) - -Accessors.metabolite_compartment(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "compartment", nothing) - -Accessors.gene_annotations(model::JSONModel, gid::String)::Annotations = maybemap( - _parse_annotations, - get(model.genes[model.gene_index[gid]], "annotation", nothing), -) - -Accessors.gene_notes(model::JSONModel, gid::String)::Notes = - maybemap(_parse_notes, get(model.genes[model.gene_index[gid]], "notes", nothing)) - -Accessors.reaction_annotations(model::JSONModel, rid::String)::Annotations = maybemap( - _parse_annotations, - get(model.rxns[model.rxn_index[rid]], "annotation", nothing), -) - -Accessors.reaction_notes(model::JSONModel, rid::String)::Notes = - maybemap(_parse_notes, get(model.rxns[model.rxn_index[rid]], "notes", nothing)) - -Accessors.metabolite_annotations(model::JSONModel, mid::String)::Annotations = maybemap( - _parse_annotations, - get(model.mets[model.met_index[mid]], "annotation", nothing), -) - -Accessors.metabolite_notes(model::JSONModel, mid::String)::Notes = - maybemap(_parse_notes, get(model.mets[model.met_index[mid]], "notes", nothing)) - -Accessors.reaction_stoichiometry(model::JSONModel, rid::String)::Dict{String,Float64} = - model.rxns[model.rxn_index[rid]]["metabolites"] - -Accessors.reaction_name(model::JSONModel, rid::String) = - get(model.rxns[model.rxn_index[rid]], "name", nothing) - -Accessors.metabolite_name(model::JSONModel, mid::String) = - get(model.mets[model.met_index[mid]], "name", nothing) - -Accessors.gene_name(model::JSONModel, gid::String) = - get(model.genes[model.gene_index[gid]], "name", nothing) - -Accessors.model_annotations(model::JSONModel)::Annotations = - get(model.json, "annotation", Annotations()) - -Accessors.model_notes(model::JSONModel)::Notes = get(model.json, "notes", Notes()) - -function Base.convert(::Type{JSONModel}, mm::AbstractMetabolicModel) - if typeof(mm) == JSONModel - return mm - end - - rxn_ids = variables(mm) - met_ids = metabolites(mm) - gene_ids = genes(mm) - S = stoichiometry(mm) - lbs, ubs = bounds(mm) - ocs = objective(mm) - - json = Dict{String,Any}() - - json["annotation"] = model_annotations(mm) - json["notes"] = model_notes(mm) - - json[first(constants.keynames.genes)] = [ - Dict([ - "id" => gid, - "name" => gene_name(mm, gid), - "annotation" => gene_annotations(mm, gid), - "notes" => gene_notes(mm, gid), - ],) for gid in gene_ids - ] - - json[first(constants.keynames.mets)] = [ - Dict([ - "id" => mid, - "name" => metabolite_name(mm, mid), - "formula" => maybemap(unparse_formula, metabolite_formula(mm, mid)), - "charge" => metabolite_charge(mm, mid), - "compartment" => metabolite_compartment(mm, mid), - "annotation" => metabolite_annotations(mm, mid), - "notes" => metabolite_notes(mm, mid), - ]) for mid in met_ids - ] - - json[first(constants.keynames.rxns)] = [ - begin - res = Dict{String,Any}() - res["id"] = rid - res["name"] = reaction_name(mm, rid) - res["subsystem"] = reaction_subsystem(mm, rid) - res["annotation"] = reaction_annotations(mm, rid) - res["notes"] = reaction_notes(mm, rid) - - grr = reaction_gene_associations(mm, rid) - if !isnothing(grr) - res["gene_reaction_rule"] = unparse_grr(String, grr) - end - - res["lower_bound"] = lbs[ri] - res["upper_bound"] = ubs[ri] - res["objective_coefficient"] = ocs[ri] - I, V = findnz(S[:, ri]) - res["metabolites"] = - Dict{String,Float64}([met_ids[ii] => vv for (ii, vv) in zip(I, V)]) - res - end for (ri, rid) in enumerate(rxn_ids) - ] - - return JSONModel(json) -end diff --git a/src/types/models/MATModel.jl b/src/types/models/MATModel.jl deleted file mode 100644 index 68b3be4b3..000000000 --- a/src/types/models/MATModel.jl +++ /dev/null @@ -1,193 +0,0 @@ -""" -$(TYPEDEF) - -Wrapper around the models loaded in dictionaries from the MATLAB representation. - -# Fields -$(TYPEDFIELDS) -""" -struct MATModel <: AbstractMetabolicModel - mat::Dict{String,Any} -end - -Accessors.n_metabolites(m::MATModel)::Int = size(m.mat["S"], 1) -Accessors.n_variables(m::MATModel)::Int = size(m.mat["S"], 2) - -function Accessors.variables(m::MATModel)::Vector{String} - if haskey(m.mat, "rxns") - reshape(m.mat["rxns"], n_variables(m)) - else - "rxn" .* string.(1:n_variables(m)) - end -end - -Accessors.Internal.@all_variables_are_reactions MATModel - -_mat_has_squashed_coupling(mat) = - haskey(mat, "A") && haskey(mat, "b") && length(mat["b"]) == size(mat["A"], 1) - - -function Accessors.metabolites(m::MATModel)::Vector{String} - nm = n_metabolites(m) - if haskey(m.mat, "mets") - reshape(m.mat["mets"], length(m.mat["mets"]))[begin:nm] - else - "met" .* string.(1:n_metabolites(m)) - end -end - -Accessors.stoichiometry(m::MATModel) = sparse(m.mat["S"]) - -Accessors.bounds(m::MATModel) = ( - reshape(get(m.mat, "lb", fill(-Inf, n_variables(m), 1)), n_variables(m)), - reshape(get(m.mat, "ub", fill(Inf, n_variables(m), 1)), n_variables(m)), -) - -function Accessors.balance(m::MATModel) - b = get(m.mat, "b", spzeros(n_metabolites(m), 1)) - if _mat_has_squashed_coupling(m.mat) - b = b[1:n_metabolites(m), :] - end - sparse(reshape(b, n_metabolites(m))) -end - -Accessors.objective(m::MATModel) = - sparse(reshape(get(m.mat, "c", zeros(n_variables(m), 1)), n_variables(m))) - -Accessors.coupling(m::MATModel) = - _mat_has_squashed_coupling(m.mat) ? sparse(m.mat["A"][n_variables(m)+1:end, :]) : - sparse(get(m.mat, "C", zeros(0, n_variables(m)))) - -function Accessors.coupling_bounds(m::MATModel) - nc = n_coupling_constraints(m) - if _mat_has_squashed_coupling(m.mat) - ( - sparse(fill(-Inf, nc)), - sparse(reshape(m.mat["b"], length(m.mat["b"]))[n_variables(m)+1:end]), - ) - else - ( - sparse(reshape(get(m.mat, "cl", fill(-Inf, nc, 1)), nc)), - sparse(reshape(get(m.mat, "cu", fill(Inf, nc, 1)), nc)), - ) - end -end - -function Accessors.genes(m::MATModel) - x = get(m.mat, "genes", []) - reshape(x, length(x)) -end - -function Accessors.reaction_gene_associations(m::MATModel, rid::String) - if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), variables(m))] - typeof(grr) == String ? parse_grr(grr) : nothing - else - nothing - end -end - -function Accessors.eval_reaction_gene_association(m::MATModel, rid::String; kwargs...) - if haskey(m.mat, "grRules") - grr = m.mat["grRules"][findfirst(==(rid), variables(m))] - typeof(grr) == String ? eval_grr(parse_grr_to_sbml(grr); kwargs...) : nothing - else - nothing - end -end - -Accessors.metabolite_formula(m::MATModel, mid::String) = maybemap( - x -> parse_formula(x[findfirst(==(mid), metabolites(m))]), - gets(m.mat, nothing, constants.keynames.metformulas), -) - -function Accessors.metabolite_charge(m::MATModel, mid::String)::Maybe{Int} - met_charge = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, constants.keynames.metcharges), - ) - maybemap(Int, isnan(met_charge) ? nothing : met_charge) -end - -function Accessors.metabolite_compartment(m::MATModel, mid::String) - res = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, constants.keynames.metcompartments), - ) - # if the metabolite is an integer or a (very integerish) float, it is an - # index to a table of metabolite names (such as in the yeast GEM) - typeof(res) <: Real || return res - return maybemap( - table -> table[Int(res)], - gets(m.mat, nothing, constants.keynames.metcomptables), - ) -end - -function Accessors.reaction_stoichiometry(m::MATModel, rid::String)::Dict{String,Float64} - ridx = first(indexin([rid], m.mat["rxns"]))[1] # get the index out of the cartesian index - reaction_stoichiometry(m, ridx) -end - -function Accessors.reaction_stoichiometry(m::MATModel, ridx::Int)::Dict{String,Float64} - met_inds = findall(m.mat["S"][:, ridx] .!= 0.0) - Dict(m.mat["mets"][met_ind] => m.mat["S"][met_ind, ridx] for met_ind in met_inds) -end - -Accessors.reaction_name(m::MATModel, rid::String) = maybemap( - x -> x[findfirst(==(rid), variables(m))], - gets(m.mat, nothing, constants.keynames.rxnnames), -) - -Accessors.metabolite_name(m::MATModel, mid::String) = maybemap( - x -> x[findfirst(==(mid), metabolites(m))], - gets(m.mat, nothing, constants.keynames.metnames), -) - -# NOTE: There's no useful standard on how and where to store notes and -# annotations in MATLAB models. We therefore leave it very open for the users, -# who can easily support any annotation scheme using a custom wrapper. -# Even the (simple) assumptions about grRules, formulas and charges that we use -# here are very likely completely incompatible with >50% of the MATLAB models -# out there. - -function Base.convert(::Type{MATModel}, m::AbstractMetabolicModel) - if typeof(m) == MATModel - return m - end - - lb, ub = bounds(m) - cl, cu = coupling_bounds(m) - nr = n_variables(m) - nm = n_metabolites(m) - return MATModel( - Dict( - "S" => stoichiometry(m), - "rxns" => variables(m), - "mets" => metabolites(m), - "lb" => Vector(lb), - "ub" => Vector(ub), - "b" => Vector(balance(m)), - "c" => Vector(objective(m)), - "C" => coupling(m), - "cl" => Vector(cl), - "cu" => Vector(cu), - "genes" => genes(m), - "grRules" => - default.( - "", - maybemap.( - x -> unparse_grr(String, x), - reaction_gene_associations.(Ref(m), variables(m)), - ), - ), - "metFormulas" => - default.( - "", - maybemap.(unparse_formula, metabolite_formula.(Ref(m), metabolites(m))), - ), - "metCharges" => default.(0, metabolite_charge.(Ref(m), metabolites(m))), - "metCompartments" => - default.("", metabolite_compartment.(Ref(m), metabolites(m))), - ), - ) -end diff --git a/src/types/models/MatrixModel.jl b/src/types/models/MatrixModel.jl deleted file mode 100644 index ab03ae587..000000000 --- a/src/types/models/MatrixModel.jl +++ /dev/null @@ -1,111 +0,0 @@ - -""" -$(TYPEDEF) - -A "bare bones" core linear optimization problem of the form, with reaction and -metabolite names. -``` -min c^T x -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct MatrixModel <: AbstractMetabolicModel - S::SparseMat - b::SparseVec - c::SparseVec - xl::Vector{Float64} - xu::Vector{Float64} - rxns::Vector{String} - mets::Vector{String} - grrs::Vector{Maybe{GeneAssociationsDNF}} - - function MatrixModel( - S::MatType, - b::VecType, - c::VecType, - xl::VecType, - xu::VecType, - rxns::StringVecType, - mets::StringVecType, - grrs::Vector{Maybe{GeneAssociationsDNF}} = Vector{Maybe{GeneAssociationsDNF}}( - nothing, - length(rxns), - ), - ) - all([length(b), length(mets)] .== size(S, 1)) || - throw(DimensionMismatch("inconsistent number of metabolites")) - - all( - [length(c), length(xl), length(xu), length(rxns), length(grrs)] .== size(S, 2), - ) || throw(DimensionMismatch("inconsistent number of reactions")) - - new(sparse(S), sparse(b), sparse(c), collect(xl), collect(xu), rxns, mets, grrs) - end -end - -Accessors.variables(a::MatrixModel)::Vector{String} = a.rxns - -Accessors.Internal.@all_variables_are_reactions MatrixModel - -Accessors.metabolites(a::MatrixModel)::Vector{String} = a.mets - -Accessors.stoichiometry(a::MatrixModel)::SparseMat = a.S - -Accessors.bounds(a::MatrixModel)::Tuple{Vector{Float64},Vector{Float64}} = (a.xl, a.xu) - -Accessors.balance(a::MatrixModel)::SparseVec = a.b - -Accessors.objective(a::MatrixModel)::SparseVec = a.c - -function Accessors.genes(a::MatrixModel)::Vector{String} - res = Set{String}() - for grr in a.grrs - isnothing(grr) && continue - for gs in grr - for g in gs - push!(res, g) - end - end - end - sort(collect(res)) -end - -Accessors.reaction_stoichiometry(m::MatrixModel, rid::String)::Dict{String,Float64} = - Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, first(indexin([rid], m.rxns))])...)) - -Accessors.reaction_stoichiometry(m::MatrixModel, ridx::Int)::Dict{String,Float64} = - Dict(m.mets[k] => v for (k, v) in zip(findnz(m.S[:, ridx])...)) - -Accessors.reaction_gene_associations( - model::MatrixModel, - ridx::Int, -)::Maybe{GeneAssociationsDNF} = model.grrs[ridx] - -Accessors.reaction_gene_associations( - model::MatrixModel, - rid::String, -)::Maybe{GeneAssociationsDNF} = model.grrs[first(indexin([rid], model.rxns))] - -function Base.convert(::Type{MatrixModel}, m::M) where {M<:AbstractMetabolicModel} - if typeof(m) == MatrixModel - return m - end - - (xl, xu) = bounds(m) - MatrixModel( - stoichiometry(m), - balance(m), - objective(m), - xl, - xu, - variables(m), - metabolites(m), - Vector{Maybe{GeneAssociationsDNF}}([ - reaction_gene_associations(m, id) for id in variables(m) - ]), - ) -end diff --git a/src/types/models/ObjectModel.jl b/src/types/models/ObjectModel.jl deleted file mode 100644 index 5847313c6..000000000 --- a/src/types/models/ObjectModel.jl +++ /dev/null @@ -1,249 +0,0 @@ -""" -$(TYPEDEF) - -`ObjectModel` is used to store a constraint based metabolic model with -meta-information. Meta-information is defined as annotation details, which -include gene-reaction-rules, formulas, etc. - -This model type seeks to keep as much meta-information as possible, as opposed -to `MatrixModel` and `MatrixModelWithCoupling`, which keep the bare neccessities -only. When merging models and keeping meta-information is important, use this as -the model type. - -In this model, reactions, metabolites, and genes are stored in ordered -dictionaries indexed by each struct's `id` field. For example, -`model.reactions["rxn1_id"]` returns a `Reaction` where the field `id` equals -`"rxn1_id"`. This makes adding and removing reactions efficient. - -Note that the stoichiometric matrix (or any other core data, e.g. flux bounds) -is not stored directly as in [`MatrixModel`](@ref). When this model type is used -in analysis functions, these core data structures are built from scratch each -time an analysis function is called. This can cause performance issues if you -run many small analysis functions sequentially. - -See also: [`Reaction`](@ref), [`Metabolite`](@ref), [`Gene`](@ref) - -# Example -``` -model = load_model(ObjectModel, "my_model.json") -keys(model.reactions) -``` - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct ObjectModel <: AbstractMetabolicModel - "Ordered dictionary of reactions." - reactions::OrderedDict{String,Reaction} = OrderedDict{String,Reaction}() - - "Ordered dictionary of metabolites." - metabolites::OrderedDict{String,Metabolite} = OrderedDict{String,Metabolite}() - - "Ordered dictionary of genes." - genes::OrderedDict{String,Gene} = OrderedDict{String,Gene}() - - "Model objective." - objective::Dict{String,Float64} = Dict{String,Float64}() - - "Machine readable reference to organism embedded via MIRIAM annotation. This should include species name, taxonomy ID, and url to the genome." - annotations::Annotations = Annotations() - - "Reference information for the model. This should include the DOI and author contact information." - notes::Notes = Notes() -end - -# AbstractMetabolicModel interface follows - -Accessors.variables(model::ObjectModel)::StringVecType = collect(keys(model.reactions)) - -Accessors.n_variables(model::ObjectModel)::Int = length(model.reactions) - -Accessors.Internal.@all_variables_are_reactions ObjectModel - -Accessors.metabolites(model::ObjectModel)::StringVecType = collect(keys(model.metabolites)) - -Accessors.n_metabolites(model::ObjectModel)::Int = length(model.metabolites) - -Accessors.genes(model::ObjectModel)::StringVecType = collect(keys(model.genes)) - -Accessors.n_genes(model::ObjectModel)::Int = length(model.genes) - -function Accessors.stoichiometry(model::ObjectModel)::SparseMat - n_entries = 0 - for (_, r) in model.reactions - for _ in r.metabolites - n_entries += 1 - end - end - - MI = Vector{Int}() - RI = Vector{Int}() - SV = Vector{Float64}() - sizehint!(MI, n_entries) - sizehint!(RI, n_entries) - sizehint!(SV, n_entries) - - # establish the ordering - rxns = variables(model) - met_idx = Dict(mid => i for (i, mid) in enumerate(metabolites(model))) - - # fill the matrix entries - for (ridx, rid) in enumerate(rxns) - for (mid, coeff) in model.reactions[rid].metabolites - haskey(met_idx, mid) || throw( - DomainError( - mid, - "Metabolite $(mid) not found in model but occurs in stoichiometry of $(rid)", - ), - ) - push!(MI, met_idx[mid]) - push!(RI, ridx) - push!(SV, coeff) - end - end - return SparseArrays.sparse(MI, RI, SV, n_metabolites(model), n_variables(model)) -end - -Accessors.bounds(model::ObjectModel)::Tuple{Vector{Float64},Vector{Float64}} = - (lower_bounds(model), upper_bounds(model)) - -Accessors.balance(model::ObjectModel)::SparseVec = spzeros(length(model.metabolites)) - -Accessors.objective(model::ObjectModel)::SparseVec = - sparse([get(model.objective, rid, 0.0) for rid in keys(model.reactions)]) - -function Accessors.reaction_gene_associations( - model::ObjectModel, - id::String, -)::Maybe{GeneAssociationsDNF} - isnothing(model.reactions[id].gene_associations) && return nothing - [ - collect(keys(rga.gene_product_stoichiometry)) for - rga in model.reactions[id].gene_associations - ] -end - -Accessors.metabolite_formula(model::ObjectModel, id::String)::Maybe{MetaboliteFormula} = - maybemap(parse_formula, model.metabolites[id].formula) - -Accessors.metabolite_charge(model::ObjectModel, id::String)::Maybe{Int} = - model.metabolites[id].charge - -Accessors.metabolite_compartment(model::ObjectModel, id::String)::Maybe{String} = - model.metabolites[id].compartment - -Accessors.reaction_subsystem(model::ObjectModel, id::String)::Maybe{String} = - model.reactions[id].subsystem - -Accessors.metabolite_notes(model::ObjectModel, id::String)::Maybe{Notes} = - model.metabolites[id].notes - -Accessors.metabolite_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = - model.metabolites[id].annotations - -Accessors.gene_notes(model::ObjectModel, gid::String) = model.genes[gid].notes - -Accessors.gene_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = - model.genes[id].annotations - -Accessors.reaction_notes(model::ObjectModel, id::String)::Maybe{Notes} = - model.reactions[id].notes - -Accessors.reaction_annotations(model::ObjectModel, id::String)::Maybe{Annotations} = - model.reactions[id].annotations - -Accessors.reaction_stoichiometry(m::ObjectModel, rid::String)::Dict{String,Float64} = - m.reactions[rid].metabolites - -Accessors.reaction_name(m::ObjectModel, rid::String) = m.reactions[rid].name - -Accessors.metabolite_name(m::ObjectModel, mid::String) = m.metabolites[mid].name - -Accessors.gene_name(m::ObjectModel, gid::String) = m.genes[gid].name - -Accessors.gene_product_molar_mass(model::ObjectModel, gid::String) = - model.genes[gid].product_molar_mass - -Accessors.reaction_isozymes(model::ObjectModel, rid::String) = - model.reactions[rid].gene_associations - -Accessors.gene_product_lower_bound(model::ObjectModel, gid::String) = - model.genes[gid].product_lower_bound - -Accessors.gene_product_upper_bound(model::ObjectModel, gid::String) = - model.genes[gid].product_upper_bound - -Accessors.model_notes(model::ObjectModel)::Notes = model.notes - -Accessors.model_annotations(model::ObjectModel)::Annotations = model.annotations - -function Base.convert(::Type{ObjectModel}, model::AbstractMetabolicModel) - if typeof(model) == ObjectModel - return model - end - - modelreactions = OrderedDict{String,Reaction}() - modelmetabolites = OrderedDict{String,Metabolite}() - modelgenes = OrderedDict{String,Gene}() - - gids = genes(model) - metids = metabolites(model) - rxnids = variables(model) - - for gid in gids - modelgenes[gid] = Gene( - gid; - name = gene_name(model, gid), - notes = gene_notes(model, gid), - annotations = gene_annotations(model, gid), - ) - end - - for mid in metids - modelmetabolites[mid] = Metabolite( - mid; - name = metabolite_name(model, mid), - charge = metabolite_charge(model, mid), - formula = maybemap(unparse_formula, metabolite_formula(model, mid)), - compartment = metabolite_compartment(model, mid), - notes = metabolite_notes(model, mid), - annotations = metabolite_annotations(model, mid), - ) - end - - S = stoichiometry(model) - lbs, ubs = bounds(model) - obj_idxs, obj_vals = findnz(objective(model)) - modelobjective = Dict(k => v for (k, v) in zip(variables(model)[obj_idxs], obj_vals)) - for (i, rid) in enumerate(rxnids) - rmets = Dict{String,Float64}() - for (j, stoich) in zip(findnz(S[:, i])...) - rmets[metids[j]] = stoich - end - rgas = reaction_gene_associations(model, rid) - modelreactions[rid] = Reaction( - rid; - name = reaction_name(model, rid), - metabolites = rmets, - lower_bound = lbs[i], - upper_bound = ubs[i], - gene_associations = isnothing(rgas) ? nothing : - [ - Isozyme(; gene_product_stoichiometry = Dict(k => 1.0 for k in rga)) for - rga in rgas - ], - notes = reaction_notes(model, rid), - annotations = reaction_annotations(model, rid), - subsystem = reaction_subsystem(model, rid), - ) - end - - return ObjectModel(; - reactions = modelreactions, - metabolites = modelmetabolites, - genes = modelgenes, - objective = modelobjective, - notes = model_notes(model), - annotations = model_annotations(model), - ) -end diff --git a/src/types/models/SBMLModel.jl b/src/types/models/SBMLModel.jl deleted file mode 100644 index 207cd37be..000000000 --- a/src/types/models/SBMLModel.jl +++ /dev/null @@ -1,371 +0,0 @@ -""" -$(TYPEDEF) - -Thin wrapper around the model from SBML.jl library. Allows easy conversion from -SBML to any other model format. - -# Fields -$(TYPEDFIELDS) -""" -struct SBMLModel <: AbstractMetabolicModel - sbml::SBML.Model - reaction_ids::Vector{String} - reaction_idx::Dict{String,Int} - metabolite_ids::Vector{String} - metabolite_idx::Dict{String,Int} - gene_ids::Vector{String} - active_objective::String -end - -""" -$(TYPEDEF) - -Construct the SBML model and add the necessary cached indexes, possibly choosing an active objective. -""" -function SBMLModel(sbml::SBML.Model, active_objective::String = "") - rxns = sort(collect(keys(sbml.reactions))) - mets = sort(collect(keys(sbml.species))) - genes = sort(collect(keys(sbml.gene_products))) - - SBMLModel( - sbml, - rxns, - Dict(rxns .=> eachindex(rxns)), - mets, - Dict(mets .=> eachindex(mets)), - genes, - active_objective, - ) -end - -Accessors.variables(model::SBMLModel)::Vector{String} = model.reaction_ids - -Accessors.Internal.@all_variables_are_reactions SBMLModel - -Accessors.metabolites(model::SBMLModel)::Vector{String} = model.metabolite_ids - -function Accessors.stoichiometry(model::SBMLModel)::SparseMat - - # find the vector size for preallocation - nnz = 0 - for (_, r) in model.sbml.reactions - for _ in r.reactants - nnz += 1 - end - for _ in r.products - nnz += 1 - end - end - - Rows = Int[] - Cols = Int[] - Vals = Float64[] - sizehint!(Rows, nnz) - sizehint!(Cols, nnz) - sizehint!(Vals, nnz) - - row_idx = Dict(k => i for (i, k) in enumerate(model.metabolite_ids)) - for (ridx, rid) in enumerate(model.reaction_ids) - r = model.sbml.reactions[rid] - for sr in r.reactants - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? -1.0 : -sr.stoichiometry) - end - for sr in r.products - push!(Rows, model.metabolite_idx[sr.species]) - push!(Cols, ridx) - push!(Vals, isnothing(sr.stoichiometry) ? 1.0 : sr.stoichiometry) - end - end - return sparse(Rows, Cols, Vals, n_metabolites(model), n_reactions(model)) -end - -function Accessors.bounds(model::SBMLModel)::Tuple{Vector{Float64},Vector{Float64}} - # There are multiple ways in SBML to specify a lower/upper bound. There are - # the "global" model bounds that we completely ignore now because no one - # uses them. In reaction, you can specify the bounds using "LOWER_BOUND" - # and "UPPER_BOUND" parameters, but also there may be a FBC plugged-in - # parameter name that refers to the parameters. We extract these, using - # the units from the parameters. For unbounded reactions we use -Inf or Inf - # as a default. - - common_unit = "" - - function get_bound(rid, fld, param, default) - rxn = model.sbml.reactions[rid] - param_name = SBML.mayfirst(getfield(rxn, fld), param) - param = get( - rxn.kinetic_parameters, - param_name, - get(model.sbml.parameters, param_name, default), - ) - unit = SBML.mayfirst(param.units, "") - if unit != "" - if common_unit != "" - if unit != common_unit - throw( - DomainError( - units_in_sbml, - "The SBML file uses multiple units; loading would need conversion", - ), - ) - end - else - common_unit = unit - end - end - return param.value - end - - return ( - get_bound.( - model.reaction_ids, - :lower_bound, - "LOWER_BOUND", - Ref(SBML.Parameter(value = -Inf)), - ), - get_bound.( - model.reaction_ids, - :upper_bound, - "UPPER_BOUND", - Ref(SBML.Parameter(value = Inf)), - ), - ) -end - -Accessors.balance(model::SBMLModel)::SparseVec = spzeros(n_metabolites(model)) - -function Accessors.objective(model::SBMLModel)::SparseVec - res = spzeros(n_reactions(model)) - - objective = get(model.sbml.objectives, model.active_objective, nothing) - if isnothing(objective) && length(model.sbml.objectives) == 1 - objective = first(values(model.sbml.objectives)) - end - if !isnothing(objective) - direction = objective.type == "maximize" ? 1.0 : -1.0 - for (rid, coef) in objective.flux_objectives - res[model.reaction_idx[rid]] = float(direction * coef) - end - else - # old-style objectives - for (rid, r) in model.sbml.reactions - oc = get(r.kinetic_parameters, "OBJECTIVE_COEFFICIENT", nothing) - isnothing(oc) || (res[model.reaction_idx[rid]] = float(oc.value)) - end - end - return res -end - -Accessors.genes(model::SBMLModel)::Vector{String} = model.gene_ids - -Accessors.reaction_gene_associations( - model::SBMLModel, - rid::String, -)::Maybe{GeneAssociationsDNF} = - maybemap(parse_grr, model.sbml.reactions[rid].gene_product_association) - -Accessors.eval_reaction_gene_association(model::SBMLModel, rid::String; kwargs...) = - maybemap( - x -> eval_grr(x; kwargs...), - model.sbml.reactions[rid].gene_product_association, - ) - -Accessors.metabolite_formula(model::SBMLModel, mid::String)::Maybe{MetaboliteFormula} = - maybemap(parse_formula, model.sbml.species[mid].formula) - -Accessors.metabolite_compartment(model::SBMLModel, mid::String) = - model.sbml.species[mid].compartment - -Accessors.metabolite_charge(model::SBMLModel, mid::String)::Maybe{Int} = - model.sbml.species[mid].charge - -function _parse_sbml_identifiers_org_uri(uri::String)::Tuple{String,String} - m = match(r"^https?://identifiers.org/([^/]+)/(.*)$", uri) - isnothing(m) ? ("RESOURCE_URI", uri) : (m[1], m[2]) -end - -function _sbml_import_cvterms(sbo::Maybe{String}, cvs::Vector{SBML.CVTerm})::Annotations - res = Annotations() - isnothing(sbo) || (res["sbo"] = [sbo]) - for cv in cvs - cv.biological_qualifier == :is || continue - for (id, val) in _parse_sbml_identifiers_org_uri.(cv.resource_uris) - push!(get!(res, id, []), val) - end - end - return res -end - -function _sbml_export_cvterms(annotations::Annotations)::Vector{SBML.CVTerm} - isempty(annotations) && return [] - length(annotations) == 1 && haskey(annotations, "sbo") && return [] - [ - SBML.CVTerm( - biological_qualifier = :is, - resource_uris = [ - id == "RESOURCE_URI" ? val : "http://identifiers.org/$id/$val" for - (id, vals) in annotations if id != "sbo" for val in vals - ], - ), - ] -end - -function _sbml_export_sbo(annotations::Annotations)::Maybe{String} - haskey(annotations, "sbo") || return nothing - if length(annotations["sbo"]) != 1 - @io_log @error "Data loss: SBO term is not unique for SBML export" annotations["sbo"] - return - end - return annotations["sbo"][1] -end - -function _sbml_import_notes(notes::Maybe{String})::Notes - isnothing(notes) ? Notes() : Notes("" => [notes]) -end - -function _sbml_export_notes(notes::Notes)::Maybe{String} - isempty(notes) || @io_log @error "Data loss: notes not exported to SBML" notes - nothing -end - - -function Accessors.reaction_stoichiometry(m::SBMLModel, rid::String)::Dict{String,Float64} - s = Dict{String,Float64}() - default1(x) = isnothing(x) ? 1 : x - for sr in m.sbml.reactions[rid].reactants - s[sr.species] = get(s, sr.species, 0.0) - default1(sr.stoichiometry) - end - for sr in m.sbml.reactions[rid].products - s[sr.species] = get(s, sr.species, 0.0) + default1(sr.stoichiometry) - end - return s -end - -Accessors.reaction_name(model::SBMLModel, rid::String) = model.sbml.reactions[rid].name - -Accessors.metabolite_name(model::SBMLModel, mid::String) = model.sbml.species[mid].name - -Accessors.gene_name(model::SBMLModel, gid::String) = model.sbml.gene_products[gid].name - -Accessors.reaction_annotations(model::SBMLModel, rid::String) = - _sbml_import_cvterms(model.sbml.reactions[rid].sbo, model.sbml.reactions[rid].cv_terms) - -Accessors.metabolite_annotations(model::SBMLModel, mid::String) = - _sbml_import_cvterms(model.sbml.species[mid].sbo, model.sbml.species[mid].cv_terms) - -Accessors.gene_annotations(model::SBMLModel, gid::String) = _sbml_import_cvterms( - model.sbml.gene_products[gid].sbo, - model.sbml.gene_products[gid].cv_terms, -) - -Accessors.reaction_notes(model::SBMLModel, rid::String) = - _sbml_import_notes(model.sbml.reactions[rid].notes) - -Accessors.metabolite_notes(model::SBMLModel, mid::String) = - _sbml_import_notes(model.sbml.species[mid].notes) - -Accessors.gene_notes(model::SBMLModel, gid::String) = - _sbml_import_notes(model.sbml.gene_products[gid].notes) - -Accessors.model_annotations(model::SBMLModel) = - _sbml_import_cvterms(model.sbml.sbo, model.sbml.cv_terms) - -Accessors.model_notes(model::SBMLModel) = _sbml_import_notes(model.sbml.notes) - -function Base.convert(::Type{SBMLModel}, mm::AbstractMetabolicModel) - if typeof(mm) == SBMLModel - return mm - end - - mets = metabolites(mm) - rxns = variables(mm) - stoi = stoichiometry(mm) - (lbs, ubs) = bounds(mm) - comps = default.("compartment", metabolite_compartment.(Ref(mm), mets)) - compss = Set(comps) - - metid(x) = startswith(x, "M_") ? x : "M_$x" - rxnid(x) = startswith(x, "R_") ? x : "R_$x" - gprid(x) = startswith(x, "G_") ? x : "G_$x" - - return SBMLModel( - SBML.Model( - compartments = Dict( - comp => SBML.Compartment(constant = true) for comp in compss - ), - species = Dict( - metid(mid) => SBML.Species( - name = metabolite_name(mm, mid), - compartment = default("compartment", comps[mi]), - formula = maybemap(unparse_formula, metabolite_formula(mm, mid)), - charge = metabolite_charge(mm, mid), - constant = false, - boundary_condition = false, - only_substance_units = false, - sbo = _sbml_export_sbo(metabolite_annotations(mm, mid)), - notes = _sbml_export_notes(metabolite_notes(mm, mid)), - metaid = metid(mid), - cv_terms = _sbml_export_cvterms(metabolite_annotations(mm, mid)), - ) for (mi, mid) in enumerate(mets) - ), - reactions = Dict( - rxnid(rid) => SBML.Reaction( - name = reaction_name(mm, rid), - reactants = [ - SBML.SpeciesReference( - species = metid(mets[i]), - stoichiometry = -stoi[i, ri], - constant = true, - ) for - i in SparseArrays.nonzeroinds(stoi[:, ri]) if stoi[i, ri] <= 0 - ], - products = [ - SBML.SpeciesReference( - species = metid(mets[i]), - stoichiometry = stoi[i, ri], - constant = true, - ) for - i in SparseArrays.nonzeroinds(stoi[:, ri]) if stoi[i, ri] > 0 - ], - kinetic_parameters = Dict( - "LOWER_BOUND" => SBML.Parameter(value = lbs[ri]), - "UPPER_BOUND" => SBML.Parameter(value = ubs[ri]), - ), - lower_bound = "LOWER_BOUND", - upper_bound = "UPPER_BOUND", - gene_product_association = maybemap( - x -> unparse_grr(SBML.GeneProductAssociation, x), - reaction_gene_associations(mm, rid), - ), - reversible = true, - sbo = _sbml_export_sbo(reaction_annotations(mm, rid)), - notes = _sbml_export_notes(reaction_notes(mm, rid)), - metaid = rxnid(rid), - cv_terms = _sbml_export_cvterms(reaction_annotations(mm, rid)), - ) for (ri, rid) in enumerate(rxns) - ), - gene_products = Dict( - gprid(gid) => SBML.GeneProduct( - label = gid, - name = gene_name(mm, gid), - sbo = _sbml_export_sbo(gene_annotations(mm, gid)), - notes = _sbml_export_notes(gene_notes(mm, gid)), - metaid = gprid(gid), - cv_terms = _sbml_export_cvterms(gene_annotations(mm, gid)), - ) for gid in genes(mm) - ), - active_objective = "objective", - objectives = Dict( - "objective" => SBML.Objective( - "maximize", - Dict(rid => oc for (rid, oc) in zip(rxns, objective(mm)) if oc != 0), - ), - ), - notes = _sbml_export_notes(model_notes(mm)), - sbo = _sbml_export_sbo(model_annotations(mm)), - cv_terms = _sbml_export_cvterms(model_annotations(mm)), - ), - ) -end diff --git a/src/types/models/Serialized.jl b/src/types/models/Serialized.jl deleted file mode 100644 index ce2ee318c..000000000 --- a/src/types/models/Serialized.jl +++ /dev/null @@ -1,31 +0,0 @@ - -""" -$(TYPEDEF) - -A meta-model that represents a model that is serialized on the disk. The -internal model will be loaded on-demand by using any accessor, or by calling -[`precache!`](@ref) directly. - -# Fields -$(TYPEDFIELDS) -""" -mutable struct Serialized{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} - m::Maybe{M} - filename::String - - Serialized{T}(filename::String) where {T} = new{T}(nothing, filename) - Serialized(model::T, filename::String) where {T<:AbstractMetabolicModel} = - new{T}(model, filename) -end - -function Accessors.unwrap_model(m::Serialized) - precache!(m) - m.m -end - -function Accessors.precache!(model::Serialized)::Nothing - if isnothing(model.m) - model.m = deserialize(model.filename) - end - nothing -end diff --git a/src/types/wrappers/EqualGrowthCommunityModel.jl b/src/types/wrappers/EqualGrowthCommunityModel.jl deleted file mode 100644 index 6e2b83d64..000000000 --- a/src/types/wrappers/EqualGrowthCommunityModel.jl +++ /dev/null @@ -1,111 +0,0 @@ -""" -$(TYPEDEF) - -A wrapper around [`CommunityModel`](@ref) that returns a community model where -the growth rates of all members are constrained to be equal to -`community_objective_id`, which is the community growth rate. The objective of -the resultant model is set to this `community_objective_id`. - -# Notes -1. No biomass metabolite exists (and none are created). - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct EqualGrowthCommunityModel <: AbstractModelWrapper - inner::CommunityModel - community_objective_id::String = "community_biomass" -end - -Accessors.unwrap_model(model::EqualGrowthCommunityModel) = model.inner - -Accessors.variables(cm::EqualGrowthCommunityModel) = - [variables(cm.inner); cm.community_objective_id] - -Accessors.n_variables(cm::EqualGrowthCommunityModel) = n_variables(cm.inner) + 1 - -Accessors.metabolites(cm::EqualGrowthCommunityModel) = - [metabolites(cm.inner); [m.id for m in cm.inner.members]] - -Accessors.n_metabolites(cm::EqualGrowthCommunityModel) = - n_metabolites(cm.inner) + length(cm.inner.members) - -Accessors.balance(cm::EqualGrowthCommunityModel) = [ - balance(cm.inner) - spzeros(length(cm.inner.members)) -] - -function Accessors.stoichiometry(cm::EqualGrowthCommunityModel) - - S = stoichiometry(cm.inner) - obj_col = spzeros(size(S, 1)) - - biomass_ids = [ - cm.inner.name_lookup[id][:variables][m.biomass_reaction_id] for - (id, m) in cm.inner.members - ] - biomass_idxs = indexin(biomass_ids, variables(cm.inner)) - - obj_links = sparse( - 1:length(biomass_idxs), - biomass_idxs, - ones(length(biomass_idxs)), - length(cm.inner.members), - size(S, 2), - ) - - obj = -ones(length(cm.inner.members)) - - return [ - S obj_col - obj_links obj - ] -end - -function Accessors.bounds(cm::EqualGrowthCommunityModel) - lbs, ubs = bounds(cm.inner) - return ([lbs; 0], [ubs; constants.default_reaction_bound]) -end - -function Accessors.objective(cm::EqualGrowthCommunityModel) - vec = spzeros(n_variables(cm)) # overwrite objective - vec[end] = 1.0 - return vec -end - -Accessors.coupling(cm::EqualGrowthCommunityModel) = - [coupling(cm.inner) spzeros(n_coupling_constraints(cm.inner))] - - -function Accessors.reaction_variables(cm::EqualGrowthCommunityModel) - r_v = reaction_variables(cm.inner) - r_v[cm.community_objective_id] = Dict(cm.community_objective_id => 1.0) - r_v -end - -Accessors.reactions(cm::EqualGrowthCommunityModel) = - [reactions(cm.inner); cm.community_objective_id] - -Accessors.n_reactions(cm::EqualGrowthCommunityModel) = n_reactions(cm.inner) + 1 - -Accessors.environmental_exchange_variables(model::EqualGrowthCommunityModel) = - environmental_exchange_variables(model.inner) - -Accessors.environmental_exchanges(model::EqualGrowthCommunityModel) = - environmental_exchanges(model.inner) - -Accessors.n_environmental_exchanges(model::EqualGrowthCommunityModel) = - n_environmental_exchanges(model.inner) - -Accessors.enzyme_variables(model::EqualGrowthCommunityModel) = enzyme_variables(model.inner) - -Accessors.enzymes(model::EqualGrowthCommunityModel) = enzymes(model.inner) - -Accessors.n_enzymes(model::EqualGrowthCommunityModel) = n_enzymes(model.inner) - -Accessors.enzyme_group_variables(model::EqualGrowthCommunityModel) = - enzyme_group_variables(model.inner) - -Accessors.enzyme_groups(model::EqualGrowthCommunityModel) = enzyme_groups(model.inner) - -Accessors.n_enzyme_groups(model::EqualGrowthCommunityModel) = n_enzyme_groups(model.inner) diff --git a/src/wrappers/MatrixCoupling.jl b/src/wrappers/MatrixCoupling.jl deleted file mode 100644 index ac982ab58..000000000 --- a/src/wrappers/MatrixCoupling.jl +++ /dev/null @@ -1,78 +0,0 @@ - -""" -$(TYPEDEF) - -A matrix-based wrap that adds reaction coupling matrix to the inner model. A -flux `x` feasible in this model must satisfy: -``` - cₗ ≤ C x ≤ cᵤ -``` - -# Fields -$(TYPEDFIELDS) -""" -mutable struct MatrixCoupling{M} <: AbstractModelWrapper where {M<:AbstractMetabolicModel} - lm::M - C::SparseMat - cl::Vector{Float64} - cu::Vector{Float64} - - function MatrixCoupling( - lm::M, - C::MatType, - cl::VecType, - cu::VecType, - ) where {M<:AbstractMetabolicModel} - length(cu) == length(cl) || - throw(DimensionMismatch("`cl` and `cu` need to have the same size")) - size(C) == (length(cu), n_variables(lm)) || - throw(DimensionMismatch("wrong dimensions of `C`")) - - new{M}(lm, sparse(C), collect(cl), collect(cu)) - end -end - -Accessors.unwrap_model(a::MatrixCoupling) = a.lm - -Accessors.coupling(a::MatrixCoupling)::SparseMat = vcat(coupling(a.lm), a.C) - -Accessors.n_coupling_constraints(a::MatrixCoupling)::Int = - n_coupling_constraints(a.lm) + size(a.C, 1) - -Accessors.coupling_bounds(a::MatrixCoupling)::Tuple{Vector{Float64},Vector{Float64}} = - vcat.(coupling_bounds(a.lm), (a.cl, a.cu)) - -function Base.convert( - ::Type{MatrixCoupling{M}}, - mm::AbstractMetabolicModel; - clone_coupling = true, -) where {M} - if mm isa MatrixCoupling{M} - mm - elseif mm isa MatrixCoupling - MatrixCoupling(convert(M, mm.lm), mm.C, mm.cl, mm.cu) - elseif clone_coupling - (cl, cu) = coupling_bounds(mm) - MatrixCoupling(convert(M, mm), coupling(mm), cl, cu) - else - MatrixCoupling(convert(M, mm), spzeros(0, n_variables(mm)), spzeros(0), spzeros(0)) - end -end - -""" - const MatrixModelWithCoupling = MatrixCoupling{MatrixModel} - -A matrix-based linear model with additional coupling constraints in the form: -``` - cₗ ≤ C x ≤ cᵤ -``` - -Internally, the model is implemented using [`MatrixCoupling`](@ref) that contains a single [`MatrixModel`](@ref). -""" -const MatrixModelWithCoupling = MatrixCoupling{MatrixModel} - -MatrixModelWithCoupling(lm::MatrixModel, C::MatType, cl::VecType, cu::VecType) = - MatrixCoupling(lm, sparse(C), collect(cl), collect(cu)) - -# these are special for MatrixModel-ish models -@inherit_model_methods MatrixModelWithCoupling (ridx::Int,) lm (ridx,) Accessors.reaction_stoichiometry diff --git a/src/wrappers/misc/MatrixModel.jl b/src/wrappers/misc/MatrixModel.jl deleted file mode 100644 index 3c854f917..000000000 --- a/src/wrappers/misc/MatrixModel.jl +++ /dev/null @@ -1,20 +0,0 @@ -Base.isequal(model1::MatrixModel, model2::MatrixModel) = - isequal(model1.S, model2.S) && - isequal(model1.b, model2.b) && - isequal(model1.c, model2.c) && - isequal(model1.xl, model2.xl) && - isequal(model1.xu, model2.xu) && - isequal(model1.rxns, model2.rxns) && - isequal(model1.mets, model2.mets) - -Base.copy(model::MatrixModel) = - MatrixModel(model.S, model.b, model.c, model.xl, model.xu, model.rxns, model.mets) - -Base.isequal(model1::MatrixModelWithCoupling, model2::MatrixModelWithCoupling) = - isequal(model1.lm, model2.lm) && - isequal(model1.C, model2.C) && - isequal(model1.cl, model2.cl) && - isequal(model1.cu, model2.cu) - -Base.copy(model::MatrixModelWithCoupling) = - MatrixModelWithCoupling(model.lm, model.C, model.cl, model.cu) From 0b5331c2c9da0f69ed602bfb4b9e623d9f382257 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 15 Sep 2023 08:36:25 +0200 Subject: [PATCH 330/531] clean up AFBCMs naming --- Project.toml | 1 + src/builders/core.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index a9f1f3edf..910771967 100644 --- a/Project.toml +++ b/Project.toml @@ -4,6 +4,7 @@ authors = ["The developers of COBREXA.jl"] version = "2.0.0" [deps] +AbstractFBCModels = "5a4f3dfa-1789-40f8-8221-69268c29937c" ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" diff --git a/src/builders/core.jl b/src/builders/core.jl index 9c71446b2..8100600ed 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,5 +1,5 @@ -import FBCModelInterface as F +import AbstractFBCModels as F import SparseArrays: sparse """ From eb1b2f7dbe7287e4a3232188ba2aa3995be246b5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 18 Sep 2023 20:29:01 +0200 Subject: [PATCH 331/531] add some sign splitting --- src/builders/core.jl | 80 +++++++++++++++++++++++++++++++++----------- src/solver.jl | 15 ++++++--- 2 files changed, 72 insertions(+), 23 deletions(-) diff --git a/src/builders/core.jl b/src/builders/core.jl index 8100600ed..7541d66fa 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -5,25 +5,67 @@ import SparseArrays: sparse """ $(TYPEDSIGNATURES) -Make a constraint tree that models the content of the given instance of +A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -C.make_constraint_tree(model::F.AbstractFBCModel) = let - rxns = Symbol.(F.reactions(model)), - mets = Symbol.(F.metabolites(model)), - lbs, ubs = F.bounds(model), - stoi = F.stoichiometry(model), - bal = F.balance(model), - obj = F.objective(model) - - # TODO coupling - :fluxes^C.allocate_variables( - keys=rxns, - bounds=zip(lbs, ubs), - ) * - :balance^C.make_constraint_tree( - m => Constraint(value=Value(sparse(row)), bound=b) for +metabolic_model(model::F.AbstractFBCModel) = + let + rxns = + Symbol.(F.reactions(model)), mets = + Symbol.(F.metabolites(model)), lbs, ubs = + F.bounds(model), stoi = + F.stoichiometry(model), bal = + F.balance(model), obj = F.objective(model) + + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :balance^C.ConstraintTree( + m => Constraint(value = Value(sparse(row)), bound = b) for (m, row, b) in zip(mets, eachrow(stoi), bals) - ) * - :objective^C.Constraint(value=C.Value(sparse(obj))) -end + ) * + :objective^C.Constraint(C.Value(sparse(obj))) + end + +""" +$(TYPEDSIGNATURES) + +Shortcut for allocation non-negative ("unsigned") variables. The argument +`keys` is forwarded to `ConstraintTrees.variables` as `keys`. +""" +unsigned_variables(; keys) = C.variables(; keys, bounds = Ref((0.0, Inf))) + +""" +$(TYPEDSIGNATURES) + +A constraint tree that bound the values present in `signed` to be sums of pairs +of `positive` and `negative` contributions to the individual values. + +Keys in the result are the same as the keys of `signed` constraints. + +Typically, this can be used to create "unidirectional" fluxes +together with [`unsigned_variables`](@ref): +``` +uvars = unsigned_variables(keys(myModel.fluxes)) + +myModel = myModel + + :fluxes_forward^uvars + + :fluxes_reverse^uvars + +myModel *= + :direction_sums^sign_split_constraints( + positive = myModel.fluxes_forward, + negative = myModel.fluxes_reverse, + signed = myModel.fluxes, + ) +``` +""" +sign_split_constraints(; + positive::C.ConstraintTree, + negative::C.ConstraintTree, + signed::C.ConstraintTree, +) = C.ConstraintTree( + C.Constraint( + value = s + (haskey(negative, k) ? negative[k].value : zero(C.Value)) - + (haskey(positive, k) ? positive[k].value : zero(C.Value)), + bound = 0.0, + ) for (k, s) in C.elems(signed) +) diff --git a/src/solver.jl b/src/solver.jl index 8ec037c37..e6d249cc0 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -12,7 +12,7 @@ function J.Model( constraints::C.ConstraintTree; objective::Maybe{C.Value} = nothing, optimizer, - sense = J.MAX_SENSE + sense = J.MAX_SENSE, ) model = J.Model(optimizer) J.@variable(model, x[1:C.var_count(cs)]) @@ -36,6 +36,13 @@ end """ $(TYPEDSIGNATURES) +Convenience re-export of `Model` from JuMP. +""" +const Model = J.Model + +""" +$(TYPEDSIGNATURES) + Convenience re-export of `optimize!` from JuMP. """ const optimize! = J.optimize! @@ -63,14 +70,14 @@ $(TYPEDSIGNATURES) The optimized variable assignment of a JuMP model, if solved. """ optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? J.value.(model[:x]) + is_solved(opt_model) ? J.value.(model[:x]) : nothing """ $(TYPEDSIGNATURES) Convenience overload for making solution trees out of JuMP models """ -C.solution_tree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = +C.SolutionTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = nothing let vars = optimized_variable_assignment(opt_model) - isnothing(vars) ? nothing : C.solution_tree(c, vars) + isnothing(vars) ? nothing : C.SolutionTree(c, vars) end From 59ad61457828347b21fa02d1c585a6270b1bc085 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 18 Sep 2023 22:32:39 +0200 Subject: [PATCH 332/531] add some basic builders --- src/builders/core.jl | 1 + src/builders/genes.jl | 15 +++++++++++++++ src/builders/objectives.jl | 15 +++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 src/builders/genes.jl create mode 100644 src/builders/objectives.jl diff --git a/src/builders/core.jl b/src/builders/core.jl index 7541d66fa..71232723d 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -57,6 +57,7 @@ myModel *= signed = myModel.fluxes, ) ``` +#TODO this might go to the docs """ sign_split_constraints(; positive::C.ConstraintTree, diff --git a/src/builders/genes.jl b/src/builders/genes.jl new file mode 100644 index 000000000..34ee47958 --- /dev/null +++ b/src/builders/genes.jl @@ -0,0 +1,15 @@ + +#TODO maybe separate this into simple boolean eval function and actual builder +knockout_constraint(ko_genes; fluxes::SolutionTree, reaction_gene_association) = + C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) + for rxn=keys(fluxes) + if begin + gss=reaction_gene_association(rxn) + if isnothing(gss) + false + else + all(gs -> any(g-> g in ko_genes, gs), gss) + end + end + ) diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl new file mode 100644 index 000000000..f3ceaf036 --- /dev/null +++ b/src/builders/objectives.jl @@ -0,0 +1,15 @@ + +sum_objective(x) = C.Constraint(sum(c.value for c=x)) +sum_objective(x::ConstraintTree) = sub_objective(values(x)) + +squared_error_objective(x) = C.Constraint(sum(c.value * c.value for c=x, init=zero(C.Value))) +squared_error_objective(x::ConstraintTree) = squared_error_objective(values(x)) + +squared_error_objective(constraints::Vector,target::Vector) = + C.Constraint( + sum(let tmp=(c.value - t); tmp*tmp; end for (c,t)=zip(constraints, target))) + +squared_error_objective(constraints::ConstraintTree, target) = + C.Constraint( + sum(let tmp=(c.value - target[k]); tmp*tmp; end for (k,c)=C.elems(constraints) if haskey(target, k)) + ) From eddd0acff84e1913a1e4ebbfd695e21830d77579 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 18 Sep 2023 22:33:36 +0200 Subject: [PATCH 333/531] add a todo --- src/solver.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/solver.jl b/src/solver.jl index e6d249cc0..fb7690e02 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -15,6 +15,7 @@ function J.Model( sense = J.MAX_SENSE, ) model = J.Model(optimizer) + #TODO add support for quadratic stuff (copy out of CTs docs) J.@variable(model, x[1:C.var_count(cs)]) isnothing(objective) || J.@objective(model, sense, C.value_product(objective, x)) function add_constraint(c::C.Constraint) From bac4ca4b6cb28fa8c2f096e559a58a95bd29639c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 18 Sep 2023 22:41:35 +0200 Subject: [PATCH 334/531] add enzymes skeleton + some basic todos --- src/builders/enzymes.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 src/builders/enzymes.jl diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl new file mode 100644 index 000000000..00e691d32 --- /dev/null +++ b/src/builders/enzymes.jl @@ -0,0 +1,13 @@ + +# the design space is: +# - total enzyme consumption y/n? (can be done very easily manually given the individual enzyme masses are present) +# - isozymes y/n (separates smoment from gecko) +# - mass groups y/n (this is basically summing up enzymes/isozymes by a label) +# - allow overproduction of enzymes (i.e., have extra variables for enzymes/isozymes to allow some slack) +# - anyone is probably able to do a partial sum over the above things themselves, we should make sure they squash well + +#TODO: this does not need the variables allocated, it's just a fancy product +enzyme_mass_sum(; forward_fluxes, reverse_fluxes, enzymes, reaction_enzyme_association) = missing + +isozyme_mass_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing +isozyme_mass_group_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing From dad432b29a63049ce1cfe2e951ded8e648bb83e1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 22 Sep 2023 10:58:17 +0200 Subject: [PATCH 335/531] format up --- src/builders/enzymes.jl | 3 ++- src/builders/genes.jl | 8 +++----- src/builders/objectives.jl | 23 ++++++++++++++--------- 3 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 00e691d32..6940ef791 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -7,7 +7,8 @@ # - anyone is probably able to do a partial sum over the above things themselves, we should make sure they squash well #TODO: this does not need the variables allocated, it's just a fancy product -enzyme_mass_sum(; forward_fluxes, reverse_fluxes, enzymes, reaction_enzyme_association) = missing +enzyme_mass_sum(; forward_fluxes, reverse_fluxes, enzymes, reaction_enzyme_association) = + missing isozyme_mass_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing isozyme_mass_group_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 34ee47958..d08d3455c 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -2,14 +2,12 @@ #TODO maybe separate this into simple boolean eval function and actual builder knockout_constraint(ko_genes; fluxes::SolutionTree, reaction_gene_association) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) - for rxn=keys(fluxes) - if begin - gss=reaction_gene_association(rxn) + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if begin + gss = reaction_gene_association(rxn) if isnothing(gss) false else - all(gs -> any(g-> g in ko_genes, gs), gss) + all(gs -> any(g -> g in ko_genes, gs), gss) end end ) diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index f3ceaf036..cb12bbd2d 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,15 +1,20 @@ -sum_objective(x) = C.Constraint(sum(c.value for c=x)) +sum_objective(x) = C.Constraint(sum(c.value for c in x)) sum_objective(x::ConstraintTree) = sub_objective(values(x)) -squared_error_objective(x) = C.Constraint(sum(c.value * c.value for c=x, init=zero(C.Value))) +squared_error_objective(x) = + C.Constraint(sum(c.value * c.value for c in x, init in zero(C.Value))) squared_error_objective(x::ConstraintTree) = squared_error_objective(values(x)) -squared_error_objective(constraints::Vector,target::Vector) = - C.Constraint( - sum(let tmp=(c.value - t); tmp*tmp; end for (c,t)=zip(constraints, target))) +squared_error_objective(constraints::Vector, target::Vector) = + C.Constraint(sum(let tmp = (c.value - t) + tmp * tmp + end for (c, t) in zip(constraints, target))) -squared_error_objective(constraints::ConstraintTree, target) = - C.Constraint( - sum(let tmp=(c.value - target[k]); tmp*tmp; end for (k,c)=C.elems(constraints) if haskey(target, k)) - ) +squared_error_objective(constraints::ConstraintTree, target) = C.Constraint( + sum( + let tmp = (c.value - target[k]) + tmp * tmp + end for (k, c) in C.elems(constraints) if haskey(target, k) + ), +) From 1de78997238925c1d0cc9aecb97ca6cd78735f0a Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 7 Oct 2023 14:13:13 +0200 Subject: [PATCH 336/531] intern the download-cache function --- src/COBREXA.jl | 1 + src/utils/downloads.jl | 39 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 40 insertions(+) create mode 100644 src/utils/downloads.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index cacd9f55e..bc1641c5b 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -10,5 +10,6 @@ import ConstraintTrees as C include("types.jl") include("solver.jl") include("builders/core.jl") #TODO more stuff +include("utils/downloads.jl") end # module COBREXA diff --git a/src/utils/downloads.jl b/src/utils/downloads.jl new file mode 100644 index 000000000..886c8c9f5 --- /dev/null +++ b/src/utils/downloads.jl @@ -0,0 +1,39 @@ + +import SHA + +function cached_download(url::AbstractString, path::AbstractString; + output=missing, + sha1=missing, + sha256=missing, + sha512=missing, + size=missing, + kwargs... +) + if !ismissing(output) + error("cached_download does not support the `output` keyword argument") + end + + ret = path + + if !isfile(path) + ret = Downloads.download(url; output=path, kwargs...) + end + + hash_matched = false + + check_hash(hi,f,h,verifies=true) = let cksum=bytes2hex(f(open(sbmlfile))) + if cksum != h + @warn "Downloaded file '$path' seems to be different from the expected one: got $hi '$cksum', expected $hi '$h'. This may be a cache corruption issue -- run `rm(\"$path\")` to flush the cache." + end + hash_checked ||= verifies + end + + ismissing(sha1) || check_hash(:SHA1, ComposedFunction(SHA.sha1, open), sha1) + ismissing(sha256) || check_hash(:SHA256, ComposedFunction(SHA.sha256, open), sha256) + ismissing(sha512) || check_hash(:SHA512, ComposedFunction(SHA.sha512, open), sha512) + ismissing(size) || check_hash(:size, filesize, size, false) + + hash_matched || @warn "Downloaded file '$path' was not checked against any checksum. Add some to improve reproducibility." + + return ret +end From 4a04dbdae84715ebc1e743a895ec8e1dba0f0ce1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 7 Oct 2023 14:47:30 +0200 Subject: [PATCH 337/531] support quadratic stuff --- Project.toml | 1 + src/solver.jl | 24 +++++++++++++++++++++--- 2 files changed, 22 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 910771967..c3bc487eb 100644 --- a/Project.toml +++ b/Project.toml @@ -18,6 +18,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] Clarabel = "0.3" +ConstraintTrees = "0.2" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" JuMP = "1" diff --git a/src/solver.jl b/src/solver.jl index fb7690e02..775900bd5 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -14,10 +14,18 @@ function J.Model( optimizer, sense = J.MAX_SENSE, ) + # TODO this might better have its own name to avoid type piracy. model = J.Model(optimizer) - #TODO add support for quadratic stuff (copy out of CTs docs) J.@variable(model, x[1:C.var_count(cs)]) - isnothing(objective) || J.@objective(model, sense, C.value_product(objective, x)) + + # objectives + if objective isa C.Value + JuMP.@objective(model, sense, C.value_product(objective, x)) + elseif objective isa C.QValue + JuMP.@objective(model, sense, C.qvalue_product(objective, x)) + end + + # constraints function add_constraint(c::C.Constraint) if c.bound isa Float64 J.@constraint(model, C.value_product(c.value, x) == c.bound) @@ -27,11 +35,21 @@ function J.Model( isinf(c.bound[2]) || J.@constraint(model, val <= c.bound[2]) end end + function add_constraint(c::C.QConstraint) + if c.bound isa Float64 + JuMP.@constraint(model, C.qvalue_product(c.qvalue, x) == c.bound) + elseif c.bound isa Tuple{Float64,Float64} + val = C.qvalue_product(c.qvalue, x) + isinf(c.bound[1]) || JuMP.@constraint(model, val >= c.bound[1]) + isinf(c.bound[2]) || JuMP.@constraint(model, val <= c.bound[2]) + end + end function add_constraint(c::C.ConstraintTree) add_constraint.(values(c)) end add_constraint(cs) - model + + return model end """ From 53d43e5955bd07f83830a7ff9f410ebe790eaf3b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 7 Oct 2023 14:58:20 +0200 Subject: [PATCH 338/531] some formatting --- src/solver.jl | 8 ++++---- src/utils/downloads.jl | 40 +++++++++++++++++++++++----------------- 2 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/solver.jl b/src/solver.jl index 775900bd5..220e69442 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -96,7 +96,7 @@ $(TYPEDSIGNATURES) Convenience overload for making solution trees out of JuMP models """ -C.SolutionTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = nothing -let vars = optimized_variable_assignment(opt_model) - isnothing(vars) ? nothing : C.SolutionTree(c, vars) -end +C.SolutionTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = + let vars = optimized_variable_assignment(opt_model) + isnothing(vars) ? nothing : C.SolutionTree(c, vars) + end diff --git a/src/utils/downloads.jl b/src/utils/downloads.jl index 886c8c9f5..e8de55444 100644 --- a/src/utils/downloads.jl +++ b/src/utils/downloads.jl @@ -1,39 +1,45 @@ import SHA - -function cached_download(url::AbstractString, path::AbstractString; - output=missing, - sha1=missing, - sha256=missing, - sha512=missing, - size=missing, - kwargs... +import Downloads + +function cached_download( + url::AbstractString, + path::AbstractString; + output = missing, + sha1 = missing, + sha256 = missing, + sha512 = missing, + size = missing, + kwargs..., ) - if !ismissing(output) + ismissing(output) || error("cached_download does not support the `output` keyword argument") - end ret = path if !isfile(path) - ret = Downloads.download(url; output=path, kwargs...) + ret = Downloads.download(url; output = path, kwargs...) end hash_matched = false - check_hash(hi,f,h,verifies=true) = let cksum=bytes2hex(f(open(sbmlfile))) + function check_hash(hi, f, h, verifies = true) + cksum = f(path) if cksum != h @warn "Downloaded file '$path' seems to be different from the expected one: got $hi '$cksum', expected $hi '$h'. This may be a cache corruption issue -- run `rm(\"$path\")` to flush the cache." end - hash_checked ||= verifies + hash_checked |= verifies end - ismissing(sha1) || check_hash(:SHA1, ComposedFunction(SHA.sha1, open), sha1) - ismissing(sha256) || check_hash(:SHA256, ComposedFunction(SHA.sha256, open), sha256) - ismissing(sha512) || check_hash(:SHA512, ComposedFunction(SHA.sha512, open), sha512) + compose(fs...) = foldl(ComposedFunction, fs) + + ismissing(sha1) || check_hash(:SHA1, compose(bytes2hex, SHA.sha1, open), sha1) + ismissing(sha256) || check_hash(:SHA256, compose(bytes2hex, SHA.sha256, open), sha256) + ismissing(sha512) || check_hash(:SHA512, compose(bytes2hex, SHA.sha512, open), sha512) ismissing(size) || check_hash(:size, filesize, size, false) - hash_matched || @warn "Downloaded file '$path' was not checked against any checksum. Add some to improve reproducibility." + hash_matched || + @warn "Downloaded file '$path' was not checked against any checksum. Add some to improve reproducibility." return ret end From 4c0c0a8c8dc76b2915823d5086558c8125e79e99 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 09:27:12 +0100 Subject: [PATCH 339/531] switch to newer ConstraintTrees --- Project.toml | 2 +- src/COBREXA.jl | 3 ++- src/builders/core.jl | 25 ++++++++++++------------- src/builders/objectives.jl | 7 +++---- src/solver.jl | 29 +++++++---------------------- test/log.jl | 13 ------------- 6 files changed, 25 insertions(+), 54 deletions(-) delete mode 100644 test/log.jl diff --git a/Project.toml b/Project.toml index c3bc487eb..7daf9d9a4 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] Clarabel = "0.3" -ConstraintTrees = "0.2" +ConstraintTrees = "0.4" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" JuMP = "1" diff --git a/src/COBREXA.jl b/src/COBREXA.jl index bc1641c5b..7cf3821a9 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -9,7 +9,8 @@ import ConstraintTrees as C include("types.jl") include("solver.jl") -include("builders/core.jl") #TODO more stuff +include("builders/core.jl") +include("builders/objectives.jl") include("utils/downloads.jl") end # module COBREXA diff --git a/src/builders/core.jl b/src/builders/core.jl index 71232723d..ddc3e9c50 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -8,22 +8,21 @@ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -metabolic_model(model::F.AbstractFBCModel) = - let - rxns = - Symbol.(F.reactions(model)), mets = - Symbol.(F.metabolites(model)), lbs, ubs = - F.bounds(model), stoi = - F.stoichiometry(model), bal = - F.balance(model), obj = F.objective(model) +function metabolic_model(model::F.AbstractFBCModel) + rxns = Symbol.(F.reactions(model)) + mets = Symbol.(F.metabolites(model)) + lbs, ubs = F.bounds(model) + stoi = F.stoichiometry(model) + bal = F.balance(model) + obj = F.objective(model) - :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + return :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * :balance^C.ConstraintTree( - m => Constraint(value = Value(sparse(row)), bound = b) for + m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for (m, row, b) in zip(mets, eachrow(stoi), bals) ) * - :objective^C.Constraint(C.Value(sparse(obj))) - end + :objective^C.Constraint(value = C.Value(sparse(obj))) +end """ $(TYPEDSIGNATURES) @@ -57,7 +56,6 @@ myModel *= signed = myModel.fluxes, ) ``` -#TODO this might go to the docs """ sign_split_constraints(; positive::C.ConstraintTree, @@ -70,3 +68,4 @@ sign_split_constraints(; bound = 0.0, ) for (k, s) in C.elems(signed) ) +#TODO the example above might as well go to docs diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index cb12bbd2d..34a53a8cf 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,9 +1,6 @@ -sum_objective(x) = C.Constraint(sum(c.value for c in x)) -sum_objective(x::ConstraintTree) = sub_objective(values(x)) - squared_error_objective(x) = - C.Constraint(sum(c.value * c.value for c in x, init in zero(C.Value))) + C.Constraint(sum((c.value * c.value for c in x), init = zero(C.Value))) squared_error_objective(x::ConstraintTree) = squared_error_objective(values(x)) squared_error_objective(constraints::Vector, target::Vector) = @@ -18,3 +15,5 @@ squared_error_objective(constraints::ConstraintTree, target) = C.Constraint( end for (k, c) in C.elems(constraints) if haskey(target, k) ), ) + +# TODO use `mergewith` to do this reasonably diff --git a/src/solver.jl b/src/solver.jl index 220e69442..2a1d7f06c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -9,41 +9,26 @@ JuMP `Model` created for solving in `optimizer`, with a given optional `objective` and optimization `sense`. """ function J.Model( - constraints::C.ConstraintTree; - objective::Maybe{C.Value} = nothing, + constraints::C.ConstraintTreeElem; + objective::Union{Nothing,C.LinearValue,C.QuadraticValue} = nothing, optimizer, sense = J.MAX_SENSE, ) # TODO this might better have its own name to avoid type piracy. model = J.Model(optimizer) J.@variable(model, x[1:C.var_count(cs)]) - - # objectives - if objective isa C.Value - JuMP.@objective(model, sense, C.value_product(objective, x)) - elseif objective isa C.QValue - JuMP.@objective(model, sense, C.qvalue_product(objective, x)) - end + JuMP.@objective(model, sense, C.substitute(objective, x)) # constraints function add_constraint(c::C.Constraint) if c.bound isa Float64 - J.@constraint(model, C.value_product(c.value, x) == c.bound) + J.@constraint(model, C.substitute(c.value, x) == c.bound) elseif c.bound isa C.IntervalBound - val = C.value_product(c.value, x) + val = C.substitute(c.value, x) isinf(c.bound[1]) || J.@constraint(model, val >= c.bound[1]) isinf(c.bound[2]) || J.@constraint(model, val <= c.bound[2]) end end - function add_constraint(c::C.QConstraint) - if c.bound isa Float64 - JuMP.@constraint(model, C.qvalue_product(c.qvalue, x) == c.bound) - elseif c.bound isa Tuple{Float64,Float64} - val = C.qvalue_product(c.qvalue, x) - isinf(c.bound[1]) || JuMP.@constraint(model, val >= c.bound[1]) - isinf(c.bound[2]) || JuMP.@constraint(model, val <= c.bound[2]) - end - end function add_constraint(c::C.ConstraintTree) add_constraint.(values(c)) end @@ -96,7 +81,7 @@ $(TYPEDSIGNATURES) Convenience overload for making solution trees out of JuMP models """ -C.SolutionTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.SolutionTree} = +C.ValueTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = let vars = optimized_variable_assignment(opt_model) - isnothing(vars) ? nothing : C.SolutionTree(c, vars) + isnothing(vars) ? nothing : C.ValueTree(c, vars) end diff --git a/test/log.jl b/test/log.jl deleted file mode 100644 index 702adff1e..000000000 --- a/test/log.jl +++ /dev/null @@ -1,13 +0,0 @@ -COBREXA.Log.Internal.@make_logging_tag TEST "testing stuff" - -log_TEST() -@testset "Logging on" begin - @test TEST_log_enabled - @test_logs (:warn, "qweasdzxc") @TEST_log @warn "qweasdzxc" -end - -log_TEST(false) -@testset "Logging off" begin - @test !TEST_log_enabled - @test_logs @TEST_log @warn "all okay!" -end From 668c5caf1c9830400622cb930040ce4981f5c1a5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 9 Nov 2023 17:46:55 +0100 Subject: [PATCH 340/531] delete src and test and re-org --- src/COBREXA.jl | 1 + src/analysis/modifications/community.jl | 53 --- src/analysis/modifications/loopless.jl | 57 --- .../gapfill_minimum_reactions.jl | 210 --------- src/io/h5.jl | 54 --- src/io/io.jl | 82 ---- src/io/json.jl | 26 -- src/io/mat.jl | 37 -- src/io/misc/h5.jl | 32 -- src/io/sbml.jl | 25 -- src/misc/checkkeys.jl | 101 ----- src/misc/constants.jl | 66 --- src/misc/guesskey.jl | 36 -- src/misc/identifiers.jl | 64 --- src/misc/ontology/SBOTerms.jl | 98 ----- src/reconstruction/CommunityModel.jl | 105 ----- src/reconstruction/enzyme_constrained.jl | 192 -------- .../simplified_enzyme_constrained.jl | 147 ------- src/reconstruction/thermodynamic.jl | 8 - src/solver.jl | 2 - src/utils/Annotation.jl | 61 --- src/utils/Reaction.jl | 155 ------- src/utils/Serialized.jl | 33 -- src/utils/bounds.jl | 23 - src/utils/downloads.jl | 45 -- src/utils/enzyme_constrained.jl | 16 - src/utils/flux_summary.jl | 184 -------- src/utils/fluxes.jl | 69 --- src/utils/looks_like.jl | 153 ------- src/wrappers/EnzymeConstrainedModel.jl | 187 -------- src/wrappers/MaxMinDrivingForceModel.jl | 239 ---------- src/wrappers/MinimizeDistance.jl | 133 ------ .../SimplifiedEnzymeConstrainedModel.jl | 137 ------ src/wrappers/bits/enzyme_constrained.jl | 51 --- src/wrappers/misc/enzyme_constrained.jl | 117 ----- src/wrappers/misc/mmdf.jl | 23 - test/io/h5.jl | 31 -- test/io/io.jl | 29 -- test/io/json.jl | 24 - test/io/mat.jl | 22 - test/io/sbml.jl | 33 -- test/reconstruction/MatrixCoupling.jl | 193 -------- test/reconstruction/MatrixModel.jl | 275 ------------ test/reconstruction/ObjectModel.jl | 199 --------- test/reconstruction/SerializedModel.jl | 15 - test/reconstruction/constrained_allocation.jl | 84 ---- test/reconstruction/enzyme_constrained.jl | 207 --------- .../gapfill_minimum_reactions.jl | 54 --- test/reconstruction/knockouts.jl | 82 ---- .../simplified_enzyme_constrained.jl | 114 ----- test/runtests.jl | 26 +- test/types/CommunityModel.jl | 416 ------------------ test/types/FluxSummary.jl | 25 -- test/types/FluxVariabilitySummary.jl | 23 - test/types/Gene.jl | 32 -- test/types/JSONModel.jl | 16 - test/types/MATModel.jl | 18 - test/types/MatrixCoupling.jl | 20 - test/types/MatrixModel.jl | 19 - test/types/Metabolite.jl | 31 -- test/types/ModelWithResult.jl | 5 - test/types/ObjectModel.jl | 152 ------- test/types/Reaction.jl | 85 ---- test/types/SBMLModel.jl | 36 -- test/types/abstract/AbstractMetabolicModel.jl | 12 - test/types/misc/gene_associations.jl | 17 - test/utils/MatrixModel.jl | 16 - test/utils/ObjectModel.jl | 25 -- test/utils/Serialized.jl | 25 -- test/utils/fluxes.jl | 24 - test/utils/looks_like.jl | 93 ---- test/utils/reaction.jl | 48 -- 72 files changed, 2 insertions(+), 5546 deletions(-) delete mode 100644 src/analysis/modifications/community.jl delete mode 100644 src/analysis/modifications/loopless.jl delete mode 100644 src/analysis/reconstruction/gapfill_minimum_reactions.jl delete mode 100644 src/io/h5.jl delete mode 100644 src/io/io.jl delete mode 100644 src/io/json.jl delete mode 100644 src/io/mat.jl delete mode 100644 src/io/misc/h5.jl delete mode 100644 src/io/sbml.jl delete mode 100644 src/misc/checkkeys.jl delete mode 100644 src/misc/constants.jl delete mode 100644 src/misc/guesskey.jl delete mode 100644 src/misc/identifiers.jl delete mode 100644 src/misc/ontology/SBOTerms.jl delete mode 100644 src/reconstruction/CommunityModel.jl delete mode 100644 src/reconstruction/enzyme_constrained.jl delete mode 100644 src/reconstruction/simplified_enzyme_constrained.jl delete mode 100644 src/reconstruction/thermodynamic.jl delete mode 100644 src/utils/Annotation.jl delete mode 100644 src/utils/Reaction.jl delete mode 100644 src/utils/Serialized.jl delete mode 100644 src/utils/bounds.jl delete mode 100644 src/utils/downloads.jl delete mode 100644 src/utils/enzyme_constrained.jl delete mode 100644 src/utils/flux_summary.jl delete mode 100644 src/utils/fluxes.jl delete mode 100644 src/utils/looks_like.jl delete mode 100644 src/wrappers/EnzymeConstrainedModel.jl delete mode 100644 src/wrappers/MaxMinDrivingForceModel.jl delete mode 100644 src/wrappers/MinimizeDistance.jl delete mode 100644 src/wrappers/SimplifiedEnzymeConstrainedModel.jl delete mode 100644 src/wrappers/bits/enzyme_constrained.jl delete mode 100644 src/wrappers/misc/enzyme_constrained.jl delete mode 100644 src/wrappers/misc/mmdf.jl delete mode 100644 test/io/h5.jl delete mode 100644 test/io/io.jl delete mode 100644 test/io/json.jl delete mode 100644 test/io/mat.jl delete mode 100644 test/io/sbml.jl delete mode 100644 test/reconstruction/MatrixCoupling.jl delete mode 100644 test/reconstruction/MatrixModel.jl delete mode 100644 test/reconstruction/ObjectModel.jl delete mode 100644 test/reconstruction/SerializedModel.jl delete mode 100644 test/reconstruction/constrained_allocation.jl delete mode 100644 test/reconstruction/enzyme_constrained.jl delete mode 100644 test/reconstruction/gapfill_minimum_reactions.jl delete mode 100644 test/reconstruction/knockouts.jl delete mode 100644 test/reconstruction/simplified_enzyme_constrained.jl delete mode 100644 test/types/CommunityModel.jl delete mode 100644 test/types/FluxSummary.jl delete mode 100644 test/types/FluxVariabilitySummary.jl delete mode 100644 test/types/Gene.jl delete mode 100644 test/types/JSONModel.jl delete mode 100644 test/types/MATModel.jl delete mode 100644 test/types/MatrixCoupling.jl delete mode 100644 test/types/MatrixModel.jl delete mode 100644 test/types/Metabolite.jl delete mode 100644 test/types/ModelWithResult.jl delete mode 100644 test/types/ObjectModel.jl delete mode 100644 test/types/Reaction.jl delete mode 100644 test/types/SBMLModel.jl delete mode 100644 test/types/abstract/AbstractMetabolicModel.jl delete mode 100644 test/types/misc/gene_associations.jl delete mode 100644 test/utils/MatrixModel.jl delete mode 100644 test/utils/ObjectModel.jl delete mode 100644 test/utils/Serialized.jl delete mode 100644 test/utils/fluxes.jl delete mode 100644 test/utils/looks_like.jl delete mode 100644 test/utils/reaction.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 7cf3821a9..10e64ee0f 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -6,6 +6,7 @@ module COBREXA using DocStringExtensions import ConstraintTrees as C +import JuMP as J include("types.jl") include("solver.jl") diff --git a/src/analysis/modifications/community.jl b/src/analysis/modifications/community.jl deleted file mode 100644 index dd4cbd476..000000000 --- a/src/analysis/modifications/community.jl +++ /dev/null @@ -1,53 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Change the abundances of the community model within the solver. Only -[`CommunityModel`](@ref) and [`EqualGrowthCommunityModel`](@ref) are supported -at this time. -""" -modify_abundances(new_abundances::Vector{Float64}) = - (model, opt_model) -> begin - #= - Only support these community models because the location of the - environmental balances are known. - =# - model isa CommunityModel || - model isa EqualGrowthCommunityModel || - throw( - ArgumentError( - "Only CommunityModel and EqualGrowthCommunityModel are supported at this time.", - ), - ) - - check_abundances(new_abundances) # TODO consider removing: too pedantic - - env_rows = - model isa CommunityModel ? - environment_exchange_stoichiometry(model, new_abundances) : - environment_exchange_stoichiometry(model.inner, new_abundances) - env_link = spdiagm(sum(env_rows, dims = 2)[:]) - - n_vars = n_variables(model) - n_env_vars = length(model.environmental_links) - n_cons = length(opt_model[:mb]) - n_objs = model isa CommunityModel ? 0 : length(model.inner.members) - - row_offset = - model isa CommunityModel ? n_cons - n_env_vars : n_cons - n_env_vars - n_objs - - # fix abundance coefficients of species exchanges - for (i, j, v) in zip(findnz(env_rows)...) - ii = i + row_offset - set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][j], v) - end - - column_offset = - model isa CommunityModel ? n_vars - n_env_vars : n_vars - n_env_vars - 1 - - # fix total abundance to link exchange - for (i, j, v) in zip(findnz(env_link)...) - jj = j + column_offset - ii = i + row_offset - set_normalized_coefficient(opt_model[:mb][ii], opt_model[:x][jj], -v) - end - end diff --git a/src/analysis/modifications/loopless.jl b/src/analysis/modifications/loopless.jl deleted file mode 100644 index 8c17e57a4..000000000 --- a/src/analysis/modifications/loopless.jl +++ /dev/null @@ -1,57 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically -infeasible internal cycles can occur. Adds the following constraints to the problem: -``` --max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ --max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ -Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) -Nᵢₙₜ' × G = 0 -yᵢ ∈ {0, 1} -Gᵢ ∈ ℝ -i ∈ internal reactions -Nᵢₙₜ is the nullspace of the internal stoichiometric matrix -``` -Note, this modification introduces binary variables, so an optimization solver capable of -handing mixed integer problems needs to be used. The arguments `max_flux_bound` and -`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. - -For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination -of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical -journal, 2011`. -""" -add_loopless_constraints(; - max_flux_bound = constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = constants.loopless_strict_inequality_tolerance, -) = - (model, opt_model) -> begin - - internal_rxn_idxs = [ - ridx for (ridx, rid) in enumerate(variables(model)) if - !is_boundary(reaction_stoichiometry(model, rid)) - ] - - N_int = nullspace(Array(stoichiometry(model)[:, internal_rxn_idxs])) # no sparse nullspace function - - y = @variable(opt_model, y[1:length(internal_rxn_idxs)], Bin) - G = @variable(opt_model, G[1:length(internal_rxn_idxs)]) # approx ΔG for internal reactions - - x = opt_model[:x] - for (cidx, ridx) in enumerate(internal_rxn_idxs) - @constraint(opt_model, -max_flux_bound * (1 - y[cidx]) <= x[ridx]) - @constraint(opt_model, x[ridx] <= max_flux_bound * y[cidx]) - - @constraint( - opt_model, - -max_flux_bound * y[cidx] + strict_inequality_tolerance * (1 - y[cidx]) <= G[cidx] - ) - @constraint( - opt_model, - G[cidx] <= - -strict_inequality_tolerance * y[cidx] + max_flux_bound * (1 - y[cidx]) - ) - end - - @constraint(opt_model, N_int' * G .== 0) - end diff --git a/src/analysis/reconstruction/gapfill_minimum_reactions.jl b/src/analysis/reconstruction/gapfill_minimum_reactions.jl deleted file mode 100644 index f2cfd0c5f..000000000 --- a/src/analysis/reconstruction/gapfill_minimum_reactions.jl +++ /dev/null @@ -1,210 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Find a minimal set of reactions from `universal_reactions` that should be added -to `model` so that the model has a feasible solution with bounds on its -objective function given in `objective_bounds`. Weights of the added reactions -may be specified in `weights` to prefer adding reactions with lower weights. - -Internally, this builds and solves a mixed integer program, following -the method of Reed et al. (Reed, Jennifer L., et al. "Systems approach to -refining genome annotation." *Proceedings of the National Academy of Sciences* -(2006)). - -The function returns a solved JuMP optimization model, with the boolean -reaction inclusion indicators in variable vector `y`. Use -[`gapfilled_mask`](@ref) or [`gapfilled_rids`](@ref) to collect the reaction -information in Julia datatypes. - -To reduce the uncertainty in the MILP solver (and likely reduce the -complexity), you may put a limit on the size of the added reaction set in -`maximum_new_reactions`. - -# Common pitfalls - -If [`gapfill_minimum_reactions`](@ref) is supposed to generate any reasonable -output, the input model *MUST NOT* be feasible, otherwise there is "no work to -do" and no reactions are added. Notably, an inactive model (the flux is zero) -is considered to be feasible. If this is the case, [`gapfilled_rids`](@ref) -will return an empty vector (as opposed to `nothing`). - -To prevent this, you may need to modify the model to disallow the trivial -solutions (for example by putting a lower bound on reactions that you expect to -be working in the solved model, in a similar manner like how the ATP -maintenance reaction is bounded in E. Coli "core" model). The -`objective_bounds` parameter makes this easier by directly placing a bound on -the objective value of the model, which typically forces the model to be -active. - -The `maximum_new_reactions` parameter may have critical impact on performance -in some solvers, because (in a general worst case) there is -`2^maximum_new_reactions` model variants to be examined. Putting a hard limit -on the reaction count may serve as a heuristic that helps the solver not to -waste too much time solving impractically complex subproblems. -""" -function gapfill_minimum_reactions( - model::AbstractMetabolicModel, - universal_reactions::Vector{Reaction}, - optimizer; - objective_bounds = (constants.tolerance, constants.default_reaction_bound), - maximum_new_reactions = length(universal_reactions), - weights = fill(1.0, length(universal_reactions)), - modifications = [], -) - precache!(model) - - # constraints from universal reactions that can fill gaps - univs = _universal_stoichiometry(universal_reactions, metabolites(model)) - - # add space for additional metabolites and glue with the universal reaction - # stoichiometry - extended_stoichiometry = [[ - stoichiometry(model) - spzeros(length(univs.new_mids), n_variables(model)) - ] univs.stoichiometry] - - # make the model anew (we can't really use make_optimization_model because - # we need the balances and several other things completely removed. Could - # be solved either by parametrizing make_optimization_model or by making a - # tiny temporary wrapper for this. - # keep this in sync with src/base/solver.jl, except for adding balances. - opt_model = Model(optimizer) - @variable(opt_model, x[1:n_variables(model)]) - xl, xu = bounds(model) - @constraint(opt_model, lbs, xl .<= x) - @constraint(opt_model, ubs, x .<= xu) - - C = coupling(model) - isempty(C) || begin - cl, cu = coupling_bounds(model) - @constraint(opt_model, c_lbs, cl .<= C * x) - @constraint(opt_model, c_ubs, C * x .<= cu) - end - - # add the variables for new stuff - @variable(opt_model, ux[1:length(universal_reactions)]) # fluxes from universal reactions - @variable(opt_model, y[1:length(universal_reactions)], Bin) # indicators - - # combined metabolite balances - @constraint( - opt_model, - extended_stoichiometry * [x; ux] .== - [balance(model); zeros(length(univs.new_mids))] - ) - - # objective bounds - @constraint(opt_model, objective_bounds[1] <= objective(model)' * x) - @constraint(opt_model, objective_bounds[2] >= objective(model)' * x) - - # flux bounds of universal reactions with indicators - @constraint(opt_model, ulb, univs.lbs .* y .<= ux) - @constraint(opt_model, uub, univs.ubs .* y .>= ux) - - # minimize the total number of indicated reactions - @objective(opt_model, Min, weights' * y) - - # limit the number of indicated reactions - # (prevents the solver from exploring too far) - @constraint(opt_model, sum(y) <= maximum_new_reactions) - - # apply all modifications - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - return opt_model -end - -""" -$(TYPEDSIGNATURES) - -Get a `BitVector` of added reactions from the model solved by -[`gapfill_minimum_reactions`](@ref). The bit indexes correspond to the indexes -of `universal_reactions` given to the gapfilling function. In case the model is -not solved, this returns `nothing`. - -If this function returns a zero vector (instead of `nothing`), it is very -likely that the original model was already feasible and you may need to -constraint it more. Refer to "pitfalls" section in the documentation of -[`gapfill_minimum_reactions`](@ref) for more details. - -# Example - - gapfill_minimum_reactions(myModel, myReactions, Tulip.Optimizer) |> gapfilled_mask -""" -gapfilled_mask(opt_model)::BitVector = - is_solved(opt_model) ? value.(opt_model[:y]) .> 0 : nothing - -""" -$(TYPEDSIGNATURES) - -Utility to extract a short vector of IDs of the reactions added by the -gapfilling algorithm. Use with `opt_model` returned from -[`gapfill_minimum_reactions`](@ref). - -If this function returns an empty vector (instead of `nothing`), it is very -likely that the original model was already feasible and you may need to -constraint it more. Refer to "pitfalls" section in the documentation of -[`gapfill_minimum_reactions`](@ref) for more details. -""" -gapfilled_rids(opt_model, universal_reactions::Vector{Reaction}) = - let v = gapfilled_mask(opt_model) - isnothing(v) ? nothing : [rxn.id for rxn in universal_reactions[v]] - end - -""" -$(TYPEDSIGNATURES) - -Overload of [`gapfilled_rids`](@ref) that can be piped easily. - -# Example - - gapfill_minimum_reactions(myModel, myReactions, Tulip.Optimizer) |> gapfilled_rids(myReactions) -""" -gapfilled_rids(universal_reactions::Vector{Reaction}) = - opt_model -> gapfilled_rids(opt_model, universal_reactions) - -""" -$(TYPEDSIGNATURES) - -A helper function that constructs the stoichiometric matrix of a set of -`universal_reactions`. The order of the metabolites is determined with -`mids`, so that this stoichiometric matrix can be combined with -another one. -""" -function _universal_stoichiometry(urxns::Vector{Reaction}, mids::Vector{String}) - - # traversal over all elements in stoichiometry of universal_reactions - stoiMap(f) = [ - f(ridx, mid, stoi) for (ridx, rxn) in enumerate(urxns) for - (mid, stoi) in rxn.metabolites - ] - - # make an index and find new metabolites - met_id_lookup = Dict(mids .=> eachindex(mids)) - - new_mids = - collect(Set(filter(x -> !haskey(met_id_lookup, x), stoiMap((_, mid, _) -> mid)))) - all_mids = vcat(mids, new_mids) - - # remake the index with all metabolites - met_id_lookup = Dict(all_mids .=> eachindex(all_mids)) - - # build the result - return ( - stoichiometry = float.( - sparse( - stoiMap((_, mid, _) -> met_id_lookup[mid]), - stoiMap((ridx, _, _) -> ridx), - stoiMap((_, _, stoi) -> stoi), - length(all_mids), - length(urxns), - ), - ), - lbs = [rxn.lower_bound for rxn in urxns], - ubs = [rxn.upper_bound for rxn in urxns], - new_mids = new_mids, - ) -end diff --git a/src/io/h5.jl b/src/io/h5.jl deleted file mode 100644 index b71e05817..000000000 --- a/src/io/h5.jl +++ /dev/null @@ -1,54 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Return a HDF5Model associated with the given file. Does not actually load -anything (for efficiency) -- use [`precache!`](@ref) to start pulling data into -the memory. -""" -function load_h5_model(file_name::String)::HDF5Model - return HDF5Model(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Converts and writes a metabolic model to disk in the HDF5 format. - -Additionally returns an (uncached) [`HDF5Model`](@ref) that represents the -contents of the saved file. Because all HDF5-based models need to be backed by -disk storage, writing the data to disk (using this function) is the only way to -make new HDF5 models. -""" -function save_h5_model(model::AbstractMetabolicModel, file_name::String)::HDF5Model - rxns = variables(model) - rxnp = sortperm(rxns) - mets = metabolites(model) - metp = sortperm(mets) - h5open(file_name, "w") do f - write(f, "metabolites", mets[metp]) - write(f, "reactions", rxns[rxnp]) - h5_write_sparse(create_group(f, "balance"), balance(model)[metp]) - h5_write_sparse(create_group(f, "objective"), objective(model)[rxnp]) - h5_write_sparse(create_group(f, "stoichiometry"), stoichiometry(model)[metp, rxnp]) - let (lbs, ubs) = bounds(model) - write(f, "lower_bounds", lbs[rxnp]) - write(f, "upper_bounds", ubs[rxnp]) - end - end - # TODO: genes, gene_associations, compartments. Perhaps chemistry and others? - HDF5Model(file_name) -end - -""" -$(TYPEDSIGNATURES) - -Close (and un-cache) the [`HDF5Model`](@ref) data. This allows the associated -file to be opened for writing again. -""" -function Base.close(model::HDF5Model) - if !isnothing(model.h5) - close(model.h5) - model.h5 = nothing - end -end diff --git a/src/io/io.jl b/src/io/io.jl deleted file mode 100644 index 10182a267..000000000 --- a/src/io/io.jl +++ /dev/null @@ -1,82 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Generic function for loading models that chooses a specific loader function -based on the `extension` argument (e.g., `".xml"` chooses loading of the SBML -model format), or throws an error. By default the extension from `file_name` -is used. - -Currently, these model types are supported: - -- SBML models (`*.xml`, loaded with [`load_sbml_model`](@ref)) -- JSON models (`*.json`, loaded with [`load_json_model`](@ref)) -- MATLAB models (`*.mat`, loaded with [`load_mat_model`](@ref)) -- HDF5 models (`*.h5`, loaded with [`load_h5_model`](@ref)) -""" -function load_model( - file_name::String; - extension = last(splitext(file_name)), -)::AbstractMetabolicModel - - if extension == ".json" - return load_json_model(file_name) - elseif extension == ".xml" - return load_sbml_model(file_name) - elseif extension == ".mat" - return load_mat_model(file_name) - elseif extension == ".h5" - return load_h5_model(file_name) - else - throw(DomainError(extension, "Unknown file extension")) - end -end - -""" -$(TYPEDSIGNATURES) - -Helper function that loads the model using [`load_model`](@ref) and returns it -converted to `type`. - -# Example: - - load_model(MatrixModel, "mySBMLModel.xml") -""" -function load_model( - type::Type{T}, - file_name::String; - extension = last(splitext(file_name)), -)::T where {T<:AbstractMetabolicModel} - convert(type, load_model(file_name; extension)) -end - -""" -$(TYPEDSIGNATURES) - -Generic function for saving models that chooses a specific writer function from -the `extension` argument (such as `".xml"` for SBML format), or throws an -error. By default the extension from `file_name` is used. - -Currently, these model types are supported: - -- SBML models (`*.xml`, saved with [`save_sbml_model`](@ref)) -- JSON models (`*.json`, saved with [`save_json_model`](@ref)) -- MATLAB models (`*.mat`, saved with [`save_mat_model`](@ref)) -- HDF5 models (`*.h5`, saved with [`save_h5_model`](@ref)) -""" -function save_model( - model::AbstractMetabolicModel, - file_name::String; - extension = last(splitext(file_name)), -) - if extension == ".json" - return save_json_model(model, file_name) - elseif extension == ".xml" - return save_sbml_model(model, file_name) - elseif extension == ".mat" - return save_mat_model(model, file_name) - elseif extension == ".h5" - return save_h5_model(model, file_name) - else - throw(DomainError(extension, "Unknown file extension")) - end -end diff --git a/src/io/json.jl b/src/io/json.jl deleted file mode 100644 index 55269ad6d..000000000 --- a/src/io/json.jl +++ /dev/null @@ -1,26 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Load and return a JSON-formatted model that is stored in `file_name`. -""" -function load_json_model(filename::String)::JSONModel - return JSONModel(JSON.parsefile(filename)) -end - -""" -$(TYPEDSIGNATURES) - -Save a [`JSONModel`](@ref) in `model` to a JSON file `file_name`. - -In case the `model` is not `JSONModel`, it will be converted automatically. -""" -function save_json_model(model::AbstractMetabolicModel, file_name::String) - m = - typeof(model) == JSONModel ? model : - begin - @io_log @warn "Automatically converting $(typeof(model)) to JSONModel for saving, information may be lost." - convert(JSONModel, model) - end - - open(f -> JSON.print(f, m.json), file_name, "w") -end diff --git a/src/io/mat.jl b/src/io/mat.jl deleted file mode 100644 index eeb4d82c9..000000000 --- a/src/io/mat.jl +++ /dev/null @@ -1,37 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Load and return a MATLAB file `file_name` that contains a COBRA-compatible -model. -""" -function load_mat_model(file_name::String)::MATModel - model_pair = first(matread(file_name)) - @io_log @info "Loading MAT: taking a model with ID $(model_pair.first)" - return MATModel(model_pair.second) -end - -""" -$(TYPEDSIGNATURES) - -Save a [`MATModel`](@ref) in `model` to a MATLAB file `file_name` in a format -compatible with other MATLAB-based COBRA software. - -In case the `model` is not `MATModel`, it will be converted automatically. - -`model_name` is the identifier name for the whole model written to the MATLAB -file; defaults to just "model". -""" -function save_mat_model( - model::AbstractMetabolicModel, - file_path::String; - model_name = "model", -) - m = - typeof(model) == MATModel ? model : - begin - @io_log @warn "Automatically converting $(typeof(model)) to MATModel for saving, information may be lost." - convert(MATModel, model) - end - matwrite(file_path, Dict(model_name => m.mat)) -end diff --git a/src/io/misc/h5.jl b/src/io/misc/h5.jl deleted file mode 100644 index b24aefb4e..000000000 --- a/src/io/misc/h5.jl +++ /dev/null @@ -1,32 +0,0 @@ - -h5_mmap_nonempty(x) = length(x) > 0 ? HDF5.readmmap(x) : HDF5.read(x) - -function h5_write_sparse(g::HDF5.Group, v::SparseVector) - write(g, "n", v.n) - write(g, "nzind", v.nzind) - write(g, "nzval", v.nzval) -end - -function h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseVector} - n = read(g["n"]) - nzind = h5_mmap_nonempty(g["nzind"]) - nzval = h5_mmap_nonempty(g["nzval"]) - SparseVector{eltype(nzval),eltype(nzind)}(n, nzind, nzval) -end - -function h5_write_sparse(g::HDF5.Group, m::SparseMatrixCSC) - write(g, "m", m.m) - write(g, "n", m.n) - write(g, "colptr", m.colptr) - write(g, "rowval", m.rowval) - write(g, "nzval", m.nzval) -end - -function h5_read_sparse(::Type{X}, g::HDF5.Group) where {X<:SparseMatrixCSC} - m = read(g["m"]) - n = read(g["n"]) - colptr = h5_mmap_nonempty(g["colptr"]) - rowval = h5_mmap_nonempty(g["rowval"]) - nzval = h5_mmap_nonempty(g["nzval"]) - SparseMatrixCSC{eltype(nzval),eltype(colptr)}(m, n, colptr, rowval, nzval) -end diff --git a/src/io/sbml.jl b/src/io/sbml.jl deleted file mode 100644 index e61792b9e..000000000 --- a/src/io/sbml.jl +++ /dev/null @@ -1,25 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Load and return a SBML XML model in `file_name`. -""" -function load_sbml_model(file_name::String)::SBMLModel - return SBMLModel(SBML.readSBML(file_name)) -end - -""" -$(TYPEDSIGNATURES) - -Write a given SBML model to `file_name`. -""" -function save_sbml_model(model::AbstractMetabolicModel, file_name::String) - m = - typeof(model) == SBMLModel ? model : - begin - @io_log @warn "Automatically converting $(typeof(model)) to SBMLModel for saving, information may be lost." - convert(SBMLModel, model) - end - - SBML.writeSBML(m.sbml, file_name) -end diff --git a/src/misc/checkkeys.jl b/src/misc/checkkeys.jl deleted file mode 100644 index 8d992ff92..000000000 --- a/src/misc/checkkeys.jl +++ /dev/null @@ -1,101 +0,0 @@ -""" -Throw a DomainError if `model.field` has any keys in the ID field of -xs::Union{Reaction, Metabolite, Gene}. -""" -function check_arg_keys_exists(model, field, xs) - d = getfield(model, field) - ids = [x.id for x in xs if haskey(d, x.id)] - isempty(ids) || - throw(DomainError("Duplicated $field IDs already present in model: $ids")) - nothing -end - -""" -Throw a DomainError if `model.field` does not have any keys in `xs`. -""" -function check_arg_keys_missing(model, field, xs::Vector{String}) - d = getfield(model, field) - ids = [x for x in xs if !haskey(d, x)] - isempty(ids) || throw(DomainError(ids, " $field IDs not found in model.")) - nothing -end - -""" -Throw a DomainError if rxn_ids have isozymes associated with them. -""" -function check_has_isozymes(model, rxn_ids) - ids = [rid for rid in rxn_ids if !isnothing(model.reactions[rid].gene_associations)] - isempty(ids) || throw(DomainError(ids, " reactions already have isozymes.")) - nothing -end - -""" -Throw a DomainError if the `biomass_rxn_id` is not in `model_reactions`. -""" -function check_has_biomass_rxn_id(model_reactions, biomass_rxn_id) - haskey(model_reactions, biomass_rxn_id) || - throw(DomainError(biomass_rxn_id, " not found in model.")) - nothing -end - -""" -Throw a DomainError if the `biomass_rxn_id` in `model_reactions` has any -isozymes assigned to it. -""" -function check_biomass_rxn_has_isozymes(model_reactions, biomass_rxn_id) - isnothing(model_reactions[biomass_rxn_id].gene_associations) || - throw(DomainError(biomass_rxn_id, " already has isozymes associated to it.")) - nothing -end - -""" -Throw a DomainError if `virtualribosome_id` is already in the `model_genes`. -""" -function check_has_virtualribosome(model_genes, virtualribosome_id) - haskey(model_genes, virtualribosome_id) && - throw(DomainError(virtualribosome_id, " already found in model.")) - nothing -end - -""" -Throw a DomainError if `biomass_rxn_id` in `model_reactions` already has a -`biomass_metabolite_id`. -""" -function check_has_biomass_rxn_biomas_metabolite( - model_reactions, - biomass_rxn_id, - biomass_metabolite_id, -) - haskey(model_reactions[biomass_rxn_id].metabolites, biomass_metabolite_id) || - throw(DomainError(biomass_metabolite_id, " not found in $biomass_rxn_id.")) - nothing -end - -""" -Throw a DomainError if some reaction ids `rids` are not found in the -environmental_links of `cm`. Return the indices of `rids` in the environmental -linkage vector. -""" -function check_environmental_ids(cm, rids) - env_rids = [envlink.reaction_id for envlink in cm.environmental_links] - idxs = indexin(rids, env_rids) - any(isnothing.(idxs)) && begin - missing_idxs = findall(isnothing, idxs) - throw( - DomainError( - rids[missing_idxs], - " exchange reaction IDs not found in environmental links.", - ), - ) - end - idxs -end - -""" -Check if `new_abundances` sums to 1. -""" -function check_abundances(new_abundances) - isapprox(sum(new_abundances), 1.0; atol = constants.tolerance) || - throw(DomainError(new_abundances, "The abundances do not sum to 1.")) - nothing -end diff --git a/src/misc/constants.jl b/src/misc/constants.jl deleted file mode 100644 index edfa157d2..000000000 --- a/src/misc/constants.jl +++ /dev/null @@ -1,66 +0,0 @@ - -""" -A named tuple that contains the magic values that are used globally for -whatever purposes. -""" -const constants = ( - default_reaction_bound = 1e3, - default_gene_product_bound = 1e3, - tolerance = 1e-6, - sampling_keep_iters = 100, - sampling_size = 1000, - loopless_strict_inequality_tolerance = 1, - extracellular_suffixes = ["_e", "[e]", "(e)"], - exchange_prefixes = ["EX_", "Exch_", "Ex_", "R_EX_", "R_Ex", "R_Exch_"], - biomass_strings = ["BIOMASS", "biomass", "Biomass"], - keynames = ( - rxns = ["reactions", "rxns", "RXNS", "REACTIONS", "Reactions", "Rxns"], - mets = ["metabolites", "mets", "METS", "METABOLITES", "Metabolites", "Mets"], - genes = ["genes", "GENES", "Genes"], - lbs = ["lbs", "lb", "lowerbounds", "lower_bounds"], - ubs = ["ubs", "ub", "upperbounds", "upper_bounds"], - stochiometry = ["S"], - balance = ["b"], - objective = ["c"], - grrs = ["gene_reaction_rules", "grRules", "rules"], - ids = ["id", "description"], - metformulas = ["metFormula", "metFormulas"], - metcharges = ["metCharge", "metCharges"], - metcompartments = ["metCompartment", "metCompartments", "metComp", "metComps"], - metcomptables = ["comps", "compNames"], - rxnnames = ["rxnNames"], - metnames = ["metNames"], - ), - gene_annotation_checks = ( - "ncbigene", - "ncbigi", - "refseq_locus_tag", - "refseq_name", - "refseq_synonym", - "uniprot", - ), - reaction_annotation_checks = ( - "bigg.reaction", - "biocyc", - "ec-code", - "kegg.reaction", - "metanetx.reaction", - "rhea", - "sabiork", - "seed.reaction", - ), - metabolite_annotation_checks = ( - "kegg.compound", - "bigg.metabolite", - "chebi", - "inchi_key", - "sabiork", - "hmdb", - "seed.compound", - "metanetx.chemical", - "reactome.compound", - "biocyc", - ), - T = 298.15, # Kelvin - R = 8.31446261815324e-3, # kJ/K/mol -) diff --git a/src/misc/guesskey.jl b/src/misc/guesskey.jl deleted file mode 100644 index 7194526ad..000000000 --- a/src/misc/guesskey.jl +++ /dev/null @@ -1,36 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Unfortunately, many model types that contain dictionares do not have -standardized field names, so we need to try a few possibilities and guess the -best one. The keys used to look for valid field names should be ideally -specified as constants in `src/base/constants.jl`. -""" -function guesskey(avail, possibilities) - x = intersect(possibilities, avail) - - if isempty(x) - @debug "could not find any of keys: $possibilities" - return nothing - end - - if length(x) > 1 - @debug "Possible ambiguity between keys: $x" - end - return x[1] -end - -""" -$(TYPEDSIGNATURES) - -Return `fail` if key in `keys` is not in `collection`, otherwise -return `collection[key]`. Useful if may different keys need to be -tried due to non-standardized model formats. -""" -function gets(collection, fail, keys) - for key in keys - haskey(collection, key) && return collection[key] - end - return fail -end diff --git a/src/misc/identifiers.jl b/src/misc/identifiers.jl deleted file mode 100644 index 08a0ff99c..000000000 --- a/src/misc/identifiers.jl +++ /dev/null @@ -1,64 +0,0 @@ -""" - module Identifiers - -This module exports interpretation of terms to classify reactions, metabolites, -genes, etc. If an subject has a matching annotation, then it is assumed that it -is part of the associated class of objects. Where possible, this is done using -the terms in module [`SBOTerms`](@ref). - -# Exports -$(EXPORTS) -""" -module Identifiers -using ..ModuleTools -@dse - -using ..SBOTerms - -const EXCHANGE_REACTIONS = [SBOTerms.EXCHANGE_REACTION] - -const TRANSPORT_REACTIONS = [ - SBOTerms.TRANSPORT_REACTION, - SBOTerms.TRANSCELLULAR_MEMBRANE_INFLUX_REACTION, - SBOTerms.TRANSCELLULAR_MEMBRANE_EFFLUX_REACTION, - SBOTerms.TRANSLOCATION_REACTION, - SBOTerms.COTRANSPORT_REACTION, - SBOTerms.ANTIPORTER_REACTION, - SBOTerms.SYMPORTER_REACTION, - SBOTerms.ACTIVE_TRANSPORT, - SBOTerms.PASSIVE_TRANSPORT, - SBOTerms.TRANSPORTER, -] - -const METABOLIC_REACTIONS = [SBOTerms.BIOCHEMICAL_REACTION] - -const BIOMASS_REACTIONS = [SBOTerms.BIOMASS_PRODUCTION] - -const ATP_MAINTENANCE_REACTIONS = [SBOTerms.ATP_MAINTENANCE] - -const PSEUDO_REACTIONS = [ - SBOTerms.EXCHANGE_REACTION, - SBOTerms.DEMAND_REACTION, - SBOTerms.BIOMASS_PRODUCTION, - SBOTerms.ATP_MAINTENANCE, - SBOTerms.PSEUDOREACTION, - SBOTerms.SINK_REACTION, -] - -const SPONTANEOUS_REACTIONS = [SBOTerms.SPONTANEOUS_REACTION] - -const METABOLITES = [SBOTerms.SIMPLE_CHEMICAL, SBOTerms.METABOLITE] - -const GENES = [SBOTerms.GENE] - -const REACTIONS = [ - EXCHANGE_REACTIONS - TRANSPORT_REACTIONS - METABOLIC_REACTIONS - BIOMASS_REACTIONS - ATP_MAINTENANCE_REACTIONS - PSEUDO_REACTIONS - SPONTANEOUS_REACTIONS -] - -end diff --git a/src/misc/ontology/SBOTerms.jl b/src/misc/ontology/SBOTerms.jl deleted file mode 100644 index c25dab963..000000000 --- a/src/misc/ontology/SBOTerms.jl +++ /dev/null @@ -1,98 +0,0 @@ -""" - module SBOTerms - -Several selected SBO terms that are recognized by COBREXA. For the full -ontology, see https://github.com/EBI-BioModels/SBO/blob/master/SBO_OBO.obo. - -If an SBO term appears here, it *may* be recognized in a function; if an SBO -term does not appear here, then it is *not* used in any COBREXA function. - -Mostly, the SBO terms are now used in module `Identifiers`, where they get -grouped semantically to allow other functions to classify reactions (such as -exchanges vs. biomass vs. normal reactions), metabolites, compartments, etc. - -# Exports -$(EXPORTS) -""" -module SBOTerms -using ..ModuleTools -@dse - -const FLUX_BALANCE_FRAMEWORK = "SBO:0000624" -const RESOURCE_BALANCE_FRAMEWORK = "SBO:0000692" -const CONSTRAINT_BASED_FRAMEWORK = "SBO:0000693" - -const PRODUCT = "SBO:0000011" -const CONCENTRATION_OF_PRODUCT = "SBO:0000512" -const SIDE_PRODUCT = "SBO:0000603" - -const SUBSTRATE = "SBO:0000015" -const CONCENTRATION_OF_SUBSTRATE = "SBO:0000515" -const SIDE_SUBSTRATE = "SBO:0000604" - -const ENZYME = "SBO:0000014" -const TOTAL_CONCENTRATION_OF_ENZYME = "SBO:0000300" - -const TRANSCRIPTION = "SBO:0000183" -const TRANSLATION = "SBO:0000184" - -const GENE = "SBO:0000243" -const METABOLITE = "SBO:0000299" -const MACROMOLECULE = "SBO:0000245" -const SIMPLE_CHEMICAL = "SBO:0000247" -const RIBONUCLEIC_ACID = "SBO:0000250" -const DEOXYRIBONUCLEIC_ACID = "SBO:0000251" -const TRANSFER_RNA = "SBO:0000313" -const RIBOSOMAL_RNA = "SBO:0000314" -const MESSENGER_RNA = "SBO:0000278" -const TRANSPORTER = "SBO:0000284" -const PROTEIN_COMPLEX = "SBO:0000297" - -const MOLECULAR_MASS = "SBO:0000647" -const CATALYTIC_RATE_CONSTANT = "SBO:0000025" # turnover number synonym -const CAPACITY = "SBO:0000661" -const MICHAELIS_CONSTANT = "SBO:0000027" -const MICHAELIS_CONSTANT_FOR_PRODUCT = "SBO:0000323" -const MICHAELIS_CONSTANT_FOR_SUBSTRATE = "SBO:0000322" -const INHIBITORY_CONSTANT = "SBO:0000261" - -const STOICHIOMETRIC_COEFFICIENT = "SBO:0000481" -const AND = "SBO:0000173" -const OR = "SBO:0000174" - -const PH = "SBO:0000304" -const IONIC_STRENGTH = "SBO:0000623" - -const THERMODYNAMIC_TEMPERATURE = "SBO:0000147" -const STANDARD_GIBBS_FREE_ENERGY_OF_REACTION = "SBO:0000583" -const GIBBS_FREE_ENERGY_OF_REACTION = "SBO:0000617" -const STANDARD_GIBBS_FREE_ENERGY_OF_FORMATION = "SBO:0000582" -const TRANSFORMED_STANDARD_GIBBS_FREE_ENERGY_CHANGE_OF_REACTION = "SBO:0000620" -const TRANSFORMED_GIBBS_FREE_ENERGY_CHANGE_OF_REACTION = "SBO:0000622" -const TRANSFORMED_STANDARD_GIBBS_FREE_ENERGY_OF_FORMATION = "SBO:0000621" - -const BIOCHEMICAL_OR_TRANSPORT_REACTION = "SBO:0000167" -const BIOCHEMICAL_REACTION = "SBO:0000176" -const TRANSPORT_REACTION = "SBO:0000655" -const TRANSCELLULAR_MEMBRANE_INFLUX_REACTION = "SBO:0000587" -const TRANSCELLULAR_MEMBRANE_EFFLUX_REACTION = "SBO:0000588" -const FLUX_BOUND = "SBO:0000625" -const DEFAULT_FLUX_BOUND = "SBO:0000626" -const EXCHANGE_REACTION = "SBO:0000627" -const DEMAND_REACTION = "SBO:0000628" -const BIOMASS_PRODUCTION = "SBO:0000629" -const ATP_MAINTENANCE = "SBO:0000630" -const PSEUDOREACTION = "SBO:0000631" -const SINK_REACTION = "SBO:0000632" -const SPONTANEOUS_REACTION = "SBO:0000672" -const TRANSLOCATION_REACTION = "SBO:0000185" -const COTRANSPORT_REACTION = "SBO:0000654" -const ANTIPORTER_REACTION = "SBO:0000660" -const SYMPORTER_REACTION = "SBO:0000659" - -const ACTIVE_TRANSPORT = "SBO:0000657" -const PASSIVE_TRANSPORT = "SBO:0000658" - -const SUBSYSTEM = "SBO:0000633" - -end diff --git a/src/reconstruction/CommunityModel.jl b/src/reconstruction/CommunityModel.jl deleted file mode 100644 index 1cc1eeabb..000000000 --- a/src/reconstruction/CommunityModel.jl +++ /dev/null @@ -1,105 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Change the abundances of `model` with `new_abundances` in place. -""" -function change_abundances!(model::CommunityModel, new_abundances::Vector{Float64}) - check_abundances(new_abundances) - model.abundances .= new_abundances - nothing -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copy of `model` with the abundances changed. -""" -function change_abundances(model::CommunityModel, new_abundances::Vector{Float64}) - check_abundances(new_abundances) - m = copy(model) - m.abundances = copy(model.abundances) - m.abundances .= new_abundances - m -end - -""" -$(TYPEDSIGNATURES) - -Change the environmental bound (environmental exchange) reaction `rid` in -`model`. If the associated bound is `nothing`, then it is not changed. -""" -change_environmental_bound!( - model::CommunityModel, - rid::String; - lower_bound = nothing, - upper_bound = nothing, -) = change_environmental_bounds!( - model, - [rid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], -) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`change_environmental_bound!`](@ref). -""" -function change_environmental_bounds!( - model::CommunityModel, - rids::Vector{String}; - lower_bounds = fill(nothing, length(rids)), - upper_bounds = fill(nothing, length(rids)), -) - idxs = check_environmental_ids(model, rids) - for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) - isnothing(lb) || (model.environmental_links[idx].lower_bound = lb) - isnothing(ub) || (model.environmental_links[idx].upper_bound = ub) - end -end - -""" -$(TYPEDSIGNATURES) - -Return a shallow copy of `model` with the environmental bound (environmental -exchange) reaction `rid` in `model` changed. If the associated bound is -`nothing`, then it is not changed. -""" -change_environmental_bound( - model::CommunityModel, - rid::String; - lower_bound = nothing, - upper_bound = nothing, -) = change_environmental_bounds( - model, - [rid]; - lower_bounds = [lower_bound], - upper_bounds = [upper_bound], -) - -""" -$(TYPEDSIGNATURES) - -Plural variant of [`change_environmental_bound`](@ref). -""" -function change_environmental_bounds( - model::CommunityModel, - rids::Vector{String}; - lower_bounds = fill(nothing, length(rids)), - upper_bounds = fill(nothing, length(rids)), -) - idxs = check_environmental_ids(model, rids) - m = copy(model) - m.environmental_links = copy(model.environmental_links) - for (idx, lb, ub) in zip(idxs, lower_bounds, upper_bounds) - m.environmental_links[idx] = copy(model.environmental_links[idx]) - m.environmental_links[idx].reaction_id = model.environmental_links[idx].reaction_id - m.environmental_links[idx].metabolite_id = - model.environmental_links[idx].metabolite_id - m.environmental_links[idx].lower_bound = - isnothing(lb) ? model.environmental_links[idx].lower_bound : lb - m.environmental_links[idx].upper_bound = - isnothing(ub) ? model.environmental_links[idx].upper_bound : ub - end - m -end diff --git a/src/reconstruction/enzyme_constrained.jl b/src/reconstruction/enzyme_constrained.jl deleted file mode 100644 index e7edbe8c0..000000000 --- a/src/reconstruction/enzyme_constrained.jl +++ /dev/null @@ -1,192 +0,0 @@ -# TODO move this to wrappers -""" -$(TYPEDSIGNATURES) - -Wrap a `model` into an [`EnzymeConstrainedModel`](@ref), following the structure -given by the GECKO algorithm (see [`EnzymeConstrainedModel`](@ref) documentation -for details). Multiple mass constraint groups can be placed on the model using -the keyword arguments. - -Parameters `gene_product_mass_groups` and `gene_product_mass_group_bounds` specify -the groups of gene products, and the respective total mass limit for each group. -Gene products that are not listed in any gene product mass group are ignored. - -For simplicity, specifying the `total_gene_product_mass_bound` argument -overrides the above arguments by internally specifying a single group called -`uncategorized` of all gene products, and acts like the maximum "enzyme -capacity" in the model. - -# Example -``` -ecmodel = make_enzyme_constrained_model( - model; - gene_product_mass_groups = Dict( - "membrane" => ["e1", "e2"], - "total" => ["e1", "e2", "e3"], - ), - gene_product_mass_group_bounds = Dict( - "membrane" => 0.2, - "total" => 0.5, - ), -) - -ecmodel2 = make_enzyme_constrained_model( - model; - total_gene_product_mass_bound = 0.5 -) -``` -""" -function make_enzyme_constrained_model( - model::AbstractMetabolicModel; - gene_product_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, - gene_product_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, - total_gene_product_mass_bound::Maybe{Float64} = nothing, -) - if !isnothing(total_gene_product_mass_bound) - (isnothing(gene_product_mass_groups) && isnothing(gene_product_mass_groups)) || - throw( - ArgumentError( - "argument values would be overwritten by total_gene_product_mass_bound!", - ), - ) - - gene_product_mass_groups = Dict("uncategorized" => genes(model)) - gene_product_mass_group_bounds = - Dict("uncategorized" => total_gene_product_mass_bound) - end - - if isnothing(gene_product_mass_groups) || isnothing(gene_product_mass_group_bounds) - throw(ArgumentError("Insufficient mass group specification!")) - end - - gpb_(gid) = (gene_product_lower_bound(model, gid), gene_product_upper_bound(model, gid)) - - gpmm_(gid) = gene_product_molar_mass(model, gid) - - columns = Vector{Wrappers.Internal.EnzymeConstrainedReactionColumn}() - coupling_row_reaction = Int[] - coupling_row_gene_product = Int[] - - gids = genes(model) - (lbs, ubs) = bounds(model) - rids = variables(model) - - gene_name_lookup = Dict(gids .=> 1:length(gids)) - gene_row_lookup = Dict{Int,Int}() - - for i = 1:n_variables(model) - isozymes = reaction_isozymes(model, rids[i]) - - if isnothing(isozymes) - push!( - columns, - Wrappers.Internal.EnzymeConstrainedReactionColumn( - i, - 0, - 0, - 0, - lbs[i], - ubs[i], - [], - ), - ) - continue - end - - # loop over both directions for all isozymes - for (lb, ub, kcatf, dir) in [ - (-ubs[i], -lbs[i], x -> x.kcat_backward, -1), - (lbs[i], ubs[i], x -> x.kcat_forward, 1), - ] - # The coefficients in the coupling matrix will be categorized in - # separate rows for negative and positive reactions. Surprisingly, - # we do not need to explicitly remember the bounds, because the - # ones taken from the original model are perfectly okay -- the - # "reverse" direction is unreachable because of individual - # bounds on split reactions, and the "forward" direction is - # properly negated in the reverse case to work nicely with the - # global lower bound. - reaction_coupling_row = - ub > 0 && length(isozymes) > 1 ? begin - push!(coupling_row_reaction, i) - length(coupling_row_reaction) - end : 0 - - # all isozymes in this direction - for (iidx, isozyme) in enumerate(isozymes) - kcat = kcatf(isozyme) - if ub > 0 && kcat > constants.tolerance - # prepare the coupling with gene product molar - gene_product_coupling = collect( - begin - gidx = gene_name_lookup[gene] - row_idx = if haskey(gene_row_lookup, gidx) - gene_row_lookup[gidx] - else - push!(coupling_row_gene_product, gidx) - gene_row_lookup[gidx] = - length(coupling_row_gene_product) - end - (row_idx, stoich / kcat) - end for (gene, stoich) in isozyme.gene_product_stoichiometry if - haskey(gene_name_lookup, gene) - ) - - # make a new column - push!( - columns, - Wrappers.Internal.EnzymeConstrainedReactionColumn( - i, - iidx, - dir, - reaction_coupling_row, - max(lb, 0), - ub, - gene_product_coupling, - ), - ) - end - end - end - end - - # prepare enzyme capacity constraints - mg_gid_lookup = Dict{String,Vector{String}}() - for gid in gids[coupling_row_gene_product] - for (mg, mg_gids) in gene_product_mass_groups # each gid can belong to multiple mass groups - gid ∉ mg_gids && continue - if haskey(mg_gid_lookup, mg) - push!(mg_gid_lookup[mg], gid) - else - mg_gid_lookup[mg] = [gid] - end - end - end - coupling_row_mass_group = Vector{Wrappers.Internal.EnzymeConstrainedCapacity}() - for (grp, gs) in mg_gid_lookup - idxs = [gene_row_lookup[x] for x in Int.(indexin(gs, gids))] - mms = gpmm_.(gs) - push!( - coupling_row_mass_group, - Wrappers.Internal.EnzymeConstrainedCapacity( - grp, - idxs, - mms, - gene_product_mass_group_bounds[grp], - ), - ) - end - - EnzymeConstrainedModel( - [ - Wrappers.Internal.enzyme_constrained_column_reactions(columns, model)' * - objective(model) - spzeros(length(coupling_row_gene_product)) - ], - columns, - coupling_row_reaction, - collect(zip(coupling_row_gene_product, gpb_.(gids[coupling_row_gene_product]))), - coupling_row_mass_group, - model, - ) -end diff --git a/src/reconstruction/simplified_enzyme_constrained.jl b/src/reconstruction/simplified_enzyme_constrained.jl deleted file mode 100644 index 3cde7ecdb..000000000 --- a/src/reconstruction/simplified_enzyme_constrained.jl +++ /dev/null @@ -1,147 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Wrap a `model` with a structure given by sMOMENT algorithm. Returns a -[`SimplifiedEnzymeConstrainedModel`](@ref) (see the documentation for details). -The sMOMENT algorithm only uses one [`Isozyme`](@ref) per reaction. If multiple -isozymes are present in `model`, the "fastest" isozyme will be used. This is -determined based on maximum kcat (forward or backward) divided by mass of the -isozyme. Multiple enzyme capacity constraint can be placed on the model using -the keyword arguments. - -Parameters `reaction_mass_groups` and `reaction_mass_group_bounds` specify -groups of reactions (by their IDs), and their respective total mass limit. -Reactions that are not listed in any reaction mass group are ignored (likewise -if they don't have isozymes). - -For simplicity, specifying the `total_reaction_mass_bound` argument overrides -the above arguments by internally specifying a single group on all reactions -that acts like a maximum "enzyme capacity" in the model. - -# Example -``` -ecmodel = make_simplified_enzyme_constrained_model( - model; - reaction_mass_groups = Dict( - "membrane" => ["r1", "r2"], - "total" => ["r1", "r2", "r3"], - ), - reaction_mass_group_bounds = Dict( - "membrane" => 0.2, - "total" => 0.5, - ), -) - -ecmodel2 = make_simplified_enzyme_constrained_model( - model; - total_reaction_mass_bound = 0.5 -) -``` -""" -function make_simplified_enzyme_constrained_model( - model::AbstractMetabolicModel; - total_reaction_mass_bound::Maybe{Float64} = nothing, - reaction_mass_groups::Maybe{Dict{String,Vector{String}}} = nothing, - reaction_mass_group_bounds::Maybe{Dict{String,Float64}} = nothing, -) - - if !isnothing(total_reaction_mass_bound) - (isnothing(reaction_mass_groups) && isnothing(reaction_mass_group_bounds)) || throw( - ArgumentError( - "argument values would be overwritten by total_reaction_mass_bound!", - ), - ) - - reaction_mass_groups = Dict("uncategorized" => variables(model)) - reaction_mass_group_bounds = Dict("uncategorized" => total_reaction_mass_bound) - end - - if isnothing(reaction_mass_groups) || isnothing(reaction_mass_group_bounds) - throw(ArgumentError("Insufficient mass group specification!")) - end - - # helper function to rank the isozymes by relative speed - speed_enzyme(model, isozyme) = - max(isozyme.kcat_forward, isozyme.kcat_backward) / sum( - count * gene_product_molar_mass(model, gid) for - (gid, count) in isozyme.gene_product_stoichiometry - ) - - # helper function to return the fastest isozyme or nothing - ris_(model, rid) = begin - isozymes = reaction_isozymes(model, rid) - isnothing(isozymes) && return nothing - argmax(isozyme -> speed_enzyme(model, isozyme), isozymes) - end - - columns = Vector{Wrappers.Internal.SimplifiedEnzymeConstrainedColumn}() - - bound_ids = keys(reaction_mass_group_bounds) - total_reaction_mass_bounds = collect(values(reaction_mass_group_bounds)) - - (lbs, ubs) = bounds(model) # TODO need a reaction_bounds accessor for full generality - rids = variables(model) # TODO needs to be reactions - - for i = 1:n_variables(model) # TODO this should be reactions - - isozyme = ris_(model, rids[i]) - - if isnothing(isozyme) - # non-enzymatic reaction (or a totally ignored one) - push!( - columns, - Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( - i, - 0, - lbs[i], - ubs[i], - 0, - Int64[], - ), - ) - continue - end - - mw = sum( - gene_product_molar_mass(model, gid) * ps for - (gid, ps) in isozyme.gene_product_stoichiometry - ) - - bidxs = [ - idx for - (idx, bid) in enumerate(bound_ids) if rids[i] in reaction_mass_groups[bid] - ] - - if min(lbs[i], ubs[i]) < 0 && isozyme.kcat_backward > constants.tolerance - # reaction can run in reverse - push!( - columns, - Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( - i, - -1, - max(-ubs[i], 0), - -lbs[i], - mw / isozyme.kcat_backward, - bidxs, - ), - ) - end - - if max(lbs[i], ubs[i]) > 0 && isozyme.kcat_forward > constants.tolerance - # reaction can run forward - push!( - columns, - Wrappers.Internal.SimplifiedEnzymeConstrainedColumn( - i, - 1, - max(lbs[i], 0), - ubs[i], - mw / isozyme.kcat_forward, - bidxs, - ), - ) - end - end - - return SimplifiedEnzymeConstrainedModel(columns, total_reaction_mass_bounds, model) -end diff --git a/src/reconstruction/thermodynamic.jl b/src/reconstruction/thermodynamic.jl deleted file mode 100644 index a2d35afa2..000000000 --- a/src/reconstruction/thermodynamic.jl +++ /dev/null @@ -1,8 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Construct a [`MaxMinDrivingForceModel`](@ref) so that max min driving force -analysis can be performed on `model`. -""" -make_max_min_driving_force_model(model::AbstractMetabolicModel; kwargs...) = - MaxMinDrivingForceModel(; inner = model, kwargs...) diff --git a/src/solver.jl b/src/solver.jl index 2a1d7f06c..0efabfeb3 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -1,6 +1,4 @@ -import JuMP as J - """ $(TYPEDSIGNATURES) diff --git a/src/utils/Annotation.jl b/src/utils/Annotation.jl deleted file mode 100644 index 9832d0d4c..000000000 --- a/src/utils/Annotation.jl +++ /dev/null @@ -1,61 +0,0 @@ - -_annotations(m::Metabolite) = m.annotations -_annotations(r::Reaction) = r.annotations -_annotations(g::Gene) = g.annotations - -""" -$(TYPEDSIGNATURES) - -Extract annotations from a dictionary of items `xs` and build an index that -maps annotation "kinds" (e.g. `"PubChem"`) to the mapping from the annotations -(e.g. `"COMPOUND_12345"`) to item IDs that carry the annotations. - -Function `annotations` is used to access the `Annotations` object in the -dictionary values. - -This is extremely useful for finding items by annotation data. -""" -function annotation_index( - xs::AbstractDict{String}; - annotations = _annotations, -)::Dict{String,Dict{String,Set{String}}} - res = Dict{String,Dict{String,Set{String}}}() - for (n, ax) in xs - a = annotations(ax) - for (k, anns) in a - if !haskey(res, k) - res[k] = Dict{String,Set{String}}() - end - for v in anns - if !haskey(res[k], v) - res[k][v] = Set([n]) - else - push!(res[k][v], n) - end - end - end - end - res -end - -""" -$(TYPEDSIGNATURES) - -Find items (genes, metabolites, ...) from the annotation index that are -identified non-uniquely by at least one of their annotations. - -This often indicates that the items are duplicate or miscategorized. -""" -function ambiguously_identified_items( - index::Dict{String,Dict{String,Set{String}}}, -)::Set{String} - res = Set{String}() - for (_, idents) in index - for (_, items) in idents - if length(items) > 1 - push!(res, items...) - end - end - end - res -end diff --git a/src/utils/Reaction.jl b/src/utils/Reaction.jl deleted file mode 100644 index 4379dc569..000000000 --- a/src/utils/Reaction.jl +++ /dev/null @@ -1,155 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Check if `rxn` already exists in `model`, but has another `id`. For a reaction -to be duplicated the substrates, their stoichiometric coefficients, and the -reaction direction needs to be exactly the same as some other reaction in -`model`. Returns a list of reaction `id`s that are duplicates of `rxn`, or an -empty list otherwise. - -# Notes - -This method ignores any reactions in `model` with the same `id` as `rxn`. This -is convenient to identify duplicate reactions. The reaction directions need to -be exactly the same, as in a forward and a reversible reaction are counted as -different reactions for example. - -See also: [`reaction_mass_balanced`](@ref) -""" -function reaction_is_duplicated( - model::ObjectModel, # TODO this can be generalized if getting the bounds of reaction with semantics gets finalized - rxn::Reaction, -) - duplicated_rxns = String[] - - for rid in reactions(model) - rid == rxn.id && continue - - rs = reaction_stoichiometry(model, rid) - - # metabolite ids the same - issetequal(keys(rxn.metabolites), keys(rs)) || continue - - # stoichiometry the same - dir_correction = - sign(rs[first(keys(rs))]) == sign(rxn.metabolites[first(keys(rs))]) ? 1 : -1 - all(dir_correction * rs[mid] == rxn.metabolites[mid] for mid in keys(rs)) || - continue - - # directions the same: A -> B forward and B <- A backward are the same - r = model.reactions[rid] - if dir_correction == 1 - sign(r.lower_bound) == sign(rxn.lower_bound) && - sign(r.upper_bound) == sign(rxn.upper_bound) && - push!(duplicated_rxns, rid) - else - sign(dir_correction * r.lower_bound) == sign(rxn.upper_bound) && - sign(dir_correction * r.upper_bound) == sign(rxn.lower_bound) && - push!(duplicated_rxns, rid) - end - end - - return duplicated_rxns -end - -""" -$(TYPEDSIGNATURES) - -Return true if the reaction denoted by `rxn_dict` is a boundary reaction, otherwise return false. -Checks if on boundary by inspecting the number of metabolites in `rxn_dict`. -Boundary reactions have only one metabolite, e.g. an exchange reaction, or a sink/demand reaction. -""" -function is_boundary(rxn_dict::Dict{String,Float64})::Bool - length(keys(rxn_dict)) == 1 -end - -is_boundary(model::AbstractMetabolicModel, rxn_id::String) = - is_boundary(reaction_stoichiometry(model, rxn_id)) - -is_boundary(rxn::Reaction) = is_boundary(rxn.metabolites) - -is_boundary(model::ObjectModel, rxn::Reaction) = is_boundary(rxn) # for consistency with functions below - -""" -$(TYPEDSIGNATURES) - -Returns a dictionary mapping the stoichiometry of atoms through a single reaction. Uses the -metabolite information in `model` to determine the mass balance. Accepts a reaction -dictionary, a reaction string id or a `Reaction` as an argument for `rxn`. - -See also: [`reaction_mass_balanced`](@ref) -""" -function reaction_atom_balance( - model::AbstractMetabolicModel, - reaction_dict::Dict{String,Float64}, -) - atom_balances = Dict{String,Float64}() - for (met, stoich_rxn) in reaction_dict - adict = metabolite_formula(model, met) - isnothing(adict) && - throw(ErrorException("Metabolite $met does not have a formula assigned to it.")) - for (atom, stoich_molecule) in adict - atom_balances[atom] = - get(atom_balances, atom, 0.0) + stoich_rxn * stoich_molecule - end - end - return atom_balances -end - -function reaction_atom_balance(model::AbstractMetabolicModel, rxn_id::String) - reaction_atom_balance(model, reaction_stoichiometry(model, rxn_id)) -end - -reaction_atom_balance(model::ObjectModel, rxn::Reaction) = - reaction_atom_balance(model, rxn.id) - -""" -$(TYPEDSIGNATURES) - -Checks if `rxn` is atom balanced. Returns a boolean for whether the reaction is balanced, -and the associated balance of atoms for convenience (useful if not balanced). Calls -`reaction_atom_balance` internally. - -See also: [`check_duplicate_reaction`](@ref), [`reaction_atom_balance`](@ref) -""" -reaction_mass_balanced(model::ObjectModel, rxn_id::String) = - all(values(reaction_atom_balance(model, rxn_id)) .== 0) - -reaction_mass_balanced(model::ObjectModel, rxn::Reaction) = - reaction_mass_balanced(model, rxn.id) - -reaction_mass_balanced(model::ObjectModel, reaction_dict::Dict{String,Float64}) = - all(values(reaction_atom_balance(model, reaction_dict)) .== 0) - -""" -$(TYPEDSIGNATURES) - -Return the reaction equation as a string. The metabolite strings can be manipulated by -setting `format_id`. - -# Example -``` -julia> req = Dict("coa_c" => -1, "for_c" => 1, "accoa_c" => 1, "pyr_c" => -1) -julia> stoichiometry_string(req) -"coa_c + pyr_c = for_c + accoa_c" - -julia> stoichiometry_string(req; format_id = x -> x[1:end-2]) -"coa + pyr = for + accoa" -``` -""" -function stoichiometry_string(req; format_id = x -> x) - count_prefix(n) = abs(n) == 1 ? "" : string(abs(n), " ") - substrates = - join((string(count_prefix(n), format_id(met)) for (met, n) in req if n < 0), " + ") - products = - join((string(count_prefix(n), format_id(met)) for (met, n) in req if n >= 0), " + ") - return substrates * " = " * products -end - -""" -$(TYPEDSIGNATURES) - -Alternative of [`stoichiometry_string`](@ref) take takes a `Reaction` as an argument. -""" -stoichiometry_string(rxn::Reaction; kwargs...) = - stoichiometry_string(rxn.metabolites; kwargs...) diff --git a/src/utils/Serialized.jl b/src/utils/Serialized.jl deleted file mode 100644 index 80a7c2c0a..000000000 --- a/src/utils/Serialized.jl +++ /dev/null @@ -1,33 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Serialize the `model` to file `filename`, returning a [`Serialized`](@ref) -model that can be loaded back transparently by [`precache!`](@ref). The result -does _not_ contain the actual model data that are deferred to the disk; it may -thus be used to save memory, or send the model efficiently to remote workers -within distributed shared-storage environments. - -The benefit of using this over "raw" `Serialization.serialize` is that the -resulting `Serialized` model will reload itself automatically with -[`precache!`](@ref) at first usage, which needs to be done manually when using -the `Serialization` package directly. -""" -function serialize_model( - model::MM, - filename::String, -)::Serialized{MM} where {MM<:AbstractMetabolicModel} - open(f -> serialize(f, model), filename, "w") - Serialized{MM}(filename) -end - -""" -$(TYPEDSIGNATURES) - -Specialization of [`serialize_model`](@ref) that prevents nested serialization -of already-serialized models. -""" -function serialize_model(model::Serialized, filename::String) - precache!(model) - serialize_model(model.m, filename) -end diff --git a/src/utils/bounds.jl b/src/utils/bounds.jl deleted file mode 100644 index 5ca724795..000000000 --- a/src/utils/bounds.jl +++ /dev/null @@ -1,23 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A bounds-generating function for [`flux_variability_analysis`](@ref) that -limits the objective value to be at least `gamma*Z₀`, as usual in COBRA -packages. Use as the `bounds` argument: -``` -flux_variability_analysis(model, some_optimizer; bounds = gamma_bounds(0.9)) -``` -""" -gamma_bounds(gamma) = z -> (gamma * z, Inf) - -""" -$(TYPEDSIGNATURES) - -A bounds-generating function for [`flux_variability_analysis`](@ref) that -limits the objective value to a small multiple of Z₀. Use as `bounds` argument, -similarly to [`gamma_bounds`](@ref). -""" -objective_bounds(tolerance) = z -> begin - vs = (z * tolerance, z / tolerance) - (minimum(vs), maximum(vs)) -end diff --git a/src/utils/downloads.jl b/src/utils/downloads.jl deleted file mode 100644 index e8de55444..000000000 --- a/src/utils/downloads.jl +++ /dev/null @@ -1,45 +0,0 @@ - -import SHA -import Downloads - -function cached_download( - url::AbstractString, - path::AbstractString; - output = missing, - sha1 = missing, - sha256 = missing, - sha512 = missing, - size = missing, - kwargs..., -) - ismissing(output) || - error("cached_download does not support the `output` keyword argument") - - ret = path - - if !isfile(path) - ret = Downloads.download(url; output = path, kwargs...) - end - - hash_matched = false - - function check_hash(hi, f, h, verifies = true) - cksum = f(path) - if cksum != h - @warn "Downloaded file '$path' seems to be different from the expected one: got $hi '$cksum', expected $hi '$h'. This may be a cache corruption issue -- run `rm(\"$path\")` to flush the cache." - end - hash_checked |= verifies - end - - compose(fs...) = foldl(ComposedFunction, fs) - - ismissing(sha1) || check_hash(:SHA1, compose(bytes2hex, SHA.sha1, open), sha1) - ismissing(sha256) || check_hash(:SHA256, compose(bytes2hex, SHA.sha256, open), sha256) - ismissing(sha512) || check_hash(:SHA512, compose(bytes2hex, SHA.sha512, open), sha512) - ismissing(size) || check_hash(:size, filesize, size, false) - - hash_matched || - @warn "Downloaded file '$path' was not checked against any checksum. Add some to improve reproducibility." - - return ret -end diff --git a/src/utils/enzyme_constrained.jl b/src/utils/enzyme_constrained.jl deleted file mode 100644 index 9a917b259..000000000 --- a/src/utils/enzyme_constrained.jl +++ /dev/null @@ -1,16 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Extract the total mass utilization in a solved [`SimplifiedEnzymeConstrainedModel`](@ref). -""" -gene_product_mass(model::SimplifiedEnzymeConstrainedModel, opt_model) = - is_solved(opt_model) ? - sum((col.capacity_required for col in model.columns) .* value.(opt_model[:x])) : nothing - -""" -$(TYPEDSIGNATURES) - -A pipe-able variant of [`gene_product_mass`](@ref). -""" -gene_product_mass(model::SimplifiedEnzymeConstrainedModel) = - x -> gene_product_mass(model, x) diff --git a/src/utils/flux_summary.jl b/src/utils/flux_summary.jl deleted file mode 100644 index 55a06fbf6..000000000 --- a/src/utils/flux_summary.jl +++ /dev/null @@ -1,184 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of fluxes into small, useful representation of the most -important information contained. Useful for pretty-printing and quickly -exploring the results. Internally this function uses -[`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments -passed to these functions. Use this if your model has non-standard ids for -reactions. Fluxes smaller than `small_flux_bound` are not stored, while fluxes -larger than `large_flux_bound` are only stored if `keep_unbounded` is `true`. - -# Example -``` -julia> sol = values_dict(:reaction, model, flux_balance_analysis(model, Tulip.Optimizer)) -julia> fr = flux_summary(sol) -Biomass: - BIOMASS_Ecoli_core_w_GAM: 0.8739 -Import: - EX_o2_e: -21.7995 - EX_glc__D_e: -10.0 - EX_nh4_e: -4.7653 - EX_pi_e: -3.2149 -Export: - EX_h_e: 17.5309 - EX_co2_e: 22.8098 - EX_h2o_e: 29.1758 -``` -""" -function flux_summary( - flux_result::Maybe{Dict{String,Float64}}; - exclude_exchanges = false, - exchange_prefixes = constants.exchange_prefixes, - biomass_strings = constants.biomass_strings, - exclude_biomass = false, - small_flux_bound = 1.0 / constants.default_reaction_bound^2, - large_flux_bound = constants.default_reaction_bound, - keep_unbounded = false, -) - isnothing(flux_result) && return FluxSummary() - - rxn_ids = collect(keys(flux_result)) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - ex_fluxes = [flux_result[k] for k in ex_rxns] - bmass_fluxes = [flux_result[k] for k in bmasses] - - idx_srt_fluxes = sortperm(ex_fluxes) - import_fluxes = [ - idx for - idx in idx_srt_fluxes if -large_flux_bound < ex_fluxes[idx] <= -small_flux_bound - ] - export_fluxes = [ - idx for - idx in idx_srt_fluxes if small_flux_bound < ex_fluxes[idx] <= large_flux_bound - ] - - if keep_unbounded - lower_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] <= -large_flux_bound] - upper_unbounded = - [idx for idx in idx_srt_fluxes if ex_fluxes[idx] >= large_flux_bound] - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip( - [ex_rxns[lower_unbounded]; ex_rxns[upper_unbounded]], - [ex_fluxes[lower_unbounded]; ex_fluxes[upper_unbounded]], - ) - ), - ) - else - return FluxSummary( - OrderedDict(k => v for (k, v) in zip(bmasses, bmass_fluxes)), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[import_fluxes], ex_fluxes[import_fluxes]) - ), - OrderedDict( - k => v for (k, v) in zip(ex_rxns[export_fluxes], ex_fluxes[export_fluxes]) - ), - OrderedDict{String,Float64}(), - ) - end -end - -""" -$(TYPEDSIGNATURES) - -Summarize a dictionary of flux dictionaries obtained eg. from -[`flux_variability_analysis_dict`](@ref). The simplified summary representation -is useful for pretty-printing and easily showing the most important results. - -Internally this function uses [`looks_like_biomass_reaction`](@ref) and -[`looks_like_exchange_reaction`](@ref). The corresponding keyword arguments are -passed to these functions. Use this if your model has an uncommon naming of -reactions. - -# Example -``` -julia> sol = flux_variability_analysis_dict(model, Gurobi.Optimizer; bounds = objective_bounds(0.99)) -julia> flux_res = flux_variability_summary(sol) -Biomass Lower bound Upper bound - BIOMASS_Ecoli_core_w_GAM: 0.8652 0.8652 -Exchange - EX_h2o_e: 28.34 28.34 - EX_co2_e: 22.0377 22.0377 - EX_o2_e: -22.1815 -22.1815 - EX_h_e: 17.3556 17.3556 - EX_glc__D_e: -10.0 -10.0 - EX_nh4_e: -4.8448 -4.8448 - EX_pi_e: -3.2149 -3.2149 - EX_for_e: 0.0 0.0 - ... ... ... -``` -""" -function flux_variability_summary( - flux_result::Tuple{Dict{String,Dict{String,Float64}},Dict{String,Dict{String,Float64}}}; - exclude_exchanges = false, - exchange_prefixes = constants.exchange_prefixes, - biomass_strings = constants.biomass_strings, - exclude_biomass = false, -) - isnothing(flux_result) && return FluxVariabilitySummary() - - rxn_ids = keys(flux_result[1]) - ex_rxns = filter( - x -> looks_like_exchange_reaction( - x, - exclude_biomass = exclude_biomass, - biomass_strings = biomass_strings, - exchange_prefixes = exchange_prefixes, - ), - rxn_ids, - ) - bmasses = filter( - x -> looks_like_biomass_reaction( - x; - exclude_exchanges = exclude_exchanges, - exchange_prefixes = exchange_prefixes, - biomass_strings = biomass_strings, - ), - rxn_ids, - ) - - biomass_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in bmasses - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - biomass_fluxes[rxn_id] = [lb, ub] - end - - ex_rxn_fluxes = Dict{String,Vector{Maybe{Float64}}}() - for rxn_id in ex_rxns - lb = isnothing(flux_result[1][rxn_id]) ? nothing : flux_result[1][rxn_id][rxn_id] - ub = isnothing(flux_result[2][rxn_id]) ? nothing : flux_result[2][rxn_id][rxn_id] - ex_rxn_fluxes[rxn_id] = [lb, ub] - end - - return FluxVariabilitySummary(biomass_fluxes, ex_rxn_fluxes) -end diff --git a/src/utils/fluxes.jl b/src/utils/fluxes.jl deleted file mode 100644 index 1c840a9aa..000000000 --- a/src/utils/fluxes.jl +++ /dev/null @@ -1,69 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Return two dictionaries of metabolite `id`s mapped to reactions that consume or -produce them, given the flux distribution supplied in `flux_dict`. -""" -function metabolite_fluxes(model::AbstractMetabolicModel, flux_dict::Dict{String,Float64}) - S = stoichiometry(model) - rids = variables(model) - mids = metabolites(model) - - producing = Dict{String,Dict{String,Float64}}() - consuming = Dict{String,Dict{String,Float64}}() - for (row, mid) in enumerate(mids) - for (col, rid) in enumerate(rids) - mf = flux_dict[rid] * S[row, col] - if mf < 0 # consuming rxn - if haskey(consuming, mid) - consuming[mid][rid] = mf - else - consuming[mid] = Dict(rid => mf) - end - elseif mf >= 0 # producing rxn - if haskey(producing, mid) - producing[mid][rid] = mf - else - producing[mid] = Dict(rid => mf) - end - end - end - end - return consuming, producing -end - -""" -$(TYPEDSIGNATURES) - -Return a dictionary mapping the flux of atoms across a flux solution given by -`reaction_fluxes` using the reactions in `model` to determine the appropriate stoichiometry. - -Note, this function ignores metabolites with no formula assigned to them, no error message -will be displayed. - -Note, if a model is mass balanced there should be not net flux of any atom. By removing -reactions from the flux_solution you can investigate how that impacts the mass balances. - -# Example -``` -# Find flux of Carbon through all metabolic reactions except the biomass reaction -delete!(fluxes, "BIOMASS_Ecoli_core_w_GAM") -atom_fluxes(model, fluxes)["C"] -``` -""" -function atom_fluxes(model::AbstractMetabolicModel, reaction_fluxes::Dict{String,Float64}) - rids = variables(model) - atom_flux = Dict{String,Float64}() - for (ridx, rid) in enumerate(rids) - haskey(reaction_fluxes, rid) || continue - rflux = reaction_fluxes[rid] - for (mid, mstoi) in reaction_stoichiometry(model, rid) - atoms = metabolite_formula(model, mid) - isnothing(atoms) && continue # missing formulas are ignored - for (atom, abundance) in atoms - atom_flux[atom] = get(atom_flux, atom, 0.0) + rflux * mstoi * abundance - end - end - end - return atom_flux -end diff --git a/src/utils/looks_like.jl b/src/utils/looks_like.jl deleted file mode 100644 index c38b6d279..000000000 --- a/src/utils/looks_like.jl +++ /dev/null @@ -1,153 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A predicate that matches reaction identifiers that look like -exchange or biomass reactions, given the usual naming schemes in common model -repositories. Exchange reactions are identified based on matching prefixes in -the set `exchange_prefixes` and biomass reactions are identified by looking for -occurences of `biomass_strings` in the reaction id. - -Also see [`find_exchange_reactions`](@ref). - -# Note -While `looks_like_exchange_reaction` is useful for heuristically finding a -reaction, it is preferable to use standardized terms for finding reactions (e.g. -SBO terms). See [`is_exchange_reaction`](@ref) for a more systematic -alternative. - -# Example -``` -findall(looks_like_exchange_reaction, variables(model)) # returns indices -filter(looks_like_exchange_reaction, variables(model)) # returns Strings - -# to use the optional arguments you need to expand the function's arguments -# using an anonymous function -findall(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns indices -filter(x -> looks_like_exchange_reaction(x; exclude_biomass=true), variables(model)) # returns Strings -``` -""" -function looks_like_exchange_reaction( - rxn_id::String; - exclude_biomass = false, - biomass_strings = constants.biomass_strings, - exchange_prefixes = constants.exchange_prefixes, -)::Bool - any(startswith(rxn_id, x) for x in exchange_prefixes) && - !(exclude_biomass && any(occursin(x, rxn_id) for x in biomass_strings)) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding exchange reaction indexes in a model; arguments are -forwarded to [`looks_like_exchange_reaction`](@ref). -""" -find_exchange_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_exchange_reaction(id; kwargs...), variables(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding exchange reaction identifiers in a model; arguments are -forwarded to [`looks_like_exchange_reaction`](@ref). -""" -find_exchange_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_exchange_reaction(id, kwargs...), variables(m)) - -""" -$(TYPEDSIGNATURES) - -A predicate that matches reaction identifiers that look like biomass reactions. -Biomass reactions are identified by looking for occurences of `biomass_strings` -in the reaction id. If `exclude_exchanges` is set, the strings that look like -exchanges (from [`looks_like_exchange_reaction`](@ref)) will not match. - -# Note -While `looks_like_biomass_reaction` is useful for heuristically finding a -reaction, it is preferable to use standardized terms for finding reactions (e.g. -SBO terms). See [`is_biomass_reaction`](@ref) for a more systematic -alternative. - -# Example -``` -filter(looks_like_biomass_reaction, variables(model)) # returns strings -findall(looks_like_biomass_reaction, variables(model)) # returns indices -``` -""" -function looks_like_biomass_reaction( - rxn_id::String; - exclude_exchanges = false, - exchange_prefixes = constants.exchange_prefixes, - biomass_strings = constants.biomass_strings, -)::Bool - any(occursin(x, rxn_id) for x in biomass_strings) && - !(exclude_exchanges && any(startswith(rxn_id, x) for x in exchange_prefixes)) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding biomass reaction indexes in a model; arguments are -forwarded to [`looks_like_biomass_reaction`](@ref). -""" -find_biomass_reactions(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding biomass reaction identifiers in a model; arguments are -forwarded to [`looks_like_biomass_reaction`](@ref). -""" -find_biomass_reaction_ids(m::AbstractMetabolicModel; kwargs...) = - filter(id -> looks_like_biomass_reaction(id; kwargs...), variables(m)) - -""" -$(TYPEDSIGNATURES) - -A predicate that matches metabolite identifiers that look like they are extracellular -metabolites. Extracellular metabolites are identified by `extracellular_suffixes` at the end of the -metabolite id. - -# Example -``` -filter(looks_like_extracellular_metabolite, metabolites(model)) # returns strings -findall(looks_like_extracellular_metabolite, metabolites(model)) # returns indices -``` -""" -function looks_like_extracellular_metabolite( - met_id::String; - extracellular_suffixes = constants.extracellular_suffixes, -)::Bool - any(endswith(met_id, x) for x in extracellular_suffixes) -end - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding extracellular metabolite indexes in a model; arguments are -forwarded to [`looks_like_extracellular_metabolite`](@ref). -""" -find_extracellular_metabolites(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) - -""" -$(TYPEDSIGNATURES) - -Shortcut for finding extracellular metabolite identifiers in a model; arguments are -forwarded to [`looks_like_extracellular_metabolite`](@ref). -""" -find_extracellular_metabolite_ids(m::AbstractMetabolicModel; kwargs...) = - findall(id -> looks_like_extracellular_metabolite(id; kwargs...), metabolites(m)) - -@_is_sbo_reaction_fn "exchange" Identifiers.EXCHANGE_REACTIONS -@_is_sbo_reaction_fn "transport" Identifiers.TRANSPORT_REACTIONS -@_is_sbo_reaction_fn "biomass" Identifiers.BIOMASS_REACTIONS -@_is_sbo_reaction_fn "atp_maintenance" Identifiers.ATP_MAINTENANCE_REACTIONS -@_is_sbo_reaction_fn "pseudo" Identifiers.PSEUDO_REACTIONS -@_is_sbo_reaction_fn "metabolic" Identifiers.METABOLIC_REACTIONS -@_is_sbo_reaction_fn "spontaneous" Identifiers.SPONTANEOUS_REACTIONS - -@_is_sbo_fn "gene" Identifiers.GENES -@_is_sbo_fn "metabolite" Identifiers.METABOLITES -@_is_sbo_fn "reaction" Identifiers.REACTIONS diff --git a/src/wrappers/EnzymeConstrainedModel.jl b/src/wrappers/EnzymeConstrainedModel.jl deleted file mode 100644 index 81c9581b3..000000000 --- a/src/wrappers/EnzymeConstrainedModel.jl +++ /dev/null @@ -1,187 +0,0 @@ - -""" -$(TYPEDEF) - -A model with complex enzyme concentration and capacity bounds, as described in -*Sánchez, Benjamín J., et al. "Improving the phenotype predictions of a yeast -genome-scale metabolic model by incorporating enzymatic constraints." Molecular -systems biology 13.8 (2017): 935.* - -Use [`make_enzyme_constrained_model`](@ref) or [`with_enzyme_constraints`](@ref) -to construct this kind of model. - -The model wraps another "internal" model, and adds following modifications: -- enzymatic reactions with known enzyme information are split into multiple - forward and reverse variants for each isozyme (affects the stoichiometric - matrix), -- each enzyme reaction may have multiple variants per isozyme, thus the - stoichiometric matrix will include all these virtual enzyme balances, -- reaction coupling is added to ensure the groups of isozyme reactions obey the - global reaction flux bounds from the original model (affects the coupling), -- gene concentrations specified by each reaction and its gene product - stoichiometry, can constrained by the user to reflect measurements, such as - from mass spectrometry (affects the simple bounds), -- additional coupling is added to simulate total masses of different proteins - grouped by type (e.g., membrane-bound and free-floating proteins), which can - be again constrained by the user (this is slightly generalized from original - GECKO algorithm, which only considers a single group of indiscernible - proteins). - -The structure contains fields `columns` that describe the contents of the -stoichiometry matrix columns, `coupling_row_reaction`, -`coupling_row_gene_product` and `coupling_row_mass_group` that describe -correspondence of the coupling rows to original model and determine the coupling -bounds (note: the coupling for gene product is actually added to stoichiometry, -not in [`coupling`](@ref)), and `inner`, which is the original wrapped model. -The `objective` of the model includes also the extra columns for individual -genes, as held by `coupling_row_gene_product`. - -Implementation exposes the split reactions (available as `variables(model)`), -but retains the original "simple" reactions accessible by [`reactions`](@ref). -The related constraints are implemented using [`coupling`](@ref) and -[`coupling_bounds`](@ref). - -To implement this wrapper for a model, the accessors -[`reaction_isozymes`](@ref), [`gene_product_lower_bound`](@ref), -[`gene_product_upper_bound](@ref), [`gene_product_molar_mass`](@ref), need to be -available. Additionally, the model needs to associate [`Isozyme`](@ref)s with -reactions. Reactions without enzymes, or those that should be ignored need to -return `nothing` when [`reaction_isozymes`](@ref) is called on them. - -# Fields -$(TYPEDFIELDS) -""" -struct EnzymeConstrainedModel <: AbstractModelWrapper - objective::SparseVec - columns::Vector{EnzymeConstrainedReactionColumn} - coupling_row_reaction::Vector{Int} - coupling_row_gene_product::Vector{Tuple{Int,Tuple{Float64,Float64}}} - coupling_row_mass_group::Vector{EnzymeConstrainedCapacity} - - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(model::EnzymeConstrainedModel) = model.inner - -function Accessors.stoichiometry(model::EnzymeConstrainedModel) - irrevS = stoichiometry(model.inner) * enzyme_constrained_column_reactions(model) - enzS = enzyme_constrained_gene_product_coupling(model) - [ - irrevS spzeros(size(irrevS, 1), size(enzS, 1)) - -enzS I(size(enzS, 1)) - ] -end - -Accessors.objective(model::EnzymeConstrainedModel) = model.objective - -function Accessors.variables(model::EnzymeConstrainedModel) - inner_reactions = variables(model.inner) - mangled_reactions = [ - enzyme_constrained_reaction_name( - inner_reactions[col.reaction_idx], - col.direction, - col.isozyme_idx, - ) for col in model.columns - ] - [mangled_reactions; genes(model)] -end - -Accessors.n_variables(model::EnzymeConstrainedModel) = - length(model.columns) + n_genes(model) - -function Accessors.bounds(model::EnzymeConstrainedModel) - lbs = [ - [col.lb for col in model.columns] - [lb for (_, (lb, _)) in model.coupling_row_gene_product] - ] - ubs = [ - [col.ub for col in model.columns] - [ub for (_, (_, ub)) in model.coupling_row_gene_product] - ] - (lbs, ubs) -end - -function Accessors.reaction_variables_matrix(model::EnzymeConstrainedModel) - rxnmat = - enzyme_constrained_column_reactions(model)' * reaction_variables_matrix(model.inner) - [ - rxnmat - spzeros(n_genes(model), size(rxnmat, 2)) - ] -end - -Accessors.reaction_variables(model::EnzymeConstrainedModel) = - Accessors.Internal.make_mapping_dict( - variables(model), - reactions(model), - reaction_variables_matrix(model), - ) # TODO currently inefficient - -Accessors.enzymes(model::EnzymeConstrainedModel) = genes(model) - -Accessors.n_enzymes(model::EnzymeConstrainedModel) = n_genes(model) - -Accessors.enzyme_variables(model::EnzymeConstrainedModel) = - Dict(gid => Dict(gid => gene_product_molar_mass(model, gid)) for gid in genes(model)) - -Accessors.enzyme_groups(model::EnzymeConstrainedModel) = - [grp.group_id for grp in model.coupling_row_mass_group] -Accessors.n_enzyme_groups(model::EnzymeConstrainedModel) = - length(model.coupling_row_mass_group) - -function Accessors.enzyme_group_variables(model::EnzymeConstrainedModel) - enz_ids = genes(model) - Dict( - grp.group_id => Dict( - enz_ids[idx] => mm for - (idx, mm) in zip(grp.gene_product_idxs, grp.gene_product_molar_masses) - ) for grp in model.coupling_row_mass_group - ) -end - -function Accessors.coupling(model::EnzymeConstrainedModel) - innerC = coupling(model.inner) * enzyme_constrained_column_reactions(model) - rxnC = enzyme_constrained_reaction_coupling(model) - enzcap = enzyme_constrained_mass_group_coupling(model) - [ - innerC spzeros(size(innerC, 1), n_genes(model)) - rxnC spzeros(size(rxnC, 1), n_genes(model)) - spzeros(length(model.coupling_row_mass_group), length(model.columns)) enzcap - ] -end - -Accessors.n_coupling_constraints(model::EnzymeConstrainedModel) = - n_coupling_constraints(model.inner) + - length(model.coupling_row_reaction) + - length(model.coupling_row_mass_group) - -function Accessors.coupling_bounds(model::EnzymeConstrainedModel) - (iclb, icub) = coupling_bounds(model.inner) - (ilb, iub) = bounds(model.inner) - return ( - vcat( - iclb, - ilb[model.coupling_row_reaction], - [0.0 for _ in model.coupling_row_mass_group], - ), - vcat( - icub, - iub[model.coupling_row_reaction], - [grp.group_upper_bound for grp in model.coupling_row_mass_group], - ), - ) -end - -Accessors.balance(model::EnzymeConstrainedModel) = - [balance(model.inner); spzeros(length(model.coupling_row_gene_product))] - -Accessors.n_genes(model::EnzymeConstrainedModel) = length(model.coupling_row_gene_product) - -Accessors.genes(model::EnzymeConstrainedModel) = - genes(model.inner)[[idx for (idx, _) in model.coupling_row_gene_product]] - -Accessors.metabolites(model::EnzymeConstrainedModel) = - [metabolites(model.inner); genes(model) .* "#enzyme_constrained"] - -Accessors.n_metabolites(model::EnzymeConstrainedModel) = - n_metabolites(model.inner) + n_genes(model) diff --git a/src/wrappers/MaxMinDrivingForceModel.jl b/src/wrappers/MaxMinDrivingForceModel.jl deleted file mode 100644 index d7931a118..000000000 --- a/src/wrappers/MaxMinDrivingForceModel.jl +++ /dev/null @@ -1,239 +0,0 @@ -""" -$(TYPEDEF) - -Return a [`MaxMinDrivingForceModel`](@ref) that can be used to perform max-min -driving force analysis. It is based on the work by Noor, et al., "Pathway -thermodynamics highlights kinetic obstacles in central metabolism.", PLoS -computational biology, 2014. - -When [`flux_balance_analysis`](@ref) is called in this type of model, the -max-min driving force algorithm is solved i.e. the objective of the model is to -find the maximum minimum Gibbs free energy of reaction across all reactions in -the model by changing the metabolite concentrations. The variables for max-min -driving force analysis are the actual maximum minimum driving force of the -model, the log metabolite concentrations, and the gibbs free energy reaction -potentials across each reaction. Reaction fluxes are assumed constant.The -optimization problem solved is: -``` -max min -ΔᵣG -s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) - ΔᵣG ≤ 0 - ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas -constant, T is the temperature, S is the stoichiometry of the model, and C is -the vector of metabolite concentrations (and their respective lower and upper -bounds). - -In case no feasible solution exists, `nothing` is returned. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` -*need* to be specified so that they can be ignored in the calculations. -Effectively this assumes an aqueous environment at constant pH is used. - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct MaxMinDrivingForceModel <: AbstractModelWrapper - "A dictionary mapping ΔrG⁰ to reactions." - reaction_standard_gibbs_free_energies::Dict{String,Float64} = Dict{String,Float64}() - - "A cycle-free reference reaction flux solution that is used to set the directions of the reactions. For example, this could be generated this using loopless FBA." - flux_solution::Dict{String,Float64} = Dict{String,Float64}() - - "Metabolite ids of protons." - proton_ids::Vector{String} = ["h_c", "h_e"] - - "Metabolite ids of water." - water_ids::Vector{String} = ["h2o_c", "h2o_e"] - - "A dictionationay mapping metabolite ids to concentrations that are held constant." - constant_concentrations::Dict{String,Float64} = Dict{String,Float64}() - - "A dictionary mapping metabolite ids to constant concentration ratios in the form `(m1, m2) = r === m1/m2 = r`." - concentration_ratios::Dict{Tuple{String,String},Float64} = - Dict{Tuple{String,String},Float64}() - - "Global metabolite concentration lower bound." - concentration_lb = 1e-9 - - "Global metabolite concentration upper bound." - concentration_ub = 100e-3 - - "Thermodynamic temperature." - T::Float64 = constants.T - - "Real gas constant." - R::Float64 = constants.R - - "Tolerance use to distinguish flux carrying reactions from zero flux reactions." - small_flux_tol::Float64 = 1e-6 - - "Maximum absolute ΔG bound allowed by a reaction." - max_dg_bound::Float64 = 1000.0 - - "Reaction ids that are ignored internally during thermodynamic calculations. This should include water and proton importers." - ignore_reaction_ids::Vector{String} = String[] - - "Inner metabolic model calculations are based on." - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(model::MaxMinDrivingForceModel) = model.inner - -Accessors.variables(model::MaxMinDrivingForceModel) = - ["mmdf"; "log " .* metabolites(model); "ΔG " .* reactions(model)] - -Accessors.n_variables(model::MaxMinDrivingForceModel) = - 1 + n_metabolites(model) + n_reactions(model) - -Accessors.metabolite_log_concentrations(model::MaxMinDrivingForceModel) = - "log " .* metabolites(model) -Accessors.n_metabolite_log_concentrations(model::MaxMinDrivingForceModel) = - n_metabolites(model) -Accessors.metabolite_log_concentration_variables(model::MaxMinDrivingForceModel) = - Dict(mid => Dict(mid => 1.0) for mid in "log " .* metabolites(model)) - -Accessors.gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = - "ΔG " .* reactions(model) -Accessors.n_gibbs_free_energy_reactions(model::MaxMinDrivingForceModel) = n_reactions(model) -Accessors.gibbs_free_energy_reaction_variables(model::MaxMinDrivingForceModel) = - Dict(rid => Dict(rid => 1.0) for rid in "ΔG " .* reactions(model)) - - -Accessors.objective(model::MaxMinDrivingForceModel) = - [1.0; fill(0.0, n_variables(model) - 1)] - -function Accessors.balance(model::MaxMinDrivingForceModel) - # proton water balance - num_proton_water = length(model.proton_ids) + length(model.water_ids) - proton_water_vec = spzeros(num_proton_water) - - # constant concentration balance - const_conc_vec = log.(collect(values(model.constant_concentrations))) - - # ratio balance - const_ratio_vec = log.(collect(values(model.concentration_ratios))) - - # give dummy dG0 for reactions that don't have data - dg0s = [ - get(model.reaction_standard_gibbs_free_energies, rid, 0.0) for - rid in reactions(model) - ] - - return [ - proton_water_vec - const_conc_vec - const_ratio_vec - dg0s - ] -end - -function Accessors.stoichiometry(model::MaxMinDrivingForceModel) - var_ids = Internal.original_variables(model) - - # set proton and water equality constraints - num_proton_water = length(model.proton_ids) + length(model.water_ids) - proton_water_mat = spzeros(num_proton_water, n_variables(model)) - idxs = indexin([model.proton_ids; model.water_ids], var_ids) - for (i, j) in enumerate(idxs) - isnothing(j) && throw(error("Water or proton ID not found in model.")) - proton_water_mat[i, j] = 1.0 - end - - # constant concentration constraints - const_conc_mat = spzeros(length(model.constant_concentrations), n_variables(model)) - ids = collect(keys(model.constant_concentrations)) - idxs = indexin(ids, var_ids) - for (i, j) in enumerate(idxs) - isnothing(j) && - throw(DomainError(ids[j], "Constant metabolite ID not found in model.")) - const_conc_mat[i, j] = 1.0 - end - - # add the relative bounds - const_ratio_mat = spzeros(length(model.concentration_ratios), n_variables(model)) - for (i, (mid1, mid2)) in enumerate(keys(model.concentration_ratios)) - idxs = indexin([mid1, mid2], var_ids) - any(isnothing.(idxs)) && - throw(DomainError((mid1, mid2), "Metabolite ratio pair not found in model.")) - const_ratio_mat[i, first(idxs)] = 1.0 - const_ratio_mat[i, last(idxs)] = -1.0 - end - - # add ΔG relationships - dgrs = spdiagm(ones(length(reactions(model)))) - S = stoichiometry(model.inner) - stoich_mat = -(model.R * model.T) * S' - dg_mat = [spzeros(n_reactions(model)) stoich_mat dgrs] - - return [ - proton_water_mat - const_conc_mat - const_ratio_mat - dg_mat - ] -end - -function Accessors.bounds(model::MaxMinDrivingForceModel) - var_ids = Internal.original_variables(model) - - lbs = fill(-model.max_dg_bound, n_variables(model)) - ubs = fill(model.max_dg_bound, n_variables(model)) - - # mmdf must be positive for problem to be feasible (it is defined as -ΔG) - lbs[1] = 0.0 - ubs[1] = 1000.0 - - # log concentrations - lbs[2:(1+n_metabolites(model))] .= log(model.concentration_lb) - ubs[2:(1+n_metabolites(model))] .= log(model.concentration_ub) - - # need to make special adjustments for the constants - idxs = indexin([model.proton_ids; model.water_ids], var_ids) - lbs[idxs] .= -1.0 - ubs[idxs] .= 1.0 - - # ΔG for each reaction can be any sign, but that is filled before default - - return (lbs, ubs) -end - -function Accessors.coupling(model::MaxMinDrivingForceModel) - - # only constrain reactions that have thermo data - active_rids = Internal.active_reaction_ids(model) - idxs = Int.(indexin(active_rids, reactions(model))) - - # thermodynamic sign should correspond to the fluxes - flux_signs = spzeros(length(idxs), n_reactions(model)) - for (i, j) in enumerate(idxs) - flux_signs[i, j] = sign(model.flux_solution[reactions(model)[j]]) - end - - neg_dg_mat = [ - spzeros(length(idxs)) spzeros(length(idxs), n_metabolites(model)) flux_signs - ] - - mmdf_mat = sparse( - [ - -ones(length(idxs)) spzeros(length(idxs), n_metabolites(model)) -flux_signs - ], - ) - - return [ - neg_dg_mat - mmdf_mat - ] -end - -function Accessors.coupling_bounds(model::MaxMinDrivingForceModel) - n = length(Internal.active_reaction_ids(model)) - neg_dg_lb = fill(-model.max_dg_bound, n) - neg_dg_ub = fill(0.0, n) - - mmdf_lb = fill(0.0, n) - mmdf_ub = fill(model.max_dg_bound, n) - - return ([neg_dg_lb; mmdf_lb], [neg_dg_ub; mmdf_ub]) -end diff --git a/src/wrappers/MinimizeDistance.jl b/src/wrappers/MinimizeDistance.jl deleted file mode 100644 index 599abb86e..000000000 --- a/src/wrappers/MinimizeDistance.jl +++ /dev/null @@ -1,133 +0,0 @@ - -""" -$(TYPEDEF) - -A wrapper for setting an Euclidean-distance-from-a-reference-point-minimizing -objective on solution variables. -""" -struct MinimizeSolutionDistance <: AbstractModelWrapper - center::Vector{Float64} - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(m::MinimizeSolutionDistance) = m.inner - -Accessors.objective(m::MinimizeSolutionDistance) = - [spdiagm(fill(-0.5, length(m.center))) m.center] - -""" -$(TYPEDSIGNATURES) - -Set a quadratic objective that minimizes the solution distance from a selected -point. Use [`minimize_semantic_distance`](@ref) or -[`minimize_projected_distance`](@ref) for more fine-grained and weighted -variants of the same concept. Internally powered by -[`MinimizeSolutionDistance`](@ref). -""" -minimize_solution_distance(center::Vector{Float64}) = - model::AbstractMetabolicModel -> MinimizeSolutionDistance(center, model) - -""" -$(TYPEDSIGNATURES) - -Set an objective that finds a solution of minimal norm. Typically, it is -necessary to also add more constraints to the objective that prevent optimality -of the trivial zero solution. - -This can be used to implement [`parsimonious_flux_balance_analysis`](@ref) in a -flexible way that fits into larger model systems. -""" -with_parsimonious_objective() = - model::AbstractMetabolicModel -> - MinimizeSolutionDistance(zeros(n_variables(model)), model) - -""" -$(TYPEDEF) - -A wrapper that sets an objective that minimizes Euclidean distance from a given -point in a semantics. -""" -struct MinimizeSemanticDistance <: AbstractModelWrapper - semantics::Symbol - center::Vector{Float64} - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(m::MinimizeSemanticDistance) = m.inner - -function Accessors.objective(m::MinimizeSemanticDistance) - (_, _, _, smtx) = Accessors.Internal.semantics(m.semantics) - Sem = smtx(m.inner) - - return Sem * - [spdiagm(fill(-0.5, size(Sem, 2))) m.center] * - [Sem' zeros(size(Sem, 2)); zeros(size(Sem, 1))' 1.0] -end - -""" -$(TYPEDSIGNATURES) - -Set a quadratic objective that minimizes the solution distance from a selected -point in a space defined by a given semantics. Use -[`minimize_projected_distance`](@ref) for more fine-grained and weighted -variant, and [`minimize_solution_distance`](@ref) for working directly upon -variables. Internally powered by [`MinimizeSemanticDistance`](@ref). -""" -minimize_semantic_distance(semantics::Symbol, center::Vector{Float64}) = - model::AbstractMetabolicModel -> MinimizeSolutionDistance(semantics, center, model) - -""" -$(TYPEDSIGNATURES) - -Set an objective that finds a solution of minimal norm in a given semantics. -This can be used to implement various realistic variants of -[`parsimonious_flux_balance_analysis`](@ref). -""" -with_parsimonious_objective(semantics::Symbol) = - model::AbstractMetabolicModel -> let - (_, n_sem, _, _) = Accessors.Internal.semantics(semantics) - MinimizeSemanticDistance(semantics, zeros(n_sem(model)), model) - end - -""" -$(TYPEDEF) - -A wrapper that sets an objective that minimizes Euclidean distance from a given -point in a space defined by a projection matrix. -""" -struct MinimizeProjectedDistance <: AbstractModelWrapper - proj::SparseMat - center::Vector{Float64} - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(m::MinimizeProjectedDistance) = m.inner - -Accessors.objective(m::MinimizeProjectedDistance) = - let - m.proj' * - [spdiagm(fill(-0.5, length(m.center))) m.center] * - [m.proj zeros(size(R, 1)); zeros(size(R, 2))' 1.0] - end - -""" -$(TYPEDSIGNATURES) - -Set a quadratic objective that minimizes the solution distance from a selected -point in a space defined by a custom projection matrix. See -[`minimize_solution_distance`](@ref) and [`minimize_semantic_distance`](@ref) -for simpler variants. Internally powered by -[`MinimizeProjectedDistance`](@ref). -""" -minimize_projected_distance(proj::SparseMat, center::Vector{Float64}) = - model::AbstractMetabolicModel -> MinimizeProjectedDistance(proj, center, model) - -""" -$(TYPEDSIGNATURES) - -Set a quadratic objective that minimizes the norm in a given projection of -model variables. -""" -with_parsimonious_objective(proj::SparseMat) = - model::AbstractMetabolicModel -> - MinimizeProjectedDistance(proj, zeros(size(proj, 1)), model) diff --git a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl b/src/wrappers/SimplifiedEnzymeConstrainedModel.jl deleted file mode 100644 index fbd6c1ae4..000000000 --- a/src/wrappers/SimplifiedEnzymeConstrainedModel.jl +++ /dev/null @@ -1,137 +0,0 @@ -""" -$(TYPEDEF) - -An enzyme-capacity-constrained model using sMOMENT algorithm, as described by -*Bekiaris, Pavlos Stephanos, and Steffen Klamt, "Automatic construction of -metabolic models with enzyme constraints" BMC bioinformatics, 2020*. - -Use [`make_simplified_enzyme_constrained_model`](@ref) or -[`with_simplified_enzyme_constraints`](@ref) to construct the models. - -The model is constructed as follows: -- stoichiometry of the original model is retained as much as possible, but - enzymatic reations are split into forward and reverse parts (marked by a - suffix like `...#forward` and `...#reverse`), -- coupling is added to simulate lumped virtual metabolites that act like enzyme - capacities. These are consumed by enzymatic reactions at a rate given by - enzyme mass divided by the corresponding kcat, -- the total consumption of the enzyme capacity bounds is constrained to be less - than some fixed values. - -The `SimplifiedEnzymeConstrainedModel` structure contains a worked-out -representation of the optimization problem atop a wrapped -[`AbstractMetabolicModel`](@ref). The internal representation of the model -splits reactions into unidirectional forward and reverse parts (which changes -the stoichiometric matrix). - -In the structure, the field `columns` describes the correspondence of -stoichiometry columns to the stoichiometry, and data of the internal wrapped -model. Multiple capacity bounds may be added through -`total_reaction_mass_bounds`. These bounds are connected to the model through -`columns`. Since this algorithm is reaction centered, no enzymes directly appear -in the formulation. - -This implementation allows easy access to fluxes from the split reactions -(available in `variables(model)`), while the original "simple" reactions from -the wrapped model are retained as [`reactions`](@ref). All additional -constraints are implemented using [`coupling`](@ref) and -[`coupling_bounds`](@ref). - -To implement this wrapper for a model, the accessors [`reaction_isozymes`](@ref) -and [`gene_product_molar_mass`](@ref), need to be available. Additionally, the -model needs to associate [`Isozyme`](@ref)s with reactions. Reactions without -enzymes, or those that should be ignored need to return `nothing` when -[`reaction_isozymes`](@ref) is called on them. - -# Fields -$(TYPEDFIELDS) -""" -struct SimplifiedEnzymeConstrainedModel <: AbstractModelWrapper - columns::Vector{SimplifiedEnzymeConstrainedColumn} - total_reaction_mass_bounds::Vector{Float64} - - inner::AbstractMetabolicModel -end - -Accessors.unwrap_model(model::SimplifiedEnzymeConstrainedModel) = model.inner - -Accessors.stoichiometry(model::SimplifiedEnzymeConstrainedModel) = - stoichiometry(model.inner) * simplified_enzyme_constrained_column_reactions(model) - -Accessors.objective(model::SimplifiedEnzymeConstrainedModel) = - simplified_enzyme_constrained_column_reactions(model)' * objective(model.inner) - -Accessors.variables(model::SimplifiedEnzymeConstrainedModel) = - let inner_reactions = variables(model.inner) - [ - simplified_enzyme_constrained_reaction_name( - inner_reactions[col.reaction_idx], - col.direction, - ) for col in model.columns - ] - end - -Accessors.n_variables(model::SimplifiedEnzymeConstrainedModel) = length(model.columns) - -Accessors.bounds(model::SimplifiedEnzymeConstrainedModel) = - ([col.lb for col in model.columns], [col.ub for col in model.columns]) - -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in -[`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the -wrapped model (as a matrix). -""" -Accessors.reaction_variables_matrix(model::SimplifiedEnzymeConstrainedModel) = - simplified_enzyme_constrained_column_reactions(model)' * - reaction_variables_matrix(model.inner) - -""" -$(TYPEDSIGNATURES) - -Get the mapping of the reaction rates in -[`SimplifiedEnzymeConstrainedModel`](@ref) to the original fluxes in the -wrapped model. -""" -Accessors.reaction_variables(model::SimplifiedEnzymeConstrainedModel) = - Accessors.Internal.make_mapping_dict( - variables(model), - reactions(model.inner), - reaction_variables_matrix(model), - ) # TODO currently inefficient - -function Accessors.coupling(model::SimplifiedEnzymeConstrainedModel) - inner_coupling = - coupling(model.inner) * simplified_enzyme_constrained_column_reactions(model) - - I = Int64[] - J = Int64[] - V = Float64[] - for (col_idx, col) in enumerate(model.columns) - for row_idx in col.capacity_bound_idxs - push!(J, col_idx) - push!(I, row_idx) - push!(V, col.capacity_contribution) - end - end - - capacity_coupling = - sparse(I, J, V, length(model.total_reaction_mass_bounds), length(model.columns)) - - return [ - inner_coupling - capacity_coupling - ] -end - -Accessors.n_coupling_constraints(model::SimplifiedEnzymeConstrainedModel) = - n_coupling_constraints(model.inner) + length(model.total_reaction_mass_bounds) - -Accessors.coupling_bounds(model::SimplifiedEnzymeConstrainedModel) = - let (iclb, icub) = coupling_bounds(model.inner) - ( - vcat(iclb, zeros(length(model.total_reaction_mass_bounds))), - vcat(icub, model.total_reaction_mass_bounds), - ) - end diff --git a/src/wrappers/bits/enzyme_constrained.jl b/src/wrappers/bits/enzyme_constrained.jl deleted file mode 100644 index e76921de2..000000000 --- a/src/wrappers/bits/enzyme_constrained.jl +++ /dev/null @@ -1,51 +0,0 @@ - -""" -$(TYPEDEF) - -A helper type that describes the contents of [`SimplifiedEnzymeConstrainedModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct SimplifiedEnzymeConstrainedColumn - reaction_idx::Int # number of the corresponding reaction in the inner model - direction::Int # 0 if "as is" and unique, -1 if reverse-only part, 1 if forward-only part - lb::Float64 # must be 0 if the reaction is unidirectional (if direction!=0) - ub::Float64 - capacity_contribution::Float64 # must be 0 for bidirectional reactions (if direction==0) - capacity_bound_idxs::Vector{Int64} # index of associated bound(s) -end - -""" -$(TYPEDEF) - -A helper type for describing the contents of [`EnzymeConstrainedModel`](@ref)s. - -# Fields -$(TYPEDFIELDS) -""" -struct EnzymeConstrainedReactionColumn - reaction_idx::Int - isozyme_idx::Int - direction::Int - reaction_coupling_row::Int - lb::Float64 - ub::Float64 - gene_product_coupling::Vector{Tuple{Int,Float64}} -end - -""" -$(TYPEDEF) - -A helper struct that contains the gene product capacity terms organized by -the grouping type, e.g. metabolic or membrane groups etc. - -# Fields -$(TYPEDFIELDS) -""" -struct EnzymeConstrainedCapacity - group_id::String - gene_product_idxs::Vector{Int} - gene_product_molar_masses::Vector{Float64} - group_upper_bound::Float64 -end diff --git a/src/wrappers/misc/enzyme_constrained.jl b/src/wrappers/misc/enzyme_constrained.jl deleted file mode 100644 index 120af97bb..000000000 --- a/src/wrappers/misc/enzyme_constrained.jl +++ /dev/null @@ -1,117 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`EnzymeConstrainedModel`](@ref). -""" -enzyme_constrained_reaction_name(original_name::String, direction::Int, isozyme_idx::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward#$isozyme_idx" : - "$original_name#reverse#$isozyme_idx" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -enzyme_constrained_column_reactions(model::EnzymeConstrainedModel) = - enzyme_constrained_column_reactions(model.columns, model.inner) - -""" -$(TYPEDSIGNATURES) - -Helper method that doesn't require the whole [`EnzymeConstrainedModel`](@ref). -""" -enzyme_constrained_column_reactions(columns, inner) = sparse( - [col.reaction_idx for col in columns], - 1:length(columns), - [col.direction >= 0 ? 1 : -1 for col in columns], - n_variables(inner), - length(columns), -) - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for [`EnzymeConstrainedModel`](@ref) that limits the -"arm" reactions (which group the individual split unidirectional reactions). -""" -enzyme_constrained_reaction_coupling(model::EnzymeConstrainedModel) = - let tmp = [ - (col.reaction_coupling_row, i, col.direction) for - (i, col) in enumerate(model.columns) if col.reaction_coupling_row != 0 - ] - sparse( - [row for (row, _, _) in tmp], - [col for (_, col, _) in tmp], - [val for (_, _, val) in tmp], - length(model.coupling_row_reaction), - length(model.columns), - ) - end - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for EnzymeConstrainedModel that limits the amount of each -kind of protein available. -""" -enzyme_constrained_gene_product_coupling(model::EnzymeConstrainedModel) = - let - tmp = [ - (row, i, val) for (i, col) in enumerate(model.columns) for - (row, val) in col.gene_product_coupling - ] - sparse( - [row for (row, _, _) in tmp], - [col for (_, col, _) in tmp], - [val for (_, _, val) in tmp], - length(model.coupling_row_gene_product), - length(model.columns), - ) - end - -""" -$(TYPEDSIGNATURES) - -Compute the part of the coupling for [`EnzymeConstrainedModel`](@ref) that limits the total -mass of each group of gene products. -""" -function enzyme_constrained_mass_group_coupling(model::EnzymeConstrainedModel) - tmp = [ # mm = molar mass, mg = mass group, i = row idx, j = col idx - (i, j, mm) for (i, mg) in enumerate(model.coupling_row_mass_group) for - (j, mm) in zip(mg.gene_product_idxs, mg.gene_product_molar_masses) - ] - sparse( - [i for (i, _, _) in tmp], - [j for (_, j, _) in tmp], - [mm for (_, _, mm) in tmp], - length(model.coupling_row_mass_group), - n_genes(model), - ) -end - -""" -$(TYPEDSIGNATURES) - -Internal helper for systematically naming reactions in [`SimplifiedEnzymeConstrainedModel`](@ref). -""" -simplified_enzyme_constrained_reaction_name(original_name::String, direction::Int) = - direction == 0 ? original_name : - direction > 0 ? "$original_name#forward" : "$original_name#reverse" - -""" -$(TYPEDSIGNATURES) - -Retrieve a utility mapping between reactions and split reactions; rows -correspond to "original" reactions, columns correspond to "split" reactions. -""" -simplified_enzyme_constrained_column_reactions(model::SimplifiedEnzymeConstrainedModel) = - sparse( - [col.reaction_idx for col in model.columns], - 1:length(model.columns), - [col.direction >= 0 ? 1 : -1 for col in model.columns], - n_variables(model.inner), - length(model.columns), - ) diff --git a/src/wrappers/misc/mmdf.jl b/src/wrappers/misc/mmdf.jl deleted file mode 100644 index 86c3b7c50..000000000 --- a/src/wrappers/misc/mmdf.jl +++ /dev/null @@ -1,23 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A helper function that returns the reaction ids that are active. Active reaction -have thermodynamic data AND a flux bigger than `small_flux_tol` AND are not -ignored. -""" -active_reaction_ids(model::MaxMinDrivingForceModel) = filter( - rid -> - haskey(model.reaction_standard_gibbs_free_energies, rid) && - abs(get(model.flux_solution, rid, model.small_flux_tol / 2)) > - model.small_flux_tol && - !(rid in model.ignore_reaction_ids), - reactions(model), -) - -""" -$(TYPEDSIGNATURES) - -Helper function that returns the unmangled variable IDs. -""" -original_variables(model::MaxMinDrivingForceModel) = - ["mmdf"; metabolites(model); reactions(model)] diff --git a/test/io/h5.jl b/test/io/h5.jl deleted file mode 100644 index b368cc674..000000000 --- a/test/io/h5.jl +++ /dev/null @@ -1,31 +0,0 @@ - -@testset "HDF5 model SBML model" begin - model = load_model(MatrixModel, model_paths["e_coli_core.xml"]) - fn = "ecoli_test.h5.noextension" - h5m = save_model(model, fn, extension = ".h5") - @test h5m isa HDF5Model - @test h5m.filename == fn - @test h5m.h5 == nothing #the file should not be open by default - - h5 = load_model(fn, extension = ".h5") - precache!(h5) - @test !isnothing(h5.h5) - - # briefly test that the loading is okay - @test n_variables(model) == n_variables(h5) - @test n_metabolites(model) == n_metabolites(h5) - @test issetequal(variables(model), variables(h5)) - @test issetequal(metabolites(model), metabolites(h5)) - @test issorted(metabolites(h5)) - @test issorted(variables(h5)) - @test size(stoichiometry(model)) == size(stoichiometry(h5)) - @test isapprox(sum(stoichiometry(model)), sum(stoichiometry(h5))) - rxnp = sortperm(variables(model)) - @test bounds(model)[1][rxnp] == bounds(h5)[1] - @test bounds(model)[2][rxnp] == bounds(h5)[2] - @test objective(model)[rxnp] == objective(h5) - @test all(iszero, balance(h5)) - - close(h5) - @test isnothing(h5.h5) -end diff --git a/test/io/io.jl b/test/io/io.jl deleted file mode 100644 index 479562301..000000000 --- a/test/io/io.jl +++ /dev/null @@ -1,29 +0,0 @@ -@testset "Opening models from BIGG" begin - - sbmlmodel = load_model(model_paths["iJO1366.xml"]) - @test sbmlmodel isa SBMLModel - @test n_variables(sbmlmodel) == 2583 - - matlabmodel = load_model(model_paths["iJO1366.mat"]) - @test matlabmodel isa MATModel - @test n_variables(matlabmodel) == 2583 - - jsonmodel = load_model(model_paths["iJO1366.json"]) - @test jsonmodel isa JSONModel - @test n_variables(jsonmodel) == 2583 - - @test Set(lowercase.(variables(sbmlmodel))) == - Set("r_" .* lowercase.(variables(matlabmodel))) - @test Set(lowercase.(variables(sbmlmodel))) == - Set("r_" .* lowercase.(variables(jsonmodel))) - - # specifically test parsing of gene-reaction associations in Recon - reconmodel = load_model(ObjectModel, model_paths["Recon3D.json"]) - @test n_variables(reconmodel) == 10600 - recon_grrs = [ - r.gene_associations for - (i, r) in reconmodel.reactions if !isnothing(r.gene_associations) - ] - @test length(recon_grrs) == 5938 - @test sum(length.(recon_grrs)) == 31504 -end diff --git a/test/io/json.jl b/test/io/json.jl deleted file mode 100644 index 90f3cad5c..000000000 --- a/test/io/json.jl +++ /dev/null @@ -1,24 +0,0 @@ -@testset "Test conversion from JSONModel to ObjectModel" begin - - jsonmodel = load_model(model_paths["e_coli_core.json"]) - stdmodel = convert(ObjectModel, jsonmodel) - - # test if same reaction ids - @test issetequal(variables(jsonmodel), variables(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) - @test issetequal(genes(jsonmodel), genes(stdmodel)) - # not the best tests since it is possible that error could cancel each other out: - @test sum(stoichiometry(jsonmodel)) == sum(stoichiometry(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(jsonmodel) - @test sum(jlbs) == sum(slbs) - @test sum(jubs) == sum(subs) -end - -@testset "Save JSON model" begin - model = load_model(MatrixModel, model_paths["e_coli_core.json"]) - testpath = tmpfile("modeltest.json") - save_model(model, testpath) - wrote = convert(MatrixModel, load_json_model(testpath)) - @test isequal(model, wrote) -end diff --git a/test/io/mat.jl b/test/io/mat.jl deleted file mode 100644 index cd0beed82..000000000 --- a/test/io/mat.jl +++ /dev/null @@ -1,22 +0,0 @@ - -@testset "Import MAT model" begin - cp = load_model(MatrixModel, model_paths["iJR904.mat"]) - @test cp isa MatrixModel - @test size(cp.S) == (761, 1075) -end - -@testset "Save MAT model" begin - loaded = load_model(MatrixModel, model_paths["iJR904.mat"]) - testpath = tmpfile("iJR904-clone.mat") - save_model(loaded, testpath) - wrote = load_model(MatrixModel, testpath) - @test wrote isa MatrixModel - @test isequal(wrote, loaded) -end - -@testset "Import yeast-GEM (mat)" begin - m = load_model(ObjectModel, model_paths["yeast-GEM.mat"]) - @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 - @test n_genes(m) == 1160 -end diff --git a/test/io/sbml.jl b/test/io/sbml.jl deleted file mode 100644 index c9e9abd0c..000000000 --- a/test/io/sbml.jl +++ /dev/null @@ -1,33 +0,0 @@ - -@testset "SBML import and conversion" begin - sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - m = convert(MatrixModel, sbmlm) - - @test size(stoichiometry(sbmlm)) == (92, 95) - @test size(stoichiometry(m)) == (n_metabolites(sbmlm), n_variables(sbmlm)) - @test length(m.S.nzval) == 380 - @test length.(bounds(sbmlm)) == (95, 95) - @test length.(bounds(m)) == (95, 95) - @test all([length(m.xl), length(m.xu), length(m.c)] .== 95) - - @test metabolites(m)[1:3] == ["M_13dpg_c", "M_2pg_c", "M_3pg_c"] - @test reactions(m)[1:3] == ["R_ACALD", "R_ACALDt", "R_ACKr"] - - cm = convert(MatrixModelWithCoupling, sbmlm) - @test n_coupling_constraints(cm) == 0 -end - -@testset "Save SBML model" begin - model = load_model(MatrixModel, model_paths["e_coli_core.xml"]) - testpath = tmpfile("modeltest.xml") - save_model(convert(SBMLModel, model), testpath) - wrote = convert(MatrixModel, load_sbml_model(testpath)) - @test isequal(model, wrote) -end - -@testset "Import yeast-GEM (sbml)" begin - m = load_model(ObjectModel, model_paths["yeast-GEM.xml"]) - @test n_metabolites(m) == 2744 - @test n_reactions(m) == 4063 - @test n_genes(m) == 1160 -end diff --git a/test/reconstruction/MatrixCoupling.jl b/test/reconstruction/MatrixCoupling.jl deleted file mode 100644 index 5b3de7464..000000000 --- a/test/reconstruction/MatrixCoupling.jl +++ /dev/null @@ -1,193 +0,0 @@ -@testset "Coupling constraints" begin - cp = convert(MatrixModelWithCoupling, test_LP()) - @test size(cp.lm.S) == (4, 3) - @test size(stoichiometry(convert(MatrixModel, cp))) == (4, 3) - new_cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - @test n_coupling_constraints(cp) + 1 == n_coupling_constraints(new_cp) - - new_cp = - add_coupling_constraints(cp, stoichiometry(cp)[1:2, :], [-1.0; -1.0], [1.0; 1.0]) - @test n_coupling_constraints(cp) + 2 == n_coupling_constraints(new_cp) - - n_c = n_coupling_constraints(cp) - add_coupling_constraints!(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - @test n_c + 1 == n_coupling_constraints(cp) - add_coupling_constraints!(cp, stoichiometry(cp)[1:2, :], [-1.0; -1.0], [1.0; 1.0]) - @test n_c + 3 == n_coupling_constraints(cp) - - n_c = n_coupling_constraints(cp) - remove_coupling_constraints!(cp, 1) - @test n_c - 1 == n_coupling_constraints(cp) - remove_coupling_constraints!(cp, [1, 2]) - @test n_c - 3 == n_coupling_constraints(cp) - @test n_coupling_constraints(cp) == 0 - - cp = test_coupledLP() - n_c = n_coupling_constraints(cp) - new_cp = remove_coupling_constraints(cp, 1) - @test size(coupling(cp)) == (n_c, n_variables(cp)) - @test n_c - 1 == n_coupling_constraints(new_cp) - @test n_coupling_constraints(cp) == n_c - new_cp = remove_coupling_constraints(cp, [1, 2]) - @test n_c - 2 == n_coupling_constraints(new_cp) - new_cp = remove_coupling_constraints(cp, Vector(1:n_coupling_constraints(cp))) - @test n_coupling_constraints(new_cp) == 0 - @test n_coupling_constraints(cp) == n_c - - cp = test_coupledLP() - change_coupling_bounds!(cp, [3, 1], cl = [-10.0, -20], cu = [10.0, 20]) - cl, cu = coupling_bounds(cp) - @test cl[[1, 3]] == [-20, -10] - @test cu[[1, 3]] == [20, 10] - change_coupling_bounds!(cp, [1000, 1001], cl = [-50.0, -60.0]) - cl, cu = coupling_bounds(cp) - @test cl[[1000, 1001]] == [-50.0, -60.0] -end - -@testset "Add reactions" begin - cp = convert(MatrixModelWithCoupling, test_LP()) - cp = add_coupling_constraints(cp, stoichiometry(cp)[end, :], -1.0, 1.0) - - new_cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0) - @test new_cp isa MatrixModelWithCoupling - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4), - 3 .* ones(4), - 2.0, - -1.0, - 1.0, - "r4", - ["m$i" for i = 1:4], - ) - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ) - @test cp.C == new_cp.C[:, 1:end-10] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ["r$i" for i = 1:10], - ["m$i" for i = 1:4], - ) - @test cp.C == new_cp.C[:, 1:end-7] # 3 reactions were already present - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - new_cp = - add_reactions(cp, 2.0 * sprand(4000, 0.5), 3 .* sprand(4000, 0.5), 2.0, -1.0, 1.0) - @test cp.C == new_cp.C[:, 1:end-1] - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu - - cm = MatrixModel( - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ["r$i" for i = 1:10], - ["m$i" for i = 1:4], - ) - new_cp = add_reactions(cp, cm) - @test cp.C == new_cp.C[:, 1:end-7] # 3 reactions were already present - @test cp.cl == new_cp.cl - @test cp.cu == new_cp.cu -end - -@testset "Remove reactions" begin - cp = convert(MatrixModelWithCoupling, test_LP()) - cp = add_coupling_constraints(cp, 1.0 .* collect(1:n_variables(cp)), -1.0, 1.0) - - new_cp = remove_reactions(cp, [3, 2]) - @test new_cp isa MatrixModelWithCoupling - @test new_cp.C[:] == cp.C[:, 1] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reaction(cp, 2) - @test new_cp.C == cp.C[:, [1, 3]] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reaction(cp, "r1") - @test new_cp.C == cp.C[:, 2:3] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reactions(cp, ["r1", "r3"]) - @test new_cp.C[:] == cp.C[:, 2] - @test new_cp.cl == cp.cl - @test new_cp.cu == cp.cu - - new_cp = remove_reactions(cp, [1, 4]) - @test new_cp.C == cp.C[:, 2:3] - - new_cp = remove_reactions(cp, [1, 1, 2]) - @test new_cp.C[:] == cp.C[:, 3] -end - -@testset "Change bounds" begin - cp = convert(MatrixModelWithCoupling, test_LP()) - @test cp isa MatrixModelWithCoupling - - change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) - @test cp.lm.xl[1] == -10 - @test cp.lm.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) - @test cp.lm.xl[2] == -12.2 - @test cp.lm.xu[1] == 11 - - change_bound!(cp, "r1", lower_bound = -101, upper_bound = 101) - @test cp.lm.xl[1] == -101 - @test cp.lm.xu[1] == 101 - change_bounds!( - cp, - ["r1", "r2"]; - lower_bounds = [-113, -12.23], - upper_bounds = [113, 233.0], - ) - @test cp.lm.xl[2] == -12.23 - @test cp.lm.xu[1] == 113 - - new_model = change_bound(cp, 1, lower_bound = -10, upper_bound = 10) - @test new_model.lm.xl[1] == -10 - @test new_model.lm.xu[1] == 10 - new_model = - change_bounds(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) - @test new_model.lm.xl[2] == -12.2 - @test new_model.lm.xu[1] == 11 - - new_model = change_bound(cp, "r1", lower_bound = -101, upper_bound = 101) - @test new_model.lm.xl[1] == -101 - @test new_model.lm.xu[1] == 101 - new_model = change_bounds( - cp, - ["r1", "r2"]; - lower_bounds = [-113, -12.23], - upper_bounds = [113, 233.0], - ) - @test new_model.lm.xl[2] == -12.23 - @test new_model.lm.xu[1] == 113 - -end diff --git a/test/reconstruction/MatrixModel.jl b/test/reconstruction/MatrixModel.jl deleted file mode 100644 index 480d579b4..000000000 --- a/test/reconstruction/MatrixModel.jl +++ /dev/null @@ -1,275 +0,0 @@ -@testset "Change bounds" begin - cp = test_LP() - - change_bound!(cp, 1, lower_bound = -10, upper_bound = 10) - @test cp.xl[1] == -10 - @test cp.xu[1] == 10 - change_bounds!(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) - @test cp.xl[2] == -12.2 - @test cp.xu[1] == 11 - - change_bound!(cp, "r1", lower_bound = -101, upper_bound = 101) - @test cp.xl[1] == -101 - @test cp.xu[1] == 101 - change_bounds!( - cp, - ["r1", "r2"]; - lower_bounds = [-113, -12.23], - upper_bounds = [114, 233.0], - ) - @test cp.xl[2] == -12.23 - @test cp.xu[1] == 114 - change_bounds!( - cp, - ["r1", "r2"]; - lower_bounds = [-114, nothing], - upper_bounds = [nothing, 2333.0], - ) - @test cp.xl[1] == -114 - @test cp.xl[2] == -12.23 - @test cp.xu[1] == 114 - @test cp.xu[2] == 2333 - - new_model = change_bound(cp, 1, lower_bound = -10, upper_bound = 10) - @test new_model.xl[1] == -10 - @test new_model.xu[1] == 10 - new_model = - change_bounds(cp, [1, 2]; lower_bounds = [-11, -12.2], upper_bounds = [11, 23.0]) - @test new_model.xl[2] == -12.2 - @test new_model.xu[1] == 11 - - new_model = change_bound(cp, "r1", lower_bound = -101, upper_bound = 101) - @test new_model.xl[1] == -101 - @test new_model.xu[1] == 101 - new_model = change_bounds( - cp, - ["r1", "r2"]; - lower_bounds = [-113, -12.23], - upper_bounds = [113, 1000], - ) - @test new_model.xl[2] == -12.23 - @test new_model.xu[1] == 113 - new_model = change_bounds( - cp, - ["r1", "r2"]; - lower_bounds = [nothing, -10], - upper_bounds = [110, nothing], - ) - @test new_model.xl[1] == -114 - @test new_model.xl[2] == -10 - @test new_model.xu[1] == 110 - @test new_model.xu[2] == 2333 -end - -@testset "Verify consistency" begin - cp = test_LP() - (new_reactions, new_mets) = verify_consistency( - cp, - reshape(cp.S[:, end], :, 1), - [1.0, 2.0, 3.0, 4.0], - [2.0], - [-1.0], - [1.0], - ["r4"], - ["m1", "m2", "m3", "m6"], - [1], - [4], - ) - @test new_reactions == [1] - @test new_mets == [4] - - (new_reactions, new_mets) = verify_consistency( - cp, - reshape(cp.S[:, end], :, 1), - [1.0, 2.0, 3.0, 4.0], - [2.0], - [-1.0], - [1.0], - ["r1"], - ["m1", "m2", "m3", "m6"], - [], - [4], - ) - @test new_reactions == [] - @test new_mets == [4] -end - -@testset "Add reactions (checking existence and consistency)" begin - cp = test_LP() - (new_cp, new_reactions, new_mets) = add_reactions( - cp, - cp.S[:, end], - [1.0, 2.0, 3.0, 4.0], - 2.0, - -1.0, - 1.0, - check_consistency = true, - ) - @test n_variables(cp) + 1 == n_variables(new_cp) - - (new_cp, new_reactions, new_mets) = add_reactions( - cp, - cp.S[:, end], - [1.0, 2.0, 3.0, 4.0], - 2.0, - -1.0, - 1.0, - "r1", - ["m1", "m2", "m3", "m6"], - check_consistency = true, - ) - @test n_variables(cp) == n_variables(new_cp) - @test n_metabolites(cp) + 1 == n_metabolites(new_cp) -end - -@testset "Add reactions" begin - cp = test_LP() - cp = add_reactions(cp, 2.0 * ones(4), 3 .* ones(4), 2.0, -1.0, 1.0) - @test size(cp.S) == (8, 4) - cp = add_reactions(cp, 2.0 * ones(4, 1), 3 .* ones(4), 2 .* ones(1), -ones(1), ones(1)) - @test size(cp.S) == (12, 5) - cp = add_reactions( - cp, - 2.0 * ones(4, 10), - 3 .* ones(4), - 2 .* ones(10), - -ones(10), - ones(10), - ) - @test size(cp.S) == (16, 15) - - cp = test_sparseLP() - @test size(cp.S) == (4000, 3000) - cp = add_reactions(cp, 2.0 * sprand(4000, 0.5), 3 .* sprand(4000, 0.5), 2.0, -1.0, 1.0) - @test size(cp.S) == (8000, 3001) - cp = add_reactions( - cp, - 2.0 * sprand(4000, 1, 0.5), - 3 .* sprand(4000, 0.5), - 2 .* sprand(1, 0.5), - -sprand(1, 0.5), - sprand(1, 0.5), - ) - @test size(cp.S) == (12000, 3002) - cp = add_reactions( - cp, - 2.0 * sprand(4000, 1000, 0.5), - 3 .* sprand(4000, 0.5), - 2 .* sprand(1000, 0.5), - -sprand(1000, 0.5), - sprand(1000, 0.5), - ) - @test size(cp.S) == (16000, 4002) - - cp = test_sparseLP() - @test size(cp.S) == (4000, 3000) - cp = add_reactions(cp, 2.0 * ones(4000), 3 .* ones(4000), 2.0, -1.0, 1.0) - @test size(cp.S) == (8000, 3001) - cp = add_reactions( - cp, - 2.0 * ones(4000, 1), - 3 .* ones(4000), - 2 .* ones(1), - -ones(1), - ones(1), - ) - @test size(cp.S) == (12000, 3002) - cp = add_reactions( - cp, - 2.0 * ones(4000, 1000), - 3 .* ones(4000), - 2 .* ones(1000), - -ones(1000), - ones(1000), - ) - @test size(cp.S) == (16000, 4002) - - # proper subset of existing metabolites - cp = test_LP() - new_cp = add_reactions(cp, [-1.0], zeros(1), 1.0, 0.0, 1.0, "r4", ["m1"]) - @test n_variables(cp) + 1 == n_variables(new_cp) - - @test_throws DimensionMismatch add_reactions( - cp, - 2.0 * ones(4000, 1), - 3 .* ones(4000), - 2 .* ones(2), - -ones(1), - ones(1), - ) -end - -@testset "Remove reactions" begin - cp = test_LP() - cp = remove_reaction(cp, 2) - @test size(cp.S) == (4, 2) - cp = remove_reactions(cp, [2, 1]) - @test size(cp.S) == (4, 0) - - cp = test_LP() - cp = remove_reaction(cp, "r1") - @test size(cp.S) == (4, 2) - cp = remove_reactions(cp, ["r2"]) - @test size(cp.S) == (4, 1) - - lp = MatrixModel( - [1.0 1 1 0; 1 1 1 0; 1 1 1 0; 0 0 0 1], - collect(1.0:4), - collect(1.0:4), - collect(1.0:4), - collect(1.0:4), - ["r1"; "r2"; "r3"; "r4"], - ["m1"; "m2"; "m3"; "m4"], - ) - - modLp = remove_reactions(lp, [4; 1]) - @test stoichiometry(modLp) == stoichiometry(lp)[:, 2:3] - @test balance(modLp) == balance(lp) - @test objective(modLp) == objective(lp)[2:3] - @test bounds(modLp)[1] == bounds(lp)[1][2:3] - @test bounds(modLp)[2] == bounds(lp)[2][2:3] - @test variables(modLp) == variables(lp)[2:3] - @test metabolites(modLp) == metabolites(lp) -end - -@testset "Remove metabolites" begin - model = load_model(MatrixModel, model_paths["e_coli_core.json"]) - - m1 = remove_metabolites(model, ["glc__D_e", "for_c"]) - m2 = remove_metabolite(model, "glc__D_e") - m3 = remove_metabolites(model, Int.(indexin(["glc__D_e", "for_c"], metabolites(model)))) - m4 = remove_metabolite(model, first(indexin(["glc__D_e"], metabolites(model)))) - - @test size(stoichiometry(m1)) == (70, 90) - @test size(stoichiometry(m2)) == (71, 93) - @test size(stoichiometry(m3)) == (70, 90) - @test size(stoichiometry(m4)) == (71, 93) - @test all((!in(metabolites(m1))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m2)) - @test all((!in(metabolites(m3))).(["glc__D_e", "for_c"])) - @test !(["glc__D_e"] in metabolites(m4)) -end - -@testset "Core in place modifications" begin - toymodel = test_toyModel() - - rxn1 = Reaction("nr1"; metabolites = Dict("m1[c]" => -1, "m3[c]" => 1)) - rxn2 = Reaction("nr2"; metabolites = Dict("m1[c]" => -1, "m2[c]" => 1)) - rxn3 = Reaction("nr3"; metabolites = Dict("m2[c]" => -1, "m3[c]" => 1)) - rxn3.lower_bound = 10 - - add_reaction!(toymodel, rxn1) - @test toymodel.S[1, 8] == -1 - @test toymodel.S[2, 8] == 1 - @test all(toymodel.S[3:end, 8] .== 0) - @test size(toymodel.S) == (6, 8) - @test toymodel.rxns[end] == "nr1" - - add_reactions!(toymodel, [rxn2, rxn3]) - @test size(toymodel.S) == (6, 10) - @test toymodel.xl[end] == 10 - - change_objective!(toymodel, "nr1") - @test objective(toymodel)[8] == 1.0 - @test objective(toymodel)[7] == 0.0 -end diff --git a/test/reconstruction/ObjectModel.jl b/test/reconstruction/ObjectModel.jl deleted file mode 100644 index 668d6029e..000000000 --- a/test/reconstruction/ObjectModel.jl +++ /dev/null @@ -1,199 +0,0 @@ -@testset "Model manipulation" begin - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") - mets = [m1, m2, m3, m4] - m5 = Metabolite("m5") - m6 = Metabolite("m6") - m7 = Metabolite("m7") - mtest = Metabolite("mtest") - - g1 = Gene("g1") - g2 = Gene("g2") - g3 = Gene("g3") - g4 = Gene("g4") - gene_vec = [g1, g2, g3, g4] - g5 = Gene("g5") - g6 = Gene("g6") - g7 = Gene("g7") - gtest = Gene("gtest") - - r1 = ReactionForward("r1", Dict(m1.id => -1.0, m2.id => 1.0)) - r2 = ReactionBidirectional("r2", Dict(m2.id => -2.0, m3.id => 1.0)) - r2.gene_associations = [Isozyme(x) for x in [["g2"], ["g1", "g3"]]] - r3 = ReactionBackward("r3", Dict(m1.id => -1.0, m4.id => 2.0)) - r4 = ReactionBackward("r4", Dict(m1.id => -5.0, m4.id => 2.0)) - r5 = ReactionBackward("r5", Dict(m1.id => -11.0, m4.id => 2.0, m3.id => 2.0)) - rtest = ReactionForward("rtest", Dict(m1.id => -1.0, m2.id => 1.0)) - - rxns = [r1, r2] - - model = ObjectModel() - model.reactions = OrderedDict(r.id => r for r in rxns) - model.metabolites = OrderedDict(m.id => m for m in mets) - model.genes = OrderedDict(g.id => g for g in gene_vec) - - # change bound tests - in place - change_bound!(model, "r2"; lower_bound = -10, upper_bound = 10) - @test model.reactions["r2"].lower_bound == -10 - @test model.reactions["r2"].upper_bound == 10 - change_bound!(model, "r2"; lower_bound = -100) - @test model.reactions["r2"].lower_bound == -100 - @test model.reactions["r2"].upper_bound == 10 - change_bound!(model, "r2"; upper_bound = 111) - @test model.reactions["r2"].lower_bound == -100 - @test model.reactions["r2"].upper_bound == 111 - - change_bounds!( - model, - ["r1", "r2"]; - lower_bounds = [-110, -220], - upper_bounds = [110.0, 220.0], - ) - @test model.reactions["r1"].lower_bound == -110 - @test model.reactions["r1"].upper_bound == 110 - @test model.reactions["r2"].lower_bound == -220 - @test model.reactions["r2"].upper_bound == 220 - - # change bound - new model - new_model = change_bound(model, "r2"; lower_bound = -10, upper_bound = 10) - @test new_model.reactions["r2"].lower_bound == -10 - @test new_model.reactions["r2"].upper_bound == 10 - - new_model = change_bound(model, "r2"; lower_bound = -10) - @test new_model.reactions["r2"].lower_bound == -10 - @test new_model.reactions["r2"].upper_bound == 220 - - new_model = change_bounds( - model, - ["r1", "r2"]; - lower_bounds = [-10, -20], - upper_bounds = [10.0, 20.0], - ) - @test new_model.reactions["r1"].lower_bound == -10 - @test new_model.reactions["r1"].upper_bound == 10 - @test new_model.reactions["r2"].lower_bound == -20 - @test new_model.reactions["r2"].upper_bound == 20 - - new_model = change_bounds( - model, - ["r1", "r2"]; - lower_bounds = [-10, nothing], - upper_bounds = [nothing, 20.0], - ) - @test new_model.reactions["r1"].lower_bound == -10 - @test new_model.reactions["r1"].upper_bound == 110 - @test new_model.reactions["r2"].lower_bound == -220 - @test new_model.reactions["r2"].upper_bound == 20 - - ### objective - change_objective!(model, "r2") - @test model.objective["r2"] == 1.0 - - new_model = change_objective(model, "r1"; weight = 2.0) - @test new_model.objective["r1"] == 2.0 - - ### reactions - add_reactions!(model, [r3, r4]) - @test length(model.reactions) == 4 - - add_reaction!(model, r5) - @test length(model.reactions) == 5 - - remove_reactions!(model, ["r5", "r4"]) - @test length(model.reactions) == 3 - - remove_reaction!(model, "r1") - @test length(model.reactions) == 2 - - new_model = model |> with_added_reaction(rtest) - @test haskey(new_model.reactions, "rtest") - @test !haskey(model.reactions, "rtest") - - new_model2 = new_model |> with_removed_reaction("rtest") - @test !haskey(new_model2.reactions, "rtest") - @test haskey(new_model.reactions, "rtest") - - @test_throws DomainError add_reaction!(model, r3) - @test_throws DomainError remove_reaction!(model, "abc") - - ### metabolites - add_metabolites!(model, [m5, m6]) - @test length(model.metabolites) == 6 - - add_metabolite!(model, m7) - @test length(model.metabolites) == 7 - - remove_metabolites!(model, ["m5", "m4"]) - @test length(model.metabolites) == 5 - - remove_metabolite!(model, "m1") - @test length(model.metabolites) == 4 - - new_model = model |> with_added_metabolite(mtest) - @test haskey(new_model.metabolites, "mtest") - @test !haskey(model.metabolites, "mtest") - - new_model2 = new_model |> with_removed_metabolite("mtest") - @test !haskey(new_model2.metabolites, "mtest") - @test haskey(new_model.metabolites, "mtest") - - @test_throws DomainError add_metabolite!(model, m2) - @test_throws DomainError remove_metabolite!(model, "abc") - - ### genes - add_genes!(model, [g5, g6]) - @test length(model.genes) == 6 - - add_gene!(model, g7) - @test length(model.genes) == 7 - - remove_genes!(model, ["g5", "g4"]) - @test length(model.genes) == 5 - - remove_gene!(model, "g1") - @test length(model.genes) == 4 - - new_model = model |> with_added_gene(gtest) - @test haskey(new_model.genes, "gtest") - @test !haskey(model.genes, "gtest") - - new_model2 = new_model |> with_removed_gene("gtest") - @test !haskey(new_model2.genes, "gtest") - @test haskey(new_model.genes, "gtest") - - @test_throws DomainError add_gene!(model, g7) - @test_throws DomainError remove_gene!(model, "abc") - - # change gene - change_gene_product_bound!(model, "g3"; lower_bound = -10, upper_bound = 10) - @test model.genes["g3"].product_lower_bound == -10.0 - @test model.genes["g3"].product_upper_bound == 10.0 - - new_model = change_gene_product_bound(model, "g2"; lower_bound = -10, upper_bound = 10) - @test new_model.genes["g2"].product_lower_bound == -10.0 - @test new_model.genes["g2"].product_upper_bound == 10.0 - @test model.genes["g2"].product_lower_bound == 0.0 - - # isozymes - isos = [Isozyme(["g1"])] - new_model = model |> with_removed_isozymes("r2") - @test isnothing(new_model.reactions["r2"].gene_associations) - @test !isnothing(model.reactions["r2"].gene_associations) - - new_model2 = new_model |> with_added_isozymes("r2", isos) - @test !isnothing(new_model2.reactions["r2"].gene_associations) - @test isnothing(new_model.reactions["r2"].gene_associations) - - # test added biomass metabolite - new_model = model |> with_added_biomass_metabolite("r2") - @test "biomass" in metabolites(new_model) - @test !("biomass" in metabolites(model)) - @test haskey(new_model.reactions["r2"].metabolites, "biomass") - @test !haskey(model.reactions["r2"].metabolites, "biomass") - - new_model2 = new_model |> with_removed_biomass_metabolite("r2") - @test !("biomass" in metabolites(new_model2)) - @test !haskey(new_model2.reactions["r2"].metabolites, "biomass") -end diff --git a/test/reconstruction/SerializedModel.jl b/test/reconstruction/SerializedModel.jl deleted file mode 100644 index 21d41e1ff..000000000 --- a/test/reconstruction/SerializedModel.jl +++ /dev/null @@ -1,15 +0,0 @@ - -@testset "Serialized modifications" begin - m = test_LP() - - sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = unwrap_serialized(sm) - - @test typeof(m2) == typeof(m) - - sm = serialize_model(m, tmpfile("recon.serialized")) - m2 = remove_reaction(sm, variables(m)[3]) - - @test typeof(m2) == typeof(m) - @test !(variables(m)[3] in variables(m2)) -end diff --git a/test/reconstruction/constrained_allocation.jl b/test/reconstruction/constrained_allocation.jl deleted file mode 100644 index e721be06c..000000000 --- a/test/reconstruction/constrained_allocation.jl +++ /dev/null @@ -1,84 +0,0 @@ -@testset "Constrained allocation FBA" begin - - m = ObjectModel() - - add_reactions!( - m, - [ - ReactionForward("r1", Dict("m1" => 1)), - ReactionForward("r2", Dict("m2" => 1)), - ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), - ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), - ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), - ReactionBidirectional("r6", Dict("m4" => -1)), # different! - ], - ) - - m.reactions["r3"].gene_associations = - [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] - m.reactions["r4"].gene_associations = [ - Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0), - Isozyme(["g2"]; kcat_forward = 3.0, kcat_backward = 3.0), - ] - m.reactions["r5"].gene_associations = [ - Isozyme(; - gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), - kcat_forward = 70.0, - kcat_backward = 70.0, - ), - ] - m.reactions["r6"].gene_associations = [ # this should get removed - Isozyme(; - gene_product_stoichiometry = Dict("g3" => 1), - kcat_forward = 10.0, - kcat_backward = 0.0, - ), - ] - m.objective = Dict("r6" => 1.0) - - add_genes!( - m, - [ - Gene(id = "g$i", product_upper_bound = 10.0, product_molar_mass = float(i)) for - i = 1:4 - ], - ) - - add_metabolites!(m, [Metabolite("m$i") for i = 1:4]) - - @test_throws DomainError (m |> with_virtualribosome("r6", 0.2)) - - ribomodel = m |> with_removed_isozymes("r6") |> with_virtualribosome("r6", 0.2) - - @test haskey(ribomodel.genes, "virtualribosome") - @test first(ribomodel.reactions["r6"].gene_associations).kcat_forward == 0.2 - @test first(m.reactions["r6"].gene_associations).kcat_forward == 10.0 - - cam = - make_simplified_enzyme_constrained_model(ribomodel; total_reaction_mass_bound = 0.5) - - @test coupling(cam)[1, 7] == 5.0 - - rxn_fluxes = - flux_balance_analysis( - cam, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) |> values_dict(:reaction) - @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) - - # test inplace variant - remove_isozymes!(m, "r6") - add_virtualribosome!(m, "r6", 0.2) - cam = m |> with_simplified_enzyme_constraints(total_reaction_mass_bound = 0.5) - - @test coupling(cam)[1, 7] == 5.0 - - rxn_fluxes = - flux_balance_analysis( - cam, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) |> values_dict(:reaction) - @test isapprox(rxn_fluxes["r6"], 0.09695290851008717, atol = TEST_TOLERANCE) -end diff --git a/test/reconstruction/enzyme_constrained.jl b/test/reconstruction/enzyme_constrained.jl deleted file mode 100644 index 83a791c47..000000000 --- a/test/reconstruction/enzyme_constrained.jl +++ /dev/null @@ -1,207 +0,0 @@ -@testset "GECKO" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # add molar masses to gene products - for gid in genes(model) - model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) - end - model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) - - # update isozymes with kinetic information - for rid in reactions(model) - if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr - newisozymes = Isozyme[] - for (i, grr) in enumerate(reaction_gene_associations(model, rid)) - push!( - newisozymes, - Isozyme( - gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ), - ) - end - model.reactions[rid].gene_associations = newisozymes - else - model.reactions[rid].gene_associations = nothing - end - end - - total_gene_product_mass_bound = 100.0 - - # set gene product bounds - for gid in genes(model) - lb, ub = gid == "b2779" ? (0.01, 0.06) : (0.0, 1.0) - model.genes[gid].product_lower_bound = lb - model.genes[gid].product_upper_bound = ub - end - - gm = - model |> - with_changed_bounds( - ["EX_glc__D_e", "GLCpts"]; - lower_bounds = [-1000.0, -1.0], - upper_bounds = [nothing, 12.0], - ) |> - with_enzyme_constraints(; total_gene_product_mass_bound) - - res = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - rxn_fluxes = values_dict(:reaction, res) - prot_concens = values_dict(:enzyme, res) - - @test isapprox( - rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8128427019072836, - atol = TEST_TOLERANCE, - ) - - mass_groups = values_dict(:enzyme_group, res) - - @test isapprox( - sum(values(prot_concens)), - total_gene_product_mass_bound, - atol = TEST_TOLERANCE, - ) - @test isapprox( - sum(values(prot_concens)), - mass_groups["uncategorized"], - atol = TEST_TOLERANCE, - ) - - # test pFBA - growth_lb = rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"] * 0.9 - - res = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [ - modify_objective(genes(gm); weights = [], sense = MIN_SENSE), - modify_constraint("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb), - modify_optimizer_attribute("IPM_IterationsLimit", 1000), - ], - ) - mass_groups_min = values_dict(:enzyme_group, res) - @test mass_groups_min["uncategorized"] < mass_groups["uncategorized"] - - res2 = - model |> - with_changed_bound("BIOMASS_Ecoli_core_w_GAM", lower_bound = growth_lb) |> - with_enzyme_constraints(; total_gene_product_mass_bound) |> - with_parsimonious_objective(:enzyme) |> - flux_balance_analysis( - Clarabel.Optimizer; - modifications = [modify_optimizer_attribute("max_iter", 1000), silence], - ) - - @test isapprox( - values_dict(:reaction, res2)["BIOMASS_Ecoli_core_w_GAM"], - 0.7315450597991255, - atol = QP_TEST_TOLERANCE, - ) - - @test isapprox( - values_dict(:enzyme_group, res2)["uncategorized"], - 89.35338, - atol = QP_TEST_TOLERANCE, - ) -end - -@testset "GECKO small model" begin - #= - Implement the small model found in the supplment of the - original GECKO paper. This model is nice to troubleshoot with, - because the stoich matrix is small. - =# - m = ObjectModel() - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") - - add_reactions!( - m, - [ - ReactionForward("r1", Dict("m1" => 1)), - ReactionForward("r2", Dict("m2" => 1)), - ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), - ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), - ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), - ReactionForward("r6", Dict("m4" => -1)), - ], - ) - - gs = [ - Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) - Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) - Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) - Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) - ] - - m.reactions["r3"].gene_associations = - [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] - m.reactions["r4"].gene_associations = [ - Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0), - Isozyme(["g2"]; kcat_forward = 3.0, kcat_backward = 3.0), - ] - m.reactions["r5"].gene_associations = [ - Isozyme(; - gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), - kcat_forward = 70.0, - kcat_backward = 70.0, - ), - ] - m.objective = Dict("r6" => 1.0) - - add_genes!(m, gs) - add_metabolites!(m, [m1, m2, m3, m4]) - - gm = make_enzyme_constrained_model( - m; - gene_product_mass_groups = Dict("uncategorized" => genes(m), "bound2" => ["g3"]), - gene_product_mass_group_bounds = Dict("uncategorized" => 0.5, "bound2" => 0.04), - ) - - res = flux_balance_analysis( - gm, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - rxn_fluxes = values_dict(:reaction, res) - gene_products = values_dict(:enzyme, res) - mass_groups = values_dict(:enzyme_group, res) - - @test isapprox(rxn_fluxes["r6"], 1.1688888886502442, atol = TEST_TOLERANCE) - @test isapprox(gene_products["g4"], 0.02666666666304931 * 4.0, atol = TEST_TOLERANCE) - @test isapprox(mass_groups["uncategorized"], 0.5, atol = TEST_TOLERANCE) - @test isapprox(mass_groups["bound2"], 0.04, atol = TEST_TOLERANCE) - @test length(genes(gm)) == 4 - @test length(genes(gm.inner)) == 4 - - # new pfba test - growth_lb = rxn_fluxes["r6"] * 0.9 - - mqp = - m |> - with_changed_bound("r6", lower_bound = growth_lb) |> - with_enzyme_constraints(; - gene_product_mass_groups = Dict( - "uncategorized" => genes(m), - "bound2" => ["g3"], - ), - gene_product_mass_group_bounds = Dict("uncategorized" => 0.5, "bound2" => 0.04), - ) |> - with_parsimonious_objective(:enzyme) - - @test all(stoichiometry(mqp) .== stoichiometry(gm)) - @test all(coupling(mqp) .== coupling(gm)) - - # this QP minimizes the squared sum of enzyme masses in g/gDW! - Q = sparse([9, 10, 11, 12], [9, 10, 11, 12], [-0.5, -2.0, -8.0, -4.5], 12, 13) - @test all(objective(mqp) .== Q) -end diff --git a/test/reconstruction/gapfill_minimum_reactions.jl b/test/reconstruction/gapfill_minimum_reactions.jl deleted file mode 100644 index a141b9ba3..000000000 --- a/test/reconstruction/gapfill_minimum_reactions.jl +++ /dev/null @@ -1,54 +0,0 @@ -@testset "Gap fill with minimum reactions" begin - #= - Implement the small model that should be gapfilled. - =# - model = ObjectModel() - - (m1, m2, m3, m4, m5, m6, m7, m8) = [Metabolite(id = "m$i") for i = 1:8] - - #= - here reactions are added to the model, but some are commented out. The goal - of the gap filling is to identify these commented reactions. - =# - add_reactions!( - model, - [ - ReactionForward("r1", Dict("m1" => 1); default_bound = 1), - ReactionBidirectional("r2", Dict("m1" => -1, "m2" => 1); default_bound = 10), - ReactionForward("r3", Dict("m1" => -1, "m3" => 1)), - ReactionBidirectional("r4", Dict("m2" => -1, "m4" => 1)), - # ReactionForward("r5", Dict("m3" => -1, "m4" => 1)), - ReactionForward("r6", Dict("m4" => -1)), - # ReactionForward("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1)), - ReactionForward("r8", Dict("m7" => -1, "m8" => 1)), - ReactionForward("r9", Dict("m8" => -1)), - # ReactionForward("r10", Dict("m6" => -1)), - ReactionForward("r11", Dict("m2" => -1, "m3" => -1, "m7" => -1)), - ReactionBidirectional("r12", Dict("m3" => -1, "m5" => 1); default_bound = 10), - ], - ) - - model.objective = Dict("r11" => 1) - - add_metabolites!(model, [m1, m2, m3, m4, m5, m7, m8]) - - r5 = ReactionForward("r5", Dict("m3" => -1, "m4" => 1)) - r7 = ReactionForward("r7", Dict("m2" => -1, "m7" => 1, "m6" => 1)) - r10 = ReactionForward("r10", Dict("m6" => -1)) - rA = ReactionForward("rA", Dict("m1" => -1, "m2" => 1, "m3" => 1)) - rB = ReactionForward("rB", Dict("m2" => -1, "m9" => 1)) - rC = ReactionBidirectional("rC", Dict("m9" => -1, "m10" => 1)) - rD = ReactionBackward("rC", Dict("m10" => -1)) - - universal_reactions = [r5, r7, r10, rA, rB, rC, rD] - - rxns = - gapfill_minimum_reactions( - model, - universal_reactions, - GLPK.Optimizer; - objective_bounds = (0.1, 1000.0), - ) |> gapfilled_rids(universal_reactions) - - @test issetequal(["r7", "r10"], rxns) -end diff --git a/test/reconstruction/knockouts.jl b/test/reconstruction/knockouts.jl deleted file mode 100644 index 8133009af..000000000 --- a/test/reconstruction/knockouts.jl +++ /dev/null @@ -1,82 +0,0 @@ -""" -The gene-reaction rules (grr) are written as -[[gene1 and gene2...] or [gene3 and...] ...] -so for a reaction to be available it is sufficient that one group -is available, but inside a group all of the genes need to be available -""" - -@testset "knockout_single_gene" begin - m = ObjectModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_reaction!( - m, - Reaction( - "v1"; - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1"]]], - ), - ) - add_reaction!( - m, - Reaction( - "v2", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1", "g2"]]], - ), - ) - add_reaction!( - m, - Reaction( - "v3", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1"], ["g2"]]], - ), - ) - add_reaction!( - m, - Reaction( - "v4", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1", "g2"], ["g2"]]], - ), - ) - - remove_gene!(m, "g1", knockout_reactions = true) - - @test length(m.reactions) == 2 - @test !haskey(m.reactions, "v1") - @test !haskey(m.reactions, "v2") -end - -@testset "knockout_multiple_genes" begin - m = ObjectModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite(id = "B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_gene!(m, Gene("g3")) - add_reaction!( - m, - Reaction( - "v1"; - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1"], ["g2"], ["g3"]]], - ), - ) - add_reaction!( - m, - Reaction( - "v2"; - metabolites = Dict("A" => 1.0, "B" => -1.0), - gene_associations = [Isozyme(x) for x in [["g1"], ["g3"]]], - ), - ) - - remove_genes!(m, ["g1", "g3"], knockout_reactions = true) - - @test haskey(m.reactions, "v1") - @test !haskey(m.reactions, "v2") -end diff --git a/test/reconstruction/simplified_enzyme_constrained.jl b/test/reconstruction/simplified_enzyme_constrained.jl deleted file mode 100644 index a0b22fd63..000000000 --- a/test/reconstruction/simplified_enzyme_constrained.jl +++ /dev/null @@ -1,114 +0,0 @@ -@testset "SMOMENT" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # add molar masses to gene products - for gid in genes(model) - model.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) - end - model.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) - - # update isozymes with kinetic information - for rid in reactions(model) - if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr - newisozymes = Isozyme[] - for (i, grr) in enumerate(reaction_gene_associations(model, rid)) - push!( - newisozymes, - Isozyme( - gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ), - ) - end - model.reactions[rid].gene_associations = newisozymes - else - model.reactions[rid].gene_associations = nothing - end - end - - simplified_enzyme_constrained_model = - model |> - with_changed_bounds( - ["EX_glc__D_e", "GLCpts"], - lower_bounds = [-1000.0, -1.0], - upper_bounds = [nothing, 12.0], - ) |> - with_simplified_enzyme_constraints(total_reaction_mass_bound = 100.0) - - rxn_fluxes = - flux_balance_analysis( - simplified_enzyme_constrained_model, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) |> values_dict - - @test isapprox(rxn_fluxes["BIOMASS_Ecoli_core_w_GAM"], 0.890731, atol = TEST_TOLERANCE) -end - -@testset "Small SMOMENT" begin - - m = ObjectModel() - m1 = Metabolite("m1") - m2 = Metabolite("m2") - m3 = Metabolite("m3") - m4 = Metabolite("m4") - - add_reactions!( - m, - [ - ReactionForward("r1", Dict("m1" => 1)), - ReactionForward("r2", Dict("m2" => 1)), - ReactionForward("r3", Dict("m1" => -1, "m2" => -1, "m3" => 1)), - ReactionForward("r4", Dict("m3" => -1, "m4" => 1)), - ReactionBidirectional("r5", Dict("m2" => -1, "m4" => 1)), - ReactionForward("r6", Dict("m4" => -1)), - ], - ) - - gs = [ - Gene(id = "g1", product_upper_bound = 10.0, product_molar_mass = 1.0) - Gene(id = "g2", product_upper_bound = 10.0, product_molar_mass = 2.0) - Gene(id = "g3", product_upper_bound = 10.0, product_molar_mass = 3.0) - Gene(id = "g4", product_upper_bound = 10.0, product_molar_mass = 4.0) - ] - - m.reactions["r3"].gene_associations = - [Isozyme(["g1"]; kcat_forward = 1.0, kcat_backward = 1.0)] - m.reactions["r4"].gene_associations = - [Isozyme(["g1"]; kcat_forward = 2.0, kcat_backward = 2.0)] - m.reactions["r5"].gene_associations = [ - Isozyme(; - gene_product_stoichiometry = Dict("g3" => 1, "g4" => 2), - kcat_forward = 2.0, - kcat_backward = 2.0, - ), - ] - m.objective = Dict("r6" => 1.0) - - add_genes!(m, gs) - add_metabolites!(m, [m1, m2, m3, m4]) - - sm = make_simplified_enzyme_constrained_model( - m; - reaction_mass_groups = Dict("b1" => ["r3", "r4"], "b2" => ["r5", "r4"]), - reaction_mass_group_bounds = Dict("b2" => 0.5, "b1" => 0.2), - ) - - stoichiometry(sm) - - cpling = sum(coupling(sm), dims = 2) - @test 1.5 in cpling && 11.5 in cpling - - lbs, ubs = coupling_bounds(sm) - @test all(lbs .== 0.0) - @test 0.5 in ubs && 0.2 in ubs - - res = flux_balance_analysis( - sm, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test isapprox(solved_objective_value(res), 0.21212120975836252; atol = TEST_TOLERANCE) -end diff --git a/test/runtests.jl b/test/runtests.jl index d1fcd5fa9..0aa9e89c7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,22 +1,14 @@ using COBREXA, Test - -using COBREXA.Everything - using Aqua using Clarabel using Distributed using Downloads using GLPK # for MILPs -using JSON -using JuMP using LinearAlgebra -using MAT -using OrderedCollections using Serialization using SHA using SparseArrays using Statistics -using Tulip # tolerance for comparing analysis results (should be a bit bigger than the # error tolerance in computations) @@ -41,15 +33,7 @@ end # set up the workers for Distributed, so that the tests that require more # workers do not unnecessarily load the stuff multiple times W = addprocs(2) -t = @elapsed @everywhere using COBREXA, COBREXA.Analysis, Tulip, JuMP -print_timing("import of packages", t) -t = @elapsed @everywhere begin - model = Model(Tulip.Optimizer) - @variable(model, 0 <= x <= 1) - @objective(model, Max, x) - optimize!(model) -end -print_timing("JuMP+Tulip code warmup", t) +t = @elapsed @everywhere using COBREXA, Tulip, JuMP # make sure there's a directory for temporary data tmpdir = "tmpfiles" @@ -62,15 +46,7 @@ run_test_file("data_downloaded.jl") # import base files @testset "COBREXA test suite" begin - run_test_dir(joinpath("types", "abstract"), "Abstract types") - run_test_dir("types", "Model types and wrappers") - run_test_dir(joinpath("types", "misc"), "Model helper functions") - run_test_file("log.jl") - run_test_dir("utils", "Utilities") - run_test_dir("io", "I/O functions") - run_test_dir("reconstruction") run_test_dir("analysis") - run_test_dir(joinpath("analysis", "sampling"), "Sampling") run_test_file("aqua.jl") end diff --git a/test/types/CommunityModel.jl b/test/types/CommunityModel.jl deleted file mode 100644 index 9804d4787..000000000 --- a/test/types/CommunityModel.jl +++ /dev/null @@ -1,416 +0,0 @@ -@testset "CommunityModel: structure" begin - - m1 = ObjectModel() - add_metabolites!( - m1, - [Metabolite("A"), Metabolite("B"), Metabolite("Ae"), Metabolite("Be")], - ) - add_genes!(m1, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) - add_reactions!( - m1, - [ - ReactionBidirectional("EX_A", Dict("Ae" => -1)), - ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), - ReactionForward("r2", Dict("A" => -1, "B" => 1)), # bm1 - ReactionForward("r3", Dict("B" => -1, "Be" => 1)), - ReactionForward("EX_B", Dict("Be" => -1)), - ], - ) - - m2 = ObjectModel() - add_metabolites!( - m2, - [Metabolite("Ae"), Metabolite("A"), Metabolite("C"), Metabolite("Ce")], - ) - add_genes!(m2, [Gene("g1"), Gene("g2"), Gene("g3"), Gene("g4")]) - add_reactions!( - m2, - [ - ReactionForward("r3", Dict("C" => -1, "Ce" => 1)), - ReactionForward("EX_C", Dict("Ce" => -1)), - ReactionBidirectional("EX_A", Dict("Ae" => -1)), - ReactionBidirectional("r1", Dict("Ae" => -1, "A" => 1)), - ReactionForward("r2", Dict("A" => -1, "C" => 1)), #bm2 - ], - ) - - cm1 = CommunityMember( - model = m1, - exchange_reaction_ids = ["EX_A", "EX_B"], - biomass_reaction_id = "r2", - ) - @test contains(sprint(show, MIME("text/plain"), cm1), "community member") - - cm2 = CommunityMember( - model = m2, - exchange_reaction_ids = ["EX_A", "EX_C"], - biomass_reaction_id = "r2", - ) - - cm = CommunityModel( - members = OrderedDict("m1" => cm1, "m2" => cm2), - abundances = [0.2, 0.8], - environmental_links = [ - EnvironmentalLink("EX_A", "Ae", -10.0, 10.0) - EnvironmentalLink("EX_B", "Be", -20.0, 20.0) - EnvironmentalLink("EX_C", "Ce", -30, 30) - ], - ) - @test contains(sprint(show, MIME("text/plain"), cm), "community model") - - @test issetequal( - variables(cm), - [ - "m1#EX_A" - "m1#r1" - "m1#r2" - "m1#r3" - "m1#EX_B" - "m2#r3" - "m2#EX_C" - "m2#EX_A" - "m2#r1" - "m2#r2" - "EX_A" - "EX_C" - "EX_B" - ], - ) - - @test issetequal( - metabolites(cm), - [ - "m1#A" - "m1#B" - "m1#Ae" - "m1#Be" - "m2#Ae" - "m2#A" - "m2#C" - "m2#Ce" - "ENV_Ae" - "ENV_Be" - "ENV_Ce" - ], - ) - - @test issetequal( - genes(cm), - [ - "m1#g1" - "m1#g2" - "m1#g3" - "m1#g4" - "m2#g1" - "m2#g2" - "m2#g3" - "m2#g4" - ], - ) - - @test n_variables(cm) == 13 - @test n_metabolites(cm) == 11 - @test n_genes(cm) == 8 - - @test all( - stoichiometry(cm) .== [ - 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - -1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 -1.0 -1.0 0.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.0 0.0 0.0 0.0 1.0 0.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 1.0 -1.0 0.0 0.0 0.0 0.0 0.0 0.0 - 0.2 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 -1.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.2 0.0 0.0 0.0 0.0 0.0 0.0 -0.2 0.0 - 0.0 0.0 0.0 0.0 0.0 0.0 0.8 0.0 0.0 0.0 0.0 0.0 -0.8 - ], - ) - - lbs, ubs = bounds(cm) - @test all(lbs .== [ - -1000.0 - -1000.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - -1000.0 - -1000.0 - 0.0 - -10.0 - -20.0 - -30.0 - ]) - @test all( - ubs .== [ - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 1000.0 - 10.0 - 20.0 - 30.0 - ], - ) - - @test all(objective(cm) .== [ - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - 0.0 - ]) - - @test n_coupling_constraints(cm) == 0 - @test isempty(coupling(cm)) - @test all(isempty.(coupling_bounds(cm))) - - # test modification for community model - res = flux_balance_analysis(cm, Tulip.Optimizer) - mb = res.result[:mb] - x = res.result[:x] - @test normalized_coefficient(mb[9], x[1]) == 0.2 - @test normalized_coefficient(mb[11], x[13]) == -0.8 - - res2 = flux_balance_analysis( - cm, - Tulip.Optimizer; - modifications = [modify_abundances([0.5, 0.5])], - ) - mb = res2.result[:mb] - x = res2.result[:x] - @test normalized_coefficient(mb[9], x[1]) == 0.5 - @test normalized_coefficient(mb[11], x[13]) == -0.5 - - @test_throws ArgumentError flux_balance_analysis( - m1, - Tulip.Optimizer; - modifications = [modify_abundances([0.5, 0.5])], - ) - @test_throws DomainError flux_balance_analysis( - cm, - Tulip.Optimizer; - modifications = [modify_abundances([0.3, 0.5])], - ) - - # test modification for EqualGrowthCommunityModel - eqgr = cm |> with_equal_growth_objective() - - res3 = flux_balance_analysis( - cm, - Tulip.Optimizer; - modifications = [modify_abundances([0.3, 0.7])], - ) - mb = res3.result[:mb] - x = res3.result[:x] - @test normalized_coefficient(mb[10], x[5]) == 0.3 - @test normalized_coefficient(mb[10], x[12]) == -0.3 -end - -@testset "EqualGrowthCommunityModel: e coli core" begin - - ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) - ecoli.reactions["EX_glc__D_e"].lower_bound = -1000 - ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) - - cm1 = CommunityMember( - model = ecoli, - exchange_reaction_ids = ex_rxns, - biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", - ) - cm2 = CommunityMember( - model = ecoli, - exchange_reaction_ids = ex_rxns, - biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", - ) - - ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) - ex_mids = [first(keys(reaction_stoichiometry(ecoli, rid))) for rid in ex_rxns] - ex_lbs = [ecoli.reactions[rid].lower_bound for rid in ex_rxns] - ex_ubs = [ecoli.reactions[rid].upper_bound for rid in ex_rxns] - - a1 = 0.1 # abundance species 1 - a2 = 0.8 # abundance species 2 - - cm = CommunityModel( - members = OrderedDict("ecoli1" => cm1, "ecoli2" => cm2), - abundances = [a1, a2], - environmental_links = [ - EnvironmentalLink(rid, mid, lb, ub) for - (rid, mid, lb, ub) in zip(ex_rxns, ex_mids, ex_lbs, ex_ubs) - ], - ) - a1 = 0.2 - change_abundances!(cm, [a1, 0.8]) - @test cm.abundances[1] == 0.2 - - @test_throws DomainError cm |> with_changed_abundances([0.1, 0.2]) - - @test_throws DomainError cm |> with_changed_environmental_bound( - "abc"; - lower_bound = -10, - upper_bound = 10, - ) - - cm3 = - cm |> - with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -10, upper_bound = 10) - change_environmental_bound!(cm3, "EX_ac_e"; upper_bound = 100) - - @test cm3.environmental_links[1].upper_bound == 100 - @test cm3.environmental_links[9].lower_bound == -10 - - eqcm = cm3 |> with_equal_growth_objective() - d = flux_balance_analysis(eqcm, Tulip.Optimizer) |> values_dict - - @test isapprox( - d[eqcm.community_objective_id], - 0.8739215069521299, - atol = TEST_TOLERANCE, - ) - - # test if growth rates are the same - @test isapprox( - d["ecoli1#BIOMASS_Ecoli_core_w_GAM"], - d[eqcm.community_objective_id], - atol = TEST_TOLERANCE, - ) - - @test isapprox( - d["ecoli2#BIOMASS_Ecoli_core_w_GAM"], - d[eqcm.community_objective_id], - atol = TEST_TOLERANCE, - ) - - @test isapprox( - d["EX_glc__D_e"], - a1 * d["ecoli1#EX_glc__D_e"] + a2 * d["ecoli2#EX_glc__D_e"], - atol = TEST_TOLERANCE, - ) - - # test if model can be converted to another type - om = convert(ObjectModel, cm) - @test n_variables(om) == n_variables(cm) -end - -@testset "EqualGrowthCommunityModel: enzyme constrained e coli" begin - ecoli = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # add molar masses to gene products - for gid in genes(ecoli) - ecoli.genes[gid].product_molar_mass = get(ecoli_core_gene_product_masses, gid, 0.0) - ecoli.genes[gid].product_upper_bound = 10.0 - end - ecoli.genes["s0001"] = Gene(id = "s0001"; product_molar_mass = 0.0) - ecoli.genes["s0001"].product_upper_bound = 10.0 - - # update isozymes with kinetic information - for rid in reactions(ecoli) - if haskey(ecoli_core_reaction_kcats, rid) # if has kcat, then has grr - newisozymes = Isozyme[] - for (i, grr) in enumerate(reaction_gene_associations(ecoli, rid)) - push!( - newisozymes, - Isozyme( - gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ), - ) - end - ecoli.reactions[rid].gene_associations = newisozymes - else - ecoli.reactions[rid].gene_associations = nothing - end - end - - ex_rxns = COBREXA.Utils.find_exchange_reaction_ids(ecoli) - - gm = - ecoli |> - with_changed_bound("EX_glc__D_e"; lower_bound = -1000.0, upper_bound = 0) |> - with_enzyme_constraints(total_gene_product_mass_bound = 100.0) - - cm1 = CommunityMember( - model = gm, - exchange_reaction_ids = ex_rxns, - biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", - ) - cm2 = CommunityMember( - model = gm, - exchange_reaction_ids = ex_rxns, - biomass_reaction_id = "BIOMASS_Ecoli_core_w_GAM", - ) - - a1 = 0.2 # abundance species 1 - a2 = 0.8 # abundance species 2 - - ex_mids = [first(keys(reaction_stoichiometry(ecoli, rid))) for rid in ex_rxns] - ex_lbs = [ecoli.reactions[rid].lower_bound for rid in ex_rxns] - ex_ubs = [ecoli.reactions[rid].upper_bound for rid in ex_rxns] - - cm = CommunityModel( - members = OrderedDict("ecoli1" => cm1, "ecoli2" => cm2), - abundances = [a1, a2], - environmental_links = [ - EnvironmentalLink(rid, mid, lb, ub) for - (rid, mid, lb, ub) in zip(ex_rxns, ex_mids, ex_lbs, ex_ubs) - ], - ) - - eqgr = - cm |> - with_changed_environmental_bound("EX_glc__D_e"; lower_bound = -1000.0) |> - with_equal_growth_objective() - - res = flux_balance_analysis( - eqgr, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], - ) - - f_a = values_dict(res) - @test length(f_a) == 665 - - f_r = values_dict(:reaction, res) - @test isapprox( - f_r[eqgr.community_objective_id], - 0.9210836692534606, - atol = TEST_TOLERANCE, - ) - - # test convenience operators - f_env = values_dict(:environmental_exchange, res) - @test isapprox( - f_env["EX_o2_e"], - a1 * f_r["ecoli1#EX_o2_e"] + a2 * f_r["ecoli2#EX_o2_e"]; - atol = TEST_TOLERANCE, - ) - - f_e = values_dict(:enzyme, res) - @test isapprox( - sum(v for (k, v) in f_e if startswith(k, "ecoli1")), - 100.0; - atol = TEST_TOLERANCE, - ) - - f_g = values_dict(:enzyme_group, res) - @test isapprox(f_g["ecoli2#uncategorized"], 100.0; atol = TEST_TOLERANCE) -end diff --git a/test/types/FluxSummary.jl b/test/types/FluxSummary.jl deleted file mode 100644 index f0e6bfdd8..000000000 --- a/test/types/FluxSummary.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "Flux summary" begin - model = load_model(model_paths["e_coli_core.json"]) - - sol = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 200)], - ) |> values_dict - - fr = flux_summary(sol; keep_unbounded = true, large_flux_bound = 25) - - @test isapprox( - fr.biomass_fluxes["BIOMASS_Ecoli_core_w_GAM"], - 0.8739215022690006; - atol = TEST_TOLERANCE, - ) - @test isapprox(fr.export_fluxes["EX_co2_e"], 22.80983339307183; atol = TEST_TOLERANCE) - @test isapprox(fr.import_fluxes["EX_o2_e"], -21.799492758430517; atol = TEST_TOLERANCE) - @test isapprox( - fr.unbounded_fluxes["EX_h2o_e"], - 29.175827202663395; - atol = TEST_TOLERANCE, - ) -end diff --git a/test/types/FluxVariabilitySummary.jl b/test/types/FluxVariabilitySummary.jl deleted file mode 100644 index 7cc2e003c..000000000 --- a/test/types/FluxVariabilitySummary.jl +++ /dev/null @@ -1,23 +0,0 @@ -@testset "Flux variability summary" begin - model = load_model(model_paths["e_coli_core.json"]) - - sol = - flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.90), - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 2000)], - ) |> result - - fr = flux_variability_summary(sol) - @test isapprox( - fr.biomass_fluxes["BIOMASS_Ecoli_core_w_GAM"][1], - 0.7865293520891825; - atol = TEST_TOLERANCE, - ) - @test isapprox( - fr.exchange_fluxes["EX_for_e"][2], - 11.322324494491848; - atol = TEST_TOLERANCE, - ) -end diff --git a/test/types/Gene.jl b/test/types/Gene.jl deleted file mode 100644 index f681d13e3..000000000 --- a/test/types/Gene.jl +++ /dev/null @@ -1,32 +0,0 @@ -@testset "Gene: construction, printing, utils" begin - g = Gene("testgene") - - # test defaults - @test isempty(g.notes) - @test isempty(g.annotations) - - # Now assign - g.id = "gene1" - g.notes = Dict("notes" => ["blah", "blah"]) - g.annotations = Dict("sboterm" => ["sbo"], "ncbigene" => ["ads", "asds"]) - - # Test pretty printing - @test all(contains.(sprint(show, MIME("text/plain"), g), ["gene1", "blah", "asds"])) - - # Test duplicate annotation finder - g2 = Gene("gene2") - g2.annotations = Dict("sboterm" => ["sbo2"], "ncbigene" => ["fff", "ggg"]) - g3 = Gene("g3") - g3.annotations = Dict("sboterm" => ["sbo3"], "ncbigene" => ["ads"]) - g4 = Gene("g4") - g4.annotations = Dict("sboterm" => ["sbo4"], "ncbigene" => ["ads22", "asd22s"]) - gdict = OrderedDict(g.id => g for g in [g, g2, g3, g4]) # this is how genes are stored in ObjectModel - - idx = annotation_index(gdict) - @test length(idx["ncbigene"]["ads"]) > 1 - @test "gene1" in idx["ncbigene"]["ads"] - - ambiguous = ambiguously_identified_items(idx) - @test "g3" in ambiguous - @test !("g4" in ambiguous) -end diff --git a/test/types/JSONModel.jl b/test/types/JSONModel.jl deleted file mode 100644 index 97b8ff05e..000000000 --- a/test/types/JSONModel.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "Conversion from and to SBML model" begin - json_model = model_paths["iJO1366.json"] - - jm = load_json_model(json_model) - sm = convert(ObjectModel, jm) - jm2 = convert(JSONModel, sm) - - @test Set(variables(jm)) == Set(variables(sm)) - @test Set(variables(jm)) == Set(variables(jm2)) -end - -@testset "JSONModel generic interface" begin - model = load_model(model_paths["e_coli_core.json"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) -end diff --git a/test/types/MATModel.jl b/test/types/MATModel.jl deleted file mode 100644 index 4bd536a7c..000000000 --- a/test/types/MATModel.jl +++ /dev/null @@ -1,18 +0,0 @@ - -@testset "Conversion from and to MATLAB model" begin - filename = model_paths["iJO1366.mat"] - - mm = load_mat_model(filename) - sm = convert(ObjectModel, mm) - mm2 = convert(MATModel, sm) - - @test Set(variables(mm)) == Set(variables(sm)) - @test Set(variables(mm)) == Set(variables(mm2)) -end - -@testset "MATModel generic interface" begin - model = load_model(model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) -end diff --git a/test/types/MatrixCoupling.jl b/test/types/MatrixCoupling.jl deleted file mode 100644 index c2b816fd8..000000000 --- a/test/types/MatrixCoupling.jl +++ /dev/null @@ -1,20 +0,0 @@ -@testset "MatrixModelWithCoupling generic interface" begin - model = load_model(MatrixModelWithCoupling, model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) - -end - -@testset "Conversion from and to ObjectModel" begin - cm = load_model(MatrixModelWithCoupling, model_paths["e_coli_core.mat"]) - - sm = convert(ObjectModel, cm) - cm2 = convert(MatrixModelWithCoupling, sm) - - @test Set(variables(cm)) == Set(variables(sm)) - @test Set(variables(cm)) == Set(variables(cm2)) - - @test reaction_gene_associations(sm, variables(sm)[1]) == - reaction_gene_associations(cm, variables(sm)[1]) -end diff --git a/test/types/MatrixModel.jl b/test/types/MatrixModel.jl deleted file mode 100644 index c6ddfcbec..000000000 --- a/test/types/MatrixModel.jl +++ /dev/null @@ -1,19 +0,0 @@ -@testset "MatrixModel generic interface" begin - model = load_model(MatrixModel, model_paths["e_coli_core.mat"]) - - @test reaction_stoichiometry(model, "EX_ac_e") == Dict("ac_e" => -1) - @test reaction_stoichiometry(model, 44) == Dict("ac_e" => -1) -end - -@testset "Conversion from and to ObjectModel" begin - cm = load_model(MatrixModel, model_paths["e_coli_core.mat"]) - - sm = convert(ObjectModel, cm) - cm2 = convert(MatrixModel, sm) - - @test Set(variables(cm)) == Set(variables(sm)) - @test Set(variables(cm)) == Set(variables(cm2)) - - @test sort(sort.(reaction_gene_associations(sm, reactions(sm)[1]))) == - sort(sort.(reaction_gene_associations(cm, reactions(sm)[1]))) -end diff --git a/test/types/Metabolite.jl b/test/types/Metabolite.jl deleted file mode 100644 index 66d04cbd0..000000000 --- a/test/types/Metabolite.jl +++ /dev/null @@ -1,31 +0,0 @@ -@testset "Metabolite" begin - m1 = Metabolite("testmetabolite") - m1.id = "met1" - m1.formula = "C6H12O6N" - m1.charge = 1 - m1.compartment = "c" - m1.notes = Dict("notes" => ["blah", "blah"]) - m1.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ads", "asds"]) - - @test all( - contains.( - sprint(show, MIME("text/plain"), m1), - ["met1", "C6H12O6N", "blah", "asds"], - ), - ) - - m2 = Metabolite("met2") - - m2.formula = "C6H12O6N" - - m3 = Metabolite("met3") - m3.formula = "X" - m3.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["ad2s", "asds"]) - - m4 = Metabolite("met4") - m4.formula = "X" - m4.annotations = Dict("sboterm" => ["sbo"], "kegg.compound" => ["adxxx2s", "asdxxxs"]) - - md = OrderedDict(m.id => m for m in [m1, m2, m3]) - @test issetequal(["met1", "met3"], ambiguously_identified_items(annotation_index(md))) -end diff --git a/test/types/ModelWithResult.jl b/test/types/ModelWithResult.jl deleted file mode 100644 index 6ee2a274e..000000000 --- a/test/types/ModelWithResult.jl +++ /dev/null @@ -1,5 +0,0 @@ -@testset "ModelWithResult" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - res = flux_balance_analysis(model, Tulip.Optimizer) - @test contains(sprint(show, MIME("text/plain"), res), "ModelWithResult") -end diff --git a/test/types/ObjectModel.jl b/test/types/ObjectModel.jl deleted file mode 100644 index f1528a1c3..000000000 --- a/test/types/ObjectModel.jl +++ /dev/null @@ -1,152 +0,0 @@ -@testset "ObjectModel generic interface" begin - # create a small model - m1 = Metabolite("m1") - m1.formula = "C2H3" - m1.compartment = "cytosol" - m2 = Metabolite("m2") - m2.formula = "H3C2" - m3 = Metabolite("m3") - m3.charge = -1 - m4 = Metabolite("m4") - m4.notes = Dict("confidence" => ["iffy"]) - m4.annotations = Dict("sbo" => ["blah"]) - - g1 = Gene("g1") - g2 = Gene("g2") - g2.notes = Dict("confidence" => ["iffy"]) - g2.annotations = Dict("sbo" => ["blah"]) - g3 = Gene("g3") - - r1 = Reaction(id = "r1") - r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lower_bound = -100.0 - r1.upper_bound = 100.0 - r1.gene_associations = [ - Isozyme(gene_product_stoichiometry = Dict("g1" => 1, "g2" => 1)), - Isozyme(gene_product_stoichiometry = Dict("g3" => 1)), - ] - r1.subsystem = "glycolysis" - r1.notes = Dict("notes" => ["blah", "blah"]) - r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - - r2 = ReactionBackward("r2", Dict(m1.id => -2.0, m4.id => 1.0)) - r3 = ReactionForward("r3", Dict(m3.id => -1.0, m4.id => 1.0)) - r4 = ReactionBidirectional("r4", Dict(m3.id => -1.0, m4.id => 1.0)) - r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - - mets = [m1, m2, m3, m4] - gs = [g1, g2, g3] - rxns = [r1, r2, r3, r4] - - model = ObjectModel() - model.reactions = OrderedDict(r.id => r for r in rxns) - model.metabolites = OrderedDict(m.id => m for m in mets) - model.genes = OrderedDict(g.id => g for g in gs) - model.objective = Dict("r1" => 1.0) - - @test contains(sprint(show, MIME("text/plain"), model), "ObjectModel") - - @test "r1" in variables(model) - @test "m4" in metabolites(model) - @test "g2" in genes(model) - @test n_variables(model) == 4 - @test n_metabolites(model) == 4 - @test n_genes(model) == 3 - - S_test = spzeros(4, 4) - S_test[1, 1] = -1.0 - S_test[2, 1] = 1.0 - S_test[1, 2] = -2.0 - S_test[4, 2] = 1.0 - S_test[3, 3] = -1.0 - S_test[4, 3] = 1.0 - S_test[3, 4] = -1.0 - S_test[4, 4] = 1.0 - @test S_test == stoichiometry(model) - - lb_test = spzeros(4) - lb_test[1] = -100.0 - lb_test[2] = -1000.0 - lb_test[3] = 0.0 - lb_test[4] = -1000.0 - ub_test = spzeros(4) - ub_test[1] = 100.0 - ub_test[2] = 0.0 - ub_test[3] = 1000.0 - ub_test[4] = 1000.0 - lbs, ubs = bounds(model) - @test lb_test == lbs - @test ub_test == ubs - - @test balance(model) == spzeros(n_metabolites(model)) - - obj_test = spzeros(4) - obj_test[1] = 1.0 - @test objective(model) == obj_test - - @test all( - occursin.( - ["g1", "g2", "g3"], - Ref( - COBREXA.Internal.unparse_grr( - String, - reaction_gene_associations(model, "r1"), - ), - ), - ), - ) - @test isnothing(reaction_gene_associations(model, "r2")) - - @test metabolite_formula(model, "m2")["C"] == 2 - @test isnothing(metabolite_formula(model, "m3")) - - @test metabolite_charge(model, "m3") == -1 - @test isnothing(metabolite_charge(model, "m2")) - - @test metabolite_compartment(model, "m1") == "cytosol" - @test isnothing(metabolite_compartment(model, "m2")) - - @test reaction_subsystem(model, "r1") == "glycolysis" - @test isnothing(reaction_subsystem(model, "r2")) - - @test metabolite_notes(model, "m4")["confidence"] == ["iffy"] - @test metabolite_annotations(model, "m4")["sbo"] == ["blah"] - @test isempty(metabolite_notes(model, "m3")) - @test isempty(metabolite_annotations(model, "m3")) - - @test gene_notes(model, "g2")["confidence"] == ["iffy"] - @test gene_annotations(model, "g2")["sbo"] == ["blah"] - @test isempty(gene_notes(model, "g1")) - @test isempty(gene_annotations(model, "g1")) - - @test reaction_notes(model, "r1")["notes"] == ["blah", "blah"] - @test reaction_annotations(model, "r1")["biocyc"] == ["ads", "asds"] - @test isempty(reaction_notes(model, "r2")) - @test isempty(reaction_annotations(model, "r2")) - - @test reaction_stoichiometry(model, "r1") == Dict("m1" => -1.0, "m2" => 1.0) - - # To do: test convert - same_model = convert(ObjectModel, model) - @test same_model == model - - jsonmodel = convert(JSONModel, model) - stdmodel = convert(ObjectModel, jsonmodel) - @test issetequal(variables(jsonmodel), variables(stdmodel)) - @test issetequal(genes(jsonmodel), genes(stdmodel)) - @test issetequal(metabolites(jsonmodel), metabolites(stdmodel)) - jlbs, jubs = bounds(jsonmodel) - slbs, subs = bounds(stdmodel) - @test issetequal(jlbs, slbs) - @test issetequal(jubs, subs) - jS = stoichiometry(jsonmodel) - sS = stoichiometry(stdmodel) - j_r1_index = findfirst(x -> x == "r1", variables(jsonmodel)) - s_r1_index = findfirst(x -> x == "r1", variables(stdmodel)) - j_m1_index = findfirst(x -> x == "m1", metabolites(jsonmodel)) - j_m2_index = findfirst(x -> x == "m2", metabolites(jsonmodel)) - s_m1_index = findfirst(x -> x == "m1", metabolites(stdmodel)) - s_m2_index = findfirst(x -> x == "m2", metabolites(stdmodel)) - @test jS[j_m1_index, j_r1_index] == sS[s_m1_index, s_r1_index] - @test jS[j_m2_index, j_r1_index] == sS[s_m2_index, s_r1_index] -end diff --git a/test/types/Reaction.jl b/test/types/Reaction.jl deleted file mode 100644 index bb589bbed..000000000 --- a/test/types/Reaction.jl +++ /dev/null @@ -1,85 +0,0 @@ -@testset "Reaction" begin - m1 = Metabolite("m1") - m1.formula = "C2H3" - m2 = Metabolite("m2") - m2.formula = "H3C2" - m3 = Metabolite("m3") - m4 = Metabolite("m4") - m5 = Metabolite("m5") - - g1 = Gene("g1") - g2 = Gene("g2") - g3 = Gene("g3") - - r1 = Reaction(id = "r1") - r1.metabolites = Dict(m1.id => -1.0, m2.id => 1.0) - r1.lower_bound = -100.0 - r1.upper_bound = 100.0 - r1.gene_associations = [ - Isozyme(gene_product_stoichiometry = Dict("g1" => 1, "g2" => 1)), - Isozyme(gene_product_stoichiometry = Dict("g3" => 1)), - ] - r1.subsystem = "glycolysis" - r1.notes = Dict("notes" => ["blah", "blah"]) - r1.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - - @test all( - contains.( - sprint(show, MIME("text/plain"), r1), - ["r1", "100.0", "glycolysis", "blah", "biocyc", "g3"], - ), - ) - - r2 = ReactionBackward("r2", Dict(m1.id => -2.0, m4.id => 1.0)) - @test r2.lower_bound == -1000.0 && r2.upper_bound == 0.0 - - r3 = ReactionForward("r3", Dict(m3.id => -1.0, m4.id => 1.0)) - @test r3.lower_bound == 0.0 && r3.upper_bound == 1000.0 - - r4 = ReactionBidirectional("r4", Dict(m4.id => -1.0, m5.id => 1.0)) - r4.annotations = Dict("sboterm" => ["sbo"], "biocyc" => ["ads", "asds"]) - @test r4.lower_bound == -1000.0 && r4.upper_bound == 1000.0 - - rd = OrderedDict(r.id => r for r in [r1, r2, r3, r4]) - @test issetequal(["r1", "r4"], ambiguously_identified_items(annotation_index(rd))) - - model = ObjectModel() - add_reactions!(model, [r1, r2, r3, r4]) - add_metabolites!(model, [m1, m2, m3, m4, m5]) - - @test isempty(reaction_is_duplicated(model, r4)) - @test isempty( - reaction_is_duplicated( - model, - ReactionBidirectional("r5", Dict(m3.id => -1.0, m4.id => 1.0)), - ), - ) - @test "r3" in reaction_is_duplicated( - model, - ReactionForward("r5", Dict(m3.id => -1.0, m4.id => 1.0)), - ) - @test "r3" in reaction_is_duplicated( - model, - ReactionBackward("r5", Dict(m3.id => 1.0, m4.id => -1.0)), - ) - @test isempty( - reaction_is_duplicated( - model, - ReactionBackward("r5", Dict(m3.id => -1.0, m4.id => 1.0)), - ), - ) - @test "r4" in reaction_is_duplicated( - model, - ReactionBidirectional("r5", Dict(m4.id => -1.0, m5.id => 1.0)), - ) - @test "r4" in reaction_is_duplicated( - model, - ReactionBidirectional("r5", Dict(m4.id => 1.0, m5.id => -1.0)), - ) - @test isempty( - reaction_is_duplicated( - model, - ReactionBidirectional("r5", Dict(m4.id => 2.0, m5.id => -1.0)), - ), - ) -end diff --git a/test/types/SBMLModel.jl b/test/types/SBMLModel.jl deleted file mode 100644 index 1cb52cb59..000000000 --- a/test/types/SBMLModel.jl +++ /dev/null @@ -1,36 +0,0 @@ - -@testset "Conversion from and to SBML model" begin - sbmlm = load_sbml_model(model_paths["ecoli_core_model.xml"]) - sm = convert(ObjectModel, sbmlm) - sbmlm2 = convert(SBMLModel, sm) - - @test Set(variables(sbmlm)) == Set(variables(sbmlm2)) - @test Set(variables(sbmlm)) == Set(variables(sm)) - @test Set(metabolites(sbmlm)) == Set(metabolites(sbmlm2)) - sp(x) = x.species - @test all([ - issetequal( - sp.(sbmlm.sbml.reactions[i].reactants), - sp.(sbmlm2.sbml.reactions[i].reactants), - ) && issetequal( - sp.(sbmlm.sbml.reactions[i].products), - sp.(sbmlm2.sbml.reactions[i].products), - ) for i in variables(sbmlm2) - ]) - st(x) = isnothing(x.stoichiometry) ? 1.0 : x.stoichiometry - @test all([ - issetequal( - st.(sbmlm.sbml.reactions[i].reactants), - st.(sbmlm2.sbml.reactions[i].reactants), - ) && issetequal( - st.(sbmlm.sbml.reactions[i].products), - st.(sbmlm2.sbml.reactions[i].products), - ) for i in variables(sbmlm2) - ]) -end - -@testset "SBMLModel generic interface" begin - model = load_model(model_paths["e_coli_core.xml"]) - - @test reaction_stoichiometry(model, "R_EX_ac_e") == Dict("M_ac_e" => -1) -end diff --git a/test/types/abstract/AbstractMetabolicModel.jl b/test/types/abstract/AbstractMetabolicModel.jl deleted file mode 100644 index 714e9494d..000000000 --- a/test/types/abstract/AbstractMetabolicModel.jl +++ /dev/null @@ -1,12 +0,0 @@ - -struct FakeModel <: AbstractMetabolicModel - dummy::Int -end - -@testset "Base abstract model methods require proper minimal implementation" begin - @test_throws MethodError variables(123) - x = FakeModel(123) - for m in [variables, metabolites, stoichiometry, bounds, objective] - @test_throws MethodError m(x) - end -end diff --git a/test/types/misc/gene_associations.jl b/test/types/misc/gene_associations.jl deleted file mode 100644 index 99d280216..000000000 --- a/test/types/misc/gene_associations.jl +++ /dev/null @@ -1,17 +0,0 @@ - -@testset "GRR parsing" begin - @test sort( - sort.( - COBREXA.Types.Internal.parse_grr( - "(αλφα OR βητα\x03) AND (R2-D2's_gene OR prefix:su[ff]ix)", - ) - ), - ) == [ - ["R2-D2's_gene", "αλφα"], - ["R2-D2's_gene", "βητα\x03"], - ["prefix:su[ff]ix", "αλφα"], - ["prefix:su[ff]ix", "βητα\x03"], - ] - @test_throws DomainError COBREXA.Types.Internal.parse_grr("(") - @test isnothing(COBREXA.Types.Internal.parse_grr(" ")) -end diff --git a/test/utils/MatrixModel.jl b/test/utils/MatrixModel.jl deleted file mode 100644 index d808700c1..000000000 --- a/test/utils/MatrixModel.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "MatrixModel utilities" begin - cp = test_LP() - @test n_variables(cp) == 3 - @test n_metabolites(cp) == 4 - @test n_coupling_constraints(cp) == 0 - - cp2 = test_LP() - @test isequal(cp, cp2) - cp2.S[1] = 1 - @test !isequal(cp, cp2) - @test isequal(cp, copy(cp)) - - cp = test_coupledLP() - @test n_coupling_constraints(cp) == 2000 - @test isequal(cp, copy(cp)) -end diff --git a/test/utils/ObjectModel.jl b/test/utils/ObjectModel.jl deleted file mode 100644 index 7a3c73666..000000000 --- a/test/utils/ObjectModel.jl +++ /dev/null @@ -1,25 +0,0 @@ -@testset "ObjectModel utilities" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # FBA - fluxes = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], - ) |> values_dict - - # bounds setting # TODO why is this here? - cbm = make_optimization_model(model, Tulip.Optimizer) - ubs = cbm[:ubs] - lbs = cbm[:lbs] - glucose_index = first(indexin(["EX_glc__D_e"], variables(model))) - o2_index = first(indexin(["EX_o2_e"], variables(model))) - atpm_index = first(indexin(["ATPM"], variables(model))) - set_optmodel_bound!(glucose_index, cbm; upper_bound = -1.0, lower_bound = -1.0) - @test normalized_rhs(ubs[glucose_index]) == -1.0 - @test normalized_rhs(lbs[glucose_index]) == 1.0 - set_optmodel_bound!(o2_index, cbm; upper_bound = 1.0, lower_bound = 1.0) - @test normalized_rhs(ubs[o2_index]) == 1.0 - @test normalized_rhs(lbs[o2_index]) == -1.0 -end diff --git a/test/utils/Serialized.jl b/test/utils/Serialized.jl deleted file mode 100644 index 32d325362..000000000 --- a/test/utils/Serialized.jl +++ /dev/null @@ -1,25 +0,0 @@ - -@testset "Serialized models" begin - m = test_simpleLP() - - sm = serialize_model(m, tmpfile("toy1.serialized")) - sm2 = serialize_model(sm, tmpfile("toy2.serialized")) - - @test typeof(sm) == Serialized{MatrixModel} # expected type - @test typeof(sm2) == Serialized{MatrixModel} # no multi-layer serialization - - precache!(sm) - - @test isequal(m, sm.m) # the data is kept okay - @test sm2.m == nothing # nothing is cached here - @test isequal(m, deserialize(tmpfile("toy2.serialized"))) # it was written as-is - @test issetequal( - variables(convert(ObjectModel, sm)), - variables(convert(ObjectModel, sm2)), - ) - sm.m = nothing - @test issetequal( - metabolites(convert(MatrixModelWithCoupling, sm)), - metabolites(convert(MatrixModelWithCoupling, sm2)), - ) -end diff --git a/test/utils/fluxes.jl b/test/utils/fluxes.jl deleted file mode 100644 index effc4e82d..000000000 --- a/test/utils/fluxes.jl +++ /dev/null @@ -1,24 +0,0 @@ -@testset "Flux utilities" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - fluxes = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], - ) |> values_dict - - consuming, producing = metabolite_fluxes(model, fluxes) - @test isapprox(consuming["atp_c"]["PFK"], -7.47738; atol = TEST_TOLERANCE) - @test isapprox(producing["atp_c"]["PYK"], 1.75818; atol = TEST_TOLERANCE) - - # remove the biomass production from the fluxes, so that there's some atom - # disbalance that can be measured - delete!(fluxes, "BIOMASS_Ecoli_core_w_GAM") - - # atom tracker - atom_fluxes_out = atom_fluxes(model, fluxes) - @test isapprox(atom_fluxes_out["C"], 37.190166489763214; atol = TEST_TOLERANCE) - @test isapprox(atom_fluxes_out["O"], 41.663071522672226; atol = TEST_TOLERANCE) - @test isapprox(atom_fluxes_out["N"], 4.765319167566247; atol = TEST_TOLERANCE) -end diff --git a/test/utils/looks_like.jl b/test/utils/looks_like.jl deleted file mode 100644 index 9b6f0996c..000000000 --- a/test/utils/looks_like.jl +++ /dev/null @@ -1,93 +0,0 @@ -@testset "Looks like in MatrixModel, detailed test" begin - cp = test_LP() - @test isempty(filter(looks_like_exchange_reaction, variables(cp))) - - cp = test_simpleLP() - @test isempty(filter(looks_like_exchange_reaction, variables(cp))) - - cp = MatrixModel( - [-1.0 -1 -2; 0 -1 0; 0 0 0], - zeros(3), - ones(3), - ones(3), - ones(3), - ["EX_m1"; "r2"; "r3"], - ["m1"; "m2"; "m3"], - ) - @test filter(looks_like_exchange_reaction, variables(cp)) == ["EX_m1"] - - cp = MatrixModel( - [-1.0 0 0; 0 0 -1; 0 -1 0], - zeros(3), - ones(3), - ones(3), - ones(3), - ["EX_m1"; "Exch_m3"; "Ex_m2"], - ["m1"; "m2"; "m3_e"], - ) - @test filter(looks_like_exchange_reaction, variables(cp)) == - ["EX_m1", "Exch_m3", "Ex_m2"] - @test filter( - x -> looks_like_exchange_reaction(x; exchange_prefixes = ["Exch_"]), - variables(cp), - ) == ["Exch_m3"] - - # this is originally the "toyModel1.mat" - cp = test_toyModel() - - @test filter(looks_like_exchange_reaction, variables(cp)) == - ["EX_m1(e)", "EX_m3(e)", "EX_biomass(e)"] - @test filter( - x -> looks_like_exchange_reaction(x; exclude_biomass = true), - variables(cp), - ) == ["EX_m1(e)", "EX_m3(e)"] - @test filter(looks_like_extracellular_metabolite, metabolites(cp)) == ["m1[e]", "m3[e]"] - @test filter(looks_like_biomass_reaction, variables(cp)) == - ["EX_biomass(e)", "biomass1"] - @test filter( - x -> looks_like_biomass_reaction(x; exclude_exchanges = true), - variables(cp), - ) == ["biomass1"] -end - -@testset "Looks like functions, basic" begin - model = load_model(model_paths["e_coli_core.json"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = load_model(model_paths["e_coli_core.xml"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = load_model(model_paths["e_coli_core.mat"]) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = convert(ObjectModel, model) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 - - model = convert(MatrixModelWithCoupling, model) - @test length(filter(looks_like_exchange_reaction, variables(model))) == 20 - @test length(filter(looks_like_biomass_reaction, variables(model))) == 1 - @test length(filter(looks_like_extracellular_metabolite, metabolites(model))) == 20 -end - -@testset "Ontology usage in is_xxx_reaction" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # macro generated, so only test positive and negative case - @test !is_sbo_biomass_reaction(model, "PFL") - @test is_sbo_biomass_reaction(model, "BIOMASS_Ecoli_core_w_GAM") - - @test is_sbo_reaction(model, "PFL") - @test !is_sbo_reaction(model, "atp_c") - @test is_sbo_gene(model, "b2464") - @test !is_sbo_gene(model, "atp_c") - @test is_sbo_metabolite(model, "atp_c") - @test !is_sbo_metabolite(model, "PFL") -end diff --git a/test/utils/reaction.jl b/test/utils/reaction.jl deleted file mode 100644 index 8a49715a6..000000000 --- a/test/utils/reaction.jl +++ /dev/null @@ -1,48 +0,0 @@ -@testset "Reaction utilities" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - # FBA - fluxes = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_objective("BIOMASS_Ecoli_core_w_GAM")], - ) |> values_dict - - # test if reaction is balanced - @test reaction_mass_balanced(model, "PFL") - @test !reaction_mass_balanced(model, "BIOMASS_Ecoli_core_w_GAM") - @test reaction_mass_balanced(model, model.reactions["PFL"]) - @test !reaction_mass_balanced(model, model.reactions["BIOMASS_Ecoli_core_w_GAM"]) - @test reaction_mass_balanced(model, Dict("h_c" => -1.0, "h_e" => 1.0)) - @test !reaction_mass_balanced(model, Dict("h_c" => -1.0, "h2o_c" => 1.0)) - - # test if a reaction is a boundary reaction - @test !is_boundary(model.reactions["PFL"]) - @test is_boundary(model.reactions["EX_glc__D_e"]) - @test !is_boundary(model, "PFL") - @test is_boundary(model, "EX_glc__D_e") - @test !is_boundary(model, model.reactions["PFL"]) - @test is_boundary(model, model.reactions["EX_glc__D_e"]) - - # single-reaction atom balance - @test reaction_atom_balance(model, "FBA")["C"] == 0.0 - @test isapprox( - reaction_atom_balance(model, "BIOMASS_Ecoli_core_w_GAM")["C"], - -42.5555; - atol = TEST_TOLERANCE, - ) - @test reaction_atom_balance(model, model.reactions["FBA"])["C"] == 0.0 - @test isapprox( - reaction_atom_balance(model, model.reactions["BIOMASS_Ecoli_core_w_GAM"])["C"], - -42.5555; - atol = TEST_TOLERANCE, - ) - @test reaction_atom_balance(model, Dict("h_c" => -1.0, "h2o_c" => 1.0))["H"] == 1.0 - - # test if reaction equation can be built back into a sensible reaction string - req = Dict("coa_c" => -1, "for_c" => 1, "accoa_c" => 1, "pyr_c" => -1) - rstr_out = stoichiometry_string(req) - @test occursin("coa_c", split(rstr_out, " = ")[1]) - @test occursin("for", split(rstr_out, " = ")[2]) -end From 77ad5193561d95db431adc933a36d5c633e80c8b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 9 Nov 2023 17:55:06 +0100 Subject: [PATCH 341/531] get minimal structure --- src/COBREXA.jl | 10 +- src/analysis/envelopes.jl | 129 --------- src/analysis/minimize_metabolic_adjustment.jl | 46 --- src/analysis/sampling/affine_hit_and_run.jl | 141 --------- src/analysis/sampling/warmup_variability.jl | 102 ------- src/analysis/screening.jl | 270 ------------------ src/analysis/variability_analysis.jl | 226 --------------- src/builders/enzymes.jl | 14 - src/solver.jl | 24 +- test/analysis/envelopes.jl | 40 --- test/analysis/loopless.jl | 17 -- test/analysis/max_min_driving_force.jl | 52 ---- .../analysis/minimize_metabolic_adjustment.jl | 15 - test/analysis/sampling/affine_hit_and_run.jl | 29 -- test/analysis/sampling/warmup_variability.jl | 16 -- test/analysis/screening.jl | 63 ---- test/analysis/variability_analysis.jl | 112 -------- test/data_static.jl | 138 ++++----- 18 files changed, 83 insertions(+), 1361 deletions(-) delete mode 100644 src/analysis/envelopes.jl delete mode 100644 src/analysis/minimize_metabolic_adjustment.jl delete mode 100644 src/analysis/sampling/affine_hit_and_run.jl delete mode 100644 src/analysis/sampling/warmup_variability.jl delete mode 100644 src/analysis/screening.jl delete mode 100644 src/analysis/variability_analysis.jl delete mode 100644 src/builders/enzymes.jl delete mode 100644 test/analysis/envelopes.jl delete mode 100644 test/analysis/loopless.jl delete mode 100644 test/analysis/max_min_driving_force.jl delete mode 100644 test/analysis/minimize_metabolic_adjustment.jl delete mode 100644 test/analysis/sampling/affine_hit_and_run.jl delete mode 100644 test/analysis/sampling/warmup_variability.jl delete mode 100644 test/analysis/screening.jl delete mode 100644 test/analysis/variability_analysis.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 10e64ee0f..26dc474fd 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -10,8 +10,16 @@ import JuMP as J include("types.jl") include("solver.jl") + include("builders/core.jl") +include("builders/genes.jl") include("builders/objectives.jl") -include("utils/downloads.jl") + +include("analysis/flux_balance_analysis.jl") +include("analysis/parsimonious_flux_balance_analysis.jl") + +include("analysis/modifications/generic.jl") +include("analysis/modifications/knockout.jl") +include("analysis/modifications/optimizer.jl") end # module COBREXA diff --git a/src/analysis/envelopes.jl b/src/analysis/envelopes.jl deleted file mode 100644 index 5606ae3a9..000000000 --- a/src/analysis/envelopes.jl +++ /dev/null @@ -1,129 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Version of [`envelope_lattice`](@ref) that works on string reaction IDs instead -of integer indexes. -""" -envelope_lattice(model::AbstractMetabolicModel, rids::Vector{String}; kwargs...) = - envelope_lattice(model, Vector{Int}(indexin(rids, variables(model))); kwargs...) - -""" -$(TYPEDSIGNATURES) - -Create a lattice (list of "tick" vectors) for reactions at indexes `ridxs` in a -model. Arguments `samples`, `ranges`, and `reaction_samples` may be optionally -specified to customize the lattice creation process. -""" -envelope_lattice( - model::AbstractMetabolicModel, - ridxs::Vector{Int}; - samples = 10, - ranges = collect(zip(bounds(model)...))[ridxs], - reaction_samples = fill(samples, length(ridxs)), -) = ( - lb .+ (ub - lb) .* ((1:s) .- 1) ./ max(s - 1, 1) for - (s, (lb, ub)) in zip(reaction_samples, ranges) -) - -""" -$(TYPEDSIGNATURES) - -Version of [`objective_envelope`](@ref) that works on string reaction IDs -instead of integer indexes. -""" -objective_envelope( - model::AbstractMetabolicModel, - rids::Vector{String}, - args...; - kwargs..., -) = objective_envelope( - model, - Vector{Int}(indexin(rids, variables(model))), - args...; - kwargs..., -) - -""" -$(TYPEDSIGNATURES) - -Compute an array of objective values for the `model` for rates of reactions -specified `ridxs` fixed to a regular range of values between their respective -lower and upper bounds. - -This can be used to compute a "production envelope" of a metabolite; but -generalizes to any specifiable objective and to multiple dimensions of the -examined space. For example, to retrieve a production envelope of any -metabolite, set the objective coefficient vector of the `model` to a vector -that contains a single `1` for the exchange reaction that "outputs" this -metabolite, and run [`objective_envelope`](@ref) with the exchange reaction of -the "parameter" metabolite specified in `ridxs`. - -Returns a named tuple that contains `lattice` with reference values of the -metabolites, and an N-dimensional array `values` with the computed objective -values, where N is the number of specified reactions. Because of the -increasing dimensionality, the computation gets very voluminous with increasing -length of `ridxs`. The `lattice` for computing the optima can be supplied in -the argument; by default it is created by [`envelope_lattice`](@ref) called on -the model and reaction indexes. Additional arguments for the call to -[`envelope_lattice`](@ref) can be optionally specified in `lattice_args`. - -`kwargs` are internally forwarded to [`screen_optmodel_modifications`](@ref). -`modifications` are appended to the list of modifications after the lattice -bounds are set. By default, this returns the objective values for all points in -the lattice; alternate outputs can be implemented via the `analysis` argument. - -# Example -``` -julia> m = load_model("e_coli_core.xml"); - -julia> envelope = objective_envelope(m, ["R_EX_gln__L_e", "R_EX_fum_e"], - Tulip.Optimizer; - lattice_args=(samples=6,)); - -julia> envelope.lattice # the reaction rates for which the optima were computed -2-element Vector{Vector{Float64}}: - [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0] - [0.0, 200.0, 400.0, 600.0, 800.0, 1000.0] - -julia> envelope.values # the computed flux objective values for each reaction rate combination -6×6 Matrix{Float64}: - 0.873922 9.25815 17.4538 19.56 20.4121 20.4121 - 13.0354 17.508 19.9369 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 - 16.6666 18.6097 20.2847 21.894 22.6825 22.6825 -``` -""" -objective_envelope( - model::AbstractMetabolicModel, - ridxs::Vector{Int}, - optimizer; - modifications = [], - lattice_args = (), - lattice = envelope_lattice(model, ridxs; lattice_args...), - analysis = screen_optimize_objective, - kwargs..., -) = ( - lattice = collect.(lattice), - values = screen_optmodel_modifications( - model, - optimizer; - modifications = collect( - vcat( - [ - (_, optmodel) -> begin - for (i, ridx) in enumerate(ridxs) - set_normalized_rhs(optmodel[:lbs][ridx], -fluxes[i]) - set_normalized_rhs(optmodel[:ubs][ridx], fluxes[i]) - end - end, - ], - modifications, - ) for fluxes in Iterators.product(lattice...) - ), - analysis = analysis, - kwargs..., - ), -) diff --git a/src/analysis/minimize_metabolic_adjustment.jl b/src/analysis/minimize_metabolic_adjustment.jl deleted file mode 100644 index 52aa16ab6..000000000 --- a/src/analysis/minimize_metabolic_adjustment.jl +++ /dev/null @@ -1,46 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run minimization of metabolic adjustment (MOMA) on `model` with respect to -`reference_flux`, which is a vector of fluxes in the order of -`variables(model)`. MOMA finds the shortest Euclidian distance between -`reference_flux` and `model` with `modifications`: -``` -min Σᵢ (xᵢ - flux_refᵢ)² -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -Because the problem has a quadratic objective, a QP solver is required. See -"Daniel, Vitkup & Church, Analysis of Optimality in Natural and Perturbed -Metabolic Networks, Proceedings of the National Academy of Sciences, 2002" for -more details. - -Additional arguments are passed to [`flux_balance_analysis`](@ref). -See [`minimize_solution_distance`](@ref) for implementation details. - -Returns an optimized model that contains the feasible flux nearest to the -reference. - -# Example -``` -model = load_model("e_coli_core.json") -reference_flux = flux_balance_analysis(model, Gurobi.Optimizer) |> value_vec -optmodel = minimize_metabolic_adjustment( - model, - reference_flux, - Gurobi.Optimizer; - modifications = [modify_constraint("PFL"; lower_bound=0, upper_bound=0)], # find flux of mutant that is closest to the wild type (reference) model - ) -value.(solution[:x]) # extract the flux from the optimizer -``` -""" -minimize_metabolic_adjustment_analysis( - model::AbstractMetabolicModel, - reference_flux::Union{Vector{Float64}}, - optimizer; - kwargs..., -) = flux_balance_analysis( - model |> minimize_solution_distance(reference_flux), - optimizer; - kwargs..., -) diff --git a/src/analysis/sampling/affine_hit_and_run.jl b/src/analysis/sampling/affine_hit_and_run.jl deleted file mode 100644 index 072ff0d09..000000000 --- a/src/analysis/sampling/affine_hit_and_run.jl +++ /dev/null @@ -1,141 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run a hit-and-run style sampling that starts from `warmup_points` and uses -their affine combinations for generating the run directions to sample the space -delimited by `lbs` and `ubs`. The reaction rate vectors in `warmup_points` -should be organized in columns, i.e. `warmup_points[:,1]` is the first set of -reaction rates. - -There are total `chains` of hit-and-run runs, each on a batch of -`size(warmup_points, 2)` points. The runs are scheduled on `workers`, for good -load balancing `chains` should be ideally much greater than `length(workers)`. - -Each run continues for `maximum(sample_iters)` iterations; the numbers in -`sample_iters` represent the iterations at which the whole "current" batch of -points is collected for output. For example, `sample_iters=[1,4,5]` causes the -process run for 5 iterations, returning the sample batch that was produced by -1st, 4th and last (5th) iteration. - -Returns a matrix of sampled reaction rates (in columns), with all collected -samples horizontally concatenated. The total number of samples (columns) will -be `size(warmup_points,2) * chains * length(sample_iters)`. - -# Example -``` -warmup_points = warmup_from_variability(model, GLPK.Optimizer) -samples = affine_hit_and_run(model, warmup_points, sample_iters = 101:105) - -# convert the result to flux (for models where the distinction matters): -fluxes = reaction_variables_matrix(model)' * samples -``` -""" -function affine_hit_and_run( - m::AbstractMetabolicModel, - warmup_points::Matrix{Float64}; - sample_iters = 100 .* (1:5), - workers = [myid()], - chains = length(workers), - seed = rand(Int), -) - @assert size(warmup_points, 1) == n_variables(m) - - lbs, ubs = bounds(m) - C = coupling(m) - cl, cu = coupling_bounds(m) - if isnothing(C) - C = zeros(0, n_variables(m)) - cl = zeros(0) - cu = zeros(0) - end - save_at.(workers, :cobrexa_hit_and_run_data, Ref((warmup_points, lbs, ubs, C, cl, cu))) - - # sample all chains - samples = hcat( - pmap( - chain -> _affine_hit_and_run_chain( - (@remote cobrexa_hit_and_run_data)..., - sample_iters, - seed + chain, - ), - CachingPool(workers), - 1:chains, - )..., - ) - - # remove warmup points from workers - map(fetch, remove_from.(workers, :cobrexa_hit_and_run_data)) - - return samples -end - -""" -$(TYPEDSIGNATURES) - -Internal helper function for computing a single affine hit-and-run chain. -""" -function _affine_hit_and_run_chain(warmup, lbs, ubs, C, cl, cu, iters, seed) - - rng = StableRNG(seed % UInt) - points = copy(warmup) - d, n_points = size(points) - n_couplings = size(C, 1) - result = Matrix{Float64}(undef, d, n_points * length(iters)) - - # helper for reducing the available run range - function update_range(range, pos, dir, lb, ub) - dl = lb - pos - du = ub - pos - lower, upper = - dir < -constants.tolerance ? (du, dl) ./ dir : - dir > constants.tolerance ? (dl, du) ./ dir : (-Inf, Inf) - return (max(range[1], lower), min(range[2], upper)) - end - - iter = 0 - - for (iter_idx, iter_target) in enumerate(iters) - - while iter < iter_target - iter += 1 - - new_points = copy(points) - - for i = 1:n_points - - mix = rand(rng, n_points) .+ constants.tolerance - dir = points * (mix ./ sum(mix)) - points[:, i] - - # iteratively collect the maximum and minimum possible multiple - # of `dir` added to the current point - run_range = (-Inf, Inf) - for j = 1:d - run_range = - update_range(run_range, points[j, i], dir[j], lbs[j], ubs[j]) - end - - # do the same for coupling - dc = C * dir - pc = C * points[:, i] - for j = 1:n_couplings - run_range = update_range(run_range, pc[j], dc[j], cl[j], cu[j]) - end - - # generate a point in the viable run range and update it - lambda = run_range[1] + rand(rng) * (run_range[2] - run_range[1]) - isfinite(lambda) || continue # avoid divergence - new_points[:, i] = points[:, i] .+ lambda .* dir - - # TODO normally, here we would check if sum(S*new_point) is still - # lower than the tolerance, but we shall trust the computer - # instead. - end - - points = new_points - end - - result[:, n_points*(iter_idx-1)+1:iter_idx*n_points] .= points - end - - result -end diff --git a/src/analysis/sampling/warmup_variability.jl b/src/analysis/sampling/warmup_variability.jl deleted file mode 100644 index 9c6b8c3f0..000000000 --- a/src/analysis/sampling/warmup_variability.jl +++ /dev/null @@ -1,102 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Generates FVA-like warmup points for samplers, by selecting random points by -minimizing and maximizing reactions. Can not return more than 2 times the -number of reactions in the model. -""" -function warmup_from_variability( - model::AbstractMetabolicModel, - optimizer, - n_points::Int, - seed = rand(Int); - kwargs..., -) - nr = n_variables(model) - - n_points > 2 * nr && throw( - DomainError( - n_points, - "Variability method can not generate more than $(2*nr) points from this model", - ), - ) - - sample = shuffle(StableRNG(seed % UInt), vcat(1:nr, -(1:nr)))[begin:n_points] - warmup_from_variability( - model, - optimizer, - -filter(x -> x < 0, sample), - filter(x -> x > 0, sample); - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Generate FVA-like warmup points for samplers, by minimizing and maximizing the -specified reactions. The result is returned as a matrix, each point occupies as -single column in the result. - -!!! warning Limited effect of modifications in `warmup_from_variability` - Modifications of the optimization model applied in `modifications` - parameter that change the semantics of the model have an effect on the - warmup points, but do not automatically carry to the subsequent sampling. - Users are expected to manually transplant any semantic changes to the - actual sampling functions, such as [`affine_hit_and_run`](@ref). -""" -function warmup_from_variability( - model::AbstractMetabolicModel, - optimizer, - min_reactions::AbstractVector{Int} = 1:n_variables(model), - max_reactions::AbstractVector{Int} = 1:n_variables(model); - modifications = [], - workers::Vector{Int} = [myid()], -)::Matrix{Float64} - - # create optimization problem at workers, apply modifications - save_model = :( - begin - local model = $model - local optmodel = $make_optimization_model(model, $optimizer) - for mod in $modifications - mod(model, optmodel) - end - optmodel - end - ) - - asyncmap(fetch, save_at.(workers, :cobrexa_sampling_warmup_optmodel, Ref(save_model))) - - fluxes = hcat( - dpmap( - rid -> :($_maximize_warmup_reaction( - cobrexa_sampling_warmup_optmodel, - $rid, - om -> $JuMP.value.(om[:x]), - )), - CachingPool(workers), - vcat(-min_reactions, max_reactions), - )..., - ) - - # free the data on workers - asyncmap(fetch, remove_from.(workers, :cobrexa_sampling_warmup_optmodel)) - - return fluxes -end - -""" -$(TYPEDSIGNATURES) - -A helper function for finding warmup points from reaction variability. -""" -function _maximize_warmup_reaction(opt_model, rid, ret) - sense = rid > 0 ? MAX_SENSE : MIN_SENSE - var = all_variables(opt_model)[abs(rid)] - - @objective(opt_model, sense, var) - optimize!(opt_model) - - is_solved(opt_model) ? ret(opt_model) : nothing -end diff --git a/src/analysis/screening.jl b/src/analysis/screening.jl deleted file mode 100644 index ce0f4fa47..000000000 --- a/src/analysis/screening.jl +++ /dev/null @@ -1,270 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Internal helper to check the presence and shape of modification and argument -arrays in [`screen`](@ref) and pals. -""" -function _screen_args(argtuple, kwargtuple, modsname) - - mods = get(kwargtuple, modsname, nothing) - args = get(kwargtuple, :args, nothing) - - if isnothing(mods) - if isnothing(args) - throw( - DomainError(args, "at least one of `$modsname` and `args` must be defined"), - ) - end - return NamedTuple{(modsname,)}(Ref([[] for _ in args])) - elseif isnothing(args) - return (args = [() for _ in mods],) - else - if size(mods) != size(args) - throw( - DomainError( - "$(size(mods)) != $(size(args))", - "sizes of `$modsname` and `args` differ", - ), - ) - end - return () - end -end - -""" -$(TYPEDSIGNATURES) - -Take an array of model-modifying function vectors in `variants`, and execute -the function `analysis` on all variants of the `model` specified by `variants`. -The computation is distributed over worker IDs in `workers`. If `args` are -supplied (as an array of the same size as the `variants`), they are forwarded -as arguments to the corresponding analysis function calls. - -The array of variants must contain vectors of single-parameter functions, these -are applied to model in order. The functions must *not* modify the model, but -rather return a modified copy. The copy should be made as shallow as possible, -to increase memory efficiency of the process. Variant generators that modify -the argument model in-place will cause unpredictable results. Refer to the -definition of [`screen_variant`](@ref) for details. - -The function `analysis` will receive a single argument (the modified model), -together with arguments from `args` expanded by `...`. Supply an array of -tuples or vectors to pass in multiple arguments to each function. If the -argument values should be left intact (not expanded to multiple arguments), -they must be wrapped in single-item tuple or vector. - -The modification and analysis functions are transferred to `workers` as-is; all -packages required to run them (e.g. the optimization solvers) must be loaded -there. Typically, you want to use the macro `@everywhere using -MyFavoriteSolver` from `Distributed` package for loading the solvers. - -# Return value - -The results of running `analysis` are collected in to the resulting array, in a -way that preserves the shape of the `variants`, similarly as with `pmap`. - -The results of `analysis` function must be serializable, preferably made only -from pure Julia structures, because they may be transferred over the network -between the computation nodes. For that reason, functions that return whole -JuMP models that contain pointers to allocated C structures (such as -[`flux_balance_analysis`](@ref) used with `GLPK` or `Gurobi` otimizers) will -generally not work in this context. - -Note: this function is a thin argument-handling wrapper around -[`_screen_impl`](@ref). - -# Example -``` -function reverse_reaction(i::Int) - (model::MatrixModel) -> begin - mod = copy(model) - mod.S[:,i] .*= -1 # this is unrealistic but sufficient for demonstration - mod - end -end - -m = load_model(MatrixModel, "e_coli_core.xml") - -screen(m, - variants = [ - [reverse_reaction(5)], - [reverse_reaction(3), reverse_reaction(6)] - ], - analysis = mod -> mod.S[:,3]) # observe the changes in S - -screen(m, - variants = [ - [reverse_reaction(5)], - [reverse_reaction(3), reverse_reaction(6)] - ], - analysis = mod -> flux_balance_analysis(mod, GLPK.Optimizer) |> value_vec) -``` -""" -screen(args...; kwargs...) = - _screen_impl(args...; kwargs..., _screen_args(args, kwargs, :variants)...) - -""" -$(TYPEDSIGNATURES) - -The actual implementation of [`screen`](@ref). -""" -function _screen_impl( - model::AbstractMetabolicModel; - variants::Array{V,N}, - analysis, - args::Array{A,N}, - workers = [myid()], -)::Array where {V<:AbstractVector,A,N} - - asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_model, Ref(model))) - asyncmap(fetch, save_at.(workers, :cobrexa_screen_variants_analysis_fn, Ref(analysis))) - asyncmap(fetch, get_from.(workers, Ref(:($(precache!)(cobrexa_screen_variants_model))))) - - res = pmap( - (vars, args)::Tuple -> screen_variant( - (@remote cobrexa_screen_variants_model), - vars, - (@remote cobrexa_screen_variants_analysis_fn), - args, - ), - CachingPool(workers), - zip(variants, args), - ) - - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_variants_model)) - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_variants_analysis_fn)) - - return res -end - -""" -$(TYPEDSIGNATURES) - -Helper function for [`screen`](@ref) that applies all single-argument -functions in `variant` to the `model` (in order from "first" to -"last"), and executes `analysis` on the result. - -Can be used to test model variants locally. -""" -function screen_variant(model::AbstractMetabolicModel, variant::Vector, analysis, args = ()) - for fn in variant - model = fn(model) - end - analysis(model, args...) -end - -""" -$(TYPEDSIGNATURES) - -A shortcut for [`screen`](@ref) that only works with model variants. -""" -screen_variants(model, variants, analysis; workers = [myid()]) = - screen(model; variants = variants, analysis = analysis, workers = workers) - -""" -$(TYPEDSIGNATURES) - -A variant of [`optimize_objective`](@ref) directly usable in -[`screen_optmodel_modifications`](@ref). -""" -screen_optimize_objective(_, optmodel)::Maybe{Float64} = optimize_objective(optmodel) - -""" -$(TYPEDSIGNATURES) - -Internal helper for [`screen_optmodel_modifications`](@ref) that creates the -model and applies the modifications. -""" -function _screen_optmodel_prepare(model, optimizer, common_modifications) - precache!(model) - optmodel = make_optimization_model(model, optimizer) - for mod in common_modifications - mod(model, optmodel) - end - return (model, optmodel) -end - -""" -$(TYPEDSIGNATURES) - -Internal helper for [`screen_optmodel_modifications`](@ref) that computes one -item of the screening task. -""" -function _screen_optmodel_item((mods, args)) - (model, optmodel) = @remote cobrexa_screen_optmodel_modifications_data - for mod in mods - mod(model, optmodel) - end - (@remote cobrexa_screen_optmodel_modifications_fn)(model, optmodel, args...) -end - -""" -$(TYPEDSIGNATURES) - -Screen multiple modifications of the same optimization model. - -This function is potentially more efficient than [`screen`](@ref) because it -avoids making variants of the model structure and remaking of the optimization -model. On the other hand, modification functions need to keep the optimization -model in a recoverable state (one that leaves the model usable for the next -modification), which limits the possible spectrum of modifications applied. - -Internally, `model` is distributed to `workers` and transformed into the -optimization model using [`make_optimization_model`](@ref). -`common_modifications` are applied to the models at that point. Next, vectors -of functions in `modifications` are consecutively applied, and the result of -`analysis` function called on model are collected to an array of the same -extent as `modifications`. Calls of `analysis` are optionally supplied with -extra arguments from `args` expanded with `...`, just like in [`screen`](@ref). - -Both the modification functions (in vectors) and the analysis function here -have 2 base parameters (as opposed to 1 with [`screen`](@ref)): first is the -`model` (carried through as-is), second is the prepared JuMP optimization model -that may be modified and acted upon. As an example, you can use modification -[`modify_constraint`](@ref) and analysis [`screen_optimize_objective`](@ref). - -Note: This function is a thin argument-handling wrapper around -[`_screen_optmodel_modifications_impl`](@ref). -""" -screen_optmodel_modifications(args...; kwargs...) = _screen_optmodel_modifications_impl( - args...; - kwargs..., - _screen_args(args, kwargs, :modifications)..., -) - -""" -$(TYPEDSIGNATURES) - -The actual implementation of [`screen_optmodel_modifications`](@ref). -""" -function _screen_optmodel_modifications_impl( - model::AbstractMetabolicModel, - optimizer; - common_modifications::VF = [], - modifications::Array{V,N}, - args::Array{A,N}, - analysis::Function, - workers = [myid()], -)::Array where {V<:AbstractVector,VF<:AbstractVector,A,N} - - asyncmap( - fetch, - save_at.( - workers, - :cobrexa_screen_optmodel_modifications_data, - Ref(:($_screen_optmodel_prepare($model, $optimizer, $common_modifications))), - ), - ) - asyncmap( - fetch, - save_at.(workers, :cobrexa_screen_optmodel_modifications_fn, Ref(analysis)), - ) - - res = pmap(_screen_optmodel_item, CachingPool(workers), zip(modifications, args)) - - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_data)) - asyncmap(fetch, remove_from.(workers, :cobrexa_screen_optmodel_modifications_fn)) - - return res -end diff --git a/src/analysis/variability_analysis.jl b/src/analysis/variability_analysis.jl deleted file mode 100644 index 820c601cf..000000000 --- a/src/analysis/variability_analysis.jl +++ /dev/null @@ -1,226 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Flux variability analysis solves a pair of optimization problems in `model` for -each reaction flux `f` described by [`reactions`](@ref): -``` -min,max fᵀxᵢ -s.t. S x = b - xₗ ≤ x ≤ xᵤ - cᵀx ≥ bounds(Z₀)[1] - cᵀx ≤ bounds(Z₀)[2] -``` -where Z₀:= cᵀx₀ is the objective value of an optimal solution of the associated -FBA problem (see [`flux_balance_analysis`](@ref) for a related analysis, also -for explanation of the `modifications` argument). - -This is the simplest overload of the function which forwards the arguments to -more complicated ones; see documentation of [`variability_analysis`](@ref) for -details. Arguments `reaction_ids` and `reaction_indexes` may be used to -restrict the analysis to a specified subset of reactions. - -# Example -``` -model = load_model("e_coli_core.json") -flux_variability_analysis(model, GLPK.optimizer) -``` -""" -function flux_variability_analysis( - model::AbstractMetabolicModel, - optimizer; - reaction_ids::Maybe{Vector{String}} = nothing, - reaction_indexes::Maybe{Vector{Int}} = nothing, - kwargs..., -) - variability_analysis( - :reaction, - model, - optimizer; - ids = reaction_ids, - indexes = reaction_indexes, - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A variability analysis over a selected semantics, picking up only objects -specified by IDs or indexes from the selected semantics. For semantics -`:reaction`, this is equivalent to [`flux_variability_analysis`](@ref). -""" -function variability_analysis( - semantics::Symbol, - model::AbstractMetabolicModel, - optimizer; - ids::Maybe{Vector{String}} = nothing, - indexes::Maybe{Vector{Int}} = nothing, - kwargs..., -) - (sem_ids, n_ids, _, sem_varmtx) = Accessors.Internal.semantics(semantics) - - if isnothing(indexes) - idxs = if isnothing(ids) - collect(1:n_ids(model)) - else - indexin(ids, sem_ids(model)) - end - any(isnothing.(idxs)) && - throw(DomainError(ids[isnothing.(idxs)], "Unknown IDs specified")) - indexes = Int.(idxs) - end - - if any((indexes .< 1) .| (indexes .> n_ids(model))) - throw(DomainError(indexes, "Index out of range")) - end - - variability_analysis( - model, - optimizer; - directions = sem_varmtx(model)[:, indexes], - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -A generic variability analysis function that maximizes and minimizes the flux -in the directions defined by colums of matrix `directions`. - -Argument `modifications` is applied to the model as in -[`flux_balance_analysis`](@ref). - -The `bounds` argument is a user-supplied function that specifies the objective -bounds for the variability optimizations as a tuple of minimum and maximum. By -default, it restricts the flux objective value to be greater or equal to the -optimum reached in flux balance analysis. It can return `-Inf` or `Inf` in the -first or second field to completely ignore the limit. Use -[`gamma_bounds`](@ref) and [`objective_bounds`](@ref) for simple bounds. - -`optimizer` must be set to a `JuMP`-compatible optimizer. The computation of -the individual optimization problems is transparently distributed to `workers` -(see `Distributed.workers()`). The value of optimal flux can be optionally -supplied in argument `optimal_objective_value`, which prevents this function -from calling the non-parallelizable flux balance analysis. Separating the -single-threaded FBA and multithreaded variability computation can be used to -improve resource allocation efficiency in many common use-cases. - -`ret` is a function used to extract results from optimized JuMP models of the -individual fluxes. By default, it calls and returns the value of -`JuMP.objective_value`. More information can be extracted e.g. by setting it to -a function that returns a more elaborate data structure; such as `m -> -(JuMP.objective_value(m), JuMP.value.(m[:x]))`. - -Returns a matrix of extracted `ret` values for minima and maxima, of total size -(`size(directions,2)`,2). The optimizer result status is checked with -[`is_solved`](@ref); `nothing` is returned if the optimization failed for any -reason. -""" -function variability_analysis( - model::AbstractMetabolicModel, - optimizer; - directions::SparseMat = spdiagm(fill(1.0, n_variables(model))), - modifications = [], - workers = [myid()], - optimal_objective_value = nothing, - bounds = z -> (z, Inf), - ret = objective_value, -) - if size(directions, 1) != n_variables(model) - throw( - DomainError( - size(directions, 1), - "Directions matrix size is not compatible with model variable count.", - ), - ) - end - - if isnothing(optimal_objective_value) - optimal_objective_value = - flux_balance_analysis(model, optimizer; modifications = modifications) |> - solved_objective_value - end - isnothing(optimal_objective_value) && error("model has no feasible solution for FVA") - Z = bounds(optimal_objective_value) - - flux_vector = [directions[:, i] for i = 1:size(directions, 2)] - - ModelWithResult( - model, - screen_optmodel_modifications( - model, - optimizer; - common_modifications = vcat( - modifications, - [ - (model, opt_model) -> begin - Z[1] > -Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] >= Z[1] - ) - Z[2] < Inf && @constraint( - opt_model, - objective(model)' * opt_model[:x] <= Z[2] - ) - end, - ], - ), - args = tuple.([flux_vector flux_vector], [MIN_SENSE MAX_SENSE]), - analysis = (_, opt_model, flux, sense) -> - _max_variability_flux(opt_model, flux, sense, ret), - workers = workers, - ), - ) -end - -""" -$(TYPEDSIGNATURES) - -A variant of [`flux_variability_analysis`](@ref) that returns the individual -maximized and minimized fluxes as two dictionaries of dictionaries. All -keyword arguments except `ret` are passed through. - -# Example -``` -mins, maxs = flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - modify_optimizer_attribute("IPM_IterationsLimit", 500), - modify_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), - modify_constraint("EX_o2_e"; lower_bound = 0, upper_bound = 0), - ], -) -``` -""" -function flux_variability_analysis_dict(model::AbstractMetabolicModel, optimizer; kwargs...) - # TODO generalize this (requires smart analysis results) - res = flux_variability_analysis( - model, - optimizer; - kwargs..., - ret = sol -> values_vec(:reaction, ModelWithResult(model, sol)), - ) - flxs = reactions(res.model) - dicts = zip.(Ref(flxs), res.result) - - ModelWithResult( - res.model, - (Dict(flxs .=> Dict.(dicts[:, 1])), Dict(flxs .=> Dict.(dicts[:, 2]))), - ) -end - -""" -$(TYPEDSIGNATURES) - -Internal helper for maximizing reactions in optimization model. -""" -function _max_variability_flux(opt_model, flux, sense, ret) - @objective(opt_model, sense, sum(flux .* opt_model[:x])) - optimize!(opt_model) - - # TODO should this get ModelWithResult ? - is_solved(opt_model) ? ret(opt_model) : nothing -end diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl deleted file mode 100644 index 6940ef791..000000000 --- a/src/builders/enzymes.jl +++ /dev/null @@ -1,14 +0,0 @@ - -# the design space is: -# - total enzyme consumption y/n? (can be done very easily manually given the individual enzyme masses are present) -# - isozymes y/n (separates smoment from gecko) -# - mass groups y/n (this is basically summing up enzymes/isozymes by a label) -# - allow overproduction of enzymes (i.e., have extra variables for enzymes/isozymes to allow some slack) -# - anyone is probably able to do a partial sum over the above things themselves, we should make sure they squash well - -#TODO: this does not need the variables allocated, it's just a fancy product -enzyme_mass_sum(; forward_fluxes, reverse_fluxes, enzymes, reaction_enzyme_association) = - missing - -isozyme_mass_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing -isozyme_mass_group_mapping(; forward_fluxes, reverse_fluxes, isozymes, ...) = missing diff --git a/src/solver.jl b/src/solver.jl index 0efabfeb3..e9702a5f2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -6,16 +6,16 @@ Construct a JuMP `Model` that describes the precise constraint system into the JuMP `Model` created for solving in `optimizer`, with a given optional `objective` and optimization `sense`. """ -function J.Model( - constraints::C.ConstraintTreeElem; +function make_jump_model( + cs::C.ConstraintTree; objective::Union{Nothing,C.LinearValue,C.QuadraticValue} = nothing, optimizer, sense = J.MAX_SENSE, ) - # TODO this might better have its own name to avoid type piracy. model = J.Model(optimizer) + J.@variable(model, x[1:C.var_count(cs)]) - JuMP.@objective(model, sense, C.substitute(objective, x)) + isnothing(objective) || J.@objective(model, sense, C.substitute(objective, x)) # constraints function add_constraint(c::C.Constraint) @@ -38,20 +38,6 @@ end """ $(TYPEDSIGNATURES) -Convenience re-export of `Model` from JuMP. -""" -const Model = J.Model - -""" -$(TYPEDSIGNATURES) - -Convenience re-export of `optimize!` from JuMP. -""" -const optimize! = J.optimize! - -""" -$(TYPEDSIGNATURES) - `true` if `opt_model` solved successfully (solution is optimal or locally optimal). `false` if any other termination status is reached. """ @@ -72,7 +58,7 @@ $(TYPEDSIGNATURES) The optimized variable assignment of a JuMP model, if solved. """ optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? J.value.(model[:x]) : nothing + is_solved(opt_model) ? J.value.(opt_model[:x]) : nothing """ $(TYPEDSIGNATURES) diff --git a/test/analysis/envelopes.jl b/test/analysis/envelopes.jl deleted file mode 100644 index 9a5d8d746..000000000 --- a/test/analysis/envelopes.jl +++ /dev/null @@ -1,40 +0,0 @@ - -@testset "Envelopes" begin - m = load_model(model_paths["e_coli_core.xml"]) - - rxns = [1, 2, 3] - - lat = collect.(envelope_lattice(m, rxns; samples = 3)) - @test lat == [[-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0], [-1000.0, 0.0, 1000.0]] - @test lat == collect.(envelope_lattice(m, variables(m)[rxns]; samples = 3)) - - vals = - objective_envelope( - m, - variables(m)[rxns], - Tulip.Optimizer; - lattice_args = (samples = 3, ranges = [(-5, 0), (-5, 0), (-5, 5)]), - workers = W, - ).values - - @test size(vals) == (3, 3, 3) - @test count(isnothing, vals) == 15 - @test isapprox( - filter(!isnothing, vals), - [ - 0.5833914451564178, - 0.5722033617617296, - 0.673404874265479, - 0.5610152783118744, - 0.6606736594947443, - 0.7593405739802911, - 0.7020501075121626, - 0.689318892439569, - 0.787985806919745, - 0.6765876776826879, - 0.7752545924613183, - 0.8739215069575214, - ], - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/loopless.jl b/test/analysis/loopless.jl deleted file mode 100644 index 55d8cdf99..000000000 --- a/test/analysis/loopless.jl +++ /dev/null @@ -1,17 +0,0 @@ -@testset "Loopless FBA" begin - - model = load_model(model_paths["e_coli_core.json"]) - - sol = - flux_balance_analysis( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) |> values_dict - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.8739215069684292, - atol = TEST_TOLERANCE, - ) -end diff --git a/test/analysis/max_min_driving_force.jl b/test/analysis/max_min_driving_force.jl deleted file mode 100644 index 1e087a262..000000000 --- a/test/analysis/max_min_driving_force.jl +++ /dev/null @@ -1,52 +0,0 @@ -@testset "Max-min driving force analysis" begin - - model = load_model(model_paths["e_coli_core.json"]) - - flux_solution = - flux_balance_analysis( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], - ) |> values_dict - - mmdfm = make_max_min_driving_force_model( - model; - reaction_standard_gibbs_free_energies, - flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - concentration_lb = 1e-6, - concentration_ub = 100e-3, - ignore_reaction_ids = ["H2Ot"], - ) - - x = flux_balance_analysis( - mmdfm, - Tulip.Optimizer; - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - # get mmdf - @test isapprox(x |> solved_objective_value, 1.7661155558545698, atol = TEST_TOLERANCE) - - # values_dict(:reaction, mmdfm, opt_model) # TODO throw missing semantics error - @test length(x |> values_dict(:metabolite_log_concentration)) == 72 - @test length(x |> values_dict(:gibbs_free_energy_reaction)) == 95 - - sols = - variability_analysis( - :gibbs_free_energy_reaction, - mmdfm, - Tulip.Optimizer; - bounds = gamma_bounds(0.9), - modifications = [modify_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) |> result - - pyk_idx = first(indexin(["ΔG PYK"], gibbs_free_energy_reactions(mmdfm))) - @test isapprox(sols[pyk_idx, 2], -1.5895040002691128; atol = TEST_TOLERANCE) -end diff --git a/test/analysis/minimize_metabolic_adjustment.jl b/test/analysis/minimize_metabolic_adjustment.jl deleted file mode 100644 index e064f939b..000000000 --- a/test/analysis/minimize_metabolic_adjustment.jl +++ /dev/null @@ -1,15 +0,0 @@ -@testset "MOMA" begin - model = test_toyModel() - - sol = [looks_like_biomass_reaction(rid) ? 0.5 : 0.0 for rid in variables(model)] - - moma = - minimize_metabolic_adjustment_analysis( - model, - sol, - Clarabel.Optimizer; - modifications = [silence], - ) |> values_dict(:reaction) - - @test isapprox(moma["biomass1"], 0.07692307692307691, atol = QP_TEST_TOLERANCE) -end diff --git a/test/analysis/sampling/affine_hit_and_run.jl b/test/analysis/sampling/affine_hit_and_run.jl deleted file mode 100644 index e31ec13a0..000000000 --- a/test/analysis/sampling/affine_hit_and_run.jl +++ /dev/null @@ -1,29 +0,0 @@ -@testset "Sampling Tests" begin - - model = load_model(model_paths["e_coli_core.json"]) - - cm = MatrixCoupling(model, zeros(1, n_variables(model)), [17.0], [19.0]) - - pfk, tala = indexin(["PFK", "TALA"], variables(cm)) - cm.C[:, [pfk, tala]] .= 1.0 - - warmup = warmup_from_variability(cm, Tulip.Optimizer; workers = W) - - samples = affine_hit_and_run( - cm, - warmup, - sample_iters = 10 * (1:3), - workers = W, - chains = length(W), - ) - - @test size(samples, 1) == size(warmup, 1) - @test size(samples, 2) == size(warmup, 2) * 3 * length(W) - - lbs, ubs = bounds(model) - @test all(samples .>= lbs) - @test all(samples .<= ubs) - @test all(cm.C * samples .>= cm.cl) - @test all(cm.C * samples .<= cm.cu) - @test all(stoichiometry(model) * samples .< TEST_TOLERANCE) -end diff --git a/test/analysis/sampling/warmup_variability.jl b/test/analysis/sampling/warmup_variability.jl deleted file mode 100644 index a329267ff..000000000 --- a/test/analysis/sampling/warmup_variability.jl +++ /dev/null @@ -1,16 +0,0 @@ -@testset "Warm up point generation" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - rid = "EX_glc__D_e" - pts = warmup_from_variability( - model |> with_changed_bound(rid; lower_bound = -2, upper_bound = 2), - Tulip.Optimizer, - 100; - workers = W, - ) - - idx = first(indexin([rid], variables(model))) - @test size(pts) == (95, 100) - @test all(pts[idx, :] .>= -2) - @test all(pts[idx, :] .<= 2) -end diff --git a/test/analysis/screening.jl b/test/analysis/screening.jl deleted file mode 100644 index 54f5a14c1..000000000 --- a/test/analysis/screening.jl +++ /dev/null @@ -1,63 +0,0 @@ - -@testset "Screening functions" begin - m = test_toyModel() - - # nothing to analyze - @test_throws DomainError screen(m; analysis = identity) - - # array dimensionalities must match - @test_throws DomainError screen( - m; - analysis = identity, - variants = [[], []], - args = [() ()], - ) - - # sizes must match - @test_throws DomainError screen( - m; - analysis = identity, - variants = [[], []], - args = [()], - ) - - # argument handling - @test screen(m, analysis = identity, variants = [[]]) == [m] - @test screen(m, analysis = identity, args = [()]) == [m] - @test screen(m, analysis = (a, b) -> b, args = [(1,), (2,)]) == [1, 2] - - # test modifying some reactions - quad_rxn(i) = (m::MatrixModel) -> begin - mm = copy(m) - mm.S = copy(m.S) - mm.S[:, i] .^= 2 - return mm - end - - @test screen_variants( - m, - [[quad_rxn(i)] for i = 1:3], - m -> - Analysis.flux_balance_analysis(m, Tulip.Optimizer) |> COBREXA.Solver.values_vec; - workers = W, - ) == [ - [250.0, -250.0, -1000.0, 250.0, 1000.0, 250.0, 250.0], - [500.0, 500.0, 1000.0, 500.0, -1000.0, 500.0, 500.0], - [500.0, 500.0, 1000.0, -500.0, 1000.0, 500.0, 500.0], - ] - - # test solver modifications - @test screen( - m; - analysis = (m, sense) -> - Analysis.flux_balance_analysis( - m, - Tulip.Optimizer; - modifications = [modify_sense(sense)], - ) |> COBREXA.Solver.values_vec, - args = [(MIN_SENSE,), (MAX_SENSE,)], - ) == [ - [-500.0, -500.0, -1000.0, 500.0, 1000.0, -500.0, -500.0], - [500.0, 500.0, 1000.0, -500.0, -1000.0, 500.0, 500.0], - ] -end diff --git a/test/analysis/variability_analysis.jl b/test/analysis/variability_analysis.jl deleted file mode 100644 index 758431df7..000000000 --- a/test/analysis/variability_analysis.jl +++ /dev/null @@ -1,112 +0,0 @@ -@testset "Flux variability analysis" begin - cp = test_simpleLP() - optimizer = Tulip.Optimizer - fluxes = flux_variability_analysis(cp, optimizer) |> result - - @test size(fluxes) == (2, 2) - @test fluxes ≈ [ - 1.0 1.0 - 2.0 2.0 - ] - - rates = variability_analysis(cp, optimizer) |> result - @test fluxes == rates - - fluxes = flux_variability_analysis(cp, optimizer, reaction_indexes = [2]) |> result - - @test size(fluxes) == (1, 2) - @test isapprox(fluxes, [2 2], atol = TEST_TOLERANCE) - - # a special testcase for slightly sub-optimal FVA (gamma<1) - cp = MatrixModel( - [-1.0 -1.0 -1.0], - [0.0], - [1.0, 0.0, 0.0], - [0.0, 0.0, -1.0], - 1.0 * ones(3), - ["r$x" for x = 1:3], - ["m1"], - ) - fluxes = flux_variability_analysis(cp, optimizer) |> result - @test isapprox( - fluxes, - [ - 1.0 1.0 - 0.0 0.0 - -1.0 -1.0 - ], - atol = TEST_TOLERANCE, - ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = gamma_bounds(0.5)) |> result - @test isapprox( - fluxes, - [ - 0.5 1.0 - 0.0 0.5 - -1.0 -0.5 - ], - atol = TEST_TOLERANCE, - ) - fluxes = flux_variability_analysis(cp, optimizer; bounds = _ -> (0, Inf)) |> result - @test isapprox( - fluxes, - [ - 0.0 1.0 - 0.0 1.0 - -1.0 0.0 - ], - atol = TEST_TOLERANCE, - ) - - @test isempty( - flux_variability_analysis(cp, Tulip.Optimizer, reaction_ids = String[]) |> result, - ) - @test_throws DomainError flux_variability_analysis( - cp, - Tulip.Optimizer, - reaction_indexes = [-1], - ) - @test_throws DomainError flux_variability_analysis( - cp, - Tulip.Optimizer, - reaction_ids = ["not a reaction!"], - ) -end - -@testset "Parallel FVA" begin - cp = test_simpleLP() - - fluxes = - flux_variability_analysis( - cp, - Tulip.Optimizer; - workers = W, - reaction_indexes = [1, 2], - ) |> result - @test isapprox( - fluxes, - [ - 1.0 1.0 - 2.0 2.0 - ], - atol = TEST_TOLERANCE, - ) -end - -@testset "Flux variability analysis with ObjectModel" begin - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - mins, maxs = - flux_variability_analysis_dict( - model, - Tulip.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - modify_optimizer_attribute("IPM_IterationsLimit", 500), - modify_constraint("EX_glc__D_e"; lower_bound = -10, upper_bound = -10), - modify_constraint("EX_o2_e"; lower_bound = 0.0, upper_bound = 0.0), - ], - ) |> result - - @test isapprox(maxs["EX_ac_e"]["EX_ac_e"], 8.5185494, atol = TEST_TOLERANCE) - @test isapprox(mins["EX_ac_e"]["EX_ac_e"], 7.4483887, atol = TEST_TOLERANCE) -end diff --git a/test/data_static.jl b/test/data_static.jl index b8e072fa3..1ad7ff343 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -1,77 +1,77 @@ -test_LP() = MatrixModel( - zeros(4, 3), - zeros(4), - ones(3), - ones(3), - ones(3), - ["r$x" for x = 1:3], - ["m$x" for x = 1:4], -) +# test_LP() = MatrixModel( +# zeros(4, 3), +# zeros(4), +# ones(3), +# ones(3), +# ones(3), +# ["r$x" for x = 1:3], +# ["m$x" for x = 1:4], +# ) -test_simpleLP() = MatrixModel( - [ - 1.0 1.0 - -1.0 1.0 - ], - [3.0, 1.0], - [-0.25, 1.0], - -ones(2), - 2.0 * ones(2), - ["r$x" for x = 1:2], - ["m$x" for x = 1:2], -) +# test_simpleLP() = MatrixModel( +# [ +# 1.0 1.0 +# -1.0 1.0 +# ], +# [3.0, 1.0], +# [-0.25, 1.0], +# -ones(2), +# 2.0 * ones(2), +# ["r$x" for x = 1:2], +# ["m$x" for x = 1:2], +# ) -test_simpleLP2() = MatrixModel( - zeros(2, 2), - [0.0, 0.0], - [-0.25, 1.0], - -ones(2), - 2.0 * ones(2), - ["r$x" for x = 1:2], - ["m$x" for x = 1:2], -) +# test_simpleLP2() = MatrixModel( +# zeros(2, 2), +# [0.0, 0.0], +# [-0.25, 1.0], +# -ones(2), +# 2.0 * ones(2), +# ["r$x" for x = 1:2], +# ["m$x" for x = 1:2], +# ) -test_sparseLP() = MatrixModel( - sprand(4000, 3000, 0.5), - sprand(4000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - ["r$x" for x = 1:3000], - ["m$x" for x = 1:4000], -) +# test_sparseLP() = MatrixModel( +# sprand(4000, 3000, 0.5), +# sprand(4000, 0.5), +# sprand(3000, 0.5), +# sprand(3000, 0.5), +# sprand(3000, 0.5), +# ["r$x" for x = 1:3000], +# ["m$x" for x = 1:4000], +# ) -test_coupledLP() = MatrixModelWithCoupling( - MatrixModel( - sprand(4000, 3000, 0.5), - sprand(4000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - sprand(3000, 0.5), - ["r$x" for x = 1:3000], - ["m$x" for x = 1:4000], - ), - sprand(2000, 3000, 0.5), - sprand(2000, 0.5), - sprand(2000, 0.5), -) +# test_coupledLP() = MatrixModelWithCoupling( +# MatrixModel( +# sprand(4000, 3000, 0.5), +# sprand(4000, 0.5), +# sprand(3000, 0.5), +# sprand(3000, 0.5), +# sprand(3000, 0.5), +# ["r$x" for x = 1:3000], +# ["m$x" for x = 1:4000], +# ), +# sprand(2000, 3000, 0.5), +# sprand(2000, 0.5), +# sprand(2000, 0.5), +# ) -test_toyModel() = MatrixModel( - [ - -1.0 1.0 0.0 0.0 0.0 0.0 0.0 - -2.0 0.0 1.0 0.0 0.0 0.0 0.0 - 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 - 0.0 -1.0 0.0 -1.0 0.0 0.0 0.0 - 0.0 0.0 -1.0 0.0 -1.0 0.0 0.0 - 0.0 0.0 0.0 0.0 0.0 -1.0 1.0 - ], - zeros(6), - [0, 0, 0, 0, 0, 0, 1.0], - fill(-1000.0, 7), - fill(1000.0, 7), - ["r1", "m1t", "m3t", "EX_m1(e)", "EX_m3(e)", "EX_biomass(e)", "biomass1"], - ["m1[c]", "m3[c]", "m2[c]", "m1[e]", "m3[e]", "biomass[c]"], -) +# test_toyModel() = MatrixModel( +# [ +# -1.0 1.0 0.0 0.0 0.0 0.0 0.0 +# -2.0 0.0 1.0 0.0 0.0 0.0 0.0 +# 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 +# 0.0 -1.0 0.0 -1.0 0.0 0.0 0.0 +# 0.0 0.0 -1.0 0.0 -1.0 0.0 0.0 +# 0.0 0.0 0.0 0.0 0.0 -1.0 1.0 +# ], +# zeros(6), +# [0, 0, 0, 0, 0, 0, 1.0], +# fill(-1000.0, 7), +# fill(1000.0, 7), +# ["r1", "m1t", "m3t", "EX_m1(e)", "EX_m3(e)", "EX_biomass(e)", "biomass1"], +# ["m1[c]", "m3[c]", "m2[c]", "m1[e]", "m3[e]", "biomass[c]"], +# ) const reaction_standard_gibbs_free_energies = Dict{String,Float64}( #= From ee299987bc6f59a3a2e44a7b0e8aa6be8edc5631 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 10:27:21 +0100 Subject: [PATCH 342/531] continue cleaning --- src/builders/core.jl | 19 ++- src/builders/genes.jl | 6 +- src/builders/objectives.jl | 21 +-- src/solver.jl | 7 +- test/analysis/flux_balance_analysis.jl | 125 --------------- test/analysis/knockouts.jl | 150 ------------------ .../parsimonious_flux_balance_analysis.jl | 30 ---- test/data_downloaded.jl | 44 ++--- test/data_static.jl | 74 --------- test/runtests.jl | 22 +-- 10 files changed, 47 insertions(+), 451 deletions(-) delete mode 100644 test/analysis/flux_balance_analysis.jl delete mode 100644 test/analysis/knockouts.jl delete mode 100644 test/analysis/parsimonious_flux_balance_analysis.jl diff --git a/src/builders/core.jl b/src/builders/core.jl index ddc3e9c50..b69928391 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -8,7 +8,7 @@ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function metabolic_model(model::F.AbstractFBCModel) +function fbc_model_structure(model::F.AbstractFBCModel) rxns = Symbol.(F.reactions(model)) mets = Symbol.(F.metabolites(model)) lbs, ubs = F.bounds(model) @@ -16,12 +16,15 @@ function metabolic_model(model::F.AbstractFBCModel) bal = F.balance(model) obj = F.objective(model) - return :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * - :balance^C.ConstraintTree( + #TODO: is sparse() required below? + return C.ConstraintTree( + :fluxes => C.variables(keys = rxns, bounds = zip(lbs, ubs)), + :balances => C.ConstraintTree( m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for (m, row, b) in zip(mets, eachrow(stoi), bals) - ) * - :objective^C.Constraint(value = C.Value(sparse(obj))) + ), + :objective => C.Constraint(value = C.Value(sparse(obj))), + ) end """ @@ -62,9 +65,9 @@ sign_split_constraints(; negative::C.ConstraintTree, signed::C.ConstraintTree, ) = C.ConstraintTree( - C.Constraint( - value = s + (haskey(negative, k) ? negative[k].value : zero(C.Value)) - - (haskey(positive, k) ? positive[k].value : zero(C.Value)), + k => C.Constraint( + value = s + (haskey(negative, k) ? C.value(negative[k]) : zero(C.Value)) - + (haskey(positive, k) ? C.value(positive[k]) : zero(C.Value)), bound = 0.0, ) for (k, s) in C.elems(signed) ) diff --git a/src/builders/genes.jl b/src/builders/genes.jl index d08d3455c..805b963cc 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,10 +1,8 @@ -#TODO maybe separate this into simple boolean eval function and actual builder -knockout_constraint(ko_genes; fluxes::SolutionTree, reaction_gene_association) = +knockout_constraints(ko_genes; fluxes::ConstraintTree, gene_products_available) = C.ConstraintTree( rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if begin - gss = reaction_gene_association(rxn) - if isnothing(gss) + if gene_products_available(rxn, k) false else all(gs -> any(g -> g in ko_genes, gs), gss) diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 34a53a8cf..2067f2ec1 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,19 +1,20 @@ -squared_error_objective(x) = - C.Constraint(sum((c.value * c.value for c in x), init = zero(C.Value))) -squared_error_objective(x::ConstraintTree) = squared_error_objective(values(x)) +sum_objectve(x) = + C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) + +sum_objective(x::ConstraintTree) = squared_error_objective(values(x)) -squared_error_objective(constraints::Vector, target::Vector) = - C.Constraint(sum(let tmp = (c.value - t) - tmp * tmp - end for (c, t) in zip(constraints, target))) +squared_sum_objective(x) = + C.Constraint(sum(C.squared.(C.value.(x)), init = zero(C.QuadraticValue))) + +squared_sum_objective(x::ConstraintTree) = squared_sum_objective(values(x)) squared_error_objective(constraints::ConstraintTree, target) = C.Constraint( sum( - let tmp = (c.value - target[k]) + let tmp = (C.value(c) - target[k]) tmp * tmp - end for (k, c) in C.elems(constraints) if haskey(target, k) + end for (k, c) in constraints if haskey(target, k) ), ) -# TODO use `mergewith` to do this reasonably +# TODO use `mergewith` to do this reasonably (add it to ConstraintTrees) diff --git a/src/solver.jl b/src/solver.jl index e9702a5f2..69b58b43c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -6,7 +6,7 @@ Construct a JuMP `Model` that describes the precise constraint system into the JuMP `Model` created for solving in `optimizer`, with a given optional `objective` and optimization `sense`. """ -function make_jump_model( +function make_optimization_model( cs::C.ConstraintTree; objective::Union{Nothing,C.LinearValue,C.QuadraticValue} = nothing, optimizer, @@ -63,9 +63,10 @@ optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = """ $(TYPEDSIGNATURES) -Convenience overload for making solution trees out of JuMP models +Annotate a `ConstraintTree` with the values given by the optimization model, +producing a `ValueTree` (if solved). """ -C.ValueTree(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = +solution(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = let vars = optimized_variable_assignment(opt_model) isnothing(vars) ? nothing : C.ValueTree(c, vars) end diff --git a/test/analysis/flux_balance_analysis.jl b/test/analysis/flux_balance_analysis.jl deleted file mode 100644 index 630db3877..000000000 --- a/test/analysis/flux_balance_analysis.jl +++ /dev/null @@ -1,125 +0,0 @@ -@testset "Flux balance analysis with MatrixModel" begin - cp = test_simpleLP() - lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test sol ≈ [1.0, 2.0] - - # test the maximization of the objective - cp = test_simpleLP2() - lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test sol ≈ [-1.0, 2.0] - - # test with a more biologically meaningfull model - cp = load_model(MatrixModel, model_paths["iJR904.mat"]) - expected_optimum = 0.9219480950504393 - - lp = flux_balance_analysis(cp, Tulip.Optimizer) |> result - @test termination_status(lp) == MOI.OPTIMAL - sol = JuMP.value.(lp[:x]) - @test isapprox(objective_value(lp), expected_optimum, atol = TEST_TOLERANCE) - @test isapprox(cp.c' * sol, expected_optimum, atol = TEST_TOLERANCE) - - # test the "nicer output" variants - fluxes_vec = flux_balance_analysis(cp, Tulip.Optimizer) |> values_vec - @test all(fluxes_vec .== sol) - fluxes_dict = flux_balance_analysis(cp, Tulip.Optimizer) |> values_dict - rxns = variables(cp) - @test all([fluxes_dict[rxns[i]] == sol[i] for i in eachindex(rxns)]) -end - -@testset "Flux balance analysis with ObjectModel" begin - - model = load_model(ObjectModel, model_paths["e_coli_core.json"]) - - sol = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [ - modify_objective("BIOMASS_Ecoli_core_w_GAM"), - modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - modify_sense(MAX_SENSE), - modify_optimizer_attribute("IPM_IterationsLimit", 110), - ], - ) |> values_dict - - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 1.0572509997013568, - atol = TEST_TOLERANCE, - ) - - pfl_frac = 0.8 - biomass_frac = 0.2 - sol_multi = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [ - modify_objective( - ["BIOMASS_Ecoli_core_w_GAM", "PFL"]; - weights = [biomass_frac, pfl_frac], - ), - ], - ) |> values_dict - - @test isapprox( - biomass_frac * sol_multi["BIOMASS_Ecoli_core_w_GAM"] + pfl_frac * sol_multi["PFL"], - 31.999999998962604, - atol = TEST_TOLERANCE, - ) - - @test_throws DomainError flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_constraint("gbbrsh"; lower_bound = -12, upper_bound = -12)], - ) |> values_dict - - @test_throws DomainError flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_objective("gbbrsh")], - ) |> values_dict - - @test_throws DomainError flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [modify_objective(["BIOMASS_Ecoli_core_w_GAM"; "gbbrsh"])], - ) -end - -@testset "Flux balance analysis with MatrixModelWithCoupling" begin - - model = load_model(MatrixModel, model_paths["e_coli_core.json"]) - - # assume coupling constraints of the form: - # -γ ≤ vᵢ/μ ≤ γ - # I.e., enforces that the ratio between any reaction flux - # and the growth rate is bounded by γ. - γ = 40 - - # construct coupling bounds - nr = n_variables(model) - biomass_index = first(indexin(["BIOMASS_Ecoli_core_w_GAM"], variables(model))) - - Cf = sparse(1.0I, nr, nr) - Cf[:, biomass_index] .= -γ - - Cb = sparse(1.0I, nr, nr) - Cb[:, biomass_index] .= γ - - C = [Cf; Cb] - - clb = spzeros(2 * nr) - clb[1:nr] .= -1000.0 - cub = spzeros(2 * nr) - cub[nr+1:end] .= 1000 - - cmodel = MatrixModelWithCoupling(model, C, clb, cub) # construct - - dc = flux_balance_analysis(cmodel, Tulip.Optimizer) |> values_dict - @test isapprox(dc["BIOMASS_Ecoli_core_w_GAM"], 0.665585699298256, atol = TEST_TOLERANCE) -end diff --git a/test/analysis/knockouts.jl b/test/analysis/knockouts.jl deleted file mode 100644 index c4bdd322a..000000000 --- a/test/analysis/knockouts.jl +++ /dev/null @@ -1,150 +0,0 @@ -@testset "single_knockout" begin - m = ObjectModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_reaction!( - m, - Reaction( - "v1", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1"])], - ), - ) - add_reaction!( - m, - Reaction( - "v2", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1", "g2"])], - ), - ) - add_reaction!( - m, - Reaction( - "v3", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1"]), Isozyme(["g2"])], - ), - ) - add_reaction!( - m, - Reaction( - "v4", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g2"])], - ), - ) - - opt_model = make_optimization_model(m, Tulip.Optimizer) - knockout("g1")(m, opt_model) - - # Knockout should remove v1 - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Knockout should remove [g1, g2] (AND) and thus remove reaction - @test normalized_rhs(opt_model[:lbs][2]) == 0 - @test normalized_rhs(opt_model[:ubs][2]) == 0 - - # Knockout should remove [g1], but keep reaction (OR) - @test normalized_rhs(opt_model[:lbs][3]) == 1000 - @test normalized_rhs(opt_model[:ubs][3]) == 1000 - - # Knockout should remove [g1, g2] (AND), but keep reaction (OR) - @test normalized_rhs(opt_model[:lbs][4]) == 1000 - @test normalized_rhs(opt_model[:ubs][4]) == 1000 -end - -@testset "multiple_knockouts" begin - m = ObjectModel() - add_metabolite!(m, Metabolite("A")) - add_metabolite!(m, Metabolite("B")) - add_gene!(m, Gene("g1")) - add_gene!(m, Gene("g2")) - add_gene!(m, Gene("g3")) - add_reaction!( - m, - Reaction( - "v1", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1"]), Isozyme(["g3"])], - ), - ) - add_reaction!( - m, - Reaction( - "v2", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])], - ), - ) - add_reaction!( - m, - Reaction( - "v3", - metabolites = Dict("A" => -1.0, "B" => 1.0), - gene_associations = [Isozyme(x) for x in [["g1"], ["g2"], ["g3"]]], - ), - ) - - opt_model = make_optimization_model(m, Tulip.Optimizer) - knockout(["g1", "g3"])(m, opt_model) - - # Reaction 1 should be knocked out, because both - # gene1 and gene 3 are knocked out - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Reaction 2 should be knocked out, because both - # [g1, g2] is an AND relationship - @test normalized_rhs(opt_model[:lbs][1]) == 0 - @test normalized_rhs(opt_model[:ubs][1]) == 0 - - # Reaction 3 should stay, because gene2 is still - # available (the arrays have an OR relationship) - @test normalized_rhs(opt_model[:lbs][3]) == 1000 - @test normalized_rhs(opt_model[:ubs][3]) == 1000 -end - -@testset "Knockouts on realistic models" begin - for model in [ - load_model(ObjectModel, model_paths["e_coli_core.json"]), #test on standardModel - load_model(model_paths["e_coli_core.json"]), #then on JSONModel with the same contents - ] - - sol = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [ - modify_objective("BIOMASS_Ecoli_core_w_GAM"), - modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - modify_sense(MAX_SENSE), - modify_optimizer_attribute("IPM_IterationsLimit", 110), - knockout(["b0978", "b0734"]), # knockouts out cytbd - ], - ) |> values_dict - @test isapprox( - sol["BIOMASS_Ecoli_core_w_GAM"], - 0.2725811189335953, - atol = TEST_TOLERANCE, - ) - - sol = - flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [ - modify_objective("BIOMASS_Ecoli_core_w_GAM"), - modify_constraint("EX_glc__D_e"; lower_bound = -12, upper_bound = -12), - modify_sense(MAX_SENSE), - modify_optimizer_attribute("IPM_IterationsLimit", 110), - knockout("b2779"), # knockouts out enolase - ], - ) |> values_dict - @test isapprox(sol["BIOMASS_Ecoli_core_w_GAM"], 0.0, atol = TEST_TOLERANCE) - end -end diff --git a/test/analysis/parsimonious_flux_balance_analysis.jl b/test/analysis/parsimonious_flux_balance_analysis.jl deleted file mode 100644 index 19e4fd5fa..000000000 --- a/test/analysis/parsimonious_flux_balance_analysis.jl +++ /dev/null @@ -1,30 +0,0 @@ -@testset "Parsimonious flux balance analysis with ObjectModel" begin - model = test_toyModel() - - d = - parsimonious_flux_balance_analysis( - model, - Tulip.Optimizer; - modifications = [ - modify_constraint("EX_m1(e)", lower_bound = -10.0), - modify_optimizer_attribute("IPM_IterationsLimit", 500), - ], - qp_modifications = [modify_optimizer(Clarabel.Optimizer), silence], - ) |> values_dict - - # The used optimizer doesn't really converge to the same answer everytime - # here, we therefore tolerate a wide range of results. - @test isapprox(d["biomass1"], 10.0, atol = QP_TEST_TOLERANCE) - - d2 = - model |> - with_changed_bound("biomass1", lower_bound = 10.0) |> - with_parsimonious_objective(:reaction) |> - flux_balance_analysis(Clarabel.Optimizer, modifications = [silence]) |> - values_dict - - @test all(isapprox(d[k], d2[k], atol = QP_TEST_TOLERANCE) for k in keys(d2)) - - Q = objective(model |> with_parsimonious_objective(:reaction)) - @test all(diag(Q) .== -0.5) -end diff --git a/test/data_downloaded.jl b/test/data_downloaded.jl index 66951f0cd..5660417d5 100644 --- a/test/data_downloaded.jl +++ b/test/data_downloaded.jl @@ -1,79 +1,61 @@ - -function check_data_file_hash(path, expected_checksum) - actual_checksum = bytes2hex(sha256(open(path))) - if actual_checksum != expected_checksum - @error "The downloaded data file `$path' seems to be different from the expected one. Tests will likely fail." actual_checksum expected_checksum - end -end - -function download_data_file(url, path, hash) - if isfile(path) - check_data_file_hash(path, hash) - @info "using cached `$path'" - return path - end - - Downloads.download(url, path) - check_data_file_hash(path, hash) - return path -end - +# TODO this should be downloaded by documentation scripts isdir("downloaded") || mkdir("downloaded") df(s) = joinpath("downloaded", s) +const dl = A.download_data_file model_paths = Dict{String,String}( - "iJO1366.json" => download_data_file( + "iJO1366.json" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.json", df("iJO1366.json"), "9376a93f62ad430719f23e612154dd94c67e0d7c9545ed9d17a4d0c347672313", ), - "iJO1366.mat" => download_data_file( + "iJO1366.mat" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.mat", df("iJO1366.mat"), "b5cfe21b6369a00e45d600b783f89521f5cc953e25ee52c5f1d0a3f83743be30", ), - "iJO1366.xml" => download_data_file( + "iJO1366.xml" => dl( "http://bigg.ucsd.edu/static/models/iJO1366.xml", df("iJO1366.xml"), "d6d9ec61ef6f155db5bb2f49549119dc13b96f6098b403ef82ea4240b27232eb", ), - "ecoli_core_model.xml" => download_data_file( + "ecoli_core_model.xml" => dl( "http://systemsbiology.ucsd.edu/sites/systemsbiology.ucsd.edu/files/Attachments/Images/downloads/Ecoli_core/ecoli_core_model.xml", df("ecoli_core_model.xml"), "78692f8509fb36534f4f9b6ade23b23552044f3ecd8b48d84d484636922ae907", ), - "e_coli_core.json" => download_data_file( + "e_coli_core.json" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.json", df("e_coli_core.json"), "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", ), - "e_coli_core.xml" => download_data_file( + "e_coli_core.xml" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.xml", df("e_coli_core.xml"), "b4db506aeed0e434c1f5f1fdd35feda0dfe5d82badcfda0e9d1342335ab31116", ), - "e_coli_core.mat" => download_data_file( + "e_coli_core.mat" => dl( "http://bigg.ucsd.edu/static/models/e_coli_core.mat", df("e_coli_core.mat"), "478e6fa047ede1b248975d7565208ac9363a44dd64aad1900b63127394f4175b", ), - "iJR904.mat" => download_data_file( + "iJR904.mat" => dl( "http://bigg.ucsd.edu/static/models/iJR904.mat", df("iJR904.mat"), "d17be86293d4caafc32b829da4e2d0d76eb45e1bb837e0138327043a83e20c6e", ), - "Recon3D.json" => download_data_file( + "Recon3D.json" => dl( "http://bigg.ucsd.edu/static/models/Recon3D.json", df("Recon3D.json"), "aba925f17547a42f9fdb4c1f685d89364cbf4979bbe7862e9f793af7169b26d5", ), - "yeast-GEM.mat" => download_data_file( + "yeast-GEM.mat" => dl( "https://github.com/SysBioChalmers/yeast-GEM/raw/v8.6.2/model/yeast-GEM.mat", df("yeast-GEM.mat"), "c2587e258501737e0141cd47e0f854a60a47faee2d4c6ad582a00e437676b181", ), - "yeast-GEM.xml" => download_data_file( + "yeast-GEM.xml" => dl( "https://github.com/SysBioChalmers/yeast-GEM/raw/v8.6.2/model/yeast-GEM.xml", df("yeast-GEM.xml"), "c728b09d849b744ec7640cbf15776d40fb2d9cbd0b76a840a8661b626c1bd4be", diff --git a/test/data_static.jl b/test/data_static.jl index 1ad7ff343..1800aadd2 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -1,77 +1,3 @@ -# test_LP() = MatrixModel( -# zeros(4, 3), -# zeros(4), -# ones(3), -# ones(3), -# ones(3), -# ["r$x" for x = 1:3], -# ["m$x" for x = 1:4], -# ) - -# test_simpleLP() = MatrixModel( -# [ -# 1.0 1.0 -# -1.0 1.0 -# ], -# [3.0, 1.0], -# [-0.25, 1.0], -# -ones(2), -# 2.0 * ones(2), -# ["r$x" for x = 1:2], -# ["m$x" for x = 1:2], -# ) - -# test_simpleLP2() = MatrixModel( -# zeros(2, 2), -# [0.0, 0.0], -# [-0.25, 1.0], -# -ones(2), -# 2.0 * ones(2), -# ["r$x" for x = 1:2], -# ["m$x" for x = 1:2], -# ) - -# test_sparseLP() = MatrixModel( -# sprand(4000, 3000, 0.5), -# sprand(4000, 0.5), -# sprand(3000, 0.5), -# sprand(3000, 0.5), -# sprand(3000, 0.5), -# ["r$x" for x = 1:3000], -# ["m$x" for x = 1:4000], -# ) - -# test_coupledLP() = MatrixModelWithCoupling( -# MatrixModel( -# sprand(4000, 3000, 0.5), -# sprand(4000, 0.5), -# sprand(3000, 0.5), -# sprand(3000, 0.5), -# sprand(3000, 0.5), -# ["r$x" for x = 1:3000], -# ["m$x" for x = 1:4000], -# ), -# sprand(2000, 3000, 0.5), -# sprand(2000, 0.5), -# sprand(2000, 0.5), -# ) - -# test_toyModel() = MatrixModel( -# [ -# -1.0 1.0 0.0 0.0 0.0 0.0 0.0 -# -2.0 0.0 1.0 0.0 0.0 0.0 0.0 -# 1.0 0.0 0.0 0.0 0.0 0.0 -1.0 -# 0.0 -1.0 0.0 -1.0 0.0 0.0 0.0 -# 0.0 0.0 -1.0 0.0 -1.0 0.0 0.0 -# 0.0 0.0 0.0 0.0 0.0 -1.0 1.0 -# ], -# zeros(6), -# [0, 0, 0, 0, 0, 0, 1.0], -# fill(-1000.0, 7), -# fill(1000.0, 7), -# ["r1", "m1t", "m3t", "EX_m1(e)", "EX_m3(e)", "EX_biomass(e)", "biomass1"], -# ["m1[c]", "m3[c]", "m2[c]", "m1[e]", "m3[e]", "biomass[c]"], -# ) const reaction_standard_gibbs_free_energies = Dict{String,Float64}( #= diff --git a/test/runtests.jl b/test/runtests.jl index 0aa9e89c7..076f05a41 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,14 +1,10 @@ using COBREXA, Test using Aqua + using Clarabel using Distributed -using Downloads +import AbstractFBCModels as A using GLPK # for MILPs -using LinearAlgebra -using Serialization -using SHA -using SparseArrays -using Statistics # tolerance for comparing analysis results (should be a bit bigger than the # error tolerance in computations) @@ -24,10 +20,8 @@ function run_test_file(path...) print_timing(fn, t) end -function run_test_dir(dir, comment = "Directory $dir/") - @testset "$comment" begin - run_test_file.(joinpath.(dir, filter(fn -> endswith(fn, ".jl"), readdir(dir)))) - end +function run_doc(path...) + run_test_file("..", "docs", "src", path...) end # set up the workers for Distributed, so that the tests that require more @@ -35,18 +29,14 @@ end W = addprocs(2) t = @elapsed @everywhere using COBREXA, Tulip, JuMP -# make sure there's a directory for temporary data -tmpdir = "tmpfiles" -isdir(tmpdir) || mkdir(tmpdir) -tmpfile(x...) = joinpath(tmpdir, x...) - # load the test models run_test_file("data_static.jl") run_test_file("data_downloaded.jl") # import base files @testset "COBREXA test suite" begin - run_test_dir("analysis") + run_doc("loading-and-saving.jl") + run_doc("flux-balance-analysis.jl") run_test_file("aqua.jl") end From 066bd2fb4fd41a4073fa559ffb90252ae16e0f16 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 10:50:51 +0100 Subject: [PATCH 343/531] more cleanup, proper exports --- .gitignore | 36 +-- docs/src/concepts.md | 7 - docs/src/concepts/1_screen.md | 268 ------------------ docs/src/concepts/2_modifications.md | 70 ----- docs/src/concepts/3_custom_models.md | 150 ---------- docs/src/concepts/4_wrappers.md | 156 ---------- docs/src/examples.md | 7 - docs/src/examples/01_loading.jl | 73 ----- docs/src/examples/02_convert_save.jl | 91 ------ docs/src/examples/03_exploring.jl | 51 ---- docs/src/examples/03b_accessors.jl | 54 ---- docs/src/examples/04_core_model.jl | 53 ---- docs/src/examples/04_standardmodel.jl | 140 --------- .../04b_standardmodel_construction.jl | 80 ------ docs/src/examples/05a_fba.jl | 53 ---- docs/src/examples/05b_fba_mods.jl | 38 --- docs/src/examples/06_fva.jl | 89 ------ docs/src/examples/07_gene_deletion.jl | 113 -------- docs/src/examples/07_restricting_reactions.jl | 149 ---------- docs/src/examples/08_pfba.jl | 74 ----- docs/src/examples/09_loopless.jl | 45 --- docs/src/examples/10_crowding.jl | 15 - docs/src/examples/11_growth.jl | 134 --------- docs/src/examples/12_mmdf.jl | 196 ------------- docs/src/examples/13_moma.jl | 54 ---- .../14_simplified_enzyme_constrained.jl | 112 -------- docs/src/examples/15_enzyme_constrained.jl | 106 ------- docs/src/examples/16_hit_and_run.jl | 73 ----- docs/src/examples/17_envelopes.jl | 83 ------ docs/src/functions.md | 47 ++- docs/src/functions/analysis.md | 22 -- docs/src/functions/base.md | 6 - docs/src/functions/io.md | 15 - docs/src/functions/reconstruction.md | 15 - docs/src/functions/types.md | 20 -- docs/src/functions/utils.md | 22 -- docs/src/index.md | 7 + docs/src/index.md.template | 83 ------ docs/src/quickstart.md | 152 ---------- docs/src/quickstart.md.template | 96 ------- src/COBREXA.jl | 1 + src/builders/core.jl | 8 +- src/builders/genes.jl | 23 +- src/io.jl | 35 +++ src/solver.jl | 12 +- 45 files changed, 126 insertions(+), 3008 deletions(-) delete mode 100644 docs/src/concepts.md delete mode 100644 docs/src/concepts/1_screen.md delete mode 100644 docs/src/concepts/2_modifications.md delete mode 100644 docs/src/concepts/3_custom_models.md delete mode 100644 docs/src/concepts/4_wrappers.md delete mode 100644 docs/src/examples.md delete mode 100644 docs/src/examples/01_loading.jl delete mode 100644 docs/src/examples/02_convert_save.jl delete mode 100644 docs/src/examples/03_exploring.jl delete mode 100644 docs/src/examples/03b_accessors.jl delete mode 100644 docs/src/examples/04_core_model.jl delete mode 100644 docs/src/examples/04_standardmodel.jl delete mode 100644 docs/src/examples/04b_standardmodel_construction.jl delete mode 100644 docs/src/examples/05a_fba.jl delete mode 100644 docs/src/examples/05b_fba_mods.jl delete mode 100644 docs/src/examples/06_fva.jl delete mode 100644 docs/src/examples/07_gene_deletion.jl delete mode 100644 docs/src/examples/07_restricting_reactions.jl delete mode 100644 docs/src/examples/08_pfba.jl delete mode 100644 docs/src/examples/09_loopless.jl delete mode 100644 docs/src/examples/10_crowding.jl delete mode 100644 docs/src/examples/11_growth.jl delete mode 100644 docs/src/examples/12_mmdf.jl delete mode 100644 docs/src/examples/13_moma.jl delete mode 100644 docs/src/examples/14_simplified_enzyme_constrained.jl delete mode 100644 docs/src/examples/15_enzyme_constrained.jl delete mode 100644 docs/src/examples/16_hit_and_run.jl delete mode 100644 docs/src/examples/17_envelopes.jl delete mode 100644 docs/src/functions/analysis.md delete mode 100644 docs/src/functions/base.md delete mode 100644 docs/src/functions/io.md delete mode 100644 docs/src/functions/reconstruction.md delete mode 100644 docs/src/functions/types.md delete mode 100644 docs/src/functions/utils.md create mode 100644 docs/src/index.md delete mode 100644 docs/src/index.md.template delete mode 100644 docs/src/quickstart.md delete mode 100644 docs/src/quickstart.md.template create mode 100644 src/io.jl diff --git a/.gitignore b/.gitignore index 076160902..46e71d415 100644 --- a/.gitignore +++ b/.gitignore @@ -1,46 +1,22 @@ -#ignore data files -test/downloaded/ -test/tmpfiles/ -test/data -.DS_Store -# ignore VScode clutter +# ignore various editor clutter +.DS_Store /.vscode /vscode *.code-workspace +.*.swp -# Build artifacts for creating documentation generated by the Documenter package +# ignore autogenerated docs docs/build/ -docs/site/ # ignore file types -*.mat -*.xml -*.json -*.h5 *.jl.*.cov -.*.swp -# File generated by Pkg, the package manager, based on a corresponding Project.toml -# It records a fixed state of all packages used by the project. As such, it should not be -# committed for packages, but should be committed for applications that require a static -# environment. +# Pkg.jl stuff Manifest.toml -# Ignore temporary files for testing functions -temp.* - -# Ignore jupyter notebook stuff +# Ignore any jupyter notebook stuff .ipynb_checkpoints -# add generated tutorial specifics -docs/src/examples/* -!docs/src/examples/*.jl - -# add generated docs and tutorial specifics -docs/src/index.md -docs/src/howToContribute.md -docs/src/assets/output.gif - # ignore container files *.sif diff --git a/docs/src/concepts.md b/docs/src/concepts.md deleted file mode 100644 index f2279cd36..000000000 --- a/docs/src/concepts.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Core concepts and extension guide - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("concepts", join=true)) -Depth = 2 -``` diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md deleted file mode 100644 index a00204092..000000000 --- a/docs/src/concepts/1_screen.md +++ /dev/null @@ -1,268 +0,0 @@ - -# Screening many model variants - -A major goal of COBREXA.jl is to make exploring of many model variants easy and -fast. - -One main concept that can be utilized for doing that is implemented in the -function [`screen`](@ref), which takes your model, a list of model _variants_ -that you want to explore by some specified _analysis_, and schedules the -analysis of the model variants parallelly on the available distributed workers. - -In its most basic form, the "screening" may use the slightly simplified variant -of [`screen`](@ref) that is called [`screen_variants`](@ref), which works as -follows: - -```julia -m = load_model(ObjectModel, "e_coli_core.json") - -screen_variants( - m, # the model for screening - [ - [], # a variant with no modifications - [with_changed_bound("CO2t", lb = 0, ub = 0)], # disable CO2 transport - [with_changed_bound("O2t", lb = 0, ub = 0)], # disable O2 transport - [with_changed_bound("CO2t", lb = 0, ub = 0), with_changed_bound("O2t", lb = 0, ub = 0)], # disable both transports - ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], -) -``` -The call specifies a model (the `m` that we have loaded) that is being tested, -then a vector of model variants to be created and tested, and then the analysis -that is being run on each variant -- in this case, we find an optimal steady -state of each of the variants, and check out the biomass production rate at -that state. In this particular case, we are checking what will be the effect of -disabling combinations of CO2 transport and O2 transport in the cells. For -that, we get the following result: -``` -4-element Vector{Float64}: - 0.8739215022678488 - 0.46166961413944896 - 0.21166294973372135 - 0.21114065173865518 -``` - -The numbers are the biomass production rates for the specified variants. We can -see that disabling O2 transport really does not help the organism much. - -## Variant specification - -In the above example, we have specified 4 variants, thus the analysis returned -4 different results that correspond with the specifications. Let us have a look -at the precise format of the specification and result. - -Importantly, the `variants` argument is of type `Array{Vector{Any}}`, meaning -that it can be an array of any dimensionality that contains vectors. Each of the -vectors specifies precisely one variant, possibly with more modifications -applied to the model in sequence. - -For example: -- `[]` specifies no modifications at all -- `[with_changed_bound("CO2t", lb=0, ub=10)]` limits the CO2 transport -- `[with_changed_bound("CO2t", lb=0, ub=2), with_changed_bound("O2t", lb=0, ub=100)]` - severely limits the CO2 transport _and_ slightly restricts the transport of - O2 - -!!! note "Variants are single-parameter model-transforming functions" - Because the variants are just generators of single parameter functions - that take the model and return its modified version, you can also use - `identity` to specify a variant that does nothing -- `[identity]` is - perfectly same as `[]` - -The shape of the variants array is important too, because it is precisely -retained in the result (just as with `pmap`). If you pass in a matrix of -variants, you will receive a matrix of analysis results of the same size. That -can be exploited for easily exploring many combinations of possible model -properties. Let's try exploring a "cube" of possible restricted reactions: - -```julia -using IterTools # for cartesian products - -res = screen_variants(m, - [ - # for each variant we restricts 2 reactions - [with_changed_bound(r1, lb=-3, ub=3), with_changed_bound(r2, lb=-1, ub=1)] - - # the reaction pair will be chosen from a cartesian product - for (r1,r2) in product( - ["H2Ot", "CO2t", "O2t", "NH4t"], # of this set of transport reactions - ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], # and this set of exchanges - ) - ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], -) -``` - -As a result, we will receive a full matrix of the biomass productions: -``` -4×4 Matrix{Float64}: - 0.407666 0.454097 0.240106 0.183392 - 0.407666 0.485204 0.24766 0.183392 - 0.314923 0.319654 0.24766 0.183392 - 0.407666 0.485204 0.24766 0.183392 -``` -Notably, this shows that O2 transport and NH4 exchange may be serious -bottlenecks for biomass production. - -For clarity, you may always annotate the result by zipping it with the -specification structure you have used and collecting the data: -``` julia -collect(zip( - product( - ["H2Ot", "CO2t", "O2t", "NH4t"], - ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], - ), - res, -)) -``` -...which gives the following annotated result: -``` -4×4 Matrix{Tuple{Tuple{String, String}, Float64}}: - (("H2Ot", "EX_h2o_e"), 0.407666) (("H2Ot", "EX_co2_e"), 0.454097) (("H2Ot", "EX_o2_e"), 0.240106) (("H2Ot", "EX_nh4_e"), 0.183392) - (("CO2t", "EX_h2o_e"), 0.407666) (("CO2t", "EX_co2_e"), 0.485204) (("CO2t", "EX_o2_e"), 0.24766) (("CO2t", "EX_nh4_e"), 0.183392) - (("O2t", "EX_h2o_e"), 0.314923) (("O2t", "EX_co2_e"), 0.319654) (("O2t", "EX_o2_e"), 0.24766) (("O2t", "EX_nh4_e"), 0.183392) - (("NH4t", "EX_h2o_e"), 0.407666) (("NH4t", "EX_co2_e"), 0.485204) (("NH4t", "EX_o2_e"), 0.24766) (("NH4t", "EX_nh4_e"), 0.183392) -``` - -This may be easily used for e.g. scrutinizing all possible reaction pairs, to -find the ones that are redundant and not. - -There are many other variant "specifications" to choose from. You may use -[`with_added_reactions`](@ref), [`with_removed_reactions`](@ref), -[`with_removed_metabolites`](@ref), and others. Function reference contains a -complete list; as a convention, names of the specifications all start with -`with_`. - -## Writing custom variant functions - -It is actually very easy to create custom specifications that do any -modification that you can implement, to be later used with -[`screen_variants`](@ref) and [`screen`](@ref). - -Generally, the "specifications" are supposed to return a _function_ that -creates a modified copy of the model. The copy of the model may be shallow, but -the functions should always prevent modifying the original model structure -- -`screen` is keeping a single copy of the original model at each worker to -prevent unnecessary bulk data transport, and if that is changed in-place, all -following analyses of the model will work on inconsistent data, usually -returning wrong results (even randomly changing ones, because of the -asynchronous nature of [`screen`](@ref) execution). - -Despite of that, writing a modification is easy. The simplest modification that -"does nothing" (isomorphic to standard `identity`) can be formatted as follows: - -```julia -with_no_change = model -> model -``` - -The modifications may change the model, provided it is copied properly. The -following modification will remove a reaction called "O2t", effectively -removing the possibility to transport oxygen. We require a specific type of -model where this change is easy to perform (generally, not all variants may be -feasible on all model types). - -```julia -with_disabled_oxygen_transport = (model::ObjectModel) -> begin - - # make "as shallow as possible" copy of the `model`. - # Utilizing `deepcopy` is also possible, but inefficient. - new_model = copy(model) - new_model.reactions = copy(model.reactions) - - # remove the O2 transport from the model copy - delete!(new_model.reactions, "O2t") - - return new_model #return the newly created variant -end -``` - -Finally, the whole definition may be parameterized as a normal function. The -following variant removes any user-selected reaction: - -```julia -with_disabled_reaction(reaction_id) = (model::ObjectModel) -> begin - new_model = copy(model) - new_model.reactions = copy(model.reactions) - delete!(new_model.reactions, reaction_id) # use the parameter from the specification - return new_model -end -``` - -In turn, these variants can be used in [`screen_variants`](@ref) just as we -used [`with_changed_bound`](@ref) above: - -```julia -screen_variants( - m, # the model for screening - [ - [with_no_change], - [with_disabled_oxygen_transport], - [with_disabled_reaction("NH4t")], - ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], -) -``` - -That should get you the results for all new variants of the model: -``` -3-element Vector{Float64}: - 0.8739215022674809 - 0.21166294865468896 - 1.2907224478973395e-15 -``` - -!!! warning "Custom variants with distributed processing" - If using distributed evaluation, remember the variant-generating functions - need to be defined on all used workers (generating the variants in parallel - on the workers allows COBREXA to run the screening process very - efficiently, without unnecessary sending of bulk model data). Prefixing the - definition with `@everywhere` is usually sufficient for that purpose. - -## Passing extra arguments to the analysis function - -Some analysis functions may take additional arguments, which you might want to -vary for the analysis. `modifications` argument of -[`flux_balance_analysis_dict`](@ref) is one example of such argument, allowing -you to specify details of the optimization procedure. - -[`screen`](@ref) function allows you to do precisely that -- apart from -`variants`, you may also specify an array of `args` of the same shape as -`variants`, the entries of which will get passed together with the generated -model variants to your specified analysis function. If either of the arguments -is missing (or set to `nothing`), it is defaulted to "no modifications" or "no -arguments". - -The arguments _must_ be tuples; you may need to make 1-tuples from your data -(e.g. using `(value,)`) if you want to pass just a single argument. - -Let's try to use that functionality for trying to find a sufficient amount of -iterations needed for Tulip solver to find a feasible solution: - -```julia -screen(m, - args = [(i,) for i in 5:15], # the iteration counts, packed in 1-tuples - analysis = (m,a) -> # `args` elements get passed as the extra parameter here - flux_balance_analysis_vec(m, - Tulip.Optimizer; - modifications=[modify_optimizer_attribute("IPM_IterationsLimit", a)], - ), -) -``` - -From the result, we can see that Tulip would need at least 14 iterations to -find a feasible region: - -``` -11-element Vector{Union{Nothing, Vector{Float64}}}: - nothing - nothing - nothing - nothing - nothing - nothing - nothing - nothing - nothing - [7.47738193404817, 1.8840414375838503e-8, 4.860861010127701, -16.023526104614593, … ] - [7.47738193404817, 1.8840414375838503e-8, 4.860861010127701, -16.023526104614593, … ] -``` diff --git a/docs/src/concepts/2_modifications.md b/docs/src/concepts/2_modifications.md deleted file mode 100644 index 6ab03090f..000000000 --- a/docs/src/concepts/2_modifications.md +++ /dev/null @@ -1,70 +0,0 @@ - -# Writing custom optimizer modifications - -Functions such as [`flux_balance_analysis`](@ref) internally create a JuMP -model out of the [`AbstractMetabolicModel`](@ref), and run the optimizer on that. To be -able to make some modifications on the JuMP model before the optimizer is -started, most of the functions accept a `modifications` argument, where one can -list callbacks that do the changes to the prepared optimization model. - -The callbacks available in COBREXA.jl include functions that may help with -tuning the optimizer, or change the raw values in the linear model, such as: - -- [`modify_constraint`](@ref) and [`change_objective`](@ref) -- [`modify_sense`](@ref), [`modify_optimizer`](@ref), [`modify_optimizer_attribute`](@ref) -- [`silence`](@ref) -- [`knockout`](@ref) -- [`add_loopless_constraints`](@ref) - -Compared to the [variant system](1_screen.md) and the [model -wrappers](4_wrappers.md), optimizer modifications are slightly more powerful -(they can do anything they want with the optimizer!), but do not compose well --- it is very easy to break the semantics of the model or erase the previous -changes by carelessly adding the modifications. - -Here, we show how to construct the modifications. Their semantics is similar to -the [variant-generating functions](1_screen.md), which receive a model (of type -[`AbstractMetabolicModel`](@ref)), and are expected to create another (modified) model. -Contrary to that, modifications receive both the [`AbstractMetabolicModel`](@ref) and a -JuMP model structure, and are expected to cause a side effect on the latter. - -A trivial modification that does not do anything can thus be written as: - -```julia -change_nothing() = (model, opt_model) -> println("Not touching anything.") -``` - -and applied as: -```julia -flux_balance_analysis(model, GLPK.Optimizer, modifications=[change_nothing()]) -flux_variability_analysis(model, GLPK.Optimizer, modifications=[change_nothing()]) -``` - -At the call time of the modifier function, `opt_model` is usually the model -that was returned from [`make_optimization_model`](@ref) -- refer to the -function for actual model layout. The function can freely change anything in -that model. - -For demonstration, we show how to implement an impractical but illustrative -modification that adds an extra constraint that makes sure that all fluxes sum -to a certain value: - -```julia -using JuMP - -add_sum_constraint(total::Float64) = - (model, opt_model) -> begin - v = opt_model[:x] # retrieve the variable vector - @constraint(opt_model, total, sum(v) == total) # create the constraint using JuMP macro - end -``` - -The modification can be used at the expectable position: -```julia -v = flux_balance_analysis_vec( - load_model("e_coli_core.xml"), - GLPK.Optimizer, - modifications = [add_sum_constraint(100.0)]) - -sum(v) # should print ~100.0 -``` diff --git a/docs/src/concepts/3_custom_models.md b/docs/src/concepts/3_custom_models.md deleted file mode 100644 index 418e788d3..000000000 --- a/docs/src/concepts/3_custom_models.md +++ /dev/null @@ -1,150 +0,0 @@ - -# Working with custom models - -It may happen that the intuitive representation of your data does not really -match what is supported by a given COBRA package. COBREXA.jl attempts to avoid -this problem by providing a flexible framework for containing any data -structure that can, somehow, represent the constraint-based model. - -The task of having such a polymorphic model definition can be split into 2 -separate concerns: - -- How to allow the analysis functions to gather the required information from - any user-specified model data structure? -- How to make the reconstruction functions (i.e., reaction or gene deletions) - work properly on any data structure? - -To solve the first concern, COBREXA.jl specifies a set of generic accessors -that work over the abstract type [`AbstractMetabolicModel`](@ref). To use your data -structure in a model, you just make it a subtype of [`AbstractMetabolicModel`](@ref) -and overload the required accessors. The accessors are functions that extract -some relevant information, such as [`stoichiometry`](@ref) and -[`bounds`](@ref), returning a fixed simple data type that can be further used -by COBREXA. You may see a complete list of accessors -[here](../functions.md#Base-Types). - -A good solution to the second concern is a slightly more involved, as writing -generic data modifiers is notoriously hard. Still, there is support for easily -making small changes to the model using the modifications system, with -functions such as [`with_added_reactions`](@ref) and -[`with_changed_bound`](@ref). - -## Writing the generic accessors - -Let's write a data structure that represents a very small model that contains N -metabolites that are converted in a circle through N linear, coupled reactions. -(E.g., for N=3, we would have a conversion of metabolites A, B and C ordered as -A → B → C → A.) This may be useful for testing purposes; we will use it for a -simple demonstration. - -The whole model can thus be specified with a single integer N that represents -the length of the reaction cycle: - -```julia -struct CircularModel <: AbstractMetabolicModel - size::Int -end -``` - -First, define the reactions and metabolites: - -```julia -COBREXA.n_reactions(m::CircularModel) = m.size -COBREXA.n_metabolites(m::CircularModel) = m.size - -COBREXA.reactions(m::CircularModel) = ["rxn$i" for i in 1:n_reactions(m)] -COBREXA.metabolites(m::CircularModel) = ["met$i" for i in 1:n_metabolites(m)] -``` - -It is useful to re-use the already defined functions, as that improves the code -maintainability. - -We can continue with the actual linear model properties: - -```julia -function COBREXA.objective(m::CircularModel) - c = spzeros(n_reactions(m)) - c[1] = 1 #optimize the first reaction - return c -end - -COBREXA.bounds(m::CircularModel) = ( - zeros(n_reactions(m)), # lower bounds - ones(n_reactions(m)), # upper bounds -) - -function COBREXA.stoichiometry(m::CircularModel) - nr = n_reactions(m) - stoi(i,j) = - i == j ? 1.0 : - (i % nr + 1) == j ? -1.0 : - 0.0 - - sparse([stoi(i,j) for i in 1:nr, j in 1:nr]) -end -``` - -You may check that the result now works just as with [`MatrixModel`](@ref) and -[`ObjectModel`](@ref): - -```julia -julia> m = CircularModel(5) -Metabolic model of type CircularModel - - 1.0 -1.0 ⋅ ⋅ ⋅ - ⋅ 1.0 -1.0 ⋅ ⋅ - ⋅ ⋅ 1.0 -1.0 ⋅ - ⋅ ⋅ ⋅ 1.0 -1.0 - -1.0 ⋅ ⋅ ⋅ 1.0 -Number of reactions: 5 -Number of metabolites: 5 - -``` - -This interface is sufficient to run most of the basic analyses, especially the flux balance finding ones: - -```julia -julia> flux_balance_analysis_dict(m, Tulip.Optimizer) -Dict{String, Float64} with 5 entries: - "rxn5" => 1.0 - "rxn2" => 1.0 - "rxn1" => 1.0 - "rxn3" => 1.0 - "rxn4" => 1.0 - -``` - -## Writing generic model modifications - -The custom model structure can also be made compatible with many of the -existing variant-generating functions and analysis modifiers. - -The functions prepared for use as "variants" in [`screen`](@ref), usually -prefixed by `with_`, have their generic variants that only call simpler, -overloadable functions for each specific model. This choice is based on the -overloading dispatch system of Julia. For -example,[`with_removed_metabolites`](@ref) is implemented very generically by -reducing the problem to some specific [`remove_metabolites`](@ref) functions -selected by the dispatch, as follows: - -```julia -with_removed_metabolites(args...; kwargs...) = - m -> remove_metabolites(m, args...; kwargs...) -``` - -To be able to use [`with_removed_metabolites`](@ref) in your model, we can just -overload the actual inner function. For the simple circular model, the -modification might as well look like this: - -```julia -COBREXA.remove_metabolites(m::CircularModel, n::Int) = - return CircularModel(m.size - n) -``` - -!!! danger "Functions that generate model variants must be pure" - Notice that the function is "pure", i.e., does not make any in-place - modifications to the original model structure. That property is required - for [`screen`](@ref) and other functions to properly and predictably apply - the modifications to the model. To expose potential in-place modifications - to your codebase, you should instead overload the "bang" counterpart of - remove metabolites, called [`remove_metabolites!`](@ref). diff --git a/docs/src/concepts/4_wrappers.md b/docs/src/concepts/4_wrappers.md deleted file mode 100644 index e59e600da..000000000 --- a/docs/src/concepts/4_wrappers.md +++ /dev/null @@ -1,156 +0,0 @@ - -# Extending the models - -To simplify doing (and undoing) simple modifications to the existing model -structure, COBREXA.jl supports a class of model _wrappers_, which are basically -small layers that add or change the functionality of a given base models. - -Types [`Serialized`](@ref), [`MatrixCoupling`](@ref), -[`SimplifiedEnzymeConstrainedModel`](@ref), and -[`EnzymeConstrainedModel`](@ref) all work in this manner -- add some extra -functionality to the "base". Technically, they are all subtypes of the abstract -type [`AbstractModelWrapper`](@ref), which itself is a subtype of -[`AbstractMetabolicModel`](@ref) and can thus be used in all standard analysis -functions. Similarly, the model wraps can be stacked -- it is easy to e.g. -serialize a [`EnzymeConstrainedModel`](@ref), or to add coupling to an existing -[`SimplifiedEnzymeConstrainedModel`](@ref). - -As the main benefit of the approach, creating model variants using the wrapper -approach is usually more efficient than recomputing the models in place. The -wrappers are thin, and if all values can get computed and materialized only once -the model data is actually needed, we may save a great amount of computing -power. - -At the same time, since the original model stays unchanged (and may even be -immutable), undoing the modifications caused by the wrapper is extremely easy -and fast -- we just discard the wrapper. - -## Writing a model wrapper - -Creating a model wrapper structure is simple -- by declaring it a subtype of -[`AbstractModelWrapper`](@ref) and implementing a single function -[`unwrap_model`](@ref), we get default implementations of all accessors that -should work for any [`AbstractMetabolicModel`](@ref). - -As a technical example, we may make a minimal model wrapper that does not do -anything: - -```julia -struct IdentityWrap <: AbstractModelWrapper - mdl::AbstractMetabolicModel -end - -COBREXA.unwrap_model(x::IdentityWrap) = x.mdl -``` - -This is instantly usable in all analysis functions, although there is no -actual "new" functionality: - -```julia -m = IdentityWrap(load_model("e_coli_core.xml")) -flux_balance_analysis_vec(m, GLPK.Optimizer) -``` - -To modify the functionality, we simply add specific methods for accessors that -we want modified, such as [`bounds`](@ref), [`stoichiometry`](@ref) and -[`objective`](@ref). We demonstrate that on several examples below. - -## Example 1: Slower model - -Here, we construct a type `RateChangedModel` that has all bounds multiplied by -a constant factor. This can be used to e.g. simulate higher or lower abundance -of certain organism in a model. - -```julia -struct RateChangedModel <: AbstractModelWrapper - factor::Float64 - mdl::AbstractMetabolicModel -end -``` - -The overloaded accessors typically reach for basic information into the "inner" -wrapped model, and modify them in a certain way. - -```julia -COBREXA.unwrap_model(x::RateChangedModel) = x.mdl -function COBREXA.bounds(x::RateChangedModel) - (l, u) = bounds(x.mdl) # extract the original bounds - return (l .* x.factor, u .* x.factor) # return customized bounds -end -``` - -To make a 2 times faster or slower model from a base model, we can run: -```julia -faster_e_coli = RateChangedModel(2.0, load_model("e_coli_core.xml")) -slower_e_coli = RateChangedModel(1/2, load_model("e_coli_core.xml")) -``` - -## Example 2: Leaky model - -As the second example, we construct a hypothetical model that is "leaking" all -metabolites at once at a constant fixed small rate. Again, the modification is -not quite realistic, but may be useful to validate the mathematical robustness -of the models. - -```julia -struct LeakyModel <: AbstractModelWrapper - leaking_metabolites::Vector{String} - leak_rate::Float64 - mdl::AbstractMetabolicModel -end -``` - -Technically, we implement the leaks by adding an extra reaction bounded to the -precise `leak_rate`, which permanently removes all metabolites. That is done by -modifying the reaction list, stoichiometry, and bounds: - -```julia -COBREXA.unwrap_model(x::LeakyModel) = x.mdl -COBREXA.n_reactions(x::LeakyModel) = n_reactions(x.mdl) + 1 -COBREXA.reactions(x::LeakyModel) = [reactions(x.mdl); "The Leak"] -COBREXA.stoichiometry(x::LeakyModel) = [stoichiometry(x.mdl) [m in x.leaking_metabolites ? -1.0 : 0.0 for m = metabolites(x.mdl)]] -function COBREXA.bounds(x::LeakyModel) - (l, u) = bounds(x.mdl) - return ([l; x.leak_rate], [u; x.leak_rate]) -end -``` - -To make the wrapper complete and consistent, we also have to modify the -accessors that depend on correct sizes of the model items. - -```julia -COBREXA.objective(x::LeakyModel) = [objective(x.mdl); 0] -COBREXA.reaction_flux(x::LeakyModel) = [reaction_flux(x.mdl); zeros(1, n_reactions(x.mdl))] -COBREXA.coupling(x::LeakyModel) = [coupling(x.mdl) zeros(n_coupling_constraints(x.mdl))] -``` -(Among other, we modified the [`reaction_flux`](@ref) so that all analysis -methods ignore the leak reaction.) - -Now, any model can be made to lose some chosen metabolites as follows: -```julia -leaks = ["M_o2_c", "M_pi_c", "M_glx_c"] -leaky_e_coli = LeakyModel(leaks, 5, load_model("e_coli_core.xml")) -``` - -## Example 3: Combining the wrappers - -With both wrappers implemented individually, it is easy to combine them by -re-wrapping. We can easily create a model that is slowed down and moreover -leaks the metabolites as follows: -```julia -leaky_slow_e_coli = LeakyModel(leaks, 5, RateChangedModel(1/2, load_model("e_coli_core.xml"))) -``` - -As with all wrapping operations, take care about the exact order of applying -the wraps. The other combination of the model wraps differs by also changing -the rate of the metabolite leaks, which did not happen with the -`leaky_slow_e_coli` above: -```julia -slowly_leaking_slow_e_coli = RateChangedModel(1/2, LeakyModel(leaks, 5, load_model("e_coli_core.xml"))) -``` - -Expectably, the model can be solved with standard functions: -```julia -v = flux_balance_analysis_dict(slowly_leaking_slow_e_coli, GLPK.Optimizer) -v["R_BIOMASS_Ecoli_core_w_GAM"] # prints out ~0.38 -``` diff --git a/docs/src/examples.md b/docs/src/examples.md deleted file mode 100644 index d7ba920f1..000000000 --- a/docs/src/examples.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Examples - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("examples", join=true)) -Depth = 2 -``` diff --git a/docs/src/examples/01_loading.jl b/docs/src/examples/01_loading.jl deleted file mode 100644 index 361d78d0b..000000000 --- a/docs/src/examples/01_loading.jl +++ /dev/null @@ -1,73 +0,0 @@ - -# # Loading models - -# `COBREXA` can load models stored in `.mat`, `.json`, and `.xml` formats (with -# the latter denoting SBML formatted models). -# -# We will primarily use the *E. coli* "core" model to demonstrate the utilities -# found in `COBREXA`. First, let's download the model in several formats. - -## Downloads the model files if they don't already exist -!isfile("e_coli_core.mat") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.mat", "e_coli_core.mat"); -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml"); - -#md # !!! tip "Save bandwidth!" -#md # The published models usually do not change very often. It is -#md # therefore pretty useful to save them to a central location and load -#md # them from there. That saves your time, and does not unnecessarily -#md # consume the connectivity resources of the model repository. - -# Load the models using the [`load_model`](@ref) function. Models are able to -# "pretty-print" themselves, hiding the inner complexity: - -using COBREXA - -mat_model = load_model("e_coli_core.mat") -# - -json_model = load_model("e_coli_core.json") -# - -sbml_model = load_model("e_coli_core.xml") -# - -#md # !!! note "Note: `load_model` infers the input type from the file extension" -#md # Notice how each model was read into memory as a model type corresponding -#md # to its file type, i.e. the file ending with `.json` loaded as a -#md # [`JSONModel`](@ref), the file ending with `.mat` loaded as [`MATModel`](@ref), and the -#md # file ending with `.xml` loaded as an [`SBMLModel`](@ref). - -# The loaded models contain the data in a format that is preferably as -# compatible as possible with the original representation. In particular, the -# JSON model contains the representation of the JSON tree: - -json_model.json - -# SBML models contain a complicated structure from [`SBML.jl` -# package](https://github.com/LCSB-BioCore/SBML.jl): - -typeof(sbml_model.sbml) - -# MAT models contain MATLAB data: - -mat_model.mat - -# In all cases, you can access the data in the model in the same way, e.g., -# using [`variables`](@ref) to get a list of the reactions in the models: - -variables(mat_model)[1:5] -# - -variables(json_model)[1:5] - -# You can use the [generic accessors](03_exploring.md) to gather more information about -# the model contents, [convert the models](02_convert_save.md) into formats more suitable for -# hands-on processing, and export them back to disk after the -# modification. -# -# All model types can be directly [used in analysis functions](05a_fba.md), such as -# [`flux_balance_analysis`](@ref). diff --git a/docs/src/examples/02_convert_save.jl b/docs/src/examples/02_convert_save.jl deleted file mode 100644 index 564a38acd..000000000 --- a/docs/src/examples/02_convert_save.jl +++ /dev/null @@ -1,91 +0,0 @@ - -# # Converting, modifying and saving models - -# COBREXA.jl can export JSON and MATLAB-style model formats, which can be -# useful when exchanging the model data with other software. -# -# For a test, let's download and open a SBML model: - -using COBREXA - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml"); - -sbml_model = load_model("e_coli_core.xml") - - -# You can save the model as `.json` or `.mat` file using the -# [`save_model`](@ref) function: - -save_model(sbml_model, "converted_e_coli.json") -save_model(sbml_model, "converted_e_coli.mat") - - -# ## Using serialization for quick loading and saving - -# If you are saving the models only for future processing in Julia environment, -# it is often wasteful to encode the models to external formats and decode them -# back. Instead, you can use the "native" Julia data format, accessible with -# package `Serialization`. -# -# This way, you can use `serialize` to save any model format (even the -# complicated [`ObjectModel`](@ref), which does not have a "native" file format -# representation): - -using Serialization - -sm = convert(ObjectModel, sbml_model) - -open(f -> serialize(f, sm), "myModel.stdmodel", "w") - -# The models can then be loaded back using `deserialize`: - -sm2 = deserialize("myModel.stdmodel") -issetequal(metabolites(sm), metabolites(sm2)) - -# This form of loading operation is usually pretty quick: -t = @elapsed deserialize("myModel.stdmodel") -@info "Deserialization took $t seconds" -# Notably, large and complicated models with thousands of reactions and -# annotations can take tens of seconds to decode properly. Serialization allows -# you to minimize this overhead, and scales well to tens of millions of -# reactions. - -#md # !!! warning "Compatibility" -#md # The format of serialized models may change between Julia versions. -#md # In particular, never use the the serialized format for publishing models -- others will have hard time finding the correct Julia version to open them. -#md # Similarly, never use serialized models for long-term storage -- your future self will have hard time finding the historic Julia version that was used to write the data. - -# ## Converting and saving a modified model - -# To modify the models easily, it is useful to convert them to a format that -# simplifies this modification. You may use e.g. [`MatrixModel`](@ref) that -# exposes the usual matrix-and-vectors structure of models as used in MATLAB -# COBRA implementations, and [`ObjectModel`](@ref) that contains structures, -# lists and dictionaries of model contents, as typical in Python COBRA -# implementations. The object-oriented nature of [`ObjectModel`](@ref) is -# better for making small modifications that utilize known identifiers of model -# contents. -# -# Conversion of any model to [`ObjectModel`](@ref) can be performed using the -# standard Julia `convert`: - -sm = convert(ObjectModel, sbml_model) - -# The conversion can be also achieved right away when loading the model, using -# an extra parameter of [`load_model`](@ref): - -sm = load_model(ObjectModel, "e_coli_core.json") - -# As an example, we change an upper bound on one of the reactions: - -sm.reactions["PFK"].ub = 10.0 - -# After [possibly applying more modifications](04_standardmodel.md), you can again save the -# modified model in a desirable exchange format: - -save_model(sm, "modified_e_coli.json") -save_model(sm, "modified_e_coli.mat") - -# More information about [`ObjectModel`](@ref) internals is available [in a -# separate example](04_standardmodel.md). diff --git a/docs/src/examples/03_exploring.jl b/docs/src/examples/03_exploring.jl deleted file mode 100644 index 1b547051e..000000000 --- a/docs/src/examples/03_exploring.jl +++ /dev/null @@ -1,51 +0,0 @@ -# # Exploring model contents - -# For practical reasons, COBREXA.jl supports many different model types. These -# comprise ones that reflect the storage formats (such as [`JSONModel`](@ref) -# and [`SBMLModel`](@ref)), and ones that are more easily accessible for users -# and mimic the usual workflows in COBRA methodology: -# -# - [`ObjectModel`](@ref), which contains and object-oriented representation -# of model internals, built out of [`Reaction`](@ref), [`Metabolite`](@ref) -# and [`Gene`](@ref) structures, in a way similar to e.g. -# [COBRApy](https://github.com/opencobra/cobrapy/) -# - [`MatrixModel`](@ref), which contains array-oriented representation of the -# model structures, such as stoichiometry matrix and the bounds vector, in a -# way similar to e.g. [COBRA -# toolbox](https://github.com/opencobra/cobratoolbox) - -# The fields in [`ObjectModel`](@ref) structure can be discovered using `fieldnames` as follows: - -using COBREXA - -fieldnames(ObjectModel) - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); - -sm = load_model(ObjectModel, "e_coli_core.json") -typeof(sm.reactions) - -fieldnames(Reaction) - -# This process (along with e.g. Tab completion in REPL) allows you to pick -# various information about many objects, for example about a specific -# reaction: - -sm.reactions["TALA"].name -# -sm.reactions["TALA"].gene_associations #gene-reaction relationship -# -sm.reactions["TALA"].subsystem -# -sm.reactions["TALA"].ub #upper rate bound - -# The same applies to [`MatrixModel`](@ref): - -fieldnames(MatrixModel) -# -cm = load_model(MatrixModel, "e_coli_core.json") -# -cm.S -# -cm.rxns[1:10] diff --git a/docs/src/examples/03b_accessors.jl b/docs/src/examples/03b_accessors.jl deleted file mode 100644 index 7a011f596..000000000 --- a/docs/src/examples/03b_accessors.jl +++ /dev/null @@ -1,54 +0,0 @@ - -# # Generic accessors - -# To prevent the complexities of object representation, `COBREXA.jl` uses a set -# of generic interface functions that can extract various important information -# from any supported model type. This approach ensures that the analysis -# functions can work on any data. - -# For example, you can check the reactions and metabolites contained in any -# model type ([`SBMLModel`](@ref), [`JSONModel`](@ref), [`MatrixModel`](@ref), -# [`ObjectModel`](@ref), and any other) using the same accessor: - -using COBREXA - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json"); - -js = load_model("e_coli_core.json") -variables(js) -# -std = convert(MatrixModel, js) -variables(std) - -# All accessors allow systematic access to information about reactions, -# stoichiometry, metabolite properties and chemistry, genes, and various model -# annotations. -# -# The most notable ones include: -# -# - [`variables`](@ref), [`metabolites`](@ref) and [`genes`](@ref) return -# respective vectors of identifiers of reactions, metabolites and genes present -# in the model, -# - [`stoichiometry`](@ref) returns the S matrix -# - [`balance`](@ref) returns the right-hand vector of the linear model in form `Ax=b` -# - [`bounds`](@ref) return lower and upper bounds of reaction rates -# - [`metabolite_charge`](@ref) and [`metabolite_formula`](@ref) return details about metabolites -# - [`objective`](@ref) returns the objective of the model (usually labeled as `c`) -# - [`reaction_gene_associations`](@ref) describes the dependency of a reaction on gene products -# -# A complete, up-to-date list of accessors can be always generated using `methodswith`: - -using InteractiveUtils - -accessors = [ - x.name for x in methodswith(AbstractMetabolicModel, COBREXA) if - endswith(String(x.file), "AbstractMetabolicModel.jl") -] - -println.(accessors); - -#md # !!! note "Note: Not all accessors may be implemented for all the models" -#md # It is possible that not all the accessors are implemented for all the model -#md # types. If this is the case, usually `nothing` or an empty data structure is -#md # returned. If you need a specific accessor, just overload the function you require! diff --git a/docs/src/examples/04_core_model.jl b/docs/src/examples/04_core_model.jl deleted file mode 100644 index 0254c1624..000000000 --- a/docs/src/examples/04_core_model.jl +++ /dev/null @@ -1,53 +0,0 @@ -# # `MatrixModel` usage - -#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) -#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) - -# In this tutorial we will introduce `COBREXA`'s `MatrixModel` and -# `MatrixModelWithCoupling`. We will use *E. coli*'s toy model to start with. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -# ## Loading a `MatrixModel` - -model = load_model(MatrixModel, "e_coli_core.xml") # we specifically want to load a MatrixModel from the model file - -# ## Basic analysis on `MatrixModel` - -# As before, for optimization based analysis we need to load an optimizer. Here we -# will use [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl) to optimize the linear -# programs of this tutorial. Refer to the examples of [analysis](05a_fba.md) -# and [analysis modifications](05b_fba_mods.md) for details and explanations. - -using Tulip - -dict_sol = flux_balance_analysis_dict( - model, - Tulip.Optimizer; - modifications = [ - modify_objective("R_BIOMASS_Ecoli_core_w_GAM"), - modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - modify_constraint("R_EX_o2_e"; lb = 0, ub = 0), - ], -) - -# ## Structure of `MatrixModel` - -# `MatrixModel` is optimized for analysis of models that utilizes the matrix, -# linearly-algebraic "view" of the models. It stores data in a sparse format -# wherever possible. -# -# The structure contains fields that contain the expectable model elements: - -fieldnames(MatrixModel) -# -model.S - -# Contrary to the usual implementations, the model representation does not -# contain reaction coupling boudns; these can be added to any model by wrapping -# it with [`MatrixCoupling`](@ref). You may also use the prepared -# [`MatrixModelWithCoupling`](@ref) to get a version of [`MatrixModel`](@ref) with this -# coupling. diff --git a/docs/src/examples/04_standardmodel.jl b/docs/src/examples/04_standardmodel.jl deleted file mode 100644 index aac2f756a..000000000 --- a/docs/src/examples/04_standardmodel.jl +++ /dev/null @@ -1,140 +0,0 @@ -# # Basic usage of `ObjectModel` - -#md # [![](https://mybinder.org/badge_logo.svg)](@__BINDER_ROOT_URL__/notebooks/@__NAME__.ipynb) -#md # [![](https://img.shields.io/badge/show-nbviewer-579ACA.svg)](@__NBVIEWER_ROOT_URL__/notebooks/@__NAME__.ipynb) - -# In this tutorial we will use `COBREXA`'s `ObjectModel` and functions that -# specifically operate on it. As usual we will use the toy model of *E. coli* -# for demonstration. - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA - -# ## Loading a model in the ObjectModel format - -model = load_model(ObjectModel, "e_coli_core.json") # we specifically want to load a ObjectModel from the model file - -#md # !!! note "Note: Loading `ObjectModel`s implicitly uses `convert`" -#md # When using `load_model(ObjectModel, file_location)` the model at -#md # `file_location` is first loaded into its inferred format and is then -#md # converted to a `ObjectModel` using the generic accessor interface. -#md # Thus, data loss may occur. Always check your model to ensure that -#md # nothing important has been lost. - -#nb # When using `load_model(ObjectModel, file_location)` the model at -#nb # `file_location` is first loaded into its inferred format and is then -#nb # converted to a `ObjectModel` using the generic accessor interface. -#nb # Thus, data loss may occur. Always check your model to ensure that -#nb # nothing important has been lost. - -# ## Internals of `ObjectModel` - -# A benefit of `ObjectModel` is that it supports a richer internal -# infrastructure that can be used to manipulate internal model attributes in a -# systematic way. Specifically, the genes, reactions, and metabolites with of a -# model each have a type. This is particularly useful when modifying or even -# constructing a model from scratch. - -# ## `Gene`s, `Reaction`s, and `Metabolite`s - -# `ObjectModel` is composed of ordered dictionaries of `Gene`s, `Metabolite`s -# and `Reaction`s. Ordered dictionaries are used because the order of the -# reactions and metabolites are important for constructing a stoichiometric -# matrix since the rows and columns should correspond to the order of the metabolites -# and reactions returned by calling the accessors `metabolites` and `variables`. - -# Each `ObjectModel` is composed of the following fields: - -fieldnames(ObjectModel) # fields of a ObjectModel - -# The `:genes` field of a `ObjectModel` contains an ordered dictionary of gene ids mapped to `Gene`s. - -model.genes # the keys of this dictionary are the same as genes(model) - -# The `Gene` type is a struct that can be used to store information about genes -# in a `ObjectModel`. Each `Gene` is composed of the following fields: - -fieldnames(Gene) - -#md # !!! tip "Tip: Use complete to explore the structure of types" -#md # Use to quickly explore the fields of a struct. For example, -#md # Gene. will list all the fields shown above. - -#nb # Use to quickly explore the fields of a struct. For example, -#nb # Gene. will list all the fields shown above. - -# The keys used in the ordered dictionaries in -# `model.genes` are the ids returned using the generic accessor `genes`. `Gene`s -# have pretty printing, as demonstrated below for a random gene drawn from the -# model: - -random_gene_id = genes(model)[rand(1:n_genes(model))] -model.genes[random_gene_id] - -# The same idea holds for both metabolites (stored as `Metabolite`s) and -# reactions (stored as `Reaction`s). This is demonstrated below. - -random_metabolite_id = metabolites(model)[rand(1:n_metabolites(model))] -model.metabolites[random_metabolite_id] -# -random_reaction_id = variables(model)[rand(1:n_variables(model))] -model.reactions[random_reaction_id] - -# `ObjectModel` can be used to build your own metabolic model or modify an -# existing one. One of the main use cases for `ObjectModel` is that it can be -# used to merge multiple models or parts of multiple models together. Since the -# internals are uniform inside each `ObjectModel`, attributes of other model -# types are squashed into the required format (using the generic accessors). -# This ensures that the internals of all `ObjectModel`s are the same - -# allowing easy systematic evaluation. - -#md # !!! warning "Warning: Combining models with different namespaces is tricky" -#md # Combining models that use different namespaces requires care. -#md # For example, in some models the water exchange reaction is called -#md # `EX_h2o_e`, while in others it is called `R_EX_h2o_s`. This needs to -#md # manually addressed to prevent duplicates, e.g. reactions, -#md # from being added. - -# ## Checking the internals of `ObjectModel`s: `annotation_index` - -# Often when models are automatically reconstructed duplicate genes, reactions -# or metabolites end up in a model. `COBREXA` exports `annotation_index` to -# check for cases where the id of a struct may be different, but the annotations -# the same (possibly suggesting a duplication). `annotation_index` builds a -# dictionary mapping annotation features to the ids of whatever struct you are -# inspecting. This makes it easy to find structs that share certain annotation features. - -rxn_annotations = annotation_index(model.reactions) -# -rxn_annotations["ec-code"] - -# The `annotation_index` function can also be used on `Reaction`s and -# `Gene`s in the same way. - -# ## Checking the internals of `ObjectModel`s: `check_duplicate_reaction` - -# Another useful function is `check_duplicate_reaction`, which checks for -# reactions that have duplicate (or similar) reaction equations. - -pgm_duplicate = Reaction() -pgm_duplicate.id = "pgm2" # Phosphoglycerate mutase -pgm_duplicate.metabolites = Dict{String,Float64}("3pg_c" => 1, "2pg_c" => -1) -pgm_duplicate -# -check_duplicate_reaction(pgm_duplicate, model.reactions; only_metabolites = false) # can also just check if only the metabolites are the same but different stoichiometry is used - -# ## Checking the internals of `ObjectModel`s: `reaction_mass_balanced` - -# Finally, [`reaction_mass_balanced`](@ref) can be used to check if a reaction is mass -# balanced based on the formulas of the reaction equation. - -rxn_dict = Dict{String,Float64}("3pg_c" => 1, "2pg_c" => -1, "h2o_c" => 1) -reaction_mass_balanced(model, rxn_dict) - -# Now to determine which atoms are unbalanced, you can use `reaction_atom_balance` -reaction_atom_balance(model, rxn_dict) - -# Note, since `pgm_duplicate` is not in the model, we cannot use the other variants of this -# function because they find the reaction equation stored inside the `model`. diff --git a/docs/src/examples/04b_standardmodel_construction.jl b/docs/src/examples/04b_standardmodel_construction.jl deleted file mode 100644 index 8ccf7c76f..000000000 --- a/docs/src/examples/04b_standardmodel_construction.jl +++ /dev/null @@ -1,80 +0,0 @@ -# # Model construction and modification - -# `COBREXA` can load models stored in `.mat`, `.json`, and `.xml` formats; and convert -# these into `ObjectModel`s. However, it is also possible to construct models -# from scratch, and modify existing models. This will be demonstrated -# here. - -using COBREXA - -# In `COBREXA`, model construction is primarily supported through `ObjectModel`s. -# To begin, create an empty `ObjectModel`. - -model = ObjectModel() - -# Next, genes, metabolites and reactions need to be added to the model. - -# ### Add genes to the model -gene_list = [Gene(string("g", num)) for num = 1:8] - -#md # !!! warning "Warning: Don't accidentally overwrite the generic accessors" -#md # It may be tempting to call a variable `genes`, `metabolites`, or -#md # `variables`. However, these names conflict with generic accessors -#md # functions and will create problems downstream. - -add_genes!(model, gene_list) - -# ### Add metabolites to the model - -metabolite_list = [Metabolite(string("m", num)) for num = 1:4] - -metabolite_list[1].formula = "C6H12O6" # can edit metabolites, etc. directly - -add_metabolites!(model, metabolite_list) - -# ### Add reactions to the model - -r_m1 = ReactionBidirectional("EX_m1", Dict("m1" => -1.0)) # exchange reaction: m1 <-> (is the same as m1 ↔ nothing) -r1 = ReactionForward("r1", Dict("m1" => -1.0, "m2" => 1.0)) -r1.gene_associations = [Isozyme(["g1", "g2"]), Isozyme(["g3"])] # add some gene reaction rules -r2 = ReactionBackward("r2", Dict("m2" => -1.0, "m1" => 1.0)) -r3 = ReactionBidirectional("r3", Dict("m2" => -1.0, "m3" => 1.0)) -r4 = ReactionForward("r3", Dict("m2" => -1.0, "m4" => 1.0)) -r_m3 = ReactionBidirectional("r3", Dict("m3" => -1.0)) -r_m4 = ReactionForward("r3", Dict("m4" => -1.0)) -r5 = ReactionForward("r5", Dict("m4" => -1.0, "m2" => 1.0)) - -add_reactions!(model, [r1, r2, r3, r_m1, r4, r_m3, r_m4, r5]) - -m1 = metabolite_list[1] -m2 = metabolite_list[2] -m3 = metabolite_list[3] -m4 = metabolite_list[4] - -model.reactions["r4"].gene_associations = - [Isozyme(x) for x in [["g5"], ["g6", "g7"], ["g8"]]] - -#md # !!! note "Note: Writing unicode arrows" -#md # The reaction arrows can be easily written by using the `LaTeX` -#md # completions built into Julia shell (and many Julia-compatible -#md # editors). You can type: -#md # -#md # - `→` as `\rightarrow` (press `Tab` to complete) -#md # - `←` as `\leftarrow` -#md # - `↔` as `\leftrightarrow` - -# The constructed model can now be inspected. -model - -# ## Modifying existing models - -# It is also possible to modify a model by deleting certain genes. -# This is simply achieved by calling `remove_genes!`. - -remove_genes!(model, ["g1", "g2"]; knockout_reactions = false) -model - -# Likewise, reactions and metabolites can also be deleted. - -remove_metabolite!(model, "m1") -model diff --git a/docs/src/examples/05a_fba.jl b/docs/src/examples/05a_fba.jl deleted file mode 100644 index ed9e01c06..000000000 --- a/docs/src/examples/05a_fba.jl +++ /dev/null @@ -1,53 +0,0 @@ -# # Flux balance analysis (FBA) - -# We will use [`flux_balance_analysis`](@ref) and several related functions to find the optimal flux in the *E. coli* "core" model. - -# If it is not already present, download the model and load the package: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -model = load_model("e_coli_core.xml") - -# To perform any optimization-based analysis, we need to use a linear programming -# solver (also called an optimizer). Any of the [`JuMP.jl`-supported -# optimizers](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers) -# will work. Here, we will demonstrate -# [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl) and -# [GLPK](https://www.gnu.org/software/glpk/); other solvers will likely work just as -# well. - -using Tulip - -solved_model = flux_balance_analysis(model, Tulip.Optimizer) - -# `solved_model` is now an instance of optimized JuMP model. To get the -# variable values out manually, we can use `JuMP.value` function. Flux variables -# are stored as vector `x`: - -using JuMP -value.(solved_model[:x]) - -# To simplify things, there is a variant of the FBA function that does this for -# us automatically: - -flux_balance_analysis_vec(model, Tulip.Optimizer) - -# Likewise, there is another variant that returns the fluxes annotated by -# reaction names, in a dictionary: - -flux_balance_analysis_dict(model, Tulip.Optimizer) - -# Switching solvers is easy, and may be useful in case we need advanced -# functionality or performance present only in certain solvers. To switch to -# GLPK, we simply load the package and use a different optimizer to run the -# analysis: - -using GLPK -flux_balance_analysis_dict(model, GLPK.Optimizer) - -# To get a shortened but useful overview of what was found in the analysis, you -# can use [`flux_summary`](@ref) function: -flux_summary(flux_balance_analysis_dict(model, GLPK.Optimizer)) diff --git a/docs/src/examples/05b_fba_mods.jl b/docs/src/examples/05b_fba_mods.jl deleted file mode 100644 index 28602ff4f..000000000 --- a/docs/src/examples/05b_fba_mods.jl +++ /dev/null @@ -1,38 +0,0 @@ - -# # Extending FBA with modifications - -# It is often desirable to add a slight modification to the problem before -# performing the analysis, to see e.g. differences of the model behavior caused -# by the change introduced. -# -# First, let us load everything that will be required: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK, Tulip, JuMP - -model = load_model("e_coli_core.xml") - -# `COBREXA.jl` supports [many modifications](../concepts/2_modifications.md), -# which include changing objective sense, optimizer attributes, flux -# constraints, optimization objective, reaction and gene knockouts, and others. -# These modifications are applied to the optimization built within the supplied -# optimizer (in this case GLPK) in order as they are specified. User needs to -# manually ensure that the modification ordering is sensible. - -# The following example applies multiple different modifications to the *E. -# coli* core model: - -fluxes = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [ # modifications are applied in order - modify_objective("R_BIOMASS_Ecoli_core_w_GAM"), # maximize production - modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix an exchange rate - knockout(["b0978", "b0734"]), # knock out two genes - modify_optimizer(Tulip.Optimizer), # ignore the above optimizer and switch to Tulip - modify_optimizer_attribute("IPM_IterationsLimit", 1000), # customize Tulip - modify_sense(JuMP.MAX_SENSE), # explicitly tell Tulip to maximize the objective - ], -) diff --git a/docs/src/examples/06_fva.jl b/docs/src/examples/06_fva.jl deleted file mode 100644 index db90d96db..000000000 --- a/docs/src/examples/06_fva.jl +++ /dev/null @@ -1,89 +0,0 @@ -# # Flux variability analysis (FVA) - -# Here we will use [`flux_variability_analysis`](@ref) to analyze the *E. coli* -# core model. - -# As usual, if not already present, download the model and load the required -# packages. We picked the GLPK solver, but others may work as well: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The FVA implementation in [`flux_variability_analysis`](@ref) returns -# maximized and minimized reaction fluxes in a 2-column matrix. -# The bounds parameter function here (constructed with -# [`objective_bounds`](@ref)) sets that the objective value is allowed to vary -# by 1% from the optimum found by FBA on the same model: - -flux_variability_analysis(model, GLPK.Optimizer; bounds = objective_bounds(0.99)) - -# (You may also use [`gamma_bounds`](@ref).) - -# ## Detailed variability analysis with modifications -# -# A dictionary-returning variant in [`flux_variability_analysis_dict`](@ref), -# returns the result in a slightly more structured way. At the same time, we -# can specify additional [modifications](../concepts/2_modifications.md) to be -# applied to the model: - -min_fluxes, max_fluxes = flux_variability_analysis_dict( - model, - GLPK.Optimizer; - bounds = objective_bounds(0.99), - modifications = [ - modify_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - modify_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), - ], -) - -# The dictionaries can be easily used to explore the whole state of the model -# when certain reactions are maximized or minimized. For example, we can take -# the maximal acetate exchange flux when the acetate exchange is maximized: - -max_fluxes["R_EX_ac_e"]["R_EX_ac_e"] - -# We can also check that the modifications really had the desired effect on -# oxygen consumption: - -max_fluxes["R_EX_ac_e"]["R_O2t"] - -# ...and see how much carbon dioxide would produced under at the given -# metabolic extreme: - -max_fluxes["R_EX_ac_e"]["R_EX_co2_e"] - - -# ## Summarizing the flux variability -# -# A convenience function [`flux_variability_summary`](@ref) is able to display -# this information in a nice overview: -flux_variability_summary((min_fluxes, max_fluxes)) - -# ## Retrieving details about FVA output -# -# Parameter `ret` of [`flux_variability_analysis`](@ref) can be used to extract -# specific pieces of information from the individual solved (minimized and -# maximized) optimization problems. Here we show how to extract the value of -# biomass "growth" along with the minimized/maximized reaction flux. - -# First, find the index of biomass reaction in all reactions -biomass_idx = first(indexin(["R_BIOMASS_Ecoli_core_w_GAM"], variables(model))) - -# Now run the FVA: -vs = flux_variability_analysis( - model, - GLPK.Optimizer; - bounds = objective_bounds(0.50), # objective can vary by up to 50% of the optimum - modifications = [ - modify_constraint("R_EX_glc__D_e"; lb = -10, ub = -10), - modify_constraint("R_EX_o2_e"; lb = 0.0, ub = 0.0), - ], - ret = optimized_model -> ( - COBREXA.JuMP.objective_value(optimized_model), - COBREXA.JuMP.value(optimized_model[:x][biomass_idx]), - ), -) diff --git a/docs/src/examples/07_gene_deletion.jl b/docs/src/examples/07_gene_deletion.jl deleted file mode 100644 index 0844ecd0a..000000000 --- a/docs/src/examples/07_gene_deletion.jl +++ /dev/null @@ -1,113 +0,0 @@ -# # Gene knockouts - -# Here we will use the [`knockout`](@ref) function to modify the optimization -# model before solving, in order to simulate genes knocked out. We can pass -# [`knockout`](@ref) to many analysis functions that support parameter -# `modifications`, including [`flux_balance_analysis`](@ref), -# [`flux_variability_analysis`](@ref), and others. - -# ## Deleting a single gene - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# First, let's compute the "original" flux, with no knockouts. -original_flux = flux_balance_analysis_dict(model, GLPK.Optimizer); - -# One can find gene IDs that we can knock out using [`genes`](@ref) and -# [`gene_name`](@ref) functions: -genes(model) -# It is possible to sort the genes by gene name to allow easier lookups: -sort(gene_name.(Ref(model), genes(model)) .=> genes(model)) - -# Compute the flux with a genes knocked out: -flux_with_knockout = - flux_balance_analysis_dict(model, GLPK.Optimizer, modifications = [knockout("G_b3236")]) - -# We can see there is a small decrease in production upon knocking out the gene: -biomass_id = "R_BIOMASS_Ecoli_core_w_GAM" -flux_with_knockout[biomass_id] / original_flux[biomass_id] - -# Similarly, we can explore how the flux variability has changed once the gene -# is knocked out: -variability_with_knockout = - flux_variability_analysis(model, GLPK.Optimizer, modifications = [knockout("G_b3236")]) - -# ## Knocking out multiple genes - -# Multiple genes can be knocked out by simply passing a vector of genes to the -# knockout modification. This knocks out all genes that can run the FBA -# reaction: - -reaction_gene_associations(model, "R_FBA") -# -flux_with_double_knockout = flux_balance_analysis_dict( - model, - GLPK.Optimizer, - modifications = [knockout(["G_b2097", "G_b1773", "G_b2925"])], -) -# -flux_with_double_knockout[biomass_id] / original_flux[biomass_id] - -# ## Processing all single gene knockouts -# -# Function [`screen`](@ref) provides a parallelizable and extensible way to run -# the flux balance analysis with the knockout over all genes: - -knockout_fluxes = screen( - model, - args = tuple.(genes(model)), - analysis = (m, gene) -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer, modifications = [knockout(gene)]) - if !isnothing(res) - res[biomass_id] - end - end, -) - -# It is useful to display the biomass growth rates of the knockout models -# together with the gene name: -sort(gene_name.(Ref(model), genes(model)) .=> knockout_fluxes, by = first) - -# ## Processing all multiple-gene deletions -# -# ### Double gene knockouts -# -# Since we can generate any kind of argument matrix for [`screen`](@ref) to -# process, it is straightforward to generate the matrix of all double gene -# knockouts and let the function process it. This computes the biomass -# production of all double-gene knockouts: - -gene_groups = [[g1, g2] for g1 in genes(model), g2 in genes(model)]; -double_knockout_fluxes = screen( - model, - args = tuple.(gene_groups), - analysis = (m, gene_groups) -> begin - res = flux_balance_analysis_dict( - m, - GLPK.Optimizer, - modifications = [knockout(gene_groups)], - ) - if !isnothing(res) - res[biomass_id] - end - end, -) - -# The results can be converted to an easily scrutinizable form as follows: -reshape([gene_name.(Ref(model), p) for p in gene_groups] .=> double_knockout_fluxes, :) - -# ### Triple gene knockouts (and others) -# -# We can extend the same analysis to triple or other gene knockouts by -# generating a different array of gene pairs. For example, one can generate -# gene_groups for triple gene deletion screening: -gene_groups = [[g1, g2, g3] for g1 in genes(model), g2 in genes(model), g3 in genes(model)]; - -#md # !!! warning Full triple gene deletion analysis may take a long time to compute. -#md # We may use parallel processing with [`screen`](@ref) to speed up the -#md # analysis. Alternatively, process only a subset of the genes triples. diff --git a/docs/src/examples/07_restricting_reactions.jl b/docs/src/examples/07_restricting_reactions.jl deleted file mode 100644 index 8c68e5c92..000000000 --- a/docs/src/examples/07_restricting_reactions.jl +++ /dev/null @@ -1,149 +0,0 @@ - -# # Restricting and disabling individual reactions - -# Here, we show several methods how to explore the effect of disabling or choking the reactions in the models. - -# First, download the demonstration data and load the packages as usual: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model(ObjectModel, "e_coli_core.json") - -# ## Disabling a reaction - -# There are several possible ways to disable a certain reaction in the model. -# The easiest way is to use [`change_bound`](@ref) or [`change_bounds`](@ref) -# to create a variant of the model that has the corresponding bounds modified -# (or, alternatively, a pipeable "variant" version -# [`with_changed_bound`](@ref)). -# -# Alternatively, you could utilize [`modify_constraint`](@ref) as a -# modification that acts directly on the JuMP optimization model. That may be -# useful if you first apply some kind of complicated constraint scheme -# modification, such as [`add_loopless_constraints`](@ref). -# -# In turn, in the simple case, the following 2 ways of disabling the FBA -# reaction are equivalent. The first, making a variant of the model structure, -# might be slightly preferred because it better composes with other changes; -# the second does not compose as well but may be more efficient (and thus -# faster) in certain situations: - -flux1 = flux_balance_analysis_vec( - model |> with_changed_bound("FBA", lower_bound = 0.0, upper_bound = 0.0), - GLPK.Optimizer, -); - -flux2 = flux_balance_analysis_vec( - model, - GLPK.Optimizer, - modifications = [modify_constraint("FBA", lb = 0.0, ub = 0.0)], -); - -# The solutions should not differ a lot: -sum((flux1 .- flux2) .^ 2) - -# ## Restricting a reaction -# -# Quite naturally, you can restruct the reaction to a limited flow, simulating -# e.g. nutrient deficiency: - -original_flux = flux_balance_analysis_dict(model, GLPK.Optimizer); - -restricted_flux = flux_balance_analysis_dict( - model, - GLPK.Optimizer, - modifications = [modify_constraint("EX_o2_e", lb = -0.1, ub = 0.0)], -); - -# The growth in the restricted case is, expectably, lower than the original one: -original_flux["BIOMASS_Ecoli_core_w_GAM"], restricted_flux["BIOMASS_Ecoli_core_w_GAM"] - -# ## Screening for sensitive and critical reactions -# -# Using higher-order analysis scheduling functions (in particular -# [`screen`](@ref)), you can easily determine which reactions play a crucial -# role for the model viability and which are not very important. - -# We can take all reactions where the flux is not zero: - -running_reactions = [(rid, x) for (rid, x) in original_flux if abs(x) > 1e-3] - -# ...and choke these reactions to half that flux, computing the relative loss -# of the biomass production:: - -screen( - model, - variants = [ - [with_changed_bound(rid, lower_bound = -0.5 * abs(x), upper_bound = 0.5 * abs(x))] - for (rid, x) in running_reactions - ], - args = running_reactions, - analysis = (m, rid, _) -> - rid => - flux_balance_analysis_dict(m, GLPK.Optimizer)["BIOMASS_Ecoli_core_w_GAM"] / - original_flux["BIOMASS_Ecoli_core_w_GAM"], -) - -# (You may notice that restricting the ATP maintenance pseudo-reaction (`ATPM`) -# had a mildly surprising effect of actually increasing the biomass production -# by a few percent. That is because the cells are not required to produce ATP -# to survive and may invest the nutrients and energy elsewhere.) - -# ## Screening with reaction combinations - -# The same analysis can be scaled up to screen for combinations of critical -# reactions, giving possibly more insight into the redundancies in the model: - -running_reaction_combinations = [ - (rid1, rid2, x1, x2) for (rid1, x1) in running_reactions, - (rid2, x2) in running_reactions -] - -biomass_mtx = screen( - model, - variants = [ - [ - with_changed_bound( - rid1, - lower_bound = -0.5 * abs(x1), - upper_bound = 0.5 * abs(x1), - ), - with_changed_bound( - rid2, - lower_bound = -0.5 * abs(x2), - upper_bound = 0.5 * abs(x2), - ), - ] for (rid1, rid2, x1, x2) in running_reaction_combinations - ], - analysis = m -> - flux_balance_analysis_dict(m, GLPK.Optimizer)["BIOMASS_Ecoli_core_w_GAM"] / - original_flux["BIOMASS_Ecoli_core_w_GAM"], -) - -# Finally, let's plot the result: - -using CairoMakie, Clustering - -order = - hclust([ - sum((i .- j) .^ 2) for i in eachcol(biomass_mtx), j in eachcol(biomass_mtx) - ]).order - -labels = first.(running_reactions)[order]; -positions = collect(eachindex(labels)) - -f = Figure(fontsize = 8) -ax = Axis(f[1, 1], xticks = (positions, labels), yticks = (positions, labels)) -heatmap!(ax, positions, positions, biomass_mtx[order, order]) -ax.xticklabelrotation = π / 3 -ax.xticklabelalign = (:right, :center) -ax.yticklabelrotation = π / 6 -ax.yticklabelalign = (:right, :center) -f - -# Remember that [`screen`](@ref) can be parallelized just [by supplying worker -# IDs](../distributed/1_functions.md). Use that to gain significant speedup -# with analyses of larger models. diff --git a/docs/src/examples/08_pfba.jl b/docs/src/examples/08_pfba.jl deleted file mode 100644 index f8ea24bd5..000000000 --- a/docs/src/examples/08_pfba.jl +++ /dev/null @@ -1,74 +0,0 @@ -# # Parsimonious flux balance analysis (pFBA) - -# Parsimonious flux balance analysis attempts to find a realistic flux of a -# model, by trying to minimize squared sum of all fluxes while maintaining the -# reached optimum. COBREXA.jl implements it in function -# [`parsimonious_flux_balance_analysis`](@ref) (accompanied by vector- and -# dictionary-returning variants -# [`parsimonious_flux_balance_analysis_vec`](@ref) and -# [`parsimonious_flux_balance_analysis_dict`](@ref)). -# -# As usual, we demonstrate the functionality on the *E. coli* model: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, Tulip, Clarabel - -model = load_model("e_coli_core.xml") - -# Because the parsimonious objective is quadratic, we need a an optimizer -# capable of solving quadratic programs. -# -# As the simplest choice, we can use -# [`Clarabel.jl`](https://osqp.org/docs/get_started/julia.html), but any any -# [`JuMP.jl`-supported -# optimizer](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers) -# that supports quadratic programming will work. - -#md # !!! note "Note: Clarabel can be sensitive" -#md # We recommend reading the documentation of `Clarabel` before using it, since -#md # it may give inconsistent results depending on what settings -#md # you use. Commercial solvers like `Gurobi`, `Mosek`, `CPLEX`, etc. -#md # require less user engagement. - -# Running of basic pFBA is perfectly analogous to running of [FBA](05a_fba.md) -# and other analyses. We add several modifications that improve the solution -# (using functions [`silence`](@ref), and -# [`modify_optimizer_attribute`](@ref)), and fix the glucose exchange (using -# [`modify_constraint`](@ref)) in order to get a more reasonable result: - -fluxes = parsimonious_flux_balance_analysis_dict( - model, - Clarabel.Optimizer; - modifications = [ - silence, # optionally silence the optimizer (Clarabel is very verbose by default) - modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), # fix glucose consumption rate - ], -) - -# ## Using different optimizers for linear and quadratic problems -# -# It is quite useful to use specialized optimizers for specialized tasks in -# pFBA. In particular, one would usually require to get a precise solution from -# the linear programming part (where the precision is achievable), and trade -# off a little precision for vast improvements in computation time in the -# quadratic programming part. -# -# In pFBA, we can use the `modifications` and `qp_modifications` parameters to -# switch and parametrize the solvers in the middle of the process, which allows -# us to implement precisely that improvement. We demonstrate the switching on a -# vector-returning variant of pFBA: - -flux_vector = parsimonious_flux_balance_analysis_vec( - model, - Tulip.Optimizer; # start with Tulip - modifications = [ - modify_constraint("R_EX_glc__D_e"; lb = -12, ub = -12), - modify_optimizer_attribute("IPM_IterationsLimit", 500), # we may change Tulip-specific attributes here - ], - qp_modifications = [ - modify_optimizer(Clarabel.Optimizer), # now switch to Clarabel (Tulip wouldn't be able to finish the computation) - silence, # and make it quiet. - ], -) diff --git a/docs/src/examples/09_loopless.jl b/docs/src/examples/09_loopless.jl deleted file mode 100644 index 5bb6a2186..000000000 --- a/docs/src/examples/09_loopless.jl +++ /dev/null @@ -1,45 +0,0 @@ -# # Loopless FBA - -# Here we will use [`flux_balance_analysis`](@ref) and -# [`flux_variability_analysis`](@ref) to analyze a toy model of *E. coli* that -# is constrained in a way that removes all thermodynamically infeasible loops in -# the flux solution. For more details about the algorithm, see [Schellenberger, -# and, Palsson., "Elimination of thermodynamically infeasible loops in -# steady-state metabolic models.", Biophysical Journal, -# 2011](https://doi.org/10.1016/j.bpj.2010.12.3707). - -# If it is not already present, download the model: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# In COBREXA.jl, the Loopless FBA is implemented as a modification of the -# normal FBA, called [`add_loopless_constraints`](@ref). -# We use GLPK optimizer here, because the loopless constraints add integer -# programming into the problem. Simpler solvers (such as Tulip) may not be able -# to solve the mixed integer-linear (MILP) programs. - -loopless_flux = flux_balance_analysis_vec( - model, - GLPK.Optimizer, - modifications = [add_loopless_constraints()], -) - -# The representation is particularly convenient since it allows to also explore -# other properties of loopless models, such as variability and parsimonious -# balance, as well as other analyses that accept `modifications` parameter: - -loopless_variability = flux_variability_analysis( - model, - GLPK.Optimizer, - modifications = [add_loopless_constraints()], -) - -# For details about the loopless method, refer to Schellenberger, Jan, Nathan -# E. Lewis, and Bernhard Ø. Palsson: "Elimination of thermodynamically -# infeasible loops in steady-state metabolic models." *Biophysical journal* -# 100, no. 3 (2011), pp. 544-553. diff --git a/docs/src/examples/10_crowding.jl b/docs/src/examples/10_crowding.jl deleted file mode 100644 index f3820b425..000000000 --- a/docs/src/examples/10_crowding.jl +++ /dev/null @@ -1,15 +0,0 @@ -# # FBA with ??? - -# Let's starting with loading the models and packages. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, Tulip - -model = load_model("e_coli_core.xml") - -import Random -Random.seed!(1) # for repeatability of random numbers below - -# TODO add crowding example via smoment diff --git a/docs/src/examples/11_growth.jl b/docs/src/examples/11_growth.jl deleted file mode 100644 index f2c87620c..000000000 --- a/docs/src/examples/11_growth.jl +++ /dev/null @@ -1,134 +0,0 @@ -# # Growth media analysis - -# Nutrient availability is a major driving factor for growth of microorganisms -# and energy production in cells. Here, we demonstrate two main ways to examine -# the nutrient consumption with COBREXA.jl: Simulating deficiency of nutrients, -# and finding the minimal flux of nutrients required to support certain model -# output. - -# As always, we work on the toy model of *E. coli*: - -using COBREXA, GLPK - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -model = load_model(ObjectModel, "e_coli_core.xml") - -# ## What nutrients does my model need to grow? - -# The models usually ingest nutrients through exchange reactions. By changing -# the bounds on the exchange reactions, you can limit the intake of the -# nutrients and thus simulate the nutrient deficiency. If applied -# programatically to multiple exchanges, this can give you a good overview of -# what nutrients impact the model most. -# -# To check the viability of a single nutrient, you can simply change a bound on -# a selected exchange reaction and simulate the model with a limited amount. - -biomass = "R_BIOMASS_Ecoli_core_w_GAM" - -model_limited = change_bound(model, "R_EX_glc__D_e", lower_bound = -1.0) - -#md # !!! tip "Exchange directions" -#md # By a convention, the direction of exchange reaction usually goes from the -#md # model into the environment, representing the "production". Limiting the -#md # intake thus happens by disabling the "negative production", i.e., placing -#md # a lower bound. - -original_production = flux_balance_analysis_dict(model, GLPK.Optimizer)[biomass] -limited_production = flux_balance_analysis_dict(model_limited, GLPK.Optimizer)[biomass] - -original_production, limited_production - -# Function [`flux_summary`](@ref) can help with quickly spotting what has -# changed: - -flux_summary(flux_balance_analysis_dict(model_limited, GLPK.Optimizer)) - -# Similarly, you can check that the model can survive without oxygen, at the cost -# of switching the metabolism to ethanol production: - -flux_summary( - flux_balance_analysis_dict( - change_bound(model, "R_EX_o2_e", lower_bound = 0.0), - GLPK.Optimizer, - ), -) - -# The effect of all nutrients on the metabolism can be scanned using [`screen`](@ref). The [`change_bound`](@ref) function is, for this purpose, packed in a variant specified [`with_changed_bound`](@ref): - -exchanges = filter(looks_like_exchange_reaction, variables(model)) - -exchanges .=> screen( - model, - variants = [ - [with_changed_bound(exchange, lower_bound = 0.0)] for exchange in exchanges - ], - analysis = m -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer) - isnothing(res) ? nothing : res[biomass] - end, -) - - -# Similarly to gene knockouts, you can also examine the effect of combined -# nutrient deficiencies. To obtain a more interesting result, we may examine -# the effect of slight deficiencies of pairs of intake metabolites. For -# simplicity, we show the result only on a small subset of the exchanges: - -selected_exchanges = [ - "R_EX_pi_e", - "R_EX_gln__L_e", - "R_EX_nh4_e", - "R_EX_pyr_e", - "R_EX_fru_e", - "R_EX_glu__L_e", - "R_EX_glc__D_e", - "R_EX_o2_e", -] - -screen( - model, - variants = [ - [with_changed_bounds([e1, e2], lower_bound = [-1.0, -0.1])] for - e1 in selected_exchanges, e2 in selected_exchanges - ], - analysis = m -> begin - res = flux_balance_analysis_dict(m, GLPK.Optimizer) - isnothing(res) ? nothing : res[biomass] - end, -) - -# The result describes combinations of nutrient deficiencies -- the nutrient -# that corresponds to the row is mildly deficient (limited to uptake 1.0), and -# the one that corresponds to the column is severely limited (to uptake 0.1). - -#md # !!! tip "Screening can be easily parallelized" -#md # To speed up larger analyses, remember that execution of [`screen`](@ref) -#md # can be [parallelized to gain speedup](../distributed/1_functions.md). Parallelization in `screen` is optimized to avoid -#md # unnecessary data transfers that may occur when using trivial `pmap`. - -# ## What is the minimal flux of nutrients for my model to grow? - -# You can compute the minimal flux (i.e., mass per time) of required nutrients -# by constraining the model growth to a desired lower bound, and then optimize -# the model with an objective that minimizes intake of all exchanges (i.e., -# given the directionality convention of the exchanges, actually maximizes the -# flux through all exchange reactions along their direction). - -model_with_bounded_production = change_bound(model, biomass, lower_bound = 0.1) #minimum required growth - -minimal_intake_production = flux_balance_analysis_dict( - model_with_bounded_production, - GLPK.Optimizer, - modifications = [modify_objective(exchanges)], -); - -# Metabolite "cost" data may be supplemented using the `weights` argument of -# [`change_objective`](@ref), to reflect e.g. the different molar masses or -# energetic values of different nutrients. -# -# In our simple case, we obtain the following minimal required intake: - -flux_summary(minimal_intake_production) diff --git a/docs/src/examples/12_mmdf.jl b/docs/src/examples/12_mmdf.jl deleted file mode 100644 index f16138922..000000000 --- a/docs/src/examples/12_mmdf.jl +++ /dev/null @@ -1,196 +0,0 @@ - -# # Maximum-minimum driving force analysis - -# Here, we use the max-min driving force analysis (MMDF) to find optimal -# concentrations for the metabolites in glycolysis to ensure that the smallest -# driving force across all the reactions in the model is as large as possible. -# The method is described in more detail by [Flamholz, et al., "Glycolytic -# strategy as a tradeoff between energy yield and protein cost.", Proceedings of -# the National Academy of Sciences, -# 2013](https://doi.org/10.1073/pnas.1215283110). - -# We start as usual, with loading models and packages: - -using COBREXA, GLPK - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -model = load_model("e_coli_core.json") - -# For MMDF to work, we need thermodynamic data about the involved reactions. -# -# In particular, we will use reaction Gibbs free energies (ΔG⁰) that can be -# obtained e.g. from [eQuilibrator](https://equilibrator.weizmann.ac.il/) -# (possibly using the existing [Julia -# wrapper](https://github.com/stelmo/eQuilibrator.jl) that allows you to -# automate this step in Julia). -# -# Here, we have gathered a dictionary that maps the reaction IDs to calculated -# Gibbs free energy of reaction for each metabolic reaction (including the -# transporters). The units of the measurements are not crucial for the -# computation, but we use the usual kJ/mol for consistency. - -reaction_standard_gibbs_free_energies = Dict( - "ACALD" => -21.26, - "PTAr" => 8.65, - "ALCD2x" => 17.47, - "PDH" => -34.24, - "PYK" => -24.48, - "CO2t" => 0.00, - "MALt2_2" => -6.83, - "CS" => -39.33, - "PGM" => -4.47, - "TKT1" => -1.49, - "ACONTa" => 8.46, - "GLNS" => -15.77, - "ICL" => 9.53, - "FBA" => 23.37, - "SUCCt3" => -43.97, - "FORt2" => -3.42, - "G6PDH2r" => -7.39, - "AKGDH" => -28.23, - "TKT2" => -10.31, - "FRD7" => 73.61, - "SUCOAS" => -1.15, - "FBP" => -11.60, - "ICDHyr" => 5.39, - "AKGt2r" => 10.08, - "GLUSy" => -47.21, - "TPI" => 5.62, - "FORt" => 13.50, - "ACONTb" => -1.62, - "GLNabc" => -30.19, - "RPE" => -3.38, - "ACKr" => 14.02, - "THD2" => -33.84, - "PFL" => -19.81, - "RPI" => 4.47, - "D_LACt2" => -3.42, - "TALA" => -0.94, - "PPCK" => 10.65, - "ACt2r" => -3.41, - "NH4t" => -13.60, - "PGL" => -25.94, - "NADTRHD" => -0.01, - "PGK" => 19.57, - "LDH_D" => 20.04, - "ME1" => 12.08, - "PIt2r" => 10.41, - "ATPS4r" => -37.57, - "PYRt2" => -3.42, - "GLCpts" => -45.42, - "GLUDy" => 32.83, - "CYTBD" => -59.70, - "FUMt2_2" => -6.84, - "FRUpts2" => -42.67, - "GAPD" => 0.53, - "H2Ot" => 0.00, - "PPC" => -40.81, - "NADH16" => -80.37, - "PFK" => -18.54, - "MDH" => 25.91, - "PGI" => 2.63, - "O2t" => 0.00, - "ME2" => 12.09, - "GND" => 10.31, - "SUCCt2_2" => -6.82, - "GLUN" => -14.38, - "ETOHt2r" => -16.93, - "ADK1" => 0.38, - "ACALDt" => 0.00, - "SUCDi" => -73.61, - "ENO" => -3.81, - "MALS" => -39.22, - "GLUt2r" => -3.49, - "PPS" => -6.05, - "FUM" => -3.42, -); - -# COBREXA implementation of MMDF enforces that `ΔᵣG .* v ≤ 0` (where `v` is the -# flux solution). This is slightly less restrictive than the original -# formulation of MMDF, where all fluxes are enforced to be positive; instead, -# the COBREXA solution needs a pre-existing thermodynamically consistent -# solution that is used as a reference. -# -# We can generate a well-suited reference solution using e.g. the [loopless -# FBA](09_loopless.md): - -flux_solution = flux_balance_analysis_dict( - model, - GLPK.Optimizer; - modifications = [add_loopless_constraints()], -) - -# We can now run the MMDF. -# -# In the call, we specify the metabolite IDs of protons and water so that they -# are omitted from concentration calculations. Also, the water transport -# reaction should typically also be ignored. Additionally, we can fix the -# concentration ratios of certain metabolites directly. -# -# The reason for removing the protons and water from the concentration -# calculations is because the Gibbs free energies of biochemical reactions are -# measured at constant pH in aqueous environments. Allowing the model to change -# the pH would break the assumptions about validity of the thermodynamic -# measurements. - -sol = max_min_driving_force( - model, - reaction_standard_gibbs_free_energies, - GLPK.Optimizer; - flux_solution = flux_solution, - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_ratios = Dict( - ("atp_c", "adp_c") => 10.0, - ("nadh_c", "nad_c") => 0.13, - ("nadph_c", "nadp_c") => 1.3, - ), - concentration_lb = 1e-6, # 1 uM - concentration_ub = 0.1, # 100 mM - ignore_reaction_ids = [ - "H2Ot", # ignore water transport - ], -) - -sol.mmdf - -#md # !!! note "Note: transporters" -#md # Transporters can be included in MMDF analysis, however water and proton -#md # transporters must be excluded explicitly in `ignore_reaction_ids`. -#md # In turn, the ΔᵣG for these transport reactions -#md # will always be 0. If you do not exclude the transport of the metabolites, -#md # the MMDF will likely only have a zero solution. - -# Finally, we show how the concentrations are optimized to ensure that each -# reaction proceeds "down the hill" (ΔᵣG < 0). We can explore the glycolysis -# pathway reactions: - -glycolysis_pathway = - ["GLCpts", "PGI", "PFK", "FBA", "TPI", "GAPD", "PGK", "PGM", "ENO", "PYK"] - -# We additionally scale the fluxes according to their stoichiometry in the -# pathway. From the output, we can clearly see that metabolite concentrations -# play a large role in ensuring the thermodynamic consistency of in vivo -# reactions. - -using CairoMakie - -standard_dg = cumsum([ - reaction_standard_gibbs_free_energies[rid] * flux_solution[rid] for - rid in glycolysis_pathway -]); -optimal_dg = - cumsum([sol.dg_reactions[rid] * flux_solution[rid] for rid in glycolysis_pathway]); - -f = Figure(); -ax = Axis(f[1, 1], ylabel = "Cumulative ΔG", xticks = (1:10, glycolysis_pathway)); -lines!(ax, 1:10, standard_dg .- first(standard_dg), color = :blue, label = "ΔG⁰"); -lines!(ax, 1:10, optimal_dg .- first(optimal_dg), color = :red, label = "MMDF solution"); -axislegend(ax) -f - -#md # !!! tip "Thermodynamic variability" -#md # As with normal flux variability, thermodynamic constraints in a model also allow a certain amount of parameter selection freedom. -#md # Specialized [`max_min_driving_force_variability`](@ref) can be used to explore the thermodynamic solution space more easily. diff --git a/docs/src/examples/13_moma.jl b/docs/src/examples/13_moma.jl deleted file mode 100644 index 518933034..000000000 --- a/docs/src/examples/13_moma.jl +++ /dev/null @@ -1,54 +0,0 @@ -# # Minimization of metabolic adjustment (MOMA) - -# MOMA allows you to find a feasible solution of the model that is closest (in -# an Euclidean metric) to a reference solution. Often this gives a realistic -# estimate of the organism behavior that has undergone a radical change (such -# as a gene knockout) that prevents it from metabolizing optimally, but the -# rest of the metabolism has not yet adjusted to compensate for the change. - -# The original description of MOMA is by [Segre, Vitkup, and Church, "Analysis -# of optimality in natural and perturbed metabolic networks", Proceedings of the -# National Academy of Sciences, 2002](https://doi.org/10.1073/pnas.232349399). - -# As always, let's start with downloading a model. - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA - -model = load_model(ObjectModel, "e_coli_core.xml") - -# MOMA analysis requires solution of a quadratic model, we will thus use Clarabel as the main optimizer. - -using Clarabel - -# We will need a reference solution, which represents the original state of the -# organism before the change. -reference_flux = - flux_balance_analysis_dict(model, Clarabel.Optimizer; modifications = [silence]) - -# As the change here, we manually knock out CYTBD reaction: -changed_model = change_bound(model, "R_CYTBD", lower_bound = 0.0, upper_bound = 0.0); - -# Now, let's find a flux that minimizes the organism's metabolic adjustment for -# this model: -flux_summary( - minimize_metabolic_adjustment_analysis_dict( - changed_model, - reference_flux, - Clarabel.Optimizer; - modifications = [silence], - ), -) - -# For illustration, you can compare the result to the flux that is found by -# simple optimization: - -flux_summary( - flux_balance_analysis_dict( - changed_model, - Clarabel.Optimizer; - modifications = [silence], - ), -) diff --git a/docs/src/examples/14_simplified_enzyme_constrained.jl b/docs/src/examples/14_simplified_enzyme_constrained.jl deleted file mode 100644 index 6a61e7e3a..000000000 --- a/docs/src/examples/14_simplified_enzyme_constrained.jl +++ /dev/null @@ -1,112 +0,0 @@ -# # sMOMENT - -# sMOMENT algorithm can be used to easily adjust the metabolic activity within -# the cell to respect known enzymatic parameters and enzyme mass constraints -# measured by proteomics and other methods. -# -# The original description from sMOMENT is by [Bekiaris, and Klamt, "Automatic -# construction of metabolic models with enzyme constraints.", BMC -# bioinformatics, 2020](https://doi.org/10.1186/s12859-019-3329-9) -# -# Let's load some packages: - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA, GLPK - -model = load_model("e_coli_core.json") - -# We will necessarily need the enzyme turnover numbers (aka "kcats") and masses -# of the required gene products. You do not necessarily need to know all data -# for the given model, but the more you have, the better the approximation will -# be. -# -# For the demonstration purpose, we will generate the data randomly. In a -# realistic setting, you would input experimental or database-originating data -# here: - -import Random -Random.seed!(1) # repeatability - -gene_product_masses = Dict(genes(model) .=> randn(n_genes(model)) .* 10 .+ 60) - -# We only take the reactions that have gene products (i.e., enzymes) associated with them): -rxns = filter( - x -> - !looks_like_biomass_reaction(x) && - !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_associations(model, x)), - variables(model), -) - -# The information about each enzyme and its capabilities is stored in an -# [`Isozyme`](@ref) structure. For simplicity, sMOMENT ignores much of the -# information about the multiplicity of required gene products and -# other. - -rxn_isozymes = Dict( - rxn => Isozyme( - Dict(vcat(reaction_gene_associations(model, rxn)...) .=> 1), - randn() * 100 + 600, #forward kcat - randn() * 100 + 500, #reverse kcat - ) for rxn in rxns -) - -# In case some of the reactions are missing in `rxn_isozymes`, sMOMENT simply -# ignores them. -# -# Once the data is gathered, we create a model that wraps the original model -# with additional sMOMENT structure: - -simplified_enzyme_constrained_model = - model |> with_simplified_enzyme_constraints( - reaction_isozyme = rxn_isozymes, - gene_product_molar_mass = gene_product_masses, - total_gene_product_mass_bound = 50.0, - ) - -# (You could alternatively use the [`make_simplified_enzyme_constrained_model`](@ref) to create the -# structure more manually; but [`with_simplified_enzyme_constraints`](@ref) is easier to use e.g. -# with [`screen`](@ref).) - -# In turn, you should have a complete model with unidirectional reactions and -# additional coupling, as specified by the sMOMENT method: - -[ - stoichiometry(simplified_enzyme_constrained_model) - coupling(simplified_enzyme_constrained_model) -] - -# the type (SimplifiedEnzymeConstrainedModel) is a model wrapper -- it is a thin additional layer -# that just adds the necessary sMOMENT-relevant information atop the original -# model, which is unmodified. That makes the process very efficient and -# suitable for large-scale data processing. You can still access the original -# "base" model hidden in the SimplifiedEnzymeConstrainedModel using [`unwrap_model`](@ref). - -# Other than that, the [`SimplifiedEnzymeConstrainedModel`](@ref) is a model type like any other, -# and you can run any analysis you want on it, such as FBA: - -flux_balance_analysis_dict(simplified_enzyme_constrained_model, GLPK.Optimizer) - -# (Notice that the total reaction fluxes are reported despite the fact that -# reactions are indeed split in the model! The underlying mechanism is provided -# by [`reaction_variables`](@ref) accessor.) - -# [Variability](06_fva.md) of the sMOMENT model can be explored as such: - -flux_variability_analysis( - simplified_enzyme_constrained_model, - GLPK.Optimizer, - bounds = gamma_bounds(0.95), -) - -# ...and a sMOMENT model sample can be obtained [as usual with -# sampling](16_hit_and_run.md): - -( - affine_hit_and_run( - simplified_enzyme_constrained_model, - warmup_from_variability(simplified_enzyme_constrained_model, GLPK.Optimizer), - )' * reaction_variables(simplified_enzyme_constrained_model) -) diff --git a/docs/src/examples/15_enzyme_constrained.jl b/docs/src/examples/15_enzyme_constrained.jl deleted file mode 100644 index ae0b595e9..000000000 --- a/docs/src/examples/15_enzyme_constrained.jl +++ /dev/null @@ -1,106 +0,0 @@ -# # GECKO - -# GECKO algorithm can be used to easily adjust the metabolic activity within the -# cell to respect many known parameters, measured by proteomics and other -# methods. -# -# The original description from GECKO is by: [Sánchez, et. al., "Improving the -# phenotype predictions of a yeast genome‐scale metabolic model by incorporating -# enzymatic constraints.", Molecular systems biology, -# 2017](https://doi.org/10.15252/msb.20167411). -# -# The analysis method and implementation in COBREXA is similar to -# [sMOMENT](14_simplified_enzyme_constrained.md), but GECKO is able to process and represent much -# larger scale of the constraints -- mainly, it supports multiple isozymes for -# each reaction, and the isozymes can be grouped into "enzyme mass groups" to -# simplify interpretation of data from proteomics. - -# For demonstration, we will generate artificial random data in a way similar -# to the [sMOMENT example](14_simplified_enzyme_constrained.md): - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -using COBREXA, GLPK - -model = load_model("e_coli_core.json") - -import Random -Random.seed!(1) # repeatability - -gene_product_masses = Dict(genes(model) .=> randn(n_genes(model)) .* 10 .+ 60) - -rxns = filter( - x -> - !looks_like_biomass_reaction(x) && - !looks_like_exchange_reaction(x) && - !isnothing(reaction_gene_associations(model, x)), - variables(model), -) - -# The main difference from sMOMENT comes from allowing multiple isozymes per -# reaction (reactions with missing isozyme informations will be ignored, -# leaving them as-is): -rxn_isozymes = Dict( - rxn => [ - Isozyme( - Dict(isozyme_genes .=> 1), - randn() * 100 + 600, #forward kcat - randn() * 100 + 500, #reverse kcat - ) for isozyme_genes in reaction_gene_associations(model, rxn) - ] for rxn in rxns -) - -# We also construct similar bounds for total gene product amounts: -gene_product_bounds = Dict(genes(model) .=> Ref((0.0, 10.0))) - -# With this, the construction of the model constrained by all enzymatic -# information is straightforward: - -enzyme_constrained_model = - model |> with_enzyme_constraints(; - reaction_isozymes = rxn_isozymes, - gene_product_bounds, - gene_product_molar_mass = gene_product_masses, - gene_product_mass_groups = _ -> "uncategorized", # all products belong to the same "uncategorized" category - gene_product_mass_group_bounds = _ -> 100.0, # the total limit of mass in the single category - ) - -# (Alternatively, you may use [`make_enzyme_constrained_model`](@ref), which does the same -# without piping by `|>`.) - -# The stoichiometry and coupling in the enzyme_constrained model is noticeably more complex; -# you may notice new "reactions" added that simulate the gene product -# utilization: - -[stoichiometry(enzyme_constrained_model); coupling(enzyme_constrained_model)] - -# Again, the resulting model can be used in any type of analysis. For example, flux balance analysis: - -opt_model = flux_balance_analysis(enzyme_constrained_model, GLPK.Optimizer) - -# Get the fluxes - -flux_sol = values_dict(:reaction, enzyme_constrained_model, opt_model) - -# Get the gene product concentrations - -gp_concs = values_dict(:enzyme, enzyme_constrained_model, opt_model) - -# Get the total masses assigned to each mass group - -values_dict(:enzyme_group, enzyme_constrained_model, opt_model) - -# Variability: - -flux_variability_analysis( - enzyme_constrained_model, - GLPK.Optimizer, - bounds = gamma_bounds(0.95), -) - -# ...and sampling: -affine_hit_and_run( - enzyme_constrained_model, - warmup_from_variability(enzyme_constrained_model, GLPK.Optimizer), -)' * reaction_variables(enzyme_constrained_model) diff --git a/docs/src/examples/16_hit_and_run.jl b/docs/src/examples/16_hit_and_run.jl deleted file mode 100644 index b147ed2cf..000000000 --- a/docs/src/examples/16_hit_and_run.jl +++ /dev/null @@ -1,73 +0,0 @@ -# # Hit and run sampling - -# Sampling the feasible space of the model allows you to gain a realistic -# insight into the distribution of the flow and its probabilistic nature, often -# better describing the variance and correlations of the possible fluxes better -# (but more approximately) than e.g. [flux variability analysis](06_fva.md). - -# COBREXA supports a variant of hit-and-run sampling adjusted to the -# complexities of metabolic models; in particular, it implements a version -# where the next sample (and indirectly, the next run direction) is generated -# as an affine combination of the samples in the current sample set. This gives -# a result similar to the artificially centered hit-and-run sampling, with a -# slightly (unsubstantially) different biases in the generation of the next -# hits. - -# As always, we start by loading everything that is needed: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The sampling procedure requires a set of "seed" points that will form the -# basis for the first iteration of runs. This is commonly called a warm-up. You -# can generate these points manually with any method of choice, or you can use -# the COBREXA implementation of warmup that uses the extreme edges of the -# polytope, similarly to the way used by flux variability analysis: - -warmup_points = warmup_from_variability(model, GLPK.Optimizer) - -# This generates a matrix of fluxes, where each column is one sample point, and -# rows correspond to the reactions in the model. - -# With this, starting the sampling procedure is straightforward: - -samples = affine_hit_and_run(model, warmup_points) - -# As seen above, the result again contains 1 sample per column, with reactions -# in the same order with rows as before. To get a different sample, you can -# tune the parameters the function. `sample_iters` allows you to specify the -# iterations at which you want the current sample to be collected and reported -# -- by default, that is done 5 times on each 100th iteration. In the example -# below, we catch the samples in 10 iterations right after the 200th iteration -# passed. Similarly, to avoid possible degeneracy, you can choose to run more -# hit-and-run batch chains than 1, using the `chains` parameters. The total -# number of points collected is the number of points in warmup times the number -# of sample-collecting iterations times the number of chains. - -samples = affine_hit_and_run(model, warmup_points, sample_iters = 201:210, chains = 2) - -#md # !!! tip "Parallelization" -#md # Both procedures used for sampling in this example -#md # ([`warmup_from_variability`](@ref), [`affine_hit_and_run`](@ref)) can be -#md # effectively parallelized by adding `workers=` parameter, as summarized -#md # in [the documentation](../distributed/1_functions.md). Due to the nature of the algorithm, parallelization -#md # of the sampling requires at least 1 chain per worker. - -# ## Visualizing the samples - -# Samples can be displayed very efficiently in a scatterplot or a density plot, -# which naturally show correlations and distributions of the fluxes: - -using CairoMakie - -o2, co2 = indexin(["R_EX_o2_e", "R_EX_co2_e"], variables(model)) - -scatter( - samples[o2, :], - samples[co2, :]; - axis = (; xlabel = "Oxygen exchange", ylabel = "Carbon dioxide exchange"), -) diff --git a/docs/src/examples/17_envelopes.jl b/docs/src/examples/17_envelopes.jl deleted file mode 100644 index ef763ae05..000000000 --- a/docs/src/examples/17_envelopes.jl +++ /dev/null @@ -1,83 +0,0 @@ - -# # Production envelopes - -# Production envelopes show what a model is capable of doing on a wide range of -# parameters. Usually, you choose a regular grid of a small dimension in the -# parameter space, and get an information about how well the model runs at each -# point. - -# As usual, we start by loading everything: - -!isfile("e_coli_core.xml") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -using COBREXA, GLPK - -model = load_model("e_coli_core.xml") - -# The envelope analysis "structure" is similar to what you can obtain using -# [`screen`](@ref); but COBREXA provides a special functions that run the -# process in a very optimized manner. For envelopes, there is -# [`envelope_lattice`](@ref) that generates the rectangular lattice of points -# for a given model and reactions, and [`objective_envelope`](@ref) that -# computes the output (usually as the objective value) of the model at the -# lattice points. You do not need to call [`envelope_lattice`](@ref) directly -# because it is taken as a "default" way to create the lattice by -# [`objective_envelope`](@ref). - -# In short, we can compute the envelope of a single reaction in the *E. coli* -# model as follows: - -envelope = objective_envelope( - model, - ["R_EX_o2_e"], - GLPK.Optimizer, - lattice_args = (ranges = [(-50, 0)],), -) - -# (The named tuple given in `lattice_args` argument is passed to the internal -# call of [`envelope_lattice`](@ref), giving you an easy way to customize its -# behavior.) - -# The result has 2 fields which can be used to easily plot the envelope. We -# also need to "fix" the missing values (represented as `nothing`) where the -# model failed to solve -- we will simply omit them here). - -using CairoMakie - -valid = .!(isnothing.(envelope.values)) -lines(envelope.lattice[1][valid], float.(envelope.values[valid])) - -# Additional resolution can be obtained either by supplying your own larger -# lattice, or simply forwarding the `samples` argument to the internal call of -# [`envelope_lattice`](@ref): - -envelope = objective_envelope( - model, - ["R_EX_co2_e"], - GLPK.Optimizer, - lattice_args = (samples = 1000, ranges = [(-50, 100)]), -) - -valid = .!(isnothing.(envelope.values)) -lines(envelope.lattice[1][valid], float.(envelope.values[valid])) - -# ## Multi-dimensional envelopes - -# The lattice functions generalize well to more dimensions; you can easily -# explore the production of the model depending on the relative fluxes of 2 -# reactions: - -envelope = objective_envelope( - model, - ["R_EX_o2_e", "R_EX_co2_e"], - GLPK.Optimizer, - lattice_args = (samples = 100, ranges = [(-60, 0), (-15, 60)]), -) - -heatmap( - envelope.lattice[1], - envelope.lattice[2], - [isnothing(x) ? 0 : x for x in envelope.values], - axis = (; xlabel = "Oxygen exchange", ylabel = "Carbon dioxide exchange"), -) diff --git a/docs/src/functions.md b/docs/src/functions.md index 9d26940b0..add3bf10f 100644 --- a/docs/src/functions.md +++ b/docs/src/functions.md @@ -1,6 +1,47 @@ # API reference -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("functions", join=true)) -Depth = 2 +## Helper types + +```@autodocs +Modules = [COBREXA] +Pages = ["src/types.jl"] +``` + +## Model loading and saving + +```@autodocs +Modules = [COBREXA] +Pages = ["src/io.jl"] +``` + +## Constraint system building + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/core.jl"] +``` + +### Genetic constraints + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/genes.jl"] ``` + +### Objective function helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/objectives.jl"] +``` + +## Solver interface + +```@autodocs +Modules = [COBREXA] +Pages = ["src/solver.jl"] +``` + +## Analysis functions + +## Distributed analysis diff --git a/docs/src/functions/analysis.md b/docs/src/functions/analysis.md deleted file mode 100644 index f37bbaa36..000000000 --- a/docs/src/functions/analysis.md +++ /dev/null @@ -1,22 +0,0 @@ -# Analysis functions - -## Common analysis functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", file), readdir("../src/analysis")) -``` - -## Sampling - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "sampling", file), readdir("../src/analysis/sampling")) -``` - -## Analysis modifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "modifications", file), readdir("../src/analysis/modifications")) -``` diff --git a/docs/src/functions/base.md b/docs/src/functions/base.md deleted file mode 100644 index eff4308b7..000000000 --- a/docs/src/functions/base.md +++ /dev/null @@ -1,6 +0,0 @@ -# Base functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", file), readdir("../src/base")) -``` diff --git a/docs/src/functions/io.md b/docs/src/functions/io.md deleted file mode 100644 index c7d908914..000000000 --- a/docs/src/functions/io.md +++ /dev/null @@ -1,15 +0,0 @@ -# Input and output - -## File I/O and serialization - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("io", file), readdir("../src/io")) -``` - -## Pretty printing - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("io", "show", file), readdir("../src/io/show")) -``` diff --git a/docs/src/functions/reconstruction.md b/docs/src/functions/reconstruction.md deleted file mode 100644 index f85bddaf9..000000000 --- a/docs/src/functions/reconstruction.md +++ /dev/null @@ -1,15 +0,0 @@ -# Model construction functions - -## Functions for changing the models - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", file), readdir("../src/reconstruction")) -``` - -## Variant specifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", "modifications", file), readdir("../src/reconstruction/modifications")) -``` diff --git a/docs/src/functions/types.md b/docs/src/functions/types.md deleted file mode 100644 index c952444c3..000000000 --- a/docs/src/functions/types.md +++ /dev/null @@ -1,20 +0,0 @@ -# Types - -## Base types - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "abstract", file), readdir("../src/base/types/abstract")) -``` - -## Model types and contents -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", file), readdir("../src/base/types")) -``` - -## Model type wrappers -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "wrappers", file), readdir("../src/base/types/wrappers")) -``` diff --git a/docs/src/functions/utils.md b/docs/src/functions/utils.md deleted file mode 100644 index b38635b54..000000000 --- a/docs/src/functions/utils.md +++ /dev/null @@ -1,22 +0,0 @@ -# Utilities - -## Helper functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "utils", file), readdir("../src/base/utils")) -``` - -## Macro-generated functions and internal helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "macros", file), readdir("../src/base/macros")) -``` - -## Logging and debugging helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "logging", file), readdir("../src/base/logging")) -``` diff --git a/docs/src/index.md b/docs/src/index.md new file mode 100644 index 000000000..74950b077 --- /dev/null +++ b/docs/src/index.md @@ -0,0 +1,7 @@ + +# COBREXA.jl + +```@autodocs +Modules = [COBREXA] +Pages = ["src/COBREXA.jl"] +``` diff --git a/docs/src/index.md.template b/docs/src/index.md.template deleted file mode 100644 index d5d49ca5a..000000000 --- a/docs/src/index.md.template +++ /dev/null @@ -1,83 +0,0 @@ -```@raw html -
-
- - -
-
-``` - -# Constraint-Based Reconstruction and EXascale Analysis - -| **Repository** | **Tests** | **Coverage** | **How to contribute?** | -|:--------------:|:---------:|:------------:|:----------------------:| -| [![GitHub](https://img.shields.io/github/stars/LCSB-BioCore/COBREXA.jl?label=COBREXA.jl&style=social)](http://github.com/LCSB-BioCore/COBREXA.jl) | [![CI](https://github.com/LCSB-BioCore/COBREXA.jl/actions/workflows/ci.yml/badge.svg?branch=master)](https://github.com/LCSB-BioCore/COBREXA.jl/actions/workflows/ci.yml) | [![codecov](https://codecov.io/gh/LCSB-BioCore/COBREXA.jl/branch/master/graph/badge.svg?token=H3WSWOBD7L)](https://codecov.io/gh/LCSB-BioCore/COBREXA.jl) | [![contrib](https://img.shields.io/badge/contributions-start%20here-green)](howToContribute.md) | - - -COBREXA is a toolkit for working with large constraint-based metabolic models, -and running very large numbers of analysis tasks on these models in parallel. -Its main purpose is to make the methods of Constraint-based Reconstruction and -Analysis (COBRA) scale to problem sizes that require the use of huge computer -clusters and HPC environments, which allows them to be realistically applied to -pre-exascale-sized models. - -In this package, you will find the usual COBRA-like functions that interface to -underlying linear programming solvers. We use -[`JuMP.jl`](https://github.com/jump-dev/JuMP.jl) as the unified interface for -many solvers; you can plug in whichever compatible solver you want, including -the popular [`Tulip.jl`](https://github.com/ds4dm/Tulip.jl), -[`GLPK.jl`](https://github.com/jump-dev/GLPK.jl), -[`OSQP.jl`](https://github.com/osqp/OSQP.jl), and -[`Gurobi.jl`](https://github.com/jump-dev/Gurobi.jl). - -```@raw html -
-history
-Development history of COBREXA.jl. -
-``` - -## Quick start guide - -A dedicated [quick start guide](quickstart.md) is available for quickly trying out the -analysis with COBREXA.jl. - -## Example notebooks and workflows - -Detailed example listing is [available here](examples.md). -All examples are also avaialble as JuPyteR notebooks. - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("examples", join=true)) -Depth = 1 -``` - -## Core concept documentation - -Detailed concept guide listing is [available here](concepts.md). - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("concepts", join=true)) -Depth = 1 -``` - -## Functions and types API reference - -Detailed table of contents of the API documentation is [available -here](functions.md). - -```@contents -Pages = filter(x -> endswith(x, ".md"), readdir("functions", join=true)) -Depth = 1 -``` - -## Contribution guide - -If you wish to contribute code, patches or improvements to `COBREXA.jl`, please -follow the [contribution guidelines and hints](howToContribute.md). - - - -```@raw html - -``` diff --git a/docs/src/quickstart.md b/docs/src/quickstart.md deleted file mode 100644 index bffc50d9a..000000000 --- a/docs/src/quickstart.md +++ /dev/null @@ -1,152 +0,0 @@ - -# Quick start guide - -You can install COBREXA from Julia repositories. Start `julia`, **press `]`** to -switch to the Packaging environment, and type: -``` -add COBREXA -``` - -You also need to install your favorite solver supported by `JuMP.jl` (such as -Gurobi, Mosek, CPLEX, GLPK, Clarabel, etc., see a [list -here](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers)). For -example, you can install `Tulip.jl` solver by typing: -``` -add Tulip -``` - -Alternatively, you may use [prebuilt Docker and Apptainer images](#prebuilt-images). - -If you are running COBREXA.jl for the first time, it is very likely that upon -installing and importing the packages, your Julia installation will need to -precompile their source code from the scratch. In fresh installations, the -precompilation process should take less than 5 minutes. - -When the packages are installed, switch back to the "normal" julia shell by -pressing Backspace (the prompt should change color back to green). After that, -you can download [a SBML model from the -internet](http://bigg.ucsd.edu/models/e_coli_core) and perform a -flux balance analysis as follows: - -```julia -using COBREXA # loads the package -using Tulip # loads the optimization solver - -# download the model -download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml") - -# open the SBML file and load the contents -model = load_model("e_coli_core.xml") - -# run a FBA -fluxes = flux_balance_analysis_dict(model, Tulip.Optimizer) -``` - -The variable `fluxes` will now contain a dictionary of the computed optimal -flux of each reaction in the model: -``` -Dict{String,Float64} with 95 entries: - "R_EX_fum_e" => 0.0 - "R_ACONTb" => 6.00725 - "R_TPI" => 7.47738 - "R_SUCOAS" => -5.06438 - "R_GLNS" => 0.223462 - "R_EX_pi_e" => -3.2149 - "R_PPC" => 2.50431 - "R_O2t" => 21.7995 - "R_G6PDH2r" => 4.95999 - "R_TALA" => 1.49698 - ⋮ => ⋮ -``` - -#### Model variant processing - -The main feature of COBREXA.jl is the ability to easily specify and process -many analyses in parallel. To demonstrate, let's see how the organism would -perform if some reactions were disabled independently: - -```julia -# convert to a model type that is efficient to modify -m = convert(ObjectModel, model) - -# find the model objective value if oxygen or carbon dioxide transports are disabled -screen(m, # the base model - variants=[ # this specifies how to generate the desired model variants - [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0), - with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable both - ], - # this specifies what to do with the model variants (received as the argument `x`) - analysis = x -> - flux_balance_analysis_dict(x, Tulip.Optimizer)["R_BIOMASS_Ecoli_core_w_GAM"], -) -``` -You should receive a result showing that missing oxygen transport makes the -biomass production much harder: -```julia -4-element Vector{Float64}: - 0.8739215022674809 - 0.21166294973372796 - 0.46166961413944896 - 0.21114065173865457 -``` - -Most importantly, such analyses can be easily specified by automatically -generating long lists of modifications to be applied to the model, and -parallelized. - -Knocking out each reaction in the model is efficiently accomplished: - -```julia -# load the task distribution package, add several worker nodes, and load -# COBREXA and the solver on the nodes -using Distributed -addprocs(4) -@everywhere using COBREXA, Tulip - -# get a list of the workers -worker_list = workers() - -# run the processing in parallel for many model variants -res = screen(m, - variants=[ - # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower_bound =0.0, upper_bound =0.0)] - for reaction_id in reactions(m) - ], - analysis = model -> begin - # we need to check if the optimizer even found a feasible solution, - # which may not be the case if we knock out important reactions - sol = flux_balance_analysis_dict(model, Tulip.Optimizer) - isnothing(sol) ? nothing : sol["R_BIOMASS_Ecoli_core_w_GAM"] - end, - # run the screening in parallel on all workers in the list - workers = worker_list, -) -``` - -In result, you should get a long list of the biomass production for each -reaction knockout. Let's decorate it with reaction names: -```julia -Dict(reactions(m) .=> res) -``` -...which should output an easily accessible dictionary with all the objective -values named, giving a quick overview of which reactions are critical for the -model organism to create biomass: -```julia -Dict{String, Union{Nothing, Float64}} with 95 entries: - "R_ACALD" => 0.873922 - "R_PTAr" => 0.873922 - "R_ALCD2x" => 0.873922 - "R_PDH" => 0.796696 - "R_PYK" => 0.864926 - "R_CO2t" => 0.46167 - "R_EX_nh4_e" => 1.44677e-15 - "R_MALt2_2" => 0.873922 - "R_CS" => 2.44779e-14 - "R_PGM" => 1.04221e-15 - "R_TKT1" => 0.864759 - ⋮ => ⋮ -``` diff --git a/docs/src/quickstart.md.template b/docs/src/quickstart.md.template deleted file mode 100644 index f2fadddd8..000000000 --- a/docs/src/quickstart.md.template +++ /dev/null @@ -1,96 +0,0 @@ - -# Quick start guide - - - -#### Model variant processing - -The main feature of COBREXA.jl is the ability to easily specify and process -many analyses in parallel. To demonstrate, let's see how the organism would -perform if some reactions were disabled independently: - -```julia -# convert to a model type that is efficient to modify -m = convert(ObjectModel, model) - -# find the model objective value if oxygen or carbon dioxide transports are disabled -screen(m, # the base model - variants=[ # this specifies how to generate the desired model variants - [], # one with no modifications, i.e. the base case - [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0)], # disable oxygen - [with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable CO2 - [with_changed_bound("R_O2t", lower_bound =0.0, upper_bound =0.0), - with_changed_bound("R_CO2t", lower_bound =0.0, upper_bound =0.0)], # disable both - ], - # this specifies what to do with the model variants (received as the argument `x`) - analysis = x -> - flux_balance_analysis_dict(x, Tulip.Optimizer)["R_BIOMASS_Ecoli_core_w_GAM"], -) -``` -You should receive a result showing that missing oxygen transport makes the -biomass production much harder: -```julia -4-element Vector{Float64}: - 0.8739215022674809 - 0.21166294973372796 - 0.46166961413944896 - 0.21114065173865457 -``` - -Most importantly, such analyses can be easily specified by automatically -generating long lists of modifications to be applied to the model, and -parallelized. - -Knocking out each reaction in the model is efficiently accomplished: - -```julia -# load the task distribution package, add several worker nodes, and load -# COBREXA and the solver on the nodes -using Distributed -addprocs(4) -@everywhere using COBREXA, Tulip - -# get a list of the workers -worker_list = workers() - -# run the processing in parallel for many model variants -res = screen(m, - variants=[ - # create one variant for each reaction in the model, with that reaction knocked out - [with_changed_bound(reaction_id, lower_bound =0.0, upper_bound =0.0)] - for reaction_id in reactions(m) - ], - analysis = model -> begin - # we need to check if the optimizer even found a feasible solution, - # which may not be the case if we knock out important reactions - sol = flux_balance_analysis_dict(model, Tulip.Optimizer) - isnothing(sol) ? nothing : sol["R_BIOMASS_Ecoli_core_w_GAM"] - end, - # run the screening in parallel on all workers in the list - workers = worker_list, -) -``` - -In result, you should get a long list of the biomass production for each -reaction knockout. Let's decorate it with reaction names: -```julia -Dict(reactions(m) .=> res) -``` -...which should output an easily accessible dictionary with all the objective -values named, giving a quick overview of which reactions are critical for the -model organism to create biomass: -```julia -Dict{String, Union{Nothing, Float64}} with 95 entries: - "R_ACALD" => 0.873922 - "R_PTAr" => 0.873922 - "R_ALCD2x" => 0.873922 - "R_PDH" => 0.796696 - "R_PYK" => 0.864926 - "R_CO2t" => 0.46167 - "R_EX_nh4_e" => 1.44677e-15 - "R_MALt2_2" => 0.873922 - "R_CS" => 2.44779e-14 - "R_PGM" => 1.04221e-15 - "R_TKT1" => 0.864759 - ⋮ => ⋮ -``` diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 26dc474fd..247080114 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -5,6 +5,7 @@ module COBREXA using DocStringExtensions +import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J diff --git a/src/builders/core.jl b/src/builders/core.jl index b69928391..d85e1d71c 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -8,7 +8,7 @@ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_structure(model::F.AbstractFBCModel) +function fbc_model_constraints(model::F.AbstractFBCModel) rxns = Symbol.(F.reactions(model)) mets = Symbol.(F.metabolites(model)) lbs, ubs = F.bounds(model) @@ -27,6 +27,8 @@ function fbc_model_structure(model::F.AbstractFBCModel) ) end +export fbc_model_constraints + """ $(TYPEDSIGNATURES) @@ -35,6 +37,8 @@ Shortcut for allocation non-negative ("unsigned") variables. The argument """ unsigned_variables(; keys) = C.variables(; keys, bounds = Ref((0.0, Inf))) +export unsigned_variables + """ $(TYPEDSIGNATURES) @@ -72,3 +76,5 @@ sign_split_constraints(; ) for (k, s) in C.elems(signed) ) #TODO the example above might as well go to docs + +export sign_split_constraints diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 805b963cc..9113ce3fe 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,11 +1,18 @@ -knockout_constraints(ko_genes; fluxes::ConstraintTree, gene_products_available) = +knockout_constraints(;fluxes::ConstraintTree, knockout_test) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if begin - if gene_products_available(rxn, k) - false - else - all(gs -> any(g -> g in ko_genes, gs), gss) - end - end + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) ) + +export knockout_constraints + +fbc_gene_knockout_constraints(;fluxes::ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; + fluxes, + knockout_test = + rxn -> !A.reaction_gene_products_available( + rxn, + g -> not (g in genes) + ), +) + +export fbc_gene_knockout_constraints diff --git a/src/io.jl b/src/io.jl new file mode 100644 index 000000000..0e0030b81 --- /dev/null +++ b/src/io.jl @@ -0,0 +1,35 @@ + +""" + $(TYPEDSIGNATURES) + +Load a FBC model representation while guessing the correct model type. Uses +`AbstractFBCModels.load`. + +This overload almost always involves a search over types; do not use it in +environments where performance is critical. +""" +function load_fbc_model(path::String) where A<:AbstractFBCModel + A.load(path) +end + +""" + $(TYPEDSIGNATURES) + +Load a FBC model representation. Uses `AbstractFBCModels.load`. +""" +function load_fbc_model(model_type::Type{A}, path::String) where A<:AbstractFBCModel + A.load(model_type, path) +end + +export load_fbc_model + +""" + $(TYPEDSIGNATURES) + +Save a FBC model representation. Uses `AbstractFBCModels.save`. +""" +function save_fbc_model(model::A, path::String) where A<:AbstractFBCModel + A.save(model, path) +end + +export save_fbc_model diff --git a/src/solver.jl b/src/solver.jl index 69b58b43c..7cd7d1027 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -6,7 +6,7 @@ Construct a JuMP `Model` that describes the precise constraint system into the JuMP `Model` created for solving in `optimizer`, with a given optional `objective` and optimization `sense`. """ -function make_optimization_model( +function optimization_model( cs::C.ConstraintTree; objective::Union{Nothing,C.LinearValue,C.QuadraticValue} = nothing, optimizer, @@ -35,6 +35,8 @@ function make_optimization_model( return model end +export optimization_model + """ $(TYPEDSIGNATURES) @@ -44,6 +46,8 @@ locally optimal). `false` if any other termination status is reached. is_solved(opt_model::J.Model) = J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] +export is_solved + """ $(TYPEDSIGNATURES) @@ -52,6 +56,8 @@ The optimized objective value of a JuMP model, if solved. optimized_objective_value(opt_model::J.Model)::Maybe{Float64} = is_solved(opt_model) ? J.objective_value(opt_model) : nothing +export optimized_objective_value + """ $(TYPEDSIGNATURES) @@ -60,6 +66,8 @@ The optimized variable assignment of a JuMP model, if solved. optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = is_solved(opt_model) ? J.value.(opt_model[:x]) : nothing +export optimized_variable_assignment + """ $(TYPEDSIGNATURES) @@ -70,3 +78,5 @@ solution(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = let vars = optimized_variable_assignment(opt_model) isnothing(vars) ? nothing : C.ValueTree(c, vars) end + +export solution From e36b2f58a6b1665daa3d7fb55065ce0c355f6301 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 11:40:04 +0100 Subject: [PATCH 344/531] comb down the docs and tests --- Project.toml | 3 +- docs/make.jl | 96 +------ docs/src/concepts.md | 7 + docs/src/concepts/1_screen.md | 268 ++++++++++++++++++ docs/src/distributed/4_serialized.md | 50 ---- docs/src/examples.md | 7 + docs/src/examples/01-loading-and-saving.jl | 6 + docs/src/examples/02-flux-balance-analysis.jl | 6 + docs/src/{functions.md => reference.md} | 0 docs/src/reference/analysis.md | 22 ++ docs/src/reference/base.md | 6 + docs/src/reference/io.md | 15 + docs/src/reference/reconstruction.md | 15 + docs/src/reference/types.md | 20 ++ docs/src/reference/utils.md | 22 ++ test/coverage/coverage-summary.jl | 12 - test/runtests.jl | 8 +- 17 files changed, 407 insertions(+), 156 deletions(-) create mode 100644 docs/src/concepts.md create mode 100644 docs/src/concepts/1_screen.md delete mode 100644 docs/src/distributed/4_serialized.md create mode 100644 docs/src/examples.md create mode 100644 docs/src/examples/01-loading-and-saving.jl create mode 100644 docs/src/examples/02-flux-balance-analysis.jl rename docs/src/{functions.md => reference.md} (100%) create mode 100644 docs/src/reference/analysis.md create mode 100644 docs/src/reference/base.md create mode 100644 docs/src/reference/io.md create mode 100644 docs/src/reference/reconstruction.md create mode 100644 docs/src/reference/types.md create mode 100644 docs/src/reference/utils.md delete mode 100644 test/coverage/coverage-summary.jl diff --git a/Project.toml b/Project.toml index 7daf9d9a4..aec146e80 100644 --- a/Project.toml +++ b/Project.toml @@ -17,6 +17,7 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] +AbstractFBCModels = "0.2" Clarabel = "0.3" ConstraintTrees = "0.4" DistributedData = "0.2" @@ -28,9 +29,7 @@ julia = "1.5" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" diff --git a/docs/make.jl b/docs/make.jl index cec126f49..0b98cc0c3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,14 +2,7 @@ using Documenter using Literate, JSON using COBREXA -# some settings -dev_docs_folder = "dev" -pages_branch = "gh-pages" - -# This must match the repo slug on github! -github_repo_slug = ENV["CI_PROJECT_NAMESPACE"] * "/" * ENV["CI_PROJECT_NAME"] - -# generate examples +# build the examples examples_path = joinpath(@__DIR__, "src", "examples") examples_basenames = sort(filter(x -> endswith(x, ".jl"), readdir(examples_path))) @info "base names:" examples_basenames @@ -17,47 +10,14 @@ examples = joinpath.(examples_path, examples_basenames) examples_outdir = joinpath(@__DIR__, "src", "examples") for example in examples - #TODO improve how the nbviewer and binder links are inserted. Direct link to ipynb would be cool Literate.markdown( example, examples_outdir; - repo_root_url = "https://github.com/$github_repo_slug/blob/master", - nbviewer_root_url = "https://nbviewer.jupyter.org/github/$github_repo_slug/blob/gh-pages/$dev_docs_folder", - binder_root_url = "https://mybinder.org/v2/gh/$github_repo_slug/$pages_branch?filepath=$dev_docs_folder", + repo_root_url = "https://github.com/LCSB-BioCore/COBREXA.jl/blob/master", ) Literate.notebook(example, examples_outdir) end -# extract shared documentation parts from README.md -readme_md = open(f -> read(f, String), joinpath(@__DIR__, "..", "README.md")) -quickstart = - match(r"\n([^\0]*)", readme_md).captures[1] -acks = match( - r"\n([^\0]*)", - readme_md, -).captures[1] -ack_logos = - match(r"\n([^\0]*)", readme_md).captures[1] - -# insert the shared documentation parts into index and quickstart templates -#TODO use direct filename read/write -index_md = open(f -> read(f, String), joinpath(@__DIR__, "src", "index.md.template")) -index_md = replace(index_md, "\n" => acks) -index_md = replace(index_md, "\n" => ack_logos) -open(f -> write(f, index_md), joinpath(@__DIR__, "src", "index.md"), "w") - -quickstart_md = - open(f -> read(f, String), joinpath(@__DIR__, "src", "quickstart.md.template")) -quickstart_md = replace(quickstart_md, "\n" => quickstart) -open(f -> write(f, quickstart_md), joinpath(@__DIR__, "src", "quickstart.md"), "w") - -# copy the contribution guide -cp( - joinpath(@__DIR__, "..", ".github", "CONTRIBUTING.md"), - joinpath(@__DIR__, "src", "howToContribute.md"), - force = true, -) - # a helper for sourcing the documentation files from directories find_mds(path) = joinpath.( @@ -65,12 +25,6 @@ find_mds(path) = filter(x -> endswith(x, ".md"), readdir(joinpath(@__DIR__, "src", path))), ) -# Documenter tries to guess the repo slug from git remote URL but that doesn't -# work really well here, this is the only fallback. If this breaks, "Edit on -# GitHub" links will stop working. (See Documenter.jl source in -# src/Utilities/Utilities.jl, in November 2021 it was around line 500) -mk -ENV["TRAVIS_REPO_SLUG"] = github_repo_slug - # build the docs makedocs( modules = [COBREXA], @@ -85,7 +39,6 @@ makedocs( linkcheck = !("skiplinks" in ARGS), pages = [ "Home" => "index.md", - "COBREXA.jl in 10 minutes" => "quickstart.md", "Examples" => [ "Contents" => "examples.md" find_mds("examples") @@ -98,31 +51,13 @@ makedocs( "Contents" => "concepts.md" find_mds("concepts") ], - "Reference (Functions and types)" => [ - "Contents" => "functions.md" - find_mds("functions") + "Reference" => [ + "Contents" => "reference.md" + find_mds("reference") ], - "How to contribute" => "howToContribute.md", ], ) -# remove the workaround (this would cause deploydocs() to get confused and try -# to deploy the travis way) -delete!(ENV, "TRAVIS_REPO_SLUG") - -# replace the "edit this" links for the generated documentation -function replace_in_doc(filename, replacement) - contents = open(f -> read(f, String), joinpath(@__DIR__, "build", filename)) - contents = replace(contents, replacement) - open(f -> write(f, contents), joinpath(@__DIR__, "build", filename), "w") -end - -replace_in_doc("index.html", "blob/master/docs/src/index.md" => "") -replace_in_doc( - joinpath("howToContribute", "index.html"), - "blob/master/docs/src/howToContribute.md" => "blob/master/.github/CONTRIBUTING.md", -) - # clean up examples -- we do not need to deploy all the stuff that was # generated in the process # @@ -142,25 +77,10 @@ for (root, dirs, files) in walkdir(joinpath(@__DIR__, "build", "examples")) end end -# remove the template files -rm(joinpath(@__DIR__, "build", "index.md.template")) -rm(joinpath(@__DIR__, "build", "quickstart.md.template")) - -# Binder actually has 1.6.2 kernel (seen in October 2021), but for whatever -# reason it's called julia-1.1. Don't ask me. -for ipynb in joinpath.(@__DIR__, "build", "examples", ipynb_names) - @info "changing julia version to 1.1 in `$ipynb'" - js = JSON.parsefile(ipynb) - js["metadata"]["kernelspec"]["name"] = "julia-1.1" - js["metadata"]["kernelspec"]["display_name"] = "Julia 1.1.0" - js["metadata"]["language_info"]["version"] = "1.1.0" - open(f -> JSON.print(f, js), ipynb, "w") -end - # deploy the result deploydocs( - repo = "github.com/$github_repo_slug.git", + repo = "github.com/LCSB-BioCore/COBREXA.jl.git", target = "build", - branch = pages_branch, - devbranch = "develop", + branch = "gh-pages", + devbranch = "master", ) diff --git a/docs/src/concepts.md b/docs/src/concepts.md new file mode 100644 index 000000000..f2279cd36 --- /dev/null +++ b/docs/src/concepts.md @@ -0,0 +1,7 @@ + +# Core concepts and extension guide + +```@contents +Pages = filter(x -> endswith(x, ".md"), readdir("concepts", join=true)) +Depth = 2 +``` diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md new file mode 100644 index 000000000..a00204092 --- /dev/null +++ b/docs/src/concepts/1_screen.md @@ -0,0 +1,268 @@ + +# Screening many model variants + +A major goal of COBREXA.jl is to make exploring of many model variants easy and +fast. + +One main concept that can be utilized for doing that is implemented in the +function [`screen`](@ref), which takes your model, a list of model _variants_ +that you want to explore by some specified _analysis_, and schedules the +analysis of the model variants parallelly on the available distributed workers. + +In its most basic form, the "screening" may use the slightly simplified variant +of [`screen`](@ref) that is called [`screen_variants`](@ref), which works as +follows: + +```julia +m = load_model(ObjectModel, "e_coli_core.json") + +screen_variants( + m, # the model for screening + [ + [], # a variant with no modifications + [with_changed_bound("CO2t", lb = 0, ub = 0)], # disable CO2 transport + [with_changed_bound("O2t", lb = 0, ub = 0)], # disable O2 transport + [with_changed_bound("CO2t", lb = 0, ub = 0), with_changed_bound("O2t", lb = 0, ub = 0)], # disable both transports + ], + m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], +) +``` +The call specifies a model (the `m` that we have loaded) that is being tested, +then a vector of model variants to be created and tested, and then the analysis +that is being run on each variant -- in this case, we find an optimal steady +state of each of the variants, and check out the biomass production rate at +that state. In this particular case, we are checking what will be the effect of +disabling combinations of CO2 transport and O2 transport in the cells. For +that, we get the following result: +``` +4-element Vector{Float64}: + 0.8739215022678488 + 0.46166961413944896 + 0.21166294973372135 + 0.21114065173865518 +``` + +The numbers are the biomass production rates for the specified variants. We can +see that disabling O2 transport really does not help the organism much. + +## Variant specification + +In the above example, we have specified 4 variants, thus the analysis returned +4 different results that correspond with the specifications. Let us have a look +at the precise format of the specification and result. + +Importantly, the `variants` argument is of type `Array{Vector{Any}}`, meaning +that it can be an array of any dimensionality that contains vectors. Each of the +vectors specifies precisely one variant, possibly with more modifications +applied to the model in sequence. + +For example: +- `[]` specifies no modifications at all +- `[with_changed_bound("CO2t", lb=0, ub=10)]` limits the CO2 transport +- `[with_changed_bound("CO2t", lb=0, ub=2), with_changed_bound("O2t", lb=0, ub=100)]` + severely limits the CO2 transport _and_ slightly restricts the transport of + O2 + +!!! note "Variants are single-parameter model-transforming functions" + Because the variants are just generators of single parameter functions + that take the model and return its modified version, you can also use + `identity` to specify a variant that does nothing -- `[identity]` is + perfectly same as `[]` + +The shape of the variants array is important too, because it is precisely +retained in the result (just as with `pmap`). If you pass in a matrix of +variants, you will receive a matrix of analysis results of the same size. That +can be exploited for easily exploring many combinations of possible model +properties. Let's try exploring a "cube" of possible restricted reactions: + +```julia +using IterTools # for cartesian products + +res = screen_variants(m, + [ + # for each variant we restricts 2 reactions + [with_changed_bound(r1, lb=-3, ub=3), with_changed_bound(r2, lb=-1, ub=1)] + + # the reaction pair will be chosen from a cartesian product + for (r1,r2) in product( + ["H2Ot", "CO2t", "O2t", "NH4t"], # of this set of transport reactions + ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], # and this set of exchanges + ) + ], + m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], +) +``` + +As a result, we will receive a full matrix of the biomass productions: +``` +4×4 Matrix{Float64}: + 0.407666 0.454097 0.240106 0.183392 + 0.407666 0.485204 0.24766 0.183392 + 0.314923 0.319654 0.24766 0.183392 + 0.407666 0.485204 0.24766 0.183392 +``` +Notably, this shows that O2 transport and NH4 exchange may be serious +bottlenecks for biomass production. + +For clarity, you may always annotate the result by zipping it with the +specification structure you have used and collecting the data: +``` julia +collect(zip( + product( + ["H2Ot", "CO2t", "O2t", "NH4t"], + ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], + ), + res, +)) +``` +...which gives the following annotated result: +``` +4×4 Matrix{Tuple{Tuple{String, String}, Float64}}: + (("H2Ot", "EX_h2o_e"), 0.407666) (("H2Ot", "EX_co2_e"), 0.454097) (("H2Ot", "EX_o2_e"), 0.240106) (("H2Ot", "EX_nh4_e"), 0.183392) + (("CO2t", "EX_h2o_e"), 0.407666) (("CO2t", "EX_co2_e"), 0.485204) (("CO2t", "EX_o2_e"), 0.24766) (("CO2t", "EX_nh4_e"), 0.183392) + (("O2t", "EX_h2o_e"), 0.314923) (("O2t", "EX_co2_e"), 0.319654) (("O2t", "EX_o2_e"), 0.24766) (("O2t", "EX_nh4_e"), 0.183392) + (("NH4t", "EX_h2o_e"), 0.407666) (("NH4t", "EX_co2_e"), 0.485204) (("NH4t", "EX_o2_e"), 0.24766) (("NH4t", "EX_nh4_e"), 0.183392) +``` + +This may be easily used for e.g. scrutinizing all possible reaction pairs, to +find the ones that are redundant and not. + +There are many other variant "specifications" to choose from. You may use +[`with_added_reactions`](@ref), [`with_removed_reactions`](@ref), +[`with_removed_metabolites`](@ref), and others. Function reference contains a +complete list; as a convention, names of the specifications all start with +`with_`. + +## Writing custom variant functions + +It is actually very easy to create custom specifications that do any +modification that you can implement, to be later used with +[`screen_variants`](@ref) and [`screen`](@ref). + +Generally, the "specifications" are supposed to return a _function_ that +creates a modified copy of the model. The copy of the model may be shallow, but +the functions should always prevent modifying the original model structure -- +`screen` is keeping a single copy of the original model at each worker to +prevent unnecessary bulk data transport, and if that is changed in-place, all +following analyses of the model will work on inconsistent data, usually +returning wrong results (even randomly changing ones, because of the +asynchronous nature of [`screen`](@ref) execution). + +Despite of that, writing a modification is easy. The simplest modification that +"does nothing" (isomorphic to standard `identity`) can be formatted as follows: + +```julia +with_no_change = model -> model +``` + +The modifications may change the model, provided it is copied properly. The +following modification will remove a reaction called "O2t", effectively +removing the possibility to transport oxygen. We require a specific type of +model where this change is easy to perform (generally, not all variants may be +feasible on all model types). + +```julia +with_disabled_oxygen_transport = (model::ObjectModel) -> begin + + # make "as shallow as possible" copy of the `model`. + # Utilizing `deepcopy` is also possible, but inefficient. + new_model = copy(model) + new_model.reactions = copy(model.reactions) + + # remove the O2 transport from the model copy + delete!(new_model.reactions, "O2t") + + return new_model #return the newly created variant +end +``` + +Finally, the whole definition may be parameterized as a normal function. The +following variant removes any user-selected reaction: + +```julia +with_disabled_reaction(reaction_id) = (model::ObjectModel) -> begin + new_model = copy(model) + new_model.reactions = copy(model.reactions) + delete!(new_model.reactions, reaction_id) # use the parameter from the specification + return new_model +end +``` + +In turn, these variants can be used in [`screen_variants`](@ref) just as we +used [`with_changed_bound`](@ref) above: + +```julia +screen_variants( + m, # the model for screening + [ + [with_no_change], + [with_disabled_oxygen_transport], + [with_disabled_reaction("NH4t")], + ], + m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], +) +``` + +That should get you the results for all new variants of the model: +``` +3-element Vector{Float64}: + 0.8739215022674809 + 0.21166294865468896 + 1.2907224478973395e-15 +``` + +!!! warning "Custom variants with distributed processing" + If using distributed evaluation, remember the variant-generating functions + need to be defined on all used workers (generating the variants in parallel + on the workers allows COBREXA to run the screening process very + efficiently, without unnecessary sending of bulk model data). Prefixing the + definition with `@everywhere` is usually sufficient for that purpose. + +## Passing extra arguments to the analysis function + +Some analysis functions may take additional arguments, which you might want to +vary for the analysis. `modifications` argument of +[`flux_balance_analysis_dict`](@ref) is one example of such argument, allowing +you to specify details of the optimization procedure. + +[`screen`](@ref) function allows you to do precisely that -- apart from +`variants`, you may also specify an array of `args` of the same shape as +`variants`, the entries of which will get passed together with the generated +model variants to your specified analysis function. If either of the arguments +is missing (or set to `nothing`), it is defaulted to "no modifications" or "no +arguments". + +The arguments _must_ be tuples; you may need to make 1-tuples from your data +(e.g. using `(value,)`) if you want to pass just a single argument. + +Let's try to use that functionality for trying to find a sufficient amount of +iterations needed for Tulip solver to find a feasible solution: + +```julia +screen(m, + args = [(i,) for i in 5:15], # the iteration counts, packed in 1-tuples + analysis = (m,a) -> # `args` elements get passed as the extra parameter here + flux_balance_analysis_vec(m, + Tulip.Optimizer; + modifications=[modify_optimizer_attribute("IPM_IterationsLimit", a)], + ), +) +``` + +From the result, we can see that Tulip would need at least 14 iterations to +find a feasible region: + +``` +11-element Vector{Union{Nothing, Vector{Float64}}}: + nothing + nothing + nothing + nothing + nothing + nothing + nothing + nothing + nothing + [7.47738193404817, 1.8840414375838503e-8, 4.860861010127701, -16.023526104614593, … ] + [7.47738193404817, 1.8840414375838503e-8, 4.860861010127701, -16.023526104614593, … ] +``` diff --git a/docs/src/distributed/4_serialized.md b/docs/src/distributed/4_serialized.md deleted file mode 100644 index ea31ba902..000000000 --- a/docs/src/distributed/4_serialized.md +++ /dev/null @@ -1,50 +0,0 @@ - -# Faster model distribution - -When working with a large model, it may happen that the time required to send -the model to the worker nodes takes a significant portion of the total -computation time. - -You can use Julia serialization to prevent this. Because the shared filesystem -is a common feature of most HPC installations around, you can very easily -utilize it to broadcast a serialized version of the model to all worker nodes. -In COBREXA, that functionality is wrapped in a [`Serialized`](@ref) model type, which provides a tiny abstraction around this functionality: - -- you call [`serialize_model`](@ref) before you start your analysis to place - the model to the shared storage (and, possibly, free the RAM required to hold - the model) -- the freshly created [`Serialized`](@ref) model type is tiny -- it only stores - the file name where the model data can be found -- all analysis functions automatically call [`precache!`](@ref) on the model to - get the actual model data loaded before the model is used, which - transparently causes the loading from the shared media, and thus the fast - distribution - -The use is pretty straightforward. After you have your model loaded, you simply -convert it to the small serialized form: - -```julia -model = load_model(...) -# ... prepare the model ... - -cachefile = tempname(".") -sm = serialize_model(model, cachefile) -``` - -Then, the analysis functions is run as it would be with a "normal" model: -```julia -screen(sm, ...) -``` - -After the analysis is done, it is useful to remove the temporary file: -```julia -rm(cachefile) -``` - -!!! warn "Caveats of working with temporary files" - Always ensure that the temporary filenames are unique -- if there are two - jobs running in parallel and both use the same filename to cache and - distribute a model, both are likely going to crash, and almost surely - produce wrong results. At the same time, avoid creating the temporary - files in the default location of `/tmp/*`, as the temporary folder is - local, thus seldom shared between HPC nodes. diff --git a/docs/src/examples.md b/docs/src/examples.md new file mode 100644 index 000000000..d7ba920f1 --- /dev/null +++ b/docs/src/examples.md @@ -0,0 +1,7 @@ + +# Examples + +```@contents +Pages = filter(x -> endswith(x, ".md"), readdir("examples", join=true)) +Depth = 2 +``` diff --git a/docs/src/examples/01-loading-and-saving.jl b/docs/src/examples/01-loading-and-saving.jl new file mode 100644 index 000000000..584cee8cb --- /dev/null +++ b/docs/src/examples/01-loading-and-saving.jl @@ -0,0 +1,6 @@ + +# # Loading and saving models! + +using COBREXA + +# TODO: download the models into a single directory that can get cached. Probably best have a fake mktempdir(). diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl new file mode 100644 index 000000000..678930970 --- /dev/null +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -0,0 +1,6 @@ + +# # Flux balance analysis + +using COBREXA + +# TODO: run FBA on a FBC model diff --git a/docs/src/functions.md b/docs/src/reference.md similarity index 100% rename from docs/src/functions.md rename to docs/src/reference.md diff --git a/docs/src/reference/analysis.md b/docs/src/reference/analysis.md new file mode 100644 index 000000000..f37bbaa36 --- /dev/null +++ b/docs/src/reference/analysis.md @@ -0,0 +1,22 @@ +# Analysis functions + +## Common analysis functions + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("analysis", file), readdir("../src/analysis")) +``` + +## Sampling + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("analysis", "sampling", file), readdir("../src/analysis/sampling")) +``` + +## Analysis modifiers + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("analysis", "modifications", file), readdir("../src/analysis/modifications")) +``` diff --git a/docs/src/reference/base.md b/docs/src/reference/base.md new file mode 100644 index 000000000..eff4308b7 --- /dev/null +++ b/docs/src/reference/base.md @@ -0,0 +1,6 @@ +# Base functions + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", file), readdir("../src/base")) +``` diff --git a/docs/src/reference/io.md b/docs/src/reference/io.md new file mode 100644 index 000000000..c7d908914 --- /dev/null +++ b/docs/src/reference/io.md @@ -0,0 +1,15 @@ +# Input and output + +## File I/O and serialization + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("io", file), readdir("../src/io")) +``` + +## Pretty printing + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("io", "show", file), readdir("../src/io/show")) +``` diff --git a/docs/src/reference/reconstruction.md b/docs/src/reference/reconstruction.md new file mode 100644 index 000000000..f85bddaf9 --- /dev/null +++ b/docs/src/reference/reconstruction.md @@ -0,0 +1,15 @@ +# Model construction functions + +## Functions for changing the models + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("reconstruction", file), readdir("../src/reconstruction")) +``` + +## Variant specifiers + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("reconstruction", "modifications", file), readdir("../src/reconstruction/modifications")) +``` diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md new file mode 100644 index 000000000..c952444c3 --- /dev/null +++ b/docs/src/reference/types.md @@ -0,0 +1,20 @@ +# Types + +## Base types + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "types", "abstract", file), readdir("../src/base/types/abstract")) +``` + +## Model types and contents +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "types", file), readdir("../src/base/types")) +``` + +## Model type wrappers +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "types", "wrappers", file), readdir("../src/base/types/wrappers")) +``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md new file mode 100644 index 000000000..b38635b54 --- /dev/null +++ b/docs/src/reference/utils.md @@ -0,0 +1,22 @@ +# Utilities + +## Helper functions + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "utils", file), readdir("../src/base/utils")) +``` + +## Macro-generated functions and internal helpers + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "macros", file), readdir("../src/base/macros")) +``` + +## Logging and debugging helpers + +```@autodocs +Modules = [COBREXA] +Pages = map(file -> joinpath("base", "logging", file), readdir("../src/base/logging")) +``` diff --git a/test/coverage/coverage-summary.jl b/test/coverage/coverage-summary.jl deleted file mode 100644 index 0801ca4b1..000000000 --- a/test/coverage/coverage-summary.jl +++ /dev/null @@ -1,12 +0,0 @@ - -using Coverage - -cd(joinpath(@__DIR__, "..", "..")) do - processed = process_folder() - covered_lines, total_lines = get_summary(processed) - percentage = covered_lines / total_lines * 100 - println("($(percentage)%) covered") -end - -# submit the report to codecov.io -Codecov.submit_local(Codecov.process_folder(), pwd(), slug = "LCSB-BioCore/COBREXA.jl") diff --git a/test/runtests.jl b/test/runtests.jl index 076f05a41..e5a8735c6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,8 +20,8 @@ function run_test_file(path...) print_timing(fn, t) end -function run_doc(path...) - run_test_file("..", "docs", "src", path...) +function run_doc_ex(path...) + run_test_file("..", "docs", "src", "examples", path...) end # set up the workers for Distributed, so that the tests that require more @@ -35,8 +35,8 @@ run_test_file("data_downloaded.jl") # import base files @testset "COBREXA test suite" begin - run_doc("loading-and-saving.jl") - run_doc("flux-balance-analysis.jl") + run_doc("01-loading-and-saving.jl") + run_doc("02-flux-balance-analysis.jl") run_test_file("aqua.jl") end From b15fb97ab68e538430e7056e7c3f40eed61512b1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 12:20:42 +0100 Subject: [PATCH 345/531] the package is formatted and loads --- Project.toml | 2 +- src/COBREXA.jl | 7 -- src/analysis/flux_balance_analysis.jl | 54 ----------- src/analysis/modifications/generic.jl | 94 ------------------- src/analysis/modifications/knockout.jl | 44 --------- src/analysis/modifications/optimizer.jl | 41 -------- .../parsimonious_flux_balance_analysis.jl | 94 ------------------- src/builders/genes.jl | 21 +++-- src/builders/objectives.jl | 17 ++-- src/io.jl | 6 +- 10 files changed, 22 insertions(+), 358 deletions(-) delete mode 100644 src/analysis/flux_balance_analysis.jl delete mode 100644 src/analysis/modifications/generic.jl delete mode 100644 src/analysis/modifications/knockout.jl delete mode 100644 src/analysis/modifications/optimizer.jl delete mode 100644 src/analysis/parsimonious_flux_balance_analysis.jl diff --git a/Project.toml b/Project.toml index aec146e80..dc4027e93 100644 --- a/Project.toml +++ b/Project.toml @@ -34,4 +34,4 @@ Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" [targets] -test = ["Aqua", "Clarabel", "Downloads", "GLPK", "SHA", "Test", "Tulip"] +test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 247080114..5baaa18cc 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -16,11 +16,4 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") -include("analysis/flux_balance_analysis.jl") -include("analysis/parsimonious_flux_balance_analysis.jl") - -include("analysis/modifications/generic.jl") -include("analysis/modifications/knockout.jl") -include("analysis/modifications/optimizer.jl") - end # module COBREXA diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl deleted file mode 100644 index f0611fe22..000000000 --- a/src/analysis/flux_balance_analysis.jl +++ /dev/null @@ -1,54 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run flux balance analysis (FBA) on the `model` optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization problem: -``` -max cᵀx -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat -Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more -information. - -The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer` - -Optionally, you may specify one or more modifications to be applied to the -model before the analysis, such as [`modify_optimizer_attribute`](@ref), -[`change_objective`](@ref), and [`modify_sense`](@ref). - -Returns an optimized `JuMP` model. - -# Example -``` -model = load_model("e_coli_core.json") -solution = flux_balance_analysis(model, GLPK.optimizer) -value.(solution[:x]) # extract flux steady state from the optimizer - -biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") - -modified_solution = flux_balance_analysis(model, GLPK.optimizer; - modifications=[modify_objective(biomass_reaction_id)]) -``` -""" -function flux_balance_analysis(model::AbstractMetabolicModel, optimizer; modifications = []) - opt_model = make_optimization_model(model, optimizer) - - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - ModelWithResult(model, opt_model) -end - -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`flux_balance_analysis`](@ref). -""" -flux_balance_analysis(optimizer; modifications = []) = - model -> flux_balance_analysis(model, optimizer; modifications) diff --git a/src/analysis/modifications/generic.jl b/src/analysis/modifications/generic.jl deleted file mode 100644 index 5936276fc..000000000 --- a/src/analysis/modifications/generic.jl +++ /dev/null @@ -1,94 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Limit the objective value to `tolerance`-times the current objective value, as -with [`objective_bounds`](@ref). -""" -constrain_objective_value(tolerance) = - (_, opt_model) -> begin - lambda_min, lambda_max = objective_bounds(tolerance)(objective_value(opt_model)) - old_objective = objective_function(opt_model) - @constraint(opt_model, lambda_min <= sum(old_objective) <= lambda_max) - end - -""" -$(TYPEDSIGNATURES) - -Change the lower and upper bounds (`lower_bound` and `upper_bound` respectively) -of reaction `id` if supplied. -""" -modify_constraint(id::String; lower_bound = nothing, upper_bound = nothing) = - (model, opt_model) -> begin - ind = first(indexin([id], variables(model))) - isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!( - ind, - opt_model, - lower_bound = lower_bound, - upper_bound = upper_bound, - ) - end - -""" -$(TYPEDSIGNATURES) - -Change the lower and upper bounds (`lower_bounds` and `upper_bounds` -respectively) of reactions in `ids` if supplied. -""" -modify_constraints( - ids::Vector{String}; - lower_bounds = fill(nothing, length(ids)), - upper_bounds = fill(nothing, length(ids)), -) = - (model, opt_model) -> begin - for (id, lb, ub) in zip(ids, lower_bounds, upper_bounds) - ind = first(indexin([id], variables(model))) - isnothing(ind) && throw(DomainError(id, "No matching reaction was found.")) - set_optmodel_bound!(ind, opt_model, lower_bound = lb, upper_bound = ub) - end - end - -""" -$(TYPEDSIGNATURES) - -Modification that changes the objective function used in a constraint based -analysis function. `new_objective` can be a single reaction identifier, or an -array of reactions identifiers. - -Optionally, the objective can be weighted by a vector of `weights`, and a -optimization `sense` can be set to either `MAX_SENSE` or `MIN_SENSE`. Note, the -`sense` argument is a JuMP constant, thus you need to import JUMP for the name -to be available. -""" -modify_objective( - new_objective::Union{String,Vector{String}}; - weights = [], - sense = MAX_SENSE, -) = - (model, opt_model) -> begin - - # Construct objective_indices array - if typeof(new_objective) == String - objective_indices = indexin([new_objective], variables(model)) - else - objective_indices = - [first(indexin([rxnid], variables(model))) for rxnid in new_objective] - end - - any(isnothing.(objective_indices)) && throw( - DomainError(new_objective, "No matching reaction found for one or more ids."), - ) - - # Initialize weights - opt_weights = spzeros(n_variables(model)) - - isempty(weights) && (weights = ones(length(objective_indices))) # equal weights - - for (j, i) in enumerate(objective_indices) - opt_weights[i] = weights[j] - end - - v = opt_model[:x] - @objective(opt_model, sense, sum(opt_weights[i] * v[i] for i in objective_indices)) - end diff --git a/src/analysis/modifications/knockout.jl b/src/analysis/modifications/knockout.jl deleted file mode 100644 index 6612ecd24..000000000 --- a/src/analysis/modifications/knockout.jl +++ /dev/null @@ -1,44 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -A modification that zeroes the bounds of all reactions that would be knocked -out by the combination of specified genes (effectively disabling the -reactions). - -A slightly counter-intuitive behavior may occur if knocking out multiple genes: -Because this only changes the reaction bounds, multiple gene knockouts _must_ -be specified in a single call to [`knockout`](@ref), because the modifications -have no way to remember which genes are already knocked out and which not. - -In turn, having a reaction that can be catalyzed either by Gene1 or by Gene2, -specifying `modifications = [knockout(["Gene1", "Gene2"])]` does indeed disable -the reaction, but `modifications = [knockout("Gene1"), knockout("Gene2")]` does -_not_ disable the reaction (although reactions that depend either only on Gene1 -or only on Gene2 are disabled). -""" -knockout(gene_ids::Vector{String}) = - (model, optmodel) -> _do_knockout(model, optmodel, gene_ids) - -""" -$(TYPEDSIGNATURES) - -A helper variant of [`knockout`](@ref) for a single gene. -""" -knockout(gene_id::String) = knockout([gene_id]) - -""" -$(TYPEDSIGNATURES) - -Internal helper for knockouts on generic AbstractMetabolicModels. This can be -overloaded so that the knockouts may work differently (more efficiently) with -other models. -""" -function _do_knockout(model::AbstractMetabolicModel, opt_model, gene_ids::Vector{String}) - #TODO this should preferably work on reactions. Make it a wrapper. - KOs = Set(gene_ids) - for (ridx, rid) in enumerate(variables(model)) - if eval_reaction_gene_association(model, rid, falses = KOs) == false # also tests for nothing! - set_optmodel_bound!(ridx, opt_model, lower_bound = 0, upper_bound = 0) - end - end -end diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer.jl deleted file mode 100644 index 67038eca2..000000000 --- a/src/analysis/modifications/optimizer.jl +++ /dev/null @@ -1,41 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Change the objective sense of optimization. -Possible arguments are `MAX_SENSE` and `MIN_SENSE`. - -If you want to change the objective and sense at the same time, use -[`change_objective`](@ref) instead to do both at once. -""" -modify_sense(objective_sense) = - (_, opt_model) -> set_objective_sense(opt_model, objective_sense) - -""" -$(TYPEDSIGNATURES) - -Change the JuMP optimizer used to run the optimization. - -This may be used to try different approaches for reaching the optimum, and in -problems that may require different optimizers for different parts, such as the -[`parsimonious_flux_balance_analysis`](@ref). -""" -modify_optimizer(optimizer) = (_, opt_model) -> set_optimizer(opt_model, optimizer) - -""" -$(TYPEDSIGNATURES) - -Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer -to the JuMP documentation and the documentation of the specific optimizer for -usable keys and values. -""" -modify_optimizer_attribute(attribute_key, value) = - (_, opt_model) -> set_optimizer_attribute(opt_model, attribute_key, value) - -""" - silence - -Modification that disable all output from the JuMP optimizer (shortcut for -`set_silent` from JuMP). -""" -const silence = (_, opt_model) -> set_silent(opt_model) diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl deleted file mode 100644 index a975d7b38..000000000 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ /dev/null @@ -1,94 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA -runs two consecutive optimization problems. The first is traditional FBA: -``` -max cᵀx = μ -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -And the second is a quadratic optimization problem: -``` -min Σᵢ xᵢ² -s.t. S x = b - xₗ ≤ x ≤ xᵤ - μ = μ⁰ -``` -Where the optimal solution of the FBA problem, μ⁰, has been added as an -additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, -Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, -Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, -Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data -from evolved E. coli are consistent with computed optimal growth from -genome-scale models. Molecular Systems Biology, 6. 390. doi: -accession:10.1038/msb.2010.47" for more details. - -pFBA gets the model optimum by standard FBA (using -[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then -finds a minimal total flux through the model that still satisfies the (slightly -relaxed) optimum. This is done using a quadratic problem optimizer. If the -original optimizer does not support quadratic optimization, it can be changed -using the callback in `qp_modifications`, which are applied after the FBA. See -the documentation of [`flux_balance_analysis`](@ref) for usage examples of -modifications. - -The optimum relaxation sequence can be specified in `relax` parameter, it -defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original -bound. - -Returns an optimized model that contains the pFBA solution (or an unsolved model -if something went wrong). - -# Performance - -This implementation attempts to save time by executing all pFBA steps on a -single instance of the optimization model problem, trading off possible -flexibility. For slightly less performant but much more flexible use, one can -construct parsimonious models directly using -[`with_parsimonious_objective`](@ref). - -# Example -``` -model = load_model("e_coli_core.json") -parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec -``` -""" -function parsimonious_flux_balance_analysis( - model::AbstractMetabolicModel, - optimizer; - modifications = [], - qp_modifications = [], - relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], -) - # Run FBA - opt_model = flux_balance_analysis(model, optimizer; modifications) |> result - is_solved(opt_model) || return ModelWithResult(model, opt_model) # FBA failed - - # get the objective - Z = objective_value(opt_model) - original_objective = objective_function(opt_model) - - # prepare the model for pFBA - for mod in qp_modifications - mod(model, opt_model) - end - - # add the minimization constraint for total flux - v = opt_model[:x] # fluxes - @objective(opt_model, Min, sum(dot(v, v))) - - for rb in relax_bounds - lb, ub = objective_bounds(rb)(Z) - @models_log @info "pFBA step relaxed to [$lb,$ub]" - @constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) - - optimize!(opt_model) - is_solved(opt_model) && break - - delete(opt_model, pfba_constraint) - unregister(opt_model, :pfba_constraint) - end - - return ModelWithResult(model, opt_model) -end diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 9113ce3fe..2996a08f9 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,18 +1,19 @@ -knockout_constraints(;fluxes::ConstraintTree, knockout_test) = - C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) - ) +knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for + rxn in keys(fluxes) if knockout_test(rxn) +) export knockout_constraints -fbc_gene_knockout_constraints(;fluxes::ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; +fbc_gene_knockout_constraints(; + fluxes::C.ConstraintTree, + genes, + fbc_model::A.AbstractFBCModel, +) = knockout_constraints(; fluxes, - knockout_test = - rxn -> !A.reaction_gene_products_available( - rxn, - g -> not (g in genes) - ), + knockout_test = rxn -> + !A.reaction_gene_products_available(rxn, g -> not(g in genes)), ) export fbc_gene_knockout_constraints diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 2067f2ec1..93cca18ce 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,20 +1,17 @@ -sum_objectve(x) = - C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) - -sum_objective(x::ConstraintTree) = squared_error_objective(values(x)) +sum_objectve(x) = C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) + +sum_objective(x::C.ConstraintTree) = squared_error_objective(values(x)) squared_sum_objective(x) = C.Constraint(sum(C.squared.(C.value.(x)), init = zero(C.QuadraticValue))) -squared_sum_objective(x::ConstraintTree) = squared_sum_objective(values(x)) +squared_sum_objective(x::C.ConstraintTree) = squared_sum_objective(values(x)) -squared_error_objective(constraints::ConstraintTree, target) = C.Constraint( - sum( - let tmp = (C.value(c) - target[k]) +squared_error_objective(constraints::C.ConstraintTree, target) = C.Constraint( + sum(let tmp = (C.value(c) - target[k]) tmp * tmp - end for (k, c) in constraints if haskey(target, k) - ), + end for (k, c) in constraints if haskey(target, k)), ) # TODO use `mergewith` to do this reasonably (add it to ConstraintTrees) diff --git a/src/io.jl b/src/io.jl index 0e0030b81..f517bdb58 100644 --- a/src/io.jl +++ b/src/io.jl @@ -8,7 +8,7 @@ Load a FBC model representation while guessing the correct model type. Uses This overload almost always involves a search over types; do not use it in environments where performance is critical. """ -function load_fbc_model(path::String) where A<:AbstractFBCModel +function load_fbc_model(path::String) where {A<:AbstractFBCModel} A.load(path) end @@ -17,7 +17,7 @@ end Load a FBC model representation. Uses `AbstractFBCModels.load`. """ -function load_fbc_model(model_type::Type{A}, path::String) where A<:AbstractFBCModel +function load_fbc_model(model_type::Type{A}, path::String) where {A<:AbstractFBCModel} A.load(model_type, path) end @@ -28,7 +28,7 @@ export load_fbc_model Save a FBC model representation. Uses `AbstractFBCModels.save`. """ -function save_fbc_model(model::A, path::String) where A<:AbstractFBCModel +function save_fbc_model(model::A, path::String) where {A<:AbstractFBCModel} A.save(model, path) end From 77a9dbf4b1ca584c95b7fb63b9bc09622d341e5e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 12:22:43 +0100 Subject: [PATCH 346/531] make the tests pass (empty tho) --- test/runtests.jl | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e5a8735c6..0a4324e65 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,14 +6,9 @@ using Distributed import AbstractFBCModels as A using GLPK # for MILPs -# tolerance for comparing analysis results (should be a bit bigger than the -# error tolerance in computations) -TEST_TOLERANCE = 10 * COBREXA.Internal.constants.tolerance -QP_TEST_TOLERANCE = 1e-2 # for Clarabel - +# helper functions for running tests en masse print_timing(fn, t) = @info "$(fn) done in $(round(t; digits = 2))s" -# helper functions for running tests en masse function run_test_file(path...) fn = joinpath(path...) t = @elapsed include(fn) @@ -35,8 +30,8 @@ run_test_file("data_downloaded.jl") # import base files @testset "COBREXA test suite" begin - run_doc("01-loading-and-saving.jl") - run_doc("02-flux-balance-analysis.jl") + run_doc_ex("01-loading-and-saving.jl") + run_doc_ex("02-flux-balance-analysis.jl") run_test_file("aqua.jl") end From ab8dfda8bc3959aad9b4138408444069589a90b4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 11:52:43 +0100 Subject: [PATCH 347/531] ignore models --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 46e71d415..caac77fde 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ Manifest.toml # ignore container files *.sif + +# ignore models +*.xml +*.json +*.mat \ No newline at end of file From 4d1d8d9b303a6f0da65a1c86d9d80497090a6dfb Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 10 Nov 2023 12:38:16 +0100 Subject: [PATCH 348/531] docs kinda build (but 10000 doccheck fails) --- docs/Project.toml | 4 +-- docs/src/reference.md | 47 ++-------------------------- docs/src/reference/analysis.md | 22 ------------- docs/src/reference/base.md | 6 ---- docs/src/reference/builders.md | 21 +++++++++++++ docs/src/reference/io.md | 12 ++----- docs/src/reference/reconstruction.md | 15 --------- docs/src/reference/solver.md | 7 +++++ docs/src/reference/types.md | 17 ++-------- docs/src/reference/utils.md | 22 ------------- 10 files changed, 37 insertions(+), 136 deletions(-) delete mode 100644 docs/src/reference/analysis.md delete mode 100644 docs/src/reference/base.md create mode 100644 docs/src/reference/builders.md delete mode 100644 docs/src/reference/reconstruction.md create mode 100644 docs/src/reference/solver.md delete mode 100644 docs/src/reference/utils.md diff --git a/docs/Project.toml b/docs/Project.toml index 1f523ccd8..5b3dcde68 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -14,5 +14,5 @@ Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" [compat] -Documenter = "0.26" -Literate = "2.8" +Documenter = "1" +Literate = "2" diff --git a/docs/src/reference.md b/docs/src/reference.md index add3bf10f..f0b01a550 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,47 +1,6 @@ # API reference -## Helper types - -```@autodocs -Modules = [COBREXA] -Pages = ["src/types.jl"] -``` - -## Model loading and saving - -```@autodocs -Modules = [COBREXA] -Pages = ["src/io.jl"] -``` - -## Constraint system building - -```@autodocs -Modules = [COBREXA] -Pages = ["src/builders/core.jl"] -``` - -### Genetic constraints - -```@autodocs -Modules = [COBREXA] -Pages = ["src/builders/genes.jl"] +```@contents +Pages = ["reference/types.md", "reference/io.md", "reference/builders.md", "reference/solver.md"] +Depth = 2 ``` - -### Objective function helpers - -```@autodocs -Modules = [COBREXA] -Pages = ["src/builders/objectives.jl"] -``` - -## Solver interface - -```@autodocs -Modules = [COBREXA] -Pages = ["src/solver.jl"] -``` - -## Analysis functions - -## Distributed analysis diff --git a/docs/src/reference/analysis.md b/docs/src/reference/analysis.md deleted file mode 100644 index f37bbaa36..000000000 --- a/docs/src/reference/analysis.md +++ /dev/null @@ -1,22 +0,0 @@ -# Analysis functions - -## Common analysis functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", file), readdir("../src/analysis")) -``` - -## Sampling - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "sampling", file), readdir("../src/analysis/sampling")) -``` - -## Analysis modifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("analysis", "modifications", file), readdir("../src/analysis/modifications")) -``` diff --git a/docs/src/reference/base.md b/docs/src/reference/base.md deleted file mode 100644 index eff4308b7..000000000 --- a/docs/src/reference/base.md +++ /dev/null @@ -1,6 +0,0 @@ -# Base functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", file), readdir("../src/base")) -``` diff --git a/docs/src/reference/builders.md b/docs/src/reference/builders.md new file mode 100644 index 000000000..26e230364 --- /dev/null +++ b/docs/src/reference/builders.md @@ -0,0 +1,21 @@ + +## Constraint system building + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/core.jl"] +``` + +### Genetic constraints + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/genes.jl"] +``` + +### Objective function helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/objectives.jl"] +``` diff --git a/docs/src/reference/io.md b/docs/src/reference/io.md index c7d908914..5c494ea0a 100644 --- a/docs/src/reference/io.md +++ b/docs/src/reference/io.md @@ -1,15 +1,7 @@ -# Input and output -## File I/O and serialization +# Model loading and saving ```@autodocs Modules = [COBREXA] -Pages = map(file -> joinpath("io", file), readdir("../src/io")) -``` - -## Pretty printing - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("io", "show", file), readdir("../src/io/show")) +Pages = ["src/io.jl"] ``` diff --git a/docs/src/reference/reconstruction.md b/docs/src/reference/reconstruction.md deleted file mode 100644 index f85bddaf9..000000000 --- a/docs/src/reference/reconstruction.md +++ /dev/null @@ -1,15 +0,0 @@ -# Model construction functions - -## Functions for changing the models - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", file), readdir("../src/reconstruction")) -``` - -## Variant specifiers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("reconstruction", "modifications", file), readdir("../src/reconstruction/modifications")) -``` diff --git a/docs/src/reference/solver.md b/docs/src/reference/solver.md new file mode 100644 index 000000000..c01aabf78 --- /dev/null +++ b/docs/src/reference/solver.md @@ -0,0 +1,7 @@ + +# Solver interface + +```@autodocs +Modules = [COBREXA] +Pages = ["src/solver.jl"] +``` diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md index c952444c3..bb70365c6 100644 --- a/docs/src/reference/types.md +++ b/docs/src/reference/types.md @@ -1,20 +1,7 @@ -# Types -## Base types +# Helper types ```@autodocs Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "abstract", file), readdir("../src/base/types/abstract")) -``` - -## Model types and contents -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", file), readdir("../src/base/types")) -``` - -## Model type wrappers -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "types", "wrappers", file), readdir("../src/base/types/wrappers")) +Pages = ["src/types.jl"] ``` diff --git a/docs/src/reference/utils.md b/docs/src/reference/utils.md deleted file mode 100644 index b38635b54..000000000 --- a/docs/src/reference/utils.md +++ /dev/null @@ -1,22 +0,0 @@ -# Utilities - -## Helper functions - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "utils", file), readdir("../src/base/utils")) -``` - -## Macro-generated functions and internal helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "macros", file), readdir("../src/base/macros")) -``` - -## Logging and debugging helpers - -```@autodocs -Modules = [COBREXA] -Pages = map(file -> joinpath("base", "logging", file), readdir("../src/base/logging")) -``` From 7d99991d2e4cf56c58c6796089e0f796a1ed02c9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:15:35 +0100 Subject: [PATCH 349/531] get cobrexa to load --- Project.toml | 2 + docs/Project.toml | 9 +- docs/src/examples/02-flux-balance-analysis.jl | 4 + src/COBREXA.jl | 3 + src/analysis/flux_balance_analysis.jl | 54 +++++++++++ .../parsimonious_flux_balance_analysis.jl | 92 +++++++++++++++++++ src/builders/genes.jl | 21 ++--- src/builders/objectives.jl | 8 +- 8 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 src/analysis/flux_balance_analysis.jl create mode 100644 src/analysis/parsimonious_flux_balance_analysis.jl diff --git a/Project.toml b/Project.toml index dc4027e93..24409afba 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,8 @@ Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [targets] test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] diff --git a/docs/Project.toml b/docs/Project.toml index 1f523ccd8..528ff1adf 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,15 +1,10 @@ [deps] +AbstractFBCModels = "5a4f3dfa-1789-40f8-8221-69268c29937c" COBREXA = "babc4406-5200-4a30-9033-bf5ae714c842" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" -Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" -ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Escher = "8cc96de1-1b23-48cb-9272-618d67962629" -GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 678930970..8d36fd1a9 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -2,5 +2,9 @@ # # Flux balance analysis using COBREXA +import AbstractFBCModels as A +import JSONFBCModels as J # TODO: run FBA on a FBC model + +model diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 5baaa18cc..a223a25fb 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -16,4 +16,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") +include("analysis/flux_balance_analysis.jl") +include("analysis/parsimonious_flux_balance_analysis.jl") + end # module COBREXA diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl new file mode 100644 index 000000000..4e48c9a19 --- /dev/null +++ b/src/analysis/flux_balance_analysis.jl @@ -0,0 +1,54 @@ +""" +$(TYPEDSIGNATURES) + +Run flux balance analysis (FBA) on the `model` optionally specifying +`modifications` to the problem. Basically, FBA solves this optimization problem: +``` +max cᵀx +s.t. S x = b + xₗ ≤ x ≤ xᵤ +``` +See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat +Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more +information. + +The `optimizer` must be set to a `JuMP`-compatible optimizer, such as +`GLPK.Optimizer` or `Tulip.Optimizer` + +Optionally, you may specify one or more modifications to be applied to the +model before the analysis, such as [`modify_optimizer_attribute`](@ref), +[`change_objective`](@ref), and [`modify_sense`](@ref). + +Returns an optimized `JuMP` model. + +# Example +``` +model = load_model("e_coli_core.json") +solution = flux_balance_analysis(model, GLPK.optimizer) +value.(solution[:x]) # extract flux steady state from the optimizer + +biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") + +modified_solution = flux_balance_analysis(model, GLPK.optimizer; + modifications=[modify_objective(biomass_reaction_id)]) +``` +""" +function flux_balance_analysis(model::C.ConstraintTree, optimizer; modifications = []) + opt_model = make_optimization_model(model, optimizer) + + for mod in modifications + mod(model, opt_model) + end + + optimize!(opt_model) + + ModelWithResult(model, opt_model) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`flux_balance_analysis`](@ref). +""" +flux_balance_analysis(optimizer; modifications = []) = + model -> flux_balance_analysis(model, optimizer; modifications) diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl new file mode 100644 index 000000000..9e5405fe9 --- /dev/null +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -0,0 +1,92 @@ +""" +$(TYPEDSIGNATURES) + +Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA +runs two consecutive optimization problems. The first is traditional FBA: +``` +max cᵀx = μ +s.t. S x = b + xₗ ≤ x ≤ xᵤ +``` +And the second is a quadratic optimization problem: +``` +min Σᵢ xᵢ² +s.t. S x = b + xₗ ≤ x ≤ xᵤ + μ = μ⁰ +``` +Where the optimal solution of the FBA problem, μ⁰, has been added as an +additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, +Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, +Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, +Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data +from evolved E. coli are consistent with computed optimal growth from +genome-scale models. Molecular Systems Biology, 6. 390. doi: +accession:10.1038/msb.2010.47" for more details. + +pFBA gets the model optimum by standard FBA (using +[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then +finds a minimal total flux through the model that still satisfies the (slightly +relaxed) optimum. This is done using a quadratic problem optimizer. If the +original optimizer does not support quadratic optimization, it can be changed +using the callback in `qp_modifications`, which are applied after the FBA. See +the documentation of [`flux_balance_analysis`](@ref) for usage examples of +modifications. + +The optimum relaxation sequence can be specified in `relax` parameter, it +defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original +bound. + +Returns an optimized model that contains the pFBA solution (or an unsolved model +if something went wrong). + +# Performance + +This implementation attempts to save time by executing all pFBA steps on a +single instance of the optimization model problem, trading off possible +flexibility. For slightly less performant but much more flexible use, one can +construct parsimonious models directly using +[`with_parsimonious_objective`](@ref). + +# Example +``` +model = load_model("e_coli_core.json") +parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec +``` +""" +function parsimonious_flux_balance_analysis( + model::C.ConstraintTree, + optimizer; + modifications = [], + qp_modifications = [], + relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], +) + # Run FBA + opt_model = flux_balance_analysis(model, optimizer; modifications) + J.is_solved(opt_model) || return nothing # FBA failed + + # get the objective + Z = J.objective_value(opt_model) + original_objective = J.objective_function(opt_model) + + # prepare the model for pFBA + for mod in qp_modifications + mod(model, opt_model) + end + + # add the minimization constraint for total flux + v = opt_model[:x] # fluxes + J.@objective(opt_model, Min, sum(dot(v, v))) + + for rb in relax_bounds + # lb, ub = objective_bounds(rb)(Z) + J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) + + J.optimize!(opt_model) + J.is_solved(opt_model) && break + + J.delete(opt_model, pfba_constraint) + J.unregister(opt_model, :pfba_constraint) + end + +end diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 2996a08f9..17263d4ad 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,19 +1,18 @@ -knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for - rxn in keys(fluxes) if knockout_test(rxn) -) +knockout_constraints(;fluxes::C.ConstraintTree, knockout_test) = + C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) + ) export knockout_constraints -fbc_gene_knockout_constraints(; - fluxes::C.ConstraintTree, - genes, - fbc_model::A.AbstractFBCModel, -) = knockout_constraints(; +fbc_gene_knockout_constraints(;fluxes::C.ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; fluxes, - knockout_test = rxn -> - !A.reaction_gene_products_available(rxn, g -> not(g in genes)), + knockout_test = + rxn -> !A.reaction_gene_products_available( + rxn, + g -> not(g in genes) + ), ) export fbc_gene_knockout_constraints diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 93cca18ce..b6d5f7994 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,6 +1,7 @@ -sum_objectve(x) = C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) - +sum_objectve(x) = + C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) + sum_objective(x::C.ConstraintTree) = squared_error_objective(values(x)) squared_sum_objective(x) = @@ -9,7 +10,8 @@ squared_sum_objective(x) = squared_sum_objective(x::C.ConstraintTree) = squared_sum_objective(values(x)) squared_error_objective(constraints::C.ConstraintTree, target) = C.Constraint( - sum(let tmp = (C.value(c) - target[k]) + sum( + let tmp = (C.value(c) - target[k]) tmp * tmp end for (k, c) in constraints if haskey(target, k)), ) From 7e82e479df4620bf8620a2f00349df6fef5057a1 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:23:12 +0100 Subject: [PATCH 350/531] temporarily add JSONFBCModels to make coding easier --- Project.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 24409afba..54e6789c9 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -29,11 +30,11 @@ julia = "1.5" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [targets] test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] From 5090be89cb7cc76ca0b15adbdeabda2874ff6379 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:26:06 +0100 Subject: [PATCH 351/531] load model and create ctmodel --- docs/src/examples/02-flux-balance-analysis.jl | 9 ++++++--- src/COBREXA.jl | 1 + src/builders/core.jl | 19 ++++++++----------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 8d36fd1a9..b21fb525c 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,10 +1,13 @@ # # Flux balance analysis -using COBREXA +import COBREXA as X import AbstractFBCModels as A import JSONFBCModels as J -# TODO: run FBA on a FBC model +model = A.load(J.JSONFBCModel, "e_coli_core.json") + + +ctmodel = X.fbc_model_constraints(model) + -model diff --git a/src/COBREXA.jl b/src/COBREXA.jl index a223a25fb..cb0bab37a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -8,6 +8,7 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J +import SparseArrays: sparse include("types.jl") include("solver.jl") diff --git a/src/builders/core.jl b/src/builders/core.jl index d85e1d71c..221b8ee4a 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,20 +1,17 @@ -import AbstractFBCModels as F -import SparseArrays: sparse - """ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_constraints(model::F.AbstractFBCModel) - rxns = Symbol.(F.reactions(model)) - mets = Symbol.(F.metabolites(model)) - lbs, ubs = F.bounds(model) - stoi = F.stoichiometry(model) - bal = F.balance(model) - obj = F.objective(model) +function fbc_model_constraints(model::A.AbstractFBCModel) + rxns = Symbol.(A.reactions(model)) + mets = Symbol.(A.metabolites(model)) + lbs, ubs = A.bounds(model) + stoi = A.stoichiometry(model) + bals = A.balance(model) + obj = A.objective(model) #TODO: is sparse() required below? return C.ConstraintTree( @@ -23,7 +20,7 @@ function fbc_model_constraints(model::F.AbstractFBCModel) m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for (m, row, b) in zip(mets, eachrow(stoi), bals) ), - :objective => C.Constraint(value = C.Value(sparse(obj))), + :objective => C.Constraint(value = C.LinearValue(sparse(obj))), ) end From e877a1b51299050bf9a10cb57ea09618c0e0c74a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:32:43 +0100 Subject: [PATCH 352/531] small fixes to core --- src/builders/core.jl | 50 ++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/builders/core.jl b/src/builders/core.jl index 221b8ee4a..67a7179eb 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,26 +1,29 @@ +import AbstractFBCModels as F +import SparseArrays: sparse + """ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_constraints(model::A.AbstractFBCModel) - rxns = Symbol.(A.reactions(model)) - mets = Symbol.(A.metabolites(model)) - lbs, ubs = A.bounds(model) - stoi = A.stoichiometry(model) - bals = A.balance(model) - obj = A.objective(model) +function fbc_model_constraints(model::F.AbstractFBCModel) + rxns = Symbol.(F.reactions(model)) + mets = Symbol.(F.metabolites(model)) + lbs, ubs = F.bounds(model) + stoi = F.stoichiometry(model) + bal = F.balance(model) + obj = F.objective(model) #TODO: is sparse() required below? return C.ConstraintTree( - :fluxes => C.variables(keys = rxns, bounds = zip(lbs, ubs)), - :balances => C.ConstraintTree( - m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for - (m, row, b) in zip(mets, eachrow(stoi), bals) - ), - :objective => C.Constraint(value = C.LinearValue(sparse(obj))), + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :flux_stoichiometry^C.ConstraintTree( + met => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for + (met, row, b) in zip(mets, eachrow(stoi), bal) + ) * + :objective^C.Constraint(C.LinearValue(sparse(obj))), ) end @@ -67,11 +70,26 @@ sign_split_constraints(; signed::C.ConstraintTree, ) = C.ConstraintTree( k => C.Constraint( - value = s + (haskey(negative, k) ? C.value(negative[k]) : zero(C.Value)) - - (haskey(positive, k) ? C.value(positive[k]) : zero(C.Value)), + value = s.value + + (haskey(negative, k) ? negative[k].value : zero(typeof(s.value))) - + (haskey(positive, k) ? positive[k].value : zero(typeof(s.value))), bound = 0.0, - ) for (k, s) in C.elems(signed) + ) for (k, s) in signed ) #TODO the example above might as well go to docs export sign_split_constraints + +function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) + keys = Symbol[] + for (id, flux) in fluxes + if direction == :forward + last(flux.bound) > 0 && push!(keys, id) + else + first(flux.bound) < 0 && push!(keys, id) + end + end + C.variables(; keys, bounds = Ref((0.0, Inf))) +end + +export fluxes_in_direction From 612d5f40771acba6b96aa35a5c4148ea0b561575 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:34:14 +0100 Subject: [PATCH 353/531] update deps --- Project.toml | 4 ++-- docs/src/examples/02-flux-balance-analysis.jl | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 54e6789c9..068dddaf7 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,6 @@ ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -35,6 +34,7 @@ GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" [targets] -test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] +test = ["Aqua", "Clarabel", "Downloads", "GLPK", "SHA", "Test", "Tulip", "JSONFBCModels"] diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index b21fb525c..f14be4bdc 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -7,7 +7,6 @@ import JSONFBCModels as J model = A.load(J.JSONFBCModel, "e_coli_core.json") - ctmodel = X.fbc_model_constraints(model) From ab3696d4249e15c36bb73393196e6163985e9612 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 13:20:23 +0100 Subject: [PATCH 354/531] fba working --- docs/src/examples/02-flux-balance-analysis.jl | 6 +++ src/COBREXA.jl | 1 + src/analysis/flux_balance_analysis.jl | 40 ++++++++++++------- src/analysis/modifications/optimizer.jl | 36 +++++++++++++++++ 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/analysis/modifications/optimizer.jl diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index f14be4bdc..f4d811fab 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -4,9 +4,15 @@ import COBREXA as X import AbstractFBCModels as A import JSONFBCModels as J +import ConstraintTrees as C +using Gurobi model = A.load(J.JSONFBCModel, "e_coli_core.json") ctmodel = X.fbc_model_constraints(model) +vt = C.ValueTree(ctmodel, value.(opt_model[:x])) +vt = X.flux_balance_analysis(model, Gurobi.Optimizer) + +vt = X.flux_balance_analysis(ctmodel, Gurobi.Optimizer; modifications=[X.silence]) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index cb0bab37a..934ca14b3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -17,6 +17,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") +include("analysis/modifications/optimizer.jl") include("analysis/flux_balance_analysis.jl") include("analysis/parsimonious_flux_balance_analysis.jl") diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 4e48c9a19..fcfdb34ae 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Run flux balance analysis (FBA) on the `model` optionally specifying +Run flux balance analysis (FBA) on the `model`, optionally specifying `modifications` to the problem. Basically, FBA solves this optimization problem: ``` max cᵀx @@ -13,36 +13,46 @@ Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more information. The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer` +`GLPK.Optimizer` or `Tulip.Optimizer`. Optionally, you may specify one or more modifications to be applied to the model before the analysis, such as [`modify_optimizer_attribute`](@ref), [`change_objective`](@ref), and [`modify_sense`](@ref). -Returns an optimized `JuMP` model. +Returns a [`C.ValueTree`](@ref). # Example ``` model = load_model("e_coli_core.json") solution = flux_balance_analysis(model, GLPK.optimizer) -value.(solution[:x]) # extract flux steady state from the optimizer +``` +""" +function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; modifications = []) + ctmodel = fbc_model_constraints(model) + flux_balance_analysis(ctmodel, optimizer; modifications) +end -biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") +""" +$(TYPEDSIGNATURES) -modified_solution = flux_balance_analysis(model, GLPK.optimizer; - modifications=[modify_objective(biomass_reaction_id)]) -``` +A variant of [`flux_balance_analysis`](@ref) that takes in a +[`C.ConstraintTree`](@ref) as the model to optimize. The objective is inferred +from the field `objective` in `ctmodel`. All other arguments are forwarded. """ -function flux_balance_analysis(model::C.ConstraintTree, optimizer; modifications = []) - opt_model = make_optimization_model(model, optimizer) +function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) + opt_model = optimization_model( + ctmodel; + objective = ctmodel.objective.value, + optimizer, + ) for mod in modifications - mod(model, opt_model) + mod(ctmodel, opt_model) end - optimize!(opt_model) + J.optimize!(opt_model) - ModelWithResult(model, opt_model) + C.ValueTree(ctmodel, J.value.(opt_model[:x])) end """ @@ -51,4 +61,6 @@ $(TYPEDSIGNATURES) Pipe-able variant of [`flux_balance_analysis`](@ref). """ flux_balance_analysis(optimizer; modifications = []) = - model -> flux_balance_analysis(model, optimizer; modifications) + m -> flux_balance_analysis(m, optimizer; modifications) + +export flux_balance_analysis \ No newline at end of file diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer.jl new file mode 100644 index 000000000..c29b7f7c0 --- /dev/null +++ b/src/analysis/modifications/optimizer.jl @@ -0,0 +1,36 @@ + +""" +$(TYPEDSIGNATURES) + +Change the objective sense of optimization. Possible arguments are +`JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. +""" +modify_sense(objective_sense) = + (_, opt_model) -> set_objective_sense(opt_model, objective_sense) + +""" +$(TYPEDSIGNATURES) + +Change the JuMP optimizer used to run the optimization. +""" +modify_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) + +""" +$(TYPEDSIGNATURES) + +Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer +to the JuMP documentation and the documentation of the specific optimizer for +usable keys and values. +""" +modify_optimizer_attribute(attribute_key, value) = + (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) + +""" + silence + +Modification that disable all output from the JuMP optimizer (shortcut for +`set_silent` from JuMP). +""" +const silence = (_, opt_model) -> J.set_silent(opt_model) + +export modify_sense, modify_optimizer, modify_optimizer_attribute, silence From 0015fa5240f74b110cc2ba7fdbebbba1b4d41a26 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 22:00:09 +0100 Subject: [PATCH 355/531] finish docs and tests for fba --- docs/make.jl | 4 + docs/src/examples/02-flux-balance-analysis.jl | 77 ++++++++++++++++--- src/COBREXA.jl | 2 +- src/analysis/flux_balance_analysis.jl | 12 ++- .../{optimizer.jl => optimizer_settings.jl} | 10 +-- src/solver.jl | 32 -------- 6 files changed, 86 insertions(+), 51 deletions(-) rename src/analysis/modifications/{optimizer.jl => optimizer_settings.jl} (68%) diff --git a/docs/make.jl b/docs/make.jl index 0b98cc0c3..26526817c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,6 +2,10 @@ using Documenter using Literate, JSON using COBREXA +# testing constants +const TEST_TOLERANCE = 1e-3 +const QP_TEST_TOLERANCE = 1e-2 # for Clarabel + # build the examples examples_path = joinpath(@__DIR__, "src", "examples") examples_basenames = sort(filter(x -> endswith(x, ".jl"), readdir(examples_path))) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index f4d811fab..cce5e6bd5 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,18 +1,77 @@ -# # Flux balance analysis +# # Flux balance analysis (FBA) + +# We will use [`flux_balance_analysis`](@ref) and several related functions to +# find the optimal flux distribution in the *E. coli* "core" model. + +# If it is not already present, download the model and load the package: +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# next, load the necessary packages import COBREXA as X -import AbstractFBCModels as A -import JSONFBCModels as J -import ConstraintTrees as C -using Gurobi +import AbstractFBCModels as A # for the accessors +import JSONFBCModels as J # for the model type +import Tulip as T # use any JuMP supported optimizer +import GLPK as G + +model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model -model = A.load(J.JSONFBCModel, "e_coli_core.json") +# run FBA on the model using default settings + +vt = X.flux_balance_analysis(model, T.Optimizer) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# Alternatively, a constraint tree can be passed in as well ctmodel = X.fbc_model_constraints(model) -vt = C.ValueTree(ctmodel, value.(opt_model[:x])) +# We can also pass some modifications to the optimizer +# Except for `X.silence`, all other optimizer modifications +# are the same as those in JuMP. +vt = X.flux_balance_analysis( + ctmodel, + G.Optimizer; + modifications = [ + X.silence + X.set_objective_sense(X.J.MAX_SENSE) # JuMP is called J inside COBREXA + X.set_optimizer(T.Optimizer) # change to Tulip from GLPK + X.set_optimizer_attribute("IPM_IterationsLimit", 110) # Tulip specific setting + ], +) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# We can also modify the model. The most explicit way to do this is +# to make a new constraint tree representation of the model. + +import ConstraintTrees as C + +fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value + +forced_mixed_fermentation = + ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created + +vt = X.flux_balance_analysis(forced_mixed_fermentation, T.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src + +# Models that cannot be solved return `nothing`. In the example below, the +# underlying model is modified. + +ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable + +vt = X.flux_balance_analysis(ctmodel, T.Optimizer; modifications = [X.silence]) + +@test isnothing(vt) #src + +# Models can also be piped into the analysis functions -vt = X.flux_balance_analysis(model, Gurobi.Optimizer) +ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert +vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) -vt = X.flux_balance_analysis(ctmodel, Gurobi.Optimizer; modifications=[X.silence]) +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 934ca14b3..5ce9d2c6a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -17,7 +17,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") -include("analysis/modifications/optimizer.jl") +include("analysis/modifications/optimizer_settings.jl") include("analysis/flux_balance_analysis.jl") include("analysis/parsimonious_flux_balance_analysis.jl") diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index fcfdb34ae..2de61855c 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -2,7 +2,8 @@ $(TYPEDSIGNATURES) Run flux balance analysis (FBA) on the `model`, optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization problem: +`modifications` to the problem. Basically, FBA solves this optimization +problem: ``` max cᵀx s.t. S x = b @@ -15,9 +16,10 @@ information. The `optimizer` must be set to a `JuMP`-compatible optimizer, such as `GLPK.Optimizer` or `Tulip.Optimizer`. -Optionally, you may specify one or more modifications to be applied to the -model before the analysis, such as [`modify_optimizer_attribute`](@ref), -[`change_objective`](@ref), and [`modify_sense`](@ref). +Optionally, you may specify one or more modifications to be applied to the model +before the analysis, such as [`set_objective_sense`](@ref), +[`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and +[`silence`](@ref). Returns a [`C.ValueTree`](@ref). @@ -51,6 +53,8 @@ function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modificatio end J.optimize!(opt_model) + + is_solved(opt_model) || return nothing C.ValueTree(ctmodel, J.value.(opt_model[:x])) end diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer_settings.jl similarity index 68% rename from src/analysis/modifications/optimizer.jl rename to src/analysis/modifications/optimizer_settings.jl index c29b7f7c0..ed8c8a664 100644 --- a/src/analysis/modifications/optimizer.jl +++ b/src/analysis/modifications/optimizer_settings.jl @@ -5,15 +5,15 @@ $(TYPEDSIGNATURES) Change the objective sense of optimization. Possible arguments are `JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. """ -modify_sense(objective_sense) = - (_, opt_model) -> set_objective_sense(opt_model, objective_sense) +set_objective_sense(objective_sense) = + (_, opt_model) -> J.set_objective_sense(opt_model, objective_sense) """ $(TYPEDSIGNATURES) Change the JuMP optimizer used to run the optimization. """ -modify_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) +set_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) """ $(TYPEDSIGNATURES) @@ -22,7 +22,7 @@ Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer to the JuMP documentation and the documentation of the specific optimizer for usable keys and values. """ -modify_optimizer_attribute(attribute_key, value) = +set_optimizer_attribute(attribute_key, value) = (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) """ @@ -33,4 +33,4 @@ Modification that disable all output from the JuMP optimizer (shortcut for """ const silence = (_, opt_model) -> J.set_silent(opt_model) -export modify_sense, modify_optimizer, modify_optimizer_attribute, silence +export set_objective_sense, set_optimizer, set_optimizer_attribute, silence diff --git a/src/solver.jl b/src/solver.jl index 7cd7d1027..ff6fcb7b2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -48,35 +48,3 @@ is_solved(opt_model::J.Model) = export is_solved -""" -$(TYPEDSIGNATURES) - -The optimized objective value of a JuMP model, if solved. -""" -optimized_objective_value(opt_model::J.Model)::Maybe{Float64} = - is_solved(opt_model) ? J.objective_value(opt_model) : nothing - -export optimized_objective_value - -""" -$(TYPEDSIGNATURES) - -The optimized variable assignment of a JuMP model, if solved. -""" -optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? J.value.(opt_model[:x]) : nothing - -export optimized_variable_assignment - -""" -$(TYPEDSIGNATURES) - -Annotate a `ConstraintTree` with the values given by the optimization model, -producing a `ValueTree` (if solved). -""" -solution(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = - let vars = optimized_variable_assignment(opt_model) - isnothing(vars) ? nothing : C.ValueTree(c, vars) - end - -export solution From 7eda0e958cd1af62b50a6b95c1a6ed9f81f4d60a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 22:29:36 +0100 Subject: [PATCH 356/531] add knockout functionality and doctest --- docs/src/examples/02-flux-balance-analysis.jl | 13 ++++- src/builders/genes.jl | 50 ++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index cce5e6bd5..079ad534a 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -56,7 +56,11 @@ fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value forced_mixed_fermentation = ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created -vt = X.flux_balance_analysis(forced_mixed_fermentation, T.Optimizer; modifications = [X.silence]) +vt = X.flux_balance_analysis( + forced_mixed_fermentation, + T.Optimizer; + modifications = [X.silence], +) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src @@ -75,3 +79,10 @@ ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# Gene knockouts can be done with ease making use of the piping functionality. +# Here oxidative phosphorylation is knocked out. + +vt = ctmodel |> X.knockout!(["b0979", "b0734"], model) |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 17263d4ad..df4dfcd9d 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,18 +1,42 @@ -knockout_constraints(;fluxes::C.ConstraintTree, knockout_test) = - C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) - ) - -export knockout_constraints +""" +$(TYPEDSIGNATURES) +""" +knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for + rxn in keys(fluxes) if knockout_test(rxn) +) -fbc_gene_knockout_constraints(;fluxes::C.ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; +""" +$(TYPEDSIGNATURES) +""" +gene_knockouts(; + fluxes::C.ConstraintTree, + ko_genes::Vector{String}, + model::A.AbstractFBCModel, +) = knockout_constraints(; fluxes, - knockout_test = - rxn -> !A.reaction_gene_products_available( - rxn, - g -> not(g in genes) - ), + knockout_test = rxn -> begin + maybe_avail = A.reaction_gene_products_available( + model, + string(rxn), + g -> !(g in ko_genes), # not available if knocked out + ) + isnothing(maybe_avail) ? false : !maybe_avail # negate here because of knockout_constraints + end, ) -export fbc_gene_knockout_constraints +""" +$(TYPEDSIGNATURES) +""" +knockout!(ctmodel::C.ConstraintTree, ko_genes::Vector{String}, model::A.AbstractFBCModel) = + ctmodel * :gene_knockouts^gene_knockouts(; fluxes = ctmodel.fluxes, ko_genes, model) + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant. +""" +knockout!(ko_genes::Vector{String}, model::A.AbstractFBCModel) = + m -> knockout!(m, ko_genes, model) + From 6e673416cefe9fdf6c8607b137f1821ef773011b Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Nov 2023 09:21:57 +0100 Subject: [PATCH 357/531] basic pFBA and MOMA --- docs/src/examples/03-qp-problems.jl | 88 +++++++++++++++++++ src/COBREXA.jl | 2 + .../minimize_metabolic_adjustment_analysis.jl | 83 +++++++++++++++++ .../parsimonious_flux_balance_analysis.jl | 52 +++++++---- src/builders/objectives.jl | 29 +++--- 5 files changed, 222 insertions(+), 32 deletions(-) create mode 100644 docs/src/examples/03-qp-problems.jl create mode 100644 src/analysis/minimize_metabolic_adjustment_analysis.jl diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl new file mode 100644 index 000000000..a835bae0b --- /dev/null +++ b/docs/src/examples/03-qp-problems.jl @@ -0,0 +1,88 @@ + +# # Quandratic objective flux balance analysis type problems + +# We will use [`parsimonious_flux_balance_analysis`](@ref) and +# [`minimize_metabolic_adjustment_analysis`](@ref) to find the optimal flux +# distribution in the *E. coli* "core" model. + +# If it is not already present, download the model and load the package: +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# next, load the necessary packages + +import COBREXA as X +import AbstractFBCModels as A # for the accessors +import JSONFBCModels as J # for the model type +import Clarabel # can solve QPs + +model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model + +# Use the convenience function to run standard pFBA + +vt = X.parsimonious_flux_balance_analysis(model, Clarabel.Optimizer) + +# Or use the piping functionality + +model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.87392; atol=TEST_TOLERANCE) #src +@test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol=QP_TEST_TOLERANCE) #src + +# Alternatively, you can construct your own constraint tree model with +# the quadratic objective (this approach is much more flexible). + +ctmodel = X.fbc_model_constraints(model) +ctmodel *= :l2objective ^ X.squared_sum_objective(ctmodel.fluxes) +ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks + +opt_model = X.optimization_model( + ctmodel; + objective = ctmodel.:l2objective.value, + optimizer = Clarabel.Optimizer, + sense = X.J.MIN_SENSE, +) + +X.J.optimize!(opt_model) # JuMP is called J in COBREXA + +X.is_solved(opt_model) # check if solved + +vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA + +@test isapprox(vt.l2objective, ?; atol=QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds + +# It is likewise as simple to run MOMA using the convenience functions. + +ref_sol = Dict("ATPS4r" => 33.0, "CYTBD" => 22.0) + +vt = X.minimize_metabolic_adjustment_analysis(model, ref_sol, Gurobi.Optimizer) + +# Or use the piping functionality + +model |> X.minimize_metabolic_adjustment_analysis(ref_sol, Clarabel.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.:momaobjective, 0.81580806; atol=TEST_TOLERANCE) #src + +# Alternatively, you can construct your own constraint tree model with +# the quadratic objective (this approach is much more flexible). + +ctmodel = X.fbc_model_constraints(model) +ctmodel *= :minoxphospho ^ X.squared_sum_error_objective(ctmodel.fluxes, Dict(:ATPS4r => 33.0, :CYTBD => 22.0,)) +ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks + +opt_model = X.optimization_model( + ctmodel; + objective = ctmodel.minoxphospho.value, + optimizer = Clarabel.Optimizer, + sense = X.J.MIN_SENSE, +) + +X.J.optimize!(opt_model) # JuMP is called J in COBREXA + +X.is_solved(opt_model) # check if solved + +vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA + +@test isapprox(vt.l2objective, ?; atol=QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 5ce9d2c6a..fcdc70e8c 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -9,6 +9,7 @@ import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J import SparseArrays: sparse +import LinearAlgebra: dot include("types.jl") include("solver.jl") @@ -20,5 +21,6 @@ include("builders/objectives.jl") include("analysis/modifications/optimizer_settings.jl") include("analysis/flux_balance_analysis.jl") include("analysis/parsimonious_flux_balance_analysis.jl") +include("analysis/minimize_metabolic_adjustment_analysis.jl") end # module COBREXA diff --git a/src/analysis/minimize_metabolic_adjustment_analysis.jl b/src/analysis/minimize_metabolic_adjustment_analysis.jl new file mode 100644 index 000000000..3f9370075 --- /dev/null +++ b/src/analysis/minimize_metabolic_adjustment_analysis.jl @@ -0,0 +1,83 @@ + +""" +$(TYPEDSIGNATURES) + +Run minimization of metabolic adjustment (MOMA) on `model` with respect to +`reference_solution`, which is a dictionary of fluxes. MOMA finds the shortest +Euclidian distance between `reference_solution` and `model` with `modifications`: +``` +min Σᵢ (xᵢ - flux_refᵢ)² +s.t. S x = b + xₗ ≤ x ≤ xᵤ +``` +Because the problem has a quadratic objective, a QP solver is required. See +"Daniel, Vitkup & Church, Analysis of Optimality in Natural and Perturbed +Metabolic Networks, Proceedings of the National Academy of Sciences, 2002" for +more details. + +Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be +found. + +# Example +``` +model = load_model("e_coli_core.json") + +``` +""" +function minimize_metabolic_adjustment_analysis( + ctmodel::C.ConstraintTree, + reference_solution::Dict{String,Float64}, + optimizer; + modifications = [], +) + _ctmodel = + ctmodel * + :momaobjective^squared_sum_error_objective( + ctmodel.fluxes, + Dict(Symbol(k) => float(v) for (k, v) in reference_solution), + ) + + opt_model = optimization_model( + _ctmodel; + objective = _ctmodel.momaobjective.value, + optimizer, + sense = J.MIN_SENSE, + ) + + for mod in modifications + mod(ctmodel, opt_model) + end + + J.optimize!(opt_model) + + is_solved(opt_model) || return nothing + + C.ValueTree(_ctmodel, J.value.(opt_model[:x])) +end + +""" +$(TYPEDSIGNATURES) + +Variant that takes an [`A.AbstractFBCModel`](@ref) as input. All other arguments are forwarded. +""" +function minimize_metabolic_adjustment_analysis( + model::A.AbstractFBCModel, + args...; + kwargs..., +) + ctmodel = fbc_model_constraints(model) + minimize_metabolic_adjustment_analysis(ctmodel, args...; kwargs...) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`minimize_metabolic_adjustment_analysis`](@ref). +""" +minimize_metabolic_adjustment_analysis( + reference_solution::Dict{String,Float64}, + optimizer; + kwargs..., +) = m -> minimize_metabolic_adjustment_analysis(m, reference_solution, optimizer; kwargs...) + +export minimize_metabolic_adjustment_analysis diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 9e5405fe9..f4c3e9777 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -37,33 +37,31 @@ The optimum relaxation sequence can be specified in `relax` parameter, it defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original bound. -Returns an optimized model that contains the pFBA solution (or an unsolved model -if something went wrong). - -# Performance - -This implementation attempts to save time by executing all pFBA steps on a -single instance of the optimization model problem, trading off possible -flexibility. For slightly less performant but much more flexible use, one can -construct parsimonious models directly using -[`with_parsimonious_objective`](@ref). +Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be found. # Example ``` model = load_model("e_coli_core.json") -parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec +parsimonious_flux_balance_analysis(model, Gurobi.Optimizer) ``` """ function parsimonious_flux_balance_analysis( - model::C.ConstraintTree, + ctmodel::C.ConstraintTree, optimizer; modifications = [], qp_modifications = [], relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], ) # Run FBA - opt_model = flux_balance_analysis(model, optimizer; modifications) - J.is_solved(opt_model) || return nothing # FBA failed + opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) + + for mod in modifications + mod(ctmodel, opt_model) + end + + J.optimize!(opt_model) + + is_solved(opt_model) || return nothing # get the objective Z = J.objective_value(opt_model) @@ -79,14 +77,36 @@ function parsimonious_flux_balance_analysis( J.@objective(opt_model, Min, sum(dot(v, v))) for rb in relax_bounds - # lb, ub = objective_bounds(rb)(Z) + lb, ub = objective_bounds(rb)(Z) J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) J.optimize!(opt_model) - J.is_solved(opt_model) && break + is_solved(opt_model) && break + @warn("Relaxing pFBA objective bound!") J.delete(opt_model, pfba_constraint) J.unregister(opt_model, :pfba_constraint) end + C.ValueTree(ctmodel, J.value.(opt_model[:x])) +end + +""" +$(TYPEDSIGNATURES) + +Variant that takes an [`A.AbstractFBCModel`](@ref) as input. All other arguments are forwarded. +""" +function parsimonious_flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) + ctmodel = fbc_model_constraints(model) + parsimonious_flux_balance_analysis(ctmodel, optimizer; kwargs...) end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`parsimonious_flux_balance_analysis`](@ref). +""" +parsimonious_flux_balance_analysis(optimizer; modifications = []) = + m -> parsimonious_flux_balance_analysis(m, optimizer; modifications) + +export parsimonious_flux_balance_analysis diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index b6d5f7994..7bc9075f5 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,19 +1,16 @@ -sum_objectve(x) = - C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) - -sum_objective(x::C.ConstraintTree) = squared_error_objective(values(x)) +squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = + C.Constraint( + sum( + (C.value(c) - target[k]) * (C.value(c) - target[k]) for + (k, c) in constraints if haskey(target, k) + ), + ) -squared_sum_objective(x) = - C.Constraint(sum(C.squared.(C.value.(x)), init = zero(C.QuadraticValue))) +squared_sum_objective(x::C.ConstraintTree) = + squared_sum_error_objective(x, Dict(keys(x) .=> 0.0)) -squared_sum_objective(x::C.ConstraintTree) = squared_sum_objective(values(x)) - -squared_error_objective(constraints::C.ConstraintTree, target) = C.Constraint( - sum( - let tmp = (C.value(c) - target[k]) - tmp * tmp - end for (k, c) in constraints if haskey(target, k)), -) - -# TODO use `mergewith` to do this reasonably (add it to ConstraintTrees) +objective_bounds(tolerance) = z -> begin + vs = (z * tolerance, z / tolerance) + (minimum(vs), maximum(vs)) +end From 9f886af765d83eaac588aeb3f93e1f14025634d9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 11 Nov 2023 09:30:29 +0100 Subject: [PATCH 358/531] make pFBA more in line with MOMA and CTs --- .../parsimonious_flux_balance_analysis.jl | 41 ++++++++----------- 1 file changed, 18 insertions(+), 23 deletions(-) diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index f4c3e9777..162d0e97a 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -33,11 +33,11 @@ using the callback in `qp_modifications`, which are applied after the FBA. See the documentation of [`flux_balance_analysis`](@ref) for usage examples of modifications. -The optimum relaxation sequence can be specified in `relax` parameter, it -defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original -bound. +The `relax` parameter, which defaults to `0.999`, relaxes the objective bound of +the original bound to prevent numerical issues. -Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be found. +Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be +found. # Example ``` @@ -50,7 +50,7 @@ function parsimonious_flux_balance_analysis( optimizer; modifications = [], qp_modifications = [], - relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], + relax = 0.999, ) # Run FBA opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) @@ -65,30 +65,25 @@ function parsimonious_flux_balance_analysis( # get the objective Z = J.objective_value(opt_model) - original_objective = J.objective_function(opt_model) + + _ctmodel = ctmodel * :pfbaobjective ^ squared_sum_objective(ctmodel.fluxes) + _ctmodel.objective.bound = relax == 1.0 ? Z : objective_bounds(relax)(Z) # TODO currently breaks + + opt_model = X.optimization_model( + _ctmodel; + objective = ctmodel.pfbaobjective.value, + optimizer, + sense = X.J.MIN_SENSE, + ) # prepare the model for pFBA for mod in qp_modifications mod(model, opt_model) end + + J.optimize!(opt_model) - # add the minimization constraint for total flux - v = opt_model[:x] # fluxes - J.@objective(opt_model, Min, sum(dot(v, v))) - - for rb in relax_bounds - lb, ub = objective_bounds(rb)(Z) - J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) - - J.optimize!(opt_model) - is_solved(opt_model) && break - @warn("Relaxing pFBA objective bound!") - - J.delete(opt_model, pfba_constraint) - J.unregister(opt_model, :pfba_constraint) - end - - C.ValueTree(ctmodel, J.value.(opt_model[:x])) + C.ValueTree(_ctmodel, J.value.(opt_model[:x])) end """ From 888545726172ec0f54305e5c7310d55bb1270925 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 11:52:43 +0100 Subject: [PATCH 359/531] ignore models --- .gitignore | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.gitignore b/.gitignore index 46e71d415..caac77fde 100644 --- a/.gitignore +++ b/.gitignore @@ -20,3 +20,8 @@ Manifest.toml # ignore container files *.sif + +# ignore models +*.xml +*.json +*.mat \ No newline at end of file From f5b8f8b11ed6943477df874b6a6aa3ffb20a352e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:15:35 +0100 Subject: [PATCH 360/531] get cobrexa to load --- Project.toml | 2 + docs/Project.toml | 9 +- docs/src/examples/02-flux-balance-analysis.jl | 4 + src/COBREXA.jl | 3 + src/analysis/flux_balance_analysis.jl | 54 +++++++++++ .../parsimonious_flux_balance_analysis.jl | 92 +++++++++++++++++++ src/builders/genes.jl | 21 ++--- src/builders/objectives.jl | 8 +- 8 files changed, 172 insertions(+), 21 deletions(-) create mode 100644 src/analysis/flux_balance_analysis.jl create mode 100644 src/analysis/parsimonious_flux_balance_analysis.jl diff --git a/Project.toml b/Project.toml index dc4027e93..24409afba 100644 --- a/Project.toml +++ b/Project.toml @@ -32,6 +32,8 @@ Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [targets] test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] diff --git a/docs/Project.toml b/docs/Project.toml index 5b3dcde68..c8ea85a2a 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,15 +1,10 @@ [deps] +AbstractFBCModels = "5a4f3dfa-1789-40f8-8221-69268c29937c" COBREXA = "babc4406-5200-4a30-9033-bf5ae714c842" -CairoMakie = "13f3f980-e62b-5c42-98c6-ff1f3baf88f0" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" -Clustering = "aaaa29a8-35af-508c-8bc3-b662a17a0fe5" -ColorSchemes = "35d6a980-a343-548e-a6ea-1d62b119f2f4" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" -Escher = "8cc96de1-1b23-48cb-9272-618d67962629" -GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" InteractiveUtils = "b77e0a4c-d291-57a0-90e8-8db25a27a240" -JSON = "682c06a0-de6a-54ab-a142-c8b1cf79cde6" -JuMP = "4076af6c-e467-56ae-b986-b466b2749572" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" Literate = "98b081ad-f1c9-55d3-8b20-4c87d4299306" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 678930970..8d36fd1a9 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -2,5 +2,9 @@ # # Flux balance analysis using COBREXA +import AbstractFBCModels as A +import JSONFBCModels as J # TODO: run FBA on a FBC model + +model diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 5baaa18cc..a223a25fb 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -16,4 +16,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") +include("analysis/flux_balance_analysis.jl") +include("analysis/parsimonious_flux_balance_analysis.jl") + end # module COBREXA diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl new file mode 100644 index 000000000..4e48c9a19 --- /dev/null +++ b/src/analysis/flux_balance_analysis.jl @@ -0,0 +1,54 @@ +""" +$(TYPEDSIGNATURES) + +Run flux balance analysis (FBA) on the `model` optionally specifying +`modifications` to the problem. Basically, FBA solves this optimization problem: +``` +max cᵀx +s.t. S x = b + xₗ ≤ x ≤ xᵤ +``` +See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat +Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more +information. + +The `optimizer` must be set to a `JuMP`-compatible optimizer, such as +`GLPK.Optimizer` or `Tulip.Optimizer` + +Optionally, you may specify one or more modifications to be applied to the +model before the analysis, such as [`modify_optimizer_attribute`](@ref), +[`change_objective`](@ref), and [`modify_sense`](@ref). + +Returns an optimized `JuMP` model. + +# Example +``` +model = load_model("e_coli_core.json") +solution = flux_balance_analysis(model, GLPK.optimizer) +value.(solution[:x]) # extract flux steady state from the optimizer + +biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") + +modified_solution = flux_balance_analysis(model, GLPK.optimizer; + modifications=[modify_objective(biomass_reaction_id)]) +``` +""" +function flux_balance_analysis(model::C.ConstraintTree, optimizer; modifications = []) + opt_model = make_optimization_model(model, optimizer) + + for mod in modifications + mod(model, opt_model) + end + + optimize!(opt_model) + + ModelWithResult(model, opt_model) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`flux_balance_analysis`](@ref). +""" +flux_balance_analysis(optimizer; modifications = []) = + model -> flux_balance_analysis(model, optimizer; modifications) diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl new file mode 100644 index 000000000..9e5405fe9 --- /dev/null +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -0,0 +1,92 @@ +""" +$(TYPEDSIGNATURES) + +Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA +runs two consecutive optimization problems. The first is traditional FBA: +``` +max cᵀx = μ +s.t. S x = b + xₗ ≤ x ≤ xᵤ +``` +And the second is a quadratic optimization problem: +``` +min Σᵢ xᵢ² +s.t. S x = b + xₗ ≤ x ≤ xᵤ + μ = μ⁰ +``` +Where the optimal solution of the FBA problem, μ⁰, has been added as an +additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, +Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, +Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, +Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data +from evolved E. coli are consistent with computed optimal growth from +genome-scale models. Molecular Systems Biology, 6. 390. doi: +accession:10.1038/msb.2010.47" for more details. + +pFBA gets the model optimum by standard FBA (using +[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then +finds a minimal total flux through the model that still satisfies the (slightly +relaxed) optimum. This is done using a quadratic problem optimizer. If the +original optimizer does not support quadratic optimization, it can be changed +using the callback in `qp_modifications`, which are applied after the FBA. See +the documentation of [`flux_balance_analysis`](@ref) for usage examples of +modifications. + +The optimum relaxation sequence can be specified in `relax` parameter, it +defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original +bound. + +Returns an optimized model that contains the pFBA solution (or an unsolved model +if something went wrong). + +# Performance + +This implementation attempts to save time by executing all pFBA steps on a +single instance of the optimization model problem, trading off possible +flexibility. For slightly less performant but much more flexible use, one can +construct parsimonious models directly using +[`with_parsimonious_objective`](@ref). + +# Example +``` +model = load_model("e_coli_core.json") +parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec +``` +""" +function parsimonious_flux_balance_analysis( + model::C.ConstraintTree, + optimizer; + modifications = [], + qp_modifications = [], + relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], +) + # Run FBA + opt_model = flux_balance_analysis(model, optimizer; modifications) + J.is_solved(opt_model) || return nothing # FBA failed + + # get the objective + Z = J.objective_value(opt_model) + original_objective = J.objective_function(opt_model) + + # prepare the model for pFBA + for mod in qp_modifications + mod(model, opt_model) + end + + # add the minimization constraint for total flux + v = opt_model[:x] # fluxes + J.@objective(opt_model, Min, sum(dot(v, v))) + + for rb in relax_bounds + # lb, ub = objective_bounds(rb)(Z) + J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) + + J.optimize!(opt_model) + J.is_solved(opt_model) && break + + J.delete(opt_model, pfba_constraint) + J.unregister(opt_model, :pfba_constraint) + end + +end diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 2996a08f9..17263d4ad 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,19 +1,18 @@ -knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for - rxn in keys(fluxes) if knockout_test(rxn) -) +knockout_constraints(;fluxes::C.ConstraintTree, knockout_test) = + C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) + ) export knockout_constraints -fbc_gene_knockout_constraints(; - fluxes::C.ConstraintTree, - genes, - fbc_model::A.AbstractFBCModel, -) = knockout_constraints(; +fbc_gene_knockout_constraints(;fluxes::C.ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; fluxes, - knockout_test = rxn -> - !A.reaction_gene_products_available(rxn, g -> not(g in genes)), + knockout_test = + rxn -> !A.reaction_gene_products_available( + rxn, + g -> not(g in genes) + ), ) export fbc_gene_knockout_constraints diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 93cca18ce..b6d5f7994 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,6 +1,7 @@ -sum_objectve(x) = C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) - +sum_objectve(x) = + C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) + sum_objective(x::C.ConstraintTree) = squared_error_objective(values(x)) squared_sum_objective(x) = @@ -9,7 +10,8 @@ squared_sum_objective(x) = squared_sum_objective(x::C.ConstraintTree) = squared_sum_objective(values(x)) squared_error_objective(constraints::C.ConstraintTree, target) = C.Constraint( - sum(let tmp = (C.value(c) - target[k]) + sum( + let tmp = (C.value(c) - target[k]) tmp * tmp end for (k, c) in constraints if haskey(target, k)), ) From fd4b8cc6cff97146080de42337de98e00e8d5bad Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:23:12 +0100 Subject: [PATCH 361/531] temporarily add JSONFBCModels to make coding easier --- Project.toml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/Project.toml b/Project.toml index 24409afba..54e6789c9 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -29,11 +30,11 @@ julia = "1.5" [extras] Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" +Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" -Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" -SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" [targets] test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] From af52e45475d0ccf14c5828f613f6d4ecc26fefa8 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:26:06 +0100 Subject: [PATCH 362/531] load model and create ctmodel --- docs/src/examples/02-flux-balance-analysis.jl | 9 ++++++--- src/COBREXA.jl | 1 + src/builders/core.jl | 19 ++++++++----------- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 8d36fd1a9..b21fb525c 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,10 +1,13 @@ # # Flux balance analysis -using COBREXA +import COBREXA as X import AbstractFBCModels as A import JSONFBCModels as J -# TODO: run FBA on a FBC model +model = A.load(J.JSONFBCModel, "e_coli_core.json") + + +ctmodel = X.fbc_model_constraints(model) + -model diff --git a/src/COBREXA.jl b/src/COBREXA.jl index a223a25fb..cb0bab37a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -8,6 +8,7 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J +import SparseArrays: sparse include("types.jl") include("solver.jl") diff --git a/src/builders/core.jl b/src/builders/core.jl index d85e1d71c..221b8ee4a 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,20 +1,17 @@ -import AbstractFBCModels as F -import SparseArrays: sparse - """ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_constraints(model::F.AbstractFBCModel) - rxns = Symbol.(F.reactions(model)) - mets = Symbol.(F.metabolites(model)) - lbs, ubs = F.bounds(model) - stoi = F.stoichiometry(model) - bal = F.balance(model) - obj = F.objective(model) +function fbc_model_constraints(model::A.AbstractFBCModel) + rxns = Symbol.(A.reactions(model)) + mets = Symbol.(A.metabolites(model)) + lbs, ubs = A.bounds(model) + stoi = A.stoichiometry(model) + bals = A.balance(model) + obj = A.objective(model) #TODO: is sparse() required below? return C.ConstraintTree( @@ -23,7 +20,7 @@ function fbc_model_constraints(model::F.AbstractFBCModel) m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for (m, row, b) in zip(mets, eachrow(stoi), bals) ), - :objective => C.Constraint(value = C.Value(sparse(obj))), + :objective => C.Constraint(value = C.LinearValue(sparse(obj))), ) end From 580f603138ad6d8f440e38bb8d6de8bf15456e17 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:32:43 +0100 Subject: [PATCH 363/531] small fixes to core --- src/builders/core.jl | 50 ++++++++++++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/src/builders/core.jl b/src/builders/core.jl index 221b8ee4a..67a7179eb 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,26 +1,29 @@ +import AbstractFBCModels as F +import SparseArrays: sparse + """ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_constraints(model::A.AbstractFBCModel) - rxns = Symbol.(A.reactions(model)) - mets = Symbol.(A.metabolites(model)) - lbs, ubs = A.bounds(model) - stoi = A.stoichiometry(model) - bals = A.balance(model) - obj = A.objective(model) +function fbc_model_constraints(model::F.AbstractFBCModel) + rxns = Symbol.(F.reactions(model)) + mets = Symbol.(F.metabolites(model)) + lbs, ubs = F.bounds(model) + stoi = F.stoichiometry(model) + bal = F.balance(model) + obj = F.objective(model) #TODO: is sparse() required below? return C.ConstraintTree( - :fluxes => C.variables(keys = rxns, bounds = zip(lbs, ubs)), - :balances => C.ConstraintTree( - m => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for - (m, row, b) in zip(mets, eachrow(stoi), bals) - ), - :objective => C.Constraint(value = C.LinearValue(sparse(obj))), + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :flux_stoichiometry^C.ConstraintTree( + met => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for + (met, row, b) in zip(mets, eachrow(stoi), bal) + ) * + :objective^C.Constraint(C.LinearValue(sparse(obj))), ) end @@ -67,11 +70,26 @@ sign_split_constraints(; signed::C.ConstraintTree, ) = C.ConstraintTree( k => C.Constraint( - value = s + (haskey(negative, k) ? C.value(negative[k]) : zero(C.Value)) - - (haskey(positive, k) ? C.value(positive[k]) : zero(C.Value)), + value = s.value + + (haskey(negative, k) ? negative[k].value : zero(typeof(s.value))) - + (haskey(positive, k) ? positive[k].value : zero(typeof(s.value))), bound = 0.0, - ) for (k, s) in C.elems(signed) + ) for (k, s) in signed ) #TODO the example above might as well go to docs export sign_split_constraints + +function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) + keys = Symbol[] + for (id, flux) in fluxes + if direction == :forward + last(flux.bound) > 0 && push!(keys, id) + else + first(flux.bound) < 0 && push!(keys, id) + end + end + C.variables(; keys, bounds = Ref((0.0, Inf))) +end + +export fluxes_in_direction From 7ec85bde1a686d203af187fa130c5905f578520e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 12:34:14 +0100 Subject: [PATCH 364/531] update deps --- Project.toml | 4 ++-- docs/src/examples/02-flux-balance-analysis.jl | 1 - 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 54e6789c9..068dddaf7 100644 --- a/Project.toml +++ b/Project.toml @@ -9,7 +9,6 @@ ConstraintTrees = "5515826b-29c3-47a5-8849-8513ac836620" Distributed = "8ba89e20-285c-5b6f-9357-94700520ee1b" DistributedData = "f6a0035f-c5ac-4ad0-b410-ad102ced35df" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" -JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" JuMP = "4076af6c-e467-56ae-b986-b466b2749572" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" @@ -35,6 +34,7 @@ GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" [targets] -test = ["Aqua", "Clarabel", "GLPK", "Test", "Tulip"] +test = ["Aqua", "Clarabel", "Downloads", "GLPK", "SHA", "Test", "Tulip", "JSONFBCModels"] diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index b21fb525c..f14be4bdc 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -7,7 +7,6 @@ import JSONFBCModels as J model = A.load(J.JSONFBCModel, "e_coli_core.json") - ctmodel = X.fbc_model_constraints(model) From 5b65ed8b48d49750e559c01c4de5e05e7a0df1a5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 13:20:23 +0100 Subject: [PATCH 365/531] fba working --- docs/src/examples/02-flux-balance-analysis.jl | 6 +++ src/COBREXA.jl | 1 + src/analysis/flux_balance_analysis.jl | 40 ++++++++++++------- src/analysis/modifications/optimizer.jl | 36 +++++++++++++++++ 4 files changed, 69 insertions(+), 14 deletions(-) create mode 100644 src/analysis/modifications/optimizer.jl diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index f14be4bdc..f4d811fab 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -4,9 +4,15 @@ import COBREXA as X import AbstractFBCModels as A import JSONFBCModels as J +import ConstraintTrees as C +using Gurobi model = A.load(J.JSONFBCModel, "e_coli_core.json") ctmodel = X.fbc_model_constraints(model) +vt = C.ValueTree(ctmodel, value.(opt_model[:x])) +vt = X.flux_balance_analysis(model, Gurobi.Optimizer) + +vt = X.flux_balance_analysis(ctmodel, Gurobi.Optimizer; modifications=[X.silence]) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index cb0bab37a..934ca14b3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -17,6 +17,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") +include("analysis/modifications/optimizer.jl") include("analysis/flux_balance_analysis.jl") include("analysis/parsimonious_flux_balance_analysis.jl") diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 4e48c9a19..fcfdb34ae 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -1,7 +1,7 @@ """ $(TYPEDSIGNATURES) -Run flux balance analysis (FBA) on the `model` optionally specifying +Run flux balance analysis (FBA) on the `model`, optionally specifying `modifications` to the problem. Basically, FBA solves this optimization problem: ``` max cᵀx @@ -13,36 +13,46 @@ Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more information. The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer` +`GLPK.Optimizer` or `Tulip.Optimizer`. Optionally, you may specify one or more modifications to be applied to the model before the analysis, such as [`modify_optimizer_attribute`](@ref), [`change_objective`](@ref), and [`modify_sense`](@ref). -Returns an optimized `JuMP` model. +Returns a [`C.ValueTree`](@ref). # Example ``` model = load_model("e_coli_core.json") solution = flux_balance_analysis(model, GLPK.optimizer) -value.(solution[:x]) # extract flux steady state from the optimizer +``` +""" +function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; modifications = []) + ctmodel = fbc_model_constraints(model) + flux_balance_analysis(ctmodel, optimizer; modifications) +end -biomass_reaction_id = findfirst(model.reactions, "BIOMASS_Ecoli_core_w_GAM") +""" +$(TYPEDSIGNATURES) -modified_solution = flux_balance_analysis(model, GLPK.optimizer; - modifications=[modify_objective(biomass_reaction_id)]) -``` +A variant of [`flux_balance_analysis`](@ref) that takes in a +[`C.ConstraintTree`](@ref) as the model to optimize. The objective is inferred +from the field `objective` in `ctmodel`. All other arguments are forwarded. """ -function flux_balance_analysis(model::C.ConstraintTree, optimizer; modifications = []) - opt_model = make_optimization_model(model, optimizer) +function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) + opt_model = optimization_model( + ctmodel; + objective = ctmodel.objective.value, + optimizer, + ) for mod in modifications - mod(model, opt_model) + mod(ctmodel, opt_model) end - optimize!(opt_model) + J.optimize!(opt_model) - ModelWithResult(model, opt_model) + C.ValueTree(ctmodel, J.value.(opt_model[:x])) end """ @@ -51,4 +61,6 @@ $(TYPEDSIGNATURES) Pipe-able variant of [`flux_balance_analysis`](@ref). """ flux_balance_analysis(optimizer; modifications = []) = - model -> flux_balance_analysis(model, optimizer; modifications) + m -> flux_balance_analysis(m, optimizer; modifications) + +export flux_balance_analysis \ No newline at end of file diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer.jl new file mode 100644 index 000000000..c29b7f7c0 --- /dev/null +++ b/src/analysis/modifications/optimizer.jl @@ -0,0 +1,36 @@ + +""" +$(TYPEDSIGNATURES) + +Change the objective sense of optimization. Possible arguments are +`JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. +""" +modify_sense(objective_sense) = + (_, opt_model) -> set_objective_sense(opt_model, objective_sense) + +""" +$(TYPEDSIGNATURES) + +Change the JuMP optimizer used to run the optimization. +""" +modify_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) + +""" +$(TYPEDSIGNATURES) + +Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer +to the JuMP documentation and the documentation of the specific optimizer for +usable keys and values. +""" +modify_optimizer_attribute(attribute_key, value) = + (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) + +""" + silence + +Modification that disable all output from the JuMP optimizer (shortcut for +`set_silent` from JuMP). +""" +const silence = (_, opt_model) -> J.set_silent(opt_model) + +export modify_sense, modify_optimizer, modify_optimizer_attribute, silence From fa96c9015dc14563b8c129842b305fe186b9d277 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 22:00:09 +0100 Subject: [PATCH 366/531] finish docs and tests for fba --- docs/make.jl | 4 + docs/src/examples/02-flux-balance-analysis.jl | 77 ++++++++++++++++--- src/COBREXA.jl | 2 +- src/analysis/flux_balance_analysis.jl | 12 ++- .../{optimizer.jl => optimizer_settings.jl} | 10 +-- src/solver.jl | 32 -------- 6 files changed, 86 insertions(+), 51 deletions(-) rename src/analysis/modifications/{optimizer.jl => optimizer_settings.jl} (68%) diff --git a/docs/make.jl b/docs/make.jl index 0b98cc0c3..26526817c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,6 +2,10 @@ using Documenter using Literate, JSON using COBREXA +# testing constants +const TEST_TOLERANCE = 1e-3 +const QP_TEST_TOLERANCE = 1e-2 # for Clarabel + # build the examples examples_path = joinpath(@__DIR__, "src", "examples") examples_basenames = sort(filter(x -> endswith(x, ".jl"), readdir(examples_path))) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index f4d811fab..cce5e6bd5 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,18 +1,77 @@ -# # Flux balance analysis +# # Flux balance analysis (FBA) + +# We will use [`flux_balance_analysis`](@ref) and several related functions to +# find the optimal flux distribution in the *E. coli* "core" model. + +# If it is not already present, download the model and load the package: +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# next, load the necessary packages import COBREXA as X -import AbstractFBCModels as A -import JSONFBCModels as J -import ConstraintTrees as C -using Gurobi +import AbstractFBCModels as A # for the accessors +import JSONFBCModels as J # for the model type +import Tulip as T # use any JuMP supported optimizer +import GLPK as G + +model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model -model = A.load(J.JSONFBCModel, "e_coli_core.json") +# run FBA on the model using default settings + +vt = X.flux_balance_analysis(model, T.Optimizer) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# Alternatively, a constraint tree can be passed in as well ctmodel = X.fbc_model_constraints(model) -vt = C.ValueTree(ctmodel, value.(opt_model[:x])) +# We can also pass some modifications to the optimizer +# Except for `X.silence`, all other optimizer modifications +# are the same as those in JuMP. +vt = X.flux_balance_analysis( + ctmodel, + G.Optimizer; + modifications = [ + X.silence + X.set_objective_sense(X.J.MAX_SENSE) # JuMP is called J inside COBREXA + X.set_optimizer(T.Optimizer) # change to Tulip from GLPK + X.set_optimizer_attribute("IPM_IterationsLimit", 110) # Tulip specific setting + ], +) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# We can also modify the model. The most explicit way to do this is +# to make a new constraint tree representation of the model. + +import ConstraintTrees as C + +fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value + +forced_mixed_fermentation = + ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created + +vt = X.flux_balance_analysis(forced_mixed_fermentation, T.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src + +# Models that cannot be solved return `nothing`. In the example below, the +# underlying model is modified. + +ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable + +vt = X.flux_balance_analysis(ctmodel, T.Optimizer; modifications = [X.silence]) + +@test isnothing(vt) #src + +# Models can also be piped into the analysis functions -vt = X.flux_balance_analysis(model, Gurobi.Optimizer) +ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert +vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) -vt = X.flux_balance_analysis(ctmodel, Gurobi.Optimizer; modifications=[X.silence]) +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 934ca14b3..5ce9d2c6a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -17,7 +17,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") -include("analysis/modifications/optimizer.jl") +include("analysis/modifications/optimizer_settings.jl") include("analysis/flux_balance_analysis.jl") include("analysis/parsimonious_flux_balance_analysis.jl") diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index fcfdb34ae..2de61855c 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -2,7 +2,8 @@ $(TYPEDSIGNATURES) Run flux balance analysis (FBA) on the `model`, optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization problem: +`modifications` to the problem. Basically, FBA solves this optimization +problem: ``` max cᵀx s.t. S x = b @@ -15,9 +16,10 @@ information. The `optimizer` must be set to a `JuMP`-compatible optimizer, such as `GLPK.Optimizer` or `Tulip.Optimizer`. -Optionally, you may specify one or more modifications to be applied to the -model before the analysis, such as [`modify_optimizer_attribute`](@ref), -[`change_objective`](@ref), and [`modify_sense`](@ref). +Optionally, you may specify one or more modifications to be applied to the model +before the analysis, such as [`set_objective_sense`](@ref), +[`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and +[`silence`](@ref). Returns a [`C.ValueTree`](@ref). @@ -51,6 +53,8 @@ function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modificatio end J.optimize!(opt_model) + + is_solved(opt_model) || return nothing C.ValueTree(ctmodel, J.value.(opt_model[:x])) end diff --git a/src/analysis/modifications/optimizer.jl b/src/analysis/modifications/optimizer_settings.jl similarity index 68% rename from src/analysis/modifications/optimizer.jl rename to src/analysis/modifications/optimizer_settings.jl index c29b7f7c0..ed8c8a664 100644 --- a/src/analysis/modifications/optimizer.jl +++ b/src/analysis/modifications/optimizer_settings.jl @@ -5,15 +5,15 @@ $(TYPEDSIGNATURES) Change the objective sense of optimization. Possible arguments are `JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. """ -modify_sense(objective_sense) = - (_, opt_model) -> set_objective_sense(opt_model, objective_sense) +set_objective_sense(objective_sense) = + (_, opt_model) -> J.set_objective_sense(opt_model, objective_sense) """ $(TYPEDSIGNATURES) Change the JuMP optimizer used to run the optimization. """ -modify_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) +set_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) """ $(TYPEDSIGNATURES) @@ -22,7 +22,7 @@ Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer to the JuMP documentation and the documentation of the specific optimizer for usable keys and values. """ -modify_optimizer_attribute(attribute_key, value) = +set_optimizer_attribute(attribute_key, value) = (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) """ @@ -33,4 +33,4 @@ Modification that disable all output from the JuMP optimizer (shortcut for """ const silence = (_, opt_model) -> J.set_silent(opt_model) -export modify_sense, modify_optimizer, modify_optimizer_attribute, silence +export set_objective_sense, set_optimizer, set_optimizer_attribute, silence diff --git a/src/solver.jl b/src/solver.jl index 7cd7d1027..ff6fcb7b2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -48,35 +48,3 @@ is_solved(opt_model::J.Model) = export is_solved -""" -$(TYPEDSIGNATURES) - -The optimized objective value of a JuMP model, if solved. -""" -optimized_objective_value(opt_model::J.Model)::Maybe{Float64} = - is_solved(opt_model) ? J.objective_value(opt_model) : nothing - -export optimized_objective_value - -""" -$(TYPEDSIGNATURES) - -The optimized variable assignment of a JuMP model, if solved. -""" -optimized_variable_assignment(opt_model::J.Model)::Maybe{Vector{Float64}} = - is_solved(opt_model) ? J.value.(opt_model[:x]) : nothing - -export optimized_variable_assignment - -""" -$(TYPEDSIGNATURES) - -Annotate a `ConstraintTree` with the values given by the optimization model, -producing a `ValueTree` (if solved). -""" -solution(c::C.ConstraintTree, opt_model::J.Model)::Maybe{C.ValueTree} = - let vars = optimized_variable_assignment(opt_model) - isnothing(vars) ? nothing : C.ValueTree(c, vars) - end - -export solution From 360886aa7a3bf01c1c23b0975ef595aa38ebe850 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 22:29:36 +0100 Subject: [PATCH 367/531] add knockout functionality and doctest --- docs/src/examples/02-flux-balance-analysis.jl | 13 ++++- src/builders/genes.jl | 50 ++++++++++++++----- 2 files changed, 49 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index cce5e6bd5..079ad534a 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -56,7 +56,11 @@ fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value forced_mixed_fermentation = ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created -vt = X.flux_balance_analysis(forced_mixed_fermentation, T.Optimizer; modifications = [X.silence]) +vt = X.flux_balance_analysis( + forced_mixed_fermentation, + T.Optimizer; + modifications = [X.silence], +) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src @@ -75,3 +79,10 @@ ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# Gene knockouts can be done with ease making use of the piping functionality. +# Here oxidative phosphorylation is knocked out. + +vt = ctmodel |> X.knockout!(["b0979", "b0734"], model) |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 17263d4ad..df4dfcd9d 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,18 +1,42 @@ -knockout_constraints(;fluxes::C.ConstraintTree, knockout_test) = - C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for rxn in keys(fluxes) if knockout_test(rxn) - ) - -export knockout_constraints +""" +$(TYPEDSIGNATURES) +""" +knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( + rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for + rxn in keys(fluxes) if knockout_test(rxn) +) -fbc_gene_knockout_constraints(;fluxes::C.ConstraintTree, genes, fbc_model::A.AbstractFBCModel) = knockout_constraints(; +""" +$(TYPEDSIGNATURES) +""" +gene_knockouts(; + fluxes::C.ConstraintTree, + ko_genes::Vector{String}, + model::A.AbstractFBCModel, +) = knockout_constraints(; fluxes, - knockout_test = - rxn -> !A.reaction_gene_products_available( - rxn, - g -> not(g in genes) - ), + knockout_test = rxn -> begin + maybe_avail = A.reaction_gene_products_available( + model, + string(rxn), + g -> !(g in ko_genes), # not available if knocked out + ) + isnothing(maybe_avail) ? false : !maybe_avail # negate here because of knockout_constraints + end, ) -export fbc_gene_knockout_constraints +""" +$(TYPEDSIGNATURES) +""" +knockout!(ctmodel::C.ConstraintTree, ko_genes::Vector{String}, model::A.AbstractFBCModel) = + ctmodel * :gene_knockouts^gene_knockouts(; fluxes = ctmodel.fluxes, ko_genes, model) + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant. +""" +knockout!(ko_genes::Vector{String}, model::A.AbstractFBCModel) = + m -> knockout!(m, ko_genes, model) + From 8389d7bee48ec4aa0bdb45aa032d675a6b4397da Mon Sep 17 00:00:00 2001 From: exaexa Date: Fri, 1 Dec 2023 16:00:22 +0000 Subject: [PATCH 368/531] automatic formatting triggered by @exaexa on PR #800 --- docs/src/examples/02-flux-balance-analysis.jl | 11 +++++++---- src/analysis/flux_balance_analysis.jl | 10 +++------- src/builders/genes.jl | 5 ++--- src/builders/objectives.jl | 8 +++----- src/solver.jl | 1 - 5 files changed, 15 insertions(+), 20 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 079ad534a..b8d309757 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -31,7 +31,7 @@ vt = X.flux_balance_analysis(model, T.Optimizer) ctmodel = X.fbc_model_constraints(model) # We can also pass some modifications to the optimizer -# Except for `X.silence`, all other optimizer modifications +# Except for `X.silence`, all other optimizer modifications # are the same as those in JuMP. vt = X.flux_balance_analysis( ctmodel, @@ -46,7 +46,7 @@ vt = X.flux_balance_analysis( @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src -# We can also modify the model. The most explicit way to do this is +# We can also modify the model. The most explicit way to do this is # to make a new constraint tree representation of the model. import ConstraintTrees as C @@ -64,7 +64,7 @@ vt = X.flux_balance_analysis( @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src -# Models that cannot be solved return `nothing`. In the example below, the +# Models that cannot be solved return `nothing`. In the example below, the # underlying model is modified. ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable @@ -83,6 +83,9 @@ vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence] # Gene knockouts can be done with ease making use of the piping functionality. # Here oxidative phosphorylation is knocked out. -vt = ctmodel |> X.knockout!(["b0979", "b0734"], model) |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) +vt = + ctmodel |> + X.knockout!(["b0979", "b0734"], model) |> + X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) @test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 2de61855c..343537002 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -42,18 +42,14 @@ A variant of [`flux_balance_analysis`](@ref) that takes in a from the field `objective` in `ctmodel`. All other arguments are forwarded. """ function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) - opt_model = optimization_model( - ctmodel; - objective = ctmodel.objective.value, - optimizer, - ) + opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) for mod in modifications mod(ctmodel, opt_model) end J.optimize!(opt_model) - + is_solved(opt_model) || return nothing C.ValueTree(ctmodel, J.value.(opt_model[:x])) @@ -67,4 +63,4 @@ Pipe-able variant of [`flux_balance_analysis`](@ref). flux_balance_analysis(optimizer; modifications = []) = m -> flux_balance_analysis(m, optimizer; modifications) -export flux_balance_analysis \ No newline at end of file +export flux_balance_analysis diff --git a/src/builders/genes.jl b/src/builders/genes.jl index df4dfcd9d..63ed19c01 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -16,14 +16,14 @@ gene_knockouts(; model::A.AbstractFBCModel, ) = knockout_constraints(; fluxes, - knockout_test = rxn -> begin + knockout_test = rxn -> begin maybe_avail = A.reaction_gene_products_available( model, string(rxn), g -> !(g in ko_genes), # not available if knocked out ) isnothing(maybe_avail) ? false : !maybe_avail # negate here because of knockout_constraints - end, + end, ) """ @@ -39,4 +39,3 @@ Pipe-able variant. """ knockout!(ko_genes::Vector{String}, model::A.AbstractFBCModel) = m -> knockout!(m, ko_genes, model) - diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index b6d5f7994..93cca18ce 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,7 +1,6 @@ -sum_objectve(x) = - C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) - +sum_objectve(x) = C.Constraint(sum(C.value.(x), init = zero(C.LinearValue))) + sum_objective(x::C.ConstraintTree) = squared_error_objective(values(x)) squared_sum_objective(x) = @@ -10,8 +9,7 @@ squared_sum_objective(x) = squared_sum_objective(x::C.ConstraintTree) = squared_sum_objective(values(x)) squared_error_objective(constraints::C.ConstraintTree, target) = C.Constraint( - sum( - let tmp = (C.value(c) - target[k]) + sum(let tmp = (C.value(c) - target[k]) tmp * tmp end for (k, c) in constraints if haskey(target, k)), ) diff --git a/src/solver.jl b/src/solver.jl index ff6fcb7b2..c1a3b2b7b 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -47,4 +47,3 @@ is_solved(opt_model::J.Model) = J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] export is_solved - From 005e9edac206f64a81003afd1da280ef434a8b54 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 5 Dec 2023 11:53:45 +0100 Subject: [PATCH 369/531] move test tolerances back --- docs/make.jl | 4 ---- test/runtests.jl | 4 ++++ 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 26526817c..0b98cc0c3 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -2,10 +2,6 @@ using Documenter using Literate, JSON using COBREXA -# testing constants -const TEST_TOLERANCE = 1e-3 -const QP_TEST_TOLERANCE = 1e-2 # for Clarabel - # build the examples examples_path = joinpath(@__DIR__, "src", "examples") examples_basenames = sort(filter(x -> endswith(x, ".jl"), readdir(examples_path))) diff --git a/test/runtests.jl b/test/runtests.jl index 0a4324e65..d8e6be943 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -6,6 +6,10 @@ using Distributed import AbstractFBCModels as A using GLPK # for MILPs +# testing constants +const TEST_TOLERANCE = 1e-3 +const QP_TEST_TOLERANCE = 1e-2 # for Clarabel + # helper functions for running tests en masse print_timing(fn, t) = @info "$(fn) done in $(round(t; digits = 2))s" From 871e604ab1c3f7ac7fccaec3f8f60ddaea445360 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 5 Dec 2023 11:54:39 +0100 Subject: [PATCH 370/531] we've got julia 1.9 nowadays --- .gitlab-ci.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 68f70fccf..7c22afb96 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -71,9 +71,9 @@ variables: before_script: - docker login -u $CI_USER_NAME -p $GITLAB_ACCESS_TOKEN $CI_REGISTRY -.global_julia18: &global_julia18 +.global_julia19: &global_julia19 variables: - JULIA_VER: "v1.8.3" + JULIA_VER: "v1.9.4" .global_julia16: &global_julia16 variables: @@ -139,7 +139,7 @@ linux:julia1.8: tags: - slave01 <<: *global_trigger_full_tests - <<: *global_julia18 + <<: *global_julia19 <<: *global_env_linux linux:julia1.6: @@ -157,19 +157,19 @@ linux:julia1.6: windows8:julia1.8: stage: test-compat <<: *global_trigger_compat_tests - <<: *global_julia18 + <<: *global_julia19 <<: *global_env_win8 windows10:julia1.8: stage: test-compat <<: *global_trigger_compat_tests - <<: *global_julia18 + <<: *global_julia19 <<: *global_env_win10 mac:julia1.8: stage: test-compat <<: *global_trigger_compat_tests - <<: *global_julia18 + <<: *global_julia19 <<: *global_env_mac windows8:julia1.6: From 8b99c197b2f1f5d2a947526aa3a256e615882b3e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 6 Dec 2023 11:45:33 +0100 Subject: [PATCH 371/531] depend on the mutable constraint trees --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 068dddaf7..e95a375cf 100644 --- a/Project.toml +++ b/Project.toml @@ -19,7 +19,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] AbstractFBCModels = "0.2" Clarabel = "0.3" -ConstraintTrees = "0.4" +ConstraintTrees = "0.5" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" JuMP = "1" From 68d60c3967601fe4ca4c7c92407dfc87ebd349d3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 09:28:31 +0100 Subject: [PATCH 372/531] we removed coverage summary scriptage --- .gitlab-ci.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7c22afb96..4cd9beb7d 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -125,8 +125,6 @@ docker:julia1.8: image: $CI_REGISTRY/r3/docker/julia-custom script: - julia --check-bounds=yes --inline=yes --project=@. -e "import Pkg; Pkg.test(; coverage = true)" - after_script: - - julia --project=test/coverage test/coverage/coverage-summary.jl <<: *global_trigger_pull_request # From 8fa3dc09cd169fbe18875a0e71db732a53ff85c7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 09:28:41 +0100 Subject: [PATCH 373/531] more removal of julia-1.8 --- .gitlab-ci.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 4cd9beb7d..4f7aa3de7 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -120,7 +120,7 @@ variables: # any available docker and current julia # -docker:julia1.8: +docker:julia1.9: stage: test image: $CI_REGISTRY/r3/docker/julia-custom script: @@ -132,7 +132,7 @@ docker:julia1.8: # built & deployed # -linux:julia1.8: +linux:julia1.9: stage: test tags: - slave01 @@ -152,19 +152,19 @@ linux:julia1.6: # Additional platform&environment compatibility tests # -windows8:julia1.8: +windows8:julia1.9: stage: test-compat <<: *global_trigger_compat_tests <<: *global_julia19 <<: *global_env_win8 -windows10:julia1.8: +windows10:julia1.9: stage: test-compat <<: *global_trigger_compat_tests <<: *global_julia19 <<: *global_env_win10 -mac:julia1.8: +mac:julia1.9: stage: test-compat <<: *global_trigger_compat_tests <<: *global_julia19 From fd6692fe325731cc6c6bf101722ef651c7df4fd2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 09:34:20 +0100 Subject: [PATCH 374/531] fix dep versions --- Project.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index e95a375cf..14daa42be 100644 --- a/Project.toml +++ b/Project.toml @@ -18,12 +18,20 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] AbstractFBCModels = "0.2" +Aqua = "0.7" Clarabel = "0.3" ConstraintTrees = "0.5" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" +Downloads = "1" +GLPK = "1" +JSONFBCModels = "0.1" JuMP = "1" +SBMLFBCModels = "0.1" +SHA = "0.7, 1" StableRNGs = "1.0" +Test = "1" +Tulip = "0.9" julia = "1.5" [extras] @@ -31,10 +39,11 @@ Aqua = "4c88cf16-eb10-579e-8560-4a9242c79595" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" +JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" +SBMLFBCModels = "3e8f9d1a-ffc1-486d-82d6-6c7276635980" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" -JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" [targets] test = ["Aqua", "Clarabel", "Downloads", "GLPK", "SHA", "Test", "Tulip", "JSONFBCModels"] From 1da45366f868bafcbc75b6c1e0b800e81c286818 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 15:13:51 +0100 Subject: [PATCH 375/531] start clearing the docs --- docs/src/examples/01-loading-and-saving.jl | 2 + docs/src/examples/02-flux-balance-analysis.jl | 106 ++++++------------ src/io.jl | 21 +++- test/runtests.jl | 14 ++- 4 files changed, 60 insertions(+), 83 deletions(-) diff --git a/docs/src/examples/01-loading-and-saving.jl b/docs/src/examples/01-loading-and-saving.jl index 584cee8cb..05e8adc68 100644 --- a/docs/src/examples/01-loading-and-saving.jl +++ b/docs/src/examples/01-loading-and-saving.jl @@ -4,3 +4,5 @@ using COBREXA # TODO: download the models into a single directory that can get cached. Probably best have a fake mktempdir(). +# +# TODO: demonstrate download_model here and explain how to get hashes (simply not fill them in for the first time) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index b8d309757..058d9ca75 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,91 +1,51 @@ # # Flux balance analysis (FBA) -# We will use [`flux_balance_analysis`](@ref) and several related functions to -# find the optimal flux distribution in the *E. coli* "core" model. +# Here we use [`flux_balance_analysis`](@ref) and several related functions to +# find an optimal flux in the *E. coli* "core" model. We will need the model, +# which we can download using [`download_model`](@ref): -# If it is not already present, download the model and load the package: -import Downloads: download +using COBREXA -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -# next, load the necessary packages - -import COBREXA as X -import AbstractFBCModels as A # for the accessors -import JSONFBCModels as J # for the model type -import Tulip as T # use any JuMP supported optimizer -import GLPK as G - -model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model - -# run FBA on the model using default settings - -vt = X.flux_balance_analysis(model, T.Optimizer) - -@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src - -# Alternatively, a constraint tree can be passed in as well - -ctmodel = X.fbc_model_constraints(model) - -# We can also pass some modifications to the optimizer -# Except for `X.silence`, all other optimizer modifications -# are the same as those in JuMP. -vt = X.flux_balance_analysis( - ctmodel, - G.Optimizer; - modifications = [ - X.silence - X.set_objective_sense(X.J.MAX_SENSE) # JuMP is called J inside COBREXA - X.set_optimizer(T.Optimizer) # change to Tulip from GLPK - X.set_optimizer_attribute("IPM_IterationsLimit", 110) # Tulip specific setting - ], +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", ) -@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src - -# We can also modify the model. The most explicit way to do this is -# to make a new constraint tree representation of the model. - -import ConstraintTrees as C - -fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value - -forced_mixed_fermentation = - ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created - -vt = X.flux_balance_analysis( - forced_mixed_fermentation, - T.Optimizer; - modifications = [X.silence], -) +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use Tulip here: -@test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src +import JSONFBCModels +import Tulip -# Models that cannot be solved return `nothing`. In the example below, the -# underlying model is modified. +model = load_model("e_coli_core.json") -ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable +# ## Running a FBA +# +# There are many possibilities on how to arrange the metabolic model into the +# optimization framework and how to actually solve it. The "usual" assumed one +# is captured in the default behavior of function +# [`flux_balance_analysis`](@ref): -vt = X.flux_balance_analysis(ctmodel, T.Optimizer; modifications = [X.silence]) +solution = flux_balance_analysis(model, Tulip.Optimizer) -@test isnothing(vt) #src +@test isapprox(solution.objective, 0.8739, atol = TEST_TOLERANCE) #src -# Models can also be piped into the analysis functions +# The result contains a tree of all optimized values in the model, including +# fluxes, the objective value, and possibly others (given by what the model +# contains). +# +# You can explore the dot notation to explore the solution, extracting e.g. the +# value of the objective: -ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert -vt = ctmodel |> X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) +solution.objective -@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src +# ...or the value of the flux through the given reaction (note the solution is +# not unique in FBA): -# Gene knockouts can be done with ease making use of the piping functionality. -# Here oxidative phosphorylation is knocked out. +solution.fluxes.PFK -vt = - ctmodel |> - X.knockout!(["b0979", "b0734"], model) |> - X.flux_balance_analysis(T.Optimizer; modifications = [X.silence]) +# ...or make a "table" of all fluxes through all reactions: -@test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src +collect(solution.fluxes) diff --git a/src/io.jl b/src/io.jl index f517bdb58..b00730da5 100644 --- a/src/io.jl +++ b/src/io.jl @@ -8,7 +8,7 @@ Load a FBC model representation while guessing the correct model type. Uses This overload almost always involves a search over types; do not use it in environments where performance is critical. """ -function load_fbc_model(path::String) where {A<:AbstractFBCModel} +function load_model(path::String) where {A<:AbstractFBCModel} A.load(path) end @@ -17,19 +17,30 @@ end Load a FBC model representation. Uses `AbstractFBCModels.load`. """ -function load_fbc_model(model_type::Type{A}, path::String) where {A<:AbstractFBCModel} +function load_model(model_type::Type{A}, path::String) where {A<:AbstractFBCModel} A.load(model_type, path) end -export load_fbc_model +export load_model """ $(TYPEDSIGNATURES) Save a FBC model representation. Uses `AbstractFBCModels.save`. """ -function save_fbc_model(model::A, path::String) where {A<:AbstractFBCModel} +function save_model(model::A, path::String) where {A<:AbstractFBCModel} A.save(model, path) end -export save_fbc_model +export save_model + +""" + $(TYPEDSIGNATURES) + +Safely download a model with a known hash. All arguments are forwarded to +`AbstractFBCModels.download_data_file` -- see the documentation in the +AbstractFBCModels package for details. +""" +download_model(args...; kwargs...) = A.download_data_file(args...; kwargs...) + +export download_model diff --git a/test/runtests.jl b/test/runtests.jl index d8e6be943..3f2318dec 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -19,8 +19,10 @@ function run_test_file(path...) print_timing(fn, t) end -function run_doc_ex(path...) - run_test_file("..", "docs", "src", "examples", path...) +function run_doc_examples() + for dir in readdir("../docs/src/examples", join = true) + run_test_file(dir) + end end # set up the workers for Distributed, so that the tests that require more @@ -32,10 +34,12 @@ t = @elapsed @everywhere using COBREXA, Tulip, JuMP run_test_file("data_static.jl") run_test_file("data_downloaded.jl") -# import base files +# TODO data_static and data_downloaded need to be interned into the demos. +# Instead let's make a single "doc running directory" that runs all the +# documentation, which doesn't get erased to improve the test caching. + @testset "COBREXA test suite" begin - run_doc_ex("01-loading-and-saving.jl") - run_doc_ex("02-flux-balance-analysis.jl") + run_doc_examples() run_test_file("aqua.jl") end From d6388d2c8778631288d3bb662c46c31235e78b64 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 15:14:10 +0100 Subject: [PATCH 376/531] add piecified docs --- docs/src/examples/02a-optimizer-parameters.jl | 63 +++++++++++++++ docs/src/examples/02b-model-modifications.jl | 28 +++++++ .../examples/02c-constraint-modifications.jl | 78 +++++++++++++++++++ 3 files changed, 169 insertions(+) create mode 100644 docs/src/examples/02a-optimizer-parameters.jl create mode 100644 docs/src/examples/02b-model-modifications.jl create mode 100644 docs/src/examples/02c-constraint-modifications.jl diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl new file mode 100644 index 000000000..b2715bd6c --- /dev/null +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -0,0 +1,63 @@ + +# # Changing optimizer parameters +# +# Many optimizers require fine-tuning to produce best results. You can pass in +# additional optimizer settings via the `modifications` parameter of +# [`flux_balance_analysis`](@ref). These include e.g. +# +# - [`set_optimizer_attribute`](@ref) (typically allowing you to tune e.g. +# iteration limits, tolerances, or floating-point precision) +# - [`set_objective_sense`](@ref) (allowing you to change and reverse the +# optimization direction, if required) +# - [`silence`](@ref) to disable the debug output of the optimizer +# - and even [`set_optimizer`](@ref), which changes the optimizer +# implementation used (this is not quite useful in this case, but becomes +# beneficial with more complex, multi-stage optimization problems) +# +# To demonstrate this, we'll use the usual toy model: + +using COBREXA +import JSONFBCModels, Tulip + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +model = load_model("e_coli_core.json") + +# Running a FBA with a silent optimizer that has slightly increased iteration +# limit for IPM algorithm may now look as follows: +solution = flux_balance_analysis( + ctmodel, + Tulip.Optimizer; + modifications = [ + silence + set_optimizer_attribute("IPM_IterationsLimit", 1000) + ], +) + +@test !isnothing(solution) #src + +# To see some of the effects of the configuration changes, you may deliberately +# cripple the optimizer's possibilities to a few iterations, which will cause +# it to fail and return no solution: + +solution = flux_balance_analysis( + ctmodel, + Tulip.Optimizer; + modifications = [ + silence + set_optimizer_attribute("IPM_IterationsLimit", 1000) + ], +) + +println(solution) + +@test isnothing(solution) #src + +# Applicable optimizer attributes are documented in the documentations of the +# respective optimizers. To browse the possibilities, you may want to see the +# [JuMP documentation page that summarizes the references to the available +# optimizers](https://jump.dev/JuMP.jl/stable/installation/#Supported-solvers). diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl new file mode 100644 index 000000000..22307fe7c --- /dev/null +++ b/docs/src/examples/02b-model-modifications.jl @@ -0,0 +1,28 @@ + +# # Making adjustments to the model +# +# Typically, we do not need to solve the models as they come from the authors +# (someone else already did that!), but we want to perform various +# perturbations in the model structure and conditions, and explore how the +# model behaves in the changed conditions. +# +# With COBREXA, there are 2 different approaches that one can take: +# 1. We can change the model structure and use the changed metabolic model. +# This is better for doing simple and small but systematic modifications, such +# as removing metabolites, adding reactions, etc. +# 2. We can intercept the pipeline that converts the metabolic model to +# constraints and then to the optimizer representation, and make small +# modifications along that way. This is better for various technical model +# adjustments, such as using combined objectives or adding reaction-coupling +# constraints. +# +# Here we demonstrate the first, "modelling" approach. The main advantage of +# that approach is that the modified model is still a FBC model, and you can +# export, save and share it via the AbstractFBCModels interace. The main +# disadvantage is that the "common" FBC model interface does not easily express +# various complicated constructions (communities, reaction coupling, enzyme +# constraints, etc.) -- see the [example about modifying the +# constraints](02c-constraint-modifications.md) for a closer look on how to +# modify even such complex constructions. +# +# TODO here. :) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl new file mode 100644 index 000000000..a1a7d2adc --- /dev/null +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -0,0 +1,78 @@ + +# # Making adjustments to the constraint system +# +# In the [previous example about model +# adjustments](02b-model-modifications.md), we noted that some constraint +# systems may be to complex to be changed within the limits of the usual FBC +# model view, and we may require a sharper tool to do the changes we need. This +# example shows how to do that by modifying the constraint systems that are +# generated within COBREXA to represent the metabolic model contents. +# +# ## Background: Model-to-optimizer pipeline +# +# ## Background: Constraint trees +# +# ## Changing the model-to-optimizer pipeline +# +# TODO the stuff below: + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +import JSONFBCModels +import Tulip + +model = load_model("e_coli_core.json") + +# ## Customizing the model + +# We can also modify the model. The most explicit way to do this is +# to make a new constraint tree representation of the model. + +import ConstraintTrees as C + +ctmodel = fbc_model_constraints(model) + +fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value + +forced_mixed_fermentation = + ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created + +vt = flux_balance_analysis( + forced_mixed_fermentation, + Tulip.Optimizer; + modifications = [silence], +) + +@test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src + +# Models that cannot be solved return `nothing`. In the example below, the +# underlying model is modified. + +ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable + +vt = flux_balance_analysis(ctmodel, Tulip.Optimizer; modifications = [silence]) + +@test isnothing(vt) #src + +# Models can also be piped into the analysis functions + +ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert +vt = ctmodel |> flux_balance_analysis(Tulip.Optimizer; modifications = [silence]) + +@test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src + +# Gene knockouts can be done with ease making use of the piping functionality. +# Here oxidative phosphorylation is knocked out. + +vt = + ctmodel |> + X.knockout!(["b0979", "b0734"], model) |> + X.flux_balance_analysis(Tulip.Optimizer; modifications = [X.silence]) + +@test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src From 868586ebae80a9b4bafbcde37a7507165726e9ab Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 15:22:43 +0100 Subject: [PATCH 377/531] forgotten IO --- src/COBREXA.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 5ce9d2c6a..0b82866dc 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -11,6 +11,7 @@ import JuMP as J import SparseArrays: sparse include("types.jl") +include("io.jl") include("solver.jl") include("builders/core.jl") From f36063e09e6da4b4c601d7225efaf5b58a7014e1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 15:22:56 +0100 Subject: [PATCH 378/531] do not julia ipynbs --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 3f2318dec..3cdc86390 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,7 +20,7 @@ function run_test_file(path...) end function run_doc_examples() - for dir in readdir("../docs/src/examples", join = true) + for dir in readdir("../docs/src/examples", join = true) |> filter(endswith(".jl")) run_test_file(dir) end end From de43af5be23fd1fd0c77f2f47bfef03d528919b2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 17:32:43 +0100 Subject: [PATCH 379/531] a huge bunch of small fixes --- README.md | 2 +- docs/src/concepts/1_screen.md | 10 +++---- docs/src/examples/02-flux-balance-analysis.jl | 6 ++--- docs/src/examples/02a-optimizer-parameters.jl | 26 +++++++------------ .../examples/02c-constraint-modifications.jl | 24 ++++------------- src/COBREXA.jl | 7 +++-- ...ux_balance_analysis.jl => flux_balance.jl} | 19 +++++++------- ...optimizer_settings.jl => modifications.jl} | 18 +++++++++---- ...alysis.jl => parsimonious_flux_balance.jl} | 10 +++---- src/builders/core.jl | 15 +++++------ src/io.jl | 14 +++++----- test/runtests.jl | 5 +++- 12 files changed, 72 insertions(+), 84 deletions(-) rename src/analysis/{flux_balance_analysis.jl => flux_balance.jl} (69%) rename src/analysis/{modifications/optimizer_settings.jl => modifications.jl} (59%) rename src/analysis/{parsimonious_flux_balance_analysis.jl => parsimonious_flux_balance.jl} (89%) diff --git a/README.md b/README.md index 47237b005..b9fd793f1 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,7 @@ download("http://bigg.ucsd.edu/static/models/e_coli_core.xml", "e_coli_core.xml" model = load_model("e_coli_core.xml") # run a FBA -fluxes = flux_balance_analysis_dict(model, Tulip.Optimizer) +fluxes = flux_balance_dict(model, Tulip.Optimizer) ``` The variable `fluxes` will now contain a dictionary of the computed optimal diff --git a/docs/src/concepts/1_screen.md b/docs/src/concepts/1_screen.md index a00204092..cb482a4b6 100644 --- a/docs/src/concepts/1_screen.md +++ b/docs/src/concepts/1_screen.md @@ -24,7 +24,7 @@ screen_variants( [with_changed_bound("O2t", lb = 0, ub = 0)], # disable O2 transport [with_changed_bound("CO2t", lb = 0, ub = 0), with_changed_bound("O2t", lb = 0, ub = 0)], # disable both transports ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` The call specifies a model (the `m` that we have loaded) that is being tested, @@ -89,7 +89,7 @@ res = screen_variants(m, ["EX_h2o_e", "EX_co2_e", "EX_o2_e", "EX_nh4_e"], # and this set of exchanges ) ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` @@ -199,7 +199,7 @@ screen_variants( [with_disabled_oxygen_transport], [with_disabled_reaction("NH4t")], ], - m -> flux_balance_analysis_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], + m -> flux_balance_dict(m, Tulip.Optimizer)["BIOMASS_Ecoli_core_w_GAM"], ) ``` @@ -222,7 +222,7 @@ That should get you the results for all new variants of the model: Some analysis functions may take additional arguments, which you might want to vary for the analysis. `modifications` argument of -[`flux_balance_analysis_dict`](@ref) is one example of such argument, allowing +[`flux_balance_dict`](@ref) is one example of such argument, allowing you to specify details of the optimization procedure. [`screen`](@ref) function allows you to do precisely that -- apart from @@ -242,7 +242,7 @@ iterations needed for Tulip solver to find a feasible solution: screen(m, args = [(i,) for i in 5:15], # the iteration counts, packed in 1-tuples analysis = (m,a) -> # `args` elements get passed as the extra parameter here - flux_balance_analysis_vec(m, + flux_balance_vec(m, Tulip.Optimizer; modifications=[modify_optimizer_attribute("IPM_IterationsLimit", a)], ), diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 058d9ca75..9bbef41cf 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,7 +1,7 @@ # # Flux balance analysis (FBA) -# Here we use [`flux_balance_analysis`](@ref) and several related functions to +# Here we use [`flux_balance`](@ref) and several related functions to # find an optimal flux in the *E. coli* "core" model. We will need the model, # which we can download using [`download_model`](@ref): @@ -26,9 +26,9 @@ model = load_model("e_coli_core.json") # There are many possibilities on how to arrange the metabolic model into the # optimization framework and how to actually solve it. The "usual" assumed one # is captured in the default behavior of function -# [`flux_balance_analysis`](@ref): +# [`flux_balance`](@ref): -solution = flux_balance_analysis(model, Tulip.Optimizer) +solution = flux_balance(model, Tulip.Optimizer) @test isapprox(solution.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl index b2715bd6c..766d6c46a 100644 --- a/docs/src/examples/02a-optimizer-parameters.jl +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -3,7 +3,7 @@ # # Many optimizers require fine-tuning to produce best results. You can pass in # additional optimizer settings via the `modifications` parameter of -# [`flux_balance_analysis`](@ref). These include e.g. +# [`flux_balance`](@ref). These include e.g. # # - [`set_optimizer_attribute`](@ref) (typically allowing you to tune e.g. # iteration limits, tolerances, or floating-point precision) @@ -29,28 +29,22 @@ model = load_model("e_coli_core.json") # Running a FBA with a silent optimizer that has slightly increased iteration # limit for IPM algorithm may now look as follows: -solution = flux_balance_analysis( - ctmodel, +solution = flux_balance( + model, Tulip.Optimizer; - modifications = [ - silence - set_optimizer_attribute("IPM_IterationsLimit", 1000) - ], + modifications = [silence, set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) @test !isnothing(solution) #src -# To see some of the effects of the configuration changes, you may deliberately -# cripple the optimizer's possibilities to a few iterations, which will cause -# it to fail and return no solution: +# To see some of the effects of the configuration changes, you may e.g. +# deliberately cripple the optimizer's possibilities to a few iterations, which +# will cause it to fail and return no solution: -solution = flux_balance_analysis( - ctmodel, +solution = flux_balance( + model, Tulip.Optimizer; - modifications = [ - silence - set_optimizer_attribute("IPM_IterationsLimit", 1000) - ], + modifications = [silence, set_optimizer_attribute("IPM_IterationsLimit", 2)], ) println(solution) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index a1a7d2adc..584ba25ac 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -14,7 +14,7 @@ # # ## Changing the model-to-optimizer pipeline # -# TODO the stuff below: +# TODO clean up the stuff below: using COBREXA @@ -43,36 +43,22 @@ fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value forced_mixed_fermentation = ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created -vt = flux_balance_analysis( - forced_mixed_fermentation, - Tulip.Optimizer; - modifications = [silence], -) +vt = flux_balance(forced_mixed_fermentation, Tulip.Optimizer; modifications = [silence]) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src # Models that cannot be solved return `nothing`. In the example below, the # underlying model is modified. -ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) # TODO make mutable +ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) -vt = flux_balance_analysis(ctmodel, Tulip.Optimizer; modifications = [silence]) +vt = flux_balance(ctmodel, Tulip.Optimizer; modifications = [silence]) @test isnothing(vt) #src # Models can also be piped into the analysis functions ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert -vt = ctmodel |> flux_balance_analysis(Tulip.Optimizer; modifications = [silence]) +vt = ctmodel |> flux_balance(Tulip.Optimizer; modifications = [silence]) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src - -# Gene knockouts can be done with ease making use of the piping functionality. -# Here oxidative phosphorylation is knocked out. - -vt = - ctmodel |> - X.knockout!(["b0979", "b0734"], model) |> - X.flux_balance_analysis(Tulip.Optimizer; modifications = [X.silence]) - -@test isapprox(vt.objective, 0.21166, atol = TEST_TOLERANCE) #src diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 0b82866dc..14e8a2257 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -8,7 +8,6 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J -import SparseArrays: sparse include("types.jl") include("io.jl") @@ -18,8 +17,8 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") -include("analysis/modifications/optimizer_settings.jl") -include("analysis/flux_balance_analysis.jl") -include("analysis/parsimonious_flux_balance_analysis.jl") +include("analysis/modifications.jl") +include("analysis/flux_balance.jl") +include("analysis/parsimonious_flux_balance.jl") end # module COBREXA diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance.jl similarity index 69% rename from src/analysis/flux_balance_analysis.jl rename to src/analysis/flux_balance.jl index 343537002..f755b45fc 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance.jl @@ -26,26 +26,26 @@ Returns a [`C.ValueTree`](@ref). # Example ``` model = load_model("e_coli_core.json") -solution = flux_balance_analysis(model, GLPK.optimizer) +solution = flux_balance(model, GLPK.optimizer) ``` """ -function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; modifications = []) +function flux_balance(model::A.AbstractFBCModel, optimizer; modifications = []) ctmodel = fbc_model_constraints(model) - flux_balance_analysis(ctmodel, optimizer; modifications) + flux_balance(ctmodel, optimizer; modifications) end """ $(TYPEDSIGNATURES) -A variant of [`flux_balance_analysis`](@ref) that takes in a +A variant of [`flux_balance`](@ref) that takes in a [`C.ConstraintTree`](@ref) as the model to optimize. The objective is inferred from the field `objective` in `ctmodel`. All other arguments are forwarded. """ -function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) +function flux_balance(ctmodel::C.ConstraintTree, optimizer; modifications = []) opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) for mod in modifications - mod(ctmodel, opt_model) + mod(opt_model) end J.optimize!(opt_model) @@ -58,9 +58,8 @@ end """ $(TYPEDSIGNATURES) -Pipe-able variant of [`flux_balance_analysis`](@ref). +Pipe-able variant of [`flux_balance`](@ref). """ -flux_balance_analysis(optimizer; modifications = []) = - m -> flux_balance_analysis(m, optimizer; modifications) +flux_balance(optimizer; modifications = []) = m -> flux_balance(m, optimizer; modifications) -export flux_balance_analysis +export flux_balance diff --git a/src/analysis/modifications/optimizer_settings.jl b/src/analysis/modifications.jl similarity index 59% rename from src/analysis/modifications/optimizer_settings.jl rename to src/analysis/modifications.jl index ed8c8a664..585077480 100644 --- a/src/analysis/modifications/optimizer_settings.jl +++ b/src/analysis/modifications.jl @@ -1,4 +1,6 @@ +#TODO: at this point, consider renaming the whole thing to "settings" + """ $(TYPEDSIGNATURES) @@ -6,14 +8,18 @@ Change the objective sense of optimization. Possible arguments are `JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. """ set_objective_sense(objective_sense) = - (_, opt_model) -> J.set_objective_sense(opt_model, objective_sense) + opt_model -> J.set_objective_sense(opt_model, objective_sense) + +export set_objective_sense """ $(TYPEDSIGNATURES) Change the JuMP optimizer used to run the optimization. """ -set_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) +set_optimizer(optimizer) = opt_model -> J.set_optimizer(opt_model, optimizer) + +export set_optimizer """ $(TYPEDSIGNATURES) @@ -23,7 +29,9 @@ to the JuMP documentation and the documentation of the specific optimizer for usable keys and values. """ set_optimizer_attribute(attribute_key, value) = - (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) + opt_model -> J.set_optimizer_attribute(opt_model, attribute_key, value) + +export set_optimizer_attribute """ silence @@ -31,6 +39,6 @@ set_optimizer_attribute(attribute_key, value) = Modification that disable all output from the JuMP optimizer (shortcut for `set_silent` from JuMP). """ -const silence = (_, opt_model) -> J.set_silent(opt_model) +silence(opt_model) = J.set_silent(opt_model) -export set_objective_sense, set_optimizer, set_optimizer_attribute, silence +export silence diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance.jl similarity index 89% rename from src/analysis/parsimonious_flux_balance_analysis.jl rename to src/analysis/parsimonious_flux_balance.jl index 9e5405fe9..26d842a6e 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance.jl @@ -25,12 +25,12 @@ genome-scale models. Molecular Systems Biology, 6. 390. doi: accession:10.1038/msb.2010.47" for more details. pFBA gets the model optimum by standard FBA (using -[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then +[`flux_balance`](@ref) with `optimizer` and `modifications`), then finds a minimal total flux through the model that still satisfies the (slightly relaxed) optimum. This is done using a quadratic problem optimizer. If the original optimizer does not support quadratic optimization, it can be changed using the callback in `qp_modifications`, which are applied after the FBA. See -the documentation of [`flux_balance_analysis`](@ref) for usage examples of +the documentation of [`flux_balance`](@ref) for usage examples of modifications. The optimum relaxation sequence can be specified in `relax` parameter, it @@ -51,10 +51,10 @@ construct parsimonious models directly using # Example ``` model = load_model("e_coli_core.json") -parsimonious_flux_balance_analysis(model, biomass, Gurobi.Optimizer) |> values_vec +parsimonious_flux_balance(model, biomass, Gurobi.Optimizer) |> values_vec ``` """ -function parsimonious_flux_balance_analysis( +function parsimonious_flux_balance( model::C.ConstraintTree, optimizer; modifications = [], @@ -62,7 +62,7 @@ function parsimonious_flux_balance_analysis( relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], ) # Run FBA - opt_model = flux_balance_analysis(model, optimizer; modifications) + opt_model = flux_balance(model, optimizer; modifications) J.is_solved(opt_model) || return nothing # FBA failed # get the objective diff --git a/src/builders/core.jl b/src/builders/core.jl index 67a7179eb..97cf6d62a 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,5 +1,4 @@ -import AbstractFBCModels as F import SparseArrays: sparse """ @@ -8,13 +7,13 @@ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. """ -function fbc_model_constraints(model::F.AbstractFBCModel) - rxns = Symbol.(F.reactions(model)) - mets = Symbol.(F.metabolites(model)) - lbs, ubs = F.bounds(model) - stoi = F.stoichiometry(model) - bal = F.balance(model) - obj = F.objective(model) +function fbc_model_constraints(model::A.AbstractFBCModel) + rxns = Symbol.(A.reactions(model)) + mets = Symbol.(A.metabolites(model)) + lbs, ubs = A.bounds(model) + stoi = A.stoichiometry(model) + bal = A.balance(model) + obj = A.objective(model) #TODO: is sparse() required below? return C.ConstraintTree( diff --git a/src/io.jl b/src/io.jl index b00730da5..b3b59f640 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,6 +1,6 @@ """ - $(TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Load a FBC model representation while guessing the correct model type. Uses `AbstractFBCModels.load`. @@ -8,34 +8,34 @@ Load a FBC model representation while guessing the correct model type. Uses This overload almost always involves a search over types; do not use it in environments where performance is critical. """ -function load_model(path::String) where {A<:AbstractFBCModel} +function load_model(path::String) A.load(path) end """ - $(TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Load a FBC model representation. Uses `AbstractFBCModels.load`. """ -function load_model(model_type::Type{A}, path::String) where {A<:AbstractFBCModel} +function load_model(model_type::Type{T}, path::String) where {T<:A.AbstractFBCModel} A.load(model_type, path) end export load_model """ - $(TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Save a FBC model representation. Uses `AbstractFBCModels.save`. """ -function save_model(model::A, path::String) where {A<:AbstractFBCModel} +function save_model(model::T, path::String) where {T<:A.AbstractFBCModel} A.save(model, path) end export save_model """ - $(TYPEDSIGNATURES) +$(TYPEDSIGNATURES) Safely download a model with a known hash. All arguments are forwarded to `AbstractFBCModels.download_data_file` -- see the documentation in the diff --git a/test/runtests.jl b/test/runtests.jl index 3cdc86390..9fa038e87 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -28,7 +28,10 @@ end # set up the workers for Distributed, so that the tests that require more # workers do not unnecessarily load the stuff multiple times W = addprocs(2) -t = @elapsed @everywhere using COBREXA, Tulip, JuMP +t = @elapsed @everywhere begin + using COBREXA + import Tulip, JuMP +end # load the test models run_test_file("data_static.jl") From 5f6091de900df9c2ab71142915acf0c10b69bcf5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 17:49:35 +0100 Subject: [PATCH 380/531] pre-1.9 filter compat --- docs/src/reference/io.md | 7 ------- docs/src/reference/types.md | 7 ------- test/runtests.jl | 2 +- 3 files changed, 1 insertion(+), 15 deletions(-) delete mode 100644 docs/src/reference/io.md delete mode 100644 docs/src/reference/types.md diff --git a/docs/src/reference/io.md b/docs/src/reference/io.md deleted file mode 100644 index 5c494ea0a..000000000 --- a/docs/src/reference/io.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Model loading and saving - -```@autodocs -Modules = [COBREXA] -Pages = ["src/io.jl"] -``` diff --git a/docs/src/reference/types.md b/docs/src/reference/types.md deleted file mode 100644 index bb70365c6..000000000 --- a/docs/src/reference/types.md +++ /dev/null @@ -1,7 +0,0 @@ - -# Helper types - -```@autodocs -Modules = [COBREXA] -Pages = ["src/types.jl"] -``` diff --git a/test/runtests.jl b/test/runtests.jl index 9fa038e87..1bdc67ade 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -20,7 +20,7 @@ function run_test_file(path...) end function run_doc_examples() - for dir in readdir("../docs/src/examples", join = true) |> filter(endswith(".jl")) + for dir in filter(endswith(".jl"), readdir("../docs/src/examples", join = true)) run_test_file(dir) end end From 13d7883b393ae86a76bbc6cac3eef24ee8e2d23e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 21:25:02 +0100 Subject: [PATCH 381/531] some leftovers --- docs/make.jl | 10 +++++--- docs/src/reference.md | 59 ++++++++++++++++++++++++++++++++++++++++--- src/builders/genes.jl | 1 + 3 files changed, 63 insertions(+), 7 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 0b98cc0c3..253adb341 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -25,6 +25,7 @@ find_mds(path) = filter(x -> endswith(x, ".md"), readdir(joinpath(@__DIR__, "src", path))), ) +#TODO migrate this to Documenter-1, and make all checks strict # build the docs makedocs( modules = [COBREXA], @@ -51,10 +52,11 @@ makedocs( "Contents" => "concepts.md" find_mds("concepts") ], - "Reference" => [ - "Contents" => "reference.md" - find_mds("reference") - ], + "Reference" => "reference.md", + #[ # TODO re-add this when the reference gets bigger + #"Contents" => "reference.md" + #find_mds("reference") + #], ], ) diff --git a/docs/src/reference.md b/docs/src/reference.md index f0b01a550..51abbb38c 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -1,6 +1,59 @@ # API reference -```@contents -Pages = ["reference/types.md", "reference/io.md", "reference/builders.md", "reference/solver.md"] -Depth = 2 +## Helper types + +```@autodocs +Modules = [COBREXA] +Pages = ["src/types.jl"] +``` + +## Model loading and saving + +```@autodocs +Modules = [COBREXA] +Pages = ["src/io.jl"] +``` + +## Solver interface + +```@autodocs +Modules = [COBREXA] +Pages = ["src/solver.jl"] +``` + +## Constraint system building + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/core.jl"] ``` + +### Genetic constraints + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/genes.jl"] +``` + +### Objective function helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/builders/objectives.jl"] +``` + +## Analysis functions + +```@autodocs +Modules = [COBREXA] +Pages = ["src/analysis/flux_balance.jl", "src/analysis/parsimonious_flux_balance.jl"] +``` + +### Analysis modifications + +```@autodocs +Modules = [COBREXA] +Pages = ["src/analysis/modifications.jl"] +``` + +## Distributed analysis diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 63ed19c01..8cd1587dd 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -26,6 +26,7 @@ gene_knockouts(; end, ) +#TODO remove the bang from here, there's no side effect """ $(TYPEDSIGNATURES) """ From fe1ef35e773fa3ba8e34d14675abcfa093de3e4e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 21:29:27 +0100 Subject: [PATCH 382/531] format --- docs/make.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index 253adb341..1e716e90c 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -54,8 +54,8 @@ makedocs( ], "Reference" => "reference.md", #[ # TODO re-add this when the reference gets bigger - #"Contents" => "reference.md" - #find_mds("reference") + #"Contents" => "reference.md" + #find_mds("reference") #], ], ) From 15c369b8e0884b88af4a6f196baeceb182ab7976 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 7 Dec 2023 21:32:08 +0100 Subject: [PATCH 383/531] fix ws --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index caac77fde..e9648181c 100644 --- a/.gitignore +++ b/.gitignore @@ -24,4 +24,4 @@ Manifest.toml # ignore models *.xml *.json -*.mat \ No newline at end of file +*.mat From cc7cd5e7fb01ef37ededaff44da9c6f1dd6a75b7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 8 Dec 2023 16:00:01 +0100 Subject: [PATCH 384/531] format --- docs/src/examples/03-qp-problems.jl | 28 ++++++++++++------- src/analysis/flux_balance_analysis.jl | 10 ++----- .../parsimonious_flux_balance_analysis.jl | 6 ++-- src/builders/genes.jl | 2 +- 4 files changed, 25 insertions(+), 21 deletions(-) diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl index a835bae0b..ea8592c4a 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-qp-problems.jl @@ -28,14 +28,14 @@ vt = X.parsimonious_flux_balance_analysis(model, Clarabel.Optimizer) model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; modifications = [X.silence]) -@test isapprox(vt.objective, 0.87392; atol=TEST_TOLERANCE) #src -@test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol=QP_TEST_TOLERANCE) #src +@test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src +@test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol = QP_TEST_TOLERANCE) #src # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). ctmodel = X.fbc_model_constraints(model) -ctmodel *= :l2objective ^ X.squared_sum_objective(ctmodel.fluxes) +ctmodel *= :l2objective^X.squared_sum_objective(ctmodel.fluxes) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks opt_model = X.optimization_model( @@ -46,12 +46,12 @@ opt_model = X.optimization_model( ) X.J.optimize!(opt_model) # JuMP is called J in COBREXA - + X.is_solved(opt_model) # check if solved vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA -@test isapprox(vt.l2objective, ?; atol=QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds +@test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds # It is likewise as simple to run MOMA using the convenience functions. @@ -61,15 +61,23 @@ vt = X.minimize_metabolic_adjustment_analysis(model, ref_sol, Gurobi.Optimizer) # Or use the piping functionality -model |> X.minimize_metabolic_adjustment_analysis(ref_sol, Clarabel.Optimizer; modifications = [X.silence]) +model |> X.minimize_metabolic_adjustment_analysis( + ref_sol, + Clarabel.Optimizer; + modifications = [X.silence], +) -@test isapprox(vt.:momaobjective, 0.81580806; atol=TEST_TOLERANCE) #src +@test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). ctmodel = X.fbc_model_constraints(model) -ctmodel *= :minoxphospho ^ X.squared_sum_error_objective(ctmodel.fluxes, Dict(:ATPS4r => 33.0, :CYTBD => 22.0,)) +ctmodel *= + :minoxphospho^X.squared_sum_error_objective( + ctmodel.fluxes, + Dict(:ATPS4r => 33.0, :CYTBD => 22.0), + ) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks opt_model = X.optimization_model( @@ -80,9 +88,9 @@ opt_model = X.optimization_model( ) X.J.optimize!(opt_model) # JuMP is called J in COBREXA - + X.is_solved(opt_model) # check if solved vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA -@test isapprox(vt.l2objective, ?; atol=QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds +@test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl index 2de61855c..343537002 100644 --- a/src/analysis/flux_balance_analysis.jl +++ b/src/analysis/flux_balance_analysis.jl @@ -42,18 +42,14 @@ A variant of [`flux_balance_analysis`](@ref) that takes in a from the field `objective` in `ctmodel`. All other arguments are forwarded. """ function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) - opt_model = optimization_model( - ctmodel; - objective = ctmodel.objective.value, - optimizer, - ) + opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) for mod in modifications mod(ctmodel, opt_model) end J.optimize!(opt_model) - + is_solved(opt_model) || return nothing C.ValueTree(ctmodel, J.value.(opt_model[:x])) @@ -67,4 +63,4 @@ Pipe-able variant of [`flux_balance_analysis`](@ref). flux_balance_analysis(optimizer; modifications = []) = m -> flux_balance_analysis(m, optimizer; modifications) -export flux_balance_analysis \ No newline at end of file +export flux_balance_analysis diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl index 162d0e97a..e52e412a7 100644 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ b/src/analysis/parsimonious_flux_balance_analysis.jl @@ -65,8 +65,8 @@ function parsimonious_flux_balance_analysis( # get the objective Z = J.objective_value(opt_model) - - _ctmodel = ctmodel * :pfbaobjective ^ squared_sum_objective(ctmodel.fluxes) + + _ctmodel = ctmodel * :pfbaobjective^squared_sum_objective(ctmodel.fluxes) _ctmodel.objective.bound = relax == 1.0 ? Z : objective_bounds(relax)(Z) # TODO currently breaks opt_model = X.optimization_model( @@ -80,7 +80,7 @@ function parsimonious_flux_balance_analysis( for mod in qp_modifications mod(model, opt_model) end - + J.optimize!(opt_model) C.ValueTree(_ctmodel, J.value.(opt_model[:x])) diff --git a/src/builders/genes.jl b/src/builders/genes.jl index ae0e27368..8cd1587dd 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -16,7 +16,7 @@ gene_knockouts(; model::A.AbstractFBCModel, ) = knockout_constraints(; fluxes, - knockout_test = rxn -> begin + knockout_test = rxn -> begin maybe_avail = A.reaction_gene_products_available( model, string(rxn), From 800c4718743fe995c47d89bb9f00d22d09ae5cc4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 8 Dec 2023 19:35:29 +0100 Subject: [PATCH 385/531] simplify stuff --- src/COBREXA.jl | 5 +- src/analysis/flux_balance.jl | 34 ++---- src/analysis/flux_balance_analysis.jl | 66 ----------- ...sis.jl => minimal_metabolic_adjustment.jl} | 41 +++---- .../modifications/optimizer_settings.jl | 36 ------ .../parsimonious_flux_balance_analysis.jl | 107 ------------------ src/builders/objectives.jl | 21 ++-- src/misc/bounds.jl | 20 ++++ src/solver.jl | 23 ++++ 9 files changed, 81 insertions(+), 272 deletions(-) delete mode 100644 src/analysis/flux_balance_analysis.jl rename src/analysis/{minimize_metabolic_adjustment_analysis.jl => minimal_metabolic_adjustment.jl} (59%) delete mode 100644 src/analysis/modifications/optimizer_settings.jl delete mode 100644 src/analysis/parsimonious_flux_balance_analysis.jl create mode 100644 src/misc/bounds.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index f99f7358f..b03902860 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -9,7 +9,6 @@ import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J import SparseArrays: sparse -import LinearAlgebra: dot include("types.jl") include("io.jl") @@ -22,6 +21,8 @@ include("builders/objectives.jl") include("analysis/modifications.jl") include("analysis/flux_balance.jl") include("analysis/parsimonious_flux_balance.jl") -include("analysis/minimize_metabolic_adjustment.jl") +include("analysis/minimal_metabolic_adjustment.jl") + +include("misc/bounds.jl") end # module COBREXA diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index f755b45fc..bb58d8dbf 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -29,36 +29,20 @@ model = load_model("e_coli_core.json") solution = flux_balance(model, GLPK.optimizer) ``` """ -function flux_balance(model::A.AbstractFBCModel, optimizer; modifications = []) - ctmodel = fbc_model_constraints(model) - flux_balance(ctmodel, optimizer; modifications) +function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...) + constraints = fbc_model_constraints(model) + optimize_constraints( + constraints; + objective = constraints.objective, + optimizer, + kwargs..., + ) end """ $(TYPEDSIGNATURES) -A variant of [`flux_balance`](@ref) that takes in a -[`C.ConstraintTree`](@ref) as the model to optimize. The objective is inferred -from the field `objective` in `ctmodel`. All other arguments are forwarded. -""" -function flux_balance(ctmodel::C.ConstraintTree, optimizer; modifications = []) - opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) - - for mod in modifications - mod(opt_model) - end - - J.optimize!(opt_model) - - is_solved(opt_model) || return nothing - - C.ValueTree(ctmodel, J.value.(opt_model[:x])) -end - -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`flux_balance`](@ref). +Pipe-able overload of [`flux_balance`](@ref). """ flux_balance(optimizer; modifications = []) = m -> flux_balance(m, optimizer; modifications) diff --git a/src/analysis/flux_balance_analysis.jl b/src/analysis/flux_balance_analysis.jl deleted file mode 100644 index 343537002..000000000 --- a/src/analysis/flux_balance_analysis.jl +++ /dev/null @@ -1,66 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run flux balance analysis (FBA) on the `model`, optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization -problem: -``` -max cᵀx -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat -Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more -information. - -The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer`. - -Optionally, you may specify one or more modifications to be applied to the model -before the analysis, such as [`set_objective_sense`](@ref), -[`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and -[`silence`](@ref). - -Returns a [`C.ValueTree`](@ref). - -# Example -``` -model = load_model("e_coli_core.json") -solution = flux_balance_analysis(model, GLPK.optimizer) -``` -""" -function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; modifications = []) - ctmodel = fbc_model_constraints(model) - flux_balance_analysis(ctmodel, optimizer; modifications) -end - -""" -$(TYPEDSIGNATURES) - -A variant of [`flux_balance_analysis`](@ref) that takes in a -[`C.ConstraintTree`](@ref) as the model to optimize. The objective is inferred -from the field `objective` in `ctmodel`. All other arguments are forwarded. -""" -function flux_balance_analysis(ctmodel::C.ConstraintTree, optimizer; modifications = []) - opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) - - for mod in modifications - mod(ctmodel, opt_model) - end - - J.optimize!(opt_model) - - is_solved(opt_model) || return nothing - - C.ValueTree(ctmodel, J.value.(opt_model[:x])) -end - -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`flux_balance_analysis`](@ref). -""" -flux_balance_analysis(optimizer; modifications = []) = - m -> flux_balance_analysis(m, optimizer; modifications) - -export flux_balance_analysis diff --git a/src/analysis/minimize_metabolic_adjustment_analysis.jl b/src/analysis/minimal_metabolic_adjustment.jl similarity index 59% rename from src/analysis/minimize_metabolic_adjustment_analysis.jl rename to src/analysis/minimal_metabolic_adjustment.jl index 3f9370075..d586a3aa8 100644 --- a/src/analysis/minimize_metabolic_adjustment_analysis.jl +++ b/src/analysis/minimal_metabolic_adjustment.jl @@ -24,35 +24,35 @@ model = load_model("e_coli_core.json") ``` """ -function minimize_metabolic_adjustment_analysis( - ctmodel::C.ConstraintTree, +function minimal_metabolic_adjustment( + constraints::C.ConstraintTree, reference_solution::Dict{String,Float64}, optimizer; modifications = [], ) - _ctmodel = - ctmodel * + _constraints = + constraints * :momaobjective^squared_sum_error_objective( - ctmodel.fluxes, + constraints.fluxes, Dict(Symbol(k) => float(v) for (k, v) in reference_solution), ) opt_model = optimization_model( - _ctmodel; - objective = _ctmodel.momaobjective.value, + _constraints; + objective = _constraints.momaobjective.value, optimizer, sense = J.MIN_SENSE, ) for mod in modifications - mod(ctmodel, opt_model) + mod(constraints, opt_model) end J.optimize!(opt_model) is_solved(opt_model) || return nothing - C.ValueTree(_ctmodel, J.value.(opt_model[:x])) + C.ValueTree(_constraints, J.value.(opt_model[:x])) end """ @@ -60,24 +60,9 @@ $(TYPEDSIGNATURES) Variant that takes an [`A.AbstractFBCModel`](@ref) as input. All other arguments are forwarded. """ -function minimize_metabolic_adjustment_analysis( - model::A.AbstractFBCModel, - args...; - kwargs..., -) - ctmodel = fbc_model_constraints(model) - minimize_metabolic_adjustment_analysis(ctmodel, args...; kwargs...) +function minimal_metabolic_adjustment(model::A.AbstractFBCModel, args...; kwargs...) + constraints = fbc_model_constraints(model) + minimal_metabolic_adjustment(constraints, args...; kwargs...) end -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`minimize_metabolic_adjustment_analysis`](@ref). -""" -minimize_metabolic_adjustment_analysis( - reference_solution::Dict{String,Float64}, - optimizer; - kwargs..., -) = m -> minimize_metabolic_adjustment_analysis(m, reference_solution, optimizer; kwargs...) - -export minimize_metabolic_adjustment_analysis +export minimal_metabolic_adjustment diff --git a/src/analysis/modifications/optimizer_settings.jl b/src/analysis/modifications/optimizer_settings.jl deleted file mode 100644 index ed8c8a664..000000000 --- a/src/analysis/modifications/optimizer_settings.jl +++ /dev/null @@ -1,36 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Change the objective sense of optimization. Possible arguments are -`JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. -""" -set_objective_sense(objective_sense) = - (_, opt_model) -> J.set_objective_sense(opt_model, objective_sense) - -""" -$(TYPEDSIGNATURES) - -Change the JuMP optimizer used to run the optimization. -""" -set_optimizer(optimizer) = (_, opt_model) -> J.set_optimizer(opt_model, optimizer) - -""" -$(TYPEDSIGNATURES) - -Change a JuMP optimizer attribute. The attributes are optimizer-specific, refer -to the JuMP documentation and the documentation of the specific optimizer for -usable keys and values. -""" -set_optimizer_attribute(attribute_key, value) = - (_, opt_model) -> J.set_optimizer_attribute(opt_model, attribute_key, value) - -""" - silence - -Modification that disable all output from the JuMP optimizer (shortcut for -`set_silent` from JuMP). -""" -const silence = (_, opt_model) -> J.set_silent(opt_model) - -export set_objective_sense, set_optimizer, set_optimizer_attribute, silence diff --git a/src/analysis/parsimonious_flux_balance_analysis.jl b/src/analysis/parsimonious_flux_balance_analysis.jl deleted file mode 100644 index e52e412a7..000000000 --- a/src/analysis/parsimonious_flux_balance_analysis.jl +++ /dev/null @@ -1,107 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA -runs two consecutive optimization problems. The first is traditional FBA: -``` -max cᵀx = μ -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -And the second is a quadratic optimization problem: -``` -min Σᵢ xᵢ² -s.t. S x = b - xₗ ≤ x ≤ xᵤ - μ = μ⁰ -``` -Where the optimal solution of the FBA problem, μ⁰, has been added as an -additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, -Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, -Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, -Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data -from evolved E. coli are consistent with computed optimal growth from -genome-scale models. Molecular Systems Biology, 6. 390. doi: -accession:10.1038/msb.2010.47" for more details. - -pFBA gets the model optimum by standard FBA (using -[`flux_balance_analysis`](@ref) with `optimizer` and `modifications`), then -finds a minimal total flux through the model that still satisfies the (slightly -relaxed) optimum. This is done using a quadratic problem optimizer. If the -original optimizer does not support quadratic optimization, it can be changed -using the callback in `qp_modifications`, which are applied after the FBA. See -the documentation of [`flux_balance_analysis`](@ref) for usage examples of -modifications. - -The `relax` parameter, which defaults to `0.999`, relaxes the objective bound of -the original bound to prevent numerical issues. - -Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be -found. - -# Example -``` -model = load_model("e_coli_core.json") -parsimonious_flux_balance_analysis(model, Gurobi.Optimizer) -``` -""" -function parsimonious_flux_balance_analysis( - ctmodel::C.ConstraintTree, - optimizer; - modifications = [], - qp_modifications = [], - relax = 0.999, -) - # Run FBA - opt_model = optimization_model(ctmodel; objective = ctmodel.objective.value, optimizer) - - for mod in modifications - mod(ctmodel, opt_model) - end - - J.optimize!(opt_model) - - is_solved(opt_model) || return nothing - - # get the objective - Z = J.objective_value(opt_model) - - _ctmodel = ctmodel * :pfbaobjective^squared_sum_objective(ctmodel.fluxes) - _ctmodel.objective.bound = relax == 1.0 ? Z : objective_bounds(relax)(Z) # TODO currently breaks - - opt_model = X.optimization_model( - _ctmodel; - objective = ctmodel.pfbaobjective.value, - optimizer, - sense = X.J.MIN_SENSE, - ) - - # prepare the model for pFBA - for mod in qp_modifications - mod(model, opt_model) - end - - J.optimize!(opt_model) - - C.ValueTree(_ctmodel, J.value.(opt_model[:x])) -end - -""" -$(TYPEDSIGNATURES) - -Variant that takes an [`A.AbstractFBCModel`](@ref) as input. All other arguments are forwarded. -""" -function parsimonious_flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) - ctmodel = fbc_model_constraints(model) - parsimonious_flux_balance_analysis(ctmodel, optimizer; kwargs...) -end - -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`parsimonious_flux_balance_analysis`](@ref). -""" -parsimonious_flux_balance_analysis(optimizer; modifications = []) = - m -> parsimonious_flux_balance_analysis(m, optimizer; modifications) - -export parsimonious_flux_balance_analysis diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 7bc9075f5..c27ca5538 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,4 +1,17 @@ +""" +$(TYPEDSIGNATURES) + +TODO +""" +squared_sum_objective(x::C.ConstraintTree) = + squared_sum_error_objective(x, Dict(keys(x) .=> 0.0)) + +""" +$(TYPEDSIGNATURES) + +TODO +""" squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = C.Constraint( sum( @@ -6,11 +19,3 @@ squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,F (k, c) in constraints if haskey(target, k) ), ) - -squared_sum_objective(x::C.ConstraintTree) = - squared_sum_error_objective(x, Dict(keys(x) .=> 0.0)) - -objective_bounds(tolerance) = z -> begin - vs = (z * tolerance, z / tolerance) - (minimum(vs), maximum(vs)) -end diff --git a/src/misc/bounds.jl b/src/misc/bounds.jl new file mode 100644 index 000000000..5ecf68cc3 --- /dev/null +++ b/src/misc/bounds.jl @@ -0,0 +1,20 @@ + +""" +$(TYPEDSIGNATURES) + +TODO +""" +absolute_tolerance_bound(tolerance) = x -> begin + bound = (x - tolerance, x + tolerance) + (minimum(bound), maximum(bound)) +end + +""" +$(TYPEDSIGNATURES) + +TODO +""" +relative_tolerance_bound(tolerance) = x -> begin + bound = (x * tolerance, x / tolerance) + (minimum(bound), maximum(bound)) +end diff --git a/src/solver.jl b/src/solver.jl index c1a3b2b7b..ed0255f17 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -47,3 +47,26 @@ is_solved(opt_model::J.Model) = J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] export is_solved + +""" +$(TYPEDSIGNATURES) + +Make an JuMP model out of `constraints` using [`optimization_model`](@ref) +(most arguments are forwarded there), then apply the modifications, optimize +the model, and return either `nothing` if the optimization failed, or `output` +substituted with the solved values (`output` defaults to `constraints`. +""" +function optimize_constraints( + constraints::C.ConstraintTree, + args...; + modifications = [], + output = constraints, + kwargs..., +) + om = optimization_model(constraints, args..., kwargs...) + for m in modifications + m(om) + end + J.optimize!(om) + is_solved(om) ? C.ValueTree(output, J.value.(opt_model[:x])) : nothing +end From a1c2edf47aac71bfd6695acf3af9d332cad53f07 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 11:34:06 +0100 Subject: [PATCH 386/531] update for CT 0.6 --- Project.toml | 2 +- src/analysis/flux_balance.jl | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Project.toml b/Project.toml index 14daa42be..ee0651e56 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2" Aqua = "0.7" Clarabel = "0.3" -ConstraintTrees = "0.5" +ConstraintTrees = "0.6" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index f755b45fc..38eeec3dc 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -21,7 +21,7 @@ before the analysis, such as [`set_objective_sense`](@ref), [`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and [`silence`](@ref). -Returns a [`C.ValueTree`](@ref). +Returns a solved tree. # Example ``` @@ -52,7 +52,7 @@ function flux_balance(ctmodel::C.ConstraintTree, optimizer; modifications = []) is_solved(opt_model) || return nothing - C.ValueTree(ctmodel, J.value.(opt_model[:x])) + C.constraint_values(ctmodel, J.value.(opt_model[:x])) end """ From a30eea65fd9b2773d83c35e0a36e593749b61844 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 14:55:20 +0100 Subject: [PATCH 387/531] new functionality in CTs 0.6 --- Project.toml | 3 +-- docs/src/examples/03-qp-problems.jl | 4 ++-- src/analysis/flux_balance.jl | 5 +++-- src/solver.jl | 10 +++++----- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/Project.toml b/Project.toml index 544f065b4..40fdc022c 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2" Aqua = "0.7" Clarabel = "0.3" -ConstraintTrees = "0.5" +ConstraintTrees = "0.6" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" @@ -44,7 +44,6 @@ SBMLFBCModels = "3e8f9d1a-ffc1-486d-82d6-6c7276635980" SHA = "ea8e919c-243c-51af-8825-aaa63cd721ce" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" Tulip = "6dd1b50a-3aae-11e9-10b5-ef983d2400fa" -JSONFBCModels = "475c1105-d6ed-49c1-9b32-c11adca6d3e8" [targets] test = ["Aqua", "Clarabel", "Downloads", "GLPK", "JSONFBCModels", "SBMLFBCModels", "SHA", "Test", "Tulip"] diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl index ea8592c4a..7109652e7 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-qp-problems.jl @@ -49,7 +49,7 @@ X.J.optimize!(opt_model) # JuMP is called J in COBREXA X.is_solved(opt_model) # check if solved -vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds @@ -91,6 +91,6 @@ X.J.optimize!(opt_model) # JuMP is called J in COBREXA X.is_solved(opt_model) # check if solved -vt = X.C.ValueTree(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index bb58d8dbf..0b9ad1d21 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -21,7 +21,8 @@ before the analysis, such as [`set_objective_sense`](@ref), [`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and [`silence`](@ref). -Returns a [`C.ValueTree`](@ref). +Returns a tree with the optimization solution of the same shape as the model +defined by [`fbc_model_constraints`](@ref). # Example ``` @@ -33,7 +34,7 @@ function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...) constraints = fbc_model_constraints(model) optimize_constraints( constraints; - objective = constraints.objective, + objective = constraints.objective.value, optimizer, kwargs..., ) diff --git a/src/solver.jl b/src/solver.jl index ed0255f17..cdd496ce2 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -7,8 +7,8 @@ JuMP `Model` created for solving in `optimizer`, with a given optional `objective` and optimization `sense`. """ function optimization_model( - cs::C.ConstraintTree; - objective::Union{Nothing,C.LinearValue,C.QuadraticValue} = nothing, + cs::C.ConstraintTreeElem; + objective::Union{Nothing,C.Value} = nothing, optimizer, sense = J.MAX_SENSE, ) @@ -57,16 +57,16 @@ the model, and return either `nothing` if the optimization failed, or `output` substituted with the solved values (`output` defaults to `constraints`. """ function optimize_constraints( - constraints::C.ConstraintTree, + constraints::C.ConstraintTreeElem, args...; modifications = [], output = constraints, kwargs..., ) - om = optimization_model(constraints, args..., kwargs...) + om = optimization_model(constraints, args...; kwargs...) for m in modifications m(om) end J.optimize!(om) - is_solved(om) ? C.ValueTree(output, J.value.(opt_model[:x])) : nothing + is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing end From f1884dbbb5e3a69cc44ed8e782120a8ce8deeac6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 16:34:07 +0100 Subject: [PATCH 388/531] isolate errors in tested doc examples --- test/runtests.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 1bdc67ade..88b05b85b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -21,7 +21,9 @@ end function run_doc_examples() for dir in filter(endswith(".jl"), readdir("../docs/src/examples", join = true)) - run_test_file(dir) + @testset "docs/$(basename(dir))" begin + run_test_file(dir) + end end end From a985ffa8e4ca0e8a3a6697b5e311a8877af0d056 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 16:35:19 +0100 Subject: [PATCH 389/531] start cleaning out the various names for optimization --- .../examples/02c-constraint-modifications.jl | 23 ++++++++++++++++--- docs/src/examples/03-qp-problems.jl | 17 ++++++-------- src/analysis/flux_balance.jl | 2 +- src/solver.jl | 4 +++- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 584ba25ac..3ff451473 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -43,7 +43,12 @@ fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value forced_mixed_fermentation = ctmodel * :fermentation^C.Constraint(fermentation, (10.0, 1000.0)) # new modified model is created -vt = flux_balance(forced_mixed_fermentation, Tulip.Optimizer; modifications = [silence]) +vt = optimized_constraints( + forced_mixed_fermentation, + objective = forced_mixed_fermentation.objective.value, + optimizer = Tulip.Optimizer, + modifications = [silence], +) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src @@ -52,13 +57,25 @@ vt = flux_balance(forced_mixed_fermentation, Tulip.Optimizer; modifications = [s ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) -vt = flux_balance(ctmodel, Tulip.Optimizer; modifications = [silence]) +#TODO explicitly show here how false sharing looks like + +vt = optimized_constraints( + ctmodel, + objective = ctmodel.objective.value, + optimizer = Tulip.Optimizer, + modifications = [silence], +) @test isnothing(vt) #src # Models can also be piped into the analysis functions ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert -vt = ctmodel |> flux_balance(Tulip.Optimizer; modifications = [silence]) +vt = optimized_constraints( + ctmodel, + objective = ctmodel.objective.value, + optimizer = Tulip.Optimizer, + modifications = [silence], +) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl index 7109652e7..329088f81 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-qp-problems.jl @@ -1,8 +1,8 @@ # # Quandratic objective flux balance analysis type problems -# We will use [`parsimonious_flux_balance_analysis`](@ref) and -# [`minimize_metabolic_adjustment_analysis`](@ref) to find the optimal flux +# We will use [`parsimonious_flux_balance`](@ref) and +# [`minimize_metabolic_adjustment`](@ref) to find the optimal flux # distribution in the *E. coli* "core" model. # If it is not already present, download the model and load the package: @@ -22,11 +22,11 @@ model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model # Use the convenience function to run standard pFBA -vt = X.parsimonious_flux_balance_analysis(model, Clarabel.Optimizer) +vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer) # Or use the piping functionality -model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; modifications = [X.silence]) +model |> parsimonious_flux_balance(Clarabel.Optimizer; modifications = [X.silence]) @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src @test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol = QP_TEST_TOLERANCE) #src @@ -57,15 +57,12 @@ vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees ref_sol = Dict("ATPS4r" => 33.0, "CYTBD" => 22.0) -vt = X.minimize_metabolic_adjustment_analysis(model, ref_sol, Gurobi.Optimizer) +vt = X.minimize_metabolic_adjustment(model, ref_sol, Gurobi.Optimizer) # Or use the piping functionality -model |> X.minimize_metabolic_adjustment_analysis( - ref_sol, - Clarabel.Optimizer; - modifications = [X.silence], -) +model |> +X.minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; modifications = [X.silence]) @test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index 0b9ad1d21..fd3583ebe 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -32,7 +32,7 @@ solution = flux_balance(model, GLPK.optimizer) """ function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...) constraints = fbc_model_constraints(model) - optimize_constraints( + optimized_constraints( constraints; objective = constraints.objective.value, optimizer, diff --git a/src/solver.jl b/src/solver.jl index cdd496ce2..4a6e41aef 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -56,7 +56,7 @@ Make an JuMP model out of `constraints` using [`optimization_model`](@ref) the model, and return either `nothing` if the optimization failed, or `output` substituted with the solved values (`output` defaults to `constraints`. """ -function optimize_constraints( +function optimized_constraints( constraints::C.ConstraintTreeElem, args...; modifications = [], @@ -70,3 +70,5 @@ function optimize_constraints( J.optimize!(om) is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing end + +export optimized_constraints From 2e0ec3c58d6b74eff04fe26de2c79b12ceffdb12 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 16:39:05 +0100 Subject: [PATCH 390/531] kill trailing spaces --- docs/src/examples/03-qp-problems.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl index 329088f81..b0f1c5147 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-qp-problems.jl @@ -31,7 +31,7 @@ model |> parsimonious_flux_balance(Clarabel.Optimizer; modifications = [X.silenc @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src @test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol = QP_TEST_TOLERANCE) #src -# Alternatively, you can construct your own constraint tree model with +# Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). ctmodel = X.fbc_model_constraints(model) @@ -53,7 +53,7 @@ vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds -# It is likewise as simple to run MOMA using the convenience functions. +# It is likewise as simple to run MOMA using the convenience functions. ref_sol = Dict("ATPS4r" => 33.0, "CYTBD" => 22.0) @@ -66,7 +66,7 @@ X.minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; modifications = [X. @test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src -# Alternatively, you can construct your own constraint tree model with +# Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). ctmodel = X.fbc_model_constraints(model) From 48cbc6ed0540d8b75504882c7e0d8a9242c5d425 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 19:55:51 +0100 Subject: [PATCH 391/531] split pFBA into CT and easy-modeling part --- docs/src/examples/03-qp-problems.jl | 2 +- src/analysis/flux_balance.jl | 62 ++++---- src/analysis/parsimonious_flux_balance.jl | 171 ++++++++++++---------- src/builders/objectives.jl | 8 +- src/solver.jl | 25 ---- 5 files changed, 133 insertions(+), 135 deletions(-) diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-qp-problems.jl index b0f1c5147..19bdc56d5 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-qp-problems.jl @@ -22,7 +22,7 @@ model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model # Use the convenience function to run standard pFBA -vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer) +vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer; modifications = [X.silence]) # Or use the piping functionality diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index fd3583ebe..745c99896 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -1,34 +1,40 @@ """ $(TYPEDSIGNATURES) -Run flux balance analysis (FBA) on the `model`, optionally specifying -`modifications` to the problem. Basically, FBA solves this optimization -problem: -``` -max cᵀx -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -See "Orth, J., Thiele, I. & Palsson, B. What is flux balance analysis?. Nat -Biotechnol 28, 245-248 (2010). https://doi.org/10.1038/nbt.1614" for more -information. - -The `optimizer` must be set to a `JuMP`-compatible optimizer, such as -`GLPK.Optimizer` or `Tulip.Optimizer`. - -Optionally, you may specify one or more modifications to be applied to the model -before the analysis, such as [`set_objective_sense`](@ref), -[`set_optimizer`](@ref), [`set_optimizer_attribute`](@ref), and -[`silence`](@ref). - -Returns a tree with the optimization solution of the same shape as the model -defined by [`fbc_model_constraints`](@ref). - -# Example -``` -model = load_model("e_coli_core.json") -solution = flux_balance(model, GLPK.optimizer) -``` +Make an JuMP model out of `constraints` using [`optimization_model`](@ref) +(most arguments are forwarded there), then apply the `modifications`, optimize +the model, and return either `nothing` if the optimization failed, or `output` +substituted with the solved values (`output` defaults to `constraints`. + +For a "nice" version for simpler finding of metabolic model optima, use +[`flux_balance`](@ref). +""" +function optimized_constraints( + constraints::C.ConstraintTreeElem, + args...; + modifications = [], + output = constraints, + kwargs..., +) + om = optimization_model(constraints, args...; kwargs...) + for m in modifications + m(om) + end + J.optimize!(om) + is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing +end + +export optimized_constraints + +""" +$(TYPEDSIGNATURES) + +Compute an optimal objective-optimizing solution of the given `model`. + +Most arguments are forwarded to [`optimized_constraints`](@ref). + +Returns a tree with the optimization solution of the same shape as +given by [`fbc_model_constraints`](@ref). """ function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...) constraints = fbc_model_constraints(model) diff --git a/src/analysis/parsimonious_flux_balance.jl b/src/analysis/parsimonious_flux_balance.jl index 26d842a6e..0658b0d73 100644 --- a/src/analysis/parsimonious_flux_balance.jl +++ b/src/analysis/parsimonious_flux_balance.jl @@ -1,92 +1,111 @@ + """ $(TYPEDSIGNATURES) -Run parsimonious flux balance analysis (pFBA) on the `model`. In short, pFBA -runs two consecutive optimization problems. The first is traditional FBA: -``` -max cᵀx = μ -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -And the second is a quadratic optimization problem: -``` -min Σᵢ xᵢ² -s.t. S x = b - xₗ ≤ x ≤ xᵤ - μ = μ⁰ -``` -Where the optimal solution of the FBA problem, μ⁰, has been added as an -additional constraint. See "Lewis, Nathan E, Hixson, Kim K, Conrad, Tom M, -Lerman, Joshua A, Charusanti, Pep, Polpitiya, Ashoka D, Adkins, Joshua N, -Schramm, Gunnar, Purvine, Samuel O, Lopez-Ferrer, Daniel, Weitz, Karl K, Eils, -Roland, König, Rainer, Smith, Richard D, Palsson, Bernhard Ø, (2010) Omic data -from evolved E. coli are consistent with computed optimal growth from -genome-scale models. Molecular Systems Biology, 6. 390. doi: -accession:10.1038/msb.2010.47" for more details. - -pFBA gets the model optimum by standard FBA (using -[`flux_balance`](@ref) with `optimizer` and `modifications`), then -finds a minimal total flux through the model that still satisfies the (slightly -relaxed) optimum. This is done using a quadratic problem optimizer. If the -original optimizer does not support quadratic optimization, it can be changed -using the callback in `qp_modifications`, which are applied after the FBA. See -the documentation of [`flux_balance`](@ref) for usage examples of -modifications. - -The optimum relaxation sequence can be specified in `relax` parameter, it -defaults to multiplicative range of `[1.0, 0.999999, ..., 0.99]` of the original -bound. - -Returns an optimized model that contains the pFBA solution (or an unsolved model -if something went wrong). - -# Performance - -This implementation attempts to save time by executing all pFBA steps on a -single instance of the optimization model problem, trading off possible -flexibility. For slightly less performant but much more flexible use, one can -construct parsimonious models directly using -[`with_parsimonious_objective`](@ref). - -# Example -``` -model = load_model("e_coli_core.json") -parsimonious_flux_balance(model, biomass, Gurobi.Optimizer) |> values_vec -``` +Optimize the system of `constraints` to get the optimal `objective` value. Then +try to find a "parsimonious" solution with the same `objective` value, which +optimizes the `parsimonious_objective` (possibly also switching optimization +sense, optimizer, and adding more modifications). + +For efficiency, everything is performed on a single instance of JuMP model. + +A simpler version suitable for direct work with metabolic models is available +in [`parsimonious_flux_balance`](@ref). """ -function parsimonious_flux_balance( - model::C.ConstraintTree, - optimizer; +function parsimonious_optimized_constraints( + constraints::C.ConstraintTreeElem, + args...; + objective::C.Value, modifications = [], - qp_modifications = [], - relax_bounds = [1.0, 0.999999, 0.99999, 0.9999, 0.999, 0.99], + parsimonious_objective::C.Value, + parsimonious_optimizer = nothing, + parsimonious_sense = J.MIN_SENSE, + parsimonious_modifications = [], + tolerances = [absolute_tolerance_bound(0)], + output = constraints, + kwargs..., ) - # Run FBA - opt_model = flux_balance(model, optimizer; modifications) - J.is_solved(opt_model) || return nothing # FBA failed - # get the objective - Z = J.objective_value(opt_model) - original_objective = J.objective_function(opt_model) + # first solve the optimization problem with the original objective + om = optimization_model(constraints, args...; kwargs...) + for m in modifications + m(om) + end + J.optimize!(om) + is_solved(om) || return nothing + + target_objective_value = J.objective_value(om) - # prepare the model for pFBA - for mod in qp_modifications - mod(model, opt_model) + # switch to parsimonizing the solution w.r.t. to the objective value + isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer) + for m in parsimonious_modifications + m(om) end - # add the minimization constraint for total flux - v = opt_model[:x] # fluxes - J.@objective(opt_model, Min, sum(dot(v, v))) + J.@objective(om, J.MIN_SENSE, C.substitute(parsimonious_objective, om[:x])) - for rb in relax_bounds - # lb, ub = objective_bounds(rb)(Z) - J.@constraint(opt_model, pfba_constraint, lb <= original_objective <= ub) + # try all admissible tolerances + for tolerance in tolerances + (lb, ub) = tolerance(target_objective_value) + J.@constraint( + om, + pfba_tolerance_constraint, + lb <= C.substitute(objective, om[:x]) <= ub + ) - J.optimize!(opt_model) - J.is_solved(opt_model) && break + J.optimize!(om) + is_solved(om) && return C.constraint_values(output, J.value.(om[:x])) - J.delete(opt_model, pfba_constraint) - J.unregister(opt_model, :pfba_constraint) + J.delete(om, pfba_tolerance_constraint) + J.unregister(om, :pfba_tolerance_constraint) end + # all tolerances failed + return nothing end + +export parsimonious_optimized_constraints + +""" +$(TYPEDSIGNATURES) + +Compute a parsimonious flux solution for the given `model`. In short, the +objective value of the parsimonious solution should be the same as the one from +[`flux_balance`](@ref), except the squared sum of reaction fluxes is minimized. +If there are multiple possible fluxes that achieve a given objective value, +parsimonious flux thus represents the "minimum energy" one, thus arguably more +realistic. + +Most arguments are forwarded to [`parsimonious_optimized_constraints`](@ref), +with some (objectives) filled in automatically to fit the common processing of +FBC models, and some (`tolerances`) provided with more practical defaults. + +Similarly to the [`flux_balance`](@ref), returns a tree with the optimization +solutions of the shape as given by [`fbc_model_constraints`](@ref). +""" +function parsimonious_flux_balance( + model::A.AbstractFBCModel, + optimizer; + tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), + kwargs..., +) + constraints = fbc_model_constraints(model) + parsimonious_optimized_constraints( + constraints; + optimizer, + objective = constraints.objective.value, + parsimonious_objective = squared_sum_objective(constraints.fluxes), + tolerances, + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`parsimonious_flux_balance`](@ref). +""" +parsimonious_flux_balance(optimizer; kwargs...) = + model -> parsimonious_flux_balance(model, optimizer; kwargs...) + +export parsimonious_flux_balance diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index c27ca5538..e6f7a8822 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -13,9 +13,7 @@ $(TYPEDSIGNATURES) TODO """ squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = - C.Constraint( - sum( - (C.value(c) - target[k]) * (C.value(c) - target[k]) for - (k, c) in constraints if haskey(target, k) - ), + sum( + (C.squared(C.value(c) - target[k]) for (k, c) in constraints if haskey(target, k)), + init = zero(C.LinearValue), ) diff --git a/src/solver.jl b/src/solver.jl index 4a6e41aef..b0d24fd52 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -47,28 +47,3 @@ is_solved(opt_model::J.Model) = J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] export is_solved - -""" -$(TYPEDSIGNATURES) - -Make an JuMP model out of `constraints` using [`optimization_model`](@ref) -(most arguments are forwarded there), then apply the modifications, optimize -the model, and return either `nothing` if the optimization failed, or `output` -substituted with the solved values (`output` defaults to `constraints`. -""" -function optimized_constraints( - constraints::C.ConstraintTreeElem, - args...; - modifications = [], - output = constraints, - kwargs..., -) - om = optimization_model(constraints, args...; kwargs...) - for m in modifications - m(om) - end - J.optimize!(om) - is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing -end - -export optimized_constraints From a2c121e0e4dd546d8bbfaa671c291efb6305c5e3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 9 Dec 2023 23:00:53 +0100 Subject: [PATCH 392/531] fixes --- Project.toml | 6 +- ...ems.jl => 03-parsimonious-flux-balance.jl} | 56 ++++++++++--------- ...04-minimization-of-metabolic-adjustment.jl | 23 ++++++++ src/analysis/parsimonious_flux_balance.jl | 2 +- 4 files changed, 58 insertions(+), 29 deletions(-) rename docs/src/examples/{03-qp-problems.jl => 03-parsimonious-flux-balance.jl} (56%) create mode 100644 docs/src/examples/04-minimization-of-metabolic-adjustment.jl diff --git a/Project.toml b/Project.toml index 40fdc022c..1ec193418 100644 --- a/Project.toml +++ b/Project.toml @@ -17,10 +17,10 @@ SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" [compat] -AbstractFBCModels = "0.2" +AbstractFBCModels = "0.2.2" Aqua = "0.7" -Clarabel = "0.3" -ConstraintTrees = "0.6" +Clarabel = "0.6" +ConstraintTrees = "0.6.1" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" diff --git a/docs/src/examples/03-qp-problems.jl b/docs/src/examples/03-parsimonious-flux-balance.jl similarity index 56% rename from docs/src/examples/03-qp-problems.jl rename to docs/src/examples/03-parsimonious-flux-balance.jl index 19bdc56d5..924141f41 100644 --- a/docs/src/examples/03-qp-problems.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -1,9 +1,11 @@ -# # Quandratic objective flux balance analysis type problems +# # Parsimonious flux balance analysis # We will use [`parsimonious_flux_balance`](@ref) and # [`minimize_metabolic_adjustment`](@ref) to find the optimal flux # distribution in the *E. coli* "core" model. +# +# TODO pFBA citation # If it is not already present, download the model and load the package: import Downloads: download @@ -13,43 +15,45 @@ import Downloads: download # next, load the necessary packages -import COBREXA as X -import AbstractFBCModels as A # for the accessors -import JSONFBCModels as J # for the model type +using COBREXA + +import JSONFBCModels import Clarabel # can solve QPs -model = A.load(J.JSONFBCModel, "e_coli_core.json") # load the model +model = load_model("e_coli_core.json") # load the model -# Use the convenience function to run standard pFBA +# Use the convenience function to run standard pFBA on -vt = X.parsimonious_flux_balance(model, Clarabel.Optimizer; modifications = [X.silence]) +vt = parsimonious_flux_balance(model, Clarabel.Optimizer; modifications = [silence]) # Or use the piping functionality -model |> parsimonious_flux_balance(Clarabel.Optimizer; modifications = [X.silence]) +model |> parsimonious_flux_balance(Clarabel.Optimizer; modifications = [silence]) @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src -@test isapprox(sum(x^2 for x in values(vt.fluxes)), 11414.2119; atol = QP_TEST_TOLERANCE) #src +@test sum(x^2 for x in values(vt.fluxes)) < 15000 #src + +#= # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = X.fbc_model_constraints(model) -ctmodel *= :l2objective^X.squared_sum_objective(ctmodel.fluxes) +ctmodel = fbc_model_constraints(model) +ctmodel *= :l2objective^squared_sum_objective(ctmodel.fluxes) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks -opt_model = X.optimization_model( +opt_model = optimization_model( ctmodel; objective = ctmodel.:l2objective.value, optimizer = Clarabel.Optimizer, - sense = X.J.MIN_SENSE, + sense = J.MIN_SENSE, ) -X.J.optimize!(opt_model) # JuMP is called J in COBREXA +J.optimize!(opt_model) # JuMP is called J in COBREXA -X.is_solved(opt_model) # check if solved +is_solved(opt_model) # check if solved -vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = C.constraint_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds @@ -57,37 +61,39 @@ vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees ref_sol = Dict("ATPS4r" => 33.0, "CYTBD" => 22.0) -vt = X.minimize_metabolic_adjustment(model, ref_sol, Gurobi.Optimizer) +vt = minimize_metabolic_adjustment(model, ref_sol, Gurobi.Optimizer) # Or use the piping functionality model |> -X.minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; modifications = [X.silence]) +minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; modifications = [silence]) @test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = X.fbc_model_constraints(model) +ctmodel = fbc_model_constraints(model) ctmodel *= - :minoxphospho^X.squared_sum_error_objective( + :minoxphospho^squared_sum_error_objective( ctmodel.fluxes, Dict(:ATPS4r => 33.0, :CYTBD => 22.0), ) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks -opt_model = X.optimization_model( +opt_model = optimization_model( ctmodel; objective = ctmodel.minoxphospho.value, optimizer = Clarabel.Optimizer, - sense = X.J.MIN_SENSE, + sense = J.MIN_SENSE, ) -X.J.optimize!(opt_model) # JuMP is called J in COBREXA +J.optimize!(opt_model) # JuMP is called J in COBREXA -X.is_solved(opt_model) # check if solved +is_solved(opt_model) # check if solved -vt = X.C.constraint_values(ctmodel, X.J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = C.constraint_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds + +=# diff --git a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl new file mode 100644 index 000000000..0cfdd4050 --- /dev/null +++ b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl @@ -0,0 +1,23 @@ + +# # Minimization of metabolic adjustment + +# TODO MOMA citation + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +using COBREXA +import AbstractFBCModels.CanonicalModel as CM +import JSONFBCModels +import Clarabel + +# TODO this might do the convert immediately as with the old cobrexa... +# probably better have an actual output-type argument tho rather than force the +# guessing. +model = convert(CM.Model, load_model("e_coli_core.json")) + +reference_fluxes = parsimonious_flux_balance(model, Clarabel.Optimizer).fluxes + +# TODO MOMA from here diff --git a/src/analysis/parsimonious_flux_balance.jl b/src/analysis/parsimonious_flux_balance.jl index 0658b0d73..c231ce5cd 100644 --- a/src/analysis/parsimonious_flux_balance.jl +++ b/src/analysis/parsimonious_flux_balance.jl @@ -27,7 +27,7 @@ function parsimonious_optimized_constraints( ) # first solve the optimization problem with the original objective - om = optimization_model(constraints, args...; kwargs...) + om = optimization_model(constraints, args...; objective, kwargs...) for m in modifications m(om) end From 53cc6b75fd7e8b01e9e97fec99746dcc4b3f2f40 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 12:14:25 +0100 Subject: [PATCH 393/531] clean up moma --- .../examples/03-parsimonious-flux-balance.jl | 4 +- src/analysis/flux_balance.jl | 5 +- src/analysis/minimal_metabolic_adjustment.jl | 101 ++++++++++-------- src/analysis/modifications.jl | 4 +- src/analysis/parsimonious_flux_balance.jl | 13 +-- src/builders/core.jl | 5 + src/solver.jl | 29 ++++- 7 files changed, 101 insertions(+), 60 deletions(-) diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 924141f41..2e1a9c813 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -46,7 +46,7 @@ opt_model = optimization_model( ctmodel; objective = ctmodel.:l2objective.value, optimizer = Clarabel.Optimizer, - sense = J.MIN_SENSE, + sense = Minimal, ) J.optimize!(opt_model) # JuMP is called J in COBREXA @@ -85,7 +85,7 @@ opt_model = optimization_model( ctmodel; objective = ctmodel.minoxphospho.value, optimizer = Clarabel.Optimizer, - sense = J.MIN_SENSE, + sense = Minimal, ) J.optimize!(opt_model) # JuMP is called J in COBREXA diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl index 745c99896..5f9d06182 100644 --- a/src/analysis/flux_balance.jl +++ b/src/analysis/flux_balance.jl @@ -10,13 +10,12 @@ For a "nice" version for simpler finding of metabolic model optima, use [`flux_balance`](@ref). """ function optimized_constraints( - constraints::C.ConstraintTreeElem, - args...; + constraints::C.ConstraintTreeElem; modifications = [], output = constraints, kwargs..., ) - om = optimization_model(constraints, args...; kwargs...) + om = optimization_model(constraints; kwargs...) for m in modifications m(om) end diff --git a/src/analysis/minimal_metabolic_adjustment.jl b/src/analysis/minimal_metabolic_adjustment.jl index d586a3aa8..d7dcc29c5 100644 --- a/src/analysis/minimal_metabolic_adjustment.jl +++ b/src/analysis/minimal_metabolic_adjustment.jl @@ -2,67 +2,76 @@ """ $(TYPEDSIGNATURES) -Run minimization of metabolic adjustment (MOMA) on `model` with respect to -`reference_solution`, which is a dictionary of fluxes. MOMA finds the shortest -Euclidian distance between `reference_solution` and `model` with `modifications`: -``` -min Σᵢ (xᵢ - flux_refᵢ)² -s.t. S x = b - xₗ ≤ x ≤ xᵤ -``` -Because the problem has a quadratic objective, a QP solver is required. See -"Daniel, Vitkup & Church, Analysis of Optimality in Natural and Perturbed -Metabolic Networks, Proceedings of the National Academy of Sciences, 2002" for -more details. +Find a feasible solution of the "minimal metabolic adjustment analysis" (MOMA) +for the `model`, which is the "closest" feasible solution to the given +`reference_fluxes`, in the sense of squared-sum error distance. The minimized +squared distance (the objective) is present in the result tree as +`minimal_adjustment_objective`. -Returns a [`C.ValueTree`](@ref), or `nothing` if the solution could not be -found. +This is often used for models with smaller feasible region than the reference +models (typically handicapped by a knockout, nutritional deficiency or a +similar perturbation). MOMA solution then gives an expectable "easiest" +adjustment of the organism towards a somewhat working state. -# Example -``` -model = load_model("e_coli_core.json") +Reference fluxes that do not exist in the model are ignored (internally, the +objective is constructed via [`squared_sum_error_objective`](@ref)). -``` +Additional parameters are forwarded to [`optimized_constraints`](@ref). """ function minimal_metabolic_adjustment( - constraints::C.ConstraintTree, - reference_solution::Dict{String,Float64}, + model::A.AbstractFBCModel, + reference_fluxes::Dict{Symbol,Float64}, optimizer; - modifications = [], + kwargs..., ) - _constraints = - constraints * - :momaobjective^squared_sum_error_objective( - constraints.fluxes, - Dict(Symbol(k) => float(v) for (k, v) in reference_solution), - ) - - opt_model = optimization_model( - _constraints; - objective = _constraints.momaobjective.value, + constraints = fbc_model_constraints(model) + objective = squared_sum_error_objective(constraints.fluxes, reference_fluxes) + optimized_constraints( + constraints * :minimal_adjustment_objective^C.Constraint(objective); optimizer, - sense = J.MIN_SENSE, + objective, + sense = Minimal, + kwargs..., ) - - for mod in modifications - mod(constraints, opt_model) - end - - J.optimize!(opt_model) - - is_solved(opt_model) || return nothing - - C.ValueTree(_constraints, J.value.(opt_model[:x])) end """ $(TYPEDSIGNATURES) -Variant that takes an [`A.AbstractFBCModel`](@ref) as input. All other arguments are forwarded. +A slightly easier-to-use version of [`minimal_metabolic_adjustment`](@ref) that +computes the reference flux as the optimal solution of the +[`reference_model`](@ref). The reference flux is calculated using +`reference_optimizer` and `reference_modifications`, which default to the +`optimizer` and `modifications`. + +Leftover arguments are passed to the overload of +[`minimal_metabolic_adjustment`](@ref) that accepts the reference flux +dictionary. """ -function minimal_metabolic_adjustment(model::A.AbstractFBCModel, args...; kwargs...) - constraints = fbc_model_constraints(model) - minimal_metabolic_adjustment(constraints, args...; kwargs...) +function minimal_metabolic_adjustment( + model::A.AbstractFBCModel, + reference_model::A.AbstractFBCModel, + optimizer; + reference_optimizer = optimizer, + modifications = [], + reference_modifications = modifications, + kwargs..., +) + reference_constraints = fbc_model_constraints(reference_model) + reference_fluxes = optimized_constraints( + reference_constraints; + optimizer = reference_optimizer, + modifications = reference_modifications, + output = reference_constraints.fluxes, + ) + isnothing(reference_fluxes) && return nothing + minimal_metabolic_adjustment( + model, + reference_fluxes, + optimizer; + modifications, + kwargs..., + ) end export minimal_metabolic_adjustment diff --git a/src/analysis/modifications.jl b/src/analysis/modifications.jl index 585077480..dccec943e 100644 --- a/src/analysis/modifications.jl +++ b/src/analysis/modifications.jl @@ -4,8 +4,8 @@ """ $(TYPEDSIGNATURES) -Change the objective sense of optimization. Possible arguments are -`JuMP.MAX_SENSE` and `JuMP.MIN_SENSE`. +Change the objective sense of optimization. Accepted arguments include +[`Minimal`](@ref), [`Maximal`](@ref), and [`Feasible`](@ref). """ set_objective_sense(objective_sense) = opt_model -> J.set_objective_sense(opt_model, objective_sense) diff --git a/src/analysis/parsimonious_flux_balance.jl b/src/analysis/parsimonious_flux_balance.jl index c231ce5cd..114cb4652 100644 --- a/src/analysis/parsimonious_flux_balance.jl +++ b/src/analysis/parsimonious_flux_balance.jl @@ -13,8 +13,7 @@ A simpler version suitable for direct work with metabolic models is available in [`parsimonious_flux_balance`](@ref). """ function parsimonious_optimized_constraints( - constraints::C.ConstraintTreeElem, - args...; + constraints::C.ConstraintTreeElem; objective::C.Value, modifications = [], parsimonious_objective::C.Value, @@ -27,7 +26,7 @@ function parsimonious_optimized_constraints( ) # first solve the optimization problem with the original objective - om = optimization_model(constraints, args...; objective, kwargs...) + om = optimization_model(constraints; objective, kwargs...) for m in modifications m(om) end @@ -74,7 +73,8 @@ objective value of the parsimonious solution should be the same as the one from [`flux_balance`](@ref), except the squared sum of reaction fluxes is minimized. If there are multiple possible fluxes that achieve a given objective value, parsimonious flux thus represents the "minimum energy" one, thus arguably more -realistic. +realistic. The optimized squared distance is present in the result as +`parsimonious_objective`. Most arguments are forwarded to [`parsimonious_optimized_constraints`](@ref), with some (objectives) filled in automatically to fit the common processing of @@ -90,11 +90,12 @@ function parsimonious_flux_balance( kwargs..., ) constraints = fbc_model_constraints(model) + parsimonious_objective = squared_sum_objective(constraints.fluxes) parsimonious_optimized_constraints( - constraints; + constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); optimizer, objective = constraints.objective.value, - parsimonious_objective = squared_sum_objective(constraints.fluxes), + parsimonious_objective, tolerances, kwargs..., ) diff --git a/src/builders/core.jl b/src/builders/core.jl index 97cf6d62a..0d0624113 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -6,6 +6,11 @@ $(TYPEDSIGNATURES) A constraint tree that models the content of the given instance of `AbstractFBCModel`. + +The constructed tree contains subtrees `fluxes` (with the reaction-defining +"variables") and `flux_stoichiometry` (with the metabolite-balance-defining +constraints), and a single constraint `objective` thad describes the objective +function of the model. """ function fbc_model_constraints(model::A.AbstractFBCModel) rxns = Symbol.(A.reactions(model)) diff --git a/src/solver.jl b/src/solver.jl index b0d24fd52..2c48c0ccd 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -10,7 +10,7 @@ function optimization_model( cs::C.ConstraintTreeElem; objective::Union{Nothing,C.Value} = nothing, optimizer, - sense = J.MAX_SENSE, + sense = Maximal, ) model = J.Model(optimizer) @@ -47,3 +47,30 @@ is_solved(opt_model::J.Model) = J.termination_status(opt_model) in [J.MOI.OPTIMAL, J.MOI.LOCALLY_SOLVED] export is_solved + +""" + Minimal + +Objective sense for finding the minimal value of the objective. + +Same as `JuMP.MIN_SENSE`. +""" +const Minimal = J.MIN_SENSE + +""" + Maximal + +Objective sense for finding the maximal value of the objective. + +Same as `JuMP.MAX_SENSE`. +""" +const Maximal = J.MAX_SENSE + +""" + Maximal + +Objective sense for finding the any feasible value of the objective. + +Same as `JuMP.FEASIBILITY_SENSE`. +""" +const Feasible = J.FEASIBILITY_SENSE From c6b8af470bb40dbe766b173c2b65877789622166 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 14:29:40 +0100 Subject: [PATCH 394/531] some work on the docs --- docs/src/examples/02-flux-balance-analysis.jl | 4 + docs/src/examples/02a-optimizer-parameters.jl | 5 +- docs/src/examples/02b-model-modifications.jl | 130 +++++++++++++++++- 3 files changed, 130 insertions(+), 9 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 64642dbbe..e7ceb8fcc 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -47,3 +47,7 @@ solution.fluxes.PFK # ...or make a "table" of all fluxes through all reactions: collect(solution.fluxes) + +# ## Advanced: Finding flux balance via the low-level interface + +# TODO ConstraintTrees (maybe put this into a separate example?) diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl index 766d6c46a..2dd2705c0 100644 --- a/docs/src/examples/02a-optimizer-parameters.jl +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -39,12 +39,13 @@ solution = flux_balance( # To see some of the effects of the configuration changes, you may e.g. # deliberately cripple the optimizer's possibilities to a few iterations, which -# will cause it to fail and return no solution: +# will cause it to fail, return no solution, and verbosely describe what +# happened: solution = flux_balance( model, Tulip.Optimizer; - modifications = [silence, set_optimizer_attribute("IPM_IterationsLimit", 2)], + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 2)], ) println(solution) diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index 22307fe7c..c401a0944 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -8,13 +8,13 @@ # # With COBREXA, there are 2 different approaches that one can take: # 1. We can change the model structure and use the changed metabolic model. -# This is better for doing simple and small but systematic modifications, such -# as removing metabolites, adding reactions, etc. +# This is better for doing simple and small but systematic modifications, +# such as removing metabolites, adding reactions, etc. # 2. We can intercept the pipeline that converts the metabolic model to -# constraints and then to the optimizer representation, and make small -# modifications along that way. This is better for various technical model -# adjustments, such as using combined objectives or adding reaction-coupling -# constraints. +# constraints and then to the optimizer representation, and make small +# modifications along that way. This is better for various technical model +# adjustments, such as using combined objectives or adding reaction-coupling +# constraints. # # Here we demonstrate the first, "modelling" approach. The main advantage of # that approach is that the modified model is still a FBC model, and you can @@ -25,4 +25,120 @@ # constraints](02c-constraint-modifications.md) for a closer look on how to # modify even such complex constructions. # -# TODO here. :) +# ## Getting the base model + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +import JSONFBCModels + +# For applying the modifications, we will use the canonical model as exported +# from package `AbstractFBCModels`. There are other possibilities, but the +# canonical one is easiest to use for common purposes. + +import AbstractFBCModels.CanonicalModel as CM + +# We can now load the model: + +model = convert(CM.Model, load_model("e_coli_core.json")) + +# The canonical model is quite easy to work with, made basically of the most +# accessible Julia structures possible. For example, you can observe a reaction +# as such: + +model.reactions["PFK"] + +# + +model.reactions["CS"].stoichiometry + +# ## Running FBA on modified models +# +# Since the canonical model is completely mutable, you can change it in any way you like and feed it directly into [`flux_balance`](@ref). Let's first find a "original" solution, so that we have a base solution for comparing: + +import GLPK + +base_solution = flux_balance(model, GLPK.Optimizer) +base_solution.objective + +# Now, for example, we can limit the intake of glucose by the model: + +model.reactions["EX_glc__D_e"] + +# Since the original intake limit is 10 units, let's try limiting that to 5: + +model.reactions["EX_glc__D_e"].lower_bound = -5.0 + +# ...and solve the modified model: +# +low_glucose_solution = flux_balance(model, GLPK.Optimizer) +low_glucose_solution.objective + +@test isapprox(low_glucose_solution.objective, 0.41559777, atol=TEST_TOLERANCE) #src + +# ## Preventing reference-based sharing problems with `deepcopy` +# +# People often want to try different perturbations with a single base model. It +# would therefore look feasible to save retain the "unmodified" model in a +# single variable, and make copies of that with the modifications applied. +# Let's observe what happens: + +base_model = convert(CM.Model, load_model("e_coli_core.json")) # load the base + +modified_model = base_model # copy for modification + +modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 # modify the glucose intake limit + +# Surprisingly, the base model got modified too! + +base_model.reactions["EX_glc__D_e"] + +# This is because Julia uses reference-based sharing whenever anything mutable +# is copied using the `=` operator. While this is extremely useful in many +# scenarios for data processing efficiency and computational speed, it +# unfortunately breaks this simple use-case. +# +# To fix the situation, you should always ensure to make an actual copy of the +# model data by either carefully copying the changed parts with `copy()`, or +# simply by copying the whole model structure as is with `deepcopy()`. Let's +# try again: + +base_model = convert(CM.Model, load_model("e_coli_core.json")) +modified_model = deepcopy(base_model) # this forces an actual copy of the data +modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 + +# With `deepcopy`, the result works as intended: + +(modified_model.reactions["EX_glc__D_e"].lower_bound, base_model.reactions["EX_glc__D_e"].lower_bound) + +@test modified_model.reactions["EX_glc__D_e"].lower_bound != base_model.reactions["EX_glc__D_e"].lower_bound #src + +#md # !!! danger "Avoid overwriting base models when using in-place modifications" +#md # Whenever you are changing a copy of the model, make sure that you are not changing it by a reference. Always use some copy mechanism such as `copy` or `deepcopy` to prevent the default reference-based sharing. + +# ## Observing the differences +# +# We already have a `base_solution` and `low_glucose_solution` from above. What +# is the easiest way to see what has changed? We can quite easily compute +# squared distance between all dictionary entries using Julia function for +# merging dictionaries (called `mergewith`). + +# With that, we can extract the plain difference in fluxes: +flux_differences = mergewith(-, base_solution.fluxes, low_glucose_solution.fluxes) + +# ...and see what were the biggest directional differences: +sort(flux_differences, by = last) + +# ...or compute the squared distance, to see the "absolute" changes: +flux_changes = mergewith((x,y) -> (x-y)^2, base_solution.fluxes, low_glucose_solution.fluxes) + +# ...and again see what changed most: +sort(flux_changes, by = last) + +#md # !!! tip "For realistic comparisons always find an unique flux solution" +#md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance) to obtain uniquely determined and safely comparable flux solutions. From a39106146eb1b029f44efffd4a7fd56f4e652696 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 14:31:55 +0100 Subject: [PATCH 395/531] format --- docs/src/examples/02b-model-modifications.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index c401a0944..cc49a4b8a 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -79,7 +79,7 @@ model.reactions["EX_glc__D_e"].lower_bound = -5.0 low_glucose_solution = flux_balance(model, GLPK.Optimizer) low_glucose_solution.objective -@test isapprox(low_glucose_solution.objective, 0.41559777, atol=TEST_TOLERANCE) #src +@test isapprox(low_glucose_solution.objective, 0.41559777, atol = TEST_TOLERANCE) #src # ## Preventing reference-based sharing problems with `deepcopy` # @@ -114,9 +114,13 @@ modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 # With `deepcopy`, the result works as intended: -(modified_model.reactions["EX_glc__D_e"].lower_bound, base_model.reactions["EX_glc__D_e"].lower_bound) +( + modified_model.reactions["EX_glc__D_e"].lower_bound, + base_model.reactions["EX_glc__D_e"].lower_bound, +) -@test modified_model.reactions["EX_glc__D_e"].lower_bound != base_model.reactions["EX_glc__D_e"].lower_bound #src +@test modified_model.reactions["EX_glc__D_e"].lower_bound != + base_model.reactions["EX_glc__D_e"].lower_bound #src #md # !!! danger "Avoid overwriting base models when using in-place modifications" #md # Whenever you are changing a copy of the model, make sure that you are not changing it by a reference. Always use some copy mechanism such as `copy` or `deepcopy` to prevent the default reference-based sharing. @@ -135,7 +139,8 @@ flux_differences = mergewith(-, base_solution.fluxes, low_glucose_solution.fluxe sort(flux_differences, by = last) # ...or compute the squared distance, to see the "absolute" changes: -flux_changes = mergewith((x,y) -> (x-y)^2, base_solution.fluxes, low_glucose_solution.fluxes) +flux_changes = + mergewith((x, y) -> (x - y)^2, base_solution.fluxes, low_glucose_solution.fluxes) # ...and again see what changed most: sort(flux_changes, by = last) From be71bf90506dafadfb91f52e71be3e3e2c6b8121 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 15:10:59 +0100 Subject: [PATCH 396/531] some documentation fixes --- docs/src/examples/02-flux-balance-analysis.jl | 2 ++ docs/src/examples/02b-model-modifications.jl | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index e7ceb8fcc..5e55cea23 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -5,6 +5,8 @@ # find an optimal flux in the *E. coli* "core" model. We will need the model, # which we can download using [`download_model`](@ref): +using COBREXA + download_model( "http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json", diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index cc49a4b8a..15a385b2e 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -119,7 +119,7 @@ modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 base_model.reactions["EX_glc__D_e"].lower_bound, ) -@test modified_model.reactions["EX_glc__D_e"].lower_bound != +@test modified_model.reactions["EX_glc__D_e"].lower_bound != #src base_model.reactions["EX_glc__D_e"].lower_bound #src #md # !!! danger "Avoid overwriting base models when using in-place modifications" @@ -136,14 +136,14 @@ modified_model.reactions["EX_glc__D_e"].lower_bound = -123.0 flux_differences = mergewith(-, base_solution.fluxes, low_glucose_solution.fluxes) # ...and see what were the biggest directional differences: -sort(flux_differences, by = last) +sort(collect(flux_differences), by = last) # ...or compute the squared distance, to see the "absolute" changes: flux_changes = mergewith((x, y) -> (x - y)^2, base_solution.fluxes, low_glucose_solution.fluxes) # ...and again see what changed most: -sort(flux_changes, by = last) +sort(collect(flux_changes), by = last) #md # !!! tip "For realistic comparisons always find an unique flux solution" #md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance) to obtain uniquely determined and safely comparable flux solutions. From ce818557e1ec150e3af4dda12a69f611f2d2d95f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 15:23:28 +0100 Subject: [PATCH 397/531] more small doc fixes --- docs/make.jl | 2 +- docs/src/examples/02b-model-modifications.jl | 2 +- docs/src/reference.md | 7 +++++++ src/COBREXA.jl | 18 +++++++++++++++++- 4 files changed, 26 insertions(+), 3 deletions(-) diff --git a/docs/make.jl b/docs/make.jl index f488b18ed..0c1d35ef0 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -29,7 +29,6 @@ find_mds(path) = filter(x -> endswith(x, ".md"), readdir(joinpath(@__DIR__, "src", path))), ) -#TODO migrate this to Documenter-1, and make all checks strict # build the docs makedocs( modules = [COBREXA], @@ -42,6 +41,7 @@ makedocs( ), authors = "The developers of COBREXA.jl", linkcheck = !("skiplinks" in ARGS), + warnonly = true, # TODO: remove later pages = [ "Home" => "index.md", "Examples" => [ diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index 15a385b2e..36ac943eb 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -146,4 +146,4 @@ flux_changes = sort(collect(flux_changes), by = last) #md # !!! tip "For realistic comparisons always find an unique flux solution" -#md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance) to obtain uniquely determined and safely comparable flux solutions. +#md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance.md) to obtain uniquely determined and safely comparable flux solutions. diff --git a/docs/src/reference.md b/docs/src/reference.md index 51abbb38c..d791bd478 100644 --- a/docs/src/reference.md +++ b/docs/src/reference.md @@ -42,6 +42,13 @@ Modules = [COBREXA] Pages = ["src/builders/objectives.jl"] ``` +### Bounds&tolerances helpers + +```@autodocs +Modules = [COBREXA] +Pages = ["src/misc/bounds.jl"] +``` + ## Analysis functions ```@autodocs diff --git a/src/COBREXA.jl b/src/COBREXA.jl index b03902860..f6efb9c24 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -1,5 +1,21 @@ """ -$(README) + module COBREXA + +COnstraint Based Reconstruction and EXascale Analysis. COBREXA provides +functions for construction, modification, simulation and analysis of +constraint-based metabolic models that follows the COBRA methodology. + +COBREXA is built as a front-end for the combination of `AbstractFBCModels.jl` +(provides the model I/O), `ConstraintTrees.jl` (provides the constraint system +organization), `Distributed.jl` (provides HPC execution capability), and +`JuMP.jl` (provides the solvers). + +See the online documentation for a complete description of functionality aided +by copy-pastable examples. + +To start quickly, load your favorite JuMP-compatible solver, use +[`load_model`](@ref) to read a metabolic model from the disk, and solve it with +[`flux_balance`](@ref). """ module COBREXA From 39cb1e925fc72feb84bfeb321de6d4c55b165b4a Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sun, 10 Dec 2023 15:31:24 +0100 Subject: [PATCH 398/531] wording --- docs/src/examples/02b-model-modifications.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index 36ac943eb..0ee5942d0 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -145,5 +145,5 @@ flux_changes = # ...and again see what changed most: sort(collect(flux_changes), by = last) -#md # !!! tip "For realistic comparisons always find an unique flux solution" +#md # !!! tip "For realistic comparisons always use a uniquely defined flux solution" #md # Since the usual flux balance allows a lot of freedom in the "solved" flux and the only value that is "reproducible" by the analysis is the objective, one should never compare the flux distributions directly. Typically, that may result in false-positive (and sometimes false-negative) differences. Use e.g. [parsimonious FBA](03-parsimonious-flux-balance.md) to obtain uniquely determined and safely comparable flux solutions. From 19bd2da59e4888931bc1fc04dfe4130db6d5b18d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 11:57:49 +0100 Subject: [PATCH 399/531] add enzyme builder --- src/builders/enzymes.jl | 134 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 src/builders/enzymes.jl diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl new file mode 100644 index 000000000..7836a222f --- /dev/null +++ b/src/builders/enzymes.jl @@ -0,0 +1,134 @@ +""" +$(TYPEDSIGNATURES) + +Allocate enzyme variables (gene products in the model) to a constraint tree. +""" +enzyme_variables(model::A.AbstractFBCModel) = + C.variables(; keys = Symbol.(A.genes(model)), bounds = Ref((0.0, Inf))) + +""" +$(TYPEDSIGNATURES) + +Create isozyme variables for reactions. A single reaction may be catalyzed by +multiple enzymes (isozymes), and the total flux through a reaction is the sum +through of all these isozymes. These variables are linked to fluxes through +[`link_isozymes`](@ref). +""" +function isozyme_variables( + reaction_id::String, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, +) + C.variables(; + keys = Symbol.(collect(keys(reaction_isozymes[reaction_id]))), + bounds = Ref((0.0, Inf)), + ) +end + +""" +$(TYPEDSIGNATURES) + +Link isozymes to fluxes. All forward (backward) fluxes are the sum of all the +isozymes catalysing these fluxes. +""" +function link_isozymes( + fluxes_directional::C.ConstraintTree, + fluxes_isozymes::C.ConstraintTree, +) + C.ConstraintTree( + k => C.Constraint( + value = s.value - sum(x.value for (_, x) in fluxes_isozymes[k]), + bound = 0.0, + ) for (k, s) in fluxes_directional if haskey(fluxes_isozymes, k) + ) +end + +""" +$(TYPEDSIGNATURES) + +Create the enzyme "mass balance" matrix. In essence, the stoichiometric +coefficient is subunit_stoichiometry / kcat for each directional, isozyme flux, +and it must be balanced by the enzyme variable supply. +""" +function enzyme_stoichiometry( + enzymes::C.ConstraintTree, + fluxes_isozymes_forward::C.ConstraintTree, + fluxes_isozymes_backward::C.ConstraintTree, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, +) + # map enzyme ids to reactions that use them (not all isozymes have to though) + enzyme_rid_lookup = Dict{Symbol,Vector{Symbol}}() + for (rid, isozymes) in reaction_isozymes + for isozyme in values(isozymes) + for gid in keys(isozyme.gene_product_stoichiometry) + rids = get!(enzyme_rid_lookup, Symbol(gid), Symbol[]) + rid in rids || push!(rids, rid) + end + end + end + + C.ConstraintTree( + gid => C.Constraint( + value = enz.value + # supply + sum( + enzyme_balance( + gid, + rid, + fluxes_isozymes_forward, + reaction_isozymes, + :kcat_forward, + ) for rid in enzyme_rid_lookup[gid] if + haskey(fluxes_isozymes_forward, rid); + init = zero(typeof(enz.value)), + ) + # flux through positive isozymes + sum( + enzyme_balance( + gid, + rid, + fluxes_isozymes_backward, + reaction_isozymes, + :kcat_backward, + ) for rid in enzyme_rid_lookup[gid] if + haskey(fluxes_isozymes_backward, rid); + init = zero(typeof(enz.value)), + ), # flux through negative isozymes + bound = 0.0, + ) for (gid, enz) in enzymes if gid in keys(enzyme_rid_lookup) + ) +end + +""" +$(TYPEDSIGNATURES) + +Helper function to balance the forward or backward isozyme fluxes for a specific +gene product. +""" +function enzyme_balance( + gid::Symbol, + rid::Symbol, + fluxes_isozymes::C.ConstraintTree, # direction + reaction_isozymes::Dict{Symbol,Dict{Symbol,Isozyme}}, + direction = :kcat_forward, +) + isozyme_dict = reaction_isozymes[rid] + + sum( # this is where the stoichiometry comes in + -isozyme_value.value * + isozyme_dict[isozyme_id].gene_product_stoichiometry[string(gid)] / + getfield(isozyme_dict[isozyme_id], direction) for + (isozyme_id, isozyme_value) in fluxes_isozymes[rid] if + gid in Symbol.(keys(isozyme_dict[isozyme_id].gene_product_stoichiometry)); + init = zero(C.LinearValue), + ) +end + +function enzyme_capacity( + enzymes::C.ConstraintTree, + enzyme_molar_mass::Dict{Symbol,Float64}, + enzyme_ids::Vector{Symbol}, + capacity::Float64, +) + C.Constraint( + value = sum(enzymes[gid].value * enzyme_molar_mass[gid] for gid in enzyme_ids), + bound = (0.0, capacity), + ) +end From 69ce9713af5d74270c5bb782f41f163c3fd7e128 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 11:58:36 +0100 Subject: [PATCH 400/531] add enzyme model example --- docs/src/examples/04-enzyme-models.jl | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 docs/src/examples/04-enzyme-models.jl diff --git a/docs/src/examples/04-enzyme-models.jl b/docs/src/examples/04-enzyme-models.jl new file mode 100644 index 000000000..c26e4b62c --- /dev/null +++ b/docs/src/examples/04-enzyme-models.jl @@ -0,0 +1,81 @@ +import JSONFBCModels as M +import AbstractFBCModels as A +import COBREXA as X +import ConstraintTrees as C +import JuMP as J +import Gurobi as G + +model = A.load(M.JSONFBCModel, "e_coli_core.json") + +reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() +for rid in A.reactions(model) + haskey(ecoli_core_reaction_kcats, rid) || continue + for (i, grr) in enumerate(A.reaction_gene_association_dnf(model, rid)) + d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) + d["isozyme_"*string(i)] = X.Isozyme( + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ) + end +end + +m = X.metabolic_model(model) + +# create directional fluxes +m += + :fluxes_forward^X.fluxes_in_direction(m.fluxes, :forward) + + :fluxes_backward^X.fluxes_in_direction(m.fluxes, :backward) + +# link directional fluxes to original fluxes +m *= + :link_flux_directions^X.sign_split_constraints( + positive = m.fluxes_forward, + negative = m.fluxes_backward, + signed = m.fluxes, + ) + +# create fluxes for each isozyme +for (rid, _) in m.fluxes_forward + if haskey(reaction_isozymes, string(rid)) + m += :fluxes_isozymes_forward^rid^X.isozyme_variables(string(rid), reaction_isozymes) + end +end +for (rid, _) in m.fluxes_backward + if haskey(reaction_isozymes, string(rid)) + m += :fluxes_isozymes_backward^rid^X.isozyme_variables(string(rid), reaction_isozymes) + end +end + +# link isozyme fluxes to directional fluxes +m *= + :link_isozyme_fluxes_forward^X.link_isozymes( + m.fluxes_forward, + m.fluxes_isozymes_forward, + ) +m *= + :link_isozyme_fluxes_backward^X.link_isozymes( + m.fluxes_backward, + m.fluxes_isozymes_backward, + ) + +# create enzyme variables +m += :enzymes^X.enzyme_variables(model) + +# add enzyme mass balances +m *= + :enzyme_stoichiometry^X.enzyme_stoichiometry( + m.enzymes, + m.fluxes_isozymes_forward, + m.fluxes_isozymes_backward, + reaction_isozymes, + ) + +# add capacity limitations +m *= + :capacity_limitation^X.enzyme_capacity( + m.enzymes, + enzyme_molar_mass, + [:G_b0351, :G_b1241, :G_b0726, :G_b0727], + 10.0, + ) From 90df3f87c633768aa6a9e28c715c131cdb23b945 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 12:00:08 +0100 Subject: [PATCH 401/531] format --- docs/src/examples/04-enzyme-models.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/src/examples/04-enzyme-models.jl b/docs/src/examples/04-enzyme-models.jl index c26e4b62c..72f3cca49 100644 --- a/docs/src/examples/04-enzyme-models.jl +++ b/docs/src/examples/04-enzyme-models.jl @@ -38,12 +38,17 @@ m *= # create fluxes for each isozyme for (rid, _) in m.fluxes_forward if haskey(reaction_isozymes, string(rid)) - m += :fluxes_isozymes_forward^rid^X.isozyme_variables(string(rid), reaction_isozymes) + m += + :fluxes_isozymes_forward^rid^X.isozyme_variables(string(rid), reaction_isozymes) end end for (rid, _) in m.fluxes_backward if haskey(reaction_isozymes, string(rid)) - m += :fluxes_isozymes_backward^rid^X.isozyme_variables(string(rid), reaction_isozymes) + m += + :fluxes_isozymes_backward^rid^X.isozyme_variables( + string(rid), + reaction_isozymes, + ) end end From 623a56ce2d5a3b15e132c036ad561556ea8043c3 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 12:02:00 +0100 Subject: [PATCH 402/531] example temp --- docs/src/examples/04-enzyme-models.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/docs/src/examples/04-enzyme-models.jl b/docs/src/examples/04-enzyme-models.jl index 72f3cca49..a5370d87e 100644 --- a/docs/src/examples/04-enzyme-models.jl +++ b/docs/src/examples/04-enzyme-models.jl @@ -1,3 +1,25 @@ +# # Enzyme constrained models + +# Here we will construct an enzyme constrained variant of the *E. coli* "core" +# model. We will need the model, which we can download using +# [`download_model`](@ref): + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use Tulip here: + +import JSONFBCModels +import Tulip + +model = load_model("e_coli_core.json") + import JSONFBCModels as M import AbstractFBCModels as A import COBREXA as X From e8fa20510e6c9f98b380b1bd5edaeca5ab34b609 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 9 Dec 2023 12:02:47 +0100 Subject: [PATCH 403/531] add isozyme type --- src/types.jl | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/types.jl b/src/types.jl index 281c4278a..142cbb10d 100644 --- a/src/types.jl +++ b/src/types.jl @@ -5,3 +5,28 @@ Type of optional values. """ const Maybe{X} = Union{Nothing,X} + +""" +$(TYPEDEF) + +Information about isozyme composition including subunit stoichiometry and +turnover numbers. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Isozyme + gene_product_stoichiometry::Dict{String,Float64} + kcat_forward::Maybe{Float64} = nothing + kcat_backward::Maybe{Float64} = nothing +end + +""" +$(TYPEDSIGNATURES) + +A convenience constructor for [`Isozyme`](@ref) that takes a string gene +reaction rule and converts it into the appropriate format. Assumes the +`gene_product_stoichiometry` for each subunit is 1. +""" +Isozyme(gids::Vector{String}; kwargs...) = + Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) From 3839a73968d547c69c5b3fe00c2c50c05bfd307a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 10 Dec 2023 16:03:40 +0100 Subject: [PATCH 404/531] format --- docs/src/examples/04-enzyme-models.jl | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/docs/src/examples/04-enzyme-models.jl b/docs/src/examples/04-enzyme-models.jl index a5370d87e..c70505699 100644 --- a/docs/src/examples/04-enzyme-models.jl +++ b/docs/src/examples/04-enzyme-models.jl @@ -1,16 +1,14 @@ # # Enzyme constrained models +using COBREXA + # Here we will construct an enzyme constrained variant of the *E. coli* "core" -# model. We will need the model, which we can download using -# [`download_model`](@ref): +# model. We will need the model, which we can download if it is not already present. -using COBREXA +import Downloads: download -download_model( - "http://bigg.ucsd.edu/static/models/e_coli_core.json", - "e_coli_core.json", - "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", -) +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") # Additionally to COBREXA and the model format package, we will need a solver # -- let's use Tulip here: @@ -20,14 +18,10 @@ import Tulip model = load_model("e_coli_core.json") -import JSONFBCModels as M -import AbstractFBCModels as A -import COBREXA as X -import ConstraintTrees as C -import JuMP as J -import Gurobi as G +# Enzyme constrained models require parameters not usually used by conventional +# constraint based models. These include reaction specific turnover numbers -model = A.load(M.JSONFBCModel, "e_coli_core.json") +import AbstractFBCModels as A reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() for rid in A.reactions(model) From 566c70923a9693df0b768816504953ade89c7c90 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 12:44:19 +0100 Subject: [PATCH 405/531] implement basic ec functionality --- docs/src/examples/04-enzyme-models.jl | 102 ------------------ .../examples/05-enzyme-constrained-models.jl | 97 +++++++++++++++++ src/COBREXA.jl | 1 + src/builders/enzymes.jl | 88 +++++++++++++-- 4 files changed, 180 insertions(+), 108 deletions(-) delete mode 100644 docs/src/examples/04-enzyme-models.jl create mode 100644 docs/src/examples/05-enzyme-constrained-models.jl diff --git a/docs/src/examples/04-enzyme-models.jl b/docs/src/examples/04-enzyme-models.jl deleted file mode 100644 index c70505699..000000000 --- a/docs/src/examples/04-enzyme-models.jl +++ /dev/null @@ -1,102 +0,0 @@ -# # Enzyme constrained models - -using COBREXA - -# Here we will construct an enzyme constrained variant of the *E. coli* "core" -# model. We will need the model, which we can download if it is not already present. - -import Downloads: download - -!isfile("e_coli_core.json") && - download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") - -# Additionally to COBREXA and the model format package, we will need a solver -# -- let's use Tulip here: - -import JSONFBCModels -import Tulip - -model = load_model("e_coli_core.json") - -# Enzyme constrained models require parameters not usually used by conventional -# constraint based models. These include reaction specific turnover numbers - -import AbstractFBCModels as A - -reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() -for rid in A.reactions(model) - haskey(ecoli_core_reaction_kcats, rid) || continue - for (i, grr) in enumerate(A.reaction_gene_association_dnf(model, rid)) - d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) - d["isozyme_"*string(i)] = X.Isozyme( - gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], - ) - end -end - -m = X.metabolic_model(model) - -# create directional fluxes -m += - :fluxes_forward^X.fluxes_in_direction(m.fluxes, :forward) + - :fluxes_backward^X.fluxes_in_direction(m.fluxes, :backward) - -# link directional fluxes to original fluxes -m *= - :link_flux_directions^X.sign_split_constraints( - positive = m.fluxes_forward, - negative = m.fluxes_backward, - signed = m.fluxes, - ) - -# create fluxes for each isozyme -for (rid, _) in m.fluxes_forward - if haskey(reaction_isozymes, string(rid)) - m += - :fluxes_isozymes_forward^rid^X.isozyme_variables(string(rid), reaction_isozymes) - end -end -for (rid, _) in m.fluxes_backward - if haskey(reaction_isozymes, string(rid)) - m += - :fluxes_isozymes_backward^rid^X.isozyme_variables( - string(rid), - reaction_isozymes, - ) - end -end - -# link isozyme fluxes to directional fluxes -m *= - :link_isozyme_fluxes_forward^X.link_isozymes( - m.fluxes_forward, - m.fluxes_isozymes_forward, - ) -m *= - :link_isozyme_fluxes_backward^X.link_isozymes( - m.fluxes_backward, - m.fluxes_isozymes_backward, - ) - -# create enzyme variables -m += :enzymes^X.enzyme_variables(model) - -# add enzyme mass balances -m *= - :enzyme_stoichiometry^X.enzyme_stoichiometry( - m.enzymes, - m.fluxes_isozymes_forward, - m.fluxes_isozymes_backward, - reaction_isozymes, - ) - -# add capacity limitations -m *= - :capacity_limitation^X.enzyme_capacity( - m.enzymes, - enzyme_molar_mass, - [:G_b0351, :G_b1241, :G_b0726, :G_b0727], - 10.0, - ) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl new file mode 100644 index 000000000..a9168712a --- /dev/null +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -0,0 +1,97 @@ +# # Enzyme constrained models + +using COBREXA + +# Here we will construct an enzyme constrained variant of the *E. coli* "core" +# model. We will need the model, which we can download if it is not already present. + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use Tulip here: + +import JSONFBCModels +import Tulip + +model = load_model("e_coli_core.json") + +# Enzyme constrained models require parameters that are usually not used by +# conventional constraint based models. These include reaction specific turnover +# numbers, molar masses of enzymes, and capacity bounds. + +import AbstractFBCModels as A + +### Reaction turnover numbers + +# Here we will use randomly generated turnover numbers for simplicity. Each +# reaction in a constraint-based model usually has gene reaction rules +# associated with it. These typically take the form of, possibly multiple, +# isozymes that can catalyze a reaction. A turnover number needs to be assigned +# to each isozyme, as shown below. + +using Random + +reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. +for rid in A.reactions(model) + grrs = A.reaction_gene_association_dnf(model, rid) + isnothing(grrs) && continue # skip if no grr available + + for (i, grr) in enumerate(grrs) + d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) + # each isozyme gets a unique name + d["isozyme_"*string(i)] = X.Isozyme( # Isozyme struct is defined by COBREXA + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes + kcat_forward = 100 + 50 * rand(), # forward reaction turnover number + kcat_backward = 100 + 50 * rand(), # reverse reaction turnover number + # kcat_forward = ecoli_core_reaction_kcats[rid][i][1], + # kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ) + end +end + +#!!! warning "Turnover number units" +# Take care with the units of the turnover numbers. In literature they are +# usually reported in 1/s. However, flux units are typically mmol/gDW/h, +# suggesting that you should rescale the turnover numbers to 1/h if you +# want to use the traditional flux units. + +### Enzyme molar masses + +# Similarly, we will randomly generate enzyme molar masses for use in the enzyme +# constrained model. + +gene_molar_masses = Dict( + gid => 40 + 40 * rand() for gid in A.genes(model) +) + +#!!! warning "Molar mass units" +# Take care with the units of the molar masses. In literature they are +# usually reported in Da or kDa (g/mol). However, as noted above, flux +# units are typically mmol/gDW/h. Since the enzyme kinetic equation is +# `v = k * e`, where `k` is the turnover number, it suggests that the +# enzyme variable will have units of mmol/gDW. The molar masses come +# into play when setting the capacity limitations, e.g. usually a sum +# over all enzymes weighted by their molar masses: `e * mm`. Thus, if +# your capacity limitation has units of g/gDW, then the molar masses +# must have units of g/mmol. + +### Capacity limitation + +# The capacity limitation usually denotes an upper bound of protein available to +# the cell. + +total_enzyme_capacity = 0.5 # g enzyme/gDW + +### Running a basic enzyme constrained model + +import COBREXA as X + +m = X.build_enzyme_constrained_model( + model, + reaction_isozymes, + gene_molar_masses, + [("caplim", ["b0351", "b1241", "b0726", "b0727"], 0.5)] +) \ No newline at end of file diff --git a/src/COBREXA.jl b/src/COBREXA.jl index f6efb9c24..ec2fcf564 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -33,6 +33,7 @@ include("solver.jl") include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") +include("builders/enzymes.jl") include("analysis/modifications.jl") include("analysis/flux_balance.jl") diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 7836a222f..29c3e8386 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -61,7 +61,7 @@ function enzyme_stoichiometry( for isozyme in values(isozymes) for gid in keys(isozyme.gene_product_stoichiometry) rids = get!(enzyme_rid_lookup, Symbol(gid), Symbol[]) - rid in rids || push!(rids, rid) + rid in rids || push!(rids, Symbol(rid)) end end end @@ -106,10 +106,10 @@ function enzyme_balance( gid::Symbol, rid::Symbol, fluxes_isozymes::C.ConstraintTree, # direction - reaction_isozymes::Dict{Symbol,Dict{Symbol,Isozyme}}, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, direction = :kcat_forward, ) - isozyme_dict = reaction_isozymes[rid] + isozyme_dict = Dict(Symbol(k) => v for (k, v) in reaction_isozymes[string(rid)]) sum( # this is where the stoichiometry comes in -isozyme_value.value * @@ -123,12 +123,88 @@ end function enzyme_capacity( enzymes::C.ConstraintTree, - enzyme_molar_mass::Dict{Symbol,Float64}, - enzyme_ids::Vector{Symbol}, + enzyme_molar_mass::Dict{String,Float64}, + enzyme_ids::Vector{String}, capacity::Float64, ) C.Constraint( - value = sum(enzymes[gid].value * enzyme_molar_mass[gid] for gid in enzyme_ids), + value = sum(enzymes[Symbol(gid)].value * enzyme_molar_mass[gid] for gid in enzyme_ids), bound = (0.0, capacity), ) end + +function build_enzyme_constrained_model( + model::A.AbstractFBCModel, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + gene_molar_masses::Dict{String, Float64}, + capacity_limitations::Vector{Tuple{String, Vector{String}, Float64}}, +) + # create base constraint tree + m = fbc_model_constraints(model) + + # create directional fluxes + m += + :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + + :fluxes_backward^fluxes_in_direction(m.fluxes, :backward) + + # link directional fluxes to original fluxes + m *= + :link_flux_directions^sign_split_constraints( + positive = m.fluxes_forward, + negative = m.fluxes_backward, + signed = m.fluxes, + ) + + # create fluxes for each isozyme + for (rid, _) in m.fluxes_forward + if haskey(reaction_isozymes, string(rid)) + m += + :fluxes_isozymes_forward^rid^isozyme_variables(string(rid), reaction_isozymes) + end + end + for (rid, _) in m.fluxes_backward + if haskey(reaction_isozymes, string(rid)) + m += + :fluxes_isozymes_backward^rid^isozyme_variables( + string(rid), + reaction_isozymes, + ) + end + end + + # link isozyme fluxes to directional fluxes + m *= + :link_isozyme_fluxes_forward^link_isozymes( + m.fluxes_forward, + m.fluxes_isozymes_forward, + ) + m *= + :link_isozyme_fluxes_backward^link_isozymes( + m.fluxes_backward, + m.fluxes_isozymes_backward, + ) + + # create enzyme variables + m += :enzymes^enzyme_variables(model) + + # add enzyme mass balances + m *= + :enzyme_stoichiometry^enzyme_stoichiometry( + m.enzymes, + m.fluxes_isozymes_forward, + m.fluxes_isozymes_backward, + reaction_isozymes, + ) + + # add capacity limitations + for (id, gids, cap) in capacity_limitations + m *= Symbol(id)^enzyme_capacity( + m.enzymes, + gene_molar_masses, + gids, + cap, + ) + end + + return m +end \ No newline at end of file From 0e47b431e707cf002feca0287f880eedb69da82d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 12:46:20 +0100 Subject: [PATCH 406/531] format --- .../examples/05-enzyme-constrained-models.jl | 10 +++--- src/builders/enzymes.jl | 34 +++++++++---------- 2 files changed, 21 insertions(+), 23 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index a9168712a..afb2f1b7a 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -38,7 +38,7 @@ reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() # a mapping from react for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available - + for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) # each isozyme gets a unique name @@ -63,9 +63,7 @@ end # Similarly, we will randomly generate enzyme molar masses for use in the enzyme # constrained model. -gene_molar_masses = Dict( - gid => 40 + 40 * rand() for gid in A.genes(model) -) +gene_molar_masses = Dict(gid => 40 + 40 * rand() for gid in A.genes(model)) #!!! warning "Molar mass units" # Take care with the units of the molar masses. In literature they are @@ -93,5 +91,5 @@ m = X.build_enzyme_constrained_model( model, reaction_isozymes, gene_molar_masses, - [("caplim", ["b0351", "b1241", "b0726", "b0727"], 0.5)] -) \ No newline at end of file + [("caplim", ["b0351", "b1241", "b0726", "b0727"], 0.5)], +) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 29c3e8386..931fa3246 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -128,7 +128,9 @@ function enzyme_capacity( capacity::Float64, ) C.Constraint( - value = sum(enzymes[Symbol(gid)].value * enzyme_molar_mass[gid] for gid in enzyme_ids), + value = sum( + enzymes[Symbol(gid)].value * enzyme_molar_mass[gid] for gid in enzyme_ids + ), bound = (0.0, capacity), ) end @@ -136,8 +138,8 @@ end function build_enzyme_constrained_model( model::A.AbstractFBCModel, reaction_isozymes::Dict{String,Dict{String,Isozyme}}, - gene_molar_masses::Dict{String, Float64}, - capacity_limitations::Vector{Tuple{String, Vector{String}, Float64}}, + gene_molar_masses::Dict{String,Float64}, + capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}, ) # create base constraint tree m = fbc_model_constraints(model) @@ -146,7 +148,7 @@ function build_enzyme_constrained_model( m += :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + :fluxes_backward^fluxes_in_direction(m.fluxes, :backward) - + # link directional fluxes to original fluxes m *= :link_flux_directions^sign_split_constraints( @@ -154,12 +156,15 @@ function build_enzyme_constrained_model( negative = m.fluxes_backward, signed = m.fluxes, ) - + # create fluxes for each isozyme for (rid, _) in m.fluxes_forward if haskey(reaction_isozymes, string(rid)) m += - :fluxes_isozymes_forward^rid^isozyme_variables(string(rid), reaction_isozymes) + :fluxes_isozymes_forward^rid^isozyme_variables( + string(rid), + reaction_isozymes, + ) end end for (rid, _) in m.fluxes_backward @@ -171,7 +176,7 @@ function build_enzyme_constrained_model( ) end end - + # link isozyme fluxes to directional fluxes m *= :link_isozyme_fluxes_forward^link_isozymes( @@ -183,10 +188,10 @@ function build_enzyme_constrained_model( m.fluxes_backward, m.fluxes_isozymes_backward, ) - + # create enzyme variables m += :enzymes^enzyme_variables(model) - + # add enzyme mass balances m *= :enzyme_stoichiometry^enzyme_stoichiometry( @@ -195,16 +200,11 @@ function build_enzyme_constrained_model( m.fluxes_isozymes_backward, reaction_isozymes, ) - + # add capacity limitations for (id, gids, cap) in capacity_limitations - m *= Symbol(id)^enzyme_capacity( - m.enzymes, - gene_molar_masses, - gids, - cap, - ) + m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) end return m -end \ No newline at end of file +end From 95d87882308fae32b252128227f3656326d8a106 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 12:51:46 +0100 Subject: [PATCH 407/531] export ec builder --- docs/src/examples/05-enzyme-constrained-models.jl | 4 +--- src/builders/enzymes.jl | 8 ++++++++ 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index afb2f1b7a..0c43edbeb 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -85,9 +85,7 @@ total_enzyme_capacity = 0.5 # g enzyme/gDW ### Running a basic enzyme constrained model -import COBREXA as X - -m = X.build_enzyme_constrained_model( +m = build_enzyme_constrained_model( model, reaction_isozymes, gene_molar_masses, diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 931fa3246..e3ce2c5a7 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -135,6 +135,12 @@ function enzyme_capacity( ) end +""" +$(TYPEDSIGNATURES) + +Return an enzyme constrained model, taking as input a standard constraint based +model. +""" function build_enzyme_constrained_model( model::A.AbstractFBCModel, reaction_isozymes::Dict{String,Dict{String,Isozyme}}, @@ -208,3 +214,5 @@ function build_enzyme_constrained_model( return m end + +export build_enzyme_constrained_model \ No newline at end of file From 2b315efe8a4b1769df0ea2de0c16a4c9b743cf20 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 18:52:49 +0100 Subject: [PATCH 408/531] old gecko values do not match new implementation --- .../examples/05-enzyme-constrained-models.jl | 36 +++++++-- src/builders/enzymes.jl | 75 +++++++++++++------ test/data_static.jl | 1 + 3 files changed, 84 insertions(+), 28 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 0c43edbeb..e6153e749 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -38,7 +38,7 @@ reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() # a mapping from react for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available - + haskey(ecoli_core_reaction_kcats, rid) || continue #src for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) # each isozyme gets a unique name @@ -46,8 +46,11 @@ for rid in A.reactions(model) gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes kcat_forward = 100 + 50 * rand(), # forward reaction turnover number kcat_backward = 100 + 50 * rand(), # reverse reaction turnover number - # kcat_forward = ecoli_core_reaction_kcats[rid][i][1], - # kcat_backward = ecoli_core_reaction_kcats[rid][i][2], + ) + d["isozyme_"*string(i)] = X.Isozyme( #src + gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), #src + kcat_forward = ecoli_core_reaction_kcats[rid][i][1], #src + kcat_backward = ecoli_core_reaction_kcats[rid][i][2], #src ) end end @@ -63,7 +66,8 @@ end # Similarly, we will randomly generate enzyme molar masses for use in the enzyme # constrained model. -gene_molar_masses = Dict(gid => 40 + 40 * rand() for gid in A.genes(model)) +gene_molar_masses = Dict(gid => 100 + 40 * rand() for gid in A.genes(model)) +gene_molar_masses = ecoli_core_gene_product_masses #src #!!! warning "Molar mass units" # Take care with the units of the molar masses. In literature they are @@ -81,7 +85,7 @@ gene_molar_masses = Dict(gid => 40 + 40 * rand() for gid in A.genes(model)) # The capacity limitation usually denotes an upper bound of protein available to # the cell. -total_enzyme_capacity = 0.5 # g enzyme/gDW +total_enzyme_capacity = 100.0 # g enzyme/gDW ### Running a basic enzyme constrained model @@ -89,5 +93,25 @@ m = build_enzyme_constrained_model( model, reaction_isozymes, gene_molar_masses, - [("caplim", ["b0351", "b1241", "b0726", "b0727"], 0.5)], + [("total_proteome_bound", A.genes(model), total_enzyme_capacity)], ) + +m.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) # undo glucose important bound +m.fluxes.GLCpts.bound = (-1.0, 12.0) # undo glucose important bound +for (k, v) in m.enzymes + if k == :b2779 + v.bound = (0.01, 0.06) + else + v.bound = (0.0, 1.0) + end +end + +sol = optimized_constraints( + m; + objective = m.objective.value, + optimizer = Tulip.Optimizer, + modifications =[set_optimizer_attribute("IPM_IterationsLimit", 1000)] +) + + +### Building the model incrementally diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index e3ce2c5a7..2a1da13a3 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -1,18 +1,21 @@ """ $(TYPEDSIGNATURES) -Allocate enzyme variables (gene products in the model) to a constraint tree. +Allocate enzyme variables (gene products in the model) to a constraint tree +using all the genes in the `model`. """ enzyme_variables(model::A.AbstractFBCModel) = C.variables(; keys = Symbol.(A.genes(model)), bounds = Ref((0.0, Inf))) +export enzyme_variables + """ $(TYPEDSIGNATURES) -Create isozyme variables for reactions. A single reaction may be catalyzed by -multiple enzymes (isozymes), and the total flux through a reaction is the sum -through of all these isozymes. These variables are linked to fluxes through -[`link_isozymes`](@ref). +Helper function to create isozyme variables for reactions. A single reaction may +be catalyzed by multiple enzymes (isozymes), and the total flux through a +reaction is the sum through of all these isozymes. These variables are linked to +fluxes through [`link_isozymes`](@ref). """ function isozyme_variables( reaction_id::String, @@ -27,8 +30,8 @@ end """ $(TYPEDSIGNATURES) -Link isozymes to fluxes. All forward (backward) fluxes are the sum of all the -isozymes catalysing these fluxes. +Helper function to link isozymes to fluxes. All forward (backward) fluxes are +the sum of all the isozymes catalysing these fluxes. """ function link_isozymes( fluxes_directional::C.ConstraintTree, @@ -45,9 +48,9 @@ end """ $(TYPEDSIGNATURES) -Create the enzyme "mass balance" matrix. In essence, the stoichiometric -coefficient is subunit_stoichiometry / kcat for each directional, isozyme flux, -and it must be balanced by the enzyme variable supply. +Helper function to create the enzyme "mass balance" matrix. In essence, the +stoichiometric coefficient is subunit_stoichiometry / kcat for each directional, +isozyme flux, and it must be balanced by the enzyme variable supply. """ function enzyme_stoichiometry( enzymes::C.ConstraintTree, @@ -121,25 +124,53 @@ function enzyme_balance( ) end +""" +$(TYPEDSIGNATURES) + +Create enzyme capacity limitation. +""" function enzyme_capacity( enzymes::C.ConstraintTree, - enzyme_molar_mass::Dict{String,Float64}, + gene_molar_masses::Dict{String,Float64}, enzyme_ids::Vector{String}, capacity::Float64, ) C.Constraint( value = sum( - enzymes[Symbol(gid)].value * enzyme_molar_mass[gid] for gid in enzyme_ids + enzymes[Symbol(gid)].value * gene_molar_masses[gid] for gid in enzyme_ids ), bound = (0.0, capacity), ) end +export enzyme_capacity + """ $(TYPEDSIGNATURES) -Return an enzyme constrained model, taking as input a standard constraint based -model. +Create enzyme constraints. +""" +function enzyme_constraints( + fluxes::C.ConstraintTree, + enzymes::C.ConstraintTree, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, +) + +# TODO + +end + +export enzyme_constraints + +""" +$(TYPEDSIGNATURES) + +Return an enzyme constrained model, taking as input a standard constraint-based +`model`. The enzyme model is parameterized by `reaction_isozymes`, which is a +mapping of reaction IDs (those used in the fluxes of the model) to named +[`Isozyme`](@ref)s. Additionally, `gene_molar_masses` and `capacity_limitations` +should be supplied. The latter is a vector of tuples, where each tuple +represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. """ function build_enzyme_constrained_model( model::A.AbstractFBCModel, @@ -147,12 +178,15 @@ function build_enzyme_constrained_model( gene_molar_masses::Dict{String,Float64}, capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}, ) + # create base constraint tree m = fbc_model_constraints(model) + # create enzyme variables + m += :enzymes^enzyme_variables(model) + # create directional fluxes - m += - :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + + m += :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + :fluxes_backward^fluxes_in_direction(m.fluxes, :backward) # link directional fluxes to original fluxes @@ -195,9 +229,6 @@ function build_enzyme_constrained_model( m.fluxes_isozymes_backward, ) - # create enzyme variables - m += :enzymes^enzyme_variables(model) - # add enzyme mass balances m *= :enzyme_stoichiometry^enzyme_stoichiometry( @@ -206,13 +237,13 @@ function build_enzyme_constrained_model( m.fluxes_isozymes_backward, reaction_isozymes, ) - + # add capacity limitations for (id, gids, cap) in capacity_limitations m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) end - return m + m end -export build_enzyme_constrained_model \ No newline at end of file +return build_enzyme_constrained_model diff --git a/test/data_static.jl b/test/data_static.jl index 1800aadd2..2956bcf42 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -224,6 +224,7 @@ const ecoli_core_gene_product_masses = Dict{String,Float64}( "b1611" => 50.489, "b0726" => 105.062, "b2279" => 10.845, + "s0001" => 0.0, ) const ecoli_core_reaction_kcats = Dict{String,Vector{Tuple{Float64,Float64}}}( From 88ea65ccae971400f44ab49cdfc2eab5bef5974d Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 18:54:01 +0100 Subject: [PATCH 409/531] format --- docs/src/examples/05-enzyme-constrained-models.jl | 2 +- src/builders/enzymes.jl | 7 ++++--- test/enzyme-model.jl | 1 + 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 test/enzyme-model.jl diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index e6153e749..0de6e1ad8 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -110,7 +110,7 @@ sol = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, - modifications =[set_optimizer_attribute("IPM_IterationsLimit", 1000)] + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 2a1da13a3..03178ce09 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -156,7 +156,7 @@ function enzyme_constraints( reaction_isozymes::Dict{String,Dict{String,Isozyme}}, ) -# TODO + # TODO end @@ -186,7 +186,8 @@ function build_enzyme_constrained_model( m += :enzymes^enzyme_variables(model) # create directional fluxes - m += :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + + m += + :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + :fluxes_backward^fluxes_in_direction(m.fluxes, :backward) # link directional fluxes to original fluxes @@ -237,7 +238,7 @@ function build_enzyme_constrained_model( m.fluxes_isozymes_backward, reaction_isozymes, ) - + # add capacity limitations for (id, gids, cap) in capacity_limitations m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) diff --git a/test/enzyme-model.jl b/test/enzyme-model.jl new file mode 100644 index 000000000..8b1378917 --- /dev/null +++ b/test/enzyme-model.jl @@ -0,0 +1 @@ + From 8d9ae455a236b314426c5163c0b5364b747a674c Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 11 Dec 2023 19:11:22 +0100 Subject: [PATCH 410/531] include small enzyme test model --- src/types.jl | 2 ++ test/enzyme-model.jl | 85 ++++++++++++++++++++++++++++++++++++++++++++ test/runtests.jl | 1 + 3 files changed, 88 insertions(+) diff --git a/src/types.jl b/src/types.jl index 142cbb10d..850f96624 100644 --- a/src/types.jl +++ b/src/types.jl @@ -21,6 +21,8 @@ Base.@kwdef mutable struct Isozyme kcat_backward::Maybe{Float64} = nothing end +export Isozyme + """ $(TYPEDSIGNATURES) diff --git a/test/enzyme-model.jl b/test/enzyme-model.jl index 8b1378917..d09943d4d 100644 --- a/test/enzyme-model.jl +++ b/test/enzyme-model.jl @@ -1 +1,86 @@ +@testset "GECKO small model" begin + #= + Implement the small model found in the supplment of the + original GECKO paper. This model is nice to troubleshoot with, + because the stoich matrix is small. + =# + mets = Dict( + "m1" => A.CanonicalModel.Metabolite(), + "m2" => A.CanonicalModel.Metabolite(), + "m3" => A.CanonicalModel.Metabolite(), + "m4" => A.CanonicalModel.Metabolite(), + ) + + rxns = Dict( + "r1" => A.CanonicalModel.Reaction( + lower_bound = 0.0, + upper_bound = 100.0, + stoichiometry = Dict("m1" => 1.0), + ), + "r2" => A.CanonicalModel.Reaction( + lower_bound = 0.0, + upper_bound = 100.0, + stoichiometry = Dict("m2" => 1.0), + ), + "r3" => A.CanonicalModel.Reaction( + lower_bound = 0.0, + upper_bound = 100.0, + stoichiometry = Dict("m1" => -1.0, "m2" => -1.0, "m3" => 1.0), + ), + "r4" => A.CanonicalModel.Reaction( + lower_bound = 0.0, + upper_bound = 100.0, + stoichiometry = Dict("m3" => -1.0, "m4" => 1.0), + ), + "r5" => A.CanonicalModel.Reaction( + lower_bound = -100.0, + upper_bound = 100.0, + stoichiometry = Dict("m2" => -1.0, "m4" => 1.0), + ), + "r6" => A.CanonicalModel.Reaction( + lower_bound = 0.0, + upper_bound = 100.0, + stoichiometry = Dict("m4" => -1.0), + objective_coefficient = 1.0, + ), + ) + + gs = Dict("g$i" => A.CanonicalModel.Gene() for i = 1:5) + + model = A.CanonicalModel.Model(rxns, mets, gs) + + reaction_isozymes = Dict( + "r3" => Dict("iso1" => Isozyme(Dict("g1" => 1), 1.0, 1.0)), + "r4" => Dict( + "iso1" => Isozyme(Dict("g1" => 1), 2.0, 2.0), + "iso2" => Isozyme(Dict("g2" => 1), 3.0, 3.0), + ), + "r5" => Dict("iso1" => Isozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)), + ) + + gene_product_molar_mass = + Dict("g1" => 1.0, "g2" => 2.0, "g3" => 3.0, "g4" => 4.0, "g5" => 1.0) + + m = build_enzyme_constrained_model( + model, + reaction_isozymes, + gene_product_molar_mass, + [("total_proteome_bound", A.genes(model), 0.5)], + ) + + sol = optimized_constraints( + m; + objective = m.objective.value, + optimizer = Tulip.Optimizer, + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], + ) + + rxn_fluxes = flux_dict(gm, opt_model) + gene_products = gene_product_dict(gm, opt_model) + mass_groups = gene_product_mass_group_dict(gm, opt_model) + + @test isapprox(sol.objective, 3.181818181753438, atol = TEST_TOLERANCE) + @test isapprox(sol.enzymes.g4, 0.09090909090607537, atol = TEST_TOLERANCE) + @test isapprox(sol.total_proteome_bound, 0.5, atol = TEST_TOLERANCE) +end diff --git a/test/runtests.jl b/test/runtests.jl index 88b05b85b..300321206 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,6 +45,7 @@ run_test_file("data_downloaded.jl") @testset "COBREXA test suite" begin run_doc_examples() + include("enzyme-model.jl") run_test_file("aqua.jl") end From cf530cc5cebf4ae85e98d6b951795de7440b85c2 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 14 Dec 2023 11:42:02 +0100 Subject: [PATCH 411/531] export, not return --- src/builders/enzymes.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 03178ce09..0a0b8dd0e 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -247,4 +247,4 @@ function build_enzyme_constrained_model( m end -return build_enzyme_constrained_model +export build_enzyme_constrained_model From 4f146c4080baf6bc92f38f7d31986ff8bdf837c9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 14 Dec 2023 16:52:49 +0100 Subject: [PATCH 412/531] fix tests --- .../examples/05-enzyme-constrained-models.jl | 44 +++++++++---------- src/builders/enzymes.jl | 17 ------- test/enzyme-model.jl | 12 ++--- 3 files changed, 24 insertions(+), 49 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 0de6e1ad8..83602f0de 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -34,23 +34,23 @@ import AbstractFBCModels as A using Random -reaction_isozymes = Dict{String,Dict{String,X.Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. +reaction_isozymes = Dict{String,Dict{String,Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available haskey(ecoli_core_reaction_kcats, rid) || continue #src for (i, grr) in enumerate(grrs) - d = get!(reaction_isozymes, rid, Dict{String,X.Isozyme}()) + d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) # each isozyme gets a unique name - d["isozyme_"*string(i)] = X.Isozyme( # Isozyme struct is defined by COBREXA + d["isozyme_"*string(i)] = Isozyme( # Isozyme struct is defined by COBREXA gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes - kcat_forward = 100 + 50 * rand(), # forward reaction turnover number - kcat_backward = 100 + 50 * rand(), # reverse reaction turnover number + kcat_forward = (100 + 50 * rand()) * 3600.0, # forward reaction turnover number units = 1/h + kcat_backward = (100 + 50 * rand()) * 3600.0, # reverse reaction turnover number units = 1/h ) - d["isozyme_"*string(i)] = X.Isozyme( #src + d["isozyme_"*string(i)] = Isozyme( #src gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), #src - kcat_forward = ecoli_core_reaction_kcats[rid][i][1], #src - kcat_backward = ecoli_core_reaction_kcats[rid][i][2], #src + kcat_forward = ecoli_core_reaction_kcats[rid][i][1] * 3600.0, #src + kcat_backward = ecoli_core_reaction_kcats[rid][i][2] * 3600.0, #src ) end end @@ -66,7 +66,7 @@ end # Similarly, we will randomly generate enzyme molar masses for use in the enzyme # constrained model. -gene_molar_masses = Dict(gid => 100 + 40 * rand() for gid in A.genes(model)) +gene_molar_masses = Dict(gid => 20 + 40 * rand() for gid in A.genes(model)) gene_molar_masses = ecoli_core_gene_product_masses #src #!!! warning "Molar mass units" @@ -78,14 +78,14 @@ gene_molar_masses = ecoli_core_gene_product_masses #src # into play when setting the capacity limitations, e.g. usually a sum # over all enzymes weighted by their molar masses: `e * mm`. Thus, if # your capacity limitation has units of g/gDW, then the molar masses -# must have units of g/mmol. +# must have units of g/mmol (= kDa). ### Capacity limitation # The capacity limitation usually denotes an upper bound of protein available to # the cell. -total_enzyme_capacity = 100.0 # g enzyme/gDW +total_enzyme_capacity = 0.1 # g enzyme/gDW ### Running a basic enzyme constrained model @@ -96,22 +96,18 @@ m = build_enzyme_constrained_model( [("total_proteome_bound", A.genes(model), total_enzyme_capacity)], ) -m.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) # undo glucose important bound -m.fluxes.GLCpts.bound = (-1.0, 12.0) # undo glucose important bound -for (k, v) in m.enzymes - if k == :b2779 - v.bound = (0.01, 0.06) - else - v.bound = (0.0, 1.0) - end -end +m.fluxes.EX_glc__D_e.bound = (-1000.0, 0.0) # undo glucose important bound from original model +m.enzymes.b2417.bound = (0.0, 0.1) # for fun, change the bounds of the protein b2417 -sol = optimized_constraints( +solution = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], ) - -### Building the model incrementally +#src these values should be unique (glucose transporter is the only way to get carbon into the system) +@test isapprox(solution.objective, 3.2105477675077743, atol = TEST_TOLERANCE) #src +@test isapprox(solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src +@test isapprox(solution.fluxes.EX_glc__D_e, -41.996885051738445, atol = TEST_TOLERANCE) #src +@test isapprox(solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 0a0b8dd0e..c238e9533 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -148,23 +148,6 @@ export enzyme_capacity """ $(TYPEDSIGNATURES) -Create enzyme constraints. -""" -function enzyme_constraints( - fluxes::C.ConstraintTree, - enzymes::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, -) - - # TODO - -end - -export enzyme_constraints - -""" -$(TYPEDSIGNATURES) - Return an enzyme constrained model, taking as input a standard constraint-based `model`. The enzyme model is parameterized by `reaction_isozymes`, which is a mapping of reaction IDs (those used in the fluxes of the model) to named diff --git a/test/enzyme-model.jl b/test/enzyme-model.jl index d09943d4d..151b818e0 100644 --- a/test/enzyme-model.jl +++ b/test/enzyme-model.jl @@ -69,18 +69,14 @@ [("total_proteome_bound", A.genes(model), 0.5)], ) - sol = optimized_constraints( + řešení = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) - rxn_fluxes = flux_dict(gm, opt_model) - gene_products = gene_product_dict(gm, opt_model) - mass_groups = gene_product_mass_group_dict(gm, opt_model) - - @test isapprox(sol.objective, 3.181818181753438, atol = TEST_TOLERANCE) - @test isapprox(sol.enzymes.g4, 0.09090909090607537, atol = TEST_TOLERANCE) - @test isapprox(sol.total_proteome_bound, 0.5, atol = TEST_TOLERANCE) + @test isapprox(řešení.objective, 3.181818181753438, atol = TEST_TOLERANCE) + @test isapprox(řešení.enzymes.g4, 0.09090909090607537, atol = TEST_TOLERANCE) + @test isapprox(řešení.total_proteome_bound, 0.5, atol = TEST_TOLERANCE) end From df6f3951f1927c41b35aaecf6177543889445418 Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 14 Dec 2023 15:55:32 +0000 Subject: [PATCH 413/531] automatic formatting triggered by @stelmo on PR #805 --- .../examples/05-enzyme-constrained-models.jl | 34 +++++++++---------- src/builders/enzymes.jl | 2 +- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 83602f0de..8bb9623e7 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -1,4 +1,4 @@ -# # Enzyme constrained models +# # Enzyme constrained models using COBREXA @@ -20,7 +20,7 @@ model = load_model("e_coli_core.json") # Enzyme constrained models require parameters that are usually not used by # conventional constraint based models. These include reaction specific turnover -# numbers, molar masses of enzymes, and capacity bounds. +# numbers, molar masses of enzymes, and capacity bounds. import AbstractFBCModels as A @@ -42,7 +42,7 @@ for rid in A.reactions(model) for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) # each isozyme gets a unique name - d["isozyme_"*string(i)] = Isozyme( # Isozyme struct is defined by COBREXA + d["isozyme_"*string(i)] = Isozyme( # Isozyme struct is defined by COBREXA gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes kcat_forward = (100 + 50 * rand()) * 3600.0, # forward reaction turnover number units = 1/h kcat_backward = (100 + 50 * rand()) * 3600.0, # reverse reaction turnover number units = 1/h @@ -56,10 +56,10 @@ for rid in A.reactions(model) end #!!! warning "Turnover number units" -# Take care with the units of the turnover numbers. In literature they are -# usually reported in 1/s. However, flux units are typically mmol/gDW/h, -# suggesting that you should rescale the turnover numbers to 1/h if you -# want to use the traditional flux units. +# Take care with the units of the turnover numbers. In literature they are +# usually reported in 1/s. However, flux units are typically mmol/gDW/h, +# suggesting that you should rescale the turnover numbers to 1/h if you +# want to use the traditional flux units. ### Enzyme molar masses @@ -70,20 +70,20 @@ gene_molar_masses = Dict(gid => 20 + 40 * rand() for gid in A.genes(model)) gene_molar_masses = ecoli_core_gene_product_masses #src #!!! warning "Molar mass units" -# Take care with the units of the molar masses. In literature they are -# usually reported in Da or kDa (g/mol). However, as noted above, flux -# units are typically mmol/gDW/h. Since the enzyme kinetic equation is -# `v = k * e`, where `k` is the turnover number, it suggests that the -# enzyme variable will have units of mmol/gDW. The molar masses come -# into play when setting the capacity limitations, e.g. usually a sum -# over all enzymes weighted by their molar masses: `e * mm`. Thus, if -# your capacity limitation has units of g/gDW, then the molar masses -# must have units of g/mmol (= kDa). +# Take care with the units of the molar masses. In literature they are +# usually reported in Da or kDa (g/mol). However, as noted above, flux +# units are typically mmol/gDW/h. Since the enzyme kinetic equation is +# `v = k * e`, where `k` is the turnover number, it suggests that the +# enzyme variable will have units of mmol/gDW. The molar masses come +# into play when setting the capacity limitations, e.g. usually a sum +# over all enzymes weighted by their molar masses: `e * mm`. Thus, if +# your capacity limitation has units of g/gDW, then the molar masses +# must have units of g/mmol (= kDa). ### Capacity limitation # The capacity limitation usually denotes an upper bound of protein available to -# the cell. +# the cell. total_enzyme_capacity = 0.1 # g enzyme/gDW diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index c238e9533..c82f7c8a1 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -153,7 +153,7 @@ Return an enzyme constrained model, taking as input a standard constraint-based mapping of reaction IDs (those used in the fluxes of the model) to named [`Isozyme`](@ref)s. Additionally, `gene_molar_masses` and `capacity_limitations` should be supplied. The latter is a vector of tuples, where each tuple -represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. +represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. """ function build_enzyme_constrained_model( model::A.AbstractFBCModel, From 7116bc8071dfd60a5739444316004616a14618f5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 14 Dec 2023 18:32:16 +0100 Subject: [PATCH 414/531] make user friendly function and split out build steps --- .../examples/05-enzyme-constrained-models.jl | 41 +++++++-- src/builders/enzymes.jl | 87 ++++++++++++++----- 2 files changed, 97 insertions(+), 31 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 8bb9623e7..64287d165 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -89,25 +89,48 @@ total_enzyme_capacity = 0.1 # g enzyme/gDW ### Running a basic enzyme constrained model -m = build_enzyme_constrained_model( +ec_solution = enzyme_constrained_flux_balance_analysis( model, reaction_isozymes, gene_molar_masses, - [("total_proteome_bound", A.genes(model), total_enzyme_capacity)], + [("total_proteome_bound", A.genes(model), total_enzyme_capacity)]; + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], + unconstrain_reactions = ["EX_glc__D_e"], + optimizer = Tulip.Optimizer, ) +#src these values should be unique (glucose transporter is the only way to get carbon into the system) +@test isapprox(ec_solution.objective, 3.2105477675077743, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.fluxes.EX_glc__D_e, -41.996885051738445, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src + +### Building a model incrementally + +# create basic flux model +m = fbc_model_constraints(model) + +# create enzyme variables +m += :enzymes^enzyme_variables(model) + +# constrain some fluxes and enzymes manually m.fluxes.EX_glc__D_e.bound = (-1000.0, 0.0) # undo glucose important bound from original model m.enzymes.b2417.bound = (0.0, 0.1) # for fun, change the bounds of the protein b2417 -solution = optimized_constraints( +# attach the enzyme mass balances +m = add_enzyme_constraints!( + m, + reaction_isozymes, + gene_molar_masses, + [("total_proteome_bound", A.genes(model), total_enzyme_capacity)]; + fluxes = m.fluxes, # mount enzyme constraints to these fluxes + enzymes = m.enzymes, # enzyme variables +) + +# solve the model +ec_solution = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], ) - -#src these values should be unique (glucose transporter is the only way to get carbon into the system) -@test isapprox(solution.objective, 3.2105477675077743, atol = TEST_TOLERANCE) #src -@test isapprox(solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src -@test isapprox(solution.fluxes.EX_glc__D_e, -41.996885051738445, atol = TEST_TOLERANCE) #src -@test isapprox(solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index c82f7c8a1..c64a28067 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -148,37 +148,34 @@ export enzyme_capacity """ $(TYPEDSIGNATURES) -Return an enzyme constrained model, taking as input a standard constraint-based -`model`. The enzyme model is parameterized by `reaction_isozymes`, which is a -mapping of reaction IDs (those used in the fluxes of the model) to named -[`Isozyme`](@ref)s. Additionally, `gene_molar_masses` and `capacity_limitations` -should be supplied. The latter is a vector of tuples, where each tuple -represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. -""" -function build_enzyme_constrained_model( - model::A.AbstractFBCModel, +Add enzyme constraints to a constraint tree, `m`. The enzyme model is +parameterized by `reaction_isozymes`, which is a mapping of reaction IDs (those +used in the fluxes of the model) to named [`Isozyme`](@ref)s. Additionally, +`gene_molar_masses` and `capacity_limitations` should be supplied. The latter is +a vector of tuples, where each tuple represents a distinct bound as `(bound_id, +genes_in_bound, protein_mass_bound)`. Finally, specify the `fluxes` and +`enzymes` to which the constraints should be mounted. +""" +function add_enzyme_constraints!( + m::C.ConstraintTree, reaction_isozymes::Dict{String,Dict{String,Isozyme}}, gene_molar_masses::Dict{String,Float64}, - capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}, + capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; + fluxes = m.fluxes, + enzymes = m.enzymes, ) - # create base constraint tree - m = fbc_model_constraints(model) - - # create enzyme variables - m += :enzymes^enzyme_variables(model) - # create directional fluxes m += - :fluxes_forward^fluxes_in_direction(m.fluxes, :forward) + - :fluxes_backward^fluxes_in_direction(m.fluxes, :backward) + :fluxes_forward^fluxes_in_direction(fluxes, :forward) + + :fluxes_backward^fluxes_in_direction(fluxes, :backward) # link directional fluxes to original fluxes m *= :link_flux_directions^sign_split_constraints( positive = m.fluxes_forward, negative = m.fluxes_backward, - signed = m.fluxes, + signed = fluxes, ) # create fluxes for each isozyme @@ -216,7 +213,7 @@ function build_enzyme_constrained_model( # add enzyme mass balances m *= :enzyme_stoichiometry^enzyme_stoichiometry( - m.enzymes, + enzymes, m.fluxes_isozymes_forward, m.fluxes_isozymes_backward, reaction_isozymes, @@ -224,10 +221,56 @@ function build_enzyme_constrained_model( # add capacity limitations for (id, gids, cap) in capacity_limitations - m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) + m *= Symbol(id)^enzyme_capacity(enzymes, gene_molar_masses, gids, cap) end m end -export build_enzyme_constrained_model +export add_enzyme_constraints! + +""" +$(TYPEDSIGNATURES) + +Run a basic enzyme constrained flux balance analysis on `model`. The enzyme +model is parameterized by `reaction_isozymes`, which is a mapping of reaction +IDs (those used in the fluxes of the model) to named [`Isozyme`](@ref)s. +Additionally, `gene_molar_masses` and `capacity_limitations` should be supplied. +The latter is a vector of tuples, where each tuple represents a distinct bound +as `(bound_id, genes_in_bound, protein_mass_bound)`. Typically, `model` has +bounded exchange reactions, which are unnecessary in enzyme constrained models. +Unbound these reactions by listing their IDs in `unconstrain_reactions`, which +makes them reversible. Optimization `modifications` are directly forwarded. + +In the event that your model requires more complex build steps, consider +constructing it manually by using [`add_enzyme_constraints!`](@ref). +""" +function enzyme_constrained_flux_balance_analysis( + model::A.AbstractFBCModel, + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + gene_molar_masses::Dict{String,Float64}, + capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; + optimizer, + unconstrain_reactions = String[], + modifications = [], +) + m = fbc_model_constraints(model) + + # create enzyme variables + m += :enzymes^enzyme_variables(model) + + m = add_enzyme_constraints!( + m, + reaction_isozymes, + gene_molar_masses, + capacity_limitations, + ) + + for rid in Symbol.(unconstrain_reactions) + m.fluxes[rid].bound = (-1000.0, 1000.0) + end + + optimized_constraints(m; objective = m.objective.value, optimizer, modifications) +end + +export enzyme_constrained_flux_balance_analysis From 42def779681f4a6b251c7e37fdcf29460cdfa4b9 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 15 Dec 2023 10:59:41 +0100 Subject: [PATCH 415/531] fix small model tests --- test/enzyme-model.jl | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/test/enzyme-model.jl b/test/enzyme-model.jl index 151b818e0..dcbac32af 100644 --- a/test/enzyme-model.jl +++ b/test/enzyme-model.jl @@ -62,16 +62,11 @@ gene_product_molar_mass = Dict("g1" => 1.0, "g2" => 2.0, "g3" => 3.0, "g4" => 4.0, "g5" => 1.0) - m = build_enzyme_constrained_model( + řešení = enzyme_constrained_flux_balance_analysis( model, reaction_isozymes, gene_product_molar_mass, - [("total_proteome_bound", A.genes(model), 0.5)], - ) - - řešení = optimized_constraints( - m; - objective = m.objective.value, + [("total_proteome_bound", A.genes(model), 0.5)]; optimizer = Tulip.Optimizer, modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) From 5cb473b7271dfa7adfa6112d1f22f2d5fd0dfd5c Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 14 Dec 2023 17:59:32 +0100 Subject: [PATCH 416/531] start mmdf --- docs/src/examples/06-thermodynamic-models.jl | 36 +++++ src/COBREXA.jl | 1 + src/builders/thermodynamic.jl | 144 +++++++++++++++++++ 3 files changed, 181 insertions(+) create mode 100644 docs/src/examples/06-thermodynamic-models.jl create mode 100644 src/builders/thermodynamic.jl diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl new file mode 100644 index 000000000..bdaf319c0 --- /dev/null +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -0,0 +1,36 @@ +# # Thermodynamic models + +using COBREXA + +# Here we will solve the max min driving force analysis problem using the *E. +# coli* "core" model. In essence, the method attempts to find metabolite +# concentrations (NB: not fluxes) that maximize the smallest thermodynamic +# driving force through each reaction. The optimization problem solved is: +# ``` +# max min -ΔᵣG +# s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) +# ΔᵣG ≤ 0 +# ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) +# ``` +# where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas +# constant, T is the temperature, S is the reaction stoichiometry of the model, +# and C is the vector of metabolite concentrations (and their respective lower +# and upper bounds). See Noor, et al., "Pathway thermodynamics highlights +#kinetic obstacles in central metabolism.", PLoS computational biology, 2014. + +# To do this, we will need the model, which we can download if it is not already +# present. + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA, and the model format package, we will need a solver +# -- let's use Tulip here: + +import JSONFBCModels +import Tulip + +model = load_model("e_coli_core.json") + diff --git a/src/COBREXA.jl b/src/COBREXA.jl index ec2fcf564..bbe08fbf6 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -34,6 +34,7 @@ include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") include("builders/enzymes.jl") +include("builders/thermodynamic.jl") include("analysis/modifications.jl") include("analysis/flux_balance.jl") diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl new file mode 100644 index 000000000..5fd283b0b --- /dev/null +++ b/src/builders/thermodynamic.jl @@ -0,0 +1,144 @@ +""" +The function uses the supplied `optimizer` and `reaction_standard_gibbs_free_energies`. +Optionally, `flux_solution` can be used to set the directions of each reaction in `model` +(all reactions are assumed to proceed forward and are active by default). The supplied +`flux_solution` should be free of internal cycles i.e. thermodynamically consistent. This +optional input is important if a reaction in `model` normally runs in reverse (negative +flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` are also +ignored in the analysis function (for numerical stability). + + +Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the +max-min driving force. This should include water and proton importers. + +Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` need to be +specified so that they can be ignored in the calculations. Effectively this assumes an +aqueous environment at constant pH is used. + +`constant_concentrations` is used to fix the concentrations of certain metabolites (such as +CO₂). `concentration_ratios` is used to specify additional constraints on metabolite pair +concentrations (typically, this is done with various cofactors such as the ATP/ADP ratio. +For example, you can fix the concentration of ATP to be always 5× higher than of ADP by +specifying `Dict(("ATP","ADP") => 5.0)` + +`concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in the +optimization problems. + +`T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. +""" +function max_min_driving_force( + model::MetabolicModel, + reaction_standard_gibbs_free_energies::Dict{String,Float64}, + optimizer; + flux_solution::Dict{String,Float64} = Dict{String,Float64}(), + proton_ids::Vector{String} = ["h_c", "h_e"], + water_ids::Vector{String} = ["h2o_c", "h2o_e"], + constant_concentrations::Dict{String,Float64} = Dict{String,Float64}(), + concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ + Tuple{String,String}, + Float64, + }(), + concentration_lb = 1e-9, + concentration_ub = 100e-3, + T = _constants.T, + R = _constants.R, + small_flux_tol = 1e-6, + modifications = [], + ignore_reaction_ids = [], +) + opt_model = Model(optimizer) + + @variables opt_model begin + mmdf + logcs[1:n_metabolites(model)] + dgrs[1:n_reactions(model)] + end + + # set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption) + proton_idxs = Int.(indexin(proton_ids, metabolites(model))) + for idx in proton_idxs + JuMP.fix(logcs[idx], 0.0) + end + + # set water concentrations to zero (aqueous condition assumptions) + water_idxs = Int.(indexin(water_ids, metabolites(model))) + for idx in water_idxs + JuMP.fix(logcs[idx], 0.0) + end + + # only consider reactions with supplied thermodynamic data AND have a flux bigger than + # small_flux_tol => finds a thermodynamic profile that explains flux_solution + active_rids = filter( + rid -> + haskey(reaction_standard_gibbs_free_energies, rid) && + abs(get(flux_solution, rid, small_flux_tol / 2)) > small_flux_tol && + !(rid in ignore_reaction_ids), + reactions(model), + ) + active_ridxs = Int.(indexin(active_rids, reactions(model))) + + # give dummy dG0 for reactions that don't have data + dg0s = + [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] + + S = stoichiometry(model) + + @constraint(opt_model, dgrs .== dg0s .+ (R * T) * S' * logcs) + + # thermodynamics should correspond to the fluxes + flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in reactions(model)] + + # only constrain reactions that have thermo data + @constraint(opt_model, dgrs[active_ridxs] .* flux_signs[active_ridxs] .<= 0) + + # add the absolute bounds + missing_mets = + [mid for mid in keys(constant_concentrations) if !(mid in metabolites(model))] + !isempty(missing_mets) && + throw(DomainError(missing_mets, "metabolite(s) not found in model.")) + for (midx, mid) in enumerate(metabolites(model)) # idx in opt_model (missing ignore_metabolites) + midx in water_idxs && continue + midx in proton_idxs && continue + if haskey(constant_concentrations, mid) + JuMP.fix(logcs[midx], log(constant_concentrations[mid])) + else + # this metabolite needs bounds + @constraint( + opt_model, + log(concentration_lb) <= logcs[midx] <= log(concentration_ub) + ) + end + end + + # add the relative bounds + for ((mid1, mid2), val) in concentration_ratios + idxs = indexin([mid1, mid2], metabolites(model)) # TODO: this is not performant + any(isnothing.(idxs)) && + throw(DomainError((mid1, mid2), "metabolite pair not found in model.")) + @constraint(opt_model, logcs[idxs[1]] == log(val) + logcs[idxs[2]]) + end + + @constraint(opt_model, mmdf .<= -dgrs[active_ridxs] .* flux_signs[active_ridxs]) + + @objective(opt_model, Max, mmdf) + + # apply the modifications, if any + for mod in modifications + mod(model, opt_model) + end + + optimize!(opt_model) + + is_solved(opt_model) || return nothing + + return ( + mmdf = value(opt_model[:mmdf]), + dg_reactions = Dict( + rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(reactions(model)) + ), + concentrations = Dict( + mid => exp(value(opt_model[:logcs][i])) for + (i, mid) in enumerate(metabolites(model)) + ), + ) +end \ No newline at end of file From 941c9cde753d38ba8e4bcc44f43079381e67621a Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 16 Dec 2023 16:07:48 +0100 Subject: [PATCH 417/531] working MMDF --- docs/src/examples/06-thermodynamic-models.jl | 33 ++ src/COBREXA.jl | 2 +- src/builders/thermodynamic.jl | 334 +++++++++++++------ 3 files changed, 258 insertions(+), 111 deletions(-) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index bdaf319c0..649f523ac 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -34,3 +34,36 @@ import Tulip model = load_model("e_coli_core.json") +flux_solution = Dict( + "GLCpts" => 1.0, + "PGI" => 1.0, + "PFK" => 1.0, + "FBA" => 1.0, + "TPI" => 1.0, + "GAPD" => 1.0, + "PGK" => -1.0, + "PGM" => -1.0, + "ENO" => 1.0, + "PYK" => 1.0, + "LDH_D" => -1.0, +) + +m = build_max_min_driving_force_model( + model, + reaction_standard_gibbs_free_energies; + flux_solution, + concentration_lb = 1e-6, +) + +m = add_metabolite_ratio_constraints!( + m, + Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), +) + +# solve the model +maxmin_solution = optimized_constraints( + m; + objective = m.max_min_driving_force.value, + optimizer = Tulip.Optimizer, + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], +) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index bbe08fbf6..0b853fe62 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -24,7 +24,7 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J -import SparseArrays: sparse +import SparseArrays: sparse, findnz include("types.jl") include("io.jl") diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 5fd283b0b..fbbf4ed0f 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -1,4 +1,180 @@ """ +$(TYPEDSIGNATURES) + +The function uses the supplied `optimizer` and +`reaction_standard_gibbs_free_energies`. Optionally, `flux_solution` can be used +to set the directions of each reaction in `model` (all reactions are assumed to +proceed forward and are active by default). The supplied `flux_solution` should +be free of internal cycles i.e. thermodynamically consistent. This optional +input is important if a reaction in `model` normally runs in reverse (negative +flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` +are also ignored in the analysis function (for numerical stability). + + +Reactions specified in `ignore_reaction_ids` are internally ignored when +calculating the max-min driving force. This should include water and proton +importers. + +Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` +need to be specified so that they can be ignored in the calculations. +Effectively this assumes an aqueous environment at constant pH is used. + +`constant_concentrations` is used to fix the concentrations of certain +metabolites (such as CO₂). `concentration_ratios` is used to specify additional +constraints on metabolite pair concentrations (typically, this is done with +various cofactors such as the ATP/ADP ratio. For example, you can fix the +concentration of ATP to be always 5× higher than of ADP by specifying +`Dict(("ATP","ADP") => 5.0)` + +`concentration_lb` and `concentration_ub` set default concentration bounds, in M +by default. + +`T` and `R` can be specified in the corresponding units; defaults are K and +kJ/K/mol. +""" +function build_max_min_driving_force_model( + model::A.AbstractFBCModel, + reaction_standard_gibbs_free_energies::Dict{String,Float64}; + flux_solution = Dict{String,Float64}(), + proton_ids = ["h_c", "h_e"], + water_ids = ["h2o_c", "h2o_e"], + concentration_lb = 1e-9, + concentration_ub = 100e-3, + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + ignore_reaction_ids = String[], +) + # check if reactions that will be used in the model all have thermodynamic data, otherwise throw error. + # only these reactions will form part of the model. + rxns = filter( + x -> !(x in ignore_reaction_ids), + isempty(flux_solution) ? A.reactions(model) : collect(keys(flux_solution)), + ) + all(in.(rxns, Ref(collect(keys(reaction_standard_gibbs_free_energies))))) || throw( + ArgumentError(""" + Not all reactions have thermodynamic data. + Either add the reactions with missing ΔG0s to ignore_reaction_ids, + or add the data to reaction_standard_gibbs_free_energies. + """), + ) + + # import metabolite ids (if used), and reaction stoichiometries from an AbstractFBCModel + mets = Set(Symbol[]) + for rid in rxns + for met in keys(A.reaction_stoichiometry(model, rid)) + push!(mets, Symbol(met)) + end + end + mets = [m for m in mets] # TODO: constrainttrees #12 + stoi = A.stoichiometry(model) + + # create thermodynamic variables + m = C.ConstraintTree(:max_min_driving_force^C.variable()) + m += + :log_metabolite_concentrations^C.variables( + keys = mets, + bounds = zip(log(concentration_lb), log(concentration_ub)), + ) + m += :delta_G_reactions^C.variables(keys = Symbol.(rxns)) + + #= + Build gibbs free energy relations + ΔG_rxn == ΔG0 + R * T * sum ( log_concentration_variable * stoichiometry_value ) + =# + model_rxns = A.reactions(model) + model_mets = A.metabolites(model) + for rxn in rxns + met_idxs, stoich_coeffs = findnz(stoi[:, findfirst(==(rxn), model_rxns)]) + met_ids = model_mets[met_idxs] + + dG0 = reaction_standard_gibbs_free_energies[rxn] + + m *= + :delta_G_reaction_equations^Symbol( + rxn, + )^C.Constraint( + value = -m.delta_G_reactions[Symbol(rxn)].value + + dG0 + + R * + T * + sum( + m.log_metabolite_concentrations[Symbol(met_id)].value * + stoich for (met_id, stoich) in zip(met_ids, stoich_coeffs) + ), + bound = 0.0, + ) + end + + #= + Set proton log concentration to zero so that it won't impact any + calculations (biothermodynamics assumption). Also set water concentrations + to zero (aqueous condition assumptions). How true is "aqueous conditions"? + Debatable... + =# + log_met_conc_zero = [Symbol.(proton_ids); Symbol.(water_ids)] + for met in keys(m.log_metabolite_concentrations) + if met in log_met_conc_zero + m.log_metabolite_concentrations[met].bound = 0.0 + end + end + + #= + Add thermodynamic feasibility constraint (ΔG < 0 for a feasible reaction in flux direction). + Add objective constraint to solve max min problem. + =# + for rxn in rxns + m *= + :thermodynamic_feasibility^Symbol( + rxn, + )^C.Constraint( + value = m.delta_G_reactions[Symbol(rxn)].value * + sign(get(flux_solution, rxn, 1.0)), + bound = (-Inf, 0.0), + ) + + m *= + :max_min_constraints^Symbol( + rxn, + )^C.Constraint( + value = m.max_min_driving_force.value + + m.delta_G_reactions[Symbol(rxn)].value * + sign(get(flux_solution, rxn, 1.0)), + bound = (-Inf, 0.0), + ) + end + + m +end + +export build_max_min_driving_force_model + +function add_metabolite_ratio_constraints!( + m::C.ConstraintTree, + concentration_ratios::Dict{String,Tuple{String,String,Float64}}, +) + for (constraint_id, (met1, met2, ratio)) in concentration_ratios + m *= + :metabolite_ratio_constraints^Symbol( + constraint_id, + )^C.Constraint( + value = m.log_metabolite_concentrations[Symbol(met1)].value - + m.log_metabolite_concentrations[Symbol(met2)].value, + bound = log(ratio), + ) + end + + m +end + +export add_metabolite_ratio_constraints! + +""" +$(TYPEDSIGNATURES) + +Perform a max-min driving force analysis on the `model`, as defined by Noor, et al., +"Pathway thermodynamics highlights kinetic obstacles in central metabolism.", PLoS +computational biology, 2014. + The function uses the supplied `optimizer` and `reaction_standard_gibbs_free_energies`. Optionally, `flux_solution` can be used to set the directions of each reaction in `model` (all reactions are assumed to proceed forward and are active by default). The supplied @@ -7,6 +183,20 @@ optional input is important if a reaction in `model` normally runs in reverse (n flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` are also ignored in the analysis function (for numerical stability). +The max-min driving force algorithm returns the Gibbs free energy of the reactions, the +concentrations of metabolites and the actual maximum minimum driving force. The optimization +problem solved is: +``` +max min -ΔᵣG +s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) + ΔᵣG ≤ 0 + ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) +``` +where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas constant, T is +the temperature, S is the stoichiometry of the model, and C is the vector of metabolite +concentrations (and their respective lower and upper bounds). + +In case no feasible solution exists, `nothing` is returned. Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the max-min driving force. This should include water and proton importers. @@ -25,120 +215,44 @@ specifying `Dict(("ATP","ADP") => 5.0)` optimization problems. `T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. -""" -function max_min_driving_force( - model::MetabolicModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}, - optimizer; - flux_solution::Dict{String,Float64} = Dict{String,Float64}(), - proton_ids::Vector{String} = ["h_c", "h_e"], - water_ids::Vector{String} = ["h2o_c", "h2o_e"], - constant_concentrations::Dict{String,Float64} = Dict{String,Float64}(), - concentration_ratios::Dict{Tuple{String,String},Float64} = Dict{ - Tuple{String,String}, - Float64, - }(), +""" +function max_min_driving_force_analysis( + model::A.AbstractFBCModel, + reaction_standard_gibbs_free_energies::Dict{String,Float64}; + flux_solution = Dict{String,Float64}(), + concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), + proton_ids = ["h_c", "h_e"], + water_ids = ["h2o_c", "h2o_e"], concentration_lb = 1e-9, concentration_ub = 100e-3, - T = _constants.T, - R = _constants.R, - small_flux_tol = 1e-6, + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + ignore_reaction_ids = String[], modifications = [], - ignore_reaction_ids = [], + optimizer, ) - opt_model = Model(optimizer) - - @variables opt_model begin - mmdf - logcs[1:n_metabolites(model)] - dgrs[1:n_reactions(model)] - end - - # set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption) - proton_idxs = Int.(indexin(proton_ids, metabolites(model))) - for idx in proton_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # set water concentrations to zero (aqueous condition assumptions) - water_idxs = Int.(indexin(water_ids, metabolites(model))) - for idx in water_idxs - JuMP.fix(logcs[idx], 0.0) - end - - # only consider reactions with supplied thermodynamic data AND have a flux bigger than - # small_flux_tol => finds a thermodynamic profile that explains flux_solution - active_rids = filter( - rid -> - haskey(reaction_standard_gibbs_free_energies, rid) && - abs(get(flux_solution, rid, small_flux_tol / 2)) > small_flux_tol && - !(rid in ignore_reaction_ids), - reactions(model), + m = build_max_min_driving_force_model( + model, + reaction_standard_gibbs_free_energies; + flux_solution, + concentration_lb, + concentration_ub, + R, + T, + ignore_reaction_ids, + water_ids, + proton_ids, ) - active_ridxs = Int.(indexin(active_rids, reactions(model))) - - # give dummy dG0 for reactions that don't have data - dg0s = - [get(reaction_standard_gibbs_free_energies, rid, 0.0) for rid in reactions(model)] - - S = stoichiometry(model) - - @constraint(opt_model, dgrs .== dg0s .+ (R * T) * S' * logcs) - - # thermodynamics should correspond to the fluxes - flux_signs = [sign(get(flux_solution, rid, 1.0)) for rid in reactions(model)] - - # only constrain reactions that have thermo data - @constraint(opt_model, dgrs[active_ridxs] .* flux_signs[active_ridxs] .<= 0) - - # add the absolute bounds - missing_mets = - [mid for mid in keys(constant_concentrations) if !(mid in metabolites(model))] - !isempty(missing_mets) && - throw(DomainError(missing_mets, "metabolite(s) not found in model.")) - for (midx, mid) in enumerate(metabolites(model)) # idx in opt_model (missing ignore_metabolites) - midx in water_idxs && continue - midx in proton_idxs && continue - if haskey(constant_concentrations, mid) - JuMP.fix(logcs[midx], log(constant_concentrations[mid])) - else - # this metabolite needs bounds - @constraint( - opt_model, - log(concentration_lb) <= logcs[midx] <= log(concentration_ub) - ) - end - end - - # add the relative bounds - for ((mid1, mid2), val) in concentration_ratios - idxs = indexin([mid1, mid2], metabolites(model)) # TODO: this is not performant - any(isnothing.(idxs)) && - throw(DomainError((mid1, mid2), "metabolite pair not found in model.")) - @constraint(opt_model, logcs[idxs[1]] == log(val) + logcs[idxs[2]]) - end - - @constraint(opt_model, mmdf .<= -dgrs[active_ridxs] .* flux_signs[active_ridxs]) - - @objective(opt_model, Max, mmdf) - - # apply the modifications, if any - for mod in modifications - mod(model, opt_model) - end - - optimize!(opt_model) - - is_solved(opt_model) || return nothing - - return ( - mmdf = value(opt_model[:mmdf]), - dg_reactions = Dict( - rid => value(opt_model[:dgrs][i]) for (i, rid) in enumerate(reactions(model)) - ), - concentrations = Dict( - mid => exp(value(opt_model[:logcs][i])) for - (i, mid) in enumerate(metabolites(model)) - ), + + m = add_metabolite_ratio_constraints!( + m, + concentration_ratios, + ) + + optimized_constraints( + m; + objective = m.max_min_driving_force.value, + optimizer, + modifications, ) end \ No newline at end of file From 255ac7cfc5adf2c3751c636704eafbafe04fd5a7 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sat, 16 Dec 2023 19:48:45 +0100 Subject: [PATCH 418/531] implement reviews and improve docs --- docs/src/examples/06-thermodynamic-models.jl | 106 ++++++-- src/builders/thermodynamic.jl | 270 ++++++++++--------- 2 files changed, 219 insertions(+), 157 deletions(-) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 649f523ac..0e16361fe 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -2,24 +2,15 @@ using COBREXA -# Here we will solve the max min driving force analysis problem using the *E. -# coli* "core" model. In essence, the method attempts to find metabolite -# concentrations (NB: not fluxes) that maximize the smallest thermodynamic -# driving force through each reaction. The optimization problem solved is: -# ``` -# max min -ΔᵣG -# s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) -# ΔᵣG ≤ 0 -# ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -# ``` -# where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas -# constant, T is the temperature, S is the reaction stoichiometry of the model, -# and C is the vector of metabolite concentrations (and their respective lower -# and upper bounds). See Noor, et al., "Pathway thermodynamics highlights -#kinetic obstacles in central metabolism.", PLoS computational biology, 2014. - -# To do this, we will need the model, which we can download if it is not already -# present. +# Here we will solve the max min driving force analysis problem using the +# glycolysis pathway of *E. coli*. In essence, the method attempts to find +# metabolite concentrations (NB: not fluxes) that maximize the smallest +# thermodynamic driving force through each reaction. See Noor, et al., "Pathway +#thermodynamics highlights kinetic obstacles in central metabolism.", PLoS +#computational biology, 2014, for more details. + +# To do this, we will first need a model that includes glycolysis, which we can +# download if it is not already present. import Downloads: download @@ -34,7 +25,43 @@ import Tulip model = load_model("e_coli_core.json") -flux_solution = Dict( +# ## Thermodynamic data + +# We will need ΔᵣG⁰ data for each reaction we want to include in the +# thermodynamic model. To generate this data manually, go to +# https://equilibrator.weizmann.ac.il/. To generate automatically, use the +# eQuilibrator.jl package. + +reaction_standard_gibbs_free_energies = Dict{String,Float64}( + "GLCpts" => -45.42430981510088, + "PGI" => 2.6307087407442395, + "PFK" => -18.546314942995934, + "FBA" => 23.376920310319235, + "TPI" => 5.621932460512994, + "GAPD" => 0.5307809794271634, + "PGK" => 19.57192102020454, + "PGM" => -4.470553692565886, + "ENO" => -3.8108376097261782, + "PYK" => -24.48733600711958, + "LDH_D" => 20.04059765689044, +) + +# ## Running basic max min driving force analysis + +# If a reference flux is not specified, it is assumed that every reaction in the +# model should be included in the thermodynamic model, and that each reaction +# proceeds in the forward direction. This is usually not intended, and can be +# prevented by inputting a reference flux dictionary as shown below. This +# dictionary can be a flux solution, the sign of each flux is used to determine +# if the reaction runs forward or backward. + +#!!! warning "Only the signs are extracted from the reference solution" +# It is most convenient to pass a flux solution into `reference_flux`, but +# take care to round fluxes near 0 to their correct sign if they should be +# included in the resultant thermodynamic model. Otherwise, remove them from +# reference flux input. + +reference_flux = Dict( "GLCpts" => 1.0, "PGI" => 1.0, "PFK" => 1.0, @@ -48,22 +75,51 @@ flux_solution = Dict( "LDH_D" => -1.0, ) +mmdf_solution = max_min_driving_force_analysis( + model, + reaction_standard_gibbs_free_energies; + reference_flux, + concentration_ratios = Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), + proton_ids = ["h_c", "h_e"], + water_ids = ["h2o_c", "h2o_e"], + concentration_lb = 1e-6, # M + concentration_ub = 1e-1, # M + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], + optimizer = Tulip.Optimizer, +) + +@test isapprox(mmdf_solution.max_min_driving_force, 5.78353, atol = TEST_TOLERANCE) #src + +# ## Building the thermodynamic model yourself + +# It is also possible to build the thermodynamic model yourself. This allows you +# to incorporate more complex constraints and gives you more freedom. + m = build_max_min_driving_force_model( model, reaction_standard_gibbs_free_energies; - flux_solution, - concentration_lb = 1e-6, + proton_ids = ["h_c", "h_e"], + water_ids = ["h2o_c", "h2o_e"], + reference_flux, + concentration_lb = 1e-6, # M + concentration_ub = 1e-1, # M + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol ) -m = add_metabolite_ratio_constraints!( +m = add_log_ratio_constraints( m, - Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), + Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)); + name = :metabolite_ratio_constraints, + on = m.log_metabolite_concentrations, ) # solve the model -maxmin_solution = optimized_constraints( +mmdf_solution = optimized_constraints( m; objective = m.max_min_driving_force.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], ) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index fbbf4ed0f..72af7b6ce 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -1,81 +1,52 @@ """ $(TYPEDSIGNATURES) -The function uses the supplied `optimizer` and -`reaction_standard_gibbs_free_energies`. Optionally, `flux_solution` can be used -to set the directions of each reaction in `model` (all reactions are assumed to -proceed forward and are active by default). The supplied `flux_solution` should -be free of internal cycles i.e. thermodynamically consistent. This optional -input is important if a reaction in `model` normally runs in reverse (negative -flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` -are also ignored in the analysis function (for numerical stability). - - -Reactions specified in `ignore_reaction_ids` are internally ignored when -calculating the max-min driving force. This should include water and proton -importers. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` -need to be specified so that they can be ignored in the calculations. -Effectively this assumes an aqueous environment at constant pH is used. - -`constant_concentrations` is used to fix the concentrations of certain -metabolites (such as CO₂). `concentration_ratios` is used to specify additional -constraints on metabolite pair concentrations (typically, this is done with -various cofactors such as the ATP/ADP ratio. For example, you can fix the -concentration of ATP to be always 5× higher than of ADP by specifying -`Dict(("ATP","ADP") => 5.0)` - -`concentration_lb` and `concentration_ub` set default concentration bounds, in M -by default. - -`T` and `R` can be specified in the corresponding units; defaults are K and -kJ/K/mol. +Build a max min driving force analysis model. See the docstring of +[`max_min_driving_force_analysis`](@ref) for details about the arguments. """ function build_max_min_driving_force_model( model::A.AbstractFBCModel, reaction_standard_gibbs_free_energies::Dict{String,Float64}; - flux_solution = Dict{String,Float64}(), - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_lb = 1e-9, - concentration_ub = 100e-3, + reference_flux = Dict{String,Float64}(), + proton_ids::Vector{String}, + water_ids::Vector{String}, + concentration_lb = 1e-9, # M + concentration_ub = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol ignore_reaction_ids = String[], ) # check if reactions that will be used in the model all have thermodynamic data, otherwise throw error. # only these reactions will form part of the model. - rxns = filter( - x -> !(x in ignore_reaction_ids), - isempty(flux_solution) ? A.reactions(model) : collect(keys(flux_solution)), - ) - all(in.(rxns, Ref(collect(keys(reaction_standard_gibbs_free_energies))))) || throw( - ArgumentError(""" + rxn_source = + isempty(reference_flux) ? A.reactions(model) : collect(keys(reference_flux)) + rxns = Set(x for x in rxn_source if !(x in ignore_reaction_ids)) + isempty(setdiff(rxns, keys(reaction_standard_gibbs_free_energies))) || throw( + DomainError( + reaction_standard_gibbs_free_energies, + """ Not all reactions have thermodynamic data. Either add the reactions with missing ΔG0s to ignore_reaction_ids, or add the data to reaction_standard_gibbs_free_energies. - """), + """, + ), ) + rxns = collect(rxns) # import metabolite ids (if used), and reaction stoichiometries from an AbstractFBCModel - mets = Set(Symbol[]) - for rid in rxns - for met in keys(A.reaction_stoichiometry(model, rid)) - push!(mets, Symbol(met)) - end - end - mets = [m for m in mets] # TODO: constrainttrees #12 + mets = Set(m for rid in rxns for m in keys(A.reaction_stoichiometry(model, rid))) + mets = collect(mets) stoi = A.stoichiometry(model) # create thermodynamic variables - m = C.ConstraintTree(:max_min_driving_force^C.variable()) - m += + m = C.ConstraintTree( + :max_min_driving_force^C.variable() + :log_metabolite_concentrations^C.variables( - keys = mets, + keys = Symbol.(mets), bounds = zip(log(concentration_lb), log(concentration_ub)), - ) - m += :delta_G_reactions^C.variables(keys = Symbol.(rxns)) + ) + + :delta_G_reactions^C.variables(keys = Symbol.(rxns)), + ) #= Build gibbs free energy relations @@ -83,27 +54,27 @@ function build_max_min_driving_force_model( =# model_rxns = A.reactions(model) model_mets = A.metabolites(model) - for rxn in rxns + dG0s_met_ids_stoichs = Vector{Tuple{Float64,Vector{String},Vector{Float64}}}() + for rxn in rxns # prepare to create CT below met_idxs, stoich_coeffs = findnz(stoi[:, findfirst(==(rxn), model_rxns)]) met_ids = model_mets[met_idxs] - dG0 = reaction_standard_gibbs_free_energies[rxn] + push!(dG0s_met_ids_stoichs, (dG0, met_ids, stoich_coeffs)) + end - m *= - :delta_G_reaction_equations^Symbol( - rxn, - )^C.Constraint( + m *= + :delta_G_reaction_equations^C.ConstraintTree( + Symbol(rxn) => C.Constraint( value = -m.delta_G_reactions[Symbol(rxn)].value + dG0 + R * T * sum( - m.log_metabolite_concentrations[Symbol(met_id)].value * - stoich for (met_id, stoich) in zip(met_ids, stoich_coeffs) + m.log_metabolite_concentrations[Symbol(met_id)].value * stoich for (met_id, stoich) in zip(met_ids, stoich_coeffs) ), bound = 0.0, - ) - end + ) for (rxn, (dG0, met_ids, stoich_coeffs)) in zip(rxns, dG0s_met_ids_stoichs) + ) #= Set proton log concentration to zero so that it won't impact any @@ -111,54 +82,72 @@ function build_max_min_driving_force_model( to zero (aqueous condition assumptions). How true is "aqueous conditions"? Debatable... =# - log_met_conc_zero = [Symbol.(proton_ids); Symbol.(water_ids)] - for met in keys(m.log_metabolite_concentrations) - if met in log_met_conc_zero - m.log_metabolite_concentrations[met].bound = 0.0 - end + for met in [Symbol.(proton_ids); Symbol.(water_ids)] + haskey(m.log_metabolite_concentrations, met) && + (m.log_metabolite_concentrations[met].bound = 0.0) end #= Add thermodynamic feasibility constraint (ΔG < 0 for a feasible reaction in flux direction). Add objective constraint to solve max min problem. =# - for rxn in rxns - m *= - :thermodynamic_feasibility^Symbol( - rxn, - )^C.Constraint( + m *= + :reaction_delta_G_margin^C.ConstraintTree( + Symbol(rxn) => C.Constraint( value = m.delta_G_reactions[Symbol(rxn)].value * - sign(get(flux_solution, rxn, 1.0)), + sign(get(reference_flux, rxn, 1.0)), bound = (-Inf, 0.0), - ) + ) for rxn in rxns + ) - m *= - :max_min_constraints^Symbol( - rxn, - )^C.Constraint( + m *= + :min_driving_force_margin^C.ConstraintTree( + Symbol(rxn) => C.Constraint( value = m.max_min_driving_force.value + m.delta_G_reactions[Symbol(rxn)].value * - sign(get(flux_solution, rxn, 1.0)), + sign(get(reference_flux, rxn, 1.0)), bound = (-Inf, 0.0), - ) - end + ) for rxn in rxns + ) m end export build_max_min_driving_force_model -function add_metabolite_ratio_constraints!( + +""" +$(TYPEDSIGNATURES) + +Add constraints to `m` that represents ratios of variables in log space: +`log(x/y) = log(const)` where `x` and `y` are variables specified by `on`. These +constraints are called `name` in the resultant model. The constraints are +specified by `ratios`, which is a dictionary mapping a constraint id to a tuple +which consists of the variable ids, `x`, `y`, and the ratio value, `const`. The +latter is logged internally. + +# Example +``` +m = add_ratio_constraints( + m, + Dict("atp" => ("atp_c", "adp_c", log(10.0)), "nadh" => ("nadh_c", "nad_c", log(0.13))); + name = :metabolite_ratio_constraints, + on = m.log_metabolite_concentrations, +) +``` +""" +function add_log_ratio_constraints( m::C.ConstraintTree, - concentration_ratios::Dict{String,Tuple{String,String,Float64}}, + ratios::Dict{String,Tuple{String,String,Float64}}; + name::Symbol, + on::C.ConstraintTree, ) - for (constraint_id, (met1, met2, ratio)) in concentration_ratios + for (cid, (var1, var2, ratio)) in ratios m *= - :metabolite_ratio_constraints^Symbol( - constraint_id, + name^Symbol( + cid, )^C.Constraint( - value = m.log_metabolite_concentrations[Symbol(met1)].value - - m.log_metabolite_concentrations[Symbol(met2)].value, + value = on[Symbol(var1)].value - on[Symbol(var2)].value, bound = log(ratio), ) end @@ -166,65 +155,73 @@ function add_metabolite_ratio_constraints!( m end -export add_metabolite_ratio_constraints! +export add_log_ratio_constraints + """ $(TYPEDSIGNATURES) -Perform a max-min driving force analysis on the `model`, as defined by Noor, et al., -"Pathway thermodynamics highlights kinetic obstacles in central metabolism.", PLoS -computational biology, 2014. +Perform a max-min driving force analysis using `optimizer` on the `model` with +supplied ΔG⁰s in `reaction_standard_gibbs_free_energies`, as defined by Noor, et +al., "Pathway thermodynamics highlights kinetic obstacles in central +metabolism.", PLoS computational biology, 2014. -The function uses the supplied `optimizer` and `reaction_standard_gibbs_free_energies`. -Optionally, `flux_solution` can be used to set the directions of each reaction in `model` -(all reactions are assumed to proceed forward and are active by default). The supplied -`flux_solution` should be free of internal cycles i.e. thermodynamically consistent. This -optional input is important if a reaction in `model` normally runs in reverse (negative -flux). Note, reactions in `flux_solution` that are smaller than `small_flux_tol` are also -ignored in the analysis function (for numerical stability). +Optionally, `reference_flux` can be used to set the directions of each reaction +in `model` (all reactions are assumed to proceed forward by default). The +supplied `reference_flux` should be free of internal cycles i.e. +thermodynamically consistent. This optional input is important if a reaction in +`model` normally runs in reverse (negative flux). Note, only the signs are +extracted from this input, so be careful with floating point precision near 0. -The max-min driving force algorithm returns the Gibbs free energy of the reactions, the -concentrations of metabolites and the actual maximum minimum driving force. The optimization -problem solved is: +The max-min driving force algorithm returns the Gibbs free energy of the +reactions, the concentrations of metabolites and the actual maximum minimum +driving force. The optimization problem solved is: ``` max min -ΔᵣG s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) ΔᵣG ≤ 0 ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) ``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas constant, T is -the temperature, S is the stoichiometry of the model, and C is the vector of metabolite -concentrations (and their respective lower and upper bounds). +where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas +constant, T is the temperature, S is the stoichiometry of the model, and C is +the vector of metabolite concentrations (and their respective lower and upper +bounds). In case no feasible solution exists, `nothing` is returned. -Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the -max-min driving force. This should include water and proton importers. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` need to be -specified so that they can be ignored in the calculations. Effectively this assumes an -aqueous environment at constant pH is used. +Reactions specified in `ignore_reaction_ids` are internally ignored when +calculating the max-min driving force. Importantly, this should include water +and proton importers. -`constant_concentrations` is used to fix the concentrations of certain metabolites (such as -CO₂). `concentration_ratios` is used to specify additional constraints on metabolite pair -concentrations (typically, this is done with various cofactors such as the ATP/ADP ratio. -For example, you can fix the concentration of ATP to be always 5× higher than of ADP by -specifying `Dict(("ATP","ADP") => 5.0)` +Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` +need to be specified so that they can be ignored in the calculations. +Effectively this assumes an aqueous environment at constant pH is used. -`concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in the -optimization problems. +`constant_concentrations` is used to fix the concentrations of certain +metabolites (such as CO₂) by passing a dictionary mapping metabolite id to its +constant value. `concentration_ratios` is used to specify additional constraints +on metabolite pair concentrations (typically, this is done with various +cofactors, such as the ATP/ADP ratio. For example, you can fix the concentration +of ATP to be always 5× higher than of ADP by specifying `Dict("atp_ratio" => +("atp_c","adp_c", 5.0))` if these metabolites are called `atp_c` and `adp_c` in +the model. `concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in +the optimization problems (these are overwritten by `constant_concentrations` if +supplied). -`T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. +`T` and `R` can be specified in the corresponding units; defaults are K and +kJ/K/mol. The unit of metabolite concentrations is typically molar. As usual, +optimizer settings can be changed with `modifications`. """ function max_min_driving_force_analysis( model::A.AbstractFBCModel, reaction_standard_gibbs_free_energies::Dict{String,Float64}; - flux_solution = Dict{String,Float64}(), + reference_flux = Dict{String,Float64}(), concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_lb = 1e-9, - concentration_ub = 100e-3, + constant_concentrations = Dict{String,Float64}(), + proton_ids, + water_ids, + concentration_lb = 1e-9, # M + concentration_ub = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol ignore_reaction_ids = String[], @@ -234,7 +231,7 @@ function max_min_driving_force_analysis( m = build_max_min_driving_force_model( model, reaction_standard_gibbs_free_energies; - flux_solution, + reference_flux, concentration_lb, concentration_ub, R, @@ -243,16 +240,25 @@ function max_min_driving_force_analysis( water_ids, proton_ids, ) - - m = add_metabolite_ratio_constraints!( + + for (mid, val) in constant_concentrations + m.log_metabolite_concentrations[Symbol(mid)].bound = log(val) + end + + m = add_log_ratio_constraints( m, - concentration_ratios, + concentration_ratios; + name = :metabolite_ratio_constraints, + on = m.log_metabolite_concentrations, ) - + + optimized_constraints( m; objective = m.max_min_driving_force.value, optimizer, modifications, ) -end \ No newline at end of file +end + +export max_min_driving_force_analysis From dcf93d46da8352d20675e4f6016d6fc5ecb593f9 Mon Sep 17 00:00:00 2001 From: stelmo Date: Sat, 16 Dec 2023 18:50:57 +0000 Subject: [PATCH 419/531] automatic formatting triggered by @stelmo on PR #808 --- docs/src/examples/06-thermodynamic-models.jl | 11 +++++++---- src/builders/thermodynamic.jl | 16 ++++++++-------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 0e16361fe..110df7be9 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -56,9 +56,9 @@ reaction_standard_gibbs_free_energies = Dict{String,Float64}( # if the reaction runs forward or backward. #!!! warning "Only the signs are extracted from the reference solution" -# It is most convenient to pass a flux solution into `reference_flux`, but +# It is most convenient to pass a flux solution into `reference_flux`, but # take care to round fluxes near 0 to their correct sign if they should be -# included in the resultant thermodynamic model. Otherwise, remove them from +# included in the resultant thermodynamic model. Otherwise, remove them from # reference flux input. reference_flux = Dict( @@ -79,7 +79,10 @@ mmdf_solution = max_min_driving_force_analysis( model, reaction_standard_gibbs_free_energies; reference_flux, - concentration_ratios = Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), + concentration_ratios = Dict( + "atp" => ("atp_c", "adp_c", 10.0), + "nadh" => ("nadh_c", "nad_c", 0.13), + ), proton_ids = ["h_c", "h_e"], water_ids = ["h2o_c", "h2o_e"], concentration_lb = 1e-6, # M @@ -95,7 +98,7 @@ mmdf_solution = max_min_driving_force_analysis( # ## Building the thermodynamic model yourself # It is also possible to build the thermodynamic model yourself. This allows you -# to incorporate more complex constraints and gives you more freedom. +# to incorporate more complex constraints and gives you more freedom. m = build_max_min_driving_force_model( model, diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 72af7b6ce..fff88d30c 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -25,8 +25,8 @@ function build_max_min_driving_force_model( DomainError( reaction_standard_gibbs_free_energies, """ - Not all reactions have thermodynamic data. - Either add the reactions with missing ΔG0s to ignore_reaction_ids, + Not all reactions have thermodynamic data. + Either add the reactions with missing ΔG0s to ignore_reaction_ids, or add the data to reaction_standard_gibbs_free_energies. """, ), @@ -48,7 +48,7 @@ function build_max_min_driving_force_model( :delta_G_reactions^C.variables(keys = Symbol.(rxns)), ) - #= + #= Build gibbs free energy relations ΔG_rxn == ΔG0 + R * T * sum ( log_concentration_variable * stoichiometry_value ) =# @@ -76,18 +76,18 @@ function build_max_min_driving_force_model( ) for (rxn, (dG0, met_ids, stoich_coeffs)) in zip(rxns, dG0s_met_ids_stoichs) ) - #= + #= Set proton log concentration to zero so that it won't impact any calculations (biothermodynamics assumption). Also set water concentrations - to zero (aqueous condition assumptions). How true is "aqueous conditions"? - Debatable... + to zero (aqueous condition assumptions). How true is "aqueous conditions"? + Debatable... =# for met in [Symbol.(proton_ids); Symbol.(water_ids)] haskey(m.log_metabolite_concentrations, met) && (m.log_metabolite_concentrations[met].bound = 0.0) end - #= + #= Add thermodynamic feasibility constraint (ΔG < 0 for a feasible reaction in flux direction). Add objective constraint to solve max min problem. =# @@ -241,7 +241,7 @@ function max_min_driving_force_analysis( proton_ids, ) - for (mid, val) in constant_concentrations + for (mid, val) in constant_concentrations m.log_metabolite_concentrations[Symbol(mid)].bound = log(val) end From 841d02e175865afd57829474e5b6cff93c88648c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Sat, 16 Dec 2023 21:18:16 +0100 Subject: [PATCH 420/531] simplify the ratio constraints --- src/builders/thermodynamic.jl | 36 +++++++++++++---------------------- 1 file changed, 13 insertions(+), 23 deletions(-) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index fff88d30c..ebf3d7650 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -136,26 +136,17 @@ m = add_ratio_constraints( ) ``` """ -function add_log_ratio_constraints( - m::C.ConstraintTree, - ratios::Dict{String,Tuple{String,String,Float64}}; - name::Symbol, +log_ratio_constraints( + ratios::Dict{String,Tuple{String,String,Float64}}, on::C.ConstraintTree, +) = C.ConstraintTree( + Symbol(cid) => C.Constraint( + value = on[Symbol(var1)].value - on[Symbol(var2)].value, + bound = log(ratio), + ) for (cid, (var1, var2, ratio)) in ratios ) - for (cid, (var1, var2, ratio)) in ratios - m *= - name^Symbol( - cid, - )^C.Constraint( - value = on[Symbol(var1)].value - on[Symbol(var2)].value, - bound = log(ratio), - ) - end - - m -end -export add_log_ratio_constraints +export log_ratio_constraints """ @@ -245,12 +236,11 @@ function max_min_driving_force_analysis( m.log_metabolite_concentrations[Symbol(mid)].bound = log(val) end - m = add_log_ratio_constraints( - m, - concentration_ratios; - name = :metabolite_ratio_constraints, - on = m.log_metabolite_concentrations, - ) + m *= + :metabolite_ratio_constraints^log_ratio_constraints( + concentration_ratios, + m.log_metabolite_concentrations, + ) optimized_constraints( From 30b7bf2c5b345befa09b6de1e8a06aac31ea752e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 17 Dec 2023 12:23:15 +0100 Subject: [PATCH 421/531] fix broken tests --- .../examples/05-enzyme-constrained-models.jl | 2 +- docs/src/examples/06-thermodynamic-models.jl | 8 +- src/builders/thermodynamic.jl | 8 +- test/data_static.jl | 82 ------------------- 4 files changed, 7 insertions(+), 93 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 64287d165..8c5f6b71b 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -102,7 +102,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( #src these values should be unique (glucose transporter is the only way to get carbon into the system) @test isapprox(ec_solution.objective, 3.2105477675077743, atol = TEST_TOLERANCE) #src @test isapprox(ec_solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src -@test isapprox(ec_solution.fluxes.EX_glc__D_e, -41.996885051738445, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.fluxes.EX_glc__D_e, -42.0, atol = 0.1) #src @test isapprox(ec_solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src ### Building a model incrementally diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 110df7be9..d7b67bb9f 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -112,11 +112,9 @@ m = build_max_min_driving_force_model( R = 8.31446261815324e-3, # kJ/K/mol ) -m = add_log_ratio_constraints( - m, - Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)); - name = :metabolite_ratio_constraints, - on = m.log_metabolite_concentrations, +m *= :metabolite_ratio_constraints^log_ratio_constraints( + Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), + m.log_metabolite_concentrations, ) # solve the model diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index ebf3d7650..a27b62b99 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -128,11 +128,9 @@ latter is logged internally. # Example ``` -m = add_ratio_constraints( - m, - Dict("atp" => ("atp_c", "adp_c", log(10.0)), "nadh" => ("nadh_c", "nad_c", log(0.13))); - name = :metabolite_ratio_constraints, - on = m.log_metabolite_concentrations, +m *= :log_ratio_constraints^log_ratio_constraints( + Dict("atp" => ("atp_c", "adp_c", log(10.0)), "nadh" => ("nadh_c", "nad_c", log(0.13))), + m.log_metabolite_concentrations, ) ``` """ diff --git a/test/data_static.jl b/test/data_static.jl index 2956bcf42..60933d58d 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -1,86 +1,4 @@ -const reaction_standard_gibbs_free_energies = Dict{String,Float64}( - #= - ΔᵣG⁰ data from Equilibrator using the E. coli core model's reactions - To generate this data manually, go to https://equilibrator.weizmann.ac.il/ and - enter each reaction into the search bar, fill in the ΔG⁰ below from there. To generate - automatically, use the eQuilibrator.jl package. - =# - "ACALD" => -21.268198981756314, - "PTAr" => 8.651025243027988, - "ALCD2x" => 17.47260052762408, - "PDH" => -34.246780702043225, - "PYK" => -24.48733600711958, - "CO2t" => 0.0, - "MALt2_2" => -6.839209921974724, - "CS" => -39.330940148207475, - "PGM" => -4.470553692565886, - "TKT1" => -1.4976299544215408, - "ACONTa" => 8.46962176350985, - "GLNS" => -15.771242654033706, - "ICL" => 9.53738025507404, - "FBA" => 23.376920310319235, - "SUCCt3" => -43.97082007726285, - "FORt2" => -3.4211767267069124, - "G6PDH2r" => -7.3971867270165035, - "AKGDH" => -28.235442362320782, - "TKT2" => -10.318436817276165, - "FRD7" => 73.61589524884772, - "SUCOAS" => -1.1586968559555615, - "FBP" => -11.606887851480572, - "ICDHyr" => 5.398885342794983, - "AKGt2r" => 10.085299238039797, - "GLUSy" => -47.21690395283849, - "TPI" => 5.621932460512994, - "FORt" => 13.509690780237293, - "ACONTb" => -1.622946931741609, - "GLNabc" => -30.194559792842753, - "RPE" => -3.3881719029747615, - "ACKr" => 14.027197131450492, - "THD2" => -33.84686533309243, - "PFL" => -19.814421615735, - "RPI" => 4.477649590945703, - "D_LACt2" => -3.4223903975852004, - "TALA" => -0.949571985515206, - "PPCK" => 10.659841402564751, - "ACt2r" => -3.4196348363397995, - "NH4t" => -13.606633927039097, - "PGL" => -25.94931748161696, - "NADTRHD" => -0.014869680795754903, - "PGK" => 19.57192102020454, - "LDH_D" => 20.04059765689044, - "ME1" => 12.084968268076864, - "PIt2r" => 10.415108493818785, - "ATPS4r" => -37.570267233299816, - "PYRt2" => -3.422891289768689, - "GLCpts" => -45.42430981510088, - "GLUDy" => 32.834943812395665, - "CYTBD" => -59.700410815493775, - "FUMt2_2" => -6.845105500839001, - "FRUpts2" => -42.67529760694199, - "GAPD" => 0.5307809794271634, - "H2Ot" => -1.5987211554602254e-14, - "PPC" => -40.81304419704113, - "NADH16" => -80.37770501380615, - "PFK" => -18.546314942995934, - "MDH" => 25.912462872631522, - "PGI" => 2.6307087407442395, - "O2t" => 0.0, - "ME2" => 12.099837948872533, - "GND" => 10.312275879236381, - "SUCCt2_2" => -6.82178244356977, - "GLUN" => -14.381960140443113, - "ETOHt2r" => -16.930867506944217, - "ADK1" => 0.3893321583896068, - "ACALDt" => -3.197442310920451e-14, - "SUCDi" => -73.61589524884772, - "ENO" => -3.8108376097261782, - "MALS" => -39.22150045995042, - "GLUt2r" => -3.499043558772904, - "PPS" => -6.0551989457468665, - "FUM" => -3.424133018702122, -) - const ecoli_core_gene_product_masses = Dict{String,Float64}( #= Data downloaded from Uniprot for E. coli K12, From 9f4e0d2493a328a8845bd77a5c4e6dcfa987c762 Mon Sep 17 00:00:00 2001 From: stelmo Date: Sun, 17 Dec 2023 11:25:01 +0000 Subject: [PATCH 422/531] automatic formatting triggered by @stelmo on PR #805 --- docs/src/examples/06-thermodynamic-models.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index d7b67bb9f..1ac8afd0f 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -112,10 +112,11 @@ m = build_max_min_driving_force_model( R = 8.31446261815324e-3, # kJ/K/mol ) -m *= :metabolite_ratio_constraints^log_ratio_constraints( - Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), - m.log_metabolite_concentrations, -) +m *= + :metabolite_ratio_constraints^log_ratio_constraints( + Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), + m.log_metabolite_concentrations, + ) # solve the model mmdf_solution = optimized_constraints( From e7c2da09d52b138b53dc8450e6643b47065cde6e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 22 Dec 2023 14:24:03 +0100 Subject: [PATCH 423/531] update to CT v0.7 --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 1ec193418..58a3260fe 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.6.1" +ConstraintTrees = "0.7" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" From e8b78da87f6f5aab325228aa0cfb079fb59c7d34 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 14:00:29 +0100 Subject: [PATCH 424/531] all tests pass --- .../examples/02c-constraint-modifications.jl | 4 ++-- .../examples/05-enzyme-constrained-models.jl | 5 +++-- src/builders/core.jl | 12 ++++++------ src/builders/enzymes.jl | 12 ++++++------ src/builders/genes.jl | 2 +- src/builders/thermodynamic.jl | 18 +++++++++--------- src/solver.jl | 10 +++++----- 7 files changed, 32 insertions(+), 31 deletions(-) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 3ff451473..c9f616ab7 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -55,7 +55,7 @@ vt = optimized_constraints( # Models that cannot be solved return `nothing`. In the example below, the # underlying model is modified. -ctmodel.fluxes.ATPM.bound = (1000.0, 10000.0) +ctmodel.fluxes.ATPM.bound = C.Between(1000.0, 10000.0) #TODO explicitly show here how false sharing looks like @@ -70,7 +70,7 @@ vt = optimized_constraints( # Models can also be piped into the analysis functions -ctmodel.fluxes.ATPM.bound = (8.39, 10000.0) # revert +ctmodel.fluxes.ATPM.bound = C.Between(8.39, 10000.0) # revert vt = optimized_constraints( ctmodel, objective = ctmodel.objective.value, diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 8c5f6b71b..63af35ed6 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -106,6 +106,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( @test isapprox(ec_solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src ### Building a model incrementally +import ConstraintTrees as C # create basic flux model m = fbc_model_constraints(model) @@ -114,8 +115,8 @@ m = fbc_model_constraints(model) m += :enzymes^enzyme_variables(model) # constrain some fluxes and enzymes manually -m.fluxes.EX_glc__D_e.bound = (-1000.0, 0.0) # undo glucose important bound from original model -m.enzymes.b2417.bound = (0.0, 0.1) # for fun, change the bounds of the protein b2417 +m.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 0.0) # undo glucose important bound from original model +m.enzymes.b2417.bound = C.Between(0.0, 0.1) # for fun, change the bounds of the protein b2417 # attach the enzyme mass balances m = add_enzyme_constraints!( diff --git a/src/builders/core.jl b/src/builders/core.jl index 0d0624113..dba69a391 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -24,7 +24,7 @@ function fbc_model_constraints(model::A.AbstractFBCModel) return C.ConstraintTree( :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( - met => C.Constraint(value = C.LinearValue(sparse(row)), bound = b) for + met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) for (met, row, b) in zip(mets, eachrow(stoi), bal) ) * :objective^C.Constraint(C.LinearValue(sparse(obj))), @@ -39,7 +39,7 @@ $(TYPEDSIGNATURES) Shortcut for allocation non-negative ("unsigned") variables. The argument `keys` is forwarded to `ConstraintTrees.variables` as `keys`. """ -unsigned_variables(; keys) = C.variables(; keys, bounds = Ref((0.0, Inf))) +unsigned_variables(; keys) = C.variables(; keys, bounds = C.Between(0.0, Inf)) export unsigned_variables @@ -77,7 +77,7 @@ sign_split_constraints(; value = s.value + (haskey(negative, k) ? negative[k].value : zero(typeof(s.value))) - (haskey(positive, k) ? positive[k].value : zero(typeof(s.value))), - bound = 0.0, + bound = C.EqualTo(0.0), ) for (k, s) in signed ) #TODO the example above might as well go to docs @@ -88,12 +88,12 @@ function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) keys = Symbol[] for (id, flux) in fluxes if direction == :forward - last(flux.bound) > 0 && push!(keys, id) + flux.bound.upper > 0 && push!(keys, id) else - first(flux.bound) < 0 && push!(keys, id) + flux.bound.lower < 0 && push!(keys, id) end end - C.variables(; keys, bounds = Ref((0.0, Inf))) + C.variables(; keys, bounds = C.Between(0.0, Inf)) end export fluxes_in_direction diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index c64a28067..130f146e1 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -5,7 +5,7 @@ Allocate enzyme variables (gene products in the model) to a constraint tree using all the genes in the `model`. """ enzyme_variables(model::A.AbstractFBCModel) = - C.variables(; keys = Symbol.(A.genes(model)), bounds = Ref((0.0, Inf))) + C.variables(; keys = Symbol.(A.genes(model)), bounds = C.Between(0.0, Inf)) export enzyme_variables @@ -23,7 +23,7 @@ function isozyme_variables( ) C.variables(; keys = Symbol.(collect(keys(reaction_isozymes[reaction_id]))), - bounds = Ref((0.0, Inf)), + bounds = C.Between(0.0, Inf), ) end @@ -40,7 +40,7 @@ function link_isozymes( C.ConstraintTree( k => C.Constraint( value = s.value - sum(x.value for (_, x) in fluxes_isozymes[k]), - bound = 0.0, + bound = C.EqualTo(0.0), ) for (k, s) in fluxes_directional if haskey(fluxes_isozymes, k) ) end @@ -94,7 +94,7 @@ function enzyme_stoichiometry( haskey(fluxes_isozymes_backward, rid); init = zero(typeof(enz.value)), ), # flux through negative isozymes - bound = 0.0, + bound = C.EqualTo(0.0), ) for (gid, enz) in enzymes if gid in keys(enzyme_rid_lookup) ) end @@ -139,7 +139,7 @@ function enzyme_capacity( value = sum( enzymes[Symbol(gid)].value * gene_molar_masses[gid] for gid in enzyme_ids ), - bound = (0.0, capacity), + bound = C.Between(0.0, capacity), ) end @@ -267,7 +267,7 @@ function enzyme_constrained_flux_balance_analysis( ) for rid in Symbol.(unconstrain_reactions) - m.fluxes[rid].bound = (-1000.0, 1000.0) + m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) end optimized_constraints(m; objective = m.objective.value, optimizer, modifications) diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 8cd1587dd..0f6ddec90 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -3,7 +3,7 @@ $(TYPEDSIGNATURES) """ knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), 0.0) for + rxn => C.Constraint(C.value(fluxes[rxn]), C.EqualTo(0.0)) for rxn in keys(fluxes) if knockout_test(rxn) ) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index a27b62b99..628c86b8b 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -43,7 +43,7 @@ function build_max_min_driving_force_model( :max_min_driving_force^C.variable() + :log_metabolite_concentrations^C.variables( keys = Symbol.(mets), - bounds = zip(log(concentration_lb), log(concentration_ub)), + bounds = C.Between(log(concentration_lb), log(concentration_ub)), ) + :delta_G_reactions^C.variables(keys = Symbol.(rxns)), ) @@ -72,7 +72,7 @@ function build_max_min_driving_force_model( sum( m.log_metabolite_concentrations[Symbol(met_id)].value * stoich for (met_id, stoich) in zip(met_ids, stoich_coeffs) ), - bound = 0.0, + bound = C.EqualTo(0.0), ) for (rxn, (dG0, met_ids, stoich_coeffs)) in zip(rxns, dG0s_met_ids_stoichs) ) @@ -83,8 +83,9 @@ function build_max_min_driving_force_model( Debatable... =# for met in [Symbol.(proton_ids); Symbol.(water_ids)] - haskey(m.log_metabolite_concentrations, met) && - (m.log_metabolite_concentrations[met].bound = 0.0) + if haskey(m.log_metabolite_concentrations, met) + m.log_metabolite_concentrations[met] = C.Constraint(value=m.log_metabolite_concentrations[met].value, bound = C.EqualTo(0.0)) + end end #= @@ -96,7 +97,7 @@ function build_max_min_driving_force_model( Symbol(rxn) => C.Constraint( value = m.delta_G_reactions[Symbol(rxn)].value * sign(get(reference_flux, rxn, 1.0)), - bound = (-Inf, 0.0), + bound = C.Between(-Inf, 0.0), ) for rxn in rxns ) @@ -106,7 +107,7 @@ function build_max_min_driving_force_model( value = m.max_min_driving_force.value + m.delta_G_reactions[Symbol(rxn)].value * sign(get(reference_flux, rxn, 1.0)), - bound = (-Inf, 0.0), + bound = C.Between(-Inf, 0.0), ) for rxn in rxns ) @@ -140,13 +141,12 @@ log_ratio_constraints( ) = C.ConstraintTree( Symbol(cid) => C.Constraint( value = on[Symbol(var1)].value - on[Symbol(var2)].value, - bound = log(ratio), + bound = C.EqualTo(log(ratio)), ) for (cid, (var1, var2, ratio)) in ratios ) export log_ratio_constraints - """ $(TYPEDSIGNATURES) @@ -231,7 +231,7 @@ function max_min_driving_force_analysis( ) for (mid, val) in constant_concentrations - m.log_metabolite_concentrations[Symbol(mid)].bound = log(val) + m.log_metabolite_concentrations[Symbol(mid)].bound = C.EqualTo(log(val)) end m *= diff --git a/src/solver.jl b/src/solver.jl index 2c48c0ccd..57b3bf787 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -19,12 +19,12 @@ function optimization_model( # constraints function add_constraint(c::C.Constraint) - if c.bound isa Float64 - J.@constraint(model, C.substitute(c.value, x) == c.bound) - elseif c.bound isa C.IntervalBound + if c.bound isa C.EqualTo + J.@constraint(model, C.substitute(c.value, x) == c.bound.equal_to) + elseif c.bound isa C.Between val = C.substitute(c.value, x) - isinf(c.bound[1]) || J.@constraint(model, val >= c.bound[1]) - isinf(c.bound[2]) || J.@constraint(model, val <= c.bound[2]) + isinf(c.bound.lower) || J.@constraint(model, val >= c.bound.lower) + isinf(c.bound.upper) || J.@constraint(model, val <= c.bound.upper) end end function add_constraint(c::C.ConstraintTree) From d7dc3569fcf92cbba9b33e9de3f43795394d2778 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 14:00:53 +0100 Subject: [PATCH 425/531] format --- src/builders/core.jl | 4 ++-- src/builders/thermodynamic.jl | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/builders/core.jl b/src/builders/core.jl index dba69a391..93807205c 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -24,8 +24,8 @@ function fbc_model_constraints(model::A.AbstractFBCModel) return C.ConstraintTree( :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( - met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) for - (met, row, b) in zip(mets, eachrow(stoi), bal) + met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) + for (met, row, b) in zip(mets, eachrow(stoi), bal) ) * :objective^C.Constraint(C.LinearValue(sparse(obj))), ) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 628c86b8b..bc3ea9a84 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -84,7 +84,10 @@ function build_max_min_driving_force_model( =# for met in [Symbol.(proton_ids); Symbol.(water_ids)] if haskey(m.log_metabolite_concentrations, met) - m.log_metabolite_concentrations[met] = C.Constraint(value=m.log_metabolite_concentrations[met].value, bound = C.EqualTo(0.0)) + m.log_metabolite_concentrations[met] = C.Constraint( + value = m.log_metabolite_concentrations[met].value, + bound = C.EqualTo(0.0), + ) end end From 222e7f03d67b15cd387efbf602192aaf0582c294 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 14:10:26 +0100 Subject: [PATCH 426/531] simplify kcats --- test/data_static.jl | 223 ++++++++++++-------------------------------- 1 file changed, 62 insertions(+), 161 deletions(-) diff --git a/test/data_static.jl b/test/data_static.jl index 60933d58d..6c3988fd7 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -151,165 +151,66 @@ const ecoli_core_reaction_kcats = Dict{String,Vector{Tuple{Float64,Float64}}}( turnover numbers reveals protein structural correlates and improves metabolic models." Nature communications 9.1 (2018): 1-10. =# - "ACALD" => - [(568.1130792316333, 568.1130792316333), (568.856126503717, 568.856126503717)], - "PTAr" => [ - (1171.9703624351055, 1171.9703624351055), - (1173.7231032615289, 1173.7231032615289), - ], - "ALCD2x" => [ - (75.9547881894345, 75.9547881894345), - (75.96334310351442, 75.96334310351442), - (76.1472359297987, 76.1472359297987), - ], - "PDH" => [(529.7610874857239, 529.7610874857239)], - "PYK" => [ - (422.0226052080562, 422.0226052080562), - (422.1332899347833, 422.1332899347833), - ], - "MALt2_2" => [(234.03664660088714, 234.03664660088714)], - "CS" => [(113.29607453875758, 113.29607453875758)], - "PGM" => [ - (681.4234715886669, 681.4234715886669), - (681.6540601244343, 681.6540601244343), - (680.5234799168278, 680.5234799168278), - ], - "TKT1" => [ - (311.16139580671637, 311.16139580671637), - (311.20967965149947, 311.20967965149947), - ], - "ACONTa" => [ - (191.02308213992006, 191.02308213992006), - (191.03458045697235, 191.03458045697235), - ], - "GLNS" => [ - (89.83860937287024, 89.83860937287024), - (89.82177852142014, 89.82177852142014), - ], - "ICL" => [(17.45922330097792, 17.45922330097792)], - "FBA" => [ - (373.425646787578, 373.425646787578), - (372.74936053215833, 372.74936053215833), - (372.88627228768166, 372.88627228768166), - ], - "FORt2" => [ - (233.93045260179326, 233.93045260179326), - (233.84804009142908, 233.84804009142908), - ], - "G6PDH2r" => [(589.3761070080022, 589.3761070080022)], - "AKGDH" => [(264.48071159327156, 264.48071159327156)], - "TKT2" => [ - (467.4226876901618, 467.4226876901618), - (468.1440593542596, 468.1440593542596), - ], - "FRD7" => [(90.20637824912605, 90.20637824912605)], - "SUCOAS" => [(18.494387648707622, 18.494387648707622)], - "FBP" => [ - (568.5346256470805, 568.5346256470805), - (567.6367759041788, 567.6367759041788), - ], - "ICDHyr" => [(39.62446791678959, 39.62446791678959)], - "AKGt2r" => [(234.99097804446805, 234.99097804446805)], - "GLUSy" => [(33.262997317319055, 33.262997317319055)], - "TPI" => [(698.301904211076, 698.301904211076)], - "FORt" => [ - (234.38391855848187, 234.38391855848187), - (234.34725576182922, 234.34725576182922), - ], - "ACONTb" => [ - (159.74612206327865, 159.74612206327865), - (159.81975755249232, 159.81975755249232), - ], - "GLNabc" => [(233.80358131677775, 233.80358131677775)], - "RPE" => [ - (1772.4850826683305, 1772.4850826683305), - (1768.8536177485582, 1768.8536177485582), - ], - "ACKr" => [ - (554.611547307207, 554.611547307207), - (555.112707891257, 555.112707891257), - (555.2464368932744, 555.2464368932744), - ], - "THD2" => [(24.739139801185537, 24.739139801185537)], - "PFL" => [ - (96.56316095411077, 96.56316095411077), - (96.65024313036014, 96.65024313036014), - (96.60761818004025, 96.60761818004025), - (96.49541118899961, 96.49541118899961), - ], - "RPI" => [ - (51.771578021074234, 51.771578021074234), - (51.81603467243345, 51.81603467243345), - ], - "D_LACt2" => [ - (233.51709131524734, 233.51709131524734), - (233.83187606098016, 233.83187606098016), - ], - "TALA" => [ - (109.05210545422884, 109.05210545422884), - (109.04246437049026, 109.04246437049026), - ], - "PPCK" => [(218.4287805666016, 218.4287805666016)], - "PGL" => [(2120.4297518987964, 2120.4297518987964)], - "NADTRHD" => [ - (186.99387360624777, 186.99387360624777), - (187.16629305266423, 187.16629305266423), - ], - "PGK" => [(57.641966636896335, 57.641966636896335)], - "LDH_D" => [ - (31.11118891764946, 31.11118891764946), - (31.12493425054357, 31.12493425054357), - ], - "ME1" => [(487.0161203971232, 487.0161203971232)], - "PIt2r" => [ - (233.8651331835765, 233.8651331835765), - (234.27374798581067, 234.27374798581067), - ], - "ATPS4r" => [ - (7120.878030435999, 7120.878030435999), - (7116.751386037507, 7116.751386037507), - ], - "GLCpts" => [ - (233.9009878400008, 233.9009878400008), - (233.66656882114864, 233.66656882114864), - (233.66893882934883, 233.66893882934883), - ], - "GLUDy" => [(105.32811069172409, 105.32811069172409)], - "CYTBD" => [ - (153.18512795009505, 153.18512795009505), - (153.2429537682265, 153.2429537682265), - ], - "FUMt2_2" => [(234.37495609395967, 234.37495609395967)], - "FRUpts2" => [(234.1933863380989, 234.1933863380989)], - "GAPD" => [(128.76795529111456, 128.76795529111456)], - "PPC" => [(165.52424516841342, 165.52424516841342)], - "NADH16" => [(971.7487306963936, 971.7487306963936)], - "PFK" => [ - (1000.4626204522712, 1000.4626204522712), - (1000.5875517343595, 1000.5875517343595), - ], - "MDH" => [(25.931655783969283, 25.931655783969283)], - "PGI" => [(468.11833198138834, 468.11833198138834)], - "ME2" => [(443.0973626307168, 443.0973626307168)], - "GND" => [(240.1252264230952, 240.1252264230952)], - "SUCCt2_2" => [(234.18109388303225, 234.18109388303225)], - "GLUN" => [ - (44.76358496525738, 44.76358496525738), - (44.84850207360875, 44.84850207360875), - (44.76185250415503, 44.76185250415503), - ], - "ADK1" => [(111.64869652600649, 111.64869652600649)], - "SUCDi" => [(680.3193833053011, 680.3193833053011)], - "ENO" => [(209.35855069219886, 209.35855069219886)], - "MALS" => [ - (252.7540503869977, 252.7540503869977), - (252.2359738678874, 252.2359738678874), - ], - "GLUt2r" => [(234.22890837451837, 234.22890837451837)], - "PPS" => [(706.1455885214322, 706.1455885214322)], - "FUM" => [ - (1576.8372583425075, 1576.8372583425075), - (1576.233088455828, 1576.233088455828), - (1575.9638204848736, 1575.9638204848736), - ], + "ACALD" => 568.11, + "PTAr" => 1171.97, + "ALCD2x" => 75.95, + "PDH" => 529.76, + "MALt2_2" => 234.03, + "CS" => 113.29, + "PGM" => 681.4, + "TKT1" => 311.16, + "ACONTa" => 191.02, + "GLNS" => 89.83, + "ICL" => 17.45, + "FBA" => 373.42, + "FORt2" => 233.93, + "G6PDH2r" => 589.37, + "AKGDH" => 264.48, + "TKT2" => 467.42, + "FRD7" => 90.20, + "SUCOAS" => 18.49, + "ICDHyr" => 39.62, + "AKGt2r" => 234.99, + "GLUSy" => 33.26, + "TPI" => 698.30, + "FORt" => 234.38, + "ACONTb" => 159.74, + "GLNabc" => 233.80, + "RPE" => 1772.485, + "ACKr" => 554.61, + "THD2" => 24.73, + "PFL" => 96.56, + "RPI" => 51.77, + "D_LACt2" => 233.51, + "TALA" => 109.05, + "PPCK" => 218.42, + "PGL" => 2120.42, + "NADTRHD" => 186.99, + "PGK" => 57.64, + "LDH_D" => 31.11, + "ME1" => 487.01, + "PIt2r" => 233.86, + "ATPS4r" => 71.42, + "GLCpts" => 233.90, + "GLUDy" => 105.32, + "CYTBD" => 153.18, + "FUMt2_2" => 234.37, + "FRUpts2" => 234.19, + "GAPD" => 128.76, + "PPC" => 165.52, + "NADH16" => 971.74, + "PFK" => 1000.46, + "MDH" => 25.93, + "PGI" => 468.11, + "ME2" => 443.09, + "GND" => 240.12, + "SUCCt2_2" => 234.18, + "GLUN" => 44.76, + "ADK1" => 111.64, + "SUCDi" => 680.31, + "ENO" => 209.35, + "MALS" => 252.75, + "GLUt2r" => 234.22, + "PPS" => 706.14, + "FUM" => 1576.83, ) From 486ad4d96ef0fc3d2d39f6d3ff1812d7f6aa51be Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 15:43:17 +0100 Subject: [PATCH 427/531] implement reviews (partially) --- .../examples/05-enzyme-constrained-models.jl | 63 ++++++++++++------- docs/src/examples/06-thermodynamic-models.jl | 47 ++++++++------ src/builders/thermodynamic.jl | 25 +++----- test/data_static.jl | 4 +- test/{enzyme-model.jl => enzyme_model.jl} | 0 test/runtests.jl | 2 +- 6 files changed, 81 insertions(+), 60 deletions(-) rename test/{enzyme-model.jl => enzyme_model.jl} (100%) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 63af35ed6..5ade6d726 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -13,6 +13,7 @@ import Downloads: download # Additionally to COBREXA and the model format package, we will need a solver # -- let's use Tulip here: +import AbstractFBCModels as A import JSONFBCModels import Tulip @@ -22,35 +23,35 @@ model = load_model("e_coli_core.json") # conventional constraint based models. These include reaction specific turnover # numbers, molar masses of enzymes, and capacity bounds. -import AbstractFBCModels as A +# ## Reaction turnover numbers + +# Enzyme constrained models require reaction turnover numbers, which are often +# isozyme specfic. Many machine learning tools, or experimental data sets, can +# be used to estimate these parameters. -### Reaction turnover numbers +# ```@raw html +#
Reaction turnover numbers +# ??? how to load this? +#
+# ``` -# Here we will use randomly generated turnover numbers for simplicity. Each -# reaction in a constraint-based model usually has gene reaction rules +# Each reaction in a constraint-based model usually has gene reaction rules # associated with it. These typically take the form of, possibly multiple, # isozymes that can catalyze a reaction. A turnover number needs to be assigned # to each isozyme, as shown below. -using Random - reaction_isozymes = Dict{String,Dict{String,Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available - haskey(ecoli_core_reaction_kcats, rid) || continue #src + haskey(ecoli_core_reaction_kcats, rid) || continue # skip if no kcat data available for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) # each isozyme gets a unique name d["isozyme_"*string(i)] = Isozyme( # Isozyme struct is defined by COBREXA gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes - kcat_forward = (100 + 50 * rand()) * 3600.0, # forward reaction turnover number units = 1/h - kcat_backward = (100 + 50 * rand()) * 3600.0, # reverse reaction turnover number units = 1/h - ) - d["isozyme_"*string(i)] = Isozyme( #src - gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), #src - kcat_forward = ecoli_core_reaction_kcats[rid][i][1] * 3600.0, #src - kcat_backward = ecoli_core_reaction_kcats[rid][i][2] * 3600.0, #src + kcat_forward = ecoli_core_reaction_kcats[rid] * 3600.0, # forward reaction turnover number units = 1/h + kcat_backward = ecoli_core_reaction_kcats[rid] * 3600.0, # reverse reaction turnover number units = 1/h ) end end @@ -59,15 +60,21 @@ end # Take care with the units of the turnover numbers. In literature they are # usually reported in 1/s. However, flux units are typically mmol/gDW/h, # suggesting that you should rescale the turnover numbers to 1/h if you -# want to use the traditional flux units. +# want to use the conventional flux units. ### Enzyme molar masses -# Similarly, we will randomly generate enzyme molar masses for use in the enzyme -# constrained model. +# We also require the mass of each enzyme, to properly weight the contribution +# of each flux/isozyme in the capacity bound(s). These data can typically be +# found in uniprot. -gene_molar_masses = Dict(gid => 20 + 40 * rand() for gid in A.genes(model)) -gene_molar_masses = ecoli_core_gene_product_masses #src +# ```@raw html +#
Reaction turnover numbers +# ??? how to load this? +#
+# ``` + +gene_molar_masses = ecoli_core_gene_product_masses #!!! warning "Molar mass units" # Take care with the units of the molar masses. In literature they are @@ -89,6 +96,9 @@ total_enzyme_capacity = 0.1 # g enzyme/gDW ### Running a basic enzyme constrained model +# With all the parameters specified, we can directly use the enzyme constrained +# convenience function to run enzyme constrained FBA in one shot: + ec_solution = enzyme_constrained_flux_balance_analysis( model, reaction_isozymes, @@ -100,12 +110,17 @@ ec_solution = enzyme_constrained_flux_balance_analysis( ) #src these values should be unique (glucose transporter is the only way to get carbon into the system) -@test isapprox(ec_solution.objective, 3.2105477675077743, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.objective, 1.671357282901553, atol = TEST_TOLERANCE) #src @test isapprox(ec_solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src -@test isapprox(ec_solution.fluxes.EX_glc__D_e, -42.0, atol = 0.1) #src -@test isapprox(ec_solution.enzymes.b2417, 9.974991164132524e-5, atol = 1e-7) #src +@test isapprox(ec_solution.fluxes.EX_glc__D_e, -49.92966287110028, atol = 0.1) #src +@test isapprox(ec_solution.enzymes.b2417, 0.00011859224858442563, atol = 1e-7) #src ### Building a model incrementally + +# Sometimes it is necessary to build a more complicated model, perhaps using a +# novel type of constraint. For this, it is useful to build the enzyme +# constrained model incrementally, using the ConstraintTree building blocks. + import ConstraintTrees as C # create basic flux model @@ -114,8 +129,10 @@ m = fbc_model_constraints(model) # create enzyme variables m += :enzymes^enzyme_variables(model) -# constrain some fluxes and enzymes manually +# constrain some fluxes... m.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 0.0) # undo glucose important bound from original model + +# ...And enzymes manually m.enzymes.b2417.bound = C.Between(0.0, 0.1) # for fun, change the bounds of the protein b2417 # attach the enzyme mass balances diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 1ac8afd0f..7b5f225fe 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -33,17 +33,18 @@ model = load_model("e_coli_core.json") # eQuilibrator.jl package. reaction_standard_gibbs_free_energies = Dict{String,Float64}( - "GLCpts" => -45.42430981510088, - "PGI" => 2.6307087407442395, - "PFK" => -18.546314942995934, + # Units are in kJ/mol + "ENO" => -3.8108376097261782, "FBA" => 23.376920310319235, - "TPI" => 5.621932460512994, "GAPD" => 0.5307809794271634, + "GLCpts" => -45.42430981510088, + "LDH_D" => 20.04059765689044, + "PFK" => -18.546314942995934, + "PGI" => 2.6307087407442395, "PGK" => 19.57192102020454, "PGM" => -4.470553692565886, - "ENO" => -3.8108376097261782, "PYK" => -24.48733600711958, - "LDH_D" => 20.04059765689044, + "TPI" => 5.621932460512994, ) # ## Running basic max min driving force analysis @@ -55,26 +56,36 @@ reaction_standard_gibbs_free_energies = Dict{String,Float64}( # dictionary can be a flux solution, the sign of each flux is used to determine # if the reaction runs forward or backward. -#!!! warning "Only the signs are extracted from the reference solution" -# It is most convenient to pass a flux solution into `reference_flux`, but -# take care to round fluxes near 0 to their correct sign if they should be -# included in the resultant thermodynamic model. Otherwise, remove them from -# reference flux input. +# ## Using a reference solution + +# Frequently it is useful to check the max-min driving force of a specific FBA +# solution. In this case, one is usually only interested in a subset of all the +# reactions in a model. These reactions can be specified as a the +# `reference_flux`, to only compute the MMDF of these reactions, and ignore all +# other reactions. reference_flux = Dict( - "GLCpts" => 1.0, - "PGI" => 1.0, - "PFK" => 1.0, + "ENO" => 1.0, "FBA" => 1.0, - "TPI" => 1.0, "GAPD" => 1.0, + "GLCpts" => 1.0, + "LDH_D" => -1.0, + "PFK" => 1.0, + "PGI" => 1.0, "PGK" => -1.0, "PGM" => -1.0, - "ENO" => 1.0, "PYK" => 1.0, - "LDH_D" => -1.0, + "TPI" => 1.0, ) +#!!! warning "Only the signs are extracted from the reference solution" +# It is most convenient to pass a flux solution into `reference_flux`, but +# take care to round fluxes near 0 to their correct sign if they should be +# included in the resultant thermodynamic model. Otherwise, remove them from +# reference flux input. + +# ## Solving the MMDF problem + mmdf_solution = max_min_driving_force_analysis( model, reaction_standard_gibbs_free_energies; @@ -118,7 +129,7 @@ m *= m.log_metabolite_concentrations, ) -# solve the model +# solve the model, which is not just a normal constraint tree! mmdf_solution = optimized_constraints( m; objective = m.max_min_driving_force.value, diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index bc3ea9a84..9ad5e8915 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -119,24 +119,16 @@ end export build_max_min_driving_force_model - """ $(TYPEDSIGNATURES) Add constraints to `m` that represents ratios of variables in log space: -`log(x/y) = log(const)` where `x` and `y` are variables specified by `on`. These -constraints are called `name` in the resultant model. The constraints are -specified by `ratios`, which is a dictionary mapping a constraint id to a tuple -which consists of the variable ids, `x`, `y`, and the ratio value, `const`. The -latter is logged internally. - -# Example -``` -m *= :log_ratio_constraints^log_ratio_constraints( - Dict("atp" => ("atp_c", "adp_c", log(10.0)), "nadh" => ("nadh_c", "nad_c", log(0.13))), - m.log_metabolite_concentrations, -) -``` +`log(x/y) = log(const)` where `x` and `y` are variables specified by `on`. The +constraints are specified by `ratios`, which is a dictionary mapping a +constraint id to a tuple which consists of the variable ids, `x`, `y`, and the +ratio value, `const`. The latter is logged internally, while the variables are +subtracted from each other, as it is assumed they are already in log space, +`log(x/y) = log(x) - log(y)`. """ log_ratio_constraints( ratios::Dict{String,Tuple{String,String,Float64}}, @@ -201,8 +193,9 @@ the optimization problems (these are overwritten by `constant_concentrations` if supplied). `T` and `R` can be specified in the corresponding units; defaults are K and -kJ/K/mol. The unit of metabolite concentrations is typically molar. As usual, -optimizer settings can be changed with `modifications`. +kJ/K/mol. The unit of metabolite concentrations is typically molar, and the ΔG⁰s +have units of kJ/mol. Other units can be used, as long as they are consistent. +As usual, optimizer settings can be changed with `modifications`. """ function max_min_driving_force_analysis( model::A.AbstractFBCModel, diff --git a/test/data_static.jl b/test/data_static.jl index 6c3988fd7..a0bcf8950 100644 --- a/test/data_static.jl +++ b/test/data_static.jl @@ -1,5 +1,5 @@ -const ecoli_core_gene_product_masses = Dict{String,Float64}( +const ecoli_core_gene_product_masses = Dict( #= Data downloaded from Uniprot for E. coli K12, gene mass in kDa. To obtain these data yourself, go to @@ -145,7 +145,7 @@ const ecoli_core_gene_product_masses = Dict{String,Float64}( "s0001" => 0.0, ) -const ecoli_core_reaction_kcats = Dict{String,Vector{Tuple{Float64,Float64}}}( +const ecoli_core_reaction_kcats = Dict( #= Data taken from Heckmann, David, et al. "Machine learning applied to enzyme turnover numbers reveals protein structural correlates and improves metabolic diff --git a/test/enzyme-model.jl b/test/enzyme_model.jl similarity index 100% rename from test/enzyme-model.jl rename to test/enzyme_model.jl diff --git a/test/runtests.jl b/test/runtests.jl index 300321206..10d3e7fdb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,7 +45,7 @@ run_test_file("data_downloaded.jl") @testset "COBREXA test suite" begin run_doc_examples() - include("enzyme-model.jl") + include("enzyme_model.jl") run_test_file("aqua.jl") end From 0ca63f186c6c8bb21c516e1a91a5df69285a6836 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 16:09:32 +0100 Subject: [PATCH 428/531] open isozyme type --- .../examples/05-enzyme-constrained-models.jl | 6 ++-- src/builders/enzymes.jl | 34 +++++++++++-------- src/types.jl | 27 +++++++++++---- 3 files changed, 43 insertions(+), 24 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 5ade6d726..943575177 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -40,15 +40,15 @@ model = load_model("e_coli_core.json") # isozymes that can catalyze a reaction. A turnover number needs to be assigned # to each isozyme, as shown below. -reaction_isozymes = Dict{String,Dict{String,Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. +reaction_isozymes = Dict{String,Dict{String,SimpleIsozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available haskey(ecoli_core_reaction_kcats, rid) || continue # skip if no kcat data available for (i, grr) in enumerate(grrs) - d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) + d = get!(reaction_isozymes, rid, Dict{String,SimpleIsozyme}()) # each isozyme gets a unique name - d["isozyme_"*string(i)] = Isozyme( # Isozyme struct is defined by COBREXA + d["isozyme_"*string(i)] = SimpleIsozyme( # SimpleIsozyme struct is defined by COBREXA gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes kcat_forward = ecoli_core_reaction_kcats[rid] * 3600.0, # forward reaction turnover number units = 1/h kcat_backward = ecoli_core_reaction_kcats[rid] * 3600.0, # reverse reaction turnover number units = 1/h diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 130f146e1..dfd92f5f2 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -19,8 +19,8 @@ fluxes through [`link_isozymes`](@ref). """ function isozyme_variables( reaction_id::String, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, -) + reaction_isozymes::Dict{String,Dict{String,T}}, +) where {T<:Isozyme} C.variables(; keys = Symbol.(collect(keys(reaction_isozymes[reaction_id]))), bounds = C.Between(0.0, Inf), @@ -56,8 +56,8 @@ function enzyme_stoichiometry( enzymes::C.ConstraintTree, fluxes_isozymes_forward::C.ConstraintTree, fluxes_isozymes_backward::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, -) + reaction_isozymes::Dict{String,Dict{String,T}}, +) where {T<:Isozyme} # map enzyme ids to reactions that use them (not all isozymes have to though) enzyme_rid_lookup = Dict{Symbol,Vector{Symbol}}() for (rid, isozymes) in reaction_isozymes @@ -109,9 +109,9 @@ function enzyme_balance( gid::Symbol, rid::Symbol, fluxes_isozymes::C.ConstraintTree, # direction - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + reaction_isozymes::Dict{String,Dict{String,T}}, direction = :kcat_forward, -) +) where {T<:Isozyme} isozyme_dict = Dict(Symbol(k) => v for (k, v) in reaction_isozymes[string(rid)]) sum( # this is where the stoichiometry comes in @@ -150,20 +150,26 @@ $(TYPEDSIGNATURES) Add enzyme constraints to a constraint tree, `m`. The enzyme model is parameterized by `reaction_isozymes`, which is a mapping of reaction IDs (those -used in the fluxes of the model) to named [`Isozyme`](@ref)s. Additionally, -`gene_molar_masses` and `capacity_limitations` should be supplied. The latter is -a vector of tuples, where each tuple represents a distinct bound as `(bound_id, -genes_in_bound, protein_mass_bound)`. Finally, specify the `fluxes` and -`enzymes` to which the constraints should be mounted. +used in the fluxes of the model) to named struct which is a subtype of +[`Isozyme`](@ref)s. Additionally, `gene_molar_masses` and `capacity_limitations` +should be supplied. The latter is a vector of tuples, where each tuple +represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. +Finally, specify the `fluxes` and `enzymes` to which the constraints should be +mounted. + +# Note +The isozyme struct used in `reaction_isozymes` must have fields +`gene_product_stoichiometry`, `kcat_forward`, and `kcat_backward` to properly +assign kcats to reactions. Use [`SimpleIsozyme`](@ref) when in doubt. """ function add_enzyme_constraints!( m::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + reaction_isozymes::Dict{String,Dict{String,T}}, gene_molar_masses::Dict{String,Float64}, capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; fluxes = m.fluxes, enzymes = m.enzymes, -) +) where {T<:Isozyme} # create directional fluxes m += @@ -247,7 +253,7 @@ constructing it manually by using [`add_enzyme_constraints!`](@ref). """ function enzyme_constrained_flux_balance_analysis( model::A.AbstractFBCModel, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + reaction_isozymes::Dict{String,Dict{String,SimpleIsozyme}}, gene_molar_masses::Dict{String,Float64}, capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; optimizer, diff --git a/src/types.jl b/src/types.jl index 850f96624..c578f9880 100644 --- a/src/types.jl +++ b/src/types.jl @@ -9,26 +9,39 @@ const Maybe{X} = Union{Nothing,X} """ $(TYPEDEF) -Information about isozyme composition including subunit stoichiometry and -turnover numbers. +Abstract isozyme type that stores all kinetic information required for +constraint-based modeling. + +""" +abstract type Isozyme end + +""" +$(TYPEDEF) + +A simple struct storing information about the isozyme composition, including +subunit stoichiometry and turnover numbers. # Fields $(TYPEDFIELDS) """ -Base.@kwdef mutable struct Isozyme +Base.@kwdef mutable struct SimpleIsozyme <: Isozyme gene_product_stoichiometry::Dict{String,Float64} kcat_forward::Maybe{Float64} = nothing kcat_backward::Maybe{Float64} = nothing end -export Isozyme +export SimpleIsozyme """ $(TYPEDSIGNATURES) -A convenience constructor for [`Isozyme`](@ref) that takes a string gene +A convenience constructor for [`SimpleIsozyme`](@ref) that takes a string gene reaction rule and converts it into the appropriate format. Assumes the `gene_product_stoichiometry` for each subunit is 1. """ -Isozyme(gids::Vector{String}; kwargs...) = - Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kwargs...) +SimpleIsozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = + SimpleIsozyme(; + gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), + kcat_forward, + kcat_backward, + ) From 1dea9edae36e3e315fb0fd3ae3b4e1bbb4ba9aaa Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 16:12:23 +0100 Subject: [PATCH 429/531] update test to use SimpleIsozyme --- test/enzyme_model.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/enzyme_model.jl b/test/enzyme_model.jl index dcbac32af..8f26f8c9c 100644 --- a/test/enzyme_model.jl +++ b/test/enzyme_model.jl @@ -51,12 +51,12 @@ model = A.CanonicalModel.Model(rxns, mets, gs) reaction_isozymes = Dict( - "r3" => Dict("iso1" => Isozyme(Dict("g1" => 1), 1.0, 1.0)), + "r3" => Dict("iso1" => SimpleIsozyme(Dict("g1" => 1), 1.0, 1.0)), "r4" => Dict( - "iso1" => Isozyme(Dict("g1" => 1), 2.0, 2.0), - "iso2" => Isozyme(Dict("g2" => 1), 3.0, 3.0), + "iso1" => SimpleIsozyme(Dict("g1" => 1), 2.0, 2.0), + "iso2" => SimpleIsozyme(Dict("g2" => 1), 3.0, 3.0), ), - "r5" => Dict("iso1" => Isozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)), + "r5" => Dict("iso1" => SimpleIsozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)), ) gene_product_molar_mass = From 11b52eec1dca55be24ef9601db2e6871f488e87a Mon Sep 17 00:00:00 2001 From: stelmo Date: Wed, 27 Dec 2023 15:14:25 +0000 Subject: [PATCH 430/531] automatic formatting triggered by @stelmo on PR #805 --- docs/src/examples/06-thermodynamic-models.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 7b5f225fe..824be399b 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -129,7 +129,7 @@ m *= m.log_metabolite_concentrations, ) -# solve the model, which is not just a normal constraint tree! +# solve the model, which is not just a normal constraint tree! mmdf_solution = optimized_constraints( m; objective = m.max_min_driving_force.value, From 538e1f405cc5c0eb41cf11613759eabc708217a6 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 14:01:27 +0100 Subject: [PATCH 431/531] update CT compat --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 58a3260fe..f598b46c7 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.7" +ConstraintTrees = "0.8" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" From 537efcd92613c34f8dc6e96cd9ae2495ae354efc Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 17 Dec 2023 12:26:48 +0100 Subject: [PATCH 432/531] copy loopless from v1 --- src/builders/loopless.jl | 57 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 src/builders/loopless.jl diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl new file mode 100644 index 000000000..b7578ac74 --- /dev/null +++ b/src/builders/loopless.jl @@ -0,0 +1,57 @@ +""" +$(TYPEDSIGNATURES) + +Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically +infeasible internal cycles can occur. Adds the following constraints to the problem: +``` +-max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ +-max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ +Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) +Nᵢₙₜ' × G = 0 +yᵢ ∈ {0, 1} +Gᵢ ∈ ℝ +i ∈ internal reactions +Nᵢₙₜ is the nullspace of the internal stoichiometric matrix +``` +Note, this modification introduces binary variables, so an optimization solver capable of +handing mixed integer problems needs to be used. The arguments `max_flux_bound` and +`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. + +For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination +of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical +journal, 2011`. +""" +add_loopless_constraints(; + max_flux_bound = _constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = _constants.loopless_strict_inequality_tolerance, +) = + (model, opt_model) -> begin + + internal_rxn_idxs = [ + ridx for (ridx, rid) in enumerate(reactions(model)) if + !is_boundary(reaction_stoichiometry(model, rid)) + ] + + N_int = nullspace(Array(stoichiometry(model)[:, internal_rxn_idxs])) # no sparse nullspace function + + y = @variable(opt_model, y[1:length(internal_rxn_idxs)], Bin) + G = @variable(opt_model, G[1:length(internal_rxn_idxs)]) # approx ΔG for internal reactions + + x = opt_model[:x] + for (cidx, ridx) in enumerate(internal_rxn_idxs) + @constraint(opt_model, -max_flux_bound * (1 - y[cidx]) <= x[ridx]) + @constraint(opt_model, x[ridx] <= max_flux_bound * y[cidx]) + + @constraint( + opt_model, + -max_flux_bound * y[cidx] + strict_inequality_tolerance * (1 - y[cidx]) <= G[cidx] + ) + @constraint( + opt_model, + G[cidx] <= + -strict_inequality_tolerance * y[cidx] + max_flux_bound * (1 - y[cidx]) + ) + end + + @constraint(opt_model, N_int' * G .== 0) + end From dd760824ff00518579ffc32f925f486aad20eea5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Sun, 17 Dec 2023 20:02:07 +0100 Subject: [PATCH 433/531] getting trivial solution :/ --- docs/src/examples/07-loopless-models.jl | 42 +++++++++ src/COBREXA.jl | 3 + src/analysis/loopless_flux_balance.jl | 0 src/builders/loopless.jl | 110 ++++++++++++++++-------- src/misc/utils.jl | 13 +++ 5 files changed, 134 insertions(+), 34 deletions(-) create mode 100644 docs/src/examples/07-loopless-models.jl create mode 100644 src/analysis/loopless_flux_balance.jl create mode 100644 src/misc/utils.jl diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl new file mode 100644 index 000000000..6f515a400 --- /dev/null +++ b/docs/src/examples/07-loopless-models.jl @@ -0,0 +1,42 @@ + +# # Loopless flux balance analysis (ll-FBA) + +# Here we wil add loopless constraints to a flux balance model to ensure that +# the resultant solution is thermodynamically consistent. As before, we will use +# the core *E. coli* model, which we can download using +# [`download_model`](@ref): + +using COBREXA + +download_model( + "http://bigg.ucsd.edu/static/models/e_coli_core.json", + "e_coli_core.json", + "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", +) + +# Additionally to COBREXA and the model format package, we will need a solver -- +# let's use GLPK here because we will need to solve mixed interger linear +# programs (MILPs): + +import JSONFBCModels +import GLPK +import AbstractFBCModels as A + +model = load_model("e_coli_core.json") + +# ## Setting up the loopless model + +# ## Running a ll-FBA + +sol = loopless_flux_balance_analysis( + model; + optimizer = GLPK.Optimizer, + max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = 1.0, # heuristic from paper + modifications = [set_optimizer_attribute("tol_int", 1e-9)] +) + +sol.pseudo_gibbs_free_energy_reaction + +@test isapprox(mmdf_solution.max_min_driving_force, 0.8739215069684292, atol = TEST_TOLERANCE) #src + diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 0b853fe62..c9d73692d 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -25,6 +25,7 @@ import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J import SparseArrays: sparse, findnz +import LinearAlgebra: nullspace include("types.jl") include("io.jl") @@ -35,6 +36,7 @@ include("builders/genes.jl") include("builders/objectives.jl") include("builders/enzymes.jl") include("builders/thermodynamic.jl") +include("builders/loopless.jl") include("analysis/modifications.jl") include("analysis/flux_balance.jl") @@ -42,5 +44,6 @@ include("analysis/parsimonious_flux_balance.jl") include("analysis/minimal_metabolic_adjustment.jl") include("misc/bounds.jl") +include("misc/utils.jl") end # module COBREXA diff --git a/src/analysis/loopless_flux_balance.jl b/src/analysis/loopless_flux_balance.jl new file mode 100644 index 000000000..e69de29bb diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index b7578ac74..a9d0a4112 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -1,3 +1,13 @@ + +""" +$(TYPEDSIGNATURES) +""" +function add_loopless_constraints!() + +end + +export add_loopless_constraints! + """ $(TYPEDSIGNATURES) @@ -21,37 +31,69 @@ For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical journal, 2011`. """ -add_loopless_constraints(; - max_flux_bound = _constants.default_reaction_bound, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = _constants.loopless_strict_inequality_tolerance, -) = - (model, opt_model) -> begin - - internal_rxn_idxs = [ - ridx for (ridx, rid) in enumerate(reactions(model)) if - !is_boundary(reaction_stoichiometry(model, rid)) - ] - - N_int = nullspace(Array(stoichiometry(model)[:, internal_rxn_idxs])) # no sparse nullspace function - - y = @variable(opt_model, y[1:length(internal_rxn_idxs)], Bin) - G = @variable(opt_model, G[1:length(internal_rxn_idxs)]) # approx ΔG for internal reactions - - x = opt_model[:x] - for (cidx, ridx) in enumerate(internal_rxn_idxs) - @constraint(opt_model, -max_flux_bound * (1 - y[cidx]) <= x[ridx]) - @constraint(opt_model, x[ridx] <= max_flux_bound * y[cidx]) - - @constraint( - opt_model, - -max_flux_bound * y[cidx] + strict_inequality_tolerance * (1 - y[cidx]) <= G[cidx] - ) - @constraint( - opt_model, - G[cidx] <= - -strict_inequality_tolerance * y[cidx] + max_flux_bound * (1 - y[cidx]) - ) - end - - @constraint(opt_model, N_int' * G .== 0) - end +function loopless_flux_balance_analysis( + model; + max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = 10.0, # heuristic from paper + modifications=[], + optimizer, +) + + m = fbc_model_constraints(model) + + # find all internal reactions + internal_reactions = [(i, Symbol(rid)) for (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid)] + internal_reaction_ids = last.(internal_reactions) + internal_reaction_idxs = first.(internal_reactions) + + # add loopless variables: flux direction setters (binary) and pseudo gibbs free energy of reaction + m += :loopless_binary_variables^C.variables(keys=internal_reaction_ids, bounds=Ref(C.Binary)) + m += :pseudo_gibbs_free_energy_reaction^C.variables(keys=internal_reaction_ids) + + # add -1000 * (1-a) ≤ v ≤ 1000 * a which need to be split into forward and backward components + m *= :loopless_reaction_directions^:backward^C.ConstraintTree( + rid => C.Constraint( + value = m.fluxes[rid].value + max_flux_bound * (1 - m.loopless_binary_variables[rid].value), + bound = (0, Inf), + ) for rid in internal_reaction_ids + ) + m *= :loopless_reaction_directions^:forward^C.ConstraintTree( + rid => C.Constraint( + value = max_flux_bound * m.loopless_binary_variables[rid].value - m.fluxes[rid].value , + bound = (0, Inf), + ) for rid in internal_reaction_ids + ) + + # add -1000*a + 1 * (1-a) ≤ Gibbs ≤ -1 * a + 1000 * (1 - a) which also need to be split + m *= :loopless_pseudo_gibbs_sign^:backward^C.ConstraintTree( + rid => C.Constraint( + value = m.pseudo_gibbs_free_energy_reaction[rid].value + max_flux_bound * m.loopless_binary_variables[rid].value - strict_inequality_tolerance * (1 - m.loopless_binary_variables[rid].value), + bound = (0, Inf), + ) for rid in internal_reaction_ids + ) + m *= :loopless_pseudo_gibbs_sign^:forward^C.ConstraintTree( + rid => C.Constraint( + value = -strict_inequality_tolerance * m.loopless_binary_variables[rid].value + max_flux_bound * (1 - m.loopless_binary_variables[rid].value) - m.pseudo_gibbs_free_energy_reaction[rid].value, + bound = (0, Inf), + ) for rid in internal_reaction_ids + ) + + # add N_int' * Gibbs = 0 where N_int = nullspace + N_int = nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])) # no sparse nullspace function + m *= :loopless_condition^C.ConstraintTree( + Symbol(:nullspace_vector, i) => C.Constraint( + value = sum(col[j] * m.pseudo_gibbs_free_energy_reaction[internal_reaction_ids[j]].value for j in eachindex(col)), + bound = 0, + ) for (i, col) in enumerate(eachcol(N_int)) + ) + + # solve + optimized_constraints( + m; + objective = m.objective.value, + optimizer, + modifications, + ) +end + +export loopless_flux_balance_analysis diff --git a/src/misc/utils.jl b/src/misc/utils.jl new file mode 100644 index 000000000..a1a7e0bf6 --- /dev/null +++ b/src/misc/utils.jl @@ -0,0 +1,13 @@ + + +""" +$(TYPEDSIGNATURES) + +Return true if the reaction denoted by `rxn_id` in `model` is a boundary +reaction, otherwise return false. Checks if on boundary by inspecting the number +of metabolites in the reaction stoichiometry. Boundary reactions have only one +metabolite, e.g. an exchange reaction, or a sink/demand reaction. +""" +is_boundary(model::A.AbstractFBCModel, rxn_id::String) = length(keys(A.reaction_stoichiometry(model, rxn_id))) == 1 + +export is_boundary From fbc9e33fd810a4ca9adb21882a9d1ac7a09ab564 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 17:46:27 +0100 Subject: [PATCH 434/531] working loopless --- docs/src/examples/07-loopless-models.jl | 37 +++++---- src/analysis/loopless_flux_balance.jl | 1 + src/builders/loopless.jl | 101 +++++++++++++++--------- src/misc/utils.jl | 3 +- src/solver.jl | 3 + src/types.jl | 11 +++ 6 files changed, 104 insertions(+), 52 deletions(-) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 6f515a400..cff4c69f4 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -14,9 +14,8 @@ download_model( "7bedec10576cfe935b19218dc881f3fb14f890a1871448fc19a9b4ee15b448d8", ) -# Additionally to COBREXA and the model format package, we will need a solver -- -# let's use GLPK here because we will need to solve mixed interger linear -# programs (MILPs): +# Additionally to COBREXA and the JSON model format package. We will also need a +# solver that can solve mixed interger linear programs like GLPK. import JSONFBCModels import GLPK @@ -24,19 +23,29 @@ import AbstractFBCModels as A model = load_model("e_coli_core.json") -# ## Setting up the loopless model +# ## Running a simple loopless FBA (ll-FBA) -# ## Running a ll-FBA +# One can directly use `loopless_flux_balance_analysis` to solve an FBA problem +# based on `model` where loopless constraints are added to all fluxes. This is +# the direct approach. -sol = loopless_flux_balance_analysis( - model; - optimizer = GLPK.Optimizer, - max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = 1.0, # heuristic from paper - modifications = [set_optimizer_attribute("tol_int", 1e-9)] -) +sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) + +@test isapprox( + sol.objective, + 0.8739215069684303, + atol = TEST_TOLERANCE, +) #src -sol.pseudo_gibbs_free_energy_reaction +@test all( + v * sol.pseudo_gibbs_free_energy_reaction[k] <= 0 + for (k, v) in sol.fluxes if haskey(sol.pseudo_gibbs_free_energy_reaction, k) +) #src + +Dict( + k => (v, sol.pseudo_gibbs_free_energy_reaction[k], sol.loopless_binary_variables[k]) +for (k, v) in sol.fluxes if abs(v) > 0 && haskey(sol.pseudo_gibbs_free_energy_reaction, k) +) -@test isapprox(mmdf_solution.max_min_driving_force, 0.8739215069684292, atol = TEST_TOLERANCE) #src +# ## Building your own loopless model diff --git a/src/analysis/loopless_flux_balance.jl b/src/analysis/loopless_flux_balance.jl index e69de29bb..8b1378917 100644 --- a/src/analysis/loopless_flux_balance.jl +++ b/src/analysis/loopless_flux_balance.jl @@ -0,0 +1 @@ + diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index a9d0a4112..6aae8cb63 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -34,66 +34,93 @@ journal, 2011`. function loopless_flux_balance_analysis( model; max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = 10.0, # heuristic from paper - modifications=[], + strict_inequality_tolerance = 1.0, # heuristic from paper + modifications = [], optimizer, ) - - m = fbc_model_constraints(model) - # find all internal reactions - internal_reactions = [(i, Symbol(rid)) for (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid)] - internal_reaction_ids = last.(internal_reactions) - internal_reaction_idxs = first.(internal_reactions) + m = fbc_model_constraints(model) - # add loopless variables: flux direction setters (binary) and pseudo gibbs free energy of reaction - m += :loopless_binary_variables^C.variables(keys=internal_reaction_ids, bounds=Ref(C.Binary)) - m += :pseudo_gibbs_free_energy_reaction^C.variables(keys=internal_reaction_ids) + # find all internal reactions + internal_reactions = [ + (i, Symbol(rid)) for + (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) + ] + internal_reaction_ids = last.(internal_reactions) + internal_reaction_idxs = first.(internal_reactions) - # add -1000 * (1-a) ≤ v ≤ 1000 * a which need to be split into forward and backward components - m *= :loopless_reaction_directions^:backward^C.ConstraintTree( + # add loopless variables: flux direction setters (binary) and pseudo gibbs free energy of reaction + m += + :loopless_binary_variables^C.variables( + keys = internal_reaction_ids, + bounds = Binary(), + ) + m += + :pseudo_gibbs_free_energy_reaction^C.variables( + keys = internal_reaction_ids, + bounds = C.Between(-Inf, Inf), + ) + + # add -1000 * (1-a) ≤ v ≤ 1000 * a which need to be split into forward and backward components + # -1000 * (1-a) - v ≤ 0 (backward) + # v - 1000a ≤ 0 (forward) + m *= + :loopless_reaction_directions^:backward^C.ConstraintTree( rid => C.Constraint( - value = m.fluxes[rid].value + max_flux_bound * (1 - m.loopless_binary_variables[rid].value), - bound = (0, Inf), + value = -max_flux_bound * (1 - m.loopless_binary_variables[rid].value) - + m.fluxes[rid].value, + bound = C.Between(Inf, 0), ) for rid in internal_reaction_ids ) - m *= :loopless_reaction_directions^:forward^C.ConstraintTree( + m *= + :loopless_reaction_directions^:forward^C.ConstraintTree( rid => C.Constraint( - value = max_flux_bound * m.loopless_binary_variables[rid].value - m.fluxes[rid].value , - bound = (0, Inf), + value = m.fluxes[rid].value - + max_flux_bound * m.loopless_binary_variables[rid].value, + bound = C.Between(-Inf, 0), ) for rid in internal_reaction_ids ) - # add -1000*a + 1 * (1-a) ≤ Gibbs ≤ -1 * a + 1000 * (1 - a) which also need to be split - m *= :loopless_pseudo_gibbs_sign^:backward^C.ConstraintTree( + # add -1000*a + 1 * (1-a) ≤ Gibbs ≤ -1 * a + 1000 * (1 - a) which also need to be split + # -1000 * a + 1 * (1-a) - G ≤ 0 (backward) + # G + 1 * a - 1000 * (1-a) ≤ 0 (forward) + m *= + :loopless_pseudo_gibbs_sign^:backward^C.ConstraintTree( rid => C.Constraint( - value = m.pseudo_gibbs_free_energy_reaction[rid].value + max_flux_bound * m.loopless_binary_variables[rid].value - strict_inequality_tolerance * (1 - m.loopless_binary_variables[rid].value), - bound = (0, Inf), + value = -max_flux_bound * m.loopless_binary_variables[rid].value + + strict_inequality_tolerance * + (1 - m.loopless_binary_variables[rid].value) - + m.pseudo_gibbs_free_energy_reaction[rid].value, + bound = (-Inf, 0), ) for rid in internal_reaction_ids ) - m *= :loopless_pseudo_gibbs_sign^:forward^C.ConstraintTree( + m *= + :loopless_pseudo_gibbs_sign^:forward^C.ConstraintTree( rid => C.Constraint( - value = -strict_inequality_tolerance * m.loopless_binary_variables[rid].value + max_flux_bound * (1 - m.loopless_binary_variables[rid].value) - m.pseudo_gibbs_free_energy_reaction[rid].value, - bound = (0, Inf), + value = m.pseudo_gibbs_free_energy_reaction[rid].value + + strict_inequality_tolerance * + m.loopless_binary_variables[rid].value - + max_flux_bound * (1 - m.loopless_binary_variables[rid].value), + bound = (-Inf, 0), ) for rid in internal_reaction_ids ) - # add N_int' * Gibbs = 0 where N_int = nullspace - N_int = nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])) # no sparse nullspace function - m *= :loopless_condition^C.ConstraintTree( + # add N_int' * Gibbs = 0 where N_int = nullspace + N_int = nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])) # no sparse nullspace function + m *= + :loopless_condition^C.ConstraintTree( Symbol(:nullspace_vector, i) => C.Constraint( - value = sum(col[j] * m.pseudo_gibbs_free_energy_reaction[internal_reaction_ids[j]].value for j in eachindex(col)), - bound = 0, + value = sum( + col[j] * + m.pseudo_gibbs_free_energy_reaction[internal_reaction_ids[j]].value + for j in eachindex(col) + ), + bound = C.EqualTo(0), ) for (i, col) in enumerate(eachcol(N_int)) ) - # solve - optimized_constraints( - m; - objective = m.objective.value, - optimizer, - modifications, - ) + # solve + optimized_constraints(m; objective = m.objective.value, optimizer, modifications) end export loopless_flux_balance_analysis diff --git a/src/misc/utils.jl b/src/misc/utils.jl index a1a7e0bf6..7c559b045 100644 --- a/src/misc/utils.jl +++ b/src/misc/utils.jl @@ -8,6 +8,7 @@ reaction, otherwise return false. Checks if on boundary by inspecting the number of metabolites in the reaction stoichiometry. Boundary reactions have only one metabolite, e.g. an exchange reaction, or a sink/demand reaction. """ -is_boundary(model::A.AbstractFBCModel, rxn_id::String) = length(keys(A.reaction_stoichiometry(model, rxn_id))) == 1 +is_boundary(model::A.AbstractFBCModel, rxn_id::String) = + length(keys(A.reaction_stoichiometry(model, rxn_id))) == 1 export is_boundary diff --git a/src/solver.jl b/src/solver.jl index 57b3bf787..91ba4a171 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -25,6 +25,9 @@ function optimization_model( val = C.substitute(c.value, x) isinf(c.bound.lower) || J.@constraint(model, val >= c.bound.lower) isinf(c.bound.upper) || J.@constraint(model, val <= c.bound.upper) + elseif c.bound isa Binary + anon_bool = J.@variable(model, binary = true) + J.@constraint(model, C.substitute(c.value, x) == anon_bool) end end function add_constraint(c::C.ConstraintTree) diff --git a/src/types.jl b/src/types.jl index c578f9880..679d6b9e9 100644 --- a/src/types.jl +++ b/src/types.jl @@ -45,3 +45,14 @@ SimpleIsozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float6 kcat_forward, kcat_backward, ) + +""" +$(TYPEDEF) + +Representation of a binary bound, i.e. constrain a variable to only take the +value 0 or 1 exclusively. Requires a mixed integer-capable solver for +optimization. +""" +struct Binary <: C.Bound end + +export Binary From b3fb7628f7c872677b31e3d51e78e30c58c1b440 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 19:46:41 +0100 Subject: [PATCH 435/531] finish example and split out build function --- docs/src/examples/07-loopless-models.jl | 45 ++++++--- src/builders/loopless.jl | 123 +++++++++++++++--------- 2 files changed, 113 insertions(+), 55 deletions(-) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index cff4c69f4..b84c03208 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -31,21 +31,42 @@ model = load_model("e_coli_core.json") sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) -@test isapprox( - sol.objective, - 0.8739215069684303, - atol = TEST_TOLERANCE, -) #src +@test isapprox(sol.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src @test all( - v * sol.pseudo_gibbs_free_energy_reaction[k] <= 0 - for (k, v) in sol.fluxes if haskey(sol.pseudo_gibbs_free_energy_reaction, k) + v * sol.pseudo_gibbs_free_energy_reaction[k] <= -TEST_TOLERANCE for + (k, v) in sol.fluxes if + haskey(sol.pseudo_gibbs_free_energy_reaction, k) && abs(v) >= 1e-6 ) #src -Dict( - k => (v, sol.pseudo_gibbs_free_energy_reaction[k], sol.loopless_binary_variables[k]) -for (k, v) in sol.fluxes if abs(v) > 0 && haskey(sol.pseudo_gibbs_free_energy_reaction, k) -) - # ## Building your own loopless model +# ConstraintTrees allows one to add loopless constraints to any model. To +# illustrate how one would add loopless constraints to an arbitrary model (and +# not use the convenience function), let's build a loopless model from scratch. + +# First, build a normal flux balance model +m = fbc_model_constraints(model) + +# Next, find all internal reactions, and their associated indices for use later +internal_reactions = [ + (i, Symbol(rid)) for + (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) +] +internal_reaction_ids = last.(internal_reactions) +internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below + +# Construct the stoichiometric nullspace of the internal reactions +internal_reaction_stoichiometry_nullspace_columns = + eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) + +# And simply add loopless contraints on the fluxes of the model +m = add_loopless_constraints!( + m, + internal_reaction_ids, + internal_reaction_stoichiometry_nullspace_columns; + fluxes = m.fluxes, +) + +# Now the model can be solved as before! +optimized_constraints(m; objective = m.objective.value, optimizer = GLPK.Optimizer) diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 6aae8cb63..e2c0bbcc2 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -1,54 +1,35 @@ """ $(TYPEDSIGNATURES) -""" -function add_loopless_constraints!() - -end -export add_loopless_constraints! +Add loopless constraints to the model, `m`. Specify the internal reactions with +`internal_reaction_ids`, as well as the columns of the stoichiometric nullspace +of these reactions in `internal_reaction_stoichiometry_nullspace_columns`. See +the example below for more information about the nullspace. By default, the +`fluxes` of the model `m` are made loopless. -""" -$(TYPEDSIGNATURES) +The Big-M method is used to ensure the sign of fluxes and pseudo Gibbs free +energy of reactions match. For this, ensure that `max_flux_bound` is at least +one order of magnitude bigger than the largest expected absolute flux value. +Additionally, ensure that `strict_inequality_tolerance` is smaller than any +expected pseudo Gibbs free energy of reaction value. The defaults work well for +most problems. -Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically -infeasible internal cycles can occur. Adds the following constraints to the problem: +# Example ``` --max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ --max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ -Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) -Nᵢₙₜ' × G = 0 -yᵢ ∈ {0, 1} -Gᵢ ∈ ℝ -i ∈ internal reactions -Nᵢₙₜ is the nullspace of the internal stoichiometric matrix +internal_reaction_stoichiometry_nullspace_columns = + eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_rxn_idxs_in_order_of_internal_rxn_ids]))) ``` -Note, this modification introduces binary variables, so an optimization solver capable of -handing mixed integer problems needs to be used. The arguments `max_flux_bound` and -`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. - -For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination -of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical -journal, 2011`. """ -function loopless_flux_balance_analysis( - model; +function add_loopless_constraints!( + m, + internal_reaction_ids, + internal_reaction_stoichiometry_nullspace_columns; + fluxes = m.fluxes, max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic strict_inequality_tolerance = 1.0, # heuristic from paper - modifications = [], - optimizer, ) - m = fbc_model_constraints(model) - - # find all internal reactions - internal_reactions = [ - (i, Symbol(rid)) for - (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) - ] - internal_reaction_ids = last.(internal_reactions) - internal_reaction_idxs = first.(internal_reactions) - # add loopless variables: flux direction setters (binary) and pseudo gibbs free energy of reaction m += :loopless_binary_variables^C.variables( @@ -68,14 +49,14 @@ function loopless_flux_balance_analysis( :loopless_reaction_directions^:backward^C.ConstraintTree( rid => C.Constraint( value = -max_flux_bound * (1 - m.loopless_binary_variables[rid].value) - - m.fluxes[rid].value, + fluxes[rid].value, bound = C.Between(Inf, 0), ) for rid in internal_reaction_ids ) m *= :loopless_reaction_directions^:forward^C.ConstraintTree( rid => C.Constraint( - value = m.fluxes[rid].value - + value = fluxes[rid].value - max_flux_bound * m.loopless_binary_variables[rid].value, bound = C.Between(-Inf, 0), ) for rid in internal_reaction_ids @@ -105,8 +86,7 @@ function loopless_flux_balance_analysis( ) for rid in internal_reaction_ids ) - # add N_int' * Gibbs = 0 where N_int = nullspace - N_int = nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])) # no sparse nullspace function + # use nullspace to ensure there are no loops m *= :loopless_condition^C.ConstraintTree( Symbol(:nullspace_vector, i) => C.Constraint( @@ -116,9 +96,66 @@ function loopless_flux_balance_analysis( for j in eachindex(col) ), bound = C.EqualTo(0), - ) for (i, col) in enumerate(eachcol(N_int)) + ) for (i, col) in enumerate(internal_reaction_stoichiometry_nullspace_columns) ) + m +end + +export add_loopless_constraints! + +""" +$(TYPEDSIGNATURES) + +Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically +infeasible internal cycles can occur. Adds the following constraints to the problem: +``` +-max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ +-max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ +Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) +Nᵢₙₜ' × G = 0 +yᵢ ∈ {0, 1} +Gᵢ ∈ ℝ +i ∈ internal reactions +Nᵢₙₜ is the nullspace of the internal stoichiometric matrix +``` +Note, this modification introduces binary variables, so an optimization solver capable of +handing mixed integer problems needs to be used. The arguments `max_flux_bound` and +`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. + +For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination +of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical +journal, 2011`. +""" +function loopless_flux_balance_analysis( + model; + max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = 1.0, # heuristic from paper + modifications = [], + optimizer, +) + + m = fbc_model_constraints(model) + + # find all internal reactions + internal_reactions = [ + (i, Symbol(rid)) for + (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) + ] + internal_reaction_ids = last.(internal_reactions) + internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below + + internal_reaction_stoichiometry_nullspace_columns = + eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) # no sparse nullspace function + + m = add_loopless_constraints!( + m, + internal_reaction_ids, + internal_reaction_stoichiometry_nullspace_columns; + max_flux_bound, + strict_inequality_tolerance, + ) + # solve optimized_constraints(m; objective = m.objective.value, optimizer, modifications) end From 52d5257e96cd87fb56333edf1af298659546a0e5 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 19:49:55 +0100 Subject: [PATCH 436/531] import LinAlg --- docs/src/examples/07-loopless-models.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index b84c03208..f016844e6 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -57,6 +57,8 @@ internal_reaction_ids = last.(internal_reactions) internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below # Construct the stoichiometric nullspace of the internal reactions +import LinearAlgebra: nullspace + internal_reaction_stoichiometry_nullspace_columns = eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) From 69408138e8d2b74b1bedcdc98f12d70b2a66c8b4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 20 Dec 2023 11:00:39 +0100 Subject: [PATCH 437/531] start example --- docs/src/examples/08-community-models.jl | 25 ++++++++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 docs/src/examples/08-community-models.jl diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl new file mode 100644 index 000000000..0b434d2aa --- /dev/null +++ b/docs/src/examples/08-community-models.jl @@ -0,0 +1,25 @@ +# # Enzyme constrained models + +using COBREXA + +# Here we will construct an enzyme constrained variant of the *E. coli* "core" +# model. We will need the model, which we can download if it is not already present. + +import Downloads: download + +!isfile("e_coli_core.json") && + download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") + +# Additionally to COBREXA and the model format package, we will need a solver +# -- let's use Tulip here: + +import JSONFBCModels +import Tulip + +model = load_model("e_coli_core.json") + +# Enzyme constrained models require parameters that are usually not used by +# conventional constraint based models. These include reaction specific turnover +# numbers, molar masses of enzymes, and capacity bounds. + +import AbstractFBCModels as A From 7b130708c9e4f9410c42581eacc0aa824c449693 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 21 Dec 2023 17:36:29 +0100 Subject: [PATCH 438/531] working community --- docs/src/examples/08-community-models.jl | 48 ++++++++++++++++++++++ src/COBREXA.jl | 1 + src/builders/communities.jl | 51 ++++++++++++++++++++++++ 3 files changed, 100 insertions(+) create mode 100644 src/builders/communities.jl diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 0b434d2aa..1594d71e6 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -23,3 +23,51 @@ model = load_model("e_coli_core.json") # numbers, molar masses of enzymes, and capacity bounds. import AbstractFBCModels as A + +m1 = fbc_model_constraints(model) +m2 = fbc_model_constraints(model) + +lbs, ubs = A.bounds(model) +env_ex_rxns = Dict(rid => (lbs[i], ubs[i]) for (i, rid) in enumerate(A.reactions(model)) if startswith(rid, "EX_")) + + +m = build_community_environment(env_ex_rxns) +m += :bug1^m1 +m += :bug2^m2 + +member_abundances = [(:bug1, 0.2), (:bug2, 0.8)] +m *= + :environmental_exchange_balances^link_environmental_exchanges( + m, + [(:bug1, 0.2), (:bug2, 0.8)], + ) + +m *= + :equal_growth_rate_constraint^equal_growth_rate_constraints( + [(:bug1, m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), (:bug2, m.bug2.fluxes.:BIOMASS_Ecoli_core_w_GAM.value)] + ) + +m.bug1.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) +m.bug2.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) +m.bug1.fluxes.CYTBD.bound = (-10.0, 10.0) # respiration limited + +m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value) + +using Gurobi + +sol = optimized_constraints(m; objective = m.objective.value, optimizer=Gurobi.Optimizer) + +Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) +Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) + +# exchange cytosolic metabolites +mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e] +for met in mets + m.bug1.fluxes[met].bound = (-1000.0, 1000.0) + m.bug2.fluxes[met].bound = (-1000.0, 1000.0) +end + +sol = optimized_constraints(m; objective = m.objective.value, optimizer=Tulip.Optimizer, modifications=[set_optimizer_attribute("IPM_IterationsLimit", 100000)]) + +Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) +Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index c9d73692d..ce9bd2493 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -37,6 +37,7 @@ include("builders/objectives.jl") include("builders/enzymes.jl") include("builders/thermodynamic.jl") include("builders/loopless.jl") +include("builders/communities.jl") include("analysis/modifications.jl") include("analysis/flux_balance.jl") diff --git a/src/builders/communities.jl b/src/builders/communities.jl new file mode 100644 index 000000000..686fe2797 --- /dev/null +++ b/src/builders/communities.jl @@ -0,0 +1,51 @@ +""" +$(TYPEDSIGNATURES) + +Helper function to create environmental exchange rections. +""" +function environment_exchange_variables(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) + rids = collect(keys(env_ex_rxns)) + lbs_ubs = collect(values(env_ex_rxns)) + C.variables(; keys = Symbol.(rids), bounds = lbs_ubs) +end + +export environment_exchange_variables + +function build_community_environment(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) + C.ConstraintTree( + :environmental_exchange_reactions => environment_exchange_variables(env_ex_rxns), + ) +end + +export build_community_environment + +function link_environmental_exchanges( + m::C.ConstraintTree, + member_abundances::Vector{Tuple{Symbol,Float64}}; + on = m.:environmental_exchange_reactions, + member_fluxes_id = :fluxes, +) + C.ConstraintTree( + rid => C.Constraint( + value = -rxn.value + sum( + abundance * m[member][member_fluxes_id][rid].value for + (member, abundance) in member_abundances if + haskey(m[member][member_fluxes_id], rid); + init = zero(C.LinearValue), + ), + bound = 0.0, + ) for (rid, rxn) in on + ) +end + +export link_environmental_exchanges + +function equal_growth_rate_constraints(member_biomasses::Vector{Tuple{Symbol,C.LinearValue}}) + C.ConstraintTree( + Symbol(bid1, :_, bid2) => C.Constraint(value = bval1 - bval2, bound = 0.0) for + ((bid1, bval1), (bid2, bval2)) in + zip(member_biomasses[1:end-1], member_biomasses[2:end]) + ) +end + +export equal_growth_rate_constraints From 699780c9ed6d5e3b21a7e421b8356d7425548116 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 21 Dec 2023 18:35:52 +0100 Subject: [PATCH 439/531] improve docs and docstrings --- docs/src/examples/08-community-models.jl | 130 +++++++++++++++++------ src/builders/communities.jl | 20 +++- 2 files changed, 117 insertions(+), 33 deletions(-) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 1594d71e6..88e8016cb 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -1,9 +1,10 @@ -# # Enzyme constrained models +# # Community FBA models using COBREXA -# Here we will construct an enzyme constrained variant of the *E. coli* "core" -# model. We will need the model, which we can download if it is not already present. +# Here we will construct a community FBA model of two *E. coli* "core" models +# that can interact by exchanging selected metabolites. To do this, we will need +# the model, which we can download if it is not already present. import Downloads: download @@ -15,59 +16,124 @@ import Downloads: download import JSONFBCModels import Tulip +import AbstractFBCModels as A +import ConstraintTrees as C model = load_model("e_coli_core.json") -# Enzyme constrained models require parameters that are usually not used by -# conventional constraint based models. These include reaction specific turnover -# numbers, molar masses of enzymes, and capacity bounds. +# Community models work by joining its members together through their exchange +# reactions, weighted by the abundance of each microbe. These exchange reactions +# are then linked to an environmental exchange. For more theoretical details, +# see "Gottstein, et al, 2016, Constraint-based stoichiometric modelling from +# single organisms to microbial communities, Journal of the Royal Society +# Interface". -import AbstractFBCModels as A +# ## Building a community of two *E. coli*s + +# Here we will construct a simple community of two interacting microbes. To do +# this, we need to import the models. We import the models are ConstraintTrees, +# because it is easier to build the model explicitly than rely on an opaque +# one-shot function. -m1 = fbc_model_constraints(model) -m2 = fbc_model_constraints(model) +ecoli1 = fbc_model_constraints(model) +ecoli2 = fbc_model_constraints(model) +# Since the models are joined through their individual exchange reactions to an +# environmental exchange reactionq, we need to identify all possible exchange +# reactions in the community. Since the models are the same, this is +# straightforward here. Additionally, we need to specify the upper and lower +# bounds of these environmental exchange reactions. lbs, ubs = A.bounds(model) -env_ex_rxns = Dict(rid => (lbs[i], ubs[i]) for (i, rid) in enumerate(A.reactions(model)) if startswith(rid, "EX_")) +env_ex_rxns = Dict( + rid => (lbs[i], ubs[i]) for + (i, rid) in enumerate(A.reactions(model)) if startswith(rid, "EX_") +) + +# Now we simply create an blank model that only includes environmental exchange reactions. m = build_community_environment(env_ex_rxns) -m += :bug1^m1 -m += :bug2^m2 +# Next we join each member microbe to the model. +m += :bug1^ecoli1 +m += :bug2^ecoli2 + +# We also need to specify the abundances of each member, as this weights the +# flux of each metabolite each member microbe can share with other members or +# the environment. member_abundances = [(:bug1, 0.2), (:bug2, 0.8)] -m *= - :environmental_exchange_balances^link_environmental_exchanges( - m, - [(:bug1, 0.2), (:bug2, 0.8)], - ) +m *= :environmental_exchange_balances^link_environmental_exchanges(m, member_abundances) + +# Finally, the most sensible community FBA simulation involves assuming the +# growth rate of the models is the same. In this case, we simply set the growth +# rate flux of each member to be the same. m *= - :equal_growth_rate_constraint^equal_growth_rate_constraints( - [(:bug1, m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), (:bug2, m.bug2.fluxes.:BIOMASS_Ecoli_core_w_GAM.value)] - ) + :equal_growth_rate_constraint^equal_growth_rate_constraints([ + (:bug1, m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), + (:bug2, m.bug2.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), + ]) + +# Since each growth rate is the same, we can pick any of the growth rates as the +# objective for the simulation. +m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value) +# Since the models are usually used in a mono-culture context, the glucose input +# for each individual member is limited. We need to undo this limitation, and +# rather rely on the constrained environmental exchange reaction (and the bounds +# we set for it earlier). m.bug1.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) m.bug2.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) -m.bug1.fluxes.CYTBD.bound = (-10.0, 10.0) # respiration limited -m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value) - -using Gurobi +# We can also be interesting, and limit respiration in one of the members, to +# see what effect this has on the community. +m.bug1.fluxes.CYTBD.bound = (-10.0, 10.0) -sol = optimized_constraints(m; objective = m.objective.value, optimizer=Gurobi.Optimizer) +# Finally, we can simulate the system! +sol = optimized_constraints( + m; + objective = m.objective.value, + optimizer = Tulip.Optimizer, + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], +) -Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) -Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) +@test isapprox(sol.:objective, 0.66686196344, atol = TEST_TOLERANCE) #src -# exchange cytosolic metabolites +# At the moment the members cannot really exchange any metabolites. We can +# change this by changing their individual exchange bounds. mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e] for met in mets m.bug1.fluxes[met].bound = (-1000.0, 1000.0) m.bug2.fluxes[met].bound = (-1000.0, 1000.0) end -sol = optimized_constraints(m; objective = m.objective.value, optimizer=Tulip.Optimizer, modifications=[set_optimizer_attribute("IPM_IterationsLimit", 100000)]) - -Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) -Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) +sol = optimized_constraints( + m; + objective = m.objective.value, + optimizer = Tulip.Optimizer, + modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], +) + + +# We can see that by allowing the microbes to share metabolites, the growth rate +# of the system as a whole increased! We can inspect the individual exchanges to +# see which metabolites are being shared (pyruvate in this case). +bug1_ex_fluxes = Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) +bug2_ex_fluxes = Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) + +#!!! warning "Flux units" +# The unit of the environmental exchange reactions (mmol/gDW_total_biomass/h) is +# different to the unit of the individual species fluxes +# (mmol/gDW_species_biomass/h). This is because the mass balance needs to take +# into account the abundance of each species for the simulation to make sense. +# In this specific case, look at the flux of pyruvate (EX_pyr_e). There is no +# environmental exchange flux, so the two microbes share the metabolite. +# However, `bug1_ex_fluxes[:EX_pyr_e] != bug2_ex_fluxes[:EX_pyr_e]`, but rather +# `abundance_bug1 * bug1_ex_fluxes[:EX_pyr_e] != abundance_bug2 * +# bug2_ex_fluxes[:EX_pyr_e]`. Take care of this when comparing fluxes! + +@test isapprox( + abs(0.2 * bug1_ex_fluxes[:EX_pyr_e] + 0.8 * bug2_ex_fluxes[:EX_pyr_e]), + 0.0, + atol = TEST_TOLERANCE, +) #src diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 686fe2797..b282cb6ca 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -11,6 +11,11 @@ end export environment_exchange_variables +""" +$(TYPEDSIGNATURES) + +Helper function to build a "blank" community model with only environmental exchange reactions. +""" function build_community_environment(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) C.ConstraintTree( :environmental_exchange_reactions => environment_exchange_variables(env_ex_rxns), @@ -19,6 +24,12 @@ end export build_community_environment +""" +$(TYPEDSIGNATURES) + +Helper function to link species specific exchange reactions to the environmental +exchange reactions by weighting them with their abundances. +""" function link_environmental_exchanges( m::C.ConstraintTree, member_abundances::Vector{Tuple{Symbol,Float64}}; @@ -40,7 +51,14 @@ end export link_environmental_exchanges -function equal_growth_rate_constraints(member_biomasses::Vector{Tuple{Symbol,C.LinearValue}}) +""" +$(TYPEDSIGNATURES) + +Helper function to set each species growth rate equal to each other. +""" +function equal_growth_rate_constraints( + member_biomasses::Vector{Tuple{Symbol,C.LinearValue}}, +) C.ConstraintTree( Symbol(bid1, :_, bid2) => C.Constraint(value = bval1 - bval2, bound = 0.0) for ((bid1, bval1), (bid2, bval2)) in From d1e1d1f469aea27feb768bc226d388f62d305816 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Wed, 27 Dec 2023 20:00:30 +0100 Subject: [PATCH 440/531] update example slightly --- docs/src/examples/08-community-models.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 88e8016cb..082cf5ded 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -129,7 +129,7 @@ bug2_ex_fluxes = Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string( # In this specific case, look at the flux of pyruvate (EX_pyr_e). There is no # environmental exchange flux, so the two microbes share the metabolite. # However, `bug1_ex_fluxes[:EX_pyr_e] != bug2_ex_fluxes[:EX_pyr_e]`, but rather -# `abundance_bug1 * bug1_ex_fluxes[:EX_pyr_e] != abundance_bug2 * +# `abundance_bug1 * bug1_ex_fluxes[:EX_pyr_e] == abundance_bug2 * # bug2_ex_fluxes[:EX_pyr_e]`. Take care of this when comparing fluxes! @test isapprox( From d6b19058a688892ee79dca814876a2a2891bc75f Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 28 Dec 2023 13:17:40 +0000 Subject: [PATCH 441/531] automatic formatting triggered by @stelmo on PR #810 --- docs/src/examples/07-loopless-models.jl | 2 +- src/analysis/loopless_flux_balance.jl | 1 - src/builders/loopless.jl | 2 +- 3 files changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index f016844e6..d03fbffc9 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -27,7 +27,7 @@ model = load_model("e_coli_core.json") # One can directly use `loopless_flux_balance_analysis` to solve an FBA problem # based on `model` where loopless constraints are added to all fluxes. This is -# the direct approach. +# the direct approach. sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) diff --git a/src/analysis/loopless_flux_balance.jl b/src/analysis/loopless_flux_balance.jl index 8b1378917..e69de29bb 100644 --- a/src/analysis/loopless_flux_balance.jl +++ b/src/analysis/loopless_flux_balance.jl @@ -1 +0,0 @@ - diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index e2c0bbcc2..8473c41f1 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -86,7 +86,7 @@ function add_loopless_constraints!( ) for rid in internal_reaction_ids ) - # use nullspace to ensure there are no loops + # use nullspace to ensure there are no loops m *= :loopless_condition^C.ConstraintTree( Symbol(:nullspace_vector, i) => C.Constraint( From 444f2f945af2c1fc435591d3bf8aa7b60c8a8286 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 14:18:50 +0100 Subject: [PATCH 442/531] fixup tests --- docs/src/examples/08-community-models.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 082cf5ded..d2ecff1b4 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -82,12 +82,12 @@ m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value) # for each individual member is limited. We need to undo this limitation, and # rather rely on the constrained environmental exchange reaction (and the bounds # we set for it earlier). -m.bug1.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) -m.bug2.fluxes.EX_glc__D_e.bound = (-1000.0, 1000.0) +m.bug1.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) +m.bug2.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) # We can also be interesting, and limit respiration in one of the members, to # see what effect this has on the community. -m.bug1.fluxes.CYTBD.bound = (-10.0, 10.0) +m.bug1.fluxes.CYTBD.bound = C.Between(-10.0, 10.0) # Finally, we can simulate the system! sol = optimized_constraints( @@ -103,8 +103,8 @@ sol = optimized_constraints( # change this by changing their individual exchange bounds. mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e] for met in mets - m.bug1.fluxes[met].bound = (-1000.0, 1000.0) - m.bug2.fluxes[met].bound = (-1000.0, 1000.0) + m.bug1.fluxes[met].bound = C.Between(-1000.0, 1000.0) + m.bug2.fluxes[met].bound = C.Between(-1000.0, 1000.0) end sol = optimized_constraints( From 4e08d8e4eb22df37a662dd7a44212e6b4426b1ee Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 28 Dec 2023 13:20:57 +0000 Subject: [PATCH 443/531] automatic formatting triggered by @stelmo on PR #811 --- docs/src/examples/07-loopless-models.jl | 2 +- docs/src/examples/08-community-models.jl | 6 +++--- src/analysis/loopless_flux_balance.jl | 1 - src/builders/loopless.jl | 2 +- 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index f016844e6..d03fbffc9 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -27,7 +27,7 @@ model = load_model("e_coli_core.json") # One can directly use `loopless_flux_balance_analysis` to solve an FBA problem # based on `model` where loopless constraints are added to all fluxes. This is -# the direct approach. +# the direct approach. sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index d2ecff1b4..794fcd8ab 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -26,9 +26,9 @@ model = load_model("e_coli_core.json") # are then linked to an environmental exchange. For more theoretical details, # see "Gottstein, et al, 2016, Constraint-based stoichiometric modelling from # single organisms to microbial communities, Journal of the Royal Society -# Interface". +# Interface". -# ## Building a community of two *E. coli*s +# ## Building a community of two *E. coli*s # Here we will construct a simple community of two interacting microbes. To do # this, we need to import the models. We import the models are ConstraintTrees, @@ -100,7 +100,7 @@ sol = optimized_constraints( @test isapprox(sol.:objective, 0.66686196344, atol = TEST_TOLERANCE) #src # At the moment the members cannot really exchange any metabolites. We can -# change this by changing their individual exchange bounds. +# change this by changing their individual exchange bounds. mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e] for met in mets m.bug1.fluxes[met].bound = C.Between(-1000.0, 1000.0) diff --git a/src/analysis/loopless_flux_balance.jl b/src/analysis/loopless_flux_balance.jl index 8b1378917..e69de29bb 100644 --- a/src/analysis/loopless_flux_balance.jl +++ b/src/analysis/loopless_flux_balance.jl @@ -1 +0,0 @@ - diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index e2c0bbcc2..8473c41f1 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -86,7 +86,7 @@ function add_loopless_constraints!( ) for rid in internal_reaction_ids ) - # use nullspace to ensure there are no loops + # use nullspace to ensure there are no loops m *= :loopless_condition^C.ConstraintTree( Symbol(:nullspace_vector, i) => C.Constraint( From 8d784d2350627ff2359f0d4aa555e2999865c0f4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 15:12:26 +0100 Subject: [PATCH 444/531] restructure --- docs/src/examples/02-flux-balance-analysis.jl | 6 +- docs/src/examples/02a-optimizer-parameters.jl | 6 +- docs/src/examples/02b-model-modifications.jl | 6 +- .../examples/03-parsimonious-flux-balance.jl | 6 +- ...04-minimization-of-metabolic-adjustment.jl | 4 +- src/COBREXA.jl | 13 ++- src/analysis/flux_balance.jl | 55 --------- src/analysis/loopless_flux_balance.jl | 0 src/builders/core.jl | 2 - src/builders/enzymes.jl | 46 -------- src/builders/loopless.jl | 58 ---------- src/builders/thermodynamic.jl | 104 ----------------- ...nzyme_constrained_flux_balance_analysis.jl | 46 ++++++++ src/frontend/flux_balance_analysis.jl | 29 +++++ .../loopless_flux_balance_analysis.jl | 58 ++++++++++ .../max_min_driving_force_analysis.jl | 105 ++++++++++++++++++ ...ation_of_metabolic_adjustment_analysis.jl} | 12 +- .../parsimonious_flux_balance.jl | 14 +-- src/{analysis => misc}/modifications.jl | 0 src/solver.jl | 27 +++++ 20 files changed, 301 insertions(+), 296 deletions(-) delete mode 100644 src/analysis/flux_balance.jl delete mode 100644 src/analysis/loopless_flux_balance.jl create mode 100644 src/frontend/enzyme_constrained_flux_balance_analysis.jl create mode 100644 src/frontend/flux_balance_analysis.jl create mode 100644 src/frontend/loopless_flux_balance_analysis.jl create mode 100644 src/frontend/max_min_driving_force_analysis.jl rename src/{analysis/minimal_metabolic_adjustment.jl => frontend/minimization_of_metabolic_adjustment_analysis.jl} (85%) rename src/{analysis => frontend}/parsimonious_flux_balance.jl (87%) rename src/{analysis => misc}/modifications.jl (100%) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 5e55cea23..88d0c1ee6 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,7 +1,7 @@ # # Flux balance analysis (FBA) -# Here we use [`flux_balance`](@ref) and several related functions to +# Here we use [`flux_balance_analysis`](@ref) and several related functions to # find an optimal flux in the *E. coli* "core" model. We will need the model, # which we can download using [`download_model`](@ref): @@ -26,9 +26,9 @@ model = load_model("e_coli_core.json") # There are many possibilities on how to arrange the metabolic model into the # optimization framework and how to actually solve it. The "usual" assumed one # is captured in the default behavior of function -# [`flux_balance`](@ref): +# [`flux_balance_analysis`](@ref): -solution = flux_balance(model, Tulip.Optimizer) +solution = flux_balance_analysis(model, Tulip.Optimizer) @test isapprox(solution.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl index 2dd2705c0..e05a2b0cb 100644 --- a/docs/src/examples/02a-optimizer-parameters.jl +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -3,7 +3,7 @@ # # Many optimizers require fine-tuning to produce best results. You can pass in # additional optimizer settings via the `modifications` parameter of -# [`flux_balance`](@ref). These include e.g. +# [`flux_balance_analysis`](@ref). These include e.g. # # - [`set_optimizer_attribute`](@ref) (typically allowing you to tune e.g. # iteration limits, tolerances, or floating-point precision) @@ -29,7 +29,7 @@ model = load_model("e_coli_core.json") # Running a FBA with a silent optimizer that has slightly increased iteration # limit for IPM algorithm may now look as follows: -solution = flux_balance( +solution = flux_balance_analysis( model, Tulip.Optimizer; modifications = [silence, set_optimizer_attribute("IPM_IterationsLimit", 1000)], @@ -42,7 +42,7 @@ solution = flux_balance( # will cause it to fail, return no solution, and verbosely describe what # happened: -solution = flux_balance( +solution = flux_balance_analysis( model, Tulip.Optimizer; modifications = [set_optimizer_attribute("IPM_IterationsLimit", 2)], diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index 0ee5942d0..160516e2f 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -59,11 +59,11 @@ model.reactions["CS"].stoichiometry # ## Running FBA on modified models # -# Since the canonical model is completely mutable, you can change it in any way you like and feed it directly into [`flux_balance`](@ref). Let's first find a "original" solution, so that we have a base solution for comparing: +# Since the canonical model is completely mutable, you can change it in any way you like and feed it directly into [`flux_balance_analysis`](@ref). Let's first find a "original" solution, so that we have a base solution for comparing: import GLPK -base_solution = flux_balance(model, GLPK.Optimizer) +base_solution = flux_balance_analysis(model, GLPK.Optimizer) base_solution.objective # Now, for example, we can limit the intake of glucose by the model: @@ -76,7 +76,7 @@ model.reactions["EX_glc__D_e"].lower_bound = -5.0 # ...and solve the modified model: # -low_glucose_solution = flux_balance(model, GLPK.Optimizer) +low_glucose_solution = flux_balance_analysis(model, GLPK.Optimizer) low_glucose_solution.objective @test isapprox(low_glucose_solution.objective, 0.41559777, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 2e1a9c813..a9353bb32 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -1,7 +1,7 @@ # # Parsimonious flux balance analysis -# We will use [`parsimonious_flux_balance`](@ref) and +# We will use [`parsimonious_flux_balance_analysis`](@ref) and # [`minimize_metabolic_adjustment`](@ref) to find the optimal flux # distribution in the *E. coli* "core" model. # @@ -24,11 +24,11 @@ model = load_model("e_coli_core.json") # load the model # Use the convenience function to run standard pFBA on -vt = parsimonious_flux_balance(model, Clarabel.Optimizer; modifications = [silence]) +vt = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; modifications = [silence]) # Or use the piping functionality -model |> parsimonious_flux_balance(Clarabel.Optimizer; modifications = [silence]) +model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; modifications = [silence]) @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src @test sum(x^2 for x in values(vt.fluxes)) < 15000 #src diff --git a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl index 0cfdd4050..e950c57a9 100644 --- a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl +++ b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl @@ -1,5 +1,5 @@ -# # Minimization of metabolic adjustment +# # Minimization of metabolic adjustment analysis # TODO MOMA citation @@ -18,6 +18,6 @@ import Clarabel # guessing. model = convert(CM.Model, load_model("e_coli_core.json")) -reference_fluxes = parsimonious_flux_balance(model, Clarabel.Optimizer).fluxes +reference_fluxes = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer).fluxes # TODO MOMA from here diff --git a/src/COBREXA.jl b/src/COBREXA.jl index ce9bd2493..a4152e951 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -31,6 +31,7 @@ include("types.jl") include("io.jl") include("solver.jl") +# these functions build or extend constrainttrees of metabolic models include("builders/core.jl") include("builders/genes.jl") include("builders/objectives.jl") @@ -39,11 +40,15 @@ include("builders/thermodynamic.jl") include("builders/loopless.jl") include("builders/communities.jl") -include("analysis/modifications.jl") -include("analysis/flux_balance.jl") -include("analysis/parsimonious_flux_balance.jl") -include("analysis/minimal_metabolic_adjustment.jl") +# these are the one shot analysis functions +include("frontend/flux_balance_analysis.jl") +include("frontend/parsimonious_flux_balance.jl") +include("frontend/minimization_of_metabolic_adjustment_analysis.jl") +include("frontend/enzyme_constrained_flux_balance_analysis.jl") +include("frontend/loopless_flux_balance_analysis.jl") +include("frontend/max_min_driving_force_analysis.jl") +include("misc/modifications.jl") include("misc/bounds.jl") include("misc/utils.jl") diff --git a/src/analysis/flux_balance.jl b/src/analysis/flux_balance.jl deleted file mode 100644 index 5f9d06182..000000000 --- a/src/analysis/flux_balance.jl +++ /dev/null @@ -1,55 +0,0 @@ -""" -$(TYPEDSIGNATURES) - -Make an JuMP model out of `constraints` using [`optimization_model`](@ref) -(most arguments are forwarded there), then apply the `modifications`, optimize -the model, and return either `nothing` if the optimization failed, or `output` -substituted with the solved values (`output` defaults to `constraints`. - -For a "nice" version for simpler finding of metabolic model optima, use -[`flux_balance`](@ref). -""" -function optimized_constraints( - constraints::C.ConstraintTreeElem; - modifications = [], - output = constraints, - kwargs..., -) - om = optimization_model(constraints; kwargs...) - for m in modifications - m(om) - end - J.optimize!(om) - is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing -end - -export optimized_constraints - -""" -$(TYPEDSIGNATURES) - -Compute an optimal objective-optimizing solution of the given `model`. - -Most arguments are forwarded to [`optimized_constraints`](@ref). - -Returns a tree with the optimization solution of the same shape as -given by [`fbc_model_constraints`](@ref). -""" -function flux_balance(model::A.AbstractFBCModel, optimizer; kwargs...) - constraints = fbc_model_constraints(model) - optimized_constraints( - constraints; - objective = constraints.objective.value, - optimizer, - kwargs..., - ) -end - -""" -$(TYPEDSIGNATURES) - -Pipe-able overload of [`flux_balance`](@ref). -""" -flux_balance(optimizer; modifications = []) = m -> flux_balance(m, optimizer; modifications) - -export flux_balance diff --git a/src/analysis/loopless_flux_balance.jl b/src/analysis/loopless_flux_balance.jl deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/builders/core.jl b/src/builders/core.jl index 93807205c..cbfdbbcf2 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,6 +1,4 @@ -import SparseArrays: sparse - """ $(TYPEDSIGNATURES) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index dfd92f5f2..6f71e45f4 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -234,49 +234,3 @@ function add_enzyme_constraints!( end export add_enzyme_constraints! - -""" -$(TYPEDSIGNATURES) - -Run a basic enzyme constrained flux balance analysis on `model`. The enzyme -model is parameterized by `reaction_isozymes`, which is a mapping of reaction -IDs (those used in the fluxes of the model) to named [`Isozyme`](@ref)s. -Additionally, `gene_molar_masses` and `capacity_limitations` should be supplied. -The latter is a vector of tuples, where each tuple represents a distinct bound -as `(bound_id, genes_in_bound, protein_mass_bound)`. Typically, `model` has -bounded exchange reactions, which are unnecessary in enzyme constrained models. -Unbound these reactions by listing their IDs in `unconstrain_reactions`, which -makes them reversible. Optimization `modifications` are directly forwarded. - -In the event that your model requires more complex build steps, consider -constructing it manually by using [`add_enzyme_constraints!`](@ref). -""" -function enzyme_constrained_flux_balance_analysis( - model::A.AbstractFBCModel, - reaction_isozymes::Dict{String,Dict{String,SimpleIsozyme}}, - gene_molar_masses::Dict{String,Float64}, - capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; - optimizer, - unconstrain_reactions = String[], - modifications = [], -) - m = fbc_model_constraints(model) - - # create enzyme variables - m += :enzymes^enzyme_variables(model) - - m = add_enzyme_constraints!( - m, - reaction_isozymes, - gene_molar_masses, - capacity_limitations, - ) - - for rid in Symbol.(unconstrain_reactions) - m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) - end - - optimized_constraints(m; objective = m.objective.value, optimizer, modifications) -end - -export enzyme_constrained_flux_balance_analysis diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 8473c41f1..4cf5d27c1 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -103,61 +103,3 @@ function add_loopless_constraints!( end export add_loopless_constraints! - -""" -$(TYPEDSIGNATURES) - -Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically -infeasible internal cycles can occur. Adds the following constraints to the problem: -``` --max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ --max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ -Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) -Nᵢₙₜ' × G = 0 -yᵢ ∈ {0, 1} -Gᵢ ∈ ℝ -i ∈ internal reactions -Nᵢₙₜ is the nullspace of the internal stoichiometric matrix -``` -Note, this modification introduces binary variables, so an optimization solver capable of -handing mixed integer problems needs to be used. The arguments `max_flux_bound` and -`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. - -For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination -of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical -journal, 2011`. -""" -function loopless_flux_balance_analysis( - model; - max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = 1.0, # heuristic from paper - modifications = [], - optimizer, -) - - m = fbc_model_constraints(model) - - # find all internal reactions - internal_reactions = [ - (i, Symbol(rid)) for - (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) - ] - internal_reaction_ids = last.(internal_reactions) - internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below - - internal_reaction_stoichiometry_nullspace_columns = - eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) # no sparse nullspace function - - m = add_loopless_constraints!( - m, - internal_reaction_ids, - internal_reaction_stoichiometry_nullspace_columns; - max_flux_bound, - strict_inequality_tolerance, - ) - - # solve - optimized_constraints(m; objective = m.objective.value, optimizer, modifications) -end - -export loopless_flux_balance_analysis diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 9ad5e8915..ab2c27ad1 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -142,107 +142,3 @@ log_ratio_constraints( export log_ratio_constraints -""" -$(TYPEDSIGNATURES) - -Perform a max-min driving force analysis using `optimizer` on the `model` with -supplied ΔG⁰s in `reaction_standard_gibbs_free_energies`, as defined by Noor, et -al., "Pathway thermodynamics highlights kinetic obstacles in central -metabolism.", PLoS computational biology, 2014. - -Optionally, `reference_flux` can be used to set the directions of each reaction -in `model` (all reactions are assumed to proceed forward by default). The -supplied `reference_flux` should be free of internal cycles i.e. -thermodynamically consistent. This optional input is important if a reaction in -`model` normally runs in reverse (negative flux). Note, only the signs are -extracted from this input, so be careful with floating point precision near 0. - -The max-min driving force algorithm returns the Gibbs free energy of the -reactions, the concentrations of metabolites and the actual maximum minimum -driving force. The optimization problem solved is: -``` -max min -ΔᵣG -s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) - ΔᵣG ≤ 0 - ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas -constant, T is the temperature, S is the stoichiometry of the model, and C is -the vector of metabolite concentrations (and their respective lower and upper -bounds). - -In case no feasible solution exists, `nothing` is returned. - -Reactions specified in `ignore_reaction_ids` are internally ignored when -calculating the max-min driving force. Importantly, this should include water -and proton importers. - -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` -need to be specified so that they can be ignored in the calculations. -Effectively this assumes an aqueous environment at constant pH is used. - -`constant_concentrations` is used to fix the concentrations of certain -metabolites (such as CO₂) by passing a dictionary mapping metabolite id to its -constant value. `concentration_ratios` is used to specify additional constraints -on metabolite pair concentrations (typically, this is done with various -cofactors, such as the ATP/ADP ratio. For example, you can fix the concentration -of ATP to be always 5× higher than of ADP by specifying `Dict("atp_ratio" => -("atp_c","adp_c", 5.0))` if these metabolites are called `atp_c` and `adp_c` in -the model. `concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in -the optimization problems (these are overwritten by `constant_concentrations` if -supplied). - -`T` and `R` can be specified in the corresponding units; defaults are K and -kJ/K/mol. The unit of metabolite concentrations is typically molar, and the ΔG⁰s -have units of kJ/mol. Other units can be used, as long as they are consistent. -As usual, optimizer settings can be changed with `modifications`. -""" -function max_min_driving_force_analysis( - model::A.AbstractFBCModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}; - reference_flux = Dict{String,Float64}(), - concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), - constant_concentrations = Dict{String,Float64}(), - proton_ids, - water_ids, - concentration_lb = 1e-9, # M - concentration_ub = 1e-1, # M - T = 298.15, # Kelvin - R = 8.31446261815324e-3, # kJ/K/mol - ignore_reaction_ids = String[], - modifications = [], - optimizer, -) - m = build_max_min_driving_force_model( - model, - reaction_standard_gibbs_free_energies; - reference_flux, - concentration_lb, - concentration_ub, - R, - T, - ignore_reaction_ids, - water_ids, - proton_ids, - ) - - for (mid, val) in constant_concentrations - m.log_metabolite_concentrations[Symbol(mid)].bound = C.EqualTo(log(val)) - end - - m *= - :metabolite_ratio_constraints^log_ratio_constraints( - concentration_ratios, - m.log_metabolite_concentrations, - ) - - - optimized_constraints( - m; - objective = m.max_min_driving_force.value, - optimizer, - modifications, - ) -end - -export max_min_driving_force_analysis diff --git a/src/frontend/enzyme_constrained_flux_balance_analysis.jl b/src/frontend/enzyme_constrained_flux_balance_analysis.jl new file mode 100644 index 000000000..47be6fb83 --- /dev/null +++ b/src/frontend/enzyme_constrained_flux_balance_analysis.jl @@ -0,0 +1,46 @@ + +""" +$(TYPEDSIGNATURES) + +Run a basic enzyme constrained flux balance analysis on `model`. The enzyme +model is parameterized by `reaction_isozymes`, which is a mapping of reaction +IDs (those used in the fluxes of the model) to named [`Isozyme`](@ref)s. +Additionally, `gene_molar_masses` and `capacity_limitations` should be supplied. +The latter is a vector of tuples, where each tuple represents a distinct bound +as `(bound_id, genes_in_bound, protein_mass_bound)`. Typically, `model` has +bounded exchange reactions, which are unnecessary in enzyme constrained models. +Unbound these reactions by listing their IDs in `unconstrain_reactions`, which +makes them reversible. Optimization `modifications` are directly forwarded. + +In the event that your model requires more complex build steps, consider +constructing it manually by using [`add_enzyme_constraints!`](@ref). +""" +function enzyme_constrained_flux_balance_analysis( + model::A.AbstractFBCModel, + reaction_isozymes::Dict{String,Dict{String,SimpleIsozyme}}, + gene_molar_masses::Dict{String,Float64}, + capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; + optimizer, + unconstrain_reactions = String[], + modifications = [], +) + m = fbc_model_constraints(model) + + # create enzyme variables + m += :enzymes^enzyme_variables(model) + + m = add_enzyme_constraints!( + m, + reaction_isozymes, + gene_molar_masses, + capacity_limitations, + ) + + for rid in Symbol.(unconstrain_reactions) + m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) + end + + optimized_constraints(m; objective = m.objective.value, optimizer, modifications) +end + +export enzyme_constrained_flux_balance_analysis diff --git a/src/frontend/flux_balance_analysis.jl b/src/frontend/flux_balance_analysis.jl new file mode 100644 index 000000000..d0bd7ad5c --- /dev/null +++ b/src/frontend/flux_balance_analysis.jl @@ -0,0 +1,29 @@ + +""" +$(TYPEDSIGNATURES) + +Compute an optimal objective-optimizing solution of the given `model`. + +Most arguments are forwarded to [`optimized_constraints`](@ref). + +Returns a tree with the optimization solution of the same shape as +given by [`fbc_model_constraints`](@ref). +""" +function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) + constraints = fbc_model_constraints(model) + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able overload of [`flux_balance_analysis`](@ref). +""" +flux_balance_analysis(optimizer; modifications = []) = m -> flux_balance_analysis(m, optimizer; modifications) + +export flux_balance_analysis diff --git a/src/frontend/loopless_flux_balance_analysis.jl b/src/frontend/loopless_flux_balance_analysis.jl new file mode 100644 index 000000000..f55527615 --- /dev/null +++ b/src/frontend/loopless_flux_balance_analysis.jl @@ -0,0 +1,58 @@ + +""" +$(TYPEDSIGNATURES) + +Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically +infeasible internal cycles can occur. Adds the following constraints to the problem: +``` +-max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ +-max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ +Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) +Nᵢₙₜ' × G = 0 +yᵢ ∈ {0, 1} +Gᵢ ∈ ℝ +i ∈ internal reactions +Nᵢₙₜ is the nullspace of the internal stoichiometric matrix +``` +Note, this modification introduces binary variables, so an optimization solver capable of +handing mixed integer problems needs to be used. The arguments `max_flux_bound` and +`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. + +For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination +of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical +journal, 2011`. +""" +function loopless_flux_balance_analysis( + model; + max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic + strict_inequality_tolerance = 1.0, # heuristic from paper + modifications = [], + optimizer, +) + + m = fbc_model_constraints(model) + + # find all internal reactions + internal_reactions = [ + (i, Symbol(rid)) for + (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) + ] + internal_reaction_ids = last.(internal_reactions) + internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below + + internal_reaction_stoichiometry_nullspace_columns = + eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) # no sparse nullspace function + + m = add_loopless_constraints!( + m, + internal_reaction_ids, + internal_reaction_stoichiometry_nullspace_columns; + max_flux_bound, + strict_inequality_tolerance, + ) + + # solve + optimized_constraints(m; objective = m.objective.value, optimizer, modifications) +end + +export loopless_flux_balance_analysis diff --git a/src/frontend/max_min_driving_force_analysis.jl b/src/frontend/max_min_driving_force_analysis.jl new file mode 100644 index 000000000..f014ad29c --- /dev/null +++ b/src/frontend/max_min_driving_force_analysis.jl @@ -0,0 +1,105 @@ + +""" +$(TYPEDSIGNATURES) + +Perform a max-min driving force analysis using `optimizer` on the `model` with +supplied ΔG⁰s in `reaction_standard_gibbs_free_energies`, as defined by Noor, et +al., "Pathway thermodynamics highlights kinetic obstacles in central +metabolism.", PLoS computational biology, 2014. + +Optionally, `reference_flux` can be used to set the directions of each reaction +in `model` (all reactions are assumed to proceed forward by default). The +supplied `reference_flux` should be free of internal cycles i.e. +thermodynamically consistent. This optional input is important if a reaction in +`model` normally runs in reverse (negative flux). Note, only the signs are +extracted from this input, so be careful with floating point precision near 0. + +The max-min driving force algorithm returns the Gibbs free energy of the +reactions, the concentrations of metabolites and the actual maximum minimum +driving force. The optimization problem solved is: +``` +max min -ΔᵣG +s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) + ΔᵣG ≤ 0 + ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) +``` +where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas +constant, T is the temperature, S is the stoichiometry of the model, and C is +the vector of metabolite concentrations (and their respective lower and upper +bounds). + +In case no feasible solution exists, `nothing` is returned. + +Reactions specified in `ignore_reaction_ids` are internally ignored when +calculating the max-min driving force. Importantly, this should include water +and proton importers. + +Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` +need to be specified so that they can be ignored in the calculations. +Effectively this assumes an aqueous environment at constant pH is used. + +`constant_concentrations` is used to fix the concentrations of certain +metabolites (such as CO₂) by passing a dictionary mapping metabolite id to its +constant value. `concentration_ratios` is used to specify additional constraints +on metabolite pair concentrations (typically, this is done with various +cofactors, such as the ATP/ADP ratio. For example, you can fix the concentration +of ATP to be always 5× higher than of ADP by specifying `Dict("atp_ratio" => +("atp_c","adp_c", 5.0))` if these metabolites are called `atp_c` and `adp_c` in +the model. `concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in +the optimization problems (these are overwritten by `constant_concentrations` if +supplied). + +`T` and `R` can be specified in the corresponding units; defaults are K and +kJ/K/mol. The unit of metabolite concentrations is typically molar, and the ΔG⁰s +have units of kJ/mol. Other units can be used, as long as they are consistent. +As usual, optimizer settings can be changed with `modifications`. +""" +function max_min_driving_force_analysis( + model::A.AbstractFBCModel, + reaction_standard_gibbs_free_energies::Dict{String,Float64}; + reference_flux = Dict{String,Float64}(), + concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), + constant_concentrations = Dict{String,Float64}(), + proton_ids, + water_ids, + concentration_lb = 1e-9, # M + concentration_ub = 1e-1, # M + T = 298.15, # Kelvin + R = 8.31446261815324e-3, # kJ/K/mol + ignore_reaction_ids = String[], + modifications = [], + optimizer, +) + m = build_max_min_driving_force_model( + model, + reaction_standard_gibbs_free_energies; + reference_flux, + concentration_lb, + concentration_ub, + R, + T, + ignore_reaction_ids, + water_ids, + proton_ids, + ) + + for (mid, val) in constant_concentrations + m.log_metabolite_concentrations[Symbol(mid)].bound = C.EqualTo(log(val)) + end + + m *= + :metabolite_ratio_constraints^log_ratio_constraints( + concentration_ratios, + m.log_metabolite_concentrations, + ) + + + optimized_constraints( + m; + objective = m.max_min_driving_force.value, + optimizer, + modifications, + ) +end + +export max_min_driving_force_analysis diff --git a/src/analysis/minimal_metabolic_adjustment.jl b/src/frontend/minimization_of_metabolic_adjustment_analysis.jl similarity index 85% rename from src/analysis/minimal_metabolic_adjustment.jl rename to src/frontend/minimization_of_metabolic_adjustment_analysis.jl index d7dcc29c5..c9fbf00b7 100644 --- a/src/analysis/minimal_metabolic_adjustment.jl +++ b/src/frontend/minimization_of_metabolic_adjustment_analysis.jl @@ -18,7 +18,7 @@ objective is constructed via [`squared_sum_error_objective`](@ref)). Additional parameters are forwarded to [`optimized_constraints`](@ref). """ -function minimal_metabolic_adjustment( +function minimization_of_metabolic_adjustment_analysis( model::A.AbstractFBCModel, reference_fluxes::Dict{Symbol,Float64}, optimizer; @@ -38,17 +38,17 @@ end """ $(TYPEDSIGNATURES) -A slightly easier-to-use version of [`minimal_metabolic_adjustment`](@ref) that +A slightly easier-to-use version of [`minimization_of_metabolic_adjustment_analysis`](@ref) that computes the reference flux as the optimal solution of the [`reference_model`](@ref). The reference flux is calculated using `reference_optimizer` and `reference_modifications`, which default to the `optimizer` and `modifications`. Leftover arguments are passed to the overload of -[`minimal_metabolic_adjustment`](@ref) that accepts the reference flux +[`minimization_of_metabolic_adjustment_analysis`](@ref) that accepts the reference flux dictionary. """ -function minimal_metabolic_adjustment( +function minimization_of_metabolic_adjustment_analysis( model::A.AbstractFBCModel, reference_model::A.AbstractFBCModel, optimizer; @@ -65,7 +65,7 @@ function minimal_metabolic_adjustment( output = reference_constraints.fluxes, ) isnothing(reference_fluxes) && return nothing - minimal_metabolic_adjustment( + minimization_of_metabolic_adjustment_analysis( model, reference_fluxes, optimizer; @@ -74,4 +74,4 @@ function minimal_metabolic_adjustment( ) end -export minimal_metabolic_adjustment +export minimization_of_metabolic_adjustment_analysis diff --git a/src/analysis/parsimonious_flux_balance.jl b/src/frontend/parsimonious_flux_balance.jl similarity index 87% rename from src/analysis/parsimonious_flux_balance.jl rename to src/frontend/parsimonious_flux_balance.jl index 114cb4652..5ad5ac2bf 100644 --- a/src/analysis/parsimonious_flux_balance.jl +++ b/src/frontend/parsimonious_flux_balance.jl @@ -70,7 +70,7 @@ $(TYPEDSIGNATURES) Compute a parsimonious flux solution for the given `model`. In short, the objective value of the parsimonious solution should be the same as the one from -[`flux_balance`](@ref), except the squared sum of reaction fluxes is minimized. +[`flux_balance_analysis`](@ref), except the squared sum of reaction fluxes is minimized. If there are multiple possible fluxes that achieve a given objective value, parsimonious flux thus represents the "minimum energy" one, thus arguably more realistic. The optimized squared distance is present in the result as @@ -80,10 +80,10 @@ Most arguments are forwarded to [`parsimonious_optimized_constraints`](@ref), with some (objectives) filled in automatically to fit the common processing of FBC models, and some (`tolerances`) provided with more practical defaults. -Similarly to the [`flux_balance`](@ref), returns a tree with the optimization +Similarly to the [`flux_balance_analysis`](@ref), returns a tree with the optimization solutions of the shape as given by [`fbc_model_constraints`](@ref). """ -function parsimonious_flux_balance( +function parsimonious_flux_balance_analysis( model::A.AbstractFBCModel, optimizer; tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), @@ -104,9 +104,9 @@ end """ $(TYPEDSIGNATURES) -Pipe-able variant of [`parsimonious_flux_balance`](@ref). +Pipe-able variant of [`parsimonious_flux_balance_analysis`](@ref). """ -parsimonious_flux_balance(optimizer; kwargs...) = - model -> parsimonious_flux_balance(model, optimizer; kwargs...) +parsimonious_flux_balance_analysis(optimizer; kwargs...) = + model -> parsimonious_flux_balance_analysis(model, optimizer; kwargs...) -export parsimonious_flux_balance +export parsimonious_flux_balance_analysis diff --git a/src/analysis/modifications.jl b/src/misc/modifications.jl similarity index 100% rename from src/analysis/modifications.jl rename to src/misc/modifications.jl diff --git a/src/solver.jl b/src/solver.jl index 91ba4a171..e00fbb58c 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -77,3 +77,30 @@ Objective sense for finding the any feasible value of the objective. Same as `JuMP.FEASIBILITY_SENSE`. """ const Feasible = J.FEASIBILITY_SENSE + +""" +$(TYPEDSIGNATURES) + +Make an JuMP model out of `constraints` using [`optimization_model`](@ref) +(most arguments are forwarded there), then apply the `modifications`, optimize +the model, and return either `nothing` if the optimization failed, or `output` +substituted with the solved values (`output` defaults to `constraints`. + +For a "nice" version for simpler finding of metabolic model optima, use +[`flux_balance`](@ref). +""" +function optimized_constraints( + constraints::C.ConstraintTreeElem; + modifications = [], + output = constraints, + kwargs..., +) + om = optimization_model(constraints; kwargs...) + for m in modifications + m(om) + end + J.optimize!(om) + is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing +end + +export optimized_constraints From a8c6eb56bddabce6df7e84d8e208bab44b9a610a Mon Sep 17 00:00:00 2001 From: stelmo Date: Thu, 28 Dec 2023 14:18:29 +0000 Subject: [PATCH 445/531] automatic formatting triggered by @stelmo on PR #812 --- docs/src/examples/03-parsimonious-flux-balance.jl | 3 ++- src/builders/thermodynamic.jl | 1 - src/frontend/flux_balance_analysis.jl | 3 ++- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index a9353bb32..d21606053 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -24,7 +24,8 @@ model = load_model("e_coli_core.json") # load the model # Use the convenience function to run standard pFBA on -vt = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; modifications = [silence]) +vt = + parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; modifications = [silence]) # Or use the piping functionality diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index ab2c27ad1..9c0ee413e 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -141,4 +141,3 @@ log_ratio_constraints( ) export log_ratio_constraints - diff --git a/src/frontend/flux_balance_analysis.jl b/src/frontend/flux_balance_analysis.jl index d0bd7ad5c..626eb3749 100644 --- a/src/frontend/flux_balance_analysis.jl +++ b/src/frontend/flux_balance_analysis.jl @@ -24,6 +24,7 @@ $(TYPEDSIGNATURES) Pipe-able overload of [`flux_balance_analysis`](@ref). """ -flux_balance_analysis(optimizer; modifications = []) = m -> flux_balance_analysis(m, optimizer; modifications) +flux_balance_analysis(optimizer; modifications = []) = + m -> flux_balance_analysis(m, optimizer; modifications) export flux_balance_analysis From df7fc4356c49e961a8c971c9ac230aafaa7aaf43 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 16:42:34 +0100 Subject: [PATCH 446/531] move to diffmet --- test/enzyme_model.jl | 77 -------------------------------------------- 1 file changed, 77 deletions(-) delete mode 100644 test/enzyme_model.jl diff --git a/test/enzyme_model.jl b/test/enzyme_model.jl deleted file mode 100644 index 8f26f8c9c..000000000 --- a/test/enzyme_model.jl +++ /dev/null @@ -1,77 +0,0 @@ - -@testset "GECKO small model" begin - #= - Implement the small model found in the supplment of the - original GECKO paper. This model is nice to troubleshoot with, - because the stoich matrix is small. - =# - mets = Dict( - "m1" => A.CanonicalModel.Metabolite(), - "m2" => A.CanonicalModel.Metabolite(), - "m3" => A.CanonicalModel.Metabolite(), - "m4" => A.CanonicalModel.Metabolite(), - ) - - rxns = Dict( - "r1" => A.CanonicalModel.Reaction( - lower_bound = 0.0, - upper_bound = 100.0, - stoichiometry = Dict("m1" => 1.0), - ), - "r2" => A.CanonicalModel.Reaction( - lower_bound = 0.0, - upper_bound = 100.0, - stoichiometry = Dict("m2" => 1.0), - ), - "r3" => A.CanonicalModel.Reaction( - lower_bound = 0.0, - upper_bound = 100.0, - stoichiometry = Dict("m1" => -1.0, "m2" => -1.0, "m3" => 1.0), - ), - "r4" => A.CanonicalModel.Reaction( - lower_bound = 0.0, - upper_bound = 100.0, - stoichiometry = Dict("m3" => -1.0, "m4" => 1.0), - ), - "r5" => A.CanonicalModel.Reaction( - lower_bound = -100.0, - upper_bound = 100.0, - stoichiometry = Dict("m2" => -1.0, "m4" => 1.0), - ), - "r6" => A.CanonicalModel.Reaction( - lower_bound = 0.0, - upper_bound = 100.0, - stoichiometry = Dict("m4" => -1.0), - objective_coefficient = 1.0, - ), - ) - - gs = Dict("g$i" => A.CanonicalModel.Gene() for i = 1:5) - - model = A.CanonicalModel.Model(rxns, mets, gs) - - reaction_isozymes = Dict( - "r3" => Dict("iso1" => SimpleIsozyme(Dict("g1" => 1), 1.0, 1.0)), - "r4" => Dict( - "iso1" => SimpleIsozyme(Dict("g1" => 1), 2.0, 2.0), - "iso2" => SimpleIsozyme(Dict("g2" => 1), 3.0, 3.0), - ), - "r5" => Dict("iso1" => SimpleIsozyme(Dict("g3" => 1, "g4" => 2), 70.0, 70.0)), - ) - - gene_product_molar_mass = - Dict("g1" => 1.0, "g2" => 2.0, "g3" => 3.0, "g4" => 4.0, "g5" => 1.0) - - řešení = enzyme_constrained_flux_balance_analysis( - model, - reaction_isozymes, - gene_product_molar_mass, - [("total_proteome_bound", A.genes(model), 0.5)]; - optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], - ) - - @test isapprox(řešení.objective, 3.181818181753438, atol = TEST_TOLERANCE) - @test isapprox(řešení.enzymes.g4, 0.09090909090607537, atol = TEST_TOLERANCE) - @test isapprox(řešení.total_proteome_bound, 0.5, atol = TEST_TOLERANCE) -end From 086e2ab9af5feecc102f74f8e422f5056d7137f4 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 16:42:53 +0100 Subject: [PATCH 447/531] rm small model enzyme test --- test/runtests.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 10d3e7fdb..88b05b85b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,7 +45,6 @@ run_test_file("data_downloaded.jl") @testset "COBREXA test suite" begin run_doc_examples() - include("enzyme_model.jl") run_test_file("aqua.jl") end From 98b768c8c0a72eaecd6c9ec02835b11532770164 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Thu, 28 Dec 2023 16:46:40 +0100 Subject: [PATCH 448/531] split out capacity from mass balance constraints --- docs/src/examples/05-enzyme-constrained-models.jl | 7 ++++--- src/builders/enzymes.jl | 9 +-------- src/frontend/enzyme_constrained_flux_balance_analysis.jl | 8 ++++++-- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 943575177..30ea6205e 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -138,13 +138,14 @@ m.enzymes.b2417.bound = C.Between(0.0, 0.1) # for fun, change the bounds of the # attach the enzyme mass balances m = add_enzyme_constraints!( m, - reaction_isozymes, - gene_molar_masses, - [("total_proteome_bound", A.genes(model), total_enzyme_capacity)]; + reaction_isozymes; fluxes = m.fluxes, # mount enzyme constraints to these fluxes enzymes = m.enzymes, # enzyme variables ) +# add capacity limitation +m *= :total_proteome_bound^enzyme_capacity(m.enzymes, gene_molar_masses, A.genes(model), total_enzyme_capacity) + # solve the model ec_solution = optimized_constraints( m; diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 6f71e45f4..8f8b96c23 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -164,9 +164,7 @@ assign kcats to reactions. Use [`SimpleIsozyme`](@ref) when in doubt. """ function add_enzyme_constraints!( m::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,T}}, - gene_molar_masses::Dict{String,Float64}, - capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; + reaction_isozymes::Dict{String,Dict{String,T}}; fluxes = m.fluxes, enzymes = m.enzymes, ) where {T<:Isozyme} @@ -225,11 +223,6 @@ function add_enzyme_constraints!( reaction_isozymes, ) - # add capacity limitations - for (id, gids, cap) in capacity_limitations - m *= Symbol(id)^enzyme_capacity(enzymes, gene_molar_masses, gids, cap) - end - m end diff --git a/src/frontend/enzyme_constrained_flux_balance_analysis.jl b/src/frontend/enzyme_constrained_flux_balance_analysis.jl index 47be6fb83..31b26429a 100644 --- a/src/frontend/enzyme_constrained_flux_balance_analysis.jl +++ b/src/frontend/enzyme_constrained_flux_balance_analysis.jl @@ -29,13 +29,17 @@ function enzyme_constrained_flux_balance_analysis( # create enzyme variables m += :enzymes^enzyme_variables(model) + # add enzyme equality constraints (stoichiometry) m = add_enzyme_constraints!( m, reaction_isozymes, - gene_molar_masses, - capacity_limitations, ) + # add capacity limitations + for (id, gids, cap) in capacity_limitations + m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) + end + for rid in Symbol.(unconstrain_reactions) m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) end From 80c571b6239e6b731e8753a0ccd4907692c890fe Mon Sep 17 00:00:00 2001 From: stelmo Date: Mon, 1 Jan 2024 13:29:20 +0000 Subject: [PATCH 449/531] automatic formatting triggered by @stelmo on PR #813 --- docs/src/examples/05-enzyme-constrained-models.jl | 8 +++++++- src/frontend/enzyme_constrained_flux_balance_analysis.jl | 5 +---- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 30ea6205e..b3b5df1d0 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -144,7 +144,13 @@ m = add_enzyme_constraints!( ) # add capacity limitation -m *= :total_proteome_bound^enzyme_capacity(m.enzymes, gene_molar_masses, A.genes(model), total_enzyme_capacity) +m *= + :total_proteome_bound^enzyme_capacity( + m.enzymes, + gene_molar_masses, + A.genes(model), + total_enzyme_capacity, + ) # solve the model ec_solution = optimized_constraints( diff --git a/src/frontend/enzyme_constrained_flux_balance_analysis.jl b/src/frontend/enzyme_constrained_flux_balance_analysis.jl index 31b26429a..a91a7d669 100644 --- a/src/frontend/enzyme_constrained_flux_balance_analysis.jl +++ b/src/frontend/enzyme_constrained_flux_balance_analysis.jl @@ -30,10 +30,7 @@ function enzyme_constrained_flux_balance_analysis( m += :enzymes^enzyme_variables(model) # add enzyme equality constraints (stoichiometry) - m = add_enzyme_constraints!( - m, - reaction_isozymes, - ) + m = add_enzyme_constraints!(m, reaction_isozymes) # add capacity limitations for (id, gids, cap) in capacity_limitations From 807c7a2544e3c0249436c23696fe39341e4d195e Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 1 Jan 2024 14:31:55 +0100 Subject: [PATCH 450/531] improve docs for capacity a little and open to support reals --- src/builders/enzymes.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 8f8b96c23..c2ed1e8bc 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -127,13 +127,14 @@ end """ $(TYPEDSIGNATURES) -Create enzyme capacity limitation. +Create enzyme capacity limitation. Bounds the gene product masses (concentration +* molar mass) of gene products in `enzyme_ids` by `capacity`. """ function enzyme_capacity( enzymes::C.ConstraintTree, gene_molar_masses::Dict{String,Float64}, enzyme_ids::Vector{String}, - capacity::Float64, + capacity::Real, ) C.Constraint( value = sum( From 99e83e3e10bbe9e2e4b288d3112a35b57d575f10 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Mon, 1 Jan 2024 14:35:47 +0100 Subject: [PATCH 451/531] create helper and open --- src/builders/enzymes.jl | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index c2ed1e8bc..2baff628b 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -127,23 +127,37 @@ end """ $(TYPEDSIGNATURES) -Create enzyme capacity limitation. Bounds the gene product masses (concentration -* molar mass) of gene products in `enzyme_ids` by `capacity`. +Create a enzyme capacity limitation. Bounds the gene product masses (concentration +* molar mass) of gene products in `enzyme_ids` by `capacity_bound`. """ function enzyme_capacity( enzymes::C.ConstraintTree, gene_molar_masses::Dict{String,Float64}, enzyme_ids::Vector{String}, - capacity::Real, + capacity_bound::C.Bound, ) C.Constraint( value = sum( enzymes[Symbol(gid)].value * gene_molar_masses[gid] for gid in enzyme_ids ), - bound = C.Between(0.0, capacity), + bound = capacity_bound, ) end +""" +$(TYPEDSIGNATURES) + +Create an enzyme capacity limitation. Bounds the gene product masses +(concentration * molar mass) of gene products in `enzyme_ids` between `[0, capacity]`. +""" +enzyme_capacity( + enzymes::C.ConstraintTree, + gene_molar_masses::Dict{String,Float64}, + enzyme_ids::Vector{String}, + capacity::Float64, +) = enzyme_capacity(enzymes, gene_molar_masses, enzyme_ids, C.Between(0.0, capacity)) + + export enzyme_capacity """ From 830b36fe5b95eca41e68b2654cce8b803b3ebd1c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 Jan 2024 09:42:36 +0100 Subject: [PATCH 452/531] add license headers --- docs/make.jl | 16 ++++++++++++++++ docs/src/examples/01-loading-and-saving.jl | 15 +++++++++++++++ docs/src/examples/02-flux-balance-analysis.jl | 15 +++++++++++++++ docs/src/examples/02a-optimizer-parameters.jl | 15 +++++++++++++++ docs/src/examples/02b-model-modifications.jl | 15 +++++++++++++++ .../examples/02c-constraint-modifications.jl | 15 +++++++++++++++ .../examples/03-parsimonious-flux-balance.jl | 15 +++++++++++++++ .../04-minimization-of-metabolic-adjustment.jl | 15 +++++++++++++++ .../examples/05-enzyme-constrained-models.jl | 16 ++++++++++++++++ docs/src/examples/06-thermodynamic-models.jl | 16 ++++++++++++++++ docs/src/examples/07-loopless-models.jl | 15 +++++++++++++++ docs/src/examples/08-community-models.jl | 16 ++++++++++++++++ src/COBREXA.jl | 16 ++++++++++++++++ src/builders/communities.jl | 16 ++++++++++++++++ src/builders/core.jl | 15 +++++++++++++++ src/builders/enzymes.jl | 16 ++++++++++++++++ src/builders/genes.jl | 15 +++++++++++++++ src/builders/loopless.jl | 15 +++++++++++++++ src/builders/objectives.jl | 15 +++++++++++++++ src/builders/thermodynamic.jl | 16 ++++++++++++++++ ...enzyme_constrained_flux_balance_analysis.jl | 15 +++++++++++++++ src/frontend/flux_balance_analysis.jl | 15 +++++++++++++++ src/frontend/loopless_flux_balance_analysis.jl | 15 +++++++++++++++ src/frontend/max_min_driving_force_analysis.jl | 15 +++++++++++++++ ...ization_of_metabolic_adjustment_analysis.jl | 15 +++++++++++++++ src/frontend/parsimonious_flux_balance.jl | 15 +++++++++++++++ src/io.jl | 15 +++++++++++++++ src/misc/bounds.jl | 15 +++++++++++++++ src/misc/modifications.jl | 15 +++++++++++++++ src/misc/utils.jl | 15 +++++++++++++++ src/solver.jl | 18 ++++++++++++++++++ src/types.jl | 15 +++++++++++++++ test/aqua.jl | 15 +++++++++++++++ test/data_downloaded.jl | 1 + test/runtests.jl | 16 ++++++++++++++++ 35 files changed, 523 insertions(+) diff --git a/docs/make.jl b/docs/make.jl index 0c1d35ef0..bff410b4e 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2023, University of Luxembourg +# Copyright (c) 2023, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + using Documenter using Literate, JSON using COBREXA diff --git a/docs/src/examples/01-loading-and-saving.jl b/docs/src/examples/01-loading-and-saving.jl index 05e8adc68..7db77f046 100644 --- a/docs/src/examples/01-loading-and-saving.jl +++ b/docs/src/examples/01-loading-and-saving.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Loading and saving models! using COBREXA diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 88d0c1ee6..87d9f2f89 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Flux balance analysis (FBA) # Here we use [`flux_balance_analysis`](@ref) and several related functions to diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl index e05a2b0cb..1a81f20cc 100644 --- a/docs/src/examples/02a-optimizer-parameters.jl +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Changing optimizer parameters # # Many optimizers require fine-tuning to produce best results. You can pass in diff --git a/docs/src/examples/02b-model-modifications.jl b/docs/src/examples/02b-model-modifications.jl index 160516e2f..cb281a9b1 100644 --- a/docs/src/examples/02b-model-modifications.jl +++ b/docs/src/examples/02b-model-modifications.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Making adjustments to the model # # Typically, we do not need to solve the models as they come from the authors diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index c9f616ab7..07f0f690e 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Making adjustments to the constraint system # # In the [previous example about model diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index d21606053..d69ad5a07 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Parsimonious flux balance analysis # We will use [`parsimonious_flux_balance_analysis`](@ref) and diff --git a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl index e950c57a9..2d81eb2ac 100644 --- a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl +++ b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Minimization of metabolic adjustment analysis # TODO MOMA citation diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index b3b5df1d0..a65c178e9 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Enzyme constrained models using COBREXA diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 824be399b..603192272 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Thermodynamic models using COBREXA diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index d03fbffc9..1c6b9ac4d 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Loopless flux balance analysis (ll-FBA) # Here we wil add loopless constraints to a flux balance model to ensure that diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 794fcd8ab..55a1fc389 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg #src +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf #src +# #src +# Licensed under the Apache License, Version 2.0 (the "License"); #src +# you may not use this file except in compliance with the License. #src +# You may obtain a copy of the License at #src +# #src +# http://www.apache.org/licenses/LICENSE-2.0 #src +# #src +# Unless required by applicable law or agreed to in writing, software #src +# distributed under the License is distributed on an "AS IS" BASIS, #src +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. #src +# See the License for the specific language governing permissions and #src +# limitations under the License. #src + # # Community FBA models using COBREXA diff --git a/src/COBREXA.jl b/src/COBREXA.jl index a4152e951..3704b76de 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ module COBREXA diff --git a/src/builders/communities.jl b/src/builders/communities.jl index b282cb6ca..239639d97 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/builders/core.jl b/src/builders/core.jl index cbfdbbcf2..bf9f1c5cc 100644 --- a/src/builders/core.jl +++ b/src/builders/core.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 2baff628b..d53fe8d75 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/builders/genes.jl b/src/builders/genes.jl index 0f6ddec90..0e96d46dc 100644 --- a/src/builders/genes.jl +++ b/src/builders/genes.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) """ diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 4cf5d27c1..0f66d642e 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index e6f7a8822..5320bec0c 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 9c0ee413e..e0a0d9c82 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/enzyme_constrained_flux_balance_analysis.jl b/src/frontend/enzyme_constrained_flux_balance_analysis.jl index a91a7d669..16b7a0a1b 100644 --- a/src/frontend/enzyme_constrained_flux_balance_analysis.jl +++ b/src/frontend/enzyme_constrained_flux_balance_analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/flux_balance_analysis.jl b/src/frontend/flux_balance_analysis.jl index 626eb3749..82e28e3ae 100644 --- a/src/frontend/flux_balance_analysis.jl +++ b/src/frontend/flux_balance_analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/loopless_flux_balance_analysis.jl b/src/frontend/loopless_flux_balance_analysis.jl index f55527615..cc2a61376 100644 --- a/src/frontend/loopless_flux_balance_analysis.jl +++ b/src/frontend/loopless_flux_balance_analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/max_min_driving_force_analysis.jl b/src/frontend/max_min_driving_force_analysis.jl index f014ad29c..1df1251f4 100644 --- a/src/frontend/max_min_driving_force_analysis.jl +++ b/src/frontend/max_min_driving_force_analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/minimization_of_metabolic_adjustment_analysis.jl b/src/frontend/minimization_of_metabolic_adjustment_analysis.jl index c9fbf00b7..b1af4944e 100644 --- a/src/frontend/minimization_of_metabolic_adjustment_analysis.jl +++ b/src/frontend/minimization_of_metabolic_adjustment_analysis.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/frontend/parsimonious_flux_balance.jl b/src/frontend/parsimonious_flux_balance.jl index 5ad5ac2bf..c76543082 100644 --- a/src/frontend/parsimonious_flux_balance.jl +++ b/src/frontend/parsimonious_flux_balance.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/io.jl b/src/io.jl index b3b59f640..64c58d56b 100644 --- a/src/io.jl +++ b/src/io.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/misc/bounds.jl b/src/misc/bounds.jl index 5ecf68cc3..fc47ce39e 100644 --- a/src/misc/bounds.jl +++ b/src/misc/bounds.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/misc/modifications.jl b/src/misc/modifications.jl index dccec943e..1484a3634 100644 --- a/src/misc/modifications.jl +++ b/src/misc/modifications.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + #TODO: at this point, consider renaming the whole thing to "settings" """ diff --git a/src/misc/utils.jl b/src/misc/utils.jl index 7c559b045..435cb0961 100644 --- a/src/misc/utils.jl +++ b/src/misc/utils.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) diff --git a/src/solver.jl b/src/solver.jl index e00fbb58c..5059b502e 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) @@ -59,6 +74,7 @@ Objective sense for finding the minimal value of the objective. Same as `JuMP.MIN_SENSE`. """ const Minimal = J.MIN_SENSE +export Minimal """ Maximal @@ -68,6 +84,7 @@ Objective sense for finding the maximal value of the objective. Same as `JuMP.MAX_SENSE`. """ const Maximal = J.MAX_SENSE +export Maximal """ Maximal @@ -77,6 +94,7 @@ Objective sense for finding the any feasible value of the objective. Same as `JuMP.FEASIBILITY_SENSE`. """ const Feasible = J.FEASIBILITY_SENSE +export Feasible """ $(TYPEDSIGNATURES) diff --git a/src/types.jl b/src/types.jl index 679d6b9e9..56d6319e1 100644 --- a/src/types.jl +++ b/src/types.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ Maybe{X} diff --git a/test/aqua.jl b/test/aqua.jl index 1688b83e3..4c083591b 100644 --- a/test/aqua.jl +++ b/test/aqua.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + @testset "Automated QUality Assurance" begin # We can't do Aqua.test_all here (yet) because the ambiguity tests fail in # deps. Instead let's pick the other sensible tests. diff --git a/test/data_downloaded.jl b/test/data_downloaded.jl index 5660417d5..c7bc21985 100644 --- a/test/data_downloaded.jl +++ b/test/data_downloaded.jl @@ -1,3 +1,4 @@ + # TODO this should be downloaded by documentation scripts isdir("downloaded") || mkdir("downloaded") diff --git a/test/runtests.jl b/test/runtests.jl index 88b05b85b..232973799 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,3 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + using COBREXA, Test using Aqua From 0e1309028897dd29586fee02cbca5751d68b1d32 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 Jan 2024 11:22:26 +0100 Subject: [PATCH 453/531] cleanup start --- src/COBREXA.jl | 19 +++++----- src/builders/fbc.jl | 47 +++++++++++++++++++++++++ src/builders/{genes.jl => knockouts.jl} | 0 src/builders/{core.jl => unsigned.jl} | 33 +---------------- 4 files changed, 58 insertions(+), 41 deletions(-) create mode 100644 src/builders/fbc.jl rename src/builders/{genes.jl => knockouts.jl} (100%) rename src/builders/{core.jl => unsigned.jl} (69%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 3704b76de..205f2567f 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -48,24 +48,25 @@ include("io.jl") include("solver.jl") # these functions build or extend constrainttrees of metabolic models -include("builders/core.jl") -include("builders/genes.jl") -include("builders/objectives.jl") +include("builders/communities.jl") include("builders/enzymes.jl") -include("builders/thermodynamic.jl") +include("builders/fbc.jl") +include("builders/knockouts.jl") include("builders/loopless.jl") -include("builders/communities.jl") +include("builders/objectives.jl") +include("builders/thermodynamic.jl") +include("builders/unsigned.jl") # these are the one shot analysis functions -include("frontend/flux_balance_analysis.jl") -include("frontend/parsimonious_flux_balance.jl") -include("frontend/minimization_of_metabolic_adjustment_analysis.jl") include("frontend/enzyme_constrained_flux_balance_analysis.jl") +include("frontend/flux_balance_analysis.jl") include("frontend/loopless_flux_balance_analysis.jl") include("frontend/max_min_driving_force_analysis.jl") +include("frontend/minimization_of_metabolic_adjustment_analysis.jl") +include("frontend/parsimonious_flux_balance.jl") -include("misc/modifications.jl") include("misc/bounds.jl") +include("misc/modifications.jl") include("misc/utils.jl") end # module COBREXA diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl new file mode 100644 index 000000000..a19d0a40b --- /dev/null +++ b/src/builders/fbc.jl @@ -0,0 +1,47 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +A constraint tree that models the content of the given instance of +`AbstractFBCModel`. + +The constructed tree contains subtrees `fluxes` (with the reaction-defining +"variables") and `flux_stoichiometry` (with the metabolite-balance-defining +constraints), and a single constraint `objective` thad describes the objective +function of the model. +""" +function fbc_model_constraints(model::A.AbstractFBCModel) + rxns = Symbol.(A.reactions(model)) + mets = Symbol.(A.metabolites(model)) + lbs, ubs = A.bounds(model) + stoi = A.stoichiometry(model) + bal = A.balance(model) + obj = A.objective(model) + + #TODO: is sparse() required below? + return C.ConstraintTree( + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :flux_stoichiometry^C.ConstraintTree( + met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) + for (met, row, b) in zip(mets, eachrow(stoi), bal) + ) * + :objective^C.Constraint(C.LinearValue(sparse(obj))), + ) +end + +export fbc_model_constraints diff --git a/src/builders/genes.jl b/src/builders/knockouts.jl similarity index 100% rename from src/builders/genes.jl rename to src/builders/knockouts.jl diff --git a/src/builders/core.jl b/src/builders/unsigned.jl similarity index 69% rename from src/builders/core.jl rename to src/builders/unsigned.jl index bf9f1c5cc..e72ec53d6 100644 --- a/src/builders/core.jl +++ b/src/builders/unsigned.jl @@ -17,38 +17,6 @@ """ $(TYPEDSIGNATURES) -A constraint tree that models the content of the given instance of -`AbstractFBCModel`. - -The constructed tree contains subtrees `fluxes` (with the reaction-defining -"variables") and `flux_stoichiometry` (with the metabolite-balance-defining -constraints), and a single constraint `objective` thad describes the objective -function of the model. -""" -function fbc_model_constraints(model::A.AbstractFBCModel) - rxns = Symbol.(A.reactions(model)) - mets = Symbol.(A.metabolites(model)) - lbs, ubs = A.bounds(model) - stoi = A.stoichiometry(model) - bal = A.balance(model) - obj = A.objective(model) - - #TODO: is sparse() required below? - return C.ConstraintTree( - :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * - :flux_stoichiometry^C.ConstraintTree( - met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) - for (met, row, b) in zip(mets, eachrow(stoi), bal) - ) * - :objective^C.Constraint(C.LinearValue(sparse(obj))), - ) -end - -export fbc_model_constraints - -""" -$(TYPEDSIGNATURES) - Shortcut for allocation non-negative ("unsigned") variables. The argument `keys` is forwarded to `ConstraintTrees.variables` as `keys`. """ @@ -97,6 +65,7 @@ sign_split_constraints(; export sign_split_constraints +# TODO: docs, doesn't apply to fluxes only function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) keys = Symbol[] for (id, flux) in fluxes From a50bff8d417336e8e8d803ffa5099d813a3da2a5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 Jan 2024 11:24:26 +0100 Subject: [PATCH 454/531] keep filename length at bay --- src/COBREXA.jl | 12 ++++++------ .../{flux_balance_analysis.jl => balance.jl} | 0 ...lux_balance_analysis.jl => enzyme_constrained.jl} | 0 ...loopless_flux_balance_analysis.jl => loopless.jl} | 0 .../{max_min_driving_force_analysis.jl => mmdf.jl} | 0 ...n_of_metabolic_adjustment_analysis.jl => moma.jl} | 0 ...{parsimonious_flux_balance.jl => parsimonious.jl} | 0 7 files changed, 6 insertions(+), 6 deletions(-) rename src/frontend/{flux_balance_analysis.jl => balance.jl} (100%) rename src/frontend/{enzyme_constrained_flux_balance_analysis.jl => enzyme_constrained.jl} (100%) rename src/frontend/{loopless_flux_balance_analysis.jl => loopless.jl} (100%) rename src/frontend/{max_min_driving_force_analysis.jl => mmdf.jl} (100%) rename src/frontend/{minimization_of_metabolic_adjustment_analysis.jl => moma.jl} (100%) rename src/frontend/{parsimonious_flux_balance.jl => parsimonious.jl} (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 205f2567f..fb57a3e67 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -58,12 +58,12 @@ include("builders/thermodynamic.jl") include("builders/unsigned.jl") # these are the one shot analysis functions -include("frontend/enzyme_constrained_flux_balance_analysis.jl") -include("frontend/flux_balance_analysis.jl") -include("frontend/loopless_flux_balance_analysis.jl") -include("frontend/max_min_driving_force_analysis.jl") -include("frontend/minimization_of_metabolic_adjustment_analysis.jl") -include("frontend/parsimonious_flux_balance.jl") +include("frontend/balance.jl") +include("frontend/parsimonious.jl") +include("frontend/mmdf.jl") +include("frontend/moma.jl") +include("frontend/loopless.jl") +include("frontend/enzyme_constrained.jl") include("misc/bounds.jl") include("misc/modifications.jl") diff --git a/src/frontend/flux_balance_analysis.jl b/src/frontend/balance.jl similarity index 100% rename from src/frontend/flux_balance_analysis.jl rename to src/frontend/balance.jl diff --git a/src/frontend/enzyme_constrained_flux_balance_analysis.jl b/src/frontend/enzyme_constrained.jl similarity index 100% rename from src/frontend/enzyme_constrained_flux_balance_analysis.jl rename to src/frontend/enzyme_constrained.jl diff --git a/src/frontend/loopless_flux_balance_analysis.jl b/src/frontend/loopless.jl similarity index 100% rename from src/frontend/loopless_flux_balance_analysis.jl rename to src/frontend/loopless.jl diff --git a/src/frontend/max_min_driving_force_analysis.jl b/src/frontend/mmdf.jl similarity index 100% rename from src/frontend/max_min_driving_force_analysis.jl rename to src/frontend/mmdf.jl diff --git a/src/frontend/minimization_of_metabolic_adjustment_analysis.jl b/src/frontend/moma.jl similarity index 100% rename from src/frontend/minimization_of_metabolic_adjustment_analysis.jl rename to src/frontend/moma.jl diff --git a/src/frontend/parsimonious_flux_balance.jl b/src/frontend/parsimonious.jl similarity index 100% rename from src/frontend/parsimonious_flux_balance.jl rename to src/frontend/parsimonious.jl From 96b3934cb904fd050aee792a39e2178e5fa056d8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 2 Jan 2024 11:32:43 +0100 Subject: [PATCH 455/531] tidy up imports --- src/COBREXA.jl | 10 ++++++---- src/builders/fbc.jl | 9 +++++---- src/builders/thermodynamic.jl | 3 ++- src/frontend/loopless.jl | 5 +++-- 4 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index fb57a3e67..035d7add3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -40,14 +40,15 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J -import SparseArrays: sparse, findnz -import LinearAlgebra: nullspace +import SparseArrays include("types.jl") + +# core functionality include("io.jl") include("solver.jl") -# these functions build or extend constrainttrees of metabolic models +# conversion of various stuff to constraint trees include("builders/communities.jl") include("builders/enzymes.jl") include("builders/fbc.jl") @@ -57,7 +58,7 @@ include("builders/objectives.jl") include("builders/thermodynamic.jl") include("builders/unsigned.jl") -# these are the one shot analysis functions +# simplified front-ends for the above include("frontend/balance.jl") include("frontend/parsimonious.jl") include("frontend/mmdf.jl") @@ -65,6 +66,7 @@ include("frontend/moma.jl") include("frontend/loopless.jl") include("frontend/enzyme_constrained.jl") +# utilities include("misc/bounds.jl") include("misc/modifications.jl") include("misc/utils.jl") diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index a19d0a40b..92f83bd5b 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -33,14 +33,15 @@ function fbc_model_constraints(model::A.AbstractFBCModel) bal = A.balance(model) obj = A.objective(model) - #TODO: is sparse() required below? return C.ConstraintTree( :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( - met => C.Constraint(value = C.LinearValue(sparse(row)), bound = C.EqualTo(b)) - for (met, row, b) in zip(mets, eachrow(stoi), bal) + met => C.Constraint( + value = C.LinearValue(SparseArrays.sparse(row)), + bound = C.EqualTo(b), + ) for (met, row, b) in zip(mets, eachrow(stoi), bal) ) * - :objective^C.Constraint(C.LinearValue(sparse(obj))), + :objective^C.Constraint(C.LinearValue(SparseArrays.sparse(obj))), ) end diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index e0a0d9c82..8a8f186d5 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -72,7 +72,8 @@ function build_max_min_driving_force_model( model_mets = A.metabolites(model) dG0s_met_ids_stoichs = Vector{Tuple{Float64,Vector{String},Vector{Float64}}}() for rxn in rxns # prepare to create CT below - met_idxs, stoich_coeffs = findnz(stoi[:, findfirst(==(rxn), model_rxns)]) + met_idxs, stoich_coeffs = + SparseArrays.findnz(stoi[:, findfirst(==(rxn), model_rxns)]) met_ids = model_mets[met_idxs] dG0 = reaction_standard_gibbs_free_energies[rxn] push!(dG0s_met_ids_stoichs, (dG0, met_ids, stoich_coeffs)) diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index cc2a61376..6d1c49ace 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -55,8 +55,9 @@ function loopless_flux_balance_analysis( internal_reaction_ids = last.(internal_reactions) internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below - internal_reaction_stoichiometry_nullspace_columns = - eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) # no sparse nullspace function + internal_reaction_stoichiometry_nullspace_columns = eachcol( + LinearAlgebra.nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])), + ) # no sparse nullspace function m = add_loopless_constraints!( m, From 4db50b56d20e70282321ff1e4bd20704b83fa03c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 3 Jan 2024 12:30:17 +0100 Subject: [PATCH 456/531] evade `isa` in constraint creation --- src/COBREXA.jl | 1 + src/builders/unsigned.jl | 3 ++- src/solver.jl | 24 ++++++++++++++---------- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 035d7add3..820186124 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -40,6 +40,7 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C import JuMP as J +import LinearAlgebra import SparseArrays include("types.jl") diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index e72ec53d6..7885de866 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -18,12 +18,13 @@ $(TYPEDSIGNATURES) Shortcut for allocation non-negative ("unsigned") variables. The argument -`keys` is forwarded to `ConstraintTrees.variables` as `keys`. +`keys` is forwarded to `ConstraintTrees.variables`. """ unsigned_variables(; keys) = C.variables(; keys, bounds = C.Between(0.0, Inf)) export unsigned_variables + """ $(TYPEDSIGNATURES) diff --git a/src/solver.jl b/src/solver.jl index 5059b502e..d9f4f85c3 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -33,21 +33,25 @@ function optimization_model( isnothing(objective) || J.@objective(model, sense, C.substitute(objective, x)) # constraints + function add_constraint(v::C.Value, b::C.EqualTo) + J.@constraint(model, C.substitute(v, x) == b.equal_to) + end + function add_constraint(v::C.Value, b::C.Between) + vx = C.substitute(v, x) + isinf(b.lower) || J.@constraint(model, vx >= b.lower) + isinf(b.upper) || J.@constraint(model, vx <= b.upper) + end + function add_constraint(v::C.Value, _::Binary) + boolean = J.@variable(model, binary = true) + J.@constraint(model, C.substitute(v, x) == boolean) + end function add_constraint(c::C.Constraint) - if c.bound isa C.EqualTo - J.@constraint(model, C.substitute(c.value, x) == c.bound.equal_to) - elseif c.bound isa C.Between - val = C.substitute(c.value, x) - isinf(c.bound.lower) || J.@constraint(model, val >= c.bound.lower) - isinf(c.bound.upper) || J.@constraint(model, val <= c.bound.upper) - elseif c.bound isa Binary - anon_bool = J.@variable(model, binary = true) - J.@constraint(model, C.substitute(c.value, x) == anon_bool) - end + add_constraint(c.value, c.bound) end function add_constraint(c::C.ConstraintTree) add_constraint.(values(c)) end + add_constraint(cs) return model From fd1c5e8e649c9166deccf8d59fd7ff9ee5d1530d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 3 Jan 2024 15:05:03 +0100 Subject: [PATCH 457/531] rename modifications to settings --- docs/src/examples/02a-optimizer-parameters.jl | 6 +++--- docs/src/examples/02c-constraint-modifications.jl | 6 +++--- docs/src/examples/03-parsimonious-flux-balance.jl | 6 +++--- docs/src/examples/05-enzyme-constrained-models.jl | 4 ++-- docs/src/examples/06-thermodynamic-models.jl | 4 ++-- docs/src/examples/08-community-models.jl | 4 ++-- src/COBREXA.jl | 2 +- src/frontend/balance.jl | 4 ++-- src/frontend/enzyme_constrained.jl | 6 +++--- src/frontend/loopless.jl | 4 ++-- src/frontend/mmdf.jl | 6 +++--- src/frontend/moma.jl | 10 +++++----- src/frontend/parsimonious.jl | 6 +++--- src/misc/{modifications.jl => settings.jl} | 0 src/solver.jl | 6 +++--- 15 files changed, 37 insertions(+), 37 deletions(-) rename src/misc/{modifications.jl => settings.jl} (100%) diff --git a/docs/src/examples/02a-optimizer-parameters.jl b/docs/src/examples/02a-optimizer-parameters.jl index 1a81f20cc..0bc355c99 100644 --- a/docs/src/examples/02a-optimizer-parameters.jl +++ b/docs/src/examples/02a-optimizer-parameters.jl @@ -17,7 +17,7 @@ # # Changing optimizer parameters # # Many optimizers require fine-tuning to produce best results. You can pass in -# additional optimizer settings via the `modifications` parameter of +# additional optimizer settings via the `settings` parameter of # [`flux_balance_analysis`](@ref). These include e.g. # # - [`set_optimizer_attribute`](@ref) (typically allowing you to tune e.g. @@ -47,7 +47,7 @@ model = load_model("e_coli_core.json") solution = flux_balance_analysis( model, Tulip.Optimizer; - modifications = [silence, set_optimizer_attribute("IPM_IterationsLimit", 1000)], + settings = [silence, set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) @test !isnothing(solution) #src @@ -60,7 +60,7 @@ solution = flux_balance_analysis( solution = flux_balance_analysis( model, Tulip.Optimizer; - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 2)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 2)], ) println(solution) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 07f0f690e..9afaa534f 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -62,7 +62,7 @@ vt = optimized_constraints( forced_mixed_fermentation, objective = forced_mixed_fermentation.objective.value, optimizer = Tulip.Optimizer, - modifications = [silence], + settings = [silence], ) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src @@ -78,7 +78,7 @@ vt = optimized_constraints( ctmodel, objective = ctmodel.objective.value, optimizer = Tulip.Optimizer, - modifications = [silence], + settings = [silence], ) @test isnothing(vt) #src @@ -90,7 +90,7 @@ vt = optimized_constraints( ctmodel, objective = ctmodel.objective.value, optimizer = Tulip.Optimizer, - modifications = [silence], + settings = [silence], ) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index d69ad5a07..eee4439ec 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -40,11 +40,11 @@ model = load_model("e_coli_core.json") # load the model # Use the convenience function to run standard pFBA on vt = - parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; modifications = [silence]) + parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; settings = [silence]) # Or use the piping functionality -model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; modifications = [silence]) +model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; settings = [silence]) @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src @test sum(x^2 for x in values(vt.fluxes)) < 15000 #src @@ -82,7 +82,7 @@ vt = minimize_metabolic_adjustment(model, ref_sol, Gurobi.Optimizer) # Or use the piping functionality model |> -minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; modifications = [silence]) +minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; settings = [silence]) @test isapprox(vt.:momaobjective, 0.81580806; atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index a65c178e9..caad58073 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -120,7 +120,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( reaction_isozymes, gene_molar_masses, [("total_proteome_bound", A.genes(model), total_enzyme_capacity)]; - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], unconstrain_reactions = ["EX_glc__D_e"], optimizer = Tulip.Optimizer, ) @@ -173,5 +173,5 @@ ec_solution = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], ) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-thermodynamic-models.jl index 603192272..369492bc5 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-thermodynamic-models.jl @@ -116,7 +116,7 @@ mmdf_solution = max_min_driving_force_analysis( concentration_ub = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], optimizer = Tulip.Optimizer, ) @@ -150,5 +150,5 @@ mmdf_solution = optimized_constraints( m; objective = m.max_min_driving_force.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], ) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index 55a1fc389..a044b7ecc 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -110,7 +110,7 @@ sol = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) @test isapprox(sol.:objective, 0.66686196344, atol = TEST_TOLERANCE) #src @@ -127,7 +127,7 @@ sol = optimized_constraints( m; objective = m.objective.value, optimizer = Tulip.Optimizer, - modifications = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], + settings = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], ) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 820186124..43748a909 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -69,7 +69,7 @@ include("frontend/enzyme_constrained.jl") # utilities include("misc/bounds.jl") -include("misc/modifications.jl") +include("misc/settings.jl") include("misc/utils.jl") end # module COBREXA diff --git a/src/frontend/balance.jl b/src/frontend/balance.jl index 82e28e3ae..624f584eb 100644 --- a/src/frontend/balance.jl +++ b/src/frontend/balance.jl @@ -39,7 +39,7 @@ $(TYPEDSIGNATURES) Pipe-able overload of [`flux_balance_analysis`](@ref). """ -flux_balance_analysis(optimizer; modifications = []) = - m -> flux_balance_analysis(m, optimizer; modifications) +flux_balance_analysis(optimizer; settings = []) = + m -> flux_balance_analysis(m, optimizer; settings) export flux_balance_analysis diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index 16b7a0a1b..ab0779c02 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -25,7 +25,7 @@ The latter is a vector of tuples, where each tuple represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. Typically, `model` has bounded exchange reactions, which are unnecessary in enzyme constrained models. Unbound these reactions by listing their IDs in `unconstrain_reactions`, which -makes them reversible. Optimization `modifications` are directly forwarded. +makes them reversible. Optimization `settings` are directly forwarded. In the event that your model requires more complex build steps, consider constructing it manually by using [`add_enzyme_constraints!`](@ref). @@ -37,7 +37,7 @@ function enzyme_constrained_flux_balance_analysis( capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; optimizer, unconstrain_reactions = String[], - modifications = [], + settings = [], ) m = fbc_model_constraints(model) @@ -56,7 +56,7 @@ function enzyme_constrained_flux_balance_analysis( m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) end - optimized_constraints(m; objective = m.objective.value, optimizer, modifications) + optimized_constraints(m; objective = m.objective.value, optimizer, settings) end export enzyme_constrained_flux_balance_analysis diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index 6d1c49ace..0c1a8bc0c 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -41,7 +41,7 @@ function loopless_flux_balance_analysis( model; max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic strict_inequality_tolerance = 1.0, # heuristic from paper - modifications = [], + settings = [], optimizer, ) @@ -68,7 +68,7 @@ function loopless_flux_balance_analysis( ) # solve - optimized_constraints(m; objective = m.objective.value, optimizer, modifications) + optimized_constraints(m; objective = m.objective.value, optimizer, settings) end export loopless_flux_balance_analysis diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 1df1251f4..55be1fe01 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -67,7 +67,7 @@ supplied). `T` and `R` can be specified in the corresponding units; defaults are K and kJ/K/mol. The unit of metabolite concentrations is typically molar, and the ΔG⁰s have units of kJ/mol. Other units can be used, as long as they are consistent. -As usual, optimizer settings can be changed with `modifications`. +As usual, optimizer settings can be changed with `settings`. """ function max_min_driving_force_analysis( model::A.AbstractFBCModel, @@ -82,7 +82,7 @@ function max_min_driving_force_analysis( T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol ignore_reaction_ids = String[], - modifications = [], + settings = [], optimizer, ) m = build_max_min_driving_force_model( @@ -113,7 +113,7 @@ function max_min_driving_force_analysis( m; objective = m.max_min_driving_force.value, optimizer, - modifications, + settings, ) end diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index b1af4944e..b3acc69f0 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -57,7 +57,7 @@ A slightly easier-to-use version of [`minimization_of_metabolic_adjustment_analy computes the reference flux as the optimal solution of the [`reference_model`](@ref). The reference flux is calculated using `reference_optimizer` and `reference_modifications`, which default to the -`optimizer` and `modifications`. +`optimizer` and `settings`. Leftover arguments are passed to the overload of [`minimization_of_metabolic_adjustment_analysis`](@ref) that accepts the reference flux @@ -68,15 +68,15 @@ function minimization_of_metabolic_adjustment_analysis( reference_model::A.AbstractFBCModel, optimizer; reference_optimizer = optimizer, - modifications = [], - reference_modifications = modifications, + settings = [], + reference_settings = settings, kwargs..., ) reference_constraints = fbc_model_constraints(reference_model) reference_fluxes = optimized_constraints( reference_constraints; optimizer = reference_optimizer, - modifications = reference_modifications, + settings = reference_settings, output = reference_constraints.fluxes, ) isnothing(reference_fluxes) && return nothing @@ -84,7 +84,7 @@ function minimization_of_metabolic_adjustment_analysis( model, reference_fluxes, optimizer; - modifications, + settings, kwargs..., ) end diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index c76543082..1f7d732f5 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -20,7 +20,7 @@ $(TYPEDSIGNATURES) Optimize the system of `constraints` to get the optimal `objective` value. Then try to find a "parsimonious" solution with the same `objective` value, which optimizes the `parsimonious_objective` (possibly also switching optimization -sense, optimizer, and adding more modifications). +sense, optimizer, and adding more settings). For efficiency, everything is performed on a single instance of JuMP model. @@ -30,7 +30,7 @@ in [`parsimonious_flux_balance`](@ref). function parsimonious_optimized_constraints( constraints::C.ConstraintTreeElem; objective::C.Value, - modifications = [], + settings = [], parsimonious_objective::C.Value, parsimonious_optimizer = nothing, parsimonious_sense = J.MIN_SENSE, @@ -42,7 +42,7 @@ function parsimonious_optimized_constraints( # first solve the optimization problem with the original objective om = optimization_model(constraints; objective, kwargs...) - for m in modifications + for m in settings m(om) end J.optimize!(om) diff --git a/src/misc/modifications.jl b/src/misc/settings.jl similarity index 100% rename from src/misc/modifications.jl rename to src/misc/settings.jl diff --git a/src/solver.jl b/src/solver.jl index d9f4f85c3..041c677cf 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -104,7 +104,7 @@ export Feasible $(TYPEDSIGNATURES) Make an JuMP model out of `constraints` using [`optimization_model`](@ref) -(most arguments are forwarded there), then apply the `modifications`, optimize +(most arguments are forwarded there), then apply the `settings`, optimize the model, and return either `nothing` if the optimization failed, or `output` substituted with the solved values (`output` defaults to `constraints`. @@ -113,12 +113,12 @@ For a "nice" version for simpler finding of metabolic model optima, use """ function optimized_constraints( constraints::C.ConstraintTreeElem; - modifications = [], + settings = [], output = constraints, kwargs..., ) om = optimization_model(constraints; kwargs...) - for m in modifications + for m in settings m(om) end J.optimize!(om) From 3ad2a35e6469cde0d28769023a2a5ad2f43bf0a8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 3 Jan 2024 15:30:29 +0100 Subject: [PATCH 458/531] start cleaning the variables --- Project.toml | 2 +- .../examples/03-parsimonious-flux-balance.jl | 3 +-- src/COBREXA.jl | 1 + src/builders/unsigned.jl | 20 ++++++++++++++++--- src/builders/variables.jl | 8 ++++++++ src/frontend/mmdf.jl | 7 +------ 6 files changed, 29 insertions(+), 12 deletions(-) create mode 100644 src/builders/variables.jl diff --git a/Project.toml b/Project.toml index f598b46c7..cc5188a5d 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.8" +ConstraintTrees = "0.9" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index eee4439ec..13a25ca16 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -39,8 +39,7 @@ model = load_model("e_coli_core.json") # load the model # Use the convenience function to run standard pFBA on -vt = - parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; settings = [silence]) +vt = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; settings = [silence]) # Or use the piping functionality diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 43748a909..49b3a7fb1 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -58,6 +58,7 @@ include("builders/loopless.jl") include("builders/objectives.jl") include("builders/thermodynamic.jl") include("builders/unsigned.jl") +include("builders/variables.jl") # simplified front-ends for the above include("frontend/balance.jl") diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index 7885de866..7c2b47a40 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -22,8 +22,7 @@ Shortcut for allocation non-negative ("unsigned") variables. The argument """ unsigned_variables(; keys) = C.variables(; keys, bounds = C.Between(0.0, Inf)) -export unsigned_variables - +export unsigned_variables # TODO kill """ $(TYPEDSIGNATURES) @@ -62,11 +61,26 @@ sign_split_constraints(; bound = C.EqualTo(0.0), ) for (k, s) in signed ) -#TODO the example above might as well go to docs +#TODO the example above needs to go to docs +# TODO this is a prime treezip candidate export sign_split_constraints +positive_bound_contribution(b::C.EqualTo) = b.equal_to >= 0 ? b : C.EqualTo(0.0) +positive_bound_contribution(b::C.Between) = + b.lower >= 0 && b.upper >= 0 ? b : + b.lower <= 0 && b.upper <= 0 ? C.EqualTo(0) : + C.Between(max(0, b.lower), max(0, b.upper)) +# TODO binary doesn't really fit here but it would be great if it could. + +unsigned_positive_contribution_variables(cs::C.ConstraintTree) = + variables_for(c -> positive_bound_contribution(c.bound), cs) + +unsigned_negative_contribution_variables(cs::C.ConstraintTree) = + variables_for(c -> positive_bound_contribution(c.bound), cs) + # TODO: docs, doesn't apply to fluxes only +# TODO replace by the above function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) keys = Symbol[] for (id, flux) in fluxes diff --git a/src/builders/variables.jl b/src/builders/variables.jl new file mode 100644 index 000000000..0881b7c08 --- /dev/null +++ b/src/builders/variables.jl @@ -0,0 +1,8 @@ + +variables_for(makebound, cs::C.ConstraintTree) = + let var_idx = 0 + C.tree_map(cs, C.Constraint) do c + var_idx += 1 + C.variable(idx = var_idx, bound = makebound(c)) + end + end diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 55be1fe01..b0e357253 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -109,12 +109,7 @@ function max_min_driving_force_analysis( ) - optimized_constraints( - m; - objective = m.max_min_driving_force.value, - optimizer, - settings, - ) + optimized_constraints(m; objective = m.max_min_driving_force.value, optimizer, settings) end export max_min_driving_force_analysis From f0756d3016bc1d98b709098d4b56da06013f93a7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 3 Jan 2024 15:53:49 +0100 Subject: [PATCH 459/531] revamp with C.zip --- src/builders/unsigned.jl | 62 +++++++-------------------------------- src/builders/variables.jl | 13 ++++++-- 2 files changed, 21 insertions(+), 54 deletions(-) diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index 7c2b47a40..e907d2598 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -17,52 +17,24 @@ """ $(TYPEDSIGNATURES) -Shortcut for allocation non-negative ("unsigned") variables. The argument -`keys` is forwarded to `ConstraintTrees.variables`. -""" -unsigned_variables(; keys) = C.variables(; keys, bounds = C.Between(0.0, Inf)) - -export unsigned_variables # TODO kill - -""" -$(TYPEDSIGNATURES) - A constraint tree that bound the values present in `signed` to be sums of pairs of `positive` and `negative` contributions to the individual values. Keys in the result are the same as the keys of `signed` constraints. Typically, this can be used to create "unidirectional" fluxes -together with [`unsigned_variables`](@ref): -``` -uvars = unsigned_variables(keys(myModel.fluxes)) - -myModel = myModel + - :fluxes_forward^uvars + - :fluxes_reverse^uvars - -myModel *= - :direction_sums^sign_split_constraints( - positive = myModel.fluxes_forward, - negative = myModel.fluxes_reverse, - signed = myModel.fluxes, - ) -``` +together with [`unsigned_negative_contribution_variables`](@ref) and +[`unsigned_positive_contribution_variables`](@ref). """ sign_split_constraints(; positive::C.ConstraintTree, negative::C.ConstraintTree, signed::C.ConstraintTree, -) = C.ConstraintTree( - k => C.Constraint( - value = s.value + - (haskey(negative, k) ? negative[k].value : zero(typeof(s.value))) - - (haskey(positive, k) ? positive[k].value : zero(typeof(s.value))), - bound = C.EqualTo(0.0), - ) for (k, s) in signed -) -#TODO the example above needs to go to docs -# TODO this is a prime treezip candidate +) = + C.zip(positive, negative, signed, C.Constraint) do p, n, s + C.Constraint(s.value + n.value - p.value, 0.0) + end +#TODO the construction needs an example in the docs. export sign_split_constraints @@ -76,21 +48,9 @@ positive_bound_contribution(b::C.Between) = unsigned_positive_contribution_variables(cs::C.ConstraintTree) = variables_for(c -> positive_bound_contribution(c.bound), cs) -unsigned_negative_contribution_variables(cs::C.ConstraintTree) = - variables_for(c -> positive_bound_contribution(c.bound), cs) +export unsigned_positive_contribution_variables -# TODO: docs, doesn't apply to fluxes only -# TODO replace by the above -function fluxes_in_direction(fluxes::C.ConstraintTree, direction = :forward) - keys = Symbol[] - for (id, flux) in fluxes - if direction == :forward - flux.bound.upper > 0 && push!(keys, id) - else - flux.bound.lower < 0 && push!(keys, id) - end - end - C.variables(; keys, bounds = C.Between(0.0, Inf)) -end +unsigned_negative_contribution_variables(cs::C.ConstraintTree) = + variables_for(c -> positive_bound_contribution(-c.bound), cs) -export fluxes_in_direction +export unsigned_negative_contribution_variables diff --git a/src/builders/variables.jl b/src/builders/variables.jl index 0881b7c08..b373bde38 100644 --- a/src/builders/variables.jl +++ b/src/builders/variables.jl @@ -1,8 +1,15 @@ -variables_for(makebound, cs::C.ConstraintTree) = +""" +$(TYPEDSIGNATURES) + +Allocate a variable for each item in a constraint tree (or any other kind of +tree) and return a tree with variables bounded by the `makebound` function +which converts a given value into a bound for the corresponding variable. +""" +variables_for(makebound, ts::C.Tree) = let var_idx = 0 - C.tree_map(cs, C.Constraint) do c + C.tree_map(ts, C.Constraint) do x var_idx += 1 - C.variable(idx = var_idx, bound = makebound(c)) + C.variable(idx = var_idx, bound = makebound(x)) end end From c50a6ddd697a1ff0cdf5659a08119646d8d58c09 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 09:42:56 +0100 Subject: [PATCH 460/531] soak variables_for into CTs package --- src/builders/unsigned.jl | 4 ++-- src/builders/variables.jl | 15 --------------- 2 files changed, 2 insertions(+), 17 deletions(-) delete mode 100644 src/builders/variables.jl diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index e907d2598..fb0a46a91 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -46,11 +46,11 @@ positive_bound_contribution(b::C.Between) = # TODO binary doesn't really fit here but it would be great if it could. unsigned_positive_contribution_variables(cs::C.ConstraintTree) = - variables_for(c -> positive_bound_contribution(c.bound), cs) + C.variables_for(c -> positive_bound_contribution(c.bound), cs) export unsigned_positive_contribution_variables unsigned_negative_contribution_variables(cs::C.ConstraintTree) = - variables_for(c -> positive_bound_contribution(-c.bound), cs) + C.variables_for(c -> positive_bound_contribution(-c.bound), cs) export unsigned_negative_contribution_variables diff --git a/src/builders/variables.jl b/src/builders/variables.jl deleted file mode 100644 index b373bde38..000000000 --- a/src/builders/variables.jl +++ /dev/null @@ -1,15 +0,0 @@ - -""" -$(TYPEDSIGNATURES) - -Allocate a variable for each item in a constraint tree (or any other kind of -tree) and return a tree with variables bounded by the `makebound` function -which converts a given value into a bound for the corresponding variable. -""" -variables_for(makebound, ts::C.Tree) = - let var_idx = 0 - C.tree_map(ts, C.Constraint) do x - var_idx += 1 - C.variable(idx = var_idx, bound = makebound(x)) - end - end From d73776129f3dc81015c6b598e6dcc62e8d0cdbfa Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 09:45:12 +0100 Subject: [PATCH 461/531] nuke the !'s --- docs/src/examples/05-enzyme-constrained-models.jl | 2 +- docs/src/examples/07-loopless-models.jl | 2 +- src/builders/enzymes.jl | 4 ++-- src/builders/loopless.jl | 4 ++-- src/frontend/enzyme_constrained.jl | 4 ++-- src/frontend/loopless.jl | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index caad58073..2dbfc65b6 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -152,7 +152,7 @@ m.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 0.0) # undo glucose important bo m.enzymes.b2417.bound = C.Between(0.0, 0.1) # for fun, change the bounds of the protein b2417 # attach the enzyme mass balances -m = add_enzyme_constraints!( +m = with_enzyme_constraints( m, reaction_isozymes; fluxes = m.fluxes, # mount enzyme constraints to these fluxes diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 1c6b9ac4d..4581406fb 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -78,7 +78,7 @@ internal_reaction_stoichiometry_nullspace_columns = eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) # And simply add loopless contraints on the fluxes of the model -m = add_loopless_constraints!( +m = with_loopless_constraints( m, internal_reaction_ids, internal_reaction_stoichiometry_nullspace_columns; diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index d53fe8d75..856355f3e 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -193,7 +193,7 @@ The isozyme struct used in `reaction_isozymes` must have fields `gene_product_stoichiometry`, `kcat_forward`, and `kcat_backward` to properly assign kcats to reactions. Use [`SimpleIsozyme`](@ref) when in doubt. """ -function add_enzyme_constraints!( +function with_enzyme_constraints( m::C.ConstraintTree, reaction_isozymes::Dict{String,Dict{String,T}}; fluxes = m.fluxes, @@ -257,4 +257,4 @@ function add_enzyme_constraints!( m end -export add_enzyme_constraints! +export with_enzyme_constraints diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 0f66d642e..ef8789d5c 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -36,7 +36,7 @@ internal_reaction_stoichiometry_nullspace_columns = eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_rxn_idxs_in_order_of_internal_rxn_ids]))) ``` """ -function add_loopless_constraints!( +function with_loopless_constraints( m, internal_reaction_ids, internal_reaction_stoichiometry_nullspace_columns; @@ -117,4 +117,4 @@ function add_loopless_constraints!( m end -export add_loopless_constraints! +export with_loopless_constraints diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index ab0779c02..7e54258ed 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -28,7 +28,7 @@ Unbound these reactions by listing their IDs in `unconstrain_reactions`, which makes them reversible. Optimization `settings` are directly forwarded. In the event that your model requires more complex build steps, consider -constructing it manually by using [`add_enzyme_constraints!`](@ref). +constructing it manually by using [`with_enzyme_constraints`](@ref). """ function enzyme_constrained_flux_balance_analysis( model::A.AbstractFBCModel, @@ -45,7 +45,7 @@ function enzyme_constrained_flux_balance_analysis( m += :enzymes^enzyme_variables(model) # add enzyme equality constraints (stoichiometry) - m = add_enzyme_constraints!(m, reaction_isozymes) + m = with_enzyme_constraints(m, reaction_isozymes) # add capacity limitations for (id, gids, cap) in capacity_limitations diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index 0c1a8bc0c..5225a4c3d 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -59,7 +59,7 @@ function loopless_flux_balance_analysis( LinearAlgebra.nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])), ) # no sparse nullspace function - m = add_loopless_constraints!( + m = with_loopless_constraints( m, internal_reaction_ids, internal_reaction_stoichiometry_nullspace_columns; From 38a2a955aedf4f6f4df40b85f97d64bf6a84d266 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 11:18:05 +0100 Subject: [PATCH 462/531] start cleaning up the enzyme stuff --- .../examples/05-enzyme-constrained-models.jl | 8 +- src/builders/enzymes.jl | 44 +++------- src/frontend/enzyme_constrained.jl | 81 ++++++++++++++----- src/types.jl | 10 +-- 4 files changed, 83 insertions(+), 60 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 2dbfc65b6..72e321cda 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -56,15 +56,15 @@ model = load_model("e_coli_core.json") # isozymes that can catalyze a reaction. A turnover number needs to be assigned # to each isozyme, as shown below. -reaction_isozymes = Dict{String,Dict{String,SimpleIsozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. +reaction_isozymes = Dict{String,Dict{String,Isozyme}}() # a mapping from reaction IDs to isozyme IDs to isozyme structs. for rid in A.reactions(model) grrs = A.reaction_gene_association_dnf(model, rid) isnothing(grrs) && continue # skip if no grr available haskey(ecoli_core_reaction_kcats, rid) || continue # skip if no kcat data available for (i, grr) in enumerate(grrs) - d = get!(reaction_isozymes, rid, Dict{String,SimpleIsozyme}()) + d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) # each isozyme gets a unique name - d["isozyme_"*string(i)] = SimpleIsozyme( # SimpleIsozyme struct is defined by COBREXA + d["isozyme_"*string(i)] = Isozyme( # SimpleIsozyme struct is defined by COBREXA gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes kcat_forward = ecoli_core_reaction_kcats[rid] * 3600.0, # forward reaction turnover number units = 1/h kcat_backward = ecoli_core_reaction_kcats[rid] * 3600.0, # reverse reaction turnover number units = 1/h @@ -143,7 +143,7 @@ import ConstraintTrees as C m = fbc_model_constraints(model) # create enzyme variables -m += :enzymes^enzyme_variables(model) +m += :enzymes^gene_product_variables(model) # constrain some fluxes... m.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 0.0) # undo glucose important bound from original model diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 856355f3e..74c07c116 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -17,31 +17,13 @@ """ $(TYPEDSIGNATURES) -Allocate enzyme variables (gene products in the model) to a constraint tree -using all the genes in the `model`. +Allocate a (non-negative) variable for all amounts of gene products in the +`model`. """ -enzyme_variables(model::A.AbstractFBCModel) = +gene_product_variables(model::A.AbstractFBCModel) = C.variables(; keys = Symbol.(A.genes(model)), bounds = C.Between(0.0, Inf)) -export enzyme_variables - -""" -$(TYPEDSIGNATURES) - -Helper function to create isozyme variables for reactions. A single reaction may -be catalyzed by multiple enzymes (isozymes), and the total flux through a -reaction is the sum through of all these isozymes. These variables are linked to -fluxes through [`link_isozymes`](@ref). -""" -function isozyme_variables( - reaction_id::String, - reaction_isozymes::Dict{String,Dict{String,T}}, -) where {T<:Isozyme} - C.variables(; - keys = Symbol.(collect(keys(reaction_isozymes[reaction_id]))), - bounds = C.Between(0.0, Inf), - ) -end +export gene_product_variables """ $(TYPEDSIGNATURES) @@ -191,14 +173,14 @@ mounted. # Note The isozyme struct used in `reaction_isozymes` must have fields `gene_product_stoichiometry`, `kcat_forward`, and `kcat_backward` to properly -assign kcats to reactions. Use [`SimpleIsozyme`](@ref) when in doubt. +assign kcats to reactions. Use [`Isozyme`](@ref) when in doubt. """ function with_enzyme_constraints( m::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,T}}; + reaction_isozymes::Dict{String,Dict{String,Isozyme}}; fluxes = m.fluxes, enzymes = m.enzymes, -) where {T<:Isozyme} +) # create directional fluxes m += @@ -217,18 +199,18 @@ function with_enzyme_constraints( for (rid, _) in m.fluxes_forward if haskey(reaction_isozymes, string(rid)) m += - :fluxes_isozymes_forward^rid^isozyme_variables( - string(rid), - reaction_isozymes, + :fluxes_isozymes_forward^rid^C.variables( + keys = Symbol.(keys(reaction_isozymes[string(rid)])), + bounds = Between(0, Inf), ) end end for (rid, _) in m.fluxes_backward if haskey(reaction_isozymes, string(rid)) m += - :fluxes_isozymes_backward^rid^isozyme_variables( - string(rid), - reaction_isozymes, + :fluxes_isozymes_backward^rid^C.variables( + keys = Symbol.(keys(reaction_isozymes[string(rid)])), + bounds = Between(0, Inf), ) end end diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index 7e54258ed..abb7e8275 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -14,6 +14,23 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +$(TYPEDEF) + +A simple struct storing information about the isozyme composition, including +subunit stoichiometry and turnover numbers. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct Isozyme + gene_product_stoichiometry::Dict{String,Float64} + kcat_forward::Maybe{Float64} = nothing + kcat_backward::Maybe{Float64} = nothing +end + +export Isozyme + """ $(TYPEDSIGNATURES) @@ -31,32 +48,56 @@ In the event that your model requires more complex build steps, consider constructing it manually by using [`with_enzyme_constraints`](@ref). """ function enzyme_constrained_flux_balance_analysis( - model::A.AbstractFBCModel, - reaction_isozymes::Dict{String,Dict{String,SimpleIsozyme}}, - gene_molar_masses::Dict{String,Float64}, - capacity_limitations::Vector{Tuple{String,Vector{String},Float64}}; + model::A.AbstractFBCModel; + reaction_isozymes::Dict{String,Dict{String,Isozyme}}, + gene_product_molar_masses::Dict{String,Float64}, + capacity_limits::Vector{Tuple{String,Vector{String},Float64}}, optimizer, - unconstrain_reactions = String[], settings = [], ) - m = fbc_model_constraints(model) - - # create enzyme variables - m += :enzymes^enzyme_variables(model) - - # add enzyme equality constraints (stoichiometry) - m = with_enzyme_constraints(m, reaction_isozymes) + ct = fbc_model_constraints(model) - # add capacity limitations - for (id, gids, cap) in capacity_limitations - m *= Symbol(id)^enzyme_capacity(m.enzymes, gene_molar_masses, gids, cap) - end + # allocate variables for everything (nb. += doesn't associate right here) + ct = + ct + + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + + :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) + + :gene_product_amounts^gene_product_variables(model) + + :isozyme_amounts^sum( + Symbol(rid)^C.variables(keys = Symbol.(keys(is)), bounds = C.Between(0, Inf)) + for (rid, is) in reaction_isozymes + ) - for rid in Symbol.(unconstrain_reactions) - m.fluxes[rid].bound = C.Between(-1000.0, 1000.0) - end + # connect all parts with constraints + ct = + ct * + :directional_flux_balance^sign_split_constraints( + ct.fluxes_forward, + ct.fluxes_reverse, + ct.fluxes, + ) * + :isozyme_flux_balance^isozyme_flux_constraints( + ct.fluxes_forward, + ct.fluxes_reverse, + ct.isozyme_amounts, + reaction_isozymes, + ) * + :gene_product_isozyme_balance^gene_product_isozyme_constraints( + ct.isozyme_amounts, + ct.gene_amounts, + reaction_isozymes, + ) * + :gene_product_capacity_limits^C.ConstraintTree( + Symbol(id) => C.Constraint( + value = sum( + ct.gene_product_amounts[gp].value * gene_product_molar_masses[gp] + for gp in gps + ), + bound = C.Between(0, limit), + ) for (id, gps, limit) in capacity_limits + ) - optimized_constraints(m; objective = m.objective.value, optimizer, settings) + optimized_constraints(ct; objective = ct.objective.value, optimizer, settings) end export enzyme_constrained_flux_balance_analysis diff --git a/src/types.jl b/src/types.jl index 56d6319e1..9e0778cb3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -39,23 +39,23 @@ subunit stoichiometry and turnover numbers. # Fields $(TYPEDFIELDS) """ -Base.@kwdef mutable struct SimpleIsozyme <: Isozyme +Base.@kwdef mutable struct Isozyme <: Isozyme gene_product_stoichiometry::Dict{String,Float64} kcat_forward::Maybe{Float64} = nothing kcat_backward::Maybe{Float64} = nothing end -export SimpleIsozyme +export Isozyme """ $(TYPEDSIGNATURES) -A convenience constructor for [`SimpleIsozyme`](@ref) that takes a string gene +A convenience constructor for [`Isozyme`](@ref) that takes a string gene reaction rule and converts it into the appropriate format. Assumes the `gene_product_stoichiometry` for each subunit is 1. """ -SimpleIsozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = - SimpleIsozyme(; +Isozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = + Isozyme(; gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), kcat_forward, kcat_backward, From 488890aafbf0093227bbafd0186ec56373cd3bb1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 13:46:41 +0100 Subject: [PATCH 463/531] enzymes rewrite --- src/builders/enzymes.jl | 265 ++++++++--------------------- src/frontend/enzyme_constrained.jl | 67 +++++--- src/types.jl | 11 +- 3 files changed, 119 insertions(+), 224 deletions(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 74c07c116..2ec99cf08 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -17,226 +17,101 @@ """ $(TYPEDSIGNATURES) -Allocate a (non-negative) variable for all amounts of gene products in the -`model`. -""" -gene_product_variables(model::A.AbstractFBCModel) = - C.variables(; keys = Symbol.(A.genes(model)), bounds = C.Between(0.0, Inf)) +Create a `ConstraintTree` with variables for isozyme contributions to reaction +fluxes. The tree has 2 levels: the first contains all reaction flux IDs that +have isozymes, the second contains the isozyme IDs for each reaction flux. -export gene_product_variables +`fluxes` should be anything that can be iterated to give reaction flux IDs. +`flux_isozymes` is a function that, for a given reaction flux ID, returns +anything iterable that contains the isozyme IDs for the given reaction flux. +Returning an empty iterable prevents allocating the subtree for the given flux. """ -$(TYPEDSIGNATURES) - -Helper function to link isozymes to fluxes. All forward (backward) fluxes are -the sum of all the isozymes catalysing these fluxes. -""" -function link_isozymes( - fluxes_directional::C.ConstraintTree, - fluxes_isozymes::C.ConstraintTree, +isozyme_amount_variables(fluxes, flux_isozymes) = sum( + ( + f^C.variables(keys = flux_isozymes(f), bounds = C.Between(0, Inf)) for + f in fluxes if !isempty(flux_isozymes(f)) + ), + init = C.ConstraintTree(), ) - C.ConstraintTree( - k => C.Constraint( - value = s.value - sum(x.value for (_, x) in fluxes_isozymes[k]), - bound = C.EqualTo(0.0), - ) for (k, s) in fluxes_directional if haskey(fluxes_isozymes, k) - ) -end -""" -$(TYPEDSIGNATURES) - -Helper function to create the enzyme "mass balance" matrix. In essence, the -stoichiometric coefficient is subunit_stoichiometry / kcat for each directional, -isozyme flux, and it must be balanced by the enzyme variable supply. -""" -function enzyme_stoichiometry( - enzymes::C.ConstraintTree, - fluxes_isozymes_forward::C.ConstraintTree, - fluxes_isozymes_backward::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,T}}, -) where {T<:Isozyme} - # map enzyme ids to reactions that use them (not all isozymes have to though) - enzyme_rid_lookup = Dict{Symbol,Vector{Symbol}}() - for (rid, isozymes) in reaction_isozymes - for isozyme in values(isozymes) - for gid in keys(isozyme.gene_product_stoichiometry) - rids = get!(enzyme_rid_lookup, Symbol(gid), Symbol[]) - rid in rids || push!(rids, Symbol(rid)) - end - end - end - - C.ConstraintTree( - gid => C.Constraint( - value = enz.value + # supply - sum( - enzyme_balance( - gid, - rid, - fluxes_isozymes_forward, - reaction_isozymes, - :kcat_forward, - ) for rid in enzyme_rid_lookup[gid] if - haskey(fluxes_isozymes_forward, rid); - init = zero(typeof(enz.value)), - ) + # flux through positive isozymes - sum( - enzyme_balance( - gid, - rid, - fluxes_isozymes_backward, - reaction_isozymes, - :kcat_backward, - ) for rid in enzyme_rid_lookup[gid] if - haskey(fluxes_isozymes_backward, rid); - init = zero(typeof(enz.value)), - ), # flux through negative isozymes - bound = C.EqualTo(0.0), - ) for (gid, enz) in enzymes if gid in keys(enzyme_rid_lookup) - ) -end +export isozyme_amount_variables """ $(TYPEDSIGNATURES) -Helper function to balance the forward or backward isozyme fluxes for a specific -gene product. -""" -function enzyme_balance( - gid::Symbol, - rid::Symbol, - fluxes_isozymes::C.ConstraintTree, # direction - reaction_isozymes::Dict{String,Dict{String,T}}, - direction = :kcat_forward, -) where {T<:Isozyme} - isozyme_dict = Dict(Symbol(k) => v for (k, v) in reaction_isozymes[string(rid)]) - - sum( # this is where the stoichiometry comes in - -isozyme_value.value * - isozyme_dict[isozyme_id].gene_product_stoichiometry[string(gid)] / - getfield(isozyme_dict[isozyme_id], direction) for - (isozyme_id, isozyme_value) in fluxes_isozymes[rid] if - gid in Symbol.(keys(isozyme_dict[isozyme_id].gene_product_stoichiometry)); - init = zero(C.LinearValue), - ) -end +A constraint tree that sums up partial contributions of reaction isozymes to +the fluxes of reactions. -""" -$(TYPEDSIGNATURES) +For practical purposes, both fluxes and isozymes are here considered to be +unidirectional, i.e., one would typically apply this twice to constraint both +"forward" and "reverse" fluxes. -Create a enzyme capacity limitation. Bounds the gene product masses (concentration -* molar mass) of gene products in `enzyme_ids` by `capacity_bound`. +Function `kcat` should retugn the kcat value for a given reaction and isozyme +(IDs of which respectively form the 2 parameters for each call). """ -function enzyme_capacity( - enzymes::C.ConstraintTree, - gene_molar_masses::Dict{String,Float64}, - enzyme_ids::Vector{String}, - capacity_bound::C.Bound, +function isozyme_flux_constraints( + isozyme_amounts::C.ConstraintTree, + fluxes::C.ConstraintTree, + kcat, ) - C.Constraint( - value = sum( - enzymes[Symbol(gid)].value * gene_molar_masses[gid] for gid in enzyme_ids - ), - bound = capacity_bound, + C.ConstraintTree( + rid => C.Constraint( + sum(kcat(rid, iid) * i.value for (iid, i) in ri if !isnothing(kcat(rid, iid))) - fluxes[rid].value, + 0.0, + ) for (rid, ri) in isozyme_amounts if haskey(fluxes, rid) ) end -""" -$(TYPEDSIGNATURES) +export isozyme_flux_constraints -Create an enzyme capacity limitation. Bounds the gene product masses -(concentration * molar mass) of gene products in `enzyme_ids` between `[0, capacity]`. """ -enzyme_capacity( - enzymes::C.ConstraintTree, - gene_molar_masses::Dict{String,Float64}, - enzyme_ids::Vector{String}, - capacity::Float64, -) = enzyme_capacity(enzymes, gene_molar_masses, enzyme_ids, C.Between(0.0, capacity)) +$(TYPEDSIGNATURES) +A constraint tree that binds the isozyme amounts to gene product amounts +accordingly to their multiplicities (aka. stoichiometries, protein units, ...) +given by `isozyme_stoichiometry`. -export enzyme_capacity +Values in `gene_product_amounts` should describe the gene product allocations. -""" -$(TYPEDSIGNATURES) +`isozyme_amount_trees` is an iterable that contains `ConstraintTree`s that +describe the allocated isozyme amounts (such as created by +[`isozyme_amount_variables`](@ref). One may typically pass in both forward- and +reverse-direction amounts at once, but it is possible to use a single tree, +e.g., in a uni-tuple: `tuple(my_tree)`. -Add enzyme constraints to a constraint tree, `m`. The enzyme model is -parameterized by `reaction_isozymes`, which is a mapping of reaction IDs (those -used in the fluxes of the model) to named struct which is a subtype of -[`Isozyme`](@ref)s. Additionally, `gene_molar_masses` and `capacity_limitations` -should be supplied. The latter is a vector of tuples, where each tuple -represents a distinct bound as `(bound_id, genes_in_bound, protein_mass_bound)`. -Finally, specify the `fluxes` and `enzymes` to which the constraints should be -mounted. - -# Note -The isozyme struct used in `reaction_isozymes` must have fields -`gene_product_stoichiometry`, `kcat_forward`, and `kcat_backward` to properly -assign kcats to reactions. Use [`Isozyme`](@ref) when in doubt. +`isozyme_stoichiometry` gets called with a reaction and isozyme ID as given by +the isozyme amount trees. It may return `nothing` in case there's no +information. """ -function with_enzyme_constraints( - m::C.ConstraintTree, - reaction_isozymes::Dict{String,Dict{String,Isozyme}}; - fluxes = m.fluxes, - enzymes = m.enzymes, +function gene_product_isozyme_constraints( + gene_product_amounts::C.ConstraintTree, + isozymes_amount_trees, + isozyme_stoichiometry, ) - - # create directional fluxes - m += - :fluxes_forward^fluxes_in_direction(fluxes, :forward) + - :fluxes_backward^fluxes_in_direction(fluxes, :backward) - - # link directional fluxes to original fluxes - m *= - :link_flux_directions^sign_split_constraints( - positive = m.fluxes_forward, - negative = m.fluxes_backward, - signed = fluxes, - ) - - # create fluxes for each isozyme - for (rid, _) in m.fluxes_forward - if haskey(reaction_isozymes, string(rid)) - m += - :fluxes_isozymes_forward^rid^C.variables( - keys = Symbol.(keys(reaction_isozymes[string(rid)])), - bounds = Between(0, Inf), - ) - end - end - for (rid, _) in m.fluxes_backward - if haskey(reaction_isozymes, string(rid)) - m += - :fluxes_isozymes_backward^rid^C.variables( - keys = Symbol.(keys(reaction_isozymes[string(rid)])), - bounds = Between(0, Inf), - ) + res = C.ConstraintTree() + # This needs to invert the stoichiometry mapping, + # so we patch up a fresh new CT in place. + for iss in isozymes_amount_trees + for (rid, is) in iss + for (iid, i) in is + gpstoi = isozyme_stoichiometry(rid, iid) + isnothing(gpstoi) && continue + for (gp, stoi) in gpstoi + haskey(gene_product_amounts, gp) || continue + if haskey(res, gp) + res[gp].value += i.value * stoi + else + res[gp] = C.Constraint( + i.value * stoi - gene_product_amounts[gp].value, + 0.0, + ) + end + end + end end end - - # link isozyme fluxes to directional fluxes - m *= - :link_isozyme_fluxes_forward^link_isozymes( - m.fluxes_forward, - m.fluxes_isozymes_forward, - ) - m *= - :link_isozyme_fluxes_backward^link_isozymes( - m.fluxes_backward, - m.fluxes_isozymes_backward, - ) - - # add enzyme mass balances - m *= - :enzyme_stoichiometry^enzyme_stoichiometry( - enzymes, - m.fluxes_isozymes_forward, - m.fluxes_isozymes_backward, - reaction_isozymes, - ) - - m + res end -export with_enzyme_constraints +export gene_product_isozyme_constraints diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index abb7e8275..42d2c191e 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -26,7 +26,7 @@ $(TYPEDFIELDS) Base.@kwdef mutable struct Isozyme gene_product_stoichiometry::Dict{String,Float64} kcat_forward::Maybe{Float64} = nothing - kcat_backward::Maybe{Float64} = nothing + kcat_reverse::Maybe{Float64} = nothing end export Isozyme @@ -34,38 +34,46 @@ export Isozyme """ $(TYPEDSIGNATURES) -Run a basic enzyme constrained flux balance analysis on `model`. The enzyme +Run a basic enzyme-constrained flux balance analysis on `model`. The enzyme model is parameterized by `reaction_isozymes`, which is a mapping of reaction -IDs (those used in the fluxes of the model) to named [`Isozyme`](@ref)s. -Additionally, `gene_molar_masses` and `capacity_limitations` should be supplied. -The latter is a vector of tuples, where each tuple represents a distinct bound -as `(bound_id, genes_in_bound, protein_mass_bound)`. Typically, `model` has -bounded exchange reactions, which are unnecessary in enzyme constrained models. -Unbound these reactions by listing their IDs in `unconstrain_reactions`, which -makes them reversible. Optimization `settings` are directly forwarded. +identifiers to [`Isozyme`](@ref) descriptions. -In the event that your model requires more complex build steps, consider -constructing it manually by using [`with_enzyme_constraints`](@ref). +Additionally, one typically wants to supply `gene_product_molar_masses` to +describe the weights of enzyme building material, and `capacity` which limits +the mass of enzymes in the whole model. + +`capacity` may be a single number, which sets the limit for "all described +enzymes". Alternatively, `capacity` may be a vector of identifier-genes-limit +triples that make a constraint (identified by the given identifier) that limits +the listed genes to the given limit. """ function enzyme_constrained_flux_balance_analysis( model::A.AbstractFBCModel; reaction_isozymes::Dict{String,Dict{String,Isozyme}}, gene_product_molar_masses::Dict{String,Float64}, - capacity_limits::Vector{Tuple{String,Vector{String},Float64}}, + capacity::Union{Vector{Tuple{String,Vector{String},Float64}},Float64}, optimizer, settings = [], ) ct = fbc_model_constraints(model) - # allocate variables for everything (nb. += doesn't associate right here) + # might be nice to omit some conditionally (e.g. slash the direction if one + # kcat is nothing) + isozyme_amounts = isozyme_amount_variables( + Symbol.(keys(reaction_isozymes)), + rid -> Symbol.(keys(reaction_isozymes[string(rid)])), + ) + + # allocate variables for everything (nb. += wouldn't associate right here) ct = ct + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) + - :gene_product_amounts^gene_product_variables(model) + - :isozyme_amounts^sum( - Symbol(rid)^C.variables(keys = Symbol.(keys(is)), bounds = C.Between(0, Inf)) - for (rid, is) in reaction_isozymes + :isozyme_forward_amounts^isozyme_amounts + + :isozyme_reverse_amounts^isozyme_amounts + + :gene_product_amounts^C.variables( + keys = Symbol.(A.genes(model)), + bounds = Between(0, Inf), ) # connect all parts with constraints @@ -76,16 +84,29 @@ function enzyme_constrained_flux_balance_analysis( ct.fluxes_reverse, ct.fluxes, ) * - :isozyme_flux_balance^isozyme_flux_constraints( + :isozyme_flux_forward_balance^isozyme_flux_constraints( + ct.isozyme_forward_amounts, ct.fluxes_forward, + (rid, isozyme) -> maybemap( + x -> x.kcat_forward, + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), + ) * + :isozyme_flux_reverse_balance^isozyme_flux_constraints( + ct.isozyme_reverse_amounts, ct.fluxes_reverse, - ct.isozyme_amounts, - reaction_isozymes, + (rid, isozyme) -> maybemap( + x -> x.kcat_reverse, + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), ) * :gene_product_isozyme_balance^gene_product_isozyme_constraints( - ct.isozyme_amounts, - ct.gene_amounts, - reaction_isozymes, + ct.gene_product_amounts, + (ct.isozyme_forward_amounts, ct.isozyme_reverse_amounts), + (rid, isozyme) -> maybemap( + x -> x.gene_product_stoichiometry, + maybeget(reaction_isozymes, string(rid), string(isozyme)), + ), ) * :gene_product_capacity_limits^C.ConstraintTree( Symbol(id) => C.Constraint( diff --git a/src/types.jl b/src/types.jl index 9e0778cb3..0b26887d8 100644 --- a/src/types.jl +++ b/src/types.jl @@ -54,12 +54,11 @@ A convenience constructor for [`Isozyme`](@ref) that takes a string gene reaction rule and converts it into the appropriate format. Assumes the `gene_product_stoichiometry` for each subunit is 1. """ -Isozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = - Isozyme(; - gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), - kcat_forward, - kcat_backward, - ) +Isozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = Isozyme(; + gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), + kcat_forward, + kcat_backward, +) """ $(TYPEDEF) From 53eb152f3ab6e7bbe920d67820cb25c6b039cd82 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 13:59:24 +0100 Subject: [PATCH 464/531] make MILP switches algebraically bearable --- src/builders/loopless.jl | 2 +- src/solver.jl | 30 ++++++++++++++++++++++-- src/types.jl | 50 ---------------------------------------- 3 files changed, 29 insertions(+), 53 deletions(-) diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index ef8789d5c..dc7488c5e 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -49,7 +49,7 @@ function with_loopless_constraints( m += :loopless_binary_variables^C.variables( keys = internal_reaction_ids, - bounds = Binary(), + bounds = Switch(0, 1), ) m += :pseudo_gibbs_free_energy_reaction^C.variables( diff --git a/src/solver.jl b/src/solver.jl index 041c677cf..44dd6c31a 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -14,6 +14,32 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +$(TYPEDEF) + +Representation of a "binary switch" bound for `ConstraintTree`s. The value is +constrained to be either the value of field `a` or of field `b`; both fields +are `Float64`s. Upon translation to JuMP, the switches create an extra boolean +variable, and the value is constrained to equal `a + boolean_var * (b-a)`. + +Switches can be offset by adding real numbers, negated, and multiplied and +divided by scalar constraints. For optimizing some special cases, multiplying +by exact zero returns an equality bound to zero. +""" +struct Switch <: C.Bound + a::Float64 + b::Float64 +end + +export Switch + +Base.:-(x::Switch) = Switch(-s.a, -s.b) +Base.:+(x::Real, s::Switch) = b + a +Base.:+(s::Switch, x::Real) = Switch(s.a + x, s.b + x) +Base.:*(x::Real, s::Switch) = b * a +Base.:*(s::Switch, x::Real) = x == 0 ? C.EqualTo(0) : Switch(s.a * x, s.b * x) +Base.:/(s::Switch, x::Real) = Switch(s.a / x, s.b / x) + """ $(TYPEDSIGNATURES) @@ -41,9 +67,9 @@ function optimization_model( isinf(b.lower) || J.@constraint(model, vx >= b.lower) isinf(b.upper) || J.@constraint(model, vx <= b.upper) end - function add_constraint(v::C.Value, _::Binary) + function add_constraint(v::C.Value, b::Switch) boolean = J.@variable(model, binary = true) - J.@constraint(model, C.substitute(v, x) == boolean) + J.@constraint(model, C.substitute(v, x) == b.a + boolean * (b.b - b.a)) end function add_constraint(c::C.Constraint) add_constraint(c.value, c.bound) diff --git a/src/types.jl b/src/types.jl index 0b26887d8..4aa608ac3 100644 --- a/src/types.jl +++ b/src/types.jl @@ -20,53 +20,3 @@ Type of optional values. """ const Maybe{X} = Union{Nothing,X} - -""" -$(TYPEDEF) - -Abstract isozyme type that stores all kinetic information required for -constraint-based modeling. - -""" -abstract type Isozyme end - -""" -$(TYPEDEF) - -A simple struct storing information about the isozyme composition, including -subunit stoichiometry and turnover numbers. - -# Fields -$(TYPEDFIELDS) -""" -Base.@kwdef mutable struct Isozyme <: Isozyme - gene_product_stoichiometry::Dict{String,Float64} - kcat_forward::Maybe{Float64} = nothing - kcat_backward::Maybe{Float64} = nothing -end - -export Isozyme - -""" -$(TYPEDSIGNATURES) - -A convenience constructor for [`Isozyme`](@ref) that takes a string gene -reaction rule and converts it into the appropriate format. Assumes the -`gene_product_stoichiometry` for each subunit is 1. -""" -Isozyme(gids::Vector{String}; kcat_forward::Float64, kcat_backward::Float64) = Isozyme(; - gene_product_stoichiometry = Dict(gid => 1.0 for gid in gids), - kcat_forward, - kcat_backward, -) - -""" -$(TYPEDEF) - -Representation of a binary bound, i.e. constrain a variable to only take the -value 0 or 1 exclusively. Requires a mixed integer-capable solver for -optimization. -""" -struct Binary <: C.Bound end - -export Binary From 78647fcb9d1d996769f3ffb0b8bd35acba24d1df Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 14:08:15 +0100 Subject: [PATCH 465/531] clean up the senses --- src/solver.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/solver.jl b/src/solver.jl index 44dd6c31a..953618484 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -45,7 +45,8 @@ $(TYPEDSIGNATURES) Construct a JuMP `Model` that describes the precise constraint system into the JuMP `Model` created for solving in `optimizer`, with a given optional -`objective` and optimization `sense`. +`objective` and optimization `sense` chosen from [`Maximal`](@ref), +[`Minimal`](@ref) and [`Feasible`](@ref). """ function optimization_model( cs::C.ConstraintTreeElem; From 0d5a2fcc90222b9ab2da7f224ebcb36f39df2ee4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 14:25:35 +0100 Subject: [PATCH 466/531] sync up file changes --- src/COBREXA.jl | 2 +- src/misc/maybe.jl | 17 +++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 src/misc/maybe.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 49b3a7fb1..7e5d2adc3 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -58,7 +58,6 @@ include("builders/loopless.jl") include("builders/objectives.jl") include("builders/thermodynamic.jl") include("builders/unsigned.jl") -include("builders/variables.jl") # simplified front-ends for the above include("frontend/balance.jl") @@ -71,6 +70,7 @@ include("frontend/enzyme_constrained.jl") # utilities include("misc/bounds.jl") include("misc/settings.jl") +include("misc/maybe.jl") include("misc/utils.jl") end # module COBREXA diff --git a/src/misc/maybe.jl b/src/misc/maybe.jl new file mode 100644 index 000000000..ad2a20448 --- /dev/null +++ b/src/misc/maybe.jl @@ -0,0 +1,17 @@ + +""" +$(TYPEDSIGNATURES) + +Helper for getting stuff from dictionaries where keys may be easily missing. +""" +maybeget(_::Nothing, _...) = nothing +maybeget(x, k, ks...) = haskey(x, k) ? maybeget(x[k], ks...) : nothing +maybeget(x) = x + +""" +$(TYPEDSIGNATURES) + +Helper for applying functions to stuff that might be `nothing`. +""" +maybemap(f, _::Nothing) = nothing +maybemap(f, x) = f(x) From e03338e3923ce37878fda279b4872297bf681a9c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 14:40:43 +0100 Subject: [PATCH 467/531] knockout cleanup --- src/builders/knockouts.jl | 58 +++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/src/builders/knockouts.jl b/src/builders/knockouts.jl index 0e96d46dc..fd29a70eb 100644 --- a/src/builders/knockouts.jl +++ b/src/builders/knockouts.jl @@ -16,42 +16,40 @@ """ $(TYPEDSIGNATURES) -""" -knockout_constraints(; fluxes::C.ConstraintTree, knockout_test) = C.ConstraintTree( - rxn => C.Constraint(C.value(fluxes[rxn]), C.EqualTo(0.0)) for - rxn in keys(fluxes) if knockout_test(rxn) -) +Make a `ConstraintTree` that knocks out fluxes given by the predicate +`knockout_test`. The predicate function is called with a single parameter (the +key of the flux in tree `fluxes`) and must return a boolean. Returning `true` +means that the corresponding flux (usually a reaction flux) will be knocked +out. + +Use [`fbc_gene_knockout_constraints`](@ref) to apply gene knockouts easily to +models with `AbstractFBCModel` interface. """ -$(TYPEDSIGNATURES) -""" -gene_knockouts(; - fluxes::C.ConstraintTree, - ko_genes::Vector{String}, - model::A.AbstractFBCModel, -) = knockout_constraints(; - fluxes, - knockout_test = rxn -> begin - maybe_avail = A.reaction_gene_products_available( - model, - string(rxn), - g -> !(g in ko_genes), # not available if knocked out - ) - isnothing(maybe_avail) ? false : !maybe_avail # negate here because of knockout_constraints - end, +knockout_constraints(knockout_test, fluxes::C.ConstraintTree) = C.ConstraintTree( + id => C.Constraint(C.value(flux), 0) for (id, flux) in fluxes if knockout_test(id) ) -#TODO remove the bang from here, there's no side effect """ $(TYPEDSIGNATURES) -""" -knockout!(ctmodel::C.ConstraintTree, ko_genes::Vector{String}, model::A.AbstractFBCModel) = - ctmodel * :gene_knockouts^gene_knockouts(; fluxes = ctmodel.fluxes, ko_genes, model) -""" -$(TYPEDSIGNATURES) +Make a `ConstraintTree` that simulates a gene knockout of `knockout_genes` in +the `model` and disables corresponding `fluxes` accordingly. + +Keys of the fluxes must correspond to the reaction identifiers in the `model`. -Pipe-able variant. +`knockout_genes` may be any collection that support element tests using `in`. +Since the test is done many times, a `Set` is a preferred contained for longer +lists of genes. """ -knockout!(ko_genes::Vector{String}, model::A.AbstractFBCModel) = - m -> knockout!(m, ko_genes, model) +fbc_gene_knockout_constraints(; + fluxes::C.ConstraintTree, + knockout_genes, + model::A.AbstractFBCModel, +) = + knockout_constraints(fluxes) do rid + maybemap( + !, + A.reaction_gene_products_available(model, string(rid), !in(knockout_genes)), + ) + end From f12a3edc7e9308e008c4fa33e0f33ab9b4678f30 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 15:43:17 +0100 Subject: [PATCH 468/531] keep naming at bay --- src/frontend/enzyme_constrained.jl | 43 +++++++++++++++++------------- 1 file changed, 24 insertions(+), 19 deletions(-) diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index 42d2c191e..7223ca286 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -55,7 +55,7 @@ function enzyme_constrained_flux_balance_analysis( optimizer, settings = [], ) - ct = fbc_model_constraints(model) + constraints = fbc_model_constraints(model) # might be nice to omit some conditionally (e.g. slash the direction if one # kcat is nothing) @@ -65,10 +65,10 @@ function enzyme_constrained_flux_balance_analysis( ) # allocate variables for everything (nb. += wouldn't associate right here) - ct = - ct + - :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + - :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) + + constraints = + constraints + + :fluxes_forward^unsigned_positive_contribution_variables(constraints.fluxes) + + :fluxes_reverse^unsigned_negative_contribution_variables(constraints.fluxes) + :isozyme_forward_amounts^isozyme_amounts + :isozyme_reverse_amounts^isozyme_amounts + :gene_product_amounts^C.variables( @@ -77,32 +77,32 @@ function enzyme_constrained_flux_balance_analysis( ) # connect all parts with constraints - ct = - ct * + constraints = + constraints * :directional_flux_balance^sign_split_constraints( - ct.fluxes_forward, - ct.fluxes_reverse, - ct.fluxes, + constraints.fluxes_forward, + constraints.fluxes_reverse, + constraints.fluxes, ) * :isozyme_flux_forward_balance^isozyme_flux_constraints( - ct.isozyme_forward_amounts, - ct.fluxes_forward, + constraints.isozyme_forward_amounts, + constraints.fluxes_forward, (rid, isozyme) -> maybemap( x -> x.kcat_forward, maybeget(reaction_isozymes, string(rid), string(isozyme)), ), ) * :isozyme_flux_reverse_balance^isozyme_flux_constraints( - ct.isozyme_reverse_amounts, - ct.fluxes_reverse, + constraints.isozyme_reverse_amounts, + constraints.fluxes_reverse, (rid, isozyme) -> maybemap( x -> x.kcat_reverse, maybeget(reaction_isozymes, string(rid), string(isozyme)), ), ) * :gene_product_isozyme_balance^gene_product_isozyme_constraints( - ct.gene_product_amounts, - (ct.isozyme_forward_amounts, ct.isozyme_reverse_amounts), + constraints.gene_product_amounts, + (constraints.isozyme_forward_amounts, constraints.isozyme_reverse_amounts), (rid, isozyme) -> maybemap( x -> x.gene_product_stoichiometry, maybeget(reaction_isozymes, string(rid), string(isozyme)), @@ -111,14 +111,19 @@ function enzyme_constrained_flux_balance_analysis( :gene_product_capacity_limits^C.ConstraintTree( Symbol(id) => C.Constraint( value = sum( - ct.gene_product_amounts[gp].value * gene_product_molar_masses[gp] - for gp in gps + constraints.gene_product_amounts[gp].value * + gene_product_molar_masses[gp] for gp in gps ), bound = C.Between(0, limit), ) for (id, gps, limit) in capacity_limits ) - optimized_constraints(ct; objective = ct.objective.value, optimizer, settings) + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + settings, + ) end export enzyme_constrained_flux_balance_analysis From a8feaeaf06d28d7447345159700dada3ffdfc74e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 15:44:26 +0100 Subject: [PATCH 469/531] throw in L1 parsimonious analyses --- src/builders/objectives.jl | 15 ++++++ src/frontend/moma.jl | 92 +++++++++++++++++++++++++++++++++--- src/frontend/parsimonious.jl | 52 ++++++++++++++++++++ 3 files changed, 152 insertions(+), 7 deletions(-) diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 5320bec0c..9b1d487e7 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -25,6 +25,21 @@ squared_sum_objective(x::C.ConstraintTree) = """ $(TYPEDSIGNATURES) +TODO useful for L1 parsimonious stuff +""" +function sum_objective(x...) + res = zero(C.LinearValue) + for ct in x + C.map(ct) do c + res += c.value + end + end + res +end + +""" +$(TYPEDSIGNATURES) + TODO """ squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index b3acc69f0..4e2b621bd 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -53,15 +53,15 @@ end """ $(TYPEDSIGNATURES) -A slightly easier-to-use version of [`minimization_of_metabolic_adjustment_analysis`](@ref) that -computes the reference flux as the optimal solution of the -[`reference_model`](@ref). The reference flux is calculated using -`reference_optimizer` and `reference_modifications`, which default to the -`optimizer` and `settings`. +A slightly easier-to-use version of +[`minimization_of_metabolic_adjustment_analysis`](@ref) that computes the +reference flux as the optimal solution of the [`reference_model`](@ref). The +reference flux is calculated using `reference_optimizer` and +`reference_modifications`, which default to the `optimizer` and `settings`. Leftover arguments are passed to the overload of -[`minimization_of_metabolic_adjustment_analysis`](@ref) that accepts the reference flux -dictionary. +[`minimization_of_metabolic_adjustment_analysis`](@ref) that accepts the +reference flux dictionary. """ function minimization_of_metabolic_adjustment_analysis( model::A.AbstractFBCModel, @@ -90,3 +90,81 @@ function minimization_of_metabolic_adjustment_analysis( end export minimization_of_metabolic_adjustment_analysis + +""" +$(TYPEDSIGNATURES) + +Like [`minimization_of_metabolic_adjustment_analysis`](@ref) but optimizes the +L1 norm. This typically produces a sufficiently good result with less +resources, depending on the situation. See documentation of +[`linear_parsimonious_flux_balance_analysis`](@ref) for some of the +considerations. +""" +function linear_minimization_of_metabolic_adjustment_analysis( + model::A.AbstractFBCModel, + reference_fluxes::Dict{Symbol,Float64}, + optimizer; + kwargs..., +) + constraints = fbc_model_constraints(model) + + difference = C.zip(ct.fluxes, C.Tree(reference_fluxes)) do orig, ref + C.Constraint(orig.value - ref) + end + + difference_split_variables = + C.variables(keys = keys(difference), bounds = C.Between(0, Inf)) + constraints += :reference_positive_diff^difference_split_variables + constraints += :reference_negative_diff^difference_split_variables + + # `difference` actually doesn't need to go to the CT, but we include it + # anyway to calm the curiosity of good neighbors. + constraints *= :reference_diff^difference + constraints *= + :reference_directional_diff_balance^sign_split_constraints( + constraints.reference_positive_diff, + constraints.reference_negative_diff, + difference, + ) + + objective = sum_objective( + constraints.reference_positive_diff, + constraints.reference_negative_diff, + ) + + optimized_constraints( + constraints * :linear_minimal_adjustment_objective^C.Constraint(objective); + optimizer, + objective, + sense = Minimal, + kwargs..., + ) +end + +function linear_minimization_of_metabolic_adjustment_analysis( + model::A.AbstractFBCModel, + reference_model::A.AbstractFBCModel, + optimizer; + reference_optimizer = optimizer, + settings = [], + reference_settings = settings, + kwargs..., +) + reference_constraints = fbc_model_constraints(reference_model) + reference_fluxes = optimized_constraints( + reference_constraints; + optimizer = reference_optimizer, + settings = reference_settings, + output = reference_constraints.fluxes, + ) + isnothing(reference_fluxes) && return nothing + linear_minimization_of_metabolic_adjustment_analysis( + model, + reference_fluxes, + optimizer; + settings, + kwargs..., + ) +end + +export linear_minimization_of_metabolic_adjustment_analysis diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index 1f7d732f5..db30e0966 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -125,3 +125,55 @@ parsimonious_flux_balance_analysis(optimizer; kwargs...) = model -> parsimonious_flux_balance_analysis(model, optimizer; kwargs...) export parsimonious_flux_balance_analysis + +""" +$(TYPEDSIGNATURES) + +Like [`parsimonious_flux_balance_analysis`](@ref), but uses a L1 metric for +solving the parsimonious problem. + +In turn, the solution is often faster, does not require a solver capable of +quadratic objectives, and has many beneficial properties of the usual +parsimonious solutions (such as the general lack of unnecessary loops). On the +other hand, like with plain flux balance analysis there is no strong guarantee +of uniqueness of the solution. +""" +function linear_parsimonious_flux_balance_analysis( + model::A.AbstractFBCModel, + optimizer; + tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), + kwargs..., +) + constraints = fbc_model_constraints(model) + constraints = + constraints + + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + + :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) + constraints *= + :directional_flux_balance^sign_split_constraints( + ct.fluxes_forward, + ct.fluxes_reverse, + ct.fluxes, + ) + + parsimonious_objective = sum_objective(ct.fluxes_forward, ct.fluxes_reverse) + + parsimonious_optimized_constraints( + constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); + optimizer, + objective = constraints.objective.value, + parsimonious_objective, + tolerances, + kwargs..., + ) +end + +""" +$(TYPEDSIGNATURES) + +Pipe-able variant of [`linear_parsimonious_flux_balance_analysis`](@ref). +""" +linear_parsimonious_flux_balance_analysis(optimizer; kwargs...) = + model -> linear_parsimonious_flux_balance_analysis(model, optimizer; kwargs...) + +export linear_parsimonious_flux_balance_analysis From f20e37e34463896e6cf22828010404527420b90e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 15:52:07 +0100 Subject: [PATCH 470/531] license the new file --- src/misc/maybe.jl | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/misc/maybe.jl b/src/misc/maybe.jl index ad2a20448..166b2f6fd 100644 --- a/src/misc/maybe.jl +++ b/src/misc/maybe.jl @@ -1,4 +1,19 @@ +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + """ $(TYPEDSIGNATURES) From 35becabf82fe381e7dfa962dc3e47c602b6367d8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 16:53:30 +0100 Subject: [PATCH 471/531] mmdf start --- src/builders/thermodynamic.jl | 56 +++++++++++------------------------ src/frontend/mmdf.jl | 11 ++++--- 2 files changed, 25 insertions(+), 42 deletions(-) diff --git a/src/builders/thermodynamic.jl b/src/builders/thermodynamic.jl index 8a8f186d5..8a5c98969 100644 --- a/src/builders/thermodynamic.jl +++ b/src/builders/thermodynamic.jl @@ -20,7 +20,7 @@ $(TYPEDSIGNATURES) Build a max min driving force analysis model. See the docstring of [`max_min_driving_force_analysis`](@ref) for details about the arguments. """ -function build_max_min_driving_force_model( +function max_min_driving_force_constraints( model::A.AbstractFBCModel, reaction_standard_gibbs_free_energies::Dict{String,Float64}; reference_flux = Dict{String,Float64}(), @@ -55,7 +55,7 @@ function build_max_min_driving_force_model( stoi = A.stoichiometry(model) # create thermodynamic variables - m = C.ConstraintTree( + constraints = C.ConstraintTree( :max_min_driving_force^C.variable() + :log_metabolite_concentrations^C.variables( keys = Symbol.(mets), @@ -79,15 +79,18 @@ function build_max_min_driving_force_model( push!(dG0s_met_ids_stoichs, (dG0, met_ids, stoich_coeffs)) end - m *= + constraints *= :delta_G_reaction_equations^C.ConstraintTree( Symbol(rxn) => C.Constraint( - value = -m.delta_G_reactions[Symbol(rxn)].value + + value = -constraints.delta_G_reactions[Symbol(rxn)].value + dG0 + R * T * sum( - m.log_metabolite_concentrations[Symbol(met_id)].value * stoich for (met_id, stoich) in zip(met_ids, stoich_coeffs) + constraints.log_metabolite_concentrations[Symbol( + met_id, + )].value * stoich for + (met_id, stoich) in zip(met_ids, stoich_coeffs) ), bound = C.EqualTo(0.0), ) for (rxn, (dG0, met_ids, stoich_coeffs)) in zip(rxns, dG0s_met_ids_stoichs) @@ -100,9 +103,9 @@ function build_max_min_driving_force_model( Debatable... =# for met in [Symbol.(proton_ids); Symbol.(water_ids)] - if haskey(m.log_metabolite_concentrations, met) - m.log_metabolite_concentrations[met] = C.Constraint( - value = m.log_metabolite_concentrations[met].value, + if haskey(constraints.log_metabolite_concentrations, met) + constraints.log_metabolite_concentrations[met] = C.Constraint( + value = constraints.log_metabolite_concentrations[met].value, bound = C.EqualTo(0.0), ) end @@ -112,49 +115,26 @@ function build_max_min_driving_force_model( Add thermodynamic feasibility constraint (ΔG < 0 for a feasible reaction in flux direction). Add objective constraint to solve max min problem. =# - m *= + constraints *= :reaction_delta_G_margin^C.ConstraintTree( Symbol(rxn) => C.Constraint( - value = m.delta_G_reactions[Symbol(rxn)].value * + value = constraints.delta_G_reactions[Symbol(rxn)].value * sign(get(reference_flux, rxn, 1.0)), bound = C.Between(-Inf, 0.0), ) for rxn in rxns ) - m *= + constraints *= :min_driving_force_margin^C.ConstraintTree( Symbol(rxn) => C.Constraint( - value = m.max_min_driving_force.value + - m.delta_G_reactions[Symbol(rxn)].value * + value = constraints.max_min_driving_force.value + + constraints.delta_G_reactions[Symbol(rxn)].value * sign(get(reference_flux, rxn, 1.0)), bound = C.Between(-Inf, 0.0), ) for rxn in rxns ) - m + constraints end -export build_max_min_driving_force_model - -""" -$(TYPEDSIGNATURES) - -Add constraints to `m` that represents ratios of variables in log space: -`log(x/y) = log(const)` where `x` and `y` are variables specified by `on`. The -constraints are specified by `ratios`, which is a dictionary mapping a -constraint id to a tuple which consists of the variable ids, `x`, `y`, and the -ratio value, `const`. The latter is logged internally, while the variables are -subtracted from each other, as it is assumed they are already in log space, -`log(x/y) = log(x) - log(y)`. -""" -log_ratio_constraints( - ratios::Dict{String,Tuple{String,String,Float64}}, - on::C.ConstraintTree, -) = C.ConstraintTree( - Symbol(cid) => C.Constraint( - value = on[Symbol(var1)].value - on[Symbol(var2)].value, - bound = C.EqualTo(log(ratio)), - ) for (cid, (var1, var2, ratio)) in ratios -) - -export log_ratio_constraints +export max_min_driving_force_constraints diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index b0e357253..1f0d1b085 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -85,7 +85,7 @@ function max_min_driving_force_analysis( settings = [], optimizer, ) - m = build_max_min_driving_force_model( + m = max_min_driving_force_constraints( model, reaction_standard_gibbs_free_energies; reference_flux, @@ -103,9 +103,12 @@ function max_min_driving_force_analysis( end m *= - :metabolite_ratio_constraints^log_ratio_constraints( - concentration_ratios, - m.log_metabolite_concentrations, + :metabolite_ratio_constraints^C.ConstraintTree( + cid => C.Constraint( + m.log_metabolite_concentrations[Symbol(m2)].value - + m.log_metabolite_concentrations[Symbol(m1)].value, + ratio, + ) for (cid, (m1, m2, ratio)) in concentration_ratios ) From f8133477db7f3f0ccf3e57e35983c5a4f1694709 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 4 Jan 2024 16:53:47 +0100 Subject: [PATCH 472/531] rename for clarity --- src/builders/{thermodynamic.jl => mmdf.jl} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename src/builders/{thermodynamic.jl => mmdf.jl} (100%) diff --git a/src/builders/thermodynamic.jl b/src/builders/mmdf.jl similarity index 100% rename from src/builders/thermodynamic.jl rename to src/builders/mmdf.jl From 59e2f35ae7a95b28ce6ecfb4931a992c91ce16f5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 5 Jan 2024 15:00:01 +0100 Subject: [PATCH 473/531] constraint_values is renamed in CTs 0.9 --- docs/src/examples/03-parsimonious-flux-balance.jl | 4 ++-- src/frontend/parsimonious.jl | 2 +- src/solver.jl | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 13a25ca16..5fa08f335 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -68,7 +68,7 @@ J.optimize!(opt_model) # JuMP is called J in COBREXA is_solved(opt_model) # check if solved -vt = C.constraint_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = C.substitute_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds @@ -107,7 +107,7 @@ J.optimize!(opt_model) # JuMP is called J in COBREXA is_solved(opt_model) # check if solved -vt = C.constraint_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA +vt = C.substitute_values(ctmodel, J.value.(opt_model[:x])) # ConstraintTrees.jl is called C in COBREXA @test isapprox(vt.l2objective, ?; atol = QP_TEST_TOLERANCE) #src # TODO will break until mutable bounds diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index db30e0966..0075e505d 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -68,7 +68,7 @@ function parsimonious_optimized_constraints( ) J.optimize!(om) - is_solved(om) && return C.constraint_values(output, J.value.(om[:x])) + is_solved(om) && return C.substitute_values(output, J.value.(om[:x])) J.delete(om, pfba_tolerance_constraint) J.unregister(om, :pfba_tolerance_constraint) diff --git a/src/solver.jl b/src/solver.jl index 953618484..4a987a361 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -149,7 +149,7 @@ function optimized_constraints( m(om) end J.optimize!(om) - is_solved(om) ? C.constraint_values(output, J.value.(om[:x])) : nothing + is_solved(om) ? C.substitute_values(output, J.value.(om[:x])) : nothing end export optimized_constraints From e8f7ce1e923a7b22f3ce59d777535e84b682e0f9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 5 Jan 2024 17:05:26 +0100 Subject: [PATCH 474/531] fix a few errors to make enzymes work --- .../examples/03-parsimonious-flux-balance.jl | 4 +-- .../examples/05-enzyme-constrained-models.jl | 18 +++++------ src/COBREXA.jl | 2 +- src/builders/objectives.jl | 14 ++++---- src/frontend/enzyme_constrained.jl | 32 ++++++++++++------- src/frontend/moma.jl | 16 ++++------ src/frontend/parsimonious.jl | 10 +++--- src/solver.jl | 1 + 8 files changed, 52 insertions(+), 45 deletions(-) diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 5fa08f335..a39a5cfc6 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -54,7 +54,7 @@ model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; settings = [sile # the quadratic objective (this approach is much more flexible). ctmodel = fbc_model_constraints(model) -ctmodel *= :l2objective^squared_sum_objective(ctmodel.fluxes) +ctmodel *= :l2objective^squared_sum_value(ctmodel.fluxes) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks opt_model = optimization_model( @@ -90,7 +90,7 @@ minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; settings = [silence]) ctmodel = fbc_model_constraints(model) ctmodel *= - :minoxphospho^squared_sum_error_objective( + :minoxphospho^squared_sum_error_value( ctmodel.fluxes, Dict(:ATPS4r => 33.0, :CYTBD => 22.0), ) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 72e321cda..ab6d75a78 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -64,10 +64,10 @@ for rid in A.reactions(model) for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) # each isozyme gets a unique name - d["isozyme_"*string(i)] = Isozyme( # SimpleIsozyme struct is defined by COBREXA + d["isozyme_"*string(i)] = Isozyme( gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes - kcat_forward = ecoli_core_reaction_kcats[rid] * 3600.0, # forward reaction turnover number units = 1/h - kcat_backward = ecoli_core_reaction_kcats[rid] * 3600.0, # reverse reaction turnover number units = 1/h + kcat_forward = ecoli_core_reaction_kcats[rid] * 3.6, # forward reaction turnover number units = 1/h + kcat_reverse = ecoli_core_reaction_kcats[rid] * 3.6, # reverse reaction turnover number units = 1/h ) end end @@ -90,7 +90,7 @@ end # # ``` -gene_molar_masses = ecoli_core_gene_product_masses +gene_product_molar_masses = ecoli_core_gene_product_masses #!!! warning "Molar mass units" # Take care with the units of the molar masses. In literature they are @@ -108,7 +108,7 @@ gene_molar_masses = ecoli_core_gene_product_masses # The capacity limitation usually denotes an upper bound of protein available to # the cell. -total_enzyme_capacity = 0.1 # g enzyme/gDW +total_enzyme_capacity = 100.0 # mg enzyme/gDW ### Running a basic enzyme constrained model @@ -116,12 +116,12 @@ total_enzyme_capacity = 0.1 # g enzyme/gDW # convenience function to run enzyme constrained FBA in one shot: ec_solution = enzyme_constrained_flux_balance_analysis( - model, + model; reaction_isozymes, - gene_molar_masses, - [("total_proteome_bound", A.genes(model), total_enzyme_capacity)]; + gene_product_molar_masses, + capacity = total_enzyme_capacity, settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], - unconstrain_reactions = ["EX_glc__D_e"], + #unconstrain_reactions = ["EX_glc__D_e"], optimizer = Tulip.Optimizer, ) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 7e5d2adc3..c83cfa28c 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -55,8 +55,8 @@ include("builders/enzymes.jl") include("builders/fbc.jl") include("builders/knockouts.jl") include("builders/loopless.jl") +include("builders/mmdf.jl") include("builders/objectives.jl") -include("builders/thermodynamic.jl") include("builders/unsigned.jl") # simplified front-ends for the above diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 9b1d487e7..6861e56d1 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -19,15 +19,14 @@ $(TYPEDSIGNATURES) TODO """ -squared_sum_objective(x::C.ConstraintTree) = - squared_sum_error_objective(x, Dict(keys(x) .=> 0.0)) +squared_sum_value(x::C.ConstraintTree) = squared_sum_error_value(x, Dict(keys(x) .=> 0.0)) """ $(TYPEDSIGNATURES) TODO useful for L1 parsimonious stuff """ -function sum_objective(x...) +function sum_value(x...) res = zero(C.LinearValue) for ct in x C.map(ct) do c @@ -42,8 +41,7 @@ $(TYPEDSIGNATURES) TODO """ -squared_sum_error_objective(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = - sum( - (C.squared(C.value(c) - target[k]) for (k, c) in constraints if haskey(target, k)), - init = zero(C.LinearValue), - ) +squared_sum_error_value(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = sum( + (C.squared(C.value(c) - target[k]) for (k, c) in constraints if haskey(target, k)), + init = zero(C.LinearValue), +) diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index 7223ca286..d7b29dec8 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -73,16 +73,16 @@ function enzyme_constrained_flux_balance_analysis( :isozyme_reverse_amounts^isozyme_amounts + :gene_product_amounts^C.variables( keys = Symbol.(A.genes(model)), - bounds = Between(0, Inf), + bounds = C.Between(0, Inf), ) # connect all parts with constraints constraints = constraints * :directional_flux_balance^sign_split_constraints( - constraints.fluxes_forward, - constraints.fluxes_reverse, - constraints.fluxes, + positive = constraints.fluxes_forward, + negative = constraints.fluxes_reverse, + signed = constraints.fluxes, ) * :isozyme_flux_forward_balance^isozyme_flux_constraints( constraints.isozyme_forward_amounts, @@ -104,18 +104,28 @@ function enzyme_constrained_flux_balance_analysis( constraints.gene_product_amounts, (constraints.isozyme_forward_amounts, constraints.isozyme_reverse_amounts), (rid, isozyme) -> maybemap( - x -> x.gene_product_stoichiometry, + x -> [(Symbol(k), v) for (k, v) in x.gene_product_stoichiometry], maybeget(reaction_isozymes, string(rid), string(isozyme)), ), ) * - :gene_product_capacity_limits^C.ConstraintTree( - Symbol(id) => C.Constraint( + :gene_product_capacity^( + capacity isa Float64 ? + C.Constraint( value = sum( - constraints.gene_product_amounts[gp].value * - gene_product_molar_masses[gp] for gp in gps + gpa.value * gene_product_molar_masses[String(gp)] for + (gp, gpa) in constraints.gene_product_amounts ), - bound = C.Between(0, limit), - ) for (id, gps, limit) in capacity_limits + bound = C.Between(0, capacity), + ) : + C.ConstraintTree( + Symbol(id) => C.Constraint( + value = sum( + constraints.gene_product_amounts[Symbol(gp)].value * + gene_product_molar_masses[gp] for gp in gps + ), + bound = C.Between(0, limit), + ) for (id, gps, limit) in capacity_limits + ) ) optimized_constraints( diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index 4e2b621bd..f97b4a4f3 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -29,7 +29,7 @@ similar perturbation). MOMA solution then gives an expectable "easiest" adjustment of the organism towards a somewhat working state. Reference fluxes that do not exist in the model are ignored (internally, the -objective is constructed via [`squared_sum_error_objective`](@ref)). +objective is constructed via [`squared_sum_error_value`](@ref)). Additional parameters are forwarded to [`optimized_constraints`](@ref). """ @@ -40,7 +40,7 @@ function minimization_of_metabolic_adjustment_analysis( kwargs..., ) constraints = fbc_model_constraints(model) - objective = squared_sum_error_objective(constraints.fluxes, reference_fluxes) + objective = squared_sum_error_value(constraints.fluxes, reference_fluxes) optimized_constraints( constraints * :minimal_adjustment_objective^C.Constraint(objective); optimizer, @@ -122,15 +122,13 @@ function linear_minimization_of_metabolic_adjustment_analysis( constraints *= :reference_diff^difference constraints *= :reference_directional_diff_balance^sign_split_constraints( - constraints.reference_positive_diff, - constraints.reference_negative_diff, - difference, + positive = constraints.reference_positive_diff, + negative = constraints.reference_negative_diff, + signed = difference, ) - objective = sum_objective( - constraints.reference_positive_diff, - constraints.reference_negative_diff, - ) + objective = + sum_value(constraints.reference_positive_diff, constraints.reference_negative_diff) optimized_constraints( constraints * :linear_minimal_adjustment_objective^C.Constraint(objective); diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index 0075e505d..a995e2148 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -105,7 +105,7 @@ function parsimonious_flux_balance_analysis( kwargs..., ) constraints = fbc_model_constraints(model) - parsimonious_objective = squared_sum_objective(constraints.fluxes) + parsimonious_objective = squared_sum_value(constraints.fluxes) parsimonious_optimized_constraints( constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); optimizer, @@ -151,12 +151,12 @@ function linear_parsimonious_flux_balance_analysis( :fluxes_reverse^unsigned_negative_contribution_variables(ct.fluxes) constraints *= :directional_flux_balance^sign_split_constraints( - ct.fluxes_forward, - ct.fluxes_reverse, - ct.fluxes, + positive = ct.fluxes_forward, + negative = ct.fluxes_reverse, + signed = ct.fluxes, ) - parsimonious_objective = sum_objective(ct.fluxes_forward, ct.fluxes_reverse) + parsimonious_objective = sum_value(ct.fluxes_forward, ct.fluxes_reverse) parsimonious_optimized_constraints( constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); diff --git a/src/solver.jl b/src/solver.jl index 4a987a361..ad8032eef 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -72,6 +72,7 @@ function optimization_model( boolean = J.@variable(model, binary = true) J.@constraint(model, C.substitute(v, x) == b.a + boolean * (b.b - b.a)) end + add_constraint(::C.Value, _::Nothing) = nothing function add_constraint(c::C.Constraint) add_constraint(c.value, c.bound) end From ff445709248b3875659e62d860fece523a91b57f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 5 Jan 2024 17:07:02 +0100 Subject: [PATCH 475/531] require the CT bugfix --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index cc5188a5d..5b6a17c23 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.9" +ConstraintTrees = "0.9.1" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" From 6affbb78d6b32928f29791f01c3dd24ed2a96524 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 5 Jan 2024 17:21:12 +0100 Subject: [PATCH 476/531] actually constraint. --- docs/src/examples/05-enzyme-constrained-models.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index ab6d75a78..8b001ad34 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -108,7 +108,7 @@ gene_product_molar_masses = ecoli_core_gene_product_masses # The capacity limitation usually denotes an upper bound of protein available to # the cell. -total_enzyme_capacity = 100.0 # mg enzyme/gDW +total_enzyme_capacity = 50.0 # mg enzyme/gDW ### Running a basic enzyme constrained model From 7f3c75648ff1532f72366eeb264c9813786f8bc1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 8 Jan 2024 11:38:21 +0100 Subject: [PATCH 477/531] implement time limit setting Thanks @mihai-sysbio for the suggestion. Refs #807. --- src/misc/settings.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/misc/settings.jl b/src/misc/settings.jl index 1484a3634..9c87034e9 100644 --- a/src/misc/settings.jl +++ b/src/misc/settings.jl @@ -56,4 +56,11 @@ Modification that disable all output from the JuMP optimizer (shortcut for """ silence(opt_model) = J.set_silent(opt_model) +""" +$(TYPEDSIGNATURES) + +Portable way to set a time limit in seconds for the optimizer computation. +""" +set_time_limit_sec(limit) = opt_model -> J.set_time_limit_sec(opt_model, limit) + export silence From 6673d855d87c64e013e5460a10300014123603c4 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 10 Jan 2024 15:22:35 +0100 Subject: [PATCH 478/531] cleanup mmdf --- .../examples/02c-constraint-modifications.jl | 2 +- .../examples/03-parsimonious-flux-balance.jl | 4 +- .../examples/05-enzyme-constrained-models.jl | 2 +- docs/src/examples/07-loopless-models.jl | 2 +- docs/src/examples/08-community-models.jl | 4 +- src/builders/fbc.jl | 40 ++++- src/builders/mmdf.jl | 140 ------------------ src/frontend/balance.jl | 4 +- src/frontend/enzyme_constrained.jl | 2 +- src/frontend/loopless.jl | 2 +- src/frontend/mmdf.jl | 139 ++++++++++++++--- src/frontend/moma.jl | 8 +- src/frontend/parsimonious.jl | 6 +- 13 files changed, 170 insertions(+), 185 deletions(-) delete mode 100644 src/builders/mmdf.jl diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 9afaa534f..73c0f4d28 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -51,7 +51,7 @@ model = load_model("e_coli_core.json") import ConstraintTrees as C -ctmodel = fbc_model_constraints(model) +ctmodel = fbc_flux_balance_constraints(model) fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index a39a5cfc6..7d818d108 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -53,7 +53,7 @@ model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; settings = [sile # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = fbc_model_constraints(model) +ctmodel = fbc_flux_balance_constraints(model) ctmodel *= :l2objective^squared_sum_value(ctmodel.fluxes) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks @@ -88,7 +88,7 @@ minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; settings = [silence]) # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = fbc_model_constraints(model) +ctmodel = fbc_flux_balance_constraints(model) ctmodel *= :minoxphospho^squared_sum_error_value( ctmodel.fluxes, diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 8b001ad34..d7c478922 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -140,7 +140,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( import ConstraintTrees as C # create basic flux model -m = fbc_model_constraints(model) +m = fbc_flux_balance_constraints(model) # create enzyme variables m += :enzymes^gene_product_variables(model) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 4581406fb..31bf0842b 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -61,7 +61,7 @@ sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) # not use the convenience function), let's build a loopless model from scratch. # First, build a normal flux balance model -m = fbc_model_constraints(model) +m = fbc_flux_balance_constraints(model) # Next, find all internal reactions, and their associated indices for use later internal_reactions = [ diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index a044b7ecc..c40f844ec 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -51,8 +51,8 @@ model = load_model("e_coli_core.json") # because it is easier to build the model explicitly than rely on an opaque # one-shot function. -ecoli1 = fbc_model_constraints(model) -ecoli2 = fbc_model_constraints(model) +ecoli1 = fbc_flux_balance_constraints(model) +ecoli2 = fbc_flux_balance_constraints(model) # Since the models are joined through their individual exchange reactions to an # environmental exchange reactionq, we need to identify all possible exchange diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 92f83bd5b..61f39510b 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -25,7 +25,7 @@ The constructed tree contains subtrees `fluxes` (with the reaction-defining constraints), and a single constraint `objective` thad describes the objective function of the model. """ -function fbc_model_constraints(model::A.AbstractFBCModel) +function fbc_flux_balance_constraints(model::A.AbstractFBCModel) rxns = Symbol.(A.reactions(model)) mets = Symbol.(A.metabolites(model)) lbs, ubs = A.bounds(model) @@ -34,7 +34,7 @@ function fbc_model_constraints(model::A.AbstractFBCModel) obj = A.objective(model) return C.ConstraintTree( - :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * + :fluxes^C.variables(keys = reactions, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( met => C.Constraint( value = C.LinearValue(SparseArrays.sparse(row)), @@ -45,4 +45,38 @@ function fbc_model_constraints(model::A.AbstractFBCModel) ) end -export fbc_model_constraints +export fbc_flux_balance_constraints + +""" +$(TYPEDSIGNATURES) + +TODO +""" +function fbc_log_concentration_constraints( + model::A.AbstractFBCModel; + concentration_bound = _ -> nothing, +) + rxns = Symbol.(A.reations(model)) + mets = Symbol.(A.metabolites(model)) + stoi = A.stoichiometry(model) + + constraints = + :log_concentrations^C.variables(keys = mets, bounds = concentration_bound.(mets)) + + :reactant_log_concentrations^C.variables(keys = rxns) + + cs = C.ConstraintTree() + + for (midx, ridx, coeff) in zip(findnz(stoi)...) + rid = rxns[ridx] + value = constraints.log_concentrations[mets[midx]] * coeff + if haskey(cs, rid) + cs[rid].value += value + else + cs[rid] = C.Constraint(; value) + end + end + + return constraints * :concentration_stoichiometry^cs +end + +export fbc_log_concentration_constraints diff --git a/src/builders/mmdf.jl b/src/builders/mmdf.jl deleted file mode 100644 index 8a5c98969..000000000 --- a/src/builders/mmdf.jl +++ /dev/null @@ -1,140 +0,0 @@ - -# Copyright (c) 2021-2024, University of Luxembourg -# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -$(TYPEDSIGNATURES) - -Build a max min driving force analysis model. See the docstring of -[`max_min_driving_force_analysis`](@ref) for details about the arguments. -""" -function max_min_driving_force_constraints( - model::A.AbstractFBCModel, - reaction_standard_gibbs_free_energies::Dict{String,Float64}; - reference_flux = Dict{String,Float64}(), - proton_ids::Vector{String}, - water_ids::Vector{String}, - concentration_lb = 1e-9, # M - concentration_ub = 1e-1, # M - T = 298.15, # Kelvin - R = 8.31446261815324e-3, # kJ/K/mol - ignore_reaction_ids = String[], -) - # check if reactions that will be used in the model all have thermodynamic data, otherwise throw error. - # only these reactions will form part of the model. - rxn_source = - isempty(reference_flux) ? A.reactions(model) : collect(keys(reference_flux)) - rxns = Set(x for x in rxn_source if !(x in ignore_reaction_ids)) - isempty(setdiff(rxns, keys(reaction_standard_gibbs_free_energies))) || throw( - DomainError( - reaction_standard_gibbs_free_energies, - """ - Not all reactions have thermodynamic data. - Either add the reactions with missing ΔG0s to ignore_reaction_ids, - or add the data to reaction_standard_gibbs_free_energies. - """, - ), - ) - rxns = collect(rxns) - - # import metabolite ids (if used), and reaction stoichiometries from an AbstractFBCModel - mets = Set(m for rid in rxns for m in keys(A.reaction_stoichiometry(model, rid))) - mets = collect(mets) - stoi = A.stoichiometry(model) - - # create thermodynamic variables - constraints = C.ConstraintTree( - :max_min_driving_force^C.variable() + - :log_metabolite_concentrations^C.variables( - keys = Symbol.(mets), - bounds = C.Between(log(concentration_lb), log(concentration_ub)), - ) + - :delta_G_reactions^C.variables(keys = Symbol.(rxns)), - ) - - #= - Build gibbs free energy relations - ΔG_rxn == ΔG0 + R * T * sum ( log_concentration_variable * stoichiometry_value ) - =# - model_rxns = A.reactions(model) - model_mets = A.metabolites(model) - dG0s_met_ids_stoichs = Vector{Tuple{Float64,Vector{String},Vector{Float64}}}() - for rxn in rxns # prepare to create CT below - met_idxs, stoich_coeffs = - SparseArrays.findnz(stoi[:, findfirst(==(rxn), model_rxns)]) - met_ids = model_mets[met_idxs] - dG0 = reaction_standard_gibbs_free_energies[rxn] - push!(dG0s_met_ids_stoichs, (dG0, met_ids, stoich_coeffs)) - end - - constraints *= - :delta_G_reaction_equations^C.ConstraintTree( - Symbol(rxn) => C.Constraint( - value = -constraints.delta_G_reactions[Symbol(rxn)].value + - dG0 + - R * - T * - sum( - constraints.log_metabolite_concentrations[Symbol( - met_id, - )].value * stoich for - (met_id, stoich) in zip(met_ids, stoich_coeffs) - ), - bound = C.EqualTo(0.0), - ) for (rxn, (dG0, met_ids, stoich_coeffs)) in zip(rxns, dG0s_met_ids_stoichs) - ) - - #= - Set proton log concentration to zero so that it won't impact any - calculations (biothermodynamics assumption). Also set water concentrations - to zero (aqueous condition assumptions). How true is "aqueous conditions"? - Debatable... - =# - for met in [Symbol.(proton_ids); Symbol.(water_ids)] - if haskey(constraints.log_metabolite_concentrations, met) - constraints.log_metabolite_concentrations[met] = C.Constraint( - value = constraints.log_metabolite_concentrations[met].value, - bound = C.EqualTo(0.0), - ) - end - end - - #= - Add thermodynamic feasibility constraint (ΔG < 0 for a feasible reaction in flux direction). - Add objective constraint to solve max min problem. - =# - constraints *= - :reaction_delta_G_margin^C.ConstraintTree( - Symbol(rxn) => C.Constraint( - value = constraints.delta_G_reactions[Symbol(rxn)].value * - sign(get(reference_flux, rxn, 1.0)), - bound = C.Between(-Inf, 0.0), - ) for rxn in rxns - ) - - constraints *= - :min_driving_force_margin^C.ConstraintTree( - Symbol(rxn) => C.Constraint( - value = constraints.max_min_driving_force.value + - constraints.delta_G_reactions[Symbol(rxn)].value * - sign(get(reference_flux, rxn, 1.0)), - bound = C.Between(-Inf, 0.0), - ) for rxn in rxns - ) - - constraints -end - -export max_min_driving_force_constraints diff --git a/src/frontend/balance.jl b/src/frontend/balance.jl index 624f584eb..7fa55636d 100644 --- a/src/frontend/balance.jl +++ b/src/frontend/balance.jl @@ -22,10 +22,10 @@ Compute an optimal objective-optimizing solution of the given `model`. Most arguments are forwarded to [`optimized_constraints`](@ref). Returns a tree with the optimization solution of the same shape as -given by [`fbc_model_constraints`](@ref). +given by [`fbc_flux_balance_constraints`](@ref). """ function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) optimized_constraints( constraints; objective = constraints.objective.value, diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index d7b29dec8..e2c89e670 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -55,7 +55,7 @@ function enzyme_constrained_flux_balance_analysis( optimizer, settings = [], ) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) # might be nice to omit some conditionally (e.g. slash the direction if one # kcat is nothing) diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index 5225a4c3d..b69dc16dd 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -45,7 +45,7 @@ function loopless_flux_balance_analysis( optimizer, ) - m = fbc_model_constraints(model) + m = fbc_flux_balance_constraints(model) # find all internal reactions internal_reactions = [ diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 1f0d1b085..3d48dbeae 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -49,7 +49,7 @@ Reactions specified in `ignore_reaction_ids` are internally ignored when calculating the max-min driving force. Importantly, this should include water and proton importers. -Since biochemical thermodynamics are assumed, the `proton_ids` and `water_ids` +Since biochemical thermodynamics are assumed, the `proton_metabolites` and `water_metabolites` need to be specified so that they can be ignored in the calculations. Effectively this assumes an aqueous environment at constant pH is used. @@ -75,43 +75,134 @@ function max_min_driving_force_analysis( reference_flux = Dict{String,Float64}(), concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), constant_concentrations = Dict{String,Float64}(), - proton_ids, - water_ids, - concentration_lb = 1e-9, # M - concentration_ub = 1e-1, # M + ignored_metabolites = Set{String}(), + proton_metabolites = Set{String}(), + water_metabolites = Set{String}(), + concentration_lower_bound = 1e-9, # M + concentration_upper_bound = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol - ignore_reaction_ids = String[], + check_ignored_reactions = missing, settings = [], optimizer, ) - m = max_min_driving_force_constraints( - model, - reaction_standard_gibbs_free_energies; - reference_flux, - concentration_lb, - concentration_ub, - R, - T, - ignore_reaction_ids, - water_ids, - proton_ids, + + # First let's check if all the identifiers are okay because we use quite a + # lot of these. + + model_reactions = Set(A.reactions(model)) + model_metabolites = Set(A.metabolites(model)) + + all(in(model_reactions), keys(reaction_standard_gibbs_free_energies)) || throw( + DomainError( + reaction_standard_gibbs_free_energies, + "unknown reactions referenced by reaction_standard_gibbs_free_energies", + o, + ), + ) + all(in(model_reactions), keys(reference_flux)) || throw( + DomainError( + reaction_standard_gibbs_free_energies, + "unknown reactions referenced by reference_flux", + ), + ) + all(in(model_metabolites), keys(constant_concentrations)) || throw( + DomainError( + constant_concentrations, + "unknown metabolites referenced by constant_concentrations", + ), + ) + all( + in(model_metabolites), + (m for (_, (x, y, _)) in concentration_ratios for m in (x, y)), + ) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by concentration_ratios", + ), + ) + all(in(model_metabolites), proton_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by proton_metabolites", + ), + ) + all(in(model_metabolites), water_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by water_metabolites", + ), + ) + all(in(model_metabolites), ignored_metabolites) || throw( + DomainError( + concentration_ratios, + "unknown metabolites referenced by ignored_metabolites", + ), ) - for (mid, val) in constant_concentrations - m.log_metabolite_concentrations[Symbol(mid)].bound = C.EqualTo(log(val)) + if !ismissing(check_ignored_reactions) && ( + all( + x -> !haskey(reaction_standard_gibbs_free_energies, x), + check_ignored_reactions, + ) || ( + union( + Set(check_ignored_reactions), + Set(keys(reaction_standard_gibbs_free_energies)), + ) != model_reactions + ) + ) + throw(AssertionError("check_ignored_reactions validation failed")) end - m *= - :metabolite_ratio_constraints^C.ConstraintTree( - cid => C.Constraint( + default_bound = + C.Between(log(concentration_lower_bound), log(concentration_upper_bound)) + zero_concentration_metabolites = union( + Set(Symbol.(water_metabolites)), + Set(Symbol.(proton_metabolites)), + Set(Symbol.(ignored_metabolites)), + ) + + constraints = + fbc_log_concentration_constraints( + model, + concentration_bound = met -> if met in zero_concentration_metabolites + C.EqualTo(0) + else + mid = String(met) + if haskey(constant_concentrations, mid) + C.EqualTo(log(constant_concentrations[mid])) + else + default_bound + end + end, + ) + :max_min_driving_force^C.variable() + + min_driving_forces = C.ConstraintTree( + let r = Symbol(rid) + r => C.Constraint( + value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r], + bound = C.Between(-Inf, 0), + ) + end for (rid, dGr0) in reaction_standard_gibbs_free_energies + ) + + constraints = + constraints * + :min_driving_forces^min_driving_forces * + :min_driving_force_thresholds^C.map(min_driving_forces) do c + C.Constraint( + value = constraints.max_min_driving_force.value - c.value; + bound = C.Between(0, Inf), + ) + end * + :concentration_ratio_constraints^C.ConstraintTree( + Symbol(cid) => C.Constraint( m.log_metabolite_concentrations[Symbol(m2)].value - m.log_metabolite_concentrations[Symbol(m1)].value, - ratio, + log(ratio), ) for (cid, (m1, m2, ratio)) in concentration_ratios ) - optimized_constraints(m; objective = m.max_min_driving_force.value, optimizer, settings) end diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index f97b4a4f3..48aab0d88 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -39,7 +39,7 @@ function minimization_of_metabolic_adjustment_analysis( optimizer; kwargs..., ) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) objective = squared_sum_error_value(constraints.fluxes, reference_fluxes) optimized_constraints( constraints * :minimal_adjustment_objective^C.Constraint(objective); @@ -72,7 +72,7 @@ function minimization_of_metabolic_adjustment_analysis( reference_settings = settings, kwargs..., ) - reference_constraints = fbc_model_constraints(reference_model) + reference_constraints = fbc_flux_balance_constraints(reference_model) reference_fluxes = optimized_constraints( reference_constraints; optimizer = reference_optimizer, @@ -106,7 +106,7 @@ function linear_minimization_of_metabolic_adjustment_analysis( optimizer; kwargs..., ) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) difference = C.zip(ct.fluxes, C.Tree(reference_fluxes)) do orig, ref C.Constraint(orig.value - ref) @@ -148,7 +148,7 @@ function linear_minimization_of_metabolic_adjustment_analysis( reference_settings = settings, kwargs..., ) - reference_constraints = fbc_model_constraints(reference_model) + reference_constraints = fbc_flux_balance_constraints(reference_model) reference_fluxes = optimized_constraints( reference_constraints; optimizer = reference_optimizer, diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index a995e2148..a6acccc6c 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -96,7 +96,7 @@ with some (objectives) filled in automatically to fit the common processing of FBC models, and some (`tolerances`) provided with more practical defaults. Similarly to the [`flux_balance_analysis`](@ref), returns a tree with the optimization -solutions of the shape as given by [`fbc_model_constraints`](@ref). +solutions of the shape as given by [`fbc_flux_balance_constraints`](@ref). """ function parsimonious_flux_balance_analysis( model::A.AbstractFBCModel, @@ -104,7 +104,7 @@ function parsimonious_flux_balance_analysis( tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), kwargs..., ) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) parsimonious_objective = squared_sum_value(constraints.fluxes) parsimonious_optimized_constraints( constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); @@ -144,7 +144,7 @@ function linear_parsimonious_flux_balance_analysis( tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), kwargs..., ) - constraints = fbc_model_constraints(model) + constraints = fbc_flux_balance_constraints(model) constraints = constraints + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + From 5f3b7e8a5d30edb7d6d19444995dcc183bee9a9c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 10 Jan 2024 21:16:31 +0100 Subject: [PATCH 479/531] fix docs and reference flux This evening commit is written in honor of GERD, the everyone's fav baby reflux. --- src/frontend/mmdf.jl | 115 +++++++++++++++++++++---------------------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 3d48dbeae..83992af52 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -18,56 +18,41 @@ $(TYPEDSIGNATURES) Perform a max-min driving force analysis using `optimizer` on the `model` with -supplied ΔG⁰s in `reaction_standard_gibbs_free_energies`, as defined by Noor, et -al., "Pathway thermodynamics highlights kinetic obstacles in central -metabolism.", PLoS computational biology, 2014. - -Optionally, `reference_flux` can be used to set the directions of each reaction -in `model` (all reactions are assumed to proceed forward by default). The -supplied `reference_flux` should be free of internal cycles i.e. -thermodynamically consistent. This optional input is important if a reaction in -`model` normally runs in reverse (negative flux). Note, only the signs are -extracted from this input, so be careful with floating point precision near 0. - -The max-min driving force algorithm returns the Gibbs free energy of the -reactions, the concentrations of metabolites and the actual maximum minimum -driving force. The optimization problem solved is: -``` -max min -ΔᵣG -s.t. ΔᵣG = ΔrG⁰ + R T S' ln(C) - ΔᵣG ≤ 0 - ln(Cₗ) ≤ ln(C) ≤ ln(Cᵤ) -``` -where `ΔrG` are the Gibbs energies dissipated by the reactions, R is the gas -constant, T is the temperature, S is the stoichiometry of the model, and C is -the vector of metabolite concentrations (and their respective lower and upper -bounds). - -In case no feasible solution exists, `nothing` is returned. - -Reactions specified in `ignore_reaction_ids` are internally ignored when -calculating the max-min driving force. Importantly, this should include water -and proton importers. - -Since biochemical thermodynamics are assumed, the `proton_metabolites` and `water_metabolites` -need to be specified so that they can be ignored in the calculations. -Effectively this assumes an aqueous environment at constant pH is used. - -`constant_concentrations` is used to fix the concentrations of certain -metabolites (such as CO₂) by passing a dictionary mapping metabolite id to its -constant value. `concentration_ratios` is used to specify additional constraints -on metabolite pair concentrations (typically, this is done with various -cofactors, such as the ATP/ADP ratio. For example, you can fix the concentration -of ATP to be always 5× higher than of ADP by specifying `Dict("atp_ratio" => -("atp_c","adp_c", 5.0))` if these metabolites are called `atp_c` and `adp_c` in -the model. `concentration_lb` and `concentration_ub` set the `Cₗ` and `Cᵤ` in -the optimization problems (these are overwritten by `constant_concentrations` if -supplied). - -`T` and `R` can be specified in the corresponding units; defaults are K and -kJ/K/mol. The unit of metabolite concentrations is typically molar, and the ΔG⁰s -have units of kJ/mol. Other units can be used, as long as they are consistent. -As usual, optimizer settings can be changed with `settings`. +supplied reaction standard Gibbs energies in `reaction_standard_gibbs_free_energies`. + +The method was described by by: Noor, et al., "Pathway thermodynamics highlights +kinetic obstacles in central metabolism.", PLoS computational biology, 2014. + +`reference_flux` sets the directions of each reaction in `model`. The scale of +the values is not important, only the direction is examined (w.r.t. +`reference_flux_atol` tolerance). Ideally, the supplied `reference_flux` should +be completely free of internal cycles, which enables the thermodynamic +consistency. To get the cycle-free flux, you can use +[`loopless_flux_balance_analysis`](@ref) (computationally complex but gives +very good fluxes), [`parsimonious_flux_balance_analysis`](@ref) or +[`linear_parsimonious_flux_balance_analysis`](@ref) (computationally simplest +but the consistency is not guaranteed). + +Internally, [`fbc_log_concentration_constraints`](@ref) is used to lay out the +base structure of the problem. + +Following arguments are set optionally: +- `water_metabolites`, `proton_metabolites` and `ignored_metabolites` allow to + completely ignore constraints on a part of the metabolite set, which is + explicitly recommended especially for water and protons (since the analyses + generally assume aqueous environment of constant pH) +- `constant_concentrations` can be used to fix the concentrations of the + metabolites +- `concentration_lower_bound` and `concentration_upper_bound` set the default + concentration bounds for all other metabolites +- `concentration ratios` is a dictionary that assigns a tuple of + metabolite-metabolite-concentration ratio constraint names; e.g. ATP/ADP + ratio can be fixed to five-times-more-ATP by setting `concentration_ratios = + Dict("adenosin_ratio" => ("atp", "adp", 5.0))` +- `T` and `R` default to the usual required thermodynamic constraints in the + usual units (K and kJ/K/mol, respectively). These multiply the + log-concentrations to obtain the actual Gibbs energies and thus driving + forces. """ function max_min_driving_force_analysis( model::A.AbstractFBCModel, @@ -82,13 +67,15 @@ function max_min_driving_force_analysis( concentration_upper_bound = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol + reference_flux_atol = 1e-6, check_ignored_reactions = missing, settings = [], optimizer, ) - # First let's check if all the identifiers are okay because we use quite a - # lot of these. + # First let's just check if all the identifiers are okay because we use + # quite a lot of these; the rest of the function may be a bit cleaner with + # this checked properly. model_reactions = Set(A.reactions(model)) model_metabolites = Set(A.metabolites(model)) @@ -97,9 +84,10 @@ function max_min_driving_force_analysis( DomainError( reaction_standard_gibbs_free_energies, "unknown reactions referenced by reaction_standard_gibbs_free_energies", - o, ), ) + all(x -> haskey(reaction_standard_gibbs_free_energies, x), keys(reference_flux)) || + throw(DomainError(reference_flux, "some reactions have no reference flux")) all(in(model_reactions), keys(reference_flux)) || throw( DomainError( reaction_standard_gibbs_free_energies, @@ -154,9 +142,12 @@ function max_min_driving_force_analysis( throw(AssertionError("check_ignored_reactions validation failed")) end - default_bound = + # that was a lot of checking. + + default_concentration_bound = C.Between(log(concentration_lower_bound), log(concentration_upper_bound)) - zero_concentration_metabolites = union( + + no_concentration_metabolites = union( Set(Symbol.(water_metabolites)), Set(Symbol.(proton_metabolites)), Set(Symbol.(ignored_metabolites)), @@ -165,14 +156,14 @@ function max_min_driving_force_analysis( constraints = fbc_log_concentration_constraints( model, - concentration_bound = met -> if met in zero_concentration_metabolites + concentration_bound = met -> if met in no_concentration_metabolites C.EqualTo(0) else mid = String(met) if haskey(constant_concentrations, mid) C.EqualTo(log(constant_concentrations[mid])) else - default_bound + default_concentration_bound end end, ) + :max_min_driving_force^C.variable() @@ -181,7 +172,15 @@ function max_min_driving_force_analysis( let r = Symbol(rid) r => C.Constraint( value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r], - bound = C.Between(-Inf, 0), + bound = let rf = reference_flux[rid] + if isapprox(rf, 0.0, atol = reference_flux_atol) + C.EqualTo(0) + elseif rf > 0 + C.Between(-Inf, 0) + else + C.Between(0, Inf) + end + end, ) end for (rid, dGr0) in reaction_standard_gibbs_free_energies ) From c046332836f6a4ccefd71ce2e8fb596ea8fc9de7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 11 Jan 2024 10:01:52 +0100 Subject: [PATCH 480/531] clean more --- src/COBREXA.jl | 2 +- src/frontend/loopless.jl | 31 +++++++++++++----------------- src/frontend/parsimonious.jl | 4 ++-- src/misc/{utils.jl => boundary.jl} | 0 4 files changed, 16 insertions(+), 21 deletions(-) rename src/misc/{utils.jl => boundary.jl} (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index c83cfa28c..68c02b485 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -68,9 +68,9 @@ include("frontend/loopless.jl") include("frontend/enzyme_constrained.jl") # utilities +include("misc/boundary.jl") include("misc/bounds.jl") include("misc/settings.jl") include("misc/maybe.jl") -include("misc/utils.jl") end # module COBREXA diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index b69dc16dd..88cda951f 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -17,25 +17,20 @@ """ $(TYPEDSIGNATURES) -Add quasi-thermodynamic constraints to the model to ensure that no thermodynamically -infeasible internal cycles can occur. Adds the following constraints to the problem: -``` --max_flux_bound × (1 - yᵢ) ≤ xᵢ ≤ max_flux_bound × yᵢ --max_flux_bound × yᵢ + strict_inequality_tolerance × (1 - yᵢ) ≤ Gᵢ -Gᵢ ≤ -strict_inequality_tolerance × yᵢ + max_flux_bound × (1 - yᵢ) -Nᵢₙₜ' × G = 0 -yᵢ ∈ {0, 1} -Gᵢ ∈ ℝ -i ∈ internal reactions -Nᵢₙₜ is the nullspace of the internal stoichiometric matrix -``` -Note, this modification introduces binary variables, so an optimization solver capable of -handing mixed integer problems needs to be used. The arguments `max_flux_bound` and -`strict_inequality_tolerance` implement the "big-M" method of indicator constraints. +Perform a flux balance analysis with added quasi-thermodynamic constraints that +ensure that thermodynamically infeasible internal cycles can not occur. The +method is closer described by: Schellenberger, Lewis, and, Palsson. +"Elimination of thermodynamically infeasible loops in steady-state metabolic +models.", Biophysical journal, 2011`. -For more details about the algorithm, see `Schellenberger, Lewis, and, Palsson. "Elimination -of thermodynamically infeasible loops in steady-state metabolic models.", Biophysical -journal, 2011`. +The loopless condition comes with a performance overhead: the computation needs +to find the null space of the stoichiometry matrix (essentially inverting it); +and the subsequently created optimization problem contains binary variables for +each internal reaction, thus requiring a MILP solver and a potentially +exponential solving time. + +The arguments `max_flux_bound` and `strict_inequality_tolerance` implement the +"big-M" method of indicator constraints (TODO as described by the paper?). """ function loopless_flux_balance_analysis( model; diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index a6acccc6c..52c1a02dc 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -34,7 +34,7 @@ function parsimonious_optimized_constraints( parsimonious_objective::C.Value, parsimonious_optimizer = nothing, parsimonious_sense = J.MIN_SENSE, - parsimonious_modifications = [], + parsimonious_settings = [], tolerances = [absolute_tolerance_bound(0)], output = constraints, kwargs..., @@ -52,7 +52,7 @@ function parsimonious_optimized_constraints( # switch to parsimonizing the solution w.r.t. to the objective value isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer) - for m in parsimonious_modifications + for m in parsimonious_settings m(om) end diff --git a/src/misc/utils.jl b/src/misc/boundary.jl similarity index 100% rename from src/misc/utils.jl rename to src/misc/boundary.jl From d9af09cbfc772a42fb5e4b1bd97ae5c88de80e6b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 11 Jan 2024 13:29:23 +0100 Subject: [PATCH 481/531] loopless WIP --- src/COBREXA.jl | 1 - src/builders/loopless.jl | 111 +++++++++++++++------------------------ src/frontend/loopless.jl | 54 ++++++++++--------- src/misc/boundary.jl | 29 ---------- 4 files changed, 73 insertions(+), 122 deletions(-) delete mode 100644 src/misc/boundary.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 68c02b485..bc6f4e7a7 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -68,7 +68,6 @@ include("frontend/loopless.jl") include("frontend/enzyme_constrained.jl") # utilities -include("misc/boundary.jl") include("misc/bounds.jl") include("misc/settings.jl") include("misc/maybe.jl") diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index dc7488c5e..a39d9f80a 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -36,85 +36,60 @@ internal_reaction_stoichiometry_nullspace_columns = eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_rxn_idxs_in_order_of_internal_rxn_ids]))) ``` """ -function with_loopless_constraints( - m, - internal_reaction_ids, - internal_reaction_stoichiometry_nullspace_columns; - fluxes = m.fluxes, - max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = 1.0, # heuristic from paper +function loopless_constraints(; + fluxes::C.ConstraintTree, + loopless_direction_indicators::C.ConstraintTree, + loopless_driving_forces::C.ConstraintTree, + internal_reactions::Vector{Symbol}, + internal_nullspace::Matrix, + flux_infinity_bound, + driving_force_nonzero_bound, + driving_force_infinity_bound, ) - # add loopless variables: flux direction setters (binary) and pseudo gibbs free energy of reaction - m += - :loopless_binary_variables^C.variables( - keys = internal_reaction_ids, - bounds = Switch(0, 1), - ) - m += - :pseudo_gibbs_free_energy_reaction^C.variables( - keys = internal_reaction_ids, - bounds = C.Between(-Inf, Inf), - ) - - # add -1000 * (1-a) ≤ v ≤ 1000 * a which need to be split into forward and backward components - # -1000 * (1-a) - v ≤ 0 (backward) - # v - 1000a ≤ 0 (forward) - m *= - :loopless_reaction_directions^:backward^C.ConstraintTree( - rid => C.Constraint( - value = -max_flux_bound * (1 - m.loopless_binary_variables[rid].value) - - fluxes[rid].value, - bound = C.Between(Inf, 0), - ) for rid in internal_reaction_ids - ) - m *= - :loopless_reaction_directions^:forward^C.ConstraintTree( - rid => C.Constraint( - value = fluxes[rid].value - - max_flux_bound * m.loopless_binary_variables[rid].value, + C.ConstraintTree( + :flux_direction_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value + flux_infinity_bound * (1-loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r=internal_reactions + ), + :flux_direction_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value + flux_infinity_bound * loopless_direction_indicators[r].value, bound = C.Between(-Inf, 0), - ) for rid in internal_reaction_ids - ) - - # add -1000*a + 1 * (1-a) ≤ Gibbs ≤ -1 * a + 1000 * (1 - a) which also need to be split - # -1000 * a + 1 * (1-a) - G ≤ 0 (backward) - # G + 1 * a - 1000 * (1-a) ≤ 0 (forward) - m *= - :loopless_pseudo_gibbs_sign^:backward^C.ConstraintTree( - rid => C.Constraint( - value = -max_flux_bound * m.loopless_binary_variables[rid].value + + ) for r=internal_reactions + ), + :driving_force_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value - strict_inequality_tolerance * - (1 - m.loopless_binary_variables[rid].value) - - m.pseudo_gibbs_free_energy_reaction[rid].value, - bound = (-Inf, 0), - ) for rid in internal_reaction_ids - ) - m *= - :loopless_pseudo_gibbs_sign^:forward^C.ConstraintTree( - rid => C.Constraint( - value = m.pseudo_gibbs_free_energy_reaction[rid].value + + loopless_direction_indicators[r].value + + flux_infinity_bound * (1 - loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r in internal_reaction_ids + ), + :driving_force_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value + strict_inequality_tolerance * - m.loopless_binary_variables[rid].value - - max_flux_bound * (1 - m.loopless_binary_variables[rid].value), - bound = (-Inf, 0), - ) for rid in internal_reaction_ids - ) - - # use nullspace to ensure there are no loops - m *= - :loopless_condition^C.ConstraintTree( + (1 - loopless_direction_indicators[r].value) - + flux_infinity_bound * loopless_direction_indicators[r].value, + bound = C.Between(-Inf, 0), + ) for r in internal_reaction_ids + ), + :loopless_condition => C.ConstraintTree( + #TODO Symbol(:nullspace_vector, i) => C.Constraint( value = sum( col[j] * - m.pseudo_gibbs_free_energy_reaction[internal_reaction_ids[j]].value + loopless_driving_forces[internal_reaction_ids[j]].value for j in eachindex(col) ), bound = C.EqualTo(0), ) for (i, col) in enumerate(internal_reaction_stoichiometry_nullspace_columns) - ) - - m + ), + ) end -export with_loopless_constraints +export loopless_constraints diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index 88cda951f..f8424e405 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -29,40 +29,46 @@ and the subsequently created optimization problem contains binary variables for each internal reaction, thus requiring a MILP solver and a potentially exponential solving time. -The arguments `max_flux_bound` and `strict_inequality_tolerance` implement the -"big-M" method of indicator constraints (TODO as described by the paper?). +The arguments `driving_force_max_bound` and `driving_force_nonzero_bound` set +the bounds (possibly negated ones) on the virtual "driving forces" (G_i in the +paper). """ function loopless_flux_balance_analysis( - model; - max_flux_bound = 1000.0, # needs to be an order of magnitude bigger, big M method heuristic - strict_inequality_tolerance = 1.0, # heuristic from paper + model::A.AbstractFBCModel; + flux_infinity_bound = 10000.0, + driving_force_nonzero_bound = 1.0, + driving_force_infinity_bound = 1000.0, settings = [], optimizer, ) - m = fbc_flux_balance_constraints(model) + constraints = fbc_flux_balance_constraints(model) - # find all internal reactions - internal_reactions = [ - (i, Symbol(rid)) for - (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) - ] - internal_reaction_ids = last.(internal_reactions) - internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below + rxns = A.reactions(model) + stoi = A.stoichiometry(model) + internal_mask = count(stoi .!= 0; dims = 1)[begin,:] .> 1 + internal_reactions = Symbol.(rxns[reactions_internal]), - internal_reaction_stoichiometry_nullspace_columns = eachcol( - LinearAlgebra.nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs])), - ) # no sparse nullspace function - - m = with_loopless_constraints( - m, - internal_reaction_ids, - internal_reaction_stoichiometry_nullspace_columns; - max_flux_bound, - strict_inequality_tolerance, + constraints = constraints + + :loopless_directions ^ C.variables( + keys = internal_reactions, + bounds = Switch(0, 1) + ) + + :loopless_driving_forces ^ C.variables( + keys = internal_reactions + ) + + constraints *= :loopless_constraints^loopless_constraints(; + fluxes = constraints.fluxes, + loopless_directions = constraints.loopless_directions, + loopless_driving_forces = constraints.loopless_driving_forces, + internal_reactions, + internal_nullspace = LinearAlgebra.nullspace(Matrix(stoi[:, internal_mask])), + flux_infinity_bound, + driving_force_nonzero_bound, + driving_force_infinity_bound, ) - # solve optimized_constraints(m; objective = m.objective.value, optimizer, settings) end diff --git a/src/misc/boundary.jl b/src/misc/boundary.jl deleted file mode 100644 index 435cb0961..000000000 --- a/src/misc/boundary.jl +++ /dev/null @@ -1,29 +0,0 @@ - -# Copyright (c) 2021-2024, University of Luxembourg -# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. - - -""" -$(TYPEDSIGNATURES) - -Return true if the reaction denoted by `rxn_id` in `model` is a boundary -reaction, otherwise return false. Checks if on boundary by inspecting the number -of metabolites in the reaction stoichiometry. Boundary reactions have only one -metabolite, e.g. an exchange reaction, or a sink/demand reaction. -""" -is_boundary(model::A.AbstractFBCModel, rxn_id::String) = - length(keys(A.reaction_stoichiometry(model, rxn_id))) == 1 - -export is_boundary From 162bb012deabec9f41b4fd281dd28eb008f2aeaa Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 10 Jan 2024 15:22:35 +0100 Subject: [PATCH 482/531] small mmdf cleaning --- src/frontend/mmdf.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 83992af52..5a148efc2 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -127,7 +127,6 @@ function max_min_driving_force_analysis( "unknown metabolites referenced by ignored_metabolites", ), ) - if !ismissing(check_ignored_reactions) && ( all( x -> !haskey(reaction_standard_gibbs_free_energies, x), From 93e83abef05d9a9ff6dcad8d2cc7fa897c696f63 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 11 Jan 2024 15:02:41 +0100 Subject: [PATCH 483/531] finish loopless FBA --- src/builders/loopless.jl | 22 +++++++++++----------- src/frontend/loopless.jl | 39 +++++++++++++++++++-------------------- 2 files changed, 30 insertions(+), 31 deletions(-) diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index a39d9f80a..ecfb4b3a0 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -50,15 +50,17 @@ function loopless_constraints(; C.ConstraintTree( :flux_direction_lower_bounds => C.ConstraintTree( r => C.Constraint( - value = fluxes[r].value + flux_infinity_bound * (1-loopless_direction_indicators[r].value), + value = fluxes[r].value + + flux_infinity_bound * (1 - loopless_direction_indicators[r].value), bound = C.Between(0, Inf), - ) for r=internal_reactions + ) for r in internal_reactions ), :flux_direction_upper_bounds => C.ConstraintTree( r => C.Constraint( - value = fluxes[r].value + flux_infinity_bound * loopless_direction_indicators[r].value, + value = fluxes[r].value + + flux_infinity_bound * loopless_direction_indicators[r].value, bound = C.Between(-Inf, 0), - ) for r=internal_reactions + ) for r in internal_reactions ), :driving_force_lower_bounds => C.ConstraintTree( r => C.Constraint( @@ -78,16 +80,14 @@ function loopless_constraints(; bound = C.Between(-Inf, 0), ) for r in internal_reaction_ids ), - :loopless_condition => C.ConstraintTree( - #TODO - Symbol(:nullspace_vector, i) => C.Constraint( + :loopless_nullspace => C.ConstraintTree( + Symbol(:nullspace_base_, i) => C.Constraint( value = sum( - col[j] * - loopless_driving_forces[internal_reaction_ids[j]].value - for j in eachindex(col) + coeff * loopless_driving_forces[r].value for + (coeff, r) in zip(vec, internal_reactions) ), bound = C.EqualTo(0), - ) for (i, col) in enumerate(internal_reaction_stoichiometry_nullspace_columns) + ) for (i, col) in enumerate(eachcol(internal_nullspace)) ), ) end diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index f8424e405..e4853eda2 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -46,28 +46,27 @@ function loopless_flux_balance_analysis( rxns = A.reactions(model) stoi = A.stoichiometry(model) - internal_mask = count(stoi .!= 0; dims = 1)[begin,:] .> 1 - internal_reactions = Symbol.(rxns[reactions_internal]), + internal_mask = count(stoi .!= 0; dims = 1)[begin, :] .> 1 + internal_reactions = + Symbol.(rxns[reactions_internal]), constraints = + constraints + + :loopless_directions^C.variables( + keys = internal_reactions, + bounds = Switch(0, 1), + ) + + :loopless_driving_forces^C.variables(keys = internal_reactions) - constraints = constraints + - :loopless_directions ^ C.variables( - keys = internal_reactions, - bounds = Switch(0, 1) - ) + - :loopless_driving_forces ^ C.variables( - keys = internal_reactions + constraints *= + :loopless_constraints^loopless_constraints(; + fluxes = constraints.fluxes, + loopless_directions = constraints.loopless_directions, + loopless_driving_forces = constraints.loopless_driving_forces, + internal_reactions, + internal_nullspace = LinearAlgebra.nullspace(Matrix(stoi[:, internal_mask])), + flux_infinity_bound, + driving_force_nonzero_bound, + driving_force_infinity_bound, ) - - constraints *= :loopless_constraints^loopless_constraints(; - fluxes = constraints.fluxes, - loopless_directions = constraints.loopless_directions, - loopless_driving_forces = constraints.loopless_driving_forces, - internal_reactions, - internal_nullspace = LinearAlgebra.nullspace(Matrix(stoi[:, internal_mask])), - flux_infinity_bound, - driving_force_nonzero_bound, - driving_force_infinity_bound, - ) optimized_constraints(m; objective = m.objective.value, optimizer, settings) end From 2ae9b072148679860a5907a188be49f62c2ea17c Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 11 Jan 2024 15:10:52 +0100 Subject: [PATCH 484/531] remove a few unnecessary things --- src/frontend/balance.jl | 8 -------- src/frontend/moma.jl | 24 ++++++++++++------------ src/frontend/parsimonious.jl | 16 ---------------- 3 files changed, 12 insertions(+), 36 deletions(-) diff --git a/src/frontend/balance.jl b/src/frontend/balance.jl index 7fa55636d..f58692c0a 100644 --- a/src/frontend/balance.jl +++ b/src/frontend/balance.jl @@ -34,12 +34,4 @@ function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) ) end -""" -$(TYPEDSIGNATURES) - -Pipe-able overload of [`flux_balance_analysis`](@ref). -""" -flux_balance_analysis(optimizer; settings = []) = - m -> flux_balance_analysis(m, optimizer; settings) - export flux_balance_analysis diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index 48aab0d88..6c43a891d 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -33,7 +33,7 @@ objective is constructed via [`squared_sum_error_value`](@ref)). Additional parameters are forwarded to [`optimized_constraints`](@ref). """ -function minimization_of_metabolic_adjustment_analysis( +function minimization_of_metabolic_adjustment( model::A.AbstractFBCModel, reference_fluxes::Dict{Symbol,Float64}, optimizer; @@ -54,16 +54,16 @@ end $(TYPEDSIGNATURES) A slightly easier-to-use version of -[`minimization_of_metabolic_adjustment_analysis`](@ref) that computes the +[`minimization_of_metabolic_adjustment`](@ref) that computes the reference flux as the optimal solution of the [`reference_model`](@ref). The reference flux is calculated using `reference_optimizer` and `reference_modifications`, which default to the `optimizer` and `settings`. Leftover arguments are passed to the overload of -[`minimization_of_metabolic_adjustment_analysis`](@ref) that accepts the +[`minimization_of_metabolic_adjustment`](@ref) that accepts the reference flux dictionary. """ -function minimization_of_metabolic_adjustment_analysis( +function minimization_of_metabolic_adjustment( model::A.AbstractFBCModel, reference_model::A.AbstractFBCModel, optimizer; @@ -80,7 +80,7 @@ function minimization_of_metabolic_adjustment_analysis( output = reference_constraints.fluxes, ) isnothing(reference_fluxes) && return nothing - minimization_of_metabolic_adjustment_analysis( + minimization_of_metabolic_adjustment( model, reference_fluxes, optimizer; @@ -89,18 +89,18 @@ function minimization_of_metabolic_adjustment_analysis( ) end -export minimization_of_metabolic_adjustment_analysis +export minimization_of_metabolic_adjustment """ $(TYPEDSIGNATURES) -Like [`minimization_of_metabolic_adjustment_analysis`](@ref) but optimizes the +Like [`minimization_of_metabolic_adjustment`](@ref) but optimizes the L1 norm. This typically produces a sufficiently good result with less resources, depending on the situation. See documentation of -[`linear_parsimonious_flux_balance_analysis`](@ref) for some of the +[`linear_parsimonious_flux_balance`](@ref) for some of the considerations. """ -function linear_minimization_of_metabolic_adjustment_analysis( +function linear_minimization_of_metabolic_adjustment( model::A.AbstractFBCModel, reference_fluxes::Dict{Symbol,Float64}, optimizer; @@ -139,7 +139,7 @@ function linear_minimization_of_metabolic_adjustment_analysis( ) end -function linear_minimization_of_metabolic_adjustment_analysis( +function linear_minimization_of_metabolic_adjustment( model::A.AbstractFBCModel, reference_model::A.AbstractFBCModel, optimizer; @@ -156,7 +156,7 @@ function linear_minimization_of_metabolic_adjustment_analysis( output = reference_constraints.fluxes, ) isnothing(reference_fluxes) && return nothing - linear_minimization_of_metabolic_adjustment_analysis( + linear_minimization_of_metabolic_adjustment( model, reference_fluxes, optimizer; @@ -165,4 +165,4 @@ function linear_minimization_of_metabolic_adjustment_analysis( ) end -export linear_minimization_of_metabolic_adjustment_analysis +export linear_minimization_of_metabolic_adjustment diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index 52c1a02dc..9051723bd 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -116,14 +116,6 @@ function parsimonious_flux_balance_analysis( ) end -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`parsimonious_flux_balance_analysis`](@ref). -""" -parsimonious_flux_balance_analysis(optimizer; kwargs...) = - model -> parsimonious_flux_balance_analysis(model, optimizer; kwargs...) - export parsimonious_flux_balance_analysis """ @@ -168,12 +160,4 @@ function linear_parsimonious_flux_balance_analysis( ) end -""" -$(TYPEDSIGNATURES) - -Pipe-able variant of [`linear_parsimonious_flux_balance_analysis`](@ref). -""" -linear_parsimonious_flux_balance_analysis(optimizer; kwargs...) = - model -> linear_parsimonious_flux_balance_analysis(model, optimizer; kwargs...) - export linear_parsimonious_flux_balance_analysis From 7804199839b5088dfb8881ac0ab5468e80c6f8af Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 12 Jan 2024 13:14:27 +0100 Subject: [PATCH 485/531] communities with interfaces (drafty drafty) --- src/builders/communities.jl | 103 ++++++++++++++++++++++++++++++++++++ 1 file changed, 103 insertions(+) diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 239639d97..9832ad78f 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -17,6 +17,109 @@ """ $(TYPEDSIGNATURES) +Create an "interface" block for constraints created by +[`fbc_flux_balance_constraints`](@ref) (or any compatible constraint tree). +""" +fbc_boundary_constraints( + constraints::C.ConstraintTree; + ignore = _ -> false, + bound = _ -> nothing, +) +network_boundary_constraints(c.fluxes.c.flux_stoichiometry; ...) + +""" +$(TYPEDSIGNATURES) + +Create an "interface" block for a constrained reaction network described by +variables in `fluxes` and flux balance in `balances`. Boundary reactions are +assumed to be the ones that either "only create" or "only remove" the balanced +components (metabolites), i.e, each dot product of the reaction description +with all balance components is either strictly non-negative or non-positive. +""" +function network_boundary_constraints( + reactions, + balances; + ignore = _ -> false, + bound = _ -> nothing, +) + #TODO +end + +""" +$(TYPEDSIGNATURES) + +Linearly scale all bounds in a constraint tree by the `factor`. +""" +function scale_bounds(tree::C.ConstraintTree, factor) + C.map(tree) do c + isnothing(c.bound) ? c : C.Constraint(value = c.value, bound = factor * c.bound) + end +end + +""" +$(TYPEDSIGNATURES) + +Join multiple modules into a bigger module. + +Modules are like usual constraint trees, but contain an explicitly declared +`interface` part, marked properly a tuple (the parameters should form a +dictionary constructor that would generally look such as `:module_name => +(module, module.interface)`; the second tuple member may also be specified just +by name as e.g. `:interface`, or omitted while relying on `default_interface`). + +Interface parts get merged and constrained to create a new interface; networks +are copied intact. +""" +function join_modules( + ps::Pair...; + default_in_interface = :interface, + out_interface = :interface, + out_balance = :interface_balance, + ignore = _ -> false, + bound = _ -> nothing, +) + + prep(id::String, x) = prep(Symbol(id), x) + prep(id::Symbol, mod::C.ConstraintTree) = prep(id, (mod, default_interface)) + prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = + prep(id, mod, mod[interface]) + prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,C.ConstraintTree}) = + (id^(:network^mod * :interface^interface)) + + # collect everything into one huge network (while also renumbering the interfaces) + modules = sum(prep.(ps)) + + # extract a union of all non-ignored interface keys + interface_keys = + filter(!ignore, collect(union((keys(m.interface) for (_, m) in modules)...))) + + # remove the interface placeholders and add variables for the new interface + constraints = + ConstraintTree(id => (m.network) for (id, m) in modules) + + out_interface^C.variables(keys = interface_keys, bounds = bound.(interface_keys)) + + # join everything with the interrace balance and return + constraints * C.map(constraints.out_interface) do ic + C.Constraint( + value = sum(c.value for mod in modules for (k, c) in mod.interface) - ic.value, + ) + end +end + +""" +$(TYPEDSIGNATURES) + +Overload of `join_module_constraints` for general key-value containers. +""" +join_module_constraints(kv) = join_module_constraints(kv...) + +# TODO equal_growth must be preserved, should be extended to ratios (using an extra variable) +# TODO same for exchanges (ratio_bounds?) +# TODO probably similar to the distance bounds from MMDF -- unify! + +""" +$(TYPEDSIGNATURES) + Helper function to create environmental exchange rections. """ function environment_exchange_variables(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) From 86373d8ccadc01bcf18344d4e09f620d1276af98 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 12 Jan 2024 17:01:15 +0100 Subject: [PATCH 486/531] naming simplification (fbc_ prefix is a hungarian notation) --- docs/src/examples/02c-constraint-modifications.jl | 2 +- docs/src/examples/03-parsimonious-flux-balance.jl | 4 ++-- docs/src/examples/05-enzyme-constrained-models.jl | 2 +- docs/src/examples/07-loopless-models.jl | 2 +- docs/src/examples/08-community-models.jl | 4 ++-- src/builders/communities.jl | 2 +- src/builders/fbc.jl | 10 +++++----- src/frontend/balance.jl | 4 ++-- src/frontend/enzyme_constrained.jl | 2 +- src/frontend/loopless.jl | 2 +- src/frontend/mmdf.jl | 4 ++-- src/frontend/moma.jl | 8 ++++---- src/frontend/parsimonious.jl | 6 +++--- 13 files changed, 26 insertions(+), 26 deletions(-) diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 73c0f4d28..8344854f0 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -51,7 +51,7 @@ model = load_model("e_coli_core.json") import ConstraintTrees as C -ctmodel = fbc_flux_balance_constraints(model) +ctmodel = flux_balance_constraints(model) fermentation = ctmodel.fluxes.EX_ac_e.value + ctmodel.fluxes.EX_etoh_e.value diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 7d818d108..2997e7752 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -53,7 +53,7 @@ model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; settings = [sile # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = fbc_flux_balance_constraints(model) +ctmodel = flux_balance_constraints(model) ctmodel *= :l2objective^squared_sum_value(ctmodel.fluxes) ctmodel.objective.bound = 0.3 # set growth rate # TODO currently breaks @@ -88,7 +88,7 @@ minimize_metabolic_adjustment(ref_sol, Clarabel.Optimizer; settings = [silence]) # Alternatively, you can construct your own constraint tree model with # the quadratic objective (this approach is much more flexible). -ctmodel = fbc_flux_balance_constraints(model) +ctmodel = flux_balance_constraints(model) ctmodel *= :minoxphospho^squared_sum_error_value( ctmodel.fluxes, diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index d7c478922..c0bfc6abb 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -140,7 +140,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( import ConstraintTrees as C # create basic flux model -m = fbc_flux_balance_constraints(model) +m = flux_balance_constraints(model) # create enzyme variables m += :enzymes^gene_product_variables(model) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 31bf0842b..9b6df2bb5 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -61,7 +61,7 @@ sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) # not use the convenience function), let's build a loopless model from scratch. # First, build a normal flux balance model -m = fbc_flux_balance_constraints(model) +m = flux_balance_constraints(model) # Next, find all internal reactions, and their associated indices for use later internal_reactions = [ diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index c40f844ec..e3c1dd78b 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -51,8 +51,8 @@ model = load_model("e_coli_core.json") # because it is easier to build the model explicitly than rely on an opaque # one-shot function. -ecoli1 = fbc_flux_balance_constraints(model) -ecoli2 = fbc_flux_balance_constraints(model) +ecoli1 = flux_balance_constraints(model) +ecoli2 = flux_balance_constraints(model) # Since the models are joined through their individual exchange reactions to an # environmental exchange reactionq, we need to identify all possible exchange diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 9832ad78f..0d3b76b20 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -18,7 +18,7 @@ $(TYPEDSIGNATURES) Create an "interface" block for constraints created by -[`fbc_flux_balance_constraints`](@ref) (or any compatible constraint tree). +[`flux_balance_constraints`](@ref) (or any compatible constraint tree). """ fbc_boundary_constraints( constraints::C.ConstraintTree; diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 61f39510b..69e453266 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -25,7 +25,7 @@ The constructed tree contains subtrees `fluxes` (with the reaction-defining constraints), and a single constraint `objective` thad describes the objective function of the model. """ -function fbc_flux_balance_constraints(model::A.AbstractFBCModel) +function flux_balance_constraints(model::A.AbstractFBCModel) rxns = Symbol.(A.reactions(model)) mets = Symbol.(A.metabolites(model)) lbs, ubs = A.bounds(model) @@ -45,14 +45,14 @@ function fbc_flux_balance_constraints(model::A.AbstractFBCModel) ) end -export fbc_flux_balance_constraints +export flux_balance_constraints """ $(TYPEDSIGNATURES) TODO """ -function fbc_log_concentration_constraints( +function log_concentration_constraints( model::A.AbstractFBCModel; concentration_bound = _ -> nothing, ) @@ -76,7 +76,7 @@ function fbc_log_concentration_constraints( end end - return constraints * :concentration_stoichiometry^cs + return constraints * :log_concentration_stoichiometry^cs end -export fbc_log_concentration_constraints +export log_concentration_constraints diff --git a/src/frontend/balance.jl b/src/frontend/balance.jl index f58692c0a..583ce9926 100644 --- a/src/frontend/balance.jl +++ b/src/frontend/balance.jl @@ -22,10 +22,10 @@ Compute an optimal objective-optimizing solution of the given `model`. Most arguments are forwarded to [`optimized_constraints`](@ref). Returns a tree with the optimization solution of the same shape as -given by [`fbc_flux_balance_constraints`](@ref). +given by [`flux_balance_constraints`](@ref). """ function flux_balance_analysis(model::A.AbstractFBCModel, optimizer; kwargs...) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) optimized_constraints( constraints; objective = constraints.objective.value, diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzyme_constrained.jl index e2c89e670..07c2e7667 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzyme_constrained.jl @@ -55,7 +55,7 @@ function enzyme_constrained_flux_balance_analysis( optimizer, settings = [], ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) # might be nice to omit some conditionally (e.g. slash the direction if one # kcat is nothing) diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index e4853eda2..3b3918048 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -42,7 +42,7 @@ function loopless_flux_balance_analysis( optimizer, ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) rxns = A.reactions(model) stoi = A.stoichiometry(model) diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 5a148efc2..9d4eaf2a6 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -33,7 +33,7 @@ very good fluxes), [`parsimonious_flux_balance_analysis`](@ref) or [`linear_parsimonious_flux_balance_analysis`](@ref) (computationally simplest but the consistency is not guaranteed). -Internally, [`fbc_log_concentration_constraints`](@ref) is used to lay out the +Internally, [`log_concentration_constraints`](@ref) is used to lay out the base structure of the problem. Following arguments are set optionally: @@ -153,7 +153,7 @@ function max_min_driving_force_analysis( ) constraints = - fbc_log_concentration_constraints( + log_concentration_constraints( model, concentration_bound = met -> if met in no_concentration_metabolites C.EqualTo(0) diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index 6c43a891d..b596426ab 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -39,7 +39,7 @@ function minimization_of_metabolic_adjustment( optimizer; kwargs..., ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) objective = squared_sum_error_value(constraints.fluxes, reference_fluxes) optimized_constraints( constraints * :minimal_adjustment_objective^C.Constraint(objective); @@ -72,7 +72,7 @@ function minimization_of_metabolic_adjustment( reference_settings = settings, kwargs..., ) - reference_constraints = fbc_flux_balance_constraints(reference_model) + reference_constraints = flux_balance_constraints(reference_model) reference_fluxes = optimized_constraints( reference_constraints; optimizer = reference_optimizer, @@ -106,7 +106,7 @@ function linear_minimization_of_metabolic_adjustment( optimizer; kwargs..., ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) difference = C.zip(ct.fluxes, C.Tree(reference_fluxes)) do orig, ref C.Constraint(orig.value - ref) @@ -148,7 +148,7 @@ function linear_minimization_of_metabolic_adjustment( reference_settings = settings, kwargs..., ) - reference_constraints = fbc_flux_balance_constraints(reference_model) + reference_constraints = flux_balance_constraints(reference_model) reference_fluxes = optimized_constraints( reference_constraints; optimizer = reference_optimizer, diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index 9051723bd..b8c2d4c61 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -96,7 +96,7 @@ with some (objectives) filled in automatically to fit the common processing of FBC models, and some (`tolerances`) provided with more practical defaults. Similarly to the [`flux_balance_analysis`](@ref), returns a tree with the optimization -solutions of the shape as given by [`fbc_flux_balance_constraints`](@ref). +solutions of the shape as given by [`flux_balance_constraints`](@ref). """ function parsimonious_flux_balance_analysis( model::A.AbstractFBCModel, @@ -104,7 +104,7 @@ function parsimonious_flux_balance_analysis( tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), kwargs..., ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) parsimonious_objective = squared_sum_value(constraints.fluxes) parsimonious_optimized_constraints( constraints * :parsimonious_objective^C.Constraint(parsimonious_objective); @@ -136,7 +136,7 @@ function linear_parsimonious_flux_balance_analysis( tolerances = relative_tolerance_bound.(1 .- [0, 1e-6, 1e-5, 1e-4, 1e-3, 1e-2]), kwargs..., ) - constraints = fbc_flux_balance_constraints(model) + constraints = flux_balance_constraints(model) constraints = constraints + :fluxes_forward^unsigned_positive_contribution_variables(ct.fluxes) + From ba8c967078467cee3c139343b5eb58ea0c0c418e Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 12 Jan 2024 17:03:59 +0100 Subject: [PATCH 487/531] fixes --- src/builders/communities.jl | 3 +-- src/frontend/loopless.jl | 14 ++++++-------- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 0d3b76b20..0ba8af64d 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -24,8 +24,7 @@ fbc_boundary_constraints( constraints::C.ConstraintTree; ignore = _ -> false, bound = _ -> nothing, -) -network_boundary_constraints(c.fluxes.c.flux_stoichiometry; ...) +) = network_boundary_constraints(c.fluxes.c.flux_stoichiometry; ignore, bound) """ $(TYPEDSIGNATURES) diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index 3b3918048..fb0ddeb0a 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -47,14 +47,12 @@ function loopless_flux_balance_analysis( rxns = A.reactions(model) stoi = A.stoichiometry(model) internal_mask = count(stoi .!= 0; dims = 1)[begin, :] .> 1 - internal_reactions = - Symbol.(rxns[reactions_internal]), constraints = - constraints + - :loopless_directions^C.variables( - keys = internal_reactions, - bounds = Switch(0, 1), - ) + - :loopless_driving_forces^C.variables(keys = internal_reactions) + internal_reactions = Symbol.(rxns[reactions_internal]) + + constraints = + constraints + + :loopless_directions^C.variables(keys = internal_reactions, bounds = Switch(0, 1)) + + :loopless_driving_forces^C.variables(keys = internal_reactions) constraints *= :loopless_constraints^loopless_constraints(; From 27aa282ef254fc57a3156699025e1ae2a5d1c24b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 15 Jan 2024 11:42:29 +0100 Subject: [PATCH 488/531] add interface functionality directly to the model builder --- src/COBREXA.jl | 2 + src/builders/communities.jl | 83 +++++++++++-------------------------- src/builders/fbc.jl | 53 +++++++++++++++++++++-- src/builders/scale.jl | 41 ++++++++++++++++++ src/config.jl | 80 +++++++++++++++++++++++++++++++++++ 5 files changed, 197 insertions(+), 62 deletions(-) create mode 100644 src/builders/scale.jl create mode 100644 src/config.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index bc6f4e7a7..a0548e67a 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -44,6 +44,7 @@ import LinearAlgebra import SparseArrays include("types.jl") +include("config.jl") # core functionality include("io.jl") @@ -57,6 +58,7 @@ include("builders/knockouts.jl") include("builders/loopless.jl") include("builders/mmdf.jl") include("builders/objectives.jl") +include("builders/scale.jl") include("builders/unsigned.jl") # simplified front-ends for the above diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 0ba8af64d..18adc8f4a 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -17,59 +17,20 @@ """ $(TYPEDSIGNATURES) -Create an "interface" block for constraints created by -[`flux_balance_constraints`](@ref) (or any compatible constraint tree). -""" -fbc_boundary_constraints( - constraints::C.ConstraintTree; - ignore = _ -> false, - bound = _ -> nothing, -) = network_boundary_constraints(c.fluxes.c.flux_stoichiometry; ignore, bound) - -""" -$(TYPEDSIGNATURES) - -Create an "interface" block for a constrained reaction network described by -variables in `fluxes` and flux balance in `balances`. Boundary reactions are -assumed to be the ones that either "only create" or "only remove" the balanced -components (metabolites), i.e, each dot product of the reaction description -with all balance components is either strictly non-negative or non-positive. -""" -function network_boundary_constraints( - reactions, - balances; - ignore = _ -> false, - bound = _ -> nothing, -) - #TODO -end - -""" -$(TYPEDSIGNATURES) - -Linearly scale all bounds in a constraint tree by the `factor`. -""" -function scale_bounds(tree::C.ConstraintTree, factor) - C.map(tree) do c - isnothing(c.bound) ? c : C.Constraint(value = c.value, bound = factor * c.bound) - end -end - -""" -$(TYPEDSIGNATURES) - -Join multiple modules into a bigger module. +Join multiple constraint tree modules with interfaces into a bigger module with +an interface. Modules are like usual constraint trees, but contain an explicitly declared -`interface` part, marked properly a tuple (the parameters should form a -dictionary constructor that would generally look such as `:module_name => -(module, module.interface)`; the second tuple member may also be specified just -by name as e.g. `:interface`, or omitted while relying on `default_interface`). +`interface` part, marked properly in arguments using e.g. a tuple (the +parameters should form a dictionary constructor that would generally look such +as `:module_name => (module, module.interface)`; the second tuple member may +also be specified just by name as e.g. `:interface`, or omitted while relying +on `default_interface`). -Interface parts get merged and constrained to create a new interface; networks -are copied intact. +Compatible modules with interfaces may be created e.g. by +[`flux_balance_constraints`](@ref). """ -function join_modules( +function join_module_constraints( ps::Pair...; default_in_interface = :interface, out_interface = :interface, @@ -85,23 +46,27 @@ function join_modules( prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,C.ConstraintTree}) = (id^(:network^mod * :interface^interface)) - # collect everything into one huge network (while also renumbering the interfaces) + # first, collect everything into one huge network + # (while also renumbering the interfaces) modules = sum(prep.(ps)) - # extract a union of all non-ignored interface keys - interface_keys = - filter(!ignore, collect(union((keys(m.interface) for (_, m) in modules)...))) + # fold a union of all non-ignored interface keys + interface_sum = foldl(modules, init = C.ConstraintTree()) do accs, (_, ms) + C.imerge(accs, ms) do path, acc, m + ignore(path) ? missing : + ismissing(acc) ? C.Constraint(value = m.value) : + C.Constraint(value = acc.value + m.value) + end + end - # remove the interface placeholders and add variables for the new interface + # extract the plain networks and add variables for the new interfaces constraints = ConstraintTree(id => (m.network) for (id, m) in modules) + - out_interface^C.variables(keys = interface_keys, bounds = bound.(interface_keys)) + out_interface^C.variables_ifor(bound, interface_sum) # join everything with the interrace balance and return - constraints * C.map(constraints.out_interface) do ic - C.Constraint( - value = sum(c.value for mod in modules for (k, c) in mod.interface) - ic.value, - ) + constraints * C.zip(interface_sum, constraints.out_interface) do sum, out + C.Constraint(value = sum.value - out.value) end end diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 69e453266..8c2dfcd36 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -24,16 +24,36 @@ The constructed tree contains subtrees `fluxes` (with the reaction-defining "variables") and `flux_stoichiometry` (with the metabolite-balance-defining constraints), and a single constraint `objective` thad describes the objective function of the model. + +Optionally if `interface` is specified, an "interface block" will be created +within the constraint tree for later use as a "module" in creating bigger +models (such as communities) using [`join_module_constraints`](@ref). The +possible parameter values include: +- `nothing` -- default, no interface is created +- `:sbo` -- the interface gets created from model's SBO annotations) +- `:identifier_prefixes` -- the interface is guesstimated from commonly + occurring adhoc reaction ID prefixes used in contemporary models +- `:boundary` -- the interface is created from all reactions that either only + consume or only produce metabolites + +Output interface name can be set via `interface_name`. + +See [`Configuration`](@ref) for fine-tuning the default interface creation. """ -function flux_balance_constraints(model::A.AbstractFBCModel) - rxns = Symbol.(A.reactions(model)) +function flux_balance_constraints( + model::A.AbstractFBCModel; + interface::Maybe{Symbol} = nothing, + interface_name = :interface, +) + rxn_strings = A.reactions(model) + rxns = Symbol.(rxn_strings) mets = Symbol.(A.metabolites(model)) lbs, ubs = A.bounds(model) stoi = A.stoichiometry(model) bal = A.balance(model) obj = A.objective(model) - return C.ConstraintTree( + constraints = C.ConstraintTree( :fluxes^C.variables(keys = reactions, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( met => C.Constraint( @@ -43,6 +63,33 @@ function flux_balance_constraints(model::A.AbstractFBCModel) ) * :objective^C.Constraint(C.LinearValue(SparseArrays.sparse(obj))), ) + + add_interface(sym, flt) = + any(flt) && ( + constraints *= + interface_name^sym^C.ConstraintTree( + r => constraints.fluxes[r] for r in rxns[flt] + ) + ) + if interface == :sbo + sbod(sbos, rid) = any(in(sbos), get(A.reaction_annotation(model, rid), "sbo", [])) + add_interface(:exchanges, sbod.(Ref(configuration.exchange_sbos), rxn_strings)) + add_interface(:biomass, sbod.(Ref(configuration.biomass_sbos), rxn_strings)) + add_interface( + :atp_maintenance, + sbod.(Ref(configuration.atp_maintenance_sbos), rxn_strings), + ) + add_interface(:demand, sbod.(Ref(configuration.demand_sbos), rxn_strings)) + elseif interface == :identifier_prefixes + prefixed(ps, s) = any(p -> hasprefix(p, s), ps) + add_interface(:exchanges, prefixed.(Ref(configuration.exchange_id_prefixes), s)) + add_interface(:biomass, prefixed.(Ref(configuration.biomass_id_prefixes), s)) + add_interface(:atp_maintenance, in.(s, Ref(configuration.atp_maintenance_ids))) + elseif interface == :boundary + add_interface(:boundary, ((all(s .<= 0) || all(s .>= 0)) for s in eachcol(stoi))) + end + + return constraints end export flux_balance_constraints diff --git a/src/builders/scale.jl b/src/builders/scale.jl new file mode 100644 index 000000000..33fd446ba --- /dev/null +++ b/src/builders/scale.jl @@ -0,0 +1,41 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Linearly scale all bounds in a constraint tree by the `factor`. This actually +changes the model (and may not work in surprising/improper ways with some +constraint systems, esp. the MILP and QP ones). + +See also [`scale_constraints`](@ref). +""" +scale_bounds(tree::C.ConstraintTree, factor) = + C.map(tree) do c + isnothing(c.bound) ? c : C.Constraint(value = c.value, bound = factor * c.bound) + end + +""" +$(TYPEDSIGNATURES) + +Linearly scale all constraints in a constraint tree by the `factor`. + +See also [`scale_bounds`](@ref). +""" +scale_constraints(tree::C.ConstraintTree, factor) = + C.map(tree) do c + c * factor + end diff --git a/src/config.jl b/src/config.jl new file mode 100644 index 000000000..36302cd90 --- /dev/null +++ b/src/config.jl @@ -0,0 +1,80 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +Global configuration options for various COBREXA functions, mainly for various +non-interesting function parameters that are too inconvenient to be passed +around manually. + +Changing the configuration values at runtime is possible via the global +[`configuration`](@ref) variable. +""" +Base.@kwdef mutable struct Configuration + + """ + Prefixes that [`flux_balance_constraints`](@ref) uses for guessing + which reactions are exchanges. + """ + exchange_id_prefixes::Vector{String} = ["EX_", "R_EX_"] + + """ + Prefixes that [`flux_balance_constraints`](@ref) uses for guessing + which reactions are biomass reactions. + """ + biomass_id_prefixes::Vector{String} = ["BIOMASS_", "R_BIOMASS_"] + + """ + Reaction identifiers that [`flux_balance_constraints`](@ref) considers to + be ATP maintenance reactions. + """ + atp_maintenance_ids::Vector{String} = ["ATPM", "R_ATPM"] + + """ + SBO numbers that label exchange reactions for + [`flux_balance_constraints`](@ref). + """ + exchange_sbos::Vector{Int} = ["SBO:0000627"] + + """ + SBO numbers that label biomass production reactions for + [`flux_balance_constraints`](@ref). + """ + biomass_sbos::Vector{Int} = ["SBO:0000629"] + + """ + SBO numbers that label ATP maintenance reactions for + [`flux_balance_constraints`](@ref). + """ + atp_maintenance_sbos::Vector{Int} = ["SBO:0000630"] + + """ + SBO numbers that label metabolite demand reactions for + [`flux_balance_constraints`](@ref). + """ + demand_sbos::Vector{Int} = ["SBO:0000628"] +end + +""" + const configuration + +The configuration object. You can change the contents of configuration to +override the default behavior of some of the functions. + +The available options are described by struct [`Configuration`](@ref). +""" +const configuration = Configuration() From fa907f8a4f7cad6a1a227db3ed24ca98fa8193f9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Tue, 16 Jan 2024 16:32:55 +0100 Subject: [PATCH 489/531] bump deps, add a few function headers --- Project.toml | 2 +- src/COBREXA.jl | 17 ++++++-- src/analysis/envelope.jl | 24 +++++++++++ src/analysis/sample.jl | 38 ++++++++++++++++ src/analysis/screen.jl | 43 +++++++++++++++++++ src/analysis/variability.jl | 23 ++++++++++ src/builders/communities.jl | 5 ++- src/frontend/community.jl | 25 +++++++++++ src/frontend/envelope.jl | 24 +++++++++++ .../{enzyme_constrained.jl => enzymes.jl} | 3 +- src/frontend/knockout.jl | 19 ++++++++ src/frontend/sample.jl | 23 ++++++++++ src/frontend/variability.jl | 19 ++++++++ 13 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 src/analysis/envelope.jl create mode 100644 src/analysis/sample.jl create mode 100644 src/analysis/screen.jl create mode 100644 src/analysis/variability.jl create mode 100644 src/frontend/community.jl create mode 100644 src/frontend/envelope.jl rename src/frontend/{enzyme_constrained.jl => enzymes.jl} (98%) create mode 100644 src/frontend/knockout.jl create mode 100644 src/frontend/sample.jl create mode 100644 src/frontend/variability.jl diff --git a/Project.toml b/Project.toml index 5b6a17c23..70cbda6d9 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.9.1" +ConstraintTrees = "0.9.2" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" diff --git a/src/COBREXA.jl b/src/COBREXA.jl index a0548e67a..a518ff85f 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -39,6 +39,7 @@ using DocStringExtensions import AbstractFBCModels as A import ConstraintTrees as C +import Distributed as D import JuMP as J import LinearAlgebra import SparseArrays @@ -50,6 +51,12 @@ include("config.jl") include("io.jl") include("solver.jl") +# generic analysis functions +include("analysis/envelope.jl") +include("analysis/sample.jl") +include("analysis/screen.jl") +include("analysis/variability.jl") + # conversion of various stuff to constraint trees include("builders/communities.jl") include("builders/enzymes.jl") @@ -63,11 +70,15 @@ include("builders/unsigned.jl") # simplified front-ends for the above include("frontend/balance.jl") -include("frontend/parsimonious.jl") +include("frontend/envelope.jl") +include("frontend/enzymes.jl") +include("frontend/knockout.jl") +include("frontend/loopless.jl") include("frontend/mmdf.jl") include("frontend/moma.jl") -include("frontend/loopless.jl") -include("frontend/enzyme_constrained.jl") +include("frontend/parsimonious.jl") +include("frontend/sample.jl") +include("frontend/variability.jl") # utilities include("misc/bounds.jl") diff --git a/src/analysis/envelope.jl b/src/analysis/envelope.jl new file mode 100644 index 000000000..919e7c639 --- /dev/null +++ b/src/analysis/envelope.jl @@ -0,0 +1,24 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function constraints_objective_envelope( + constraints::C.ConstraintTree, + grid::Array{C.ConstraintTreeElem}, + objective::C.Value; + workers = D.workers(), +) + #TODO +end diff --git a/src/analysis/sample.jl b/src/analysis/sample.jl new file mode 100644 index 000000000..c12a3d971 --- /dev/null +++ b/src/analysis/sample.jl @@ -0,0 +1,38 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function sample_achr( + constraints::C.ConstraintTree, + points::Matrix{Float64}; + seed, + chains, + collect_iterations, + epsilon, + filter_constraints::C.ConstraintTree, + workers = D.workers(), +) end + +function sample_affine_hr( + constraints::C.ConstraintTree, + points::Matrix{Float64}; + seed, + chains, + collect_iterations, + epsilon, + mix_points, + filter_constraints::C.ConstraintTree, + workers = D.workers(), +) end diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl new file mode 100644 index 000000000..3099ed5fa --- /dev/null +++ b/src/analysis/screen.jl @@ -0,0 +1,43 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function screen( + f, + model::A.AbstractFBCModel; + args::Array{Tuple} = [()], + workers = D.workers(), + kwargs..., +) + # TODO might belong to the frontend +end + +function screen( + f, + constraints::C.ConstraintTree; + args::Array{Tuple} = [()], + workers = D.workers(), +) + # TODO +end + +function screen_optimization_model( + f, + constraints::C.ConstraintTree, + args::Maybe{Array}, + workers = D.workers(), +) + # TODO +end diff --git a/src/analysis/variability.jl b/src/analysis/variability.jl new file mode 100644 index 000000000..16ee0f50e --- /dev/null +++ b/src/analysis/variability.jl @@ -0,0 +1,23 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function constraints_variability( + constraints::C.ConstraintTree, + targets::C.ConstraintTree; + workers = D.workers(), +)::C.Tree{Tuple{Maybe{Float64},Maybe{Float64}}} + #TODO +end diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 18adc8f4a..5280b6bb7 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -27,7 +27,10 @@ as `:module_name => (module, module.interface)`; the second tuple member may also be specified just by name as e.g. `:interface`, or omitted while relying on `default_interface`). -Compatible modules with interfaces may be created e.g. by +Interface parts get merged and constrained to create a new interface; networks +are intact with disjoint variable sets. + +Compatible modules with ready-made interfaces may be created e.g. by [`flux_balance_constraints`](@ref). """ function join_module_constraints( diff --git a/src/frontend/community.jl b/src/frontend/community.jl new file mode 100644 index 000000000..c07dd4ab2 --- /dev/null +++ b/src/frontend/community.jl @@ -0,0 +1,25 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function community_flux_balance_analysis( + model_abundances::Vector{Tuple{A.AbstractFBCModel,Float64}}, + optimizer; + kwargs..., +) + # TODO f this gets complicated, make a specialized community_constraints + # builder or so. But ideally this is just module loading + 1 big join + + # optimizer run. +end diff --git a/src/frontend/envelope.jl b/src/frontend/envelope.jl new file mode 100644 index 000000000..9c785c805 --- /dev/null +++ b/src/frontend/envelope.jl @@ -0,0 +1,24 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function objective_production_envelope( + model::A.AbstractFBCModel, + dimensions; + optimizer, + kwargs..., +) + #TODO +end diff --git a/src/frontend/enzyme_constrained.jl b/src/frontend/enzymes.jl similarity index 98% rename from src/frontend/enzyme_constrained.jl rename to src/frontend/enzymes.jl index 07c2e7667..14bd8896e 100644 --- a/src/frontend/enzyme_constrained.jl +++ b/src/frontend/enzymes.jl @@ -18,7 +18,8 @@ $(TYPEDEF) A simple struct storing information about the isozyme composition, including -subunit stoichiometry and turnover numbers. +subunit stoichiometry and turnover numbers. Use with +[`enzyme_constrained_flux_balance_analysis`](@ref). # Fields $(TYPEDFIELDS) diff --git a/src/frontend/knockout.jl b/src/frontend/knockout.jl new file mode 100644 index 000000000..f03f13ff5 --- /dev/null +++ b/src/frontend/knockout.jl @@ -0,0 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function gene_knockouts(model::A.AbstractFBCModel, genes, n; kwargs...) + # TODO just use screen to do this right +end diff --git a/src/frontend/sample.jl b/src/frontend/sample.jl new file mode 100644 index 000000000..b97f0f438 --- /dev/null +++ b/src/frontend/sample.jl @@ -0,0 +1,23 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function flux_sampling_achr(model::A.AbstractFBCModel; optimizer, kwargs...) + #TODO +end + +function flux_sampling_affine_hr(model::A.AbstractFBCModel; optimizer, kwargs...) + #TODO probably share a lot of the frontend with the above thing +end diff --git a/src/frontend/variability.jl b/src/frontend/variability.jl new file mode 100644 index 000000000..3fe625f27 --- /dev/null +++ b/src/frontend/variability.jl @@ -0,0 +1,19 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +function flux_variability_analysis(model::A.AbstractFBCModel; optimizer, kwargs...) + #TODO +end From 4b7a34166c437ab5f43beaf7b88735341888e6a3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 15:32:28 +0100 Subject: [PATCH 490/531] whew we did! --- src/misc/settings.jl | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/misc/settings.jl b/src/misc/settings.jl index 9c87034e9..1418f27e7 100644 --- a/src/misc/settings.jl +++ b/src/misc/settings.jl @@ -14,8 +14,6 @@ # See the License for the specific language governing permissions and # limitations under the License. -#TODO: at this point, consider renaming the whole thing to "settings" - """ $(TYPEDSIGNATURES) From ec6386a421eea33dc3473ab90a5c49f35eac1f01 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 15:32:35 +0100 Subject: [PATCH 491/531] doc bounds --- src/misc/bounds.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/misc/bounds.jl b/src/misc/bounds.jl index fc47ce39e..b809a4bf4 100644 --- a/src/misc/bounds.jl +++ b/src/misc/bounds.jl @@ -17,7 +17,8 @@ """ $(TYPEDSIGNATURES) -TODO +Make a function that returns absolute tolerance bounds, i.e. `value - +tolerance` and `value + tolerance` in a tuple, in the increasing order. """ absolute_tolerance_bound(tolerance) = x -> begin bound = (x - tolerance, x + tolerance) @@ -27,7 +28,8 @@ end """ $(TYPEDSIGNATURES) -TODO +Make a function that returns relative tolerance bounds, i.e. `value / +tolerance` and `value * tolerance` in a tuple, in the increasing order. """ relative_tolerance_bound(tolerance) = x -> begin bound = (x * tolerance, x / tolerance) From b880837897610ad7082dd4427db29f6965379fcb Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:31:45 +0100 Subject: [PATCH 492/531] add comparison-constriant helpers --- src/COBREXA.jl | 1 + src/builders/compare.jl | 56 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 src/builders/compare.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index a518ff85f..9ee836adf 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -59,6 +59,7 @@ include("analysis/variability.jl") # conversion of various stuff to constraint trees include("builders/communities.jl") +include("builders/compare.jl") include("builders/enzymes.jl") include("builders/fbc.jl") include("builders/knockouts.jl") diff --git a/src/builders/compare.jl b/src/builders/compare.jl new file mode 100644 index 000000000..eadae64b9 --- /dev/null +++ b/src/builders/compare.jl @@ -0,0 +1,56 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +TODO +""" +difference_constraint(a, b, distance_bound) = + C.Constraint(C.value(b) - C.value(a), distance) + +""" +$(TYPEDSIGNATURES) + +TODO +""" +same_value_constraint(a, b) = C.Constraint(a - b, 0) + +""" +$(TYPEDSIGNATURES) + +TODO +""" +all_same_constraints(a, bs::C.ConstraintTree) = + C.map(bs) do b + same_value_constraint(a, b) + end + +""" +$(TYPEDSIGNATURES) + +TODO +""" +greater_or_equal_constraint(a, b) = C.Constraint(C.value(a) - C.value(b), C.Between(0, Inf)) + +""" +$(TYPEDSIGNATURES) + +TODO +""" +less_or_equal_constraint(a, b) = C.Constraint(C.value(b) - C.value(a), C.Between(0, Inf)) + +# TODO try to use the helper functions everywhere From 3aeae494a7933dce7460908bb54fedbc9a231711 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:34:08 +0100 Subject: [PATCH 493/531] interjoining cleanup --- src/builders/communities.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/builders/communities.jl b/src/builders/communities.jl index 5280b6bb7..7986631c4 100644 --- a/src/builders/communities.jl +++ b/src/builders/communities.jl @@ -42,6 +42,8 @@ function join_module_constraints( bound = _ -> nothing, ) + #TODO find a better name. Also the file name could be better. + prep(id::String, x) = prep(Symbol(id), x) prep(id::Symbol, mod::C.ConstraintTree) = prep(id, (mod, default_interface)) prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = @@ -51,7 +53,7 @@ function join_module_constraints( # first, collect everything into one huge network # (while also renumbering the interfaces) - modules = sum(prep.(ps)) + modules = sum(prep.(ps); init = C.ConstraintTree()) # fold a union of all non-ignored interface keys interface_sum = foldl(modules, init = C.ConstraintTree()) do accs, (_, ms) @@ -80,6 +82,8 @@ Overload of `join_module_constraints` for general key-value containers. """ join_module_constraints(kv) = join_module_constraints(kv...) +export join_module_constraints + # TODO equal_growth must be preserved, should be extended to ratios (using an extra variable) # TODO same for exchanges (ratio_bounds?) # TODO probably similar to the distance bounds from MMDF -- unify! From feba842817ca7176082d1d676266b779de15ba26 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:34:46 +0100 Subject: [PATCH 494/531] document and cleanup a few things --- src/builders/fbc.jl | 13 ++++++++++++- src/builders/objectives.jl | 21 ++++++++++++++++----- src/builders/scale.jl | 4 ++-- src/frontend/mmdf.jl | 11 ++++------- src/frontend/moma.jl | 3 ++- 5 files changed, 36 insertions(+), 16 deletions(-) diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 8c2dfcd36..1777e0148 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -97,7 +97,18 @@ export flux_balance_constraints """ $(TYPEDSIGNATURES) -TODO +Build log-concentration-stoichiometry constraints for the `model`, as used e.g. +by [`max_min_driving_force_analysis`](@ref). + +The output constraint tree contains a log-concentration variable for each +metabolite in subtree `log_concentrations`. Individual reactions' total +reactant log concentrations (i.e., all log concentrations of actual reactants +minus all log concentrations of products) have their own variables in +`reactant_log_concentrations`. The values are connected by +`log_concentration_stoichiometry`. + +Function `concentration_bound` may return a bound for the log-concentration of +a given metabolite (compatible with `ConstraintTrees.Bound`), or `nothing`. """ function log_concentration_constraints( model::A.AbstractFBCModel; diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 6861e56d1..6bac3ee63 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -17,14 +17,16 @@ """ $(TYPEDSIGNATURES) -TODO +Construct a `ConstraintTrees.Value` out of squared sum of all values directly +present in a given constraint tree. """ squared_sum_value(x::C.ConstraintTree) = squared_sum_error_value(x, Dict(keys(x) .=> 0.0)) """ $(TYPEDSIGNATURES) -TODO useful for L1 parsimonious stuff +Construct a `ConstraintTrees.Value` out of a sum of all values directly present +in a given constraint tree. """ function sum_value(x...) res = zero(C.LinearValue) @@ -39,9 +41,18 @@ end """ $(TYPEDSIGNATURES) -TODO +Construct a `ConstraintTrees.Value` out of squared error (in the RMSE-like +squared-error sense) between the values in the constraint tree and the +reference `target`. + +`target` is a function that takes a symbol (key) and returns either a `Float64` +reference value, or `nothing` if the error of given key should not be +considered. """ -squared_sum_error_value(constraints::C.ConstraintTree, target::Dict{Symbol,Float64}) = sum( - (C.squared(C.value(c) - target[k]) for (k, c) in constraints if haskey(target, k)), +squared_sum_error_value(constraints::C.ConstraintTree, target) = sum( + ( + C.squared(C.value(c) - t) for + (t, c) in ((target(k), c) for (k, c) in constraints) if !isnothing(t) + ), init = zero(C.LinearValue), ) diff --git a/src/builders/scale.jl b/src/builders/scale.jl index 33fd446ba..230663a91 100644 --- a/src/builders/scale.jl +++ b/src/builders/scale.jl @@ -18,8 +18,8 @@ $(TYPEDSIGNATURES) Linearly scale all bounds in a constraint tree by the `factor`. This actually -changes the model (and may not work in surprising/improper ways with some -constraint systems, esp. the MILP and QP ones). +changes the model semantics, and may not work in surprising/improper ways with +some constraint systems, esp. the MILP and QP ones. See also [`scale_constraints`](@ref). """ diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 9d4eaf2a6..2d4da6a2e 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -188,15 +188,12 @@ function max_min_driving_force_analysis( constraints * :min_driving_forces^min_driving_forces * :min_driving_force_thresholds^C.map(min_driving_forces) do c - C.Constraint( - value = constraints.max_min_driving_force.value - c.value; - bound = C.Between(0, Inf), - ) + greater_or_equal_constraint(constraints.max_min_driving_force, c) end * :concentration_ratio_constraints^C.ConstraintTree( - Symbol(cid) => C.Constraint( - m.log_metabolite_concentrations[Symbol(m2)].value - - m.log_metabolite_concentrations[Symbol(m1)].value, + Symbol(cid) => difference_constraint( + m.log_metabolite_concentrations[Symbol(m1)], + m.log_metabolite_concentrations[Symbol(m2)], log(ratio), ) for (cid, (m1, m2, ratio)) in concentration_ratios ) diff --git a/src/frontend/moma.jl b/src/frontend/moma.jl index b596426ab..c93860315 100644 --- a/src/frontend/moma.jl +++ b/src/frontend/moma.jl @@ -40,7 +40,8 @@ function minimization_of_metabolic_adjustment( kwargs..., ) constraints = flux_balance_constraints(model) - objective = squared_sum_error_value(constraints.fluxes, reference_fluxes) + objective = + squared_sum_error_value(constraints.fluxes, x -> get(reference_fluxes, x, nothing)) optimized_constraints( constraints * :minimal_adjustment_objective^C.Constraint(objective); optimizer, From 869d51d1201c399b2c94989f0d915c5d482ed8d1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:37:20 +0100 Subject: [PATCH 495/531] use C.value more frequently --- src/builders/compare.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index eadae64b9..b0c4e3f73 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -27,7 +27,7 @@ $(TYPEDSIGNATURES) TODO """ -same_value_constraint(a, b) = C.Constraint(a - b, 0) +same_value_constraint(a, b) = C.Constraint(C.value(a) - C.value(b), 0) """ $(TYPEDSIGNATURES) From b6b6845f11990ce943f21d101a5645d593b1aa10 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:42:56 +0100 Subject: [PATCH 496/531] simplify comparison constraints and add docs --- src/builders/compare.jl | 31 +++++++++++++++++++------------ 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index b0c4e3f73..7666f0b7e 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -17,40 +17,47 @@ """ $(TYPEDSIGNATURES) -TODO +A constraint that makes sure that the difference from `a` to `b` is within the +`difference_bound`. For example, `difference_constraint(-1, 1, difference_bound += 2)` will always be valid. Any type of `ConstraintTree.Bound` can be supplied. """ -difference_constraint(a, b, distance_bound) = - C.Constraint(C.value(b) - C.value(a), distance) +difference_constraint(a, b; difference_bound) = + C.Constraint(C.value(b) - C.value(a), difference_bound) """ $(TYPEDSIGNATURES) -TODO +A constraint that makes sure that the values of `a` and `b` are the same. """ -same_value_constraint(a, b) = C.Constraint(C.value(a) - C.value(b), 0) +same_value_constraint(a, b) = difference_constraint(a, b, 0) """ $(TYPEDSIGNATURES) -TODO +A constriant tree that makes sure that all values in `tree` are the same as the +value of `a`. + +Names in the output `ConstraintTree` match the names in the `tree`. """ -all_same_constraints(a, bs::C.ConstraintTree) = - C.map(bs) do b +all_same_constraints(a, tree::C.ConstraintTree) = + C.map(tree) do b same_value_constraint(a, b) end """ $(TYPEDSIGNATURES) -TODO +A constraint that makes sure that the value of `a` is greater than or equal to +the the value of `b`. """ -greater_or_equal_constraint(a, b) = C.Constraint(C.value(a) - C.value(b), C.Between(0, Inf)) +greater_or_equal_constraint(a, b) = difference_bound(a, b, C.Between(0, Inf)) """ $(TYPEDSIGNATURES) -TODO +A constraint that makes sure that the value of `a` is less than or equal to the +the value of `b`. """ -less_or_equal_constraint(a, b) = C.Constraint(C.value(b) - C.value(a), C.Between(0, Inf)) +less_or_equal_constraint(a, b) = difference_bound(b, a, C.Between(0, Inf)) # TODO try to use the helper functions everywhere From abb81e461cfef4509e25d2c7d7f727c96d1bf807 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 16:53:11 +0100 Subject: [PATCH 497/531] clean up Switch in split-sign constraints, also `equal_value_constraint` --- src/builders/compare.jl | 6 +++--- src/builders/unsigned.jl | 7 +++++-- src/solver.jl | 8 +++++++- 3 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index 7666f0b7e..5705bea04 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -29,7 +29,7 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the values of `a` and `b` are the same. """ -same_value_constraint(a, b) = difference_constraint(a, b, 0) +equal_value_constraint(a, b) = difference_constraint(a, b, 0) """ $(TYPEDSIGNATURES) @@ -39,9 +39,9 @@ value of `a`. Names in the output `ConstraintTree` match the names in the `tree`. """ -all_same_constraints(a, tree::C.ConstraintTree) = +all_equal_constraints(a, tree::C.ConstraintTree) = C.map(tree) do b - same_value_constraint(a, b) + equal_value_constraint(a, b) end """ diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index fb0a46a91..ff796f81e 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -32,7 +32,7 @@ sign_split_constraints(; signed::C.ConstraintTree, ) = C.zip(positive, negative, signed, C.Constraint) do p, n, s - C.Constraint(s.value + n.value - p.value, 0.0) + equal_value_constraint(s.value + n.value, p.value, 0.0) end #TODO the construction needs an example in the docs. @@ -43,7 +43,10 @@ positive_bound_contribution(b::C.Between) = b.lower >= 0 && b.upper >= 0 ? b : b.lower <= 0 && b.upper <= 0 ? C.EqualTo(0) : C.Between(max(0, b.lower), max(0, b.upper)) -# TODO binary doesn't really fit here but it would be great if it could. +positive_bound_contribution(b::C.Switch) = + let upper_bound = max(b.a, b.b) + upper_bound > 0 ? C.Between(0.0, upper_bound) : C.EqualTo(0.0) + end unsigned_positive_contribution_variables(cs::C.ConstraintTree) = C.variables_for(c -> positive_bound_contribution(c.bound), cs) diff --git a/src/solver.jl b/src/solver.jl index ad8032eef..7cb6ea903 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -25,9 +25,15 @@ variable, and the value is constrained to equal `a + boolean_var * (b-a)`. Switches can be offset by adding real numbers, negated, and multiplied and divided by scalar constraints. For optimizing some special cases, multiplying by exact zero returns an equality bound to zero. + +# Fields +$(TYPEDFIELDS) """ -struct Switch <: C.Bound +Base.@kwdef mutable struct Switch + "One choice" a::Float64 + + "The other choice" b::Float64 end From 5380ffdbbad41ecd3186cb0a1aa7b9b7f4a428b9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 18:12:43 +0100 Subject: [PATCH 498/531] name stuff --- src/builders/enzymes.jl | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index 2ec99cf08..c6375c8fd 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -102,10 +102,8 @@ function gene_product_isozyme_constraints( if haskey(res, gp) res[gp].value += i.value * stoi else - res[gp] = C.Constraint( - i.value * stoi - gene_product_amounts[gp].value, - 0.0, - ) + res[gp] = + equal_value_constraint(i.value * stoi, gene_product_amounts[gp]) end end end From 6b64c0d05b7517a84bd8e481a7f160955df8547d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 17 Jan 2024 21:55:22 +0100 Subject: [PATCH 499/531] clean up screening --- src/analysis/screen.jl | 19 ++++--------------- 1 file changed, 4 insertions(+), 15 deletions(-) diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl index 3099ed5fa..61e73631f 100644 --- a/src/analysis/screen.jl +++ b/src/analysis/screen.jl @@ -16,28 +16,17 @@ function screen( f, - model::A.AbstractFBCModel; - args::Array{Tuple} = [()], + args...; workers = D.workers(), - kwargs..., ) - # TODO might belong to the frontend -end - -function screen( - f, - constraints::C.ConstraintTree; - args::Array{Tuple} = [()], - workers = D.workers(), -) - # TODO + D.pmap(f, D.CachingPool(workers), args...) end function screen_optimization_model( f, constraints::C.ConstraintTree, - args::Maybe{Array}, + args..., workers = D.workers(), ) - # TODO + # TODO can we do this via simple CachingPool? end From 30a9ca18b4412c5bad0614bb5e18fd780f00b5e3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 10:58:51 +0100 Subject: [PATCH 500/531] screen functions --- src/COBREXA.jl | 1 + src/analysis/screen.jl | 28 +++++++++++++------ src/solver.jl | 18 +++++++++++- src/worker_data.jl | 63 ++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) create mode 100644 src/worker_data.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 9ee836adf..f42df6962 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -50,6 +50,7 @@ include("config.jl") # core functionality include("io.jl") include("solver.jl") +include("worker_data.jl") # generic analysis functions include("analysis/envelope.jl") diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl index 61e73631f..a276d0688 100644 --- a/src/analysis/screen.jl +++ b/src/analysis/screen.jl @@ -14,19 +14,31 @@ # See the License for the specific language governing permissions and # limitations under the License. -function screen( - f, - args...; - workers = D.workers(), -) - D.pmap(f, D.CachingPool(workers), args...) -end +""" +$(TYPEDSIGNATURES) +TODO +""" +screen(f, args...; workers = D.workers()) = D.pmap(f, D.CachingPool(workers), args...) + +""" +$(TYPEDSIGNATURES) + +TODO also point out there's [`optimized_model`](@ref) +""" function screen_optimization_model( f, constraints::C.ConstraintTree, args..., workers = D.workers(), ) - # TODO can we do this via simple CachingPool? + worker_cache = worker_local_data(constraints) do c + (c, COBREXA.optimization_model(c)) + end + + D.pmap( + (as...) -> f(get_worker_local_data(worker_cache)..., as...), + D.CachingPool(workers), + args..., + ) end diff --git a/src/solver.jl b/src/solver.jl index 7cb6ea903..2d212668a 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -155,8 +155,24 @@ function optimized_constraints( for m in settings m(om) end + + optimized_model(om; output) +end + +export optimized_constraints + +""" +$(TYPEDSIGNATURES) + +Like [`optimized_constraints`](@ref), but works directly with a given JuMP +model `om` without applying any settings or creating the optimization model. + +To run the process manually, you can use [`optimization_model`](@ref) to +convert the constraints into a suitable JuMP optimization model. +""" +function optimized_model(om; output::ConstraintTreeElem) J.optimize!(om) is_solved(om) ? C.substitute_values(output, J.value.(om[:x])) : nothing end -export optimized_constraints +export optimized_model diff --git a/src/worker_data.jl b/src/worker_data.jl new file mode 100644 index 000000000..e17bbc07d --- /dev/null +++ b/src/worker_data.jl @@ -0,0 +1,63 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDEF) + +Helper struct that provides access to local data that are unboxed and cached +directly on distributed workers. + +Use with [`get_worker_local_data`](@ref) and `Distributed.CachingPool`. + +# Fields +$(TYPEDFIELDS) +""" +Base.@kwdef mutable struct worker_local_data + "The data that is transferred to the remote worker" + transfer_data::Any + "The data that is cached on the remote worker" + local_data::Union{Some,Nothing} + """ + The function that converts the transferred data to locally-cached data on + the remote worker + """ + transform::Function + + """ + $(TYPEDSIGNATURES) + + Conveniently create a data pack to cache on the remote workers. `f` + receives a single input (the transfer data `x`) and should produce the + worker-local data. + """ + worker_local_data(f, x) = new(x, nothing, f) +end + +""" +$(TYPEDSIGNATURES) + +"Unwrap" the [`worker_local_data`](@ref) on a remote worker to get the +`local_data` out. If required, executes the `transform` function. + +Local copies of `transfer_data` are forgotten after the function executes. +""" +function get_worker_local_data(x::worker_local_data) + if isnothing(x.local_data) + x.local_data = Some(x.transform(x.transfer_data)) + x.transfer_data = nothing + end + some(x.local_data) +end From f5ec073448699b5d4d84585ff9c87c9a96507a59 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 11:40:00 +0100 Subject: [PATCH 501/531] variability on constraints --- src/analysis/screen.jl | 8 +++++-- src/analysis/variability.jl | 23 ++++++++++++++++++- src/misc/trees.jl | 44 +++++++++++++++++++++++++++++++++++++ 3 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/misc/trees.jl diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl index a276d0688..bd6e4d2b8 100644 --- a/src/analysis/screen.jl +++ b/src/analysis/screen.jl @@ -25,15 +25,19 @@ screen(f, args...; workers = D.workers()) = D.pmap(f, D.CachingPool(workers), ar $(TYPEDSIGNATURES) TODO also point out there's [`optimized_model`](@ref) + """ function screen_optimization_model( f, constraints::C.ConstraintTree, - args..., + args...; + objective::Union{Nothing,C.Value} = nothing, + optimizer, workers = D.workers(), ) + # TODO maybe settings? worker_cache = worker_local_data(constraints) do c - (c, COBREXA.optimization_model(c)) + (c, COBREXA.optimization_model(c; objective, optimizer)) end D.pmap( diff --git a/src/analysis/variability.jl b/src/analysis/variability.jl index 16ee0f50e..9221ed8ef 100644 --- a/src/analysis/variability.jl +++ b/src/analysis/variability.jl @@ -14,10 +14,31 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +$(TYPEDSIGNATURES) + +TODO +""" function constraints_variability( constraints::C.ConstraintTree, targets::C.ConstraintTree; + optimizer, workers = D.workers(), )::C.Tree{Tuple{Maybe{Float64},Maybe{Float64}}} - #TODO + + #TODO settings? + target_array = [dim * dir for dim in tree_deflate(C.value, x), dir in (-1, 1)] + + result_array = screen_optimization_model( + constraints, + target_array; + optimizer, + workers, + ) do om, target + J.@objective(om, Maximal, C.substitute(target, om[:x])) + J.optimize!(om) + if_solved(om) ? J.objective_value(om) : nothing + end + + constraint_tree_reinflate(targets, [(row[1], row[2]) for row in eachrow(result_array)]) end diff --git a/src/misc/trees.jl b/src/misc/trees.jl new file mode 100644 index 000000000..c5ab93db0 --- /dev/null +++ b/src/misc/trees.jl @@ -0,0 +1,44 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +TODO +""" +function tree_deflate(f, x::C.Tree{T})::Vector{T} where {T} + count = C.mapreduce(_ -> 1, +, x, init = 0) + res = Vector{T}(undef, count) + i = 1 + C.traverse(x) do c + res[i] = c + i += 1 + end + res +end + +""" +$(TYPEDSIGNATURES) + +TODO +""" +function tree_reinflate(x::C.Tree, elems::Vector{T})::C.Tree{T} where {T} + i = 0 + C.map(x) do _ + i += 1 + elems[i] + end +end From ce224e42d0f672e68bb956b5c18bd5a2f183f056 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 12:43:33 +0100 Subject: [PATCH 502/531] FVA frontend --- src/frontend/variability.jl | 34 ++++++++++++++++++++++++++++++++-- src/solver.jl | 4 ++-- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/src/frontend/variability.jl b/src/frontend/variability.jl index 3fe625f27..a180d5d5c 100644 --- a/src/frontend/variability.jl +++ b/src/frontend/variability.jl @@ -14,6 +14,36 @@ # See the License for the specific language governing permissions and # limitations under the License. -function flux_variability_analysis(model::A.AbstractFBCModel; optimizer, kwargs...) - #TODO +""" +$(TYPEDSIGNATURES) + +TODO +""" +function flux_variability_analysis( + model::A.AbstractFBCModel; + objective_bound, + optimizer, + settings, + workers = D.workers(), +) + constraints = flux_balance_constraints(model) + + objective = constraints.objective_value + + objective_flux = optimized_constraints( + constraints; + objective = constraints.objective.value, + output = constraints.objective, + optimizer, + settings, + ) + + constraint_variability( + constraints * + :objective_bound^C.Constraint(objective, objective_bound(objective_flux)), + constraints.fluxes; + optimizer, + settings, + workers, + ) end diff --git a/src/solver.jl b/src/solver.jl index 2d212668a..5c8b37f74 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -146,9 +146,9 @@ For a "nice" version for simpler finding of metabolic model optima, use [`flux_balance`](@ref). """ function optimized_constraints( - constraints::C.ConstraintTreeElem; + constraints::C.ConstraintTree; settings = [], - output = constraints, + output::C.ConstraintTreeElem = constraints, kwargs..., ) om = optimization_model(constraints; kwargs...) From f731a61999680457477a2f701695172d2389a14d Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 12:45:08 +0100 Subject: [PATCH 503/531] clean up FVA corners --- src/frontend/variability.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/frontend/variability.jl b/src/frontend/variability.jl index a180d5d5c..53c9f2f32 100644 --- a/src/frontend/variability.jl +++ b/src/frontend/variability.jl @@ -38,6 +38,8 @@ function flux_variability_analysis( settings, ) + isnothing(objective_flux) && return nothing + constraint_variability( constraints * :objective_bound^C.Constraint(objective, objective_bound(objective_flux)), @@ -47,3 +49,5 @@ function flux_variability_analysis( workers, ) end + +export flux_variability_analysis From 3c225a022d2eb827c040aeccd49e38baa8285976 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 13:47:36 +0100 Subject: [PATCH 504/531] envelopes --- src/analysis/envelope.jl | 36 ++++++++++++++++++++++++++++++++---- src/analysis/screen.jl | 8 ++++++-- src/analysis/variability.jl | 6 ++++-- 3 files changed, 42 insertions(+), 8 deletions(-) diff --git a/src/analysis/envelope.jl b/src/analysis/envelope.jl index 919e7c639..cad9b1705 100644 --- a/src/analysis/envelope.jl +++ b/src/analysis/envelope.jl @@ -14,11 +14,39 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +$(TYPEDSIGNATURES) + +TODO +""" function constraints_objective_envelope( - constraints::C.ConstraintTree, - grid::Array{C.ConstraintTreeElem}, - objective::C.Value; + constraints::C.ConstraintTree; + dims...; + objective::C.Value, + optimizer, + settings = [], workers = D.workers(), ) - #TODO + values = first.(dims) + ranges = last.(dims) + + screen_optimization_model( + constraints, + Iterators.product(ranges...); + objective, + optimizer, + settings, + workers, + ) do om, coords + con_refs = [ + begin + J.@constraint(om, con_ref, C.substitute(v, om[:x]) == x) + con_ref + end for (v, x) in zip(values, coords) + ] + J.optimize!(om) + res = is_solved(om) ? J.objective_value(om) : nothing + J.delete.(con_refs) + res + end end diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl index bd6e4d2b8..5f99b6934 100644 --- a/src/analysis/screen.jl +++ b/src/analysis/screen.jl @@ -33,11 +33,15 @@ function screen_optimization_model( args...; objective::Union{Nothing,C.Value} = nothing, optimizer, + settings = [], workers = D.workers(), ) - # TODO maybe settings? worker_cache = worker_local_data(constraints) do c - (c, COBREXA.optimization_model(c; objective, optimizer)) + om = COBREXA.optimization_model(c; objective, optimizer) + for s in settings + s(om) + end + om end D.pmap( diff --git a/src/analysis/variability.jl b/src/analysis/variability.jl index 9221ed8ef..8c7c42d5a 100644 --- a/src/analysis/variability.jl +++ b/src/analysis/variability.jl @@ -23,6 +23,7 @@ function constraints_variability( constraints::C.ConstraintTree, targets::C.ConstraintTree; optimizer, + settings = [], workers = D.workers(), )::C.Tree{Tuple{Maybe{Float64},Maybe{Float64}}} @@ -33,12 +34,13 @@ function constraints_variability( constraints, target_array; optimizer, + settings, workers, ) do om, target J.@objective(om, Maximal, C.substitute(target, om[:x])) J.optimize!(om) - if_solved(om) ? J.objective_value(om) : nothing + is_solved(om) ? J.objective_value(om) : nothing end - constraint_tree_reinflate(targets, [(row[1], row[2]) for row in eachrow(result_array)]) + constraint_tree_reinflate(targets, [tuple(a, b) for (a, b) in eachrow(result_array)]) end From e0689f8deace2457cd616419550de3c1be7eac0f Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 14:12:09 +0100 Subject: [PATCH 505/531] envelopes frontend --- src/frontend/envelope.jl | 51 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 48 insertions(+), 3 deletions(-) diff --git a/src/frontend/envelope.jl b/src/frontend/envelope.jl index 9c785c805..37c92dfb4 100644 --- a/src/frontend/envelope.jl +++ b/src/frontend/envelope.jl @@ -14,11 +14,56 @@ # See the License for the specific language governing permissions and # limitations under the License. +""" +$(TYPEDSIGNATURES) + +TODO +""" function objective_production_envelope( model::A.AbstractFBCModel, - dimensions; + reactions; + breaks = 10, optimizer, - kwargs..., + settings = [], + workers, ) - #TODO + constraints = flux_balance_constraints(model) + rs = Symbol.(reactions) + + envelope_bounds = constraints_variability( + constraints, + (r => constraints.fluxes[r] for r in rs); + optimizer, + settings, + workers, + ) + + #TODO check for nothings in the bounds + + bss = [split_interval(envelope_bounds[r]...; breaks) for r in rs] + + return ( + breaks = reactions .=> bss, + objective_values = constraints_objective_envelope( + constraints, + (constraints.fluxes[r] => bs for (r, bs) in zip(rs, bss))...; + objective = model.objective.value, + optimizer, + settings, + workers, + ), + ) + + # this converts nicely to a dataframe, but I'm not a total fan. + #= + xss = Iterators.product(bss) + @assert length(result) == length(xss) + xss = reshape(xss, tuple(length(xss))) + + return (; + (r => [xs[i] for xs in xss] for (i, r) in enumerate(rs))..., + objective_value_name => reshape(result, tuple(length(result))), + ) + # TODO think about it + =# end From 775bf4e5a15055588bb45c44fb16534889c063f6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 15:00:18 +0100 Subject: [PATCH 506/531] interval b b b breaker --- src/frontend/envelope.jl | 2 +- src/misc/breaks.jl | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 src/misc/breaks.jl diff --git a/src/frontend/envelope.jl b/src/frontend/envelope.jl index 37c92dfb4..12027982f 100644 --- a/src/frontend/envelope.jl +++ b/src/frontend/envelope.jl @@ -40,7 +40,7 @@ function objective_production_envelope( #TODO check for nothings in the bounds - bss = [split_interval(envelope_bounds[r]...; breaks) for r in rs] + bss = [break_interval(envelope_bounds[r]..., breaks) for r in rs] return ( breaks = reactions .=> bss, diff --git a/src/misc/breaks.jl b/src/misc/breaks.jl new file mode 100644 index 000000000..2cf41467b --- /dev/null +++ b/src/misc/breaks.jl @@ -0,0 +1,25 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Break an interval into `breaks` (count) breaks. + +Used for computing breaks in [`objective_production_envelope`](@ref). +""" +break_interval(lower, upper, breaks::Int) = + lower .+ (upper - lower) .* ((1:s) .- 1) ./ max(breaks - 1, 1) From 7f42fec108e3cd056281b69b0137cc1aa30b722b Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Thu, 18 Jan 2024 15:43:13 +0100 Subject: [PATCH 507/531] clean up, write a lot of docs for the analysis functions --- src/COBREXA.jl | 6 ++-- src/analysis/envelope.jl | 17 ++++++++++- src/analysis/screen.jl | 29 ++++++++++++++++--- src/analysis/variability.jl | 5 ++-- src/builders/{communities.jl => interface.jl} | 0 src/frontend/envelope.jl | 20 +++++++++++-- src/frontend/variability.jl | 12 ++++++-- src/misc/trees.jl | 17 ++++++++--- 8 files changed, 87 insertions(+), 19 deletions(-) rename src/builders/{communities.jl => interface.jl} (100%) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index f42df6962..cb33213c7 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -59,10 +59,10 @@ include("analysis/screen.jl") include("analysis/variability.jl") # conversion of various stuff to constraint trees -include("builders/communities.jl") include("builders/compare.jl") include("builders/enzymes.jl") include("builders/fbc.jl") +include("builders/interface.jl") include("builders/knockouts.jl") include("builders/loopless.jl") include("builders/mmdf.jl") @@ -84,7 +84,9 @@ include("frontend/variability.jl") # utilities include("misc/bounds.jl") -include("misc/settings.jl") +include("misc/breaks.jl") include("misc/maybe.jl") +include("misc/settings.jl") +include("misc/trees.jl") end # module COBREXA diff --git a/src/analysis/envelope.jl b/src/analysis/envelope.jl index cad9b1705..8b7da62de 100644 --- a/src/analysis/envelope.jl +++ b/src/analysis/envelope.jl @@ -17,12 +17,26 @@ """ $(TYPEDSIGNATURES) -TODO +Optimize the system given by `constraints` and `objective` with `optimizer` +(with custom `settings`) for all combination of constriants given by `dims`. + +`dims` should be compatible with pairs that assign a sequence of breaks to a +`ConstraintTrees.Value`: For example, `organism.fluxes.PFK => 1:3` will compute +optima of the model with the flux through `PFK` constrained to be equal to 1, 2 +and 3. + +In turn, all `dims` are converted to groups of equality constraints, and the +model is solved for all combinations. Shape of the output matrix corresponds to +`Iterators.product(last.(dims)...)`. + +Operation is parallelized by distribution over `workers`; by default all +`Distributed` workers are used. """ function constraints_objective_envelope( constraints::C.ConstraintTree; dims...; objective::C.Value, + sense = Maximal, optimizer, settings = [], workers = D.workers(), @@ -34,6 +48,7 @@ function constraints_objective_envelope( constraints, Iterators.product(ranges...); objective, + sense, optimizer, settings, workers, diff --git a/src/analysis/screen.jl b/src/analysis/screen.jl index 5f99b6934..ef917cd56 100644 --- a/src/analysis/screen.jl +++ b/src/analysis/screen.jl @@ -17,27 +17,48 @@ """ $(TYPEDSIGNATURES) -TODO +Execute a function with arguments given by `args` on `workers`. + +This is merely a nice shortcut for `Distributed.pmap` running over a +`Distributed.CachingPool` of the given workers. """ screen(f, args...; workers = D.workers()) = D.pmap(f, D.CachingPool(workers), args...) """ $(TYPEDSIGNATURES) -TODO also point out there's [`optimized_model`](@ref) +Execute a function arguments from arrays `args` on `workers`, with a pre-cached +JuMP optimization model created from `constraints`, `objective` and `optimizer` +using [`optimization_model`](@ref). `settings` are applied to the optimization +model before first execution of `f`. + +Since the model is cached and never re-created, this may be faster than just +plain [`screen`](@ref) in many use cases. + +The function `f` is supposed to take `length(args)+1` arguments, the first +argument is the JuMP model, and the other arguments are taken from `args` as +with `Distributed.pmap`. While the model may be modified in place, one should +take care to avoid modifications that change results of subsequent invocations +of `f`, as that almost always results in data races and irreproducible +executions. Ideally, all modifications of the model should be either manually +reverted in the invocation of `f`, or the future invocations of `f` must be +able to overwrite them. +`f` may use [`optimized_model`](@ref) to extract results easily w.r.t. some +given `ConstraintTree`. """ function screen_optimization_model( f, constraints::C.ConstraintTree, args...; objective::Union{Nothing,C.Value} = nothing, + sense = Maximal, optimizer, settings = [], workers = D.workers(), ) worker_cache = worker_local_data(constraints) do c - om = COBREXA.optimization_model(c; objective, optimizer) + om = COBREXA.optimization_model(c; objective, sense, optimizer) for s in settings s(om) end @@ -45,7 +66,7 @@ function screen_optimization_model( end D.pmap( - (as...) -> f(get_worker_local_data(worker_cache)..., as...), + (as...) -> f(get_worker_local_data(worker_cache), as...), D.CachingPool(workers), args..., ) diff --git a/src/analysis/variability.jl b/src/analysis/variability.jl index 8c7c42d5a..932cecbd7 100644 --- a/src/analysis/variability.jl +++ b/src/analysis/variability.jl @@ -17,7 +17,7 @@ """ $(TYPEDSIGNATURES) -TODO +Compute the variability """ function constraints_variability( constraints::C.ConstraintTree, @@ -27,8 +27,7 @@ function constraints_variability( workers = D.workers(), )::C.Tree{Tuple{Maybe{Float64},Maybe{Float64}}} - #TODO settings? - target_array = [dim * dir for dim in tree_deflate(C.value, x), dir in (-1, 1)] + target_array = [dim * dir for dim in tree_deflate(C.value, targets), dir in (-1, 1)] result_array = screen_optimization_model( constraints, diff --git a/src/builders/communities.jl b/src/builders/interface.jl similarity index 100% rename from src/builders/communities.jl rename to src/builders/interface.jl diff --git a/src/frontend/envelope.jl b/src/frontend/envelope.jl index 12027982f..a952a38b9 100644 --- a/src/frontend/envelope.jl +++ b/src/frontend/envelope.jl @@ -17,15 +17,28 @@ """ $(TYPEDSIGNATURES) -TODO +Find the objective production envelope of the `model` in the dimensions given +by `reactions`. + +This runs a variability analysis of constraints to determine an applicable +range for the dimensions, then splits the dimensions to equal-sized breaks (of +count `breaks` for each dimension, i.e. total `breaks ^ length(reactions)` +individual "multidimensional breaks") thus forming a grid, and returns an array +of fluxes through the model objective with the individual reactions fixed to +flux as given by the grid. + +`optimizer` and `settings` are used to construct the optimization models. + +The computation is distributed to the specified `workers`, defaulting to all +available workers. """ function objective_production_envelope( model::A.AbstractFBCModel, - reactions; + reactions::Vector{String}; breaks = 10, optimizer, settings = [], - workers, + workers = D.workers(), ) constraints = flux_balance_constraints(model) rs = Symbol.(reactions) @@ -48,6 +61,7 @@ function objective_production_envelope( constraints, (constraints.fluxes[r] => bs for (r, bs) in zip(rs, bss))...; objective = model.objective.value, + sense = Maximal, optimizer, settings, workers, diff --git a/src/frontend/variability.jl b/src/frontend/variability.jl index 53c9f2f32..0719b67ed 100644 --- a/src/frontend/variability.jl +++ b/src/frontend/variability.jl @@ -17,11 +17,19 @@ """ $(TYPEDSIGNATURES) -TODO +Perform a Flux Variability Analysis (FVA) on the `model`, and return a +dictionary of flux ranges where the model is able to perform optimally. The +optimality tolerance can be specified with objective_bound using e.g. +[`relative_tolerance_bound`](@ref) or [`absolute_tolerance_bound`](@ref); the +default is 99% relative tolerance. + +Parameters `optimizer` and `settings` are used as with +[`optimized_constraints`](@ref). `workers` may be used to enable parallel or +distributed processing; the execution defaults to all available workers. """ function flux_variability_analysis( model::A.AbstractFBCModel; - objective_bound, + objective_bound = relative_tolerance_bound(0.99), optimizer, settings, workers = D.workers(), diff --git a/src/misc/trees.jl b/src/misc/trees.jl index c5ab93db0..b7235743a 100644 --- a/src/misc/trees.jl +++ b/src/misc/trees.jl @@ -14,17 +14,25 @@ # See the License for the specific language governing permissions and # limitations under the License. +#TODO these are likely hot candidates to be moved to CTs + """ $(TYPEDSIGNATURES) -TODO +Extract all elements of a `ConstraintTrees.Tree` in order and return them in a +`Vector` transformed by `f`. If the order is not modified, one can re-insert a +vector of modified elements into the same-shaped tree using +[`tree_reinflate`](@ref). """ function tree_deflate(f, x::C.Tree{T})::Vector{T} where {T} - count = C.mapreduce(_ -> 1, +, x, init = 0) + count = 0 + C.traverse(x) do _ + count += 1 + end res = Vector{T}(undef, count) i = 1 C.traverse(x) do c - res[i] = c + res[i] = f(c) i += 1 end res @@ -33,7 +41,8 @@ end """ $(TYPEDSIGNATURES) -TODO +Insert a `Vector` of elements into the "values" of a `ConstraintTrees.Tree`. +The order of elements is given by [`tree_deflate`](@ref). """ function tree_reinflate(x::C.Tree, elems::Vector{T})::C.Tree{T} where {T} i = 0 From a23782c1ebe8610420c003d39b2665eafa244db8 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 11:55:46 +0100 Subject: [PATCH 508/531] make stuff compile --- src/analysis/envelope.jl | 2 +- src/builders/unsigned.jl | 2 +- src/config.jl | 8 ++++---- src/solver.jl | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/analysis/envelope.jl b/src/analysis/envelope.jl index 8b7da62de..2f1619384 100644 --- a/src/analysis/envelope.jl +++ b/src/analysis/envelope.jl @@ -33,7 +33,7 @@ Operation is parallelized by distribution over `workers`; by default all `Distributed` workers are used. """ function constraints_objective_envelope( - constraints::C.ConstraintTree; + constraints::C.ConstraintTree, dims...; objective::C.Value, sense = Maximal, diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index ff796f81e..ff0a694f3 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -43,7 +43,7 @@ positive_bound_contribution(b::C.Between) = b.lower >= 0 && b.upper >= 0 ? b : b.lower <= 0 && b.upper <= 0 ? C.EqualTo(0) : C.Between(max(0, b.lower), max(0, b.upper)) -positive_bound_contribution(b::C.Switch) = +positive_bound_contribution(b::Switch) = let upper_bound = max(b.a, b.b) upper_bound > 0 ? C.Between(0.0, upper_bound) : C.EqualTo(0.0) end diff --git a/src/config.jl b/src/config.jl index 36302cd90..be024eab8 100644 --- a/src/config.jl +++ b/src/config.jl @@ -48,25 +48,25 @@ Base.@kwdef mutable struct Configuration SBO numbers that label exchange reactions for [`flux_balance_constraints`](@ref). """ - exchange_sbos::Vector{Int} = ["SBO:0000627"] + exchange_sbos::Vector{String} = ["SBO:0000627"] """ SBO numbers that label biomass production reactions for [`flux_balance_constraints`](@ref). """ - biomass_sbos::Vector{Int} = ["SBO:0000629"] + biomass_sbos::Vector{String} = ["SBO:0000629"] """ SBO numbers that label ATP maintenance reactions for [`flux_balance_constraints`](@ref). """ - atp_maintenance_sbos::Vector{Int} = ["SBO:0000630"] + atp_maintenance_sbos::Vector{String} = ["SBO:0000630"] """ SBO numbers that label metabolite demand reactions for [`flux_balance_constraints`](@ref). """ - demand_sbos::Vector{Int} = ["SBO:0000628"] + demand_sbos::Vector{String} = ["SBO:0000628"] end """ diff --git a/src/solver.jl b/src/solver.jl index 5c8b37f74..48fa99776 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -170,7 +170,7 @@ model `om` without applying any settings or creating the optimization model. To run the process manually, you can use [`optimization_model`](@ref) to convert the constraints into a suitable JuMP optimization model. """ -function optimized_model(om; output::ConstraintTreeElem) +function optimized_model(om; output::C.ConstraintTreeElem) J.optimize!(om) is_solved(om) ? C.substitute_values(output, J.value.(om[:x])) : nothing end From 4855a5f5220fdfb6cb90d52d902634dd73f82cb9 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 11:59:35 +0100 Subject: [PATCH 509/531] require proper CTs --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 70cbda6d9..c9997d9ba 100644 --- a/Project.toml +++ b/Project.toml @@ -20,7 +20,7 @@ StableRNGs = "860ef19b-820b-49d6-a774-d7a799459cd3" AbstractFBCModels = "0.2.2" Aqua = "0.7" Clarabel = "0.6" -ConstraintTrees = "0.9.2" +ConstraintTrees = "0.9.4" DistributedData = "0.2" DocStringExtensions = "0.8, 0.9" Downloads = "1" From 635ee0bdfdcca453906379d8226f4c9797cf2f34 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 15:38:23 +0100 Subject: [PATCH 510/531] fix many small errors --- .../examples/03-parsimonious-flux-balance.jl | 4 - ...04-minimization-of-metabolic-adjustment.jl | 7 +- .../examples/05-enzyme-constrained-models.jl | 48 +------- ...{06-thermodynamic-models.jl => 06-mmdf.jl} | 41 +------ docs/src/examples/07-loopless-models.jl | 42 +------ src/builders/compare.jl | 10 +- src/builders/fbc.jl | 8 +- src/builders/loopless.jl | 108 +++++++----------- src/builders/objectives.jl | 2 +- src/builders/unsigned.jl | 2 +- src/frontend/loopless.jl | 11 +- src/frontend/mmdf.jl | 8 +- src/frontend/parsimonious.jl | 2 + src/solver.jl | 2 +- 14 files changed, 86 insertions(+), 209 deletions(-) rename docs/src/examples/{06-thermodynamic-models.jl => 06-mmdf.jl} (80%) diff --git a/docs/src/examples/03-parsimonious-flux-balance.jl b/docs/src/examples/03-parsimonious-flux-balance.jl index 2997e7752..0a7fbb0c9 100644 --- a/docs/src/examples/03-parsimonious-flux-balance.jl +++ b/docs/src/examples/03-parsimonious-flux-balance.jl @@ -41,10 +41,6 @@ model = load_model("e_coli_core.json") # load the model vt = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer; settings = [silence]) -# Or use the piping functionality - -model |> parsimonious_flux_balance_analysis(Clarabel.Optimizer; settings = [silence]) - @test isapprox(vt.objective, 0.87392; atol = TEST_TOLERANCE) #src @test sum(x^2 for x in values(vt.fluxes)) < 15000 #src diff --git a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl index 2d81eb2ac..48165fdb1 100644 --- a/docs/src/examples/04-minimization-of-metabolic-adjustment.jl +++ b/docs/src/examples/04-minimization-of-metabolic-adjustment.jl @@ -33,6 +33,11 @@ import Clarabel # guessing. model = convert(CM.Model, load_model("e_coli_core.json")) -reference_fluxes = parsimonious_flux_balance_analysis(model, Clarabel.Optimizer).fluxes +reference_fluxes = + parsimonious_flux_balance_analysis( + model, + Clarabel.Optimizer, + settings = [silence], + ).fluxes # TODO MOMA from here diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index c0bfc6abb..2178a2c0b 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -120,9 +120,8 @@ ec_solution = enzyme_constrained_flux_balance_analysis( reaction_isozymes, gene_product_molar_masses, capacity = total_enzyme_capacity, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], - #unconstrain_reactions = ["EX_glc__D_e"], optimizer = Tulip.Optimizer, + settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], ) #src these values should be unique (glucose transporter is the only way to get carbon into the system) @@ -130,48 +129,3 @@ ec_solution = enzyme_constrained_flux_balance_analysis( @test isapprox(ec_solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src @test isapprox(ec_solution.fluxes.EX_glc__D_e, -49.92966287110028, atol = 0.1) #src @test isapprox(ec_solution.enzymes.b2417, 0.00011859224858442563, atol = 1e-7) #src - -### Building a model incrementally - -# Sometimes it is necessary to build a more complicated model, perhaps using a -# novel type of constraint. For this, it is useful to build the enzyme -# constrained model incrementally, using the ConstraintTree building blocks. - -import ConstraintTrees as C - -# create basic flux model -m = flux_balance_constraints(model) - -# create enzyme variables -m += :enzymes^gene_product_variables(model) - -# constrain some fluxes... -m.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 0.0) # undo glucose important bound from original model - -# ...And enzymes manually -m.enzymes.b2417.bound = C.Between(0.0, 0.1) # for fun, change the bounds of the protein b2417 - -# attach the enzyme mass balances -m = with_enzyme_constraints( - m, - reaction_isozymes; - fluxes = m.fluxes, # mount enzyme constraints to these fluxes - enzymes = m.enzymes, # enzyme variables -) - -# add capacity limitation -m *= - :total_proteome_bound^enzyme_capacity( - m.enzymes, - gene_molar_masses, - A.genes(model), - total_enzyme_capacity, - ) - -# solve the model -ec_solution = optimized_constraints( - m; - objective = m.objective.value, - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], -) diff --git a/docs/src/examples/06-thermodynamic-models.jl b/docs/src/examples/06-mmdf.jl similarity index 80% rename from docs/src/examples/06-thermodynamic-models.jl rename to docs/src/examples/06-mmdf.jl index 369492bc5..10081632a 100644 --- a/docs/src/examples/06-thermodynamic-models.jl +++ b/docs/src/examples/06-mmdf.jl @@ -110,45 +110,14 @@ mmdf_solution = max_min_driving_force_analysis( "atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13), ), - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - concentration_lb = 1e-6, # M - concentration_ub = 1e-1, # M + proton_metabolites = ["h_c", "h_e"], + water_metabolites = ["h2o_c", "h2o_e"], + concentration_lower_bound = 1e-6, # M + concentration_upper_bound = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol - settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], optimizer = Tulip.Optimizer, + settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], ) @test isapprox(mmdf_solution.max_min_driving_force, 5.78353, atol = TEST_TOLERANCE) #src - -# ## Building the thermodynamic model yourself - -# It is also possible to build the thermodynamic model yourself. This allows you -# to incorporate more complex constraints and gives you more freedom. - -m = build_max_min_driving_force_model( - model, - reaction_standard_gibbs_free_energies; - proton_ids = ["h_c", "h_e"], - water_ids = ["h2o_c", "h2o_e"], - reference_flux, - concentration_lb = 1e-6, # M - concentration_ub = 1e-1, # M - T = 298.15, # Kelvin - R = 8.31446261815324e-3, # kJ/K/mol -) - -m *= - :metabolite_ratio_constraints^log_ratio_constraints( - Dict("atp" => ("atp_c", "adp_c", 10.0), "nadh" => ("nadh_c", "nad_c", 0.13)), - m.log_metabolite_concentrations, - ) - -# solve the model, which is not just a normal constraint tree! -mmdf_solution = optimized_constraints( - m; - objective = m.max_min_driving_force.value, - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], -) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 9b6df2bb5..9fbb8f647 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -48,42 +48,8 @@ sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) @test isapprox(sol.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src -@test all( - v * sol.pseudo_gibbs_free_energy_reaction[k] <= -TEST_TOLERANCE for - (k, v) in sol.fluxes if - haskey(sol.pseudo_gibbs_free_energy_reaction, k) && abs(v) >= 1e-6 +@test all( #src + v * sol.loopless_driving_forces[k] <= -TEST_TOLERANCE for #src + (k, v) in sol.fluxes if #src + haskey(sol.loopless_driving_forces, k) && abs(v) >= 1e-6 #src ) #src - -# ## Building your own loopless model - -# ConstraintTrees allows one to add loopless constraints to any model. To -# illustrate how one would add loopless constraints to an arbitrary model (and -# not use the convenience function), let's build a loopless model from scratch. - -# First, build a normal flux balance model -m = flux_balance_constraints(model) - -# Next, find all internal reactions, and their associated indices for use later -internal_reactions = [ - (i, Symbol(rid)) for - (i, rid) in enumerate(A.reactions(model)) if !is_boundary(model, rid) -] -internal_reaction_ids = last.(internal_reactions) -internal_reaction_idxs = first.(internal_reactions) # order needs to match the internal reaction ids below - -# Construct the stoichiometric nullspace of the internal reactions -import LinearAlgebra: nullspace - -internal_reaction_stoichiometry_nullspace_columns = - eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_reaction_idxs]))) - -# And simply add loopless contraints on the fluxes of the model -m = with_loopless_constraints( - m, - internal_reaction_ids, - internal_reaction_stoichiometry_nullspace_columns; - fluxes = m.fluxes, -) - -# Now the model can be solved as before! -optimized_constraints(m; objective = m.objective.value, optimizer = GLPK.Optimizer) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index 5705bea04..633856ca7 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -18,10 +18,10 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the difference from `a` to `b` is within the -`difference_bound`. For example, `difference_constraint(-1, 1, difference_bound -= 2)` will always be valid. Any type of `ConstraintTree.Bound` can be supplied. +`difference_bound`. For example, `difference_constraint(-1, 1, 2)` will always +be valid. Any type of `ConstraintTree.Bound` can be supplied. """ -difference_constraint(a, b; difference_bound) = +difference_constraint(a, b, difference_bound) = C.Constraint(C.value(b) - C.value(a), difference_bound) """ @@ -50,7 +50,7 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the value of `a` is greater than or equal to the the value of `b`. """ -greater_or_equal_constraint(a, b) = difference_bound(a, b, C.Between(0, Inf)) +greater_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(0, Inf)) """ $(TYPEDSIGNATURES) @@ -58,6 +58,6 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the value of `a` is less than or equal to the the value of `b`. """ -less_or_equal_constraint(a, b) = difference_bound(b, a, C.Between(0, Inf)) +less_or_equal_constraint(a, b) = difference_constraint(b, a, C.Between(0, Inf)) # TODO try to use the helper functions everywhere diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 1777e0148..fe32d947c 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -54,7 +54,7 @@ function flux_balance_constraints( obj = A.objective(model) constraints = C.ConstraintTree( - :fluxes^C.variables(keys = reactions, bounds = zip(lbs, ubs)) * + :fluxes^C.variables(keys = rxns, bounds = zip(lbs, ubs)) * :flux_stoichiometry^C.ConstraintTree( met => C.Constraint( value = C.LinearValue(SparseArrays.sparse(row)), @@ -114,7 +114,7 @@ function log_concentration_constraints( model::A.AbstractFBCModel; concentration_bound = _ -> nothing, ) - rxns = Symbol.(A.reations(model)) + rxns = Symbol.(A.reactions(model)) mets = Symbol.(A.metabolites(model)) stoi = A.stoichiometry(model) @@ -124,9 +124,9 @@ function log_concentration_constraints( cs = C.ConstraintTree() - for (midx, ridx, coeff) in zip(findnz(stoi)...) + for (midx, ridx, coeff) in zip(SparseArrays.findnz(stoi)...) rid = rxns[ridx] - value = constraints.log_concentrations[mets[midx]] * coeff + value = constraints.log_concentrations[mets[midx]].value * coeff if haskey(cs, rid) cs[rid].value += value else diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index ecfb4b3a0..0bd1f4233 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -17,26 +17,9 @@ """ $(TYPEDSIGNATURES) -Add loopless constraints to the model, `m`. Specify the internal reactions with -`internal_reaction_ids`, as well as the columns of the stoichiometric nullspace -of these reactions in `internal_reaction_stoichiometry_nullspace_columns`. See -the example below for more information about the nullspace. By default, the -`fluxes` of the model `m` are made loopless. - -The Big-M method is used to ensure the sign of fluxes and pseudo Gibbs free -energy of reactions match. For this, ensure that `max_flux_bound` is at least -one order of magnitude bigger than the largest expected absolute flux value. -Additionally, ensure that `strict_inequality_tolerance` is smaller than any -expected pseudo Gibbs free energy of reaction value. The defaults work well for -most problems. - -# Example -``` -internal_reaction_stoichiometry_nullspace_columns = - eachcol(nullspace(Array(A.stoichiometry(model)[:, internal_rxn_idxs_in_order_of_internal_rxn_ids]))) -``` +TODO """ -function loopless_constraints(; +loopless_constraints(; fluxes::C.ConstraintTree, loopless_direction_indicators::C.ConstraintTree, loopless_driving_forces::C.ConstraintTree, @@ -45,51 +28,48 @@ function loopless_constraints(; flux_infinity_bound, driving_force_nonzero_bound, driving_force_infinity_bound, +) = C.ConstraintTree( + :flux_direction_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value + + flux_infinity_bound * (1 - loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r in internal_reactions + ), + :flux_direction_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = fluxes[r].value + + flux_infinity_bound * loopless_direction_indicators[r].value, + bound = C.Between(-Inf, 0), + ) for r in internal_reactions + ), + :driving_force_lower_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value - + driving_force_nonzero_bound * loopless_direction_indicators[r].value + + driving_force_infinity_bound * + (1 - loopless_direction_indicators[r].value), + bound = C.Between(0, Inf), + ) for r in internal_reactions + ), + :driving_force_upper_bounds => C.ConstraintTree( + r => C.Constraint( + value = loopless_driving_forces[r].value + + driving_force_nonzero_bound * + (1 - loopless_direction_indicators[r].value) - + driving_force_infinity_bound * loopless_direction_indicators[r].value, + bound = C.Between(-Inf, 0), + ) for r in internal_reactions + ), + :loopless_nullspace => C.ConstraintTree( + Symbol(:nullspace_base_, i) => C.Constraint( + value = sum( + coeff * loopless_driving_forces[r].value for + (coeff, r) in zip(col, internal_reactions) + ), + bound = C.EqualTo(0), + ) for (i, col) in enumerate(eachcol(internal_nullspace)) + ), ) - C.ConstraintTree( - :flux_direction_lower_bounds => C.ConstraintTree( - r => C.Constraint( - value = fluxes[r].value + - flux_infinity_bound * (1 - loopless_direction_indicators[r].value), - bound = C.Between(0, Inf), - ) for r in internal_reactions - ), - :flux_direction_upper_bounds => C.ConstraintTree( - r => C.Constraint( - value = fluxes[r].value + - flux_infinity_bound * loopless_direction_indicators[r].value, - bound = C.Between(-Inf, 0), - ) for r in internal_reactions - ), - :driving_force_lower_bounds => C.ConstraintTree( - r => C.Constraint( - value = loopless_driving_forces[r].value - - strict_inequality_tolerance * - loopless_direction_indicators[r].value + - flux_infinity_bound * (1 - loopless_direction_indicators[r].value), - bound = C.Between(0, Inf), - ) for r in internal_reaction_ids - ), - :driving_force_upper_bounds => C.ConstraintTree( - r => C.Constraint( - value = loopless_driving_forces[r].value + - strict_inequality_tolerance * - (1 - loopless_direction_indicators[r].value) - - flux_infinity_bound * loopless_direction_indicators[r].value, - bound = C.Between(-Inf, 0), - ) for r in internal_reaction_ids - ), - :loopless_nullspace => C.ConstraintTree( - Symbol(:nullspace_base_, i) => C.Constraint( - value = sum( - coeff * loopless_driving_forces[r].value for - (coeff, r) in zip(vec, internal_reactions) - ), - bound = C.EqualTo(0), - ) for (i, col) in enumerate(eachcol(internal_nullspace)) - ), - ) -end - export loopless_constraints diff --git a/src/builders/objectives.jl b/src/builders/objectives.jl index 6bac3ee63..a9568c306 100644 --- a/src/builders/objectives.jl +++ b/src/builders/objectives.jl @@ -20,7 +20,7 @@ $(TYPEDSIGNATURES) Construct a `ConstraintTrees.Value` out of squared sum of all values directly present in a given constraint tree. """ -squared_sum_value(x::C.ConstraintTree) = squared_sum_error_value(x, Dict(keys(x) .=> 0.0)) +squared_sum_value(x::C.ConstraintTree) = squared_sum_error_value(x, _ -> 0.0) """ $(TYPEDSIGNATURES) diff --git a/src/builders/unsigned.jl b/src/builders/unsigned.jl index ff0a694f3..2762b3502 100644 --- a/src/builders/unsigned.jl +++ b/src/builders/unsigned.jl @@ -32,7 +32,7 @@ sign_split_constraints(; signed::C.ConstraintTree, ) = C.zip(positive, negative, signed, C.Constraint) do p, n, s - equal_value_constraint(s.value + n.value, p.value, 0.0) + equal_value_constraint(s.value + n.value, p.value) end #TODO the construction needs an example in the docs. diff --git a/src/frontend/loopless.jl b/src/frontend/loopless.jl index fb0ddeb0a..afdf6817b 100644 --- a/src/frontend/loopless.jl +++ b/src/frontend/loopless.jl @@ -47,7 +47,7 @@ function loopless_flux_balance_analysis( rxns = A.reactions(model) stoi = A.stoichiometry(model) internal_mask = count(stoi .!= 0; dims = 1)[begin, :] .> 1 - internal_reactions = Symbol.(rxns[reactions_internal]) + internal_reactions = Symbol.(rxns[internal_mask]) constraints = constraints + @@ -57,7 +57,7 @@ function loopless_flux_balance_analysis( constraints *= :loopless_constraints^loopless_constraints(; fluxes = constraints.fluxes, - loopless_directions = constraints.loopless_directions, + loopless_direction_indicators = constraints.loopless_directions, loopless_driving_forces = constraints.loopless_driving_forces, internal_reactions, internal_nullspace = LinearAlgebra.nullspace(Matrix(stoi[:, internal_mask])), @@ -66,7 +66,12 @@ function loopless_flux_balance_analysis( driving_force_infinity_bound, ) - optimized_constraints(m; objective = m.objective.value, optimizer, settings) + optimized_constraints( + constraints; + objective = constraints.objective.value, + optimizer, + settings, + ) end export loopless_flux_balance_analysis diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 2d4da6a2e..49951538d 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -60,9 +60,9 @@ function max_min_driving_force_analysis( reference_flux = Dict{String,Float64}(), concentration_ratios = Dict{String,Tuple{String,String,Float64}}(), constant_concentrations = Dict{String,Float64}(), - ignored_metabolites = Set{String}(), - proton_metabolites = Set{String}(), - water_metabolites = Set{String}(), + ignored_metabolites = [], + proton_metabolites = [], + water_metabolites = [], concentration_lower_bound = 1e-9, # M concentration_upper_bound = 1e-1, # M T = 298.15, # Kelvin @@ -170,7 +170,7 @@ function max_min_driving_force_analysis( min_driving_forces = C.ConstraintTree( let r = Symbol(rid) r => C.Constraint( - value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r], + value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r].value, bound = let rf = reference_flux[rid] if isapprox(rf, 0.0, atol = reference_flux_atol) C.EqualTo(0) diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index b8c2d4c61..6db0c9b89 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -40,6 +40,8 @@ function parsimonious_optimized_constraints( kwargs..., ) + # TODO move this to analysis + # first solve the optimization problem with the original objective om = optimization_model(constraints; objective, kwargs...) for m in settings diff --git a/src/solver.jl b/src/solver.jl index 48fa99776..e32cc1a20 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -29,7 +29,7 @@ by exact zero returns an equality bound to zero. # Fields $(TYPEDFIELDS) """ -Base.@kwdef mutable struct Switch +Base.@kwdef mutable struct Switch <: C.Bound "One choice" a::Float64 From 8d8f75ebf26918e6ad6c22667d72707ba5b125ee Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 15:47:45 +0100 Subject: [PATCH 511/531] move the CT-parsimonious function to analysis --- src/COBREXA.jl | 1 + src/analysis/parsimonious.jl | 81 ++++++++++++++++++++++++++++++++++++ src/frontend/parsimonious.jl | 68 ------------------------------ 3 files changed, 82 insertions(+), 68 deletions(-) create mode 100644 src/analysis/parsimonious.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index cb33213c7..fbd0438b9 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -54,6 +54,7 @@ include("worker_data.jl") # generic analysis functions include("analysis/envelope.jl") +include("analysis/parsimonious.jl") include("analysis/sample.jl") include("analysis/screen.jl") include("analysis/variability.jl") diff --git a/src/analysis/parsimonious.jl b/src/analysis/parsimonious.jl new file mode 100644 index 000000000..b8ac0ae4c --- /dev/null +++ b/src/analysis/parsimonious.jl @@ -0,0 +1,81 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Optimize the system of `constraints` to get the optimal `objective` value. Then +try to find a "parsimonious" solution with the same `objective` value, which +optimizes the `parsimonious_objective` (possibly also switching optimization +sense, optimizer, and adding more settings). + +For efficiency, everything is performed on a single instance of JuMP model. + +A simpler version suitable for direct work with metabolic models is available +in [`parsimonious_flux_balance`](@ref). +""" +function parsimonious_optimized_constraints( + constraints::C.ConstraintTreeElem; + objective::C.Value, + settings = [], + parsimonious_objective::C.Value, + parsimonious_optimizer = nothing, + parsimonious_sense = J.MIN_SENSE, + parsimonious_settings = [], + tolerances = [absolute_tolerance_bound(0)], + output = constraints, + kwargs..., +) + + # first solve the optimization problem with the original objective + om = optimization_model(constraints; objective, kwargs...) + for m in settings + m(om) + end + J.optimize!(om) + is_solved(om) || return nothing + + target_objective_value = J.objective_value(om) + + # switch to parsimonizing the solution w.r.t. to the objective value + isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer) + for m in parsimonious_settings + m(om) + end + + J.@objective(om, J.MIN_SENSE, C.substitute(parsimonious_objective, om[:x])) + + # try all admissible tolerances + for tolerance in tolerances + (lb, ub) = tolerance(target_objective_value) + J.@constraint( + om, + pfba_tolerance_constraint, + lb <= C.substitute(objective, om[:x]) <= ub + ) + + J.optimize!(om) + is_solved(om) && return C.substitute_values(output, J.value.(om[:x])) + + J.delete(om, pfba_tolerance_constraint) + J.unregister(om, :pfba_tolerance_constraint) + end + + # all tolerances failed + return nothing +end + +export parsimonious_optimized_constraints diff --git a/src/frontend/parsimonious.jl b/src/frontend/parsimonious.jl index 6db0c9b89..b94597ab5 100644 --- a/src/frontend/parsimonious.jl +++ b/src/frontend/parsimonious.jl @@ -17,74 +17,6 @@ """ $(TYPEDSIGNATURES) -Optimize the system of `constraints` to get the optimal `objective` value. Then -try to find a "parsimonious" solution with the same `objective` value, which -optimizes the `parsimonious_objective` (possibly also switching optimization -sense, optimizer, and adding more settings). - -For efficiency, everything is performed on a single instance of JuMP model. - -A simpler version suitable for direct work with metabolic models is available -in [`parsimonious_flux_balance`](@ref). -""" -function parsimonious_optimized_constraints( - constraints::C.ConstraintTreeElem; - objective::C.Value, - settings = [], - parsimonious_objective::C.Value, - parsimonious_optimizer = nothing, - parsimonious_sense = J.MIN_SENSE, - parsimonious_settings = [], - tolerances = [absolute_tolerance_bound(0)], - output = constraints, - kwargs..., -) - - # TODO move this to analysis - - # first solve the optimization problem with the original objective - om = optimization_model(constraints; objective, kwargs...) - for m in settings - m(om) - end - J.optimize!(om) - is_solved(om) || return nothing - - target_objective_value = J.objective_value(om) - - # switch to parsimonizing the solution w.r.t. to the objective value - isnothing(parsimonious_optimizer) || J.set_optimizer(om, parsimonious_optimizer) - for m in parsimonious_settings - m(om) - end - - J.@objective(om, J.MIN_SENSE, C.substitute(parsimonious_objective, om[:x])) - - # try all admissible tolerances - for tolerance in tolerances - (lb, ub) = tolerance(target_objective_value) - J.@constraint( - om, - pfba_tolerance_constraint, - lb <= C.substitute(objective, om[:x]) <= ub - ) - - J.optimize!(om) - is_solved(om) && return C.substitute_values(output, J.value.(om[:x])) - - J.delete(om, pfba_tolerance_constraint) - J.unregister(om, :pfba_tolerance_constraint) - end - - # all tolerances failed - return nothing -end - -export parsimonious_optimized_constraints - -""" -$(TYPEDSIGNATURES) - Compute a parsimonious flux solution for the given `model`. In short, the objective value of the parsimonious solution should be the same as the one from [`flux_balance_analysis`](@ref), except the squared sum of reaction fluxes is minimized. From 3c86ae5d5d117f749b81c7b9d39a328fac5564d3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 15:47:55 +0100 Subject: [PATCH 512/531] document loopless --- src/builders/loopless.jl | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 0bd1f4233..06c645028 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -17,7 +17,23 @@ """ $(TYPEDSIGNATURES) -TODO +Construct the loopless constraint system that binds `fluxes` of all +`internal_reactions` to direction of `loopless_direction_indicators` and +connects them to `loopless_driving_forces`. The solution is bounded to lie in +`internal_nullspace` (which is a sufficient algebraic condition for +loop-less-ness). + +The simplest (but by no means the fastest) way to obtain a good +`internal_nullspace` is to use `LinearAlgebra.nullspace` with the internal +reactions' stoichiometry matrix. Rows of `internal_nullspace` must correspond +to `internal_reactions`. + +`flux_infinity_bound` is used as the maximal bound for fluxes (for constraints +that connect them to indicator variables); it should optimally be greater than +the maximum possible absolute value of any flux in the original model. + +`driving_force_nonzero_bound` and `driving_force_infinity_bound` are similarly +used to limit the individual reaction's driving forces. """ loopless_constraints(; fluxes::C.ConstraintTree, From 07d043ef1d0bf45fb4696122bfdfab7455b1d239 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Fri, 19 Jan 2024 15:49:00 +0100 Subject: [PATCH 513/531] technically speaking, solving the models is analysis --- src/COBREXA.jl | 1 + src/analysis/solver.jl | 42 ++++++++++++++++++++++++++++++++++++++++++ src/solver.jl | 27 --------------------------- 3 files changed, 43 insertions(+), 27 deletions(-) create mode 100644 src/analysis/solver.jl diff --git a/src/COBREXA.jl b/src/COBREXA.jl index fbd0438b9..7e54f7876 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -57,6 +57,7 @@ include("analysis/envelope.jl") include("analysis/parsimonious.jl") include("analysis/sample.jl") include("analysis/screen.jl") +include("analysis/solver.jl") include("analysis/variability.jl") # conversion of various stuff to constraint trees diff --git a/src/analysis/solver.jl b/src/analysis/solver.jl new file mode 100644 index 000000000..4ca856738 --- /dev/null +++ b/src/analysis/solver.jl @@ -0,0 +1,42 @@ + +# Copyright (c) 2021-2024, University of Luxembourg +# Copyright (c) 2021-2024, Heinrich-Heine University Duesseldorf +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" +$(TYPEDSIGNATURES) + +Make an JuMP model out of `constraints` using [`optimization_model`](@ref) +(most arguments are forwarded there), then apply the `settings`, optimize +the model, and return either `nothing` if the optimization failed, or `output` +substituted with the solved values (`output` defaults to `constraints`. + +For a "nice" version for simpler finding of metabolic model optima, use +[`flux_balance`](@ref). +""" +function optimized_constraints( + constraints::C.ConstraintTree; + settings = [], + output::C.ConstraintTreeElem = constraints, + kwargs..., +) + om = optimization_model(constraints; kwargs...) + for m in settings + m(om) + end + + optimized_model(om; output) +end + +export optimized_constraints diff --git a/src/solver.jl b/src/solver.jl index e32cc1a20..fee6ac07d 100644 --- a/src/solver.jl +++ b/src/solver.jl @@ -137,33 +137,6 @@ export Feasible """ $(TYPEDSIGNATURES) -Make an JuMP model out of `constraints` using [`optimization_model`](@ref) -(most arguments are forwarded there), then apply the `settings`, optimize -the model, and return either `nothing` if the optimization failed, or `output` -substituted with the solved values (`output` defaults to `constraints`. - -For a "nice" version for simpler finding of metabolic model optima, use -[`flux_balance`](@ref). -""" -function optimized_constraints( - constraints::C.ConstraintTree; - settings = [], - output::C.ConstraintTreeElem = constraints, - kwargs..., -) - om = optimization_model(constraints; kwargs...) - for m in settings - m(om) - end - - optimized_model(om; output) -end - -export optimized_constraints - -""" -$(TYPEDSIGNATURES) - Like [`optimized_constraints`](@ref), but works directly with a given JuMP model `om` without applying any settings or creating the optimization model. From 328ec68ca067230e2a35416c94a8268412b98624 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 24 Jan 2024 14:48:10 +0100 Subject: [PATCH 514/531] clean up module interfacing, add abundance scaling --- src/builders/interface.jl | 98 ++++++--------------------------------- 1 file changed, 15 insertions(+), 83 deletions(-) diff --git a/src/builders/interface.jl b/src/builders/interface.jl index 7986631c4..50a54af28 100644 --- a/src/builders/interface.jl +++ b/src/builders/interface.jl @@ -33,7 +33,7 @@ are intact with disjoint variable sets. Compatible modules with ready-made interfaces may be created e.g. by [`flux_balance_constraints`](@ref). """ -function join_module_constraints( +function interface_constraints( ps::Pair...; default_in_interface = :interface, out_interface = :interface, @@ -42,12 +42,14 @@ function join_module_constraints( bound = _ -> nothing, ) - #TODO find a better name. Also the file name could be better. - prep(id::String, x) = prep(Symbol(id), x) prep(id::Symbol, mod::C.ConstraintTree) = prep(id, (mod, default_interface)) + prep(id::Symbol, (mod, multiplier)::Tuple{C.ConstraintTree,<:Real}) = + prep(id, (mod, default_interface, multiplier)) prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = prep(id, mod, mod[interface]) + prep(id::Symbol, (mod, interface, multiplier)::Tuple{C.ConstraintTree,Symbol,<:Real}) = + prep(id, mod, C.map(c -> c * multiplier, mod[interface])) prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,C.ConstraintTree}) = (id^(:network^mod * :interface^interface)) @@ -55,10 +57,14 @@ function join_module_constraints( # (while also renumbering the interfaces) modules = sum(prep.(ps); init = C.ConstraintTree()) + # TODO maybe split the interface creation into a separate function + # (BUT- people shouldn't really need it since they should have all of their + # interfacing stuff in interface subtrees anyway, right?) + # fold a union of all non-ignored interface keys - interface_sum = foldl(modules, init = C.ConstraintTree()) do accs, (_, ms) - C.imerge(accs, ms) do path, acc, m - ignore(path) ? missing : + interface_sum = foldl(modules, init = C.ConstraintTree()) do accs, (id, ms) + C.imerge(accs, ms.interface) do path, acc, m + ignore(id, path) ? missing : ismissing(acc) ? C.Constraint(value = m.value) : C.Constraint(value = acc.value + m.value) end @@ -78,82 +84,8 @@ end """ $(TYPEDSIGNATURES) -Overload of `join_module_constraints` for general key-value containers. -""" -join_module_constraints(kv) = join_module_constraints(kv...) - -export join_module_constraints - -# TODO equal_growth must be preserved, should be extended to ratios (using an extra variable) -# TODO same for exchanges (ratio_bounds?) -# TODO probably similar to the distance bounds from MMDF -- unify! - -""" -$(TYPEDSIGNATURES) - -Helper function to create environmental exchange rections. +Overload of [`interface_constraints`](@ref) for general key-value containers. """ -function environment_exchange_variables(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) - rids = collect(keys(env_ex_rxns)) - lbs_ubs = collect(values(env_ex_rxns)) - C.variables(; keys = Symbol.(rids), bounds = lbs_ubs) -end - -export environment_exchange_variables - -""" -$(TYPEDSIGNATURES) - -Helper function to build a "blank" community model with only environmental exchange reactions. -""" -function build_community_environment(env_ex_rxns = Dict{String,Tuple{Float64,Float64}}()) - C.ConstraintTree( - :environmental_exchange_reactions => environment_exchange_variables(env_ex_rxns), - ) -end - -export build_community_environment - -""" -$(TYPEDSIGNATURES) - -Helper function to link species specific exchange reactions to the environmental -exchange reactions by weighting them with their abundances. -""" -function link_environmental_exchanges( - m::C.ConstraintTree, - member_abundances::Vector{Tuple{Symbol,Float64}}; - on = m.:environmental_exchange_reactions, - member_fluxes_id = :fluxes, -) - C.ConstraintTree( - rid => C.Constraint( - value = -rxn.value + sum( - abundance * m[member][member_fluxes_id][rid].value for - (member, abundance) in member_abundances if - haskey(m[member][member_fluxes_id], rid); - init = zero(C.LinearValue), - ), - bound = 0.0, - ) for (rid, rxn) in on - ) -end - -export link_environmental_exchanges - -""" -$(TYPEDSIGNATURES) - -Helper function to set each species growth rate equal to each other. -""" -function equal_growth_rate_constraints( - member_biomasses::Vector{Tuple{Symbol,C.LinearValue}}, -) - C.ConstraintTree( - Symbol(bid1, :_, bid2) => C.Constraint(value = bval1 - bval2, bound = 0.0) for - ((bid1, bval1), (bid2, bval2)) in - zip(member_biomasses[1:end-1], member_biomasses[2:end]) - ) -end +interface_constraints(kv; kwargs...) = interface_constraints(kv...; kwargs...) -export equal_growth_rate_constraints +export interface_constraints From fb1409a03952d9ee34821f6427b6e8eabab86dda Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 24 Jan 2024 15:27:02 +0100 Subject: [PATCH 515/531] fix a mistake in enzyme builders --- docs/src/examples/05-enzyme-constrained-models.jl | 12 ++++++++---- src/builders/enzymes.jl | 2 +- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 2178a2c0b..a978b7358 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -125,7 +125,11 @@ ec_solution = enzyme_constrained_flux_balance_analysis( ) #src these values should be unique (glucose transporter is the only way to get carbon into the system) -@test isapprox(ec_solution.objective, 1.671357282901553, atol = TEST_TOLERANCE) #src -@test isapprox(ec_solution.total_proteome_bound, 0.1, atol = TEST_TOLERANCE) #src -@test isapprox(ec_solution.fluxes.EX_glc__D_e, -49.92966287110028, atol = 0.1) #src -@test isapprox(ec_solution.enzymes.b2417, 0.00011859224858442563, atol = 1e-7) #src +@test isapprox(ec_solution.objective, 0.706993382849705, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.gene_product_capacity, 50.0, atol = TEST_TOLERANCE) #src +@test isapprox(ec_solution.fluxes.EX_glc__D_e, -10, atol = TEST_TOLERANCE) #src +@test isapprox( #src + ec_solution.gene_product_amounts.b2417, #src + 0.011875920383431717, #src + atol = TEST_TOLERANCE, #src +) #src diff --git a/src/builders/enzymes.jl b/src/builders/enzymes.jl index c6375c8fd..e0c8ed828 100644 --- a/src/builders/enzymes.jl +++ b/src/builders/enzymes.jl @@ -103,7 +103,7 @@ function gene_product_isozyme_constraints( res[gp].value += i.value * stoi else res[gp] = - equal_value_constraint(i.value * stoi, gene_product_amounts[gp]) + C.Constraint(i.value * stoi - gene_product_amounts[gp].value, 0) end end end From f4c3de7ea267dd125dc6b25ebe4a775ce14399ab Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 24 Jan 2024 15:56:40 +0100 Subject: [PATCH 516/531] fixup the FBC interface creation methods --- src/builders/fbc.jl | 28 ++++++++++++++++++++++------ 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index fe32d947c..8572e5749 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -72,7 +72,7 @@ function flux_balance_constraints( ) ) if interface == :sbo - sbod(sbos, rid) = any(in(sbos), get(A.reaction_annotation(model, rid), "sbo", [])) + sbod(sbos, rid) = any(in(sbos), get(A.reaction_annotations(model, rid), "sbo", [])) add_interface(:exchanges, sbod.(Ref(configuration.exchange_sbos), rxn_strings)) add_interface(:biomass, sbod.(Ref(configuration.biomass_sbos), rxn_strings)) add_interface( @@ -81,12 +81,28 @@ function flux_balance_constraints( ) add_interface(:demand, sbod.(Ref(configuration.demand_sbos), rxn_strings)) elseif interface == :identifier_prefixes - prefixed(ps, s) = any(p -> hasprefix(p, s), ps) - add_interface(:exchanges, prefixed.(Ref(configuration.exchange_id_prefixes), s)) - add_interface(:biomass, prefixed.(Ref(configuration.biomass_id_prefixes), s)) - add_interface(:atp_maintenance, in.(s, Ref(configuration.atp_maintenance_ids))) + prefixed(ps, s) = any(p -> startswith(s, p), ps) + add_interface( + :exchanges, + prefixed.(Ref(configuration.exchange_id_prefixes), rxn_strings), + ) + add_interface( + :biomass, + prefixed.(Ref(configuration.biomass_id_prefixes), rxn_strings), + ) + add_interface( + :atp_maintenance, + in.(rxn_strings, Ref(configuration.atp_maintenance_ids)), + ) elseif interface == :boundary - add_interface(:boundary, ((all(s .<= 0) || all(s .>= 0)) for s in eachcol(stoi))) + add_interface( + :boundary, + [(all(col .<= 0) | all(col .>= 0)) for col in eachcol(stoi)], + ) + elseif interface == nothing + # nothing :] + else + throw(DomainError(interface, "unknown interface specifier")) end return constraints From 775ea6b58eeaf45750a04d13430e65e2caa03974 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Wed, 24 Jan 2024 16:23:08 +0100 Subject: [PATCH 517/531] debug interface_constriants --- src/builders/interface.jl | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/builders/interface.jl b/src/builders/interface.jl index 50a54af28..bde95e6c4 100644 --- a/src/builders/interface.jl +++ b/src/builders/interface.jl @@ -32,13 +32,18 @@ are intact with disjoint variable sets. Compatible modules with ready-made interfaces may be created e.g. by [`flux_balance_constraints`](@ref). + +`ignore` may be used to selectively ignore parts of interfaces given the +"module name" identifier and constraint path in the interface (these form 2 +parameters passed to `ignore`). Similarly, `bound` may be used to specify +bounds for the new interface, if required. """ function interface_constraints( ps::Pair...; - default_in_interface = :interface, - out_interface = :interface, - out_balance = :interface_balance, - ignore = _ -> false, + default_interface = :interface, + out_interface = default_interface, + out_balance = Symbol(out_interface, :_balance), + ignore = (_, _) -> false, bound = _ -> nothing, ) @@ -47,15 +52,16 @@ function interface_constraints( prep(id::Symbol, (mod, multiplier)::Tuple{C.ConstraintTree,<:Real}) = prep(id, (mod, default_interface, multiplier)) prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = - prep(id, mod, mod[interface]) + prep(id, (mod, mod[interface])) prep(id::Symbol, (mod, interface, multiplier)::Tuple{C.ConstraintTree,Symbol,<:Real}) = - prep(id, mod, C.map(c -> c * multiplier, mod[interface])) - prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,C.ConstraintTree}) = + prep(id, (mod, C.map(c -> c * multiplier, mod[interface]))) + prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTreeElem,C.ConstraintTreeElem}) = (id^(:network^mod * :interface^interface)) + prep_pair((a, b)) = prep(a, b) # first, collect everything into one huge network # (while also renumbering the interfaces) - modules = sum(prep.(ps); init = C.ConstraintTree()) + modules = sum(prep_pair.(ps); init = C.ConstraintTree()) # TODO maybe split the interface creation into a separate function # (BUT- people shouldn't really need it since they should have all of their @@ -72,12 +78,12 @@ function interface_constraints( # extract the plain networks and add variables for the new interfaces constraints = - ConstraintTree(id => (m.network) for (id, m) in modules) + - out_interface^C.variables_ifor(bound, interface_sum) + C.ConstraintTree(id => (m.network) for (id, m) in modules) + + out_interface^C.variables_ifor((path, _) -> bound(path), interface_sum) - # join everything with the interrace balance and return - constraints * C.zip(interface_sum, constraints.out_interface) do sum, out - C.Constraint(value = sum.value - out.value) + # join everything with the interface balance and return + constraints * out_balance^C.zip(interface_sum, constraints[out_interface]) do sum, out + C.Constraint(sum.value - out.value, 0) end end From 0957b8a2146e268cc4701565af6e642aea097d8a Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 10:11:10 +0100 Subject: [PATCH 518/531] document the configuration --- src/config.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/config.jl b/src/config.jl index be024eab8..5f707dc2a 100644 --- a/src/config.jl +++ b/src/config.jl @@ -23,6 +23,9 @@ around manually. Changing the configuration values at runtime is possible via the global [`configuration`](@ref) variable. + +# Fields +$(TYPEDFIELDS) """ Base.@kwdef mutable struct Configuration From 89adb734c6a2a061fdb45e0615cff7ce7deb39a2 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 10:11:18 +0100 Subject: [PATCH 519/531] fix mmdf --- src/COBREXA.jl | 1 - src/frontend/mmdf.jl | 11 ++++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/COBREXA.jl b/src/COBREXA.jl index 7e54f7876..9719c4b56 100644 --- a/src/COBREXA.jl +++ b/src/COBREXA.jl @@ -67,7 +67,6 @@ include("builders/fbc.jl") include("builders/interface.jl") include("builders/knockouts.jl") include("builders/loopless.jl") -include("builders/mmdf.jl") include("builders/objectives.jl") include("builders/scale.jl") include("builders/unsigned.jl") diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 49951538d..43659792e 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -192,13 +192,18 @@ function max_min_driving_force_analysis( end * :concentration_ratio_constraints^C.ConstraintTree( Symbol(cid) => difference_constraint( - m.log_metabolite_concentrations[Symbol(m1)], - m.log_metabolite_concentrations[Symbol(m2)], + constraints.log_concentrations[Symbol(m1)], + constraints.log_concentrations[Symbol(m2)], log(ratio), ) for (cid, (m1, m2, ratio)) in concentration_ratios ) - optimized_constraints(m; objective = m.max_min_driving_force.value, optimizer, settings) + optimized_constraints( + constraints; + objective = constraints.max_min_driving_force.value, + optimizer, + settings, + ) end export max_min_driving_force_analysis From c28530c35e7824a367d2c44a3ee4d3b0994548a5 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 11:31:01 +0100 Subject: [PATCH 520/531] export the constraint comparison functions --- src/builders/compare.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index 633856ca7..3f2d109c3 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -24,6 +24,8 @@ be valid. Any type of `ConstraintTree.Bound` can be supplied. difference_constraint(a, b, difference_bound) = C.Constraint(C.value(b) - C.value(a), difference_bound) +export difference_constraint + """ $(TYPEDSIGNATURES) @@ -31,6 +33,8 @@ A constraint that makes sure that the values of `a` and `b` are the same. """ equal_value_constraint(a, b) = difference_constraint(a, b, 0) +export equal_value_constraint + """ $(TYPEDSIGNATURES) @@ -44,6 +48,8 @@ all_equal_constraints(a, tree::C.ConstraintTree) = equal_value_constraint(a, b) end +export all_equal_constraints + """ $(TYPEDSIGNATURES) @@ -52,6 +58,8 @@ the the value of `b`. """ greater_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(0, Inf)) +export greater_or_equal_constraint + """ $(TYPEDSIGNATURES) @@ -60,4 +68,4 @@ the value of `b`. """ less_or_equal_constraint(a, b) = difference_constraint(b, a, C.Between(0, Inf)) -# TODO try to use the helper functions everywhere +export less_or_equal_constraint From cb21cb03fd3b0b49eddf96abf42be2da524931c6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 11:31:26 +0100 Subject: [PATCH 521/531] make the interface of the interface more interface --- src/builders/interface.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/builders/interface.jl b/src/builders/interface.jl index bde95e6c4..8620ebcf9 100644 --- a/src/builders/interface.jl +++ b/src/builders/interface.jl @@ -54,7 +54,11 @@ function interface_constraints( prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTree,Symbol}) = prep(id, (mod, mod[interface])) prep(id::Symbol, (mod, interface, multiplier)::Tuple{C.ConstraintTree,Symbol,<:Real}) = - prep(id, (mod, C.map(c -> c * multiplier, mod[interface]))) + prep(id, (mod, mod[interface], multiplier)) + prep( + id::Symbol, + (mod, interface, multiplier)::Tuple{C.ConstraintTree,C.ConstraintTreeElem,<:Real}, + ) = prep(id, (mod, C.map(c -> c * multiplier, interface))) prep(id::Symbol, (mod, interface)::Tuple{C.ConstraintTreeElem,C.ConstraintTreeElem}) = (id^(:network^mod * :interface^interface)) prep_pair((a, b)) = prep(a, b) From dfcce5637ae9ad2139115b475341113190818da6 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 11:31:43 +0100 Subject: [PATCH 522/531] fix the community tutorial --- docs/src/examples/08-community-models.jl | 154 +++++++++-------------- 1 file changed, 57 insertions(+), 97 deletions(-) diff --git a/docs/src/examples/08-community-models.jl b/docs/src/examples/08-community-models.jl index e3c1dd78b..daa293827 100644 --- a/docs/src/examples/08-community-models.jl +++ b/docs/src/examples/08-community-models.jl @@ -28,11 +28,10 @@ import Downloads: download download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") # Additionally to COBREXA and the model format package, we will need a solver -# -- let's use Tulip here: +# -- let's use GLPK here: import JSONFBCModels -import Tulip -import AbstractFBCModels as A +import GLPK import ConstraintTrees as C model = load_model("e_coli_core.json") @@ -47,109 +46,70 @@ model = load_model("e_coli_core.json") # ## Building a community of two *E. coli*s # Here we will construct a simple community of two interacting microbes. To do -# this, we need to import the models. We import the models are ConstraintTrees, -# because it is easier to build the model explicitly than rely on an opaque -# one-shot function. - -ecoli1 = flux_balance_constraints(model) -ecoli2 = flux_balance_constraints(model) - -# Since the models are joined through their individual exchange reactions to an -# environmental exchange reactionq, we need to identify all possible exchange -# reactions in the community. Since the models are the same, this is -# straightforward here. Additionally, we need to specify the upper and lower -# bounds of these environmental exchange reactions. -lbs, ubs = A.bounds(model) - -env_ex_rxns = Dict( - rid => (lbs[i], ubs[i]) for - (i, rid) in enumerate(A.reactions(model)) if startswith(rid, "EX_") -) - -# Now we simply create an blank model that only includes environmental exchange reactions. - -m = build_community_environment(env_ex_rxns) - -# Next we join each member microbe to the model. -m += :bug1^ecoli1 -m += :bug2^ecoli2 - -# We also need to specify the abundances of each member, as this weights the -# flux of each metabolite each member microbe can share with other members or -# the environment. -member_abundances = [(:bug1, 0.2), (:bug2, 0.8)] - -m *= :environmental_exchange_balances^link_environmental_exchanges(m, member_abundances) - -# Finally, the most sensible community FBA simulation involves assuming the -# growth rate of the models is the same. In this case, we simply set the growth -# rate flux of each member to be the same. -m *= - :equal_growth_rate_constraint^equal_growth_rate_constraints([ - (:bug1, m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), - (:bug2, m.bug2.fluxes.:BIOMASS_Ecoli_core_w_GAM.value), - ]) +# this, we need to import the models. We will represent the models only as +# constraint trees, because it is easier to build the model explicitly than +# rely on an opaque one-shot function. -# Since each growth rate is the same, we can pick any of the growth rates as the -# objective for the simulation. -m *= :objective^C.Constraint(m.bug1.fluxes.:BIOMASS_Ecoli_core_w_GAM.value) +ecoli1 = flux_balance_constraints(model, interface = :sbo) +ecoli2 = flux_balance_constraints(model, interface = :sbo) # Since the models are usually used in a mono-culture context, the glucose input # for each individual member is limited. We need to undo this limitation, and # rather rely on the constrained environmental exchange reaction (and the bounds # we set for it earlier). -m.bug1.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) -m.bug2.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) - -# We can also be interesting, and limit respiration in one of the members, to -# see what effect this has on the community. -m.bug1.fluxes.CYTBD.bound = C.Between(-10.0, 10.0) - -# Finally, we can simulate the system! -sol = optimized_constraints( - m; - objective = m.objective.value, - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], +ecoli1.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) +ecoli2.fluxes.EX_glc__D_e.bound = C.Between(-1000.0, 1000.0) + +# To make the community interesting, we can limit different reactions in both +# members to see how the models cope together: +ecoli1.fluxes.CYTBD.bound = C.Between(-10.0, 10.0) +ecoli2.fluxes.ACALD.bound = C.Between(-5.0, 5.0) + +# Because we created the trees with interfaces, we can connect them easily to +# form a new model with the interface. For simplicity, we use the +# interface-scaling functionality of [`interface_constraints`](@ref +# ConstraintTrees.interface_constraints) to bring in cFBA-like community member +# abundances: + +cc = interface_constraints( + :bug1 => (ecoli1, ecoli1.interface, 0.2), + :bug2 => (ecoli2, ecoli2.interface, 0.8), ) -@test isapprox(sol.:objective, 0.66686196344, atol = TEST_TOLERANCE) #src - -# At the moment the members cannot really exchange any metabolites. We can -# change this by changing their individual exchange bounds. -mets = [:EX_akg_e, :EX_succ_e, :EX_pyr_e, :EX_acald_e, :EX_fum_e, :EX_mal__L_e] -for met in mets - m.bug1.fluxes[met].bound = C.Between(-1000.0, 1000.0) - m.bug2.fluxes[met].bound = C.Between(-1000.0, 1000.0) -end - -sol = optimized_constraints( - m; - objective = m.objective.value, - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 1000)], +# To make the community behave as expected, we need to force equal (scaled) +# growth of all members: + +cc *= + :equal_growth^equal_value_constraint( + cc.bug1.fluxes.BIOMASS_Ecoli_core_w_GAM, + cc.bug2.fluxes.BIOMASS_Ecoli_core_w_GAM, + ) + +# Now we can simulate the community growth by optimizing the new "interfaced" +# biomass: + +optimized_cc = optimized_constraints( + cc, + objective = cc.interface.biomass.BIOMASS_Ecoli_core_w_GAM.value, + optimizer = GLPK.Optimizer, ) +# We can now e.g. observe the differences in individual pairs of exchanges: -# We can see that by allowing the microbes to share metabolites, the growth rate -# of the system as a whole increased! We can inspect the individual exchanges to -# see which metabolites are being shared (pyruvate in this case). -bug1_ex_fluxes = Dict(k => v for (k, v) in sol.bug1.fluxes if startswith(string(k), "EX_")) -bug2_ex_fluxes = Dict(k => v for (k, v) in sol.bug2.fluxes if startswith(string(k), "EX_")) - -#!!! warning "Flux units" -# The unit of the environmental exchange reactions (mmol/gDW_total_biomass/h) is -# different to the unit of the individual species fluxes -# (mmol/gDW_species_biomass/h). This is because the mass balance needs to take -# into account the abundance of each species for the simulation to make sense. -# In this specific case, look at the flux of pyruvate (EX_pyr_e). There is no -# environmental exchange flux, so the two microbes share the metabolite. -# However, `bug1_ex_fluxes[:EX_pyr_e] != bug2_ex_fluxes[:EX_pyr_e]`, but rather -# `abundance_bug1 * bug1_ex_fluxes[:EX_pyr_e] == abundance_bug2 * -# bug2_ex_fluxes[:EX_pyr_e]`. Take care of this when comparing fluxes! - -@test isapprox( - abs(0.2 * bug1_ex_fluxes[:EX_pyr_e] + 0.8 * bug2_ex_fluxes[:EX_pyr_e]), - 0.0, - atol = TEST_TOLERANCE, +C.zip( + tuple, + optimized_cc.bug1.interface.exchanges, + optimized_cc.bug2.interface.exchanges, + Tuple{Float64,Float64}, +) + +@test isapprox( #src + optimized_cc.interface.biomass.BIOMASS_Ecoli_core_w_GAM, #src + 15.9005, #src + atol = TEST_TOLERANCE, #src +) #src +@test isapprox( #src + optimized_cc.bug1.fluxes.BIOMASS_Ecoli_core_w_GAM, #src + optimized_cc.bug2.fluxes.BIOMASS_Ecoli_core_w_GAM, #src + atol = TEST_TOLERANCE, #src ) #src From a1489e63792521e13760806b1ba18cdea4134aa3 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 13:31:31 +0100 Subject: [PATCH 523/531] plus-minus error to fix loopless stuff --- docs/src/examples/07-loopless-models.jl | 13 +++---------- src/builders/loopless.jl | 5 ++++- 2 files changed, 7 insertions(+), 11 deletions(-) diff --git a/docs/src/examples/07-loopless-models.jl b/docs/src/examples/07-loopless-models.jl index 9fbb8f647..b29b09516 100644 --- a/docs/src/examples/07-loopless-models.jl +++ b/docs/src/examples/07-loopless-models.jl @@ -34,22 +34,15 @@ download_model( import JSONFBCModels import GLPK -import AbstractFBCModels as A model = load_model("e_coli_core.json") -# ## Running a simple loopless FBA (ll-FBA) +# ## Running a loopless FBA (ll-FBA) # One can directly use `loopless_flux_balance_analysis` to solve an FBA problem # based on `model` where loopless constraints are added to all fluxes. This is # the direct approach. -sol = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) +solution = loopless_flux_balance_analysis(model; optimizer = GLPK.Optimizer) -@test isapprox(sol.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src - -@test all( #src - v * sol.loopless_driving_forces[k] <= -TEST_TOLERANCE for #src - (k, v) in sol.fluxes if #src - haskey(sol.loopless_driving_forces, k) && abs(v) >= 1e-6 #src -) #src +@test isapprox(solution.objective, 0.8739215069684303, atol = TEST_TOLERANCE) #src diff --git a/src/builders/loopless.jl b/src/builders/loopless.jl index 06c645028..de539be7d 100644 --- a/src/builders/loopless.jl +++ b/src/builders/loopless.jl @@ -23,6 +23,9 @@ connects them to `loopless_driving_forces`. The solution is bounded to lie in `internal_nullspace` (which is a sufficient algebraic condition for loop-less-ness). +The indicators must be discrete variables, valued `1` if the reaction flux goes +forward, or `0` if the reaction flux is reversed. + The simplest (but by no means the fastest) way to obtain a good `internal_nullspace` is to use `LinearAlgebra.nullspace` with the internal reactions' stoichiometry matrix. Rows of `internal_nullspace` must correspond @@ -54,7 +57,7 @@ loopless_constraints(; ), :flux_direction_upper_bounds => C.ConstraintTree( r => C.Constraint( - value = fluxes[r].value + + value = fluxes[r].value - flux_infinity_bound * loopless_direction_indicators[r].value, bound = C.Between(-Inf, 0), ) for r in internal_reactions From fc737e36040ea0c1b8992a5a0284bfc4f156aa12 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 13:51:45 +0100 Subject: [PATCH 524/531] mmdf acts sus --- docs/src/examples/06-mmdf.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/src/examples/06-mmdf.jl b/docs/src/examples/06-mmdf.jl index 10081632a..2dcce6a07 100644 --- a/docs/src/examples/06-mmdf.jl +++ b/docs/src/examples/06-mmdf.jl @@ -30,14 +30,16 @@ using COBREXA import Downloads: download +#TODO use AFBCMs functionality !isfile("e_coli_core.json") && download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") # Additionally to COBREXA, and the model format package, we will need a solver -# -- let's use Tulip here: +# -- let's use GLPK here: +using COBREXA import JSONFBCModels -import Tulip +import GLPK model = load_model("e_coli_core.json") @@ -116,8 +118,7 @@ mmdf_solution = max_min_driving_force_analysis( concentration_upper_bound = 1e-1, # M T = 298.15, # Kelvin R = 8.31446261815324e-3, # kJ/K/mol - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 1_000)], + optimizer = GLPK.Optimizer, ) @test isapprox(mmdf_solution.max_min_driving_force, 5.78353, atol = TEST_TOLERANCE) #src From 282908f2735a8f8771f8262931ccce6c1e11fdcb Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 17:02:45 +0100 Subject: [PATCH 525/531] fix mmdf-related stuff --- src/builders/compare.jl | 4 ++-- src/builders/fbc.jl | 8 +++----- src/frontend/mmdf.jl | 13 +++++++------ 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/src/builders/compare.jl b/src/builders/compare.jl index 3f2d109c3..168645693 100644 --- a/src/builders/compare.jl +++ b/src/builders/compare.jl @@ -56,7 +56,7 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the value of `a` is greater than or equal to the the value of `b`. """ -greater_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(0, Inf)) +greater_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(-Inf, 0)) export greater_or_equal_constraint @@ -66,6 +66,6 @@ $(TYPEDSIGNATURES) A constraint that makes sure that the value of `a` is less than or equal to the the value of `b`. """ -less_or_equal_constraint(a, b) = difference_constraint(b, a, C.Between(0, Inf)) +less_or_equal_constraint(a, b) = difference_constraint(a, b, C.Between(0, Inf)) export less_or_equal_constraint diff --git a/src/builders/fbc.jl b/src/builders/fbc.jl index 8572e5749..910895a8a 100644 --- a/src/builders/fbc.jl +++ b/src/builders/fbc.jl @@ -120,8 +120,7 @@ The output constraint tree contains a log-concentration variable for each metabolite in subtree `log_concentrations`. Individual reactions' total reactant log concentrations (i.e., all log concentrations of actual reactants minus all log concentrations of products) have their own variables in -`reactant_log_concentrations`. The values are connected by -`log_concentration_stoichiometry`. +`reactant_log_concentrations`. Function `concentration_bound` may return a bound for the log-concentration of a given metabolite (compatible with `ConstraintTrees.Bound`), or `nothing`. @@ -135,8 +134,7 @@ function log_concentration_constraints( stoi = A.stoichiometry(model) constraints = - :log_concentrations^C.variables(keys = mets, bounds = concentration_bound.(mets)) + - :reactant_log_concentrations^C.variables(keys = rxns) + :log_concentrations^C.variables(keys = mets, bounds = concentration_bound.(mets)) cs = C.ConstraintTree() @@ -150,7 +148,7 @@ function log_concentration_constraints( end end - return constraints * :log_concentration_stoichiometry^cs + return constraints * :reactant_log_concentrations^cs end export log_concentration_constraints diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index 43659792e..a8a849a7a 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -165,9 +165,9 @@ function max_min_driving_force_analysis( default_concentration_bound end end, - ) + :max_min_driving_force^C.variable() + ) + :min_driving_force^C.variable() - min_driving_forces = C.ConstraintTree( + driving_forces = C.ConstraintTree( let r = Symbol(rid) r => C.Constraint( value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r].value, @@ -186,9 +186,9 @@ function max_min_driving_force_analysis( constraints = constraints * - :min_driving_forces^min_driving_forces * - :min_driving_force_thresholds^C.map(min_driving_forces) do c - greater_or_equal_constraint(constraints.max_min_driving_force, c) + :driving_forces^driving_forces * + :min_driving_force_thresholds^C.map(driving_forces) do c + less_or_equal_constraint(constraints.min_driving_force, c) end * :concentration_ratio_constraints^C.ConstraintTree( Symbol(cid) => difference_constraint( @@ -200,7 +200,8 @@ function max_min_driving_force_analysis( optimized_constraints( constraints; - objective = constraints.max_min_driving_force.value, + objective = constraints.min_driving_force.value, + sense = Maximal, optimizer, settings, ) From b09aaa4c68f3cb2691351c35a8f5a1f58e3a7fde Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 21:47:52 +0100 Subject: [PATCH 526/531] this should fix MMDF --- docs/src/examples/06-mmdf.jl | 3 ++- src/frontend/mmdf.jl | 23 +++++++++-------------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/docs/src/examples/06-mmdf.jl b/docs/src/examples/06-mmdf.jl index 2dcce6a07..53e6407f8 100644 --- a/docs/src/examples/06-mmdf.jl +++ b/docs/src/examples/06-mmdf.jl @@ -121,4 +121,5 @@ mmdf_solution = max_min_driving_force_analysis( optimizer = GLPK.Optimizer, ) -@test isapprox(mmdf_solution.max_min_driving_force, 5.78353, atol = TEST_TOLERANCE) #src +# TODO verify correctness +@test isapprox(mmdf_solution.min_driving_force, 2.79911, atol = TEST_TOLERANCE) #src diff --git a/src/frontend/mmdf.jl b/src/frontend/mmdf.jl index a8a849a7a..069383734 100644 --- a/src/frontend/mmdf.jl +++ b/src/frontend/mmdf.jl @@ -168,19 +168,15 @@ function max_min_driving_force_analysis( ) + :min_driving_force^C.variable() driving_forces = C.ConstraintTree( - let r = Symbol(rid) - r => C.Constraint( - value = dGr0 + (R * T) * constraints.reactant_log_concentrations[r].value, - bound = let rf = reference_flux[rid] - if isapprox(rf, 0.0, atol = reference_flux_atol) - C.EqualTo(0) - elseif rf > 0 - C.Between(-Inf, 0) - else - C.Between(0, Inf) - end - end, - ) + let r = Symbol(rid), + rf = reference_flux[rid], + df = dGr0 + R * T * constraints.reactant_log_concentrations[r].value + + r => if isapprox(rf, 0.0, atol = reference_flux_atol) + C.Constraint(df, C.EqualTo(0)) + else + C.Constraint(rf > 0 ? -df : df, C.Between(0, Inf)) + end end for (rid, dGr0) in reaction_standard_gibbs_free_energies ) @@ -201,7 +197,6 @@ function max_min_driving_force_analysis( optimized_constraints( constraints; objective = constraints.min_driving_force.value, - sense = Maximal, optimizer, settings, ) From 7752926be320653d244d5daca4efbed3389aa0f7 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 22:16:26 +0100 Subject: [PATCH 527/531] make CI cheaper --- .github/workflows/CompatHelper.yml | 3 +- .github/workflows/ci.yml | 20 +- .github/workflows/docs.yml | 26 +++ .github/workflows/pr-format.yml | 4 +- .gitlab-ci.yml | 298 ----------------------------- 5 files changed, 35 insertions(+), 316 deletions(-) create mode 100644 .github/workflows/docs.yml delete mode 100644 .gitlab-ci.yml diff --git a/.github/workflows/CompatHelper.yml b/.github/workflows/CompatHelper.yml index 2afd0558c..8765dd6e8 100644 --- a/.github/workflows/CompatHelper.yml +++ b/.github/workflows/CompatHelper.yml @@ -37,8 +37,7 @@ jobs: - name: "Run CompatHelper" run: | import CompatHelper - CompatHelper.main(master_branch="develop") + CompatHelper.main() shell: julia --color=yes {0} env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COMPATHELPER_PRIV: ${{ secrets.DOCUMENTER_KEY }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 703836343..96c189425 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,10 @@ name: CI + on: push: branches: - master - - develop + pull_request: jobs: test: @@ -13,8 +14,8 @@ jobs: fail-fast: false matrix: version: - - '1.6' # Oldest supported version for COBREXA.jl - '1' # This is always the latest stable release in the 1.X series + - '1.6' # LTS #- 'nightly' os: - ubuntu-latest @@ -23,21 +24,12 @@ jobs: arch: - x64 steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - uses: julia-actions/setup-julia@v1 with: version: ${{ matrix.version }} arch: ${{ matrix.arch }} - - uses: actions/cache@v1 - env: - cache-name: cache-artifacts - with: - path: ~/.julia/artifacts - key: ${{ runner.os }}-test-${{ env.cache-name }}-${{ hashFiles('**/Project.toml') }} - restore-keys: | - ${{ runner.os }}-test-${{ env.cache-name }}- - ${{ runner.os }}-test- - ${{ runner.os }}- + - uses: julia-actions/cache@v1 - uses: julia-actions/julia-buildpkg@latest - run: | git config --global user.name Tester @@ -45,6 +37,6 @@ jobs: - uses: julia-actions/julia-runtest@latest continue-on-error: ${{ matrix.version == 'nightly' }} - uses: julia-actions/julia-processcoverage@v1 - - uses: codecov/codecov-action@v1 + - uses: codecov/codecov-action@v3 with: file: lcov.info diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 000000000..4b533eb24 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,26 @@ + +# ref: https://juliadocs.github.io/Documenter.jl/stable/man/hosting/#GitHub-Actions-1 +name: Documentation + +on: + pull_request: + push: + branches: + - master + tags: '*' + release: + types: [published, created] + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: julia-actions/setup-julia@latest + - uses: julia-actions/cache@v1 + - name: Install dependencies + run: julia --color=yes --project=docs/ -e 'using Pkg; Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate()' + - name: Build and deploy + env: + DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} # For authentication with SSH deploy key + run: julia --color=yes --project=docs/ docs/make.jl diff --git a/.github/workflows/pr-format.yml b/.github/workflows/pr-format.yml index b23cbc932..3ff70d143 100644 --- a/.github/workflows/pr-format.yml +++ b/.github/workflows/pr-format.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Clone the repository - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Checkout the pull request code # this checks out the actual branch so that one can commit into it if: github.event_name == 'issue_comment' env: @@ -20,7 +20,7 @@ jobs: gh pr checkout ${{ github.event.issue.number }} - name: Install JuliaFormatter and format run: | - julia --color=yes -e 'import Pkg; Pkg.add(name="JuliaFormatter",version="1.0.22")' + julia --color=yes -e 'import Pkg; Pkg.add("JuliaFormatter")' julia --color=yes -e 'using JuliaFormatter; format(".")' - name: Remove trailing whitespace run: | diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml deleted file mode 100644 index 4f7aa3de7..000000000 --- a/.gitlab-ci.yml +++ /dev/null @@ -1,298 +0,0 @@ - -stages: - - test # this checks the viability of the code - - assets # this builds assets to be included in documentation and distribution binaries - - documentation # this processes the documentation - - test-compat # this runs many additional compatibility tests - -variables: - GIT_STRATEGY: clone - DOCKER_DRIVER: overlay2 - DOCKER_TLS_CERTDIR: "" - APPTAINER_DOCKER_TAG: "v3.9.4" - DOCKER_HUB_TAG: "lcsbbiocore/cobrexa.jl" - DOCKER_GHCR_TAG: "ghcr.io/lcsb-biocore/docker/cobrexa.jl" - APPTAINER_GHCR_TAG: "lcsb-biocore/apptainer/cobrexa.jl" - -# -# Predefined conditions for triggering jobs -# - -.global_trigger_pull_request: &global_trigger_pull_request - rules: - - if: $CI_COMMIT_BRANCH == "develop" - when: never - - if: $CI_COMMIT_BRANCH == "master" - when: never - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - -.global_trigger_build_doc: &global_trigger_build_doc - rules: - - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "master" && $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME == "develop" - - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "develop" && $CI_EXTERNAL_PULL_REQUEST_SOURCE_BRANCH_NAME == "next" - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - when: never - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_COMMIT_TAG =~ /^v/ - -.global_trigger_full_tests: &global_trigger_full_tests - rules: - - if: $CI_COMMIT_BRANCH == "develop" - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_COMMIT_TAG =~ /^v/ - -.global_trigger_compat_tests: &global_trigger_compat_tests - rules: - - if: $CI_COMMIT_BRANCH == "master" - - if: $CI_EXTERNAL_PULL_REQUEST_TARGET_BRANCH_NAME == "master" - -.global_trigger_test_containers: &global_trigger_test_containers - rules: - - if: $CI_PIPELINE_SOURCE == "external_pull_request_event" - when: never - - if: $CI_COMMIT_BRANCH == "develop" - -.global_trigger_release_containers: &global_trigger_release_containers - rules: - - if: $CI_COMMIT_TAG =~ /^v/ - -# -# Test environment & platform settings -# - -.global_dind: &global_dind - image: docker:20.10.12 - tags: - - privileged - services: - - name: docker:20.10.12-dind - command: ["--tls=false", "--mtu=1458", "--registry-mirror", "https://docker-registry.lcsb.uni.lu"] - before_script: - - docker login -u $CI_USER_NAME -p $GITLAB_ACCESS_TOKEN $CI_REGISTRY - -.global_julia19: &global_julia19 - variables: - JULIA_VER: "v1.9.4" - -.global_julia16: &global_julia16 - variables: - JULIA_VER: "v1.6.0" - -.global_env_linux: &global_env_linux - script: - - $ARTENOLIS_SOFT_PATH/julia/$JULIA_VER/bin/julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)' - -.global_env_win: &global_env_win - script: - - $global:LASTEXITCODE = 0 # Note the global prefix. - - Invoke-Expression $Env:ARTENOLIS_SOFT_PATH"\julia\"$Env:JULIA_VER"\bin\julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)'" - - exit $LASTEXITCODE - -.global_env_win8: &global_env_win8 - tags: - - windows8 - <<: *global_env_win - -.global_env_win10: &global_env_win10 - tags: - - windows10 - <<: *global_env_win - -.global_env_mac: &global_env_mac - tags: - - mac - script: - - $ARTENOLIS_SOFT_PATH_MAC/julia/$JULIA_VER/Contents/Resources/julia/bin/julia --inline=yes --check-bounds=yes --color=yes --project=@. -e 'import Pkg; Pkg.test(; coverage = true)' - -.global_build_apptainer: &global_build_apptainer - image: - name: "quay.io/singularity/singularity:$APPTAINER_DOCKER_TAG" - # the image entrypoint is the singularity binary by default - entrypoint: ["/bin/sh", "-c"] - tags: - - privileged - -# -# TESTS -# -# The "basic" required test that gets triggered for the basic testing, runs in -# any available docker and current julia -# - -docker:julia1.9: - stage: test - image: $CI_REGISTRY/r3/docker/julia-custom - script: - - julia --check-bounds=yes --inline=yes --project=@. -e "import Pkg; Pkg.test(; coverage = true)" - <<: *global_trigger_pull_request - -# -# The required compatibility test to pass on branches&tags before the docs get -# built & deployed -# - -linux:julia1.9: - stage: test - tags: - - slave01 - <<: *global_trigger_full_tests - <<: *global_julia19 - <<: *global_env_linux - -linux:julia1.6: - stage: test - tags: - - slave02 - <<: *global_trigger_full_tests - <<: *global_julia16 - <<: *global_env_linux - -# -# Additional platform&environment compatibility tests -# - -windows8:julia1.9: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia19 - <<: *global_env_win8 - -windows10:julia1.9: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia19 - <<: *global_env_win10 - -mac:julia1.9: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia19 - <<: *global_env_mac - -windows8:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_win8 - -windows10:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_win10 - -mac:julia1.6: - stage: test-compat - <<: *global_trigger_compat_tests - <<: *global_julia16 - <<: *global_env_mac - -# -# ASSETS -# -# This builds the development history gif using gource, and some containers. -# - -gource: - stage: assets - needs: [] # allow faster start - script: - - docker run -v "$PWD":/visualization $CI_REGISTRY/r3/docker/gource - artifacts: - paths: ['output.gif'] - expire_in: 1 year - <<: *global_trigger_build_doc - <<: *global_dind - -apptainer-test: - stage: assets - script: | - alias apptainer=singularity - apptainer build cobrexa-test.sif cobrexa.def - <<: *global_build_apptainer - <<: *global_trigger_test_containers - -apptainer-release: - stage: assets - script: - - | - # build the container - alias apptainer=singularity - apptainer build cobrexa.sif cobrexa.def - - | - # push to GHCR - alias apptainer=singularity - export SINGULARITY_DOCKER_USERNAME="$GITHUB_ACCESS_USERNAME" - export SINGULARITY_DOCKER_PASSWORD="$GITHUB_ACCESS_TOKEN" - apptainer push cobrexa.sif "oras://ghcr.io/$APPTAINER_GHCR_TAG:latest" - apptainer push cobrexa.sif "oras://ghcr.io/$APPTAINER_GHCR_TAG:$CI_COMMIT_TAG" - <<: *global_build_apptainer - <<: *global_trigger_release_containers - -docker-test: - stage: assets - script: - - docker build -t "$DOCKER_HUB_TAG:testing" . - <<: *global_dind - <<: *global_trigger_test_containers - -docker-release: - stage: assets - script: - - docker build -t "$DOCKER_HUB_TAG:latest" . - # alias and push to docker hub - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_HUB_TAG:$CI_COMMIT_TAG" - - echo "$DOCKER_IO_ACCESS_TOKEN" | docker login --username "$DOCKER_IO_USER" --password-stdin - - docker push "$DOCKER_HUB_TAG:latest" - - docker push "$DOCKER_HUB_TAG:$CI_COMMIT_TAG" - # make 2 extra aliases and push to GHCR - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_GHCR_TAG:latest" - - docker tag "$DOCKER_HUB_TAG:latest" "$DOCKER_GHCR_TAG:$CI_COMMIT_TAG" - - echo "$GITHUB_ACCESS_TOKEN" | docker login ghcr.io --username "$GITHUB_ACCESS_USERNAME" --password-stdin - - docker push "$DOCKER_GHCR_TAG:latest" - - docker push "$DOCKER_GHCR_TAG:$CI_COMMIT_TAG" - <<: *global_dind - <<: *global_trigger_release_containers - -# -# DOCUMENTATION -# - -pages: - stage: documentation - dependencies: - - gource - # Note: This dependency is also implied by the stage ordering, but let's - # be sure. As of Nov 2021, the assets are not used directly, but referred - # to externally from the docs. - image: $CI_REGISTRY/r3/docker/julia-custom - script: - # resolve and build packages from the docs/Project.toml file - - julia --project=docs -e 'using Pkg; Pkg.resolve(); Pkg.develop(PackageSpec(path=pwd())); Pkg.instantiate();' - - # build and deploy docs (this doesn't upload the gource animation asset) - - julia --project=docs --color=yes docs/make.jl - - # move to the directory to be picked up by Gitlab pages (with assets) - - mv docs/build public - artifacts: - paths: - - public - <<: *global_trigger_build_doc - -# -# EXTERNAL REPOSITORIES -# -# This trigger the test pipeline in external repo as defined by gitlab -# variables. -# - -trigger: - stage: test-compat - image: curlimages/curl - tags: - - privileged - script: - - curl --silent --output /dev/null -X POST -F token=$EXTERNAL_REPO_TOKEN -F ref=$EXTERNAL_REPO_BRANCH $EXTERNAL_REPO - <<: *global_trigger_full_tests From 7acb091fea490febbcf1082da166abdc91a498f1 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 22:51:15 +0100 Subject: [PATCH 528/531] make the docs build (although with warnings) --- .../examples/05-enzyme-constrained-models.jl | 243 ++++++++++++++++-- docs/src/examples/06-mmdf.jl | 8 +- 2 files changed, 231 insertions(+), 20 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index a978b7358..7b5abaafa 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -45,11 +45,79 @@ model = load_model("e_coli_core.json") # isozyme specfic. Many machine learning tools, or experimental data sets, can # be used to estimate these parameters. -# ```@raw html -#
Reaction turnover numbers -# ??? how to load this? -#
-# ``` +#md # ```@raw html +#md #
Reaction turnover numbers +#md # ``` +# This data is taken from: *Heckmann, David, et al. "Machine learning applied +# to enzyme turnover numbers reveals protein structural correlates and improves +# metabolic models." Nature communications 9.1 (2018): 1-10.* +const ecoli_core_reaction_kcats = Dict( + "ACALD" => 568.11, + "PTAr" => 1171.97, + "ALCD2x" => 75.95, + "PDH" => 529.76, + "MALt2_2" => 234.03, + "CS" => 113.29, + "PGM" => 681.4, + "TKT1" => 311.16, + "ACONTa" => 191.02, + "GLNS" => 89.83, + "ICL" => 17.45, + "FBA" => 373.42, + "FORt2" => 233.93, + "G6PDH2r" => 589.37, + "AKGDH" => 264.48, + "TKT2" => 467.42, + "FRD7" => 90.20, + "SUCOAS" => 18.49, + "ICDHyr" => 39.62, + "AKGt2r" => 234.99, + "GLUSy" => 33.26, + "TPI" => 698.30, + "FORt" => 234.38, + "ACONTb" => 159.74, + "GLNabc" => 233.80, + "RPE" => 1772.485, + "ACKr" => 554.61, + "THD2" => 24.73, + "PFL" => 96.56, + "RPI" => 51.77, + "D_LACt2" => 233.51, + "TALA" => 109.05, + "PPCK" => 218.42, + "PGL" => 2120.42, + "NADTRHD" => 186.99, + "PGK" => 57.64, + "LDH_D" => 31.11, + "ME1" => 487.01, + "PIt2r" => 233.86, + "ATPS4r" => 71.42, + "GLCpts" => 233.90, + "GLUDy" => 105.32, + "CYTBD" => 153.18, + "FUMt2_2" => 234.37, + "FRUpts2" => 234.19, + "GAPD" => 128.76, + "PPC" => 165.52, + "NADH16" => 971.74, + "PFK" => 1000.46, + "MDH" => 25.93, + "PGI" => 468.11, + "ME2" => 443.09, + "GND" => 240.12, + "SUCCt2_2" => 234.18, + "GLUN" => 44.76, + "ADK1" => 111.64, + "SUCDi" => 680.31, + "ENO" => 209.35, + "MALS" => 252.75, + "GLUt2r" => 234.22, + "PPS" => 706.14, + "FUM" => 1576.83, +) +#md # ```@raw html +#md #
+#md # ``` # Each reaction in a constraint-based model usually has gene reaction rules # associated with it. These typically take the form of, possibly multiple, @@ -63,8 +131,7 @@ for rid in A.reactions(model) haskey(ecoli_core_reaction_kcats, rid) || continue # skip if no kcat data available for (i, grr) in enumerate(grrs) d = get!(reaction_isozymes, rid, Dict{String,Isozyme}()) - # each isozyme gets a unique name - d["isozyme_"*string(i)] = Isozyme( + d["isozyme_"*string(i)] = Isozyme( # each isozyme gets a unique name gene_product_stoichiometry = Dict(grr .=> fill(1.0, size(grr))), # assume subunit stoichiometry of 1 for all isozymes kcat_forward = ecoli_core_reaction_kcats[rid] * 3.6, # forward reaction turnover number units = 1/h kcat_reverse = ecoli_core_reaction_kcats[rid] * 3.6, # reverse reaction turnover number units = 1/h @@ -72,11 +139,8 @@ for rid in A.reactions(model) end end -#!!! warning "Turnover number units" -# Take care with the units of the turnover numbers. In literature they are -# usually reported in 1/s. However, flux units are typically mmol/gDW/h, -# suggesting that you should rescale the turnover numbers to 1/h if you -# want to use the conventional flux units. +#md #!!! warning "Turnover number units" +#md # Take care with the units of the turnover numbers. In literature they are usually reported in 1/s. However, flux units are typically mmol/gDW/h, suggesting that you should rescale the turnover numbers to 1/h if you want to use the conventional flux units. ### Enzyme molar masses @@ -84,11 +148,156 @@ end # of each flux/isozyme in the capacity bound(s). These data can typically be # found in uniprot. -# ```@raw html -#
Reaction turnover numbers -# ??? how to load this? -#
-# ``` +#md # ```@raw html +#md #
Reaction turnover numbers +#md # ``` +# This data is downloaded from Uniprot for E. coli K12, gene mass in kDa. To +# obtain these data yourself, go to [Uniprot](https://www.uniprot.org/) and +# search using these terms: `reviewed:yes AND organism:"Escherichia coli +# (strain K12) [83333]"`. +const ecoli_core_gene_product_masses = Dict( + "b4301" => 23.214, + "b1602" => 48.723, + "b4154" => 65.972, + "b3236" => 32.337, + "b1621" => 56.627, + "b1779" => 35.532, + "b3951" => 85.96, + "b1676" => 50.729, + "b3114" => 85.936, + "b1241" => 96.127, + "b2276" => 52.044, + "b1761" => 48.581, + "b3925" => 35.852, + "b3493" => 53.389, + "b3733" => 31.577, + "b2926" => 41.118, + "b0979" => 42.424, + "b4015" => 47.522, + "b2296" => 43.29, + "b4232" => 36.834, + "b3732" => 50.325, + "b2282" => 36.219, + "b2283" => 100.299, + "b0451" => 44.515, + "b2463" => 82.417, + "b0734" => 42.453, + "b3738" => 30.303, + "b3386" => 24.554, + "b3603" => 59.168, + "b2416" => 63.562, + "b0729" => 29.777, + "b0767" => 36.308, + "b3734" => 55.222, + "b4122" => 60.105, + "b2987" => 53.809, + "b2579" => 14.284, + "b0809" => 26.731, + "b1524" => 33.516, + "b3612" => 56.194, + "b3735" => 19.332, + "b3731" => 15.068, + "b1817" => 35.048, + "b1603" => 54.623, + "b1773" => 30.81, + "b4090" => 16.073, + "b0114" => 99.668, + "b3962" => 51.56, + "b2464" => 35.659, + "b2976" => 80.489, + "b1818" => 27.636, + "b2285" => 18.59, + "b1702" => 87.435, + "b1849" => 42.434, + "b1812" => 50.97, + "b0902" => 28.204, + "b3403" => 59.643, + "b1612" => 60.299, + "b1854" => 51.357, + "b0811" => 27.19, + "b0721" => 14.299, + "b2914" => 22.86, + "b1297" => 53.177, + "b0723" => 64.422, + "b3919" => 26.972, + "b3115" => 43.384, + "b4077" => 47.159, + "b3528" => 45.436, + "b0351" => 33.442, + "b2029" => 51.481, + "b1819" => 30.955, + "b0728" => 41.393, + "b2935" => 72.212, + "b2415" => 9.119, + "b0727" => 44.011, + "b0116" => 50.688, + "b0485" => 32.903, + "b3736" => 17.264, + "b0008" => 35.219, + "b3212" => 163.297, + "b3870" => 51.904, + "b4014" => 60.274, + "b2280" => 19.875, + "b2133" => 64.612, + "b2278" => 66.438, + "b0118" => 93.498, + "b2288" => 16.457, + "b3739" => 13.632, + "b3916" => 34.842, + "b3952" => 32.43, + "b2925" => 39.147, + "b2465" => 73.043, + "b2297" => 77.172, + "b2417" => 18.251, + "b4395" => 24.065, + "b3956" => 99.063, + "b0722" => 12.868, + "b2779" => 45.655, + "b0115" => 66.096, + "b0733" => 58.205, + "b1478" => 35.38, + "b2492" => 30.565, + "b0724" => 26.77, + "b0755" => 28.556, + "b1136" => 45.757, + "b2286" => 68.236, + "b0978" => 57.92, + "b1852" => 55.704, + "b2281" => 20.538, + "b2587" => 47.052, + "b2458" => 36.067, + "b0904" => 30.991, + "b1101" => 50.677, + "b0875" => 23.703, + "b3213" => 52.015, + "b2975" => 58.92, + "b0720" => 48.015, + "b0903" => 85.357, + "b1723" => 32.456, + "b2097" => 38.109, + "b3737" => 8.256, + "b0810" => 24.364, + "b4025" => 61.53, + "b1380" => 36.535, + "b0356" => 39.359, + "b2277" => 56.525, + "b1276" => 97.677, + "b4152" => 15.015, + "b1479" => 63.197, + "b4153" => 27.123, + "b4151" => 13.107, + "b2287" => 25.056, + "b0474" => 23.586, + "b2284" => 49.292, + "b1611" => 50.489, + "b0726" => 105.062, + "b2279" => 10.845, + "s0001" => 0.0, +) +#md # ```@raw html +#md #
+#md # ``` + gene_product_molar_masses = ecoli_core_gene_product_masses diff --git a/docs/src/examples/06-mmdf.jl b/docs/src/examples/06-mmdf.jl index 53e6407f8..df3e672c9 100644 --- a/docs/src/examples/06-mmdf.jl +++ b/docs/src/examples/06-mmdf.jl @@ -47,11 +47,10 @@ model = load_model("e_coli_core.json") # We will need ΔᵣG⁰ data for each reaction we want to include in the # thermodynamic model. To generate this data manually, go to -# https://equilibrator.weizmann.ac.il/. To generate automatically, use the -# eQuilibrator.jl package. +# https://equilibrator.weizmann.ac.il/. To generate automatically, you may use +# the eQuilibrator.jl package. reaction_standard_gibbs_free_energies = Dict{String,Float64}( - # Units are in kJ/mol "ENO" => -3.8108376097261782, "FBA" => 23.376920310319235, "GAPD" => 0.5307809794271634, @@ -65,6 +64,9 @@ reaction_standard_gibbs_free_energies = Dict{String,Float64}( "TPI" => 5.621932460512994, ) + +# (The units of the energies are kJ/mol.) + # ## Running basic max min driving force analysis # If a reference flux is not specified, it is assumed that every reaction in the From 8d8eb4dace4fdd0f1cd6e0d2cce5054da401a684 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 22:51:34 +0100 Subject: [PATCH 529/531] this isn't necessary anymore --- test/data_static.jl | 216 -------------------------------------------- 1 file changed, 216 deletions(-) delete mode 100644 test/data_static.jl diff --git a/test/data_static.jl b/test/data_static.jl deleted file mode 100644 index a0bcf8950..000000000 --- a/test/data_static.jl +++ /dev/null @@ -1,216 +0,0 @@ - -const ecoli_core_gene_product_masses = Dict( - #= - Data downloaded from Uniprot for E. coli K12, - gene mass in kDa. To obtain these data yourself, go to - Uniprot: https://www.uniprot.org/ - and search using these terms: - =# - "b4301" => 23.214, - "b1602" => 48.723, - "b4154" => 65.972, - "b3236" => 32.337, - "b1621" => 56.627, - "b1779" => 35.532, - "b3951" => 85.96, - "b1676" => 50.729, - "b3114" => 85.936, - "b1241" => 96.127, - "b2276" => 52.044, - "b1761" => 48.581, - "b3925" => 35.852, - "b3493" => 53.389, - "b3733" => 31.577, - "b2926" => 41.118, - "b0979" => 42.424, - "b4015" => 47.522, - "b2296" => 43.29, - "b4232" => 36.834, - "b3732" => 50.325, - "b2282" => 36.219, - "b2283" => 100.299, - "b0451" => 44.515, - "b2463" => 82.417, - "b0734" => 42.453, - "b3738" => 30.303, - "b3386" => 24.554, - "b3603" => 59.168, - "b2416" => 63.562, - "b0729" => 29.777, - "b0767" => 36.308, - "b3734" => 55.222, - "b4122" => 60.105, - "b2987" => 53.809, - "b2579" => 14.284, - "b0809" => 26.731, - "b1524" => 33.516, - "b3612" => 56.194, - "b3735" => 19.332, - "b3731" => 15.068, - "b1817" => 35.048, - "b1603" => 54.623, - "b1773" => 30.81, - "b4090" => 16.073, - "b0114" => 99.668, - "b3962" => 51.56, - "b2464" => 35.659, - "b2976" => 80.489, - "b1818" => 27.636, - "b2285" => 18.59, - "b1702" => 87.435, - "b1849" => 42.434, - "b1812" => 50.97, - "b0902" => 28.204, - "b3403" => 59.643, - "b1612" => 60.299, - "b1854" => 51.357, - "b0811" => 27.19, - "b0721" => 14.299, - "b2914" => 22.86, - "b1297" => 53.177, - "b0723" => 64.422, - "b3919" => 26.972, - "b3115" => 43.384, - "b4077" => 47.159, - "b3528" => 45.436, - "b0351" => 33.442, - "b2029" => 51.481, - "b1819" => 30.955, - "b0728" => 41.393, - "b2935" => 72.212, - "b2415" => 9.119, - "b0727" => 44.011, - "b0116" => 50.688, - "b0485" => 32.903, - "b3736" => 17.264, - "b0008" => 35.219, - "b3212" => 163.297, - "b3870" => 51.904, - "b4014" => 60.274, - "b2280" => 19.875, - "b2133" => 64.612, - "b2278" => 66.438, - "b0118" => 93.498, - "b2288" => 16.457, - "b3739" => 13.632, - "b3916" => 34.842, - "b3952" => 32.43, - "b2925" => 39.147, - "b2465" => 73.043, - "b2297" => 77.172, - "b2417" => 18.251, - "b4395" => 24.065, - "b3956" => 99.063, - "b0722" => 12.868, - "b2779" => 45.655, - "b0115" => 66.096, - "b0733" => 58.205, - "b1478" => 35.38, - "b2492" => 30.565, - "b0724" => 26.77, - "b0755" => 28.556, - "b1136" => 45.757, - "b2286" => 68.236, - "b0978" => 57.92, - "b1852" => 55.704, - "b2281" => 20.538, - "b2587" => 47.052, - "b2458" => 36.067, - "b0904" => 30.991, - "b1101" => 50.677, - "b0875" => 23.703, - "b3213" => 52.015, - "b2975" => 58.92, - "b0720" => 48.015, - "b0903" => 85.357, - "b1723" => 32.456, - "b2097" => 38.109, - "b3737" => 8.256, - "b0810" => 24.364, - "b4025" => 61.53, - "b1380" => 36.535, - "b0356" => 39.359, - "b2277" => 56.525, - "b1276" => 97.677, - "b4152" => 15.015, - "b1479" => 63.197, - "b4153" => 27.123, - "b4151" => 13.107, - "b2287" => 25.056, - "b0474" => 23.586, - "b2284" => 49.292, - "b1611" => 50.489, - "b0726" => 105.062, - "b2279" => 10.845, - "s0001" => 0.0, -) - -const ecoli_core_reaction_kcats = Dict( - #= - Data taken from Heckmann, David, et al. "Machine learning applied to enzyme - turnover numbers reveals protein structural correlates and improves metabolic - models." Nature communications 9.1 (2018): 1-10. - =# - "ACALD" => 568.11, - "PTAr" => 1171.97, - "ALCD2x" => 75.95, - "PDH" => 529.76, - "MALt2_2" => 234.03, - "CS" => 113.29, - "PGM" => 681.4, - "TKT1" => 311.16, - "ACONTa" => 191.02, - "GLNS" => 89.83, - "ICL" => 17.45, - "FBA" => 373.42, - "FORt2" => 233.93, - "G6PDH2r" => 589.37, - "AKGDH" => 264.48, - "TKT2" => 467.42, - "FRD7" => 90.20, - "SUCOAS" => 18.49, - "ICDHyr" => 39.62, - "AKGt2r" => 234.99, - "GLUSy" => 33.26, - "TPI" => 698.30, - "FORt" => 234.38, - "ACONTb" => 159.74, - "GLNabc" => 233.80, - "RPE" => 1772.485, - "ACKr" => 554.61, - "THD2" => 24.73, - "PFL" => 96.56, - "RPI" => 51.77, - "D_LACt2" => 233.51, - "TALA" => 109.05, - "PPCK" => 218.42, - "PGL" => 2120.42, - "NADTRHD" => 186.99, - "PGK" => 57.64, - "LDH_D" => 31.11, - "ME1" => 487.01, - "PIt2r" => 233.86, - "ATPS4r" => 71.42, - "GLCpts" => 233.90, - "GLUDy" => 105.32, - "CYTBD" => 153.18, - "FUMt2_2" => 234.37, - "FRUpts2" => 234.19, - "GAPD" => 128.76, - "PPC" => 165.52, - "NADH16" => 971.74, - "PFK" => 1000.46, - "MDH" => 25.93, - "PGI" => 468.11, - "ME2" => 443.09, - "GND" => 240.12, - "SUCCt2_2" => 234.18, - "GLUN" => 44.76, - "ADK1" => 111.64, - "SUCDi" => 680.31, - "ENO" => 209.35, - "MALS" => 252.75, - "GLUt2r" => 234.22, - "PPS" => 706.14, - "FUM" => 1576.83, -) From 33a64f4a213119cd1feee8fefa227a127291c569 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 23:01:59 +0100 Subject: [PATCH 530/531] finish cleaning up the data sourcing --- .../examples/05-enzyme-constrained-models.jl | 27 +++++++++---------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index 7b5abaafa..f81bc69c0 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -46,7 +46,7 @@ model = load_model("e_coli_core.json") # be used to estimate these parameters. #md # ```@raw html -#md #
Reaction turnover numbers +#md #
Data for reaction turnover numbers #md # ``` # This data is taken from: *Heckmann, David, et al. "Machine learning applied # to enzyme turnover numbers reveals protein structural correlates and improves @@ -119,6 +119,10 @@ const ecoli_core_reaction_kcats = Dict( #md #
#md # ``` +# We have these here: + +ecoli_core_reaction_kcats + # Each reaction in a constraint-based model usually has gene reaction rules # associated with it. These typically take the form of, possibly multiple, # isozymes that can catalyze a reaction. A turnover number needs to be assigned @@ -149,7 +153,7 @@ end # found in uniprot. #md # ```@raw html -#md #
Reaction turnover numbers +#md #
Gene product masses #md # ``` # This data is downloaded from Uniprot for E. coli K12, gene mass in kDa. To # obtain these data yourself, go to [Uniprot](https://www.uniprot.org/) and @@ -298,26 +302,19 @@ const ecoli_core_gene_product_masses = Dict( #md #
#md # ``` +# We have the molar masses here: -gene_product_molar_masses = ecoli_core_gene_product_masses +ecoli_core_gene_product_masses -#!!! warning "Molar mass units" -# Take care with the units of the molar masses. In literature they are -# usually reported in Da or kDa (g/mol). However, as noted above, flux -# units are typically mmol/gDW/h. Since the enzyme kinetic equation is -# `v = k * e`, where `k` is the turnover number, it suggests that the -# enzyme variable will have units of mmol/gDW. The molar masses come -# into play when setting the capacity limitations, e.g. usually a sum -# over all enzymes weighted by their molar masses: `e * mm`. Thus, if -# your capacity limitation has units of g/gDW, then the molar masses -# must have units of g/mmol (= kDa). +#md # !!! warning "Molar mass units" +#md # Take care with the units of the molar masses. In literature they are usually reported in Da or kDa (g/mol). However, as noted above, flux units are typically mmol/gDW/h. Since the enzyme kinetic equation is `v = k * e`, where `k` is the turnover number, it suggests that the enzyme variable will have units of mmol/gDW. The molar masses come into play when setting the capacity limitations, e.g. usually a sum over all enzymes weighted by their molar masses: `e * mm`. Thus, if your capacity limitation has units of g/gDW, then the molar masses must have units of g/mmol (= kDa). ### Capacity limitation # The capacity limitation usually denotes an upper bound of protein available to # the cell. -total_enzyme_capacity = 50.0 # mg enzyme/gDW +total_enzyme_capacity = 50.0 # mg of enzyme/gDW ### Running a basic enzyme constrained model @@ -327,7 +324,7 @@ total_enzyme_capacity = 50.0 # mg enzyme/gDW ec_solution = enzyme_constrained_flux_balance_analysis( model; reaction_isozymes, - gene_product_molar_masses, + gene_product_molar_masses = ecoli_core_gene_product_masses, capacity = total_enzyme_capacity, optimizer = Tulip.Optimizer, settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], From 2c47c0fc52fcd34892c21ce1434b4fc52aae4125 Mon Sep 17 00:00:00 2001 From: Mirek Kratochvil Date: Mon, 29 Jan 2024 23:02:18 +0100 Subject: [PATCH 531/531] avoid having tulip by default --- docs/src/examples/02-flux-balance-analysis.jl | 6 +++--- docs/src/examples/02c-constraint-modifications.jl | 11 ++++------- docs/src/examples/05-enzyme-constrained-models.jl | 7 +++---- 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/docs/src/examples/02-flux-balance-analysis.jl b/docs/src/examples/02-flux-balance-analysis.jl index 87d9f2f89..7527c8698 100644 --- a/docs/src/examples/02-flux-balance-analysis.jl +++ b/docs/src/examples/02-flux-balance-analysis.jl @@ -29,10 +29,10 @@ download_model( ) # Additionally to COBREXA and the model format package, we will need a solver -# -- let's use Tulip here: +# -- let's use GLPK here: import JSONFBCModels -import Tulip +import GLPK model = load_model("e_coli_core.json") @@ -43,7 +43,7 @@ model = load_model("e_coli_core.json") # is captured in the default behavior of function # [`flux_balance_analysis`](@ref): -solution = flux_balance_analysis(model, Tulip.Optimizer) +solution = flux_balance_analysis(model, GLPK.Optimizer) @test isapprox(solution.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/02c-constraint-modifications.jl b/docs/src/examples/02c-constraint-modifications.jl index 8344854f0..4495771e0 100644 --- a/docs/src/examples/02c-constraint-modifications.jl +++ b/docs/src/examples/02c-constraint-modifications.jl @@ -40,7 +40,7 @@ download_model( ) import JSONFBCModels -import Tulip +import GLPK model = load_model("e_coli_core.json") @@ -61,8 +61,7 @@ forced_mixed_fermentation = vt = optimized_constraints( forced_mixed_fermentation, objective = forced_mixed_fermentation.objective.value, - optimizer = Tulip.Optimizer, - settings = [silence], + optimizer = GLPK.Optimizer, ) @test isapprox(vt.objective, 0.6337, atol = TEST_TOLERANCE) #src @@ -77,8 +76,7 @@ ctmodel.fluxes.ATPM.bound = C.Between(1000.0, 10000.0) vt = optimized_constraints( ctmodel, objective = ctmodel.objective.value, - optimizer = Tulip.Optimizer, - settings = [silence], + optimizer = GLPK.Optimizer, ) @test isnothing(vt) #src @@ -89,8 +87,7 @@ ctmodel.fluxes.ATPM.bound = C.Between(8.39, 10000.0) # revert vt = optimized_constraints( ctmodel, objective = ctmodel.objective.value, - optimizer = Tulip.Optimizer, - settings = [silence], + optimizer = GLPK.Optimizer, ) @test isapprox(vt.objective, 0.8739, atol = TEST_TOLERANCE) #src diff --git a/docs/src/examples/05-enzyme-constrained-models.jl b/docs/src/examples/05-enzyme-constrained-models.jl index f81bc69c0..358ef9d03 100644 --- a/docs/src/examples/05-enzyme-constrained-models.jl +++ b/docs/src/examples/05-enzyme-constrained-models.jl @@ -27,11 +27,11 @@ import Downloads: download download("http://bigg.ucsd.edu/static/models/e_coli_core.json", "e_coli_core.json") # Additionally to COBREXA and the model format package, we will need a solver -# -- let's use Tulip here: +# -- let's use GLPK here: import AbstractFBCModels as A import JSONFBCModels -import Tulip +import GLPK model = load_model("e_coli_core.json") @@ -326,8 +326,7 @@ ec_solution = enzyme_constrained_flux_balance_analysis( reaction_isozymes, gene_product_molar_masses = ecoli_core_gene_product_masses, capacity = total_enzyme_capacity, - optimizer = Tulip.Optimizer, - settings = [set_optimizer_attribute("IPM_IterationsLimit", 10_000)], + optimizer = GLPK.Optimizer, ) #src these values should be unique (glucose transporter is the only way to get carbon into the system)