diff --git a/Project.toml b/Project.toml index dbd20d8..058c84b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,15 +1,18 @@ name = "ConstraintTrees" uuid = "5515826b-29c3-47a5-8849-8513ac836620" authors = ["The developers of ConstraintTrees.jl"] -version = "1.1.0" +version = "1.2.0" [deps] +ConstructionBase = "187b0558-2788-49d3-abe0-74a17ed4e7c9" DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] +Accessors = "0.1" Clarabel = "0.6" +ConstructionBase = "1.5" DataStructures = "0.18" DocStringExtensions = "0.8, 0.9" GLPK = "1" @@ -19,6 +22,7 @@ SCIP = "0.11" julia = "1" [extras] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" Downloads = "f43a241f-c20a-4ad4-852c-f6b1247861c6" GLPK = "60bf3e95-4087-53dc-ae20-288a0d20c6a6" @@ -28,4 +32,4 @@ SCIP = "82193955-e24f-5292-bf16-6f2c5261a85f" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["Clarabel", "Downloads", "GLPK", "JuMP", "SBML", "SCIP", "Test"] +test = ["Accessors", "Clarabel", "Downloads", "GLPK", "JuMP", "SBML", "SCIP", "Test"] diff --git a/docs/Project.toml b/docs/Project.toml index 52e1f8c..62a8b39 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] +Accessors = "7d9f7c33-5ae7-4f3b-8dc6-eff91059b697" Clarabel = "61c947e1-3e6d-4ee4-985a-eec8c727bd6e" DocStringExtensions = "ffbed154-4ef7-542d-bbb7-c09d3a79fcae" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" diff --git a/docs/src/1-metabolic-modeling.jl b/docs/src/1-metabolic-modeling.jl index c2f05f1..f685c68 100644 --- a/docs/src/1-metabolic-modeling.jl +++ b/docs/src/1-metabolic-modeling.jl @@ -357,14 +357,15 @@ Dict(k => v.fluxes.R_BIOMASS_Ecoli_core_w_GAM for (k, v) in result.community) # change at a single place of the tree may easily change values also in other # parts of any trees, including completely different trees # - the "convenient way" of making sure that the above problem never happens is -# to deep-copy the whole tree structure, which is typically quite detrimental -# to memory use and program efficiency +# to copy-on-write the whole tree structure, which is typically quite +# detrimental to memory use and program efficiency # #md # !!! danger "Rules of thumb for safe use of in-place modification" #md # Only use the in-place modifications if: #md # - there is code that explicitly makes sure there is no false sharing via references, e.g. using a deep copy #md # - the in-place modifications are the last thing happening to the constraint tree before it is used by the solver #md # - the in-place modification code is not a part of a re-usable library +#md # - you are using a suitable wrapper interface such as [Accessors.jl](https://github.com/JuliaObjects/Accessors.jl) # # Now, if you are completely sure that ignoring the robustness guidelines will # help your code, you can do the in-place tree modifications quite easily using @@ -391,6 +392,39 @@ result.exchanges @test result_with_more_oxygen.exchanges.oxygen < -19.0 #src +# ### Alternative: Using Accessors.jl +# +# Accessors.jl implement a "lensy" way to update immutable data structures. +# That comes with a nice outcome of doing the right amount of shallow copyies +# for you automatically, thus avoiding much of the technical danger of in-place +# modifications. (You still lose the equational reasoning on your code, but that +# may not be an issue at all in usual codebases.) +# +# Accessors interface is used simply through macros `@set` (which sets a deeply +# nested field in a structure, returning a modified copy), or with `@reset` +# which automatically "assigns" the result back to the original variable: + +using Accessors + +c = @set c.exchanges.biomass.bound = C.Between(-50, 50) + +# The above code is equivalent to: + +@reset c.exchanges.biomass.bound = C.Between(-50, 50) + +# ...and it is also possible to use string and symbol indexes to pick the +# individual tree items: + +@reset c[:exchanges]["biomass"].bound = C.Between(-50, 50) + +# All of these operations give us: + +c.exchanges.biomass + +@test typeof(c.exchanges.biomass.bound) == C.Between #src +@test c.exchanges.biomass.bound.lower == -50 #src +@test c.exchanges.biomass.bound.upper == 50 #src + # ## Seeing the differences between the results # # ConstraintTrees.jl defines its own version of `zip` function that can apply a diff --git a/src/tree.jl b/src/tree.jl index 8ad2e83..acd4b67 100644 --- a/src/tree.jl +++ b/src/tree.jl @@ -13,6 +13,7 @@ # See the License for the specific language governing permissions and # limitations under the License. +import ConstructionBase import DataStructures: SortedDict, SortedSet """ @@ -75,10 +76,15 @@ Base.values(x::Tree) = values(elems(x)) Base.getindex(x::Tree, sym::Symbol) = getindex(elems(x), sym) Base.getindex(x::Tree, str::String) = getindex(x, Symbol(str)) -Base.setindex!(x::Tree{X}, val::E, sym::Symbol) where {X,E<:X} = +Base.setindex!(x::Tree{X}, val::E, sym::Symbol) where {X,E<:Union{X,Tree{X}}} = setindex!(elems(x), val, sym) Base.setindex!(x::Tree, val, str::String) = setindex!(x, val, Symbol(str)) +Base.setindex(x::Tree{X}, val::E, sym::Symbol) where {X,E<:Union{X,Tree{X}}} = + Tree{X}(elems(x)..., sym => val) +Base.setindex(x::Tree{X}, val::E, str::String) where {X,E<:Union{X,Tree{X}}} = + Base.setindex(x, val, Symbol(str)) + Base.delete!(x::Tree, sym::Symbol) = delete!(elems(x), sym) Base.delete!(x::Tree, str::String) = delete!(x, Symbol(str)) @@ -88,9 +94,12 @@ Base.hasproperty(x::Tree, sym::Symbol) = haskey(x, sym) Base.getproperty(x::Tree, sym::Symbol) = elems(x)[sym] -Base.setproperty!(x::Tree{X}, sym::Symbol, val::E) where {X,E<:X} = +Base.setproperty!(x::Tree{X}, sym::Symbol, val::E) where {X,E<:Union{X,Tree{X}}} = setindex!(elems(x), val, sym) +ConstructionBase.setproperties(x::Tree{T}, props::NamedTuple) where {T} = + Tree{T}(elems(x)..., pairs(props)...) + # # Algebraic construction #