From 888545726172ec0f54305e5c7310d55bb1270925 Mon Sep 17 00:00:00 2001 From: "St. Elmo Wilken" Date: Fri, 10 Nov 2023 11:52:43 +0100 Subject: [PATCH 01/24] 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 02/24] 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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 12/24] 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 13/24] 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 14/24] 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 15/24] 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 16/24] 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 17/24] 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 23/24] 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 24/24] 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") #], ], )