Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

small improvements #10

Merged
merged 5 commits into from
Dec 9, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ConstraintTrees"
uuid = "5515826b-29c3-47a5-8849-8513ac836620"
authors = ["The developers of ConstraintTrees.jl"]
version = "0.5.0"
version = "0.6.0"

[deps]
DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
Expand Down
22 changes: 12 additions & 10 deletions docs/src/metabolic-modeling.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@
#
# In this example we demonstrate the use of `ConstraintTree` structure for
# solving the metabolic modeling tasks. At the same time, we show how to export
# the structure to JuMP, and use `ValueTree` to find useful information
# about the result.
# the structure to JuMP, and use value trees to find useful information about
# the result.
#
# First, let's import some packages:

Expand Down Expand Up @@ -198,7 +198,7 @@ c *=
solution = [1.0, 5.0] # corresponds to :x and :y in order given in `variables`

# A value tree for this solution is constructed in a straightforward manner:
st = C.ValueTree(system, solution)
st = C.constraint_values(system, solution)

# We can now check the values of the original coordinates
st.original_coords
Expand All @@ -217,8 +217,8 @@ st.transformed_coords
#
# We can make a small function that throws our model into JuMP, optimizes it,
# and gives us back a variable assignment vector. This vector can then be used
# to determine and browse the values of constraints and variables using
# `ValueTree`.
# to determine and browse the values of constraints and variables using a
# `Float64`-valued tree.
import JuMP
function optimized_vars(cs::C.ConstraintTree, objective::C.LinearValue, optimizer)
model = JuMP.Model(optimizer)
Expand Down Expand Up @@ -249,7 +249,7 @@ optimal_variable_assignment = optimized_vars(c, c.objective.value, GLPK.Optimize

# To explore the solution more easily, we can make a tree with values that
# correspond to ones in our constraint tree:
result = C.ValueTree(c, optimal_variable_assignment)
result = C.constraint_values(c, optimal_variable_assignment)

result.fluxes.R_BIOMASS_Ecoli_core_w_GAM

Expand All @@ -259,11 +259,11 @@ result.fluxes.R_PFK

# Sometimes it is unnecessary to recover the values for all constraints, so we
# are better off selecting just the right subtree:
C.ValueTree(c.fluxes, optimal_variable_assignment)
C.constraint_values(c.fluxes, optimal_variable_assignment)

#

C.ValueTree(c.objective, optimal_variable_assignment)
C.constraint_values(c.objective, optimal_variable_assignment)

# ## Combining and extending constraint systems
#
Expand Down Expand Up @@ -307,7 +307,8 @@ c *=
)

# Let's see how much biomass are the two species capable of producing together:
result = C.ValueTree(c, optimized_vars(c, c.exchanges.biomass.value, GLPK.Optimizer))
result =
C.constraint_values(c, optimized_vars(c, c.exchanges.biomass.value, GLPK.Optimizer))
result.exchanges

# Finally, we can iterate over all species in the small community and see how
Expand Down Expand Up @@ -358,7 +359,8 @@ c[:exchanges][:production_is_zero] = C.Constraint(c.exchanges.biomass.value, 0.0
delete!(c.exchanges, :production_is_zero)

# In the end, the flux optimization yields an expectably different result:
result = C.ValueTree(c, optimized_vars(c, c.exchanges.biomass.value, GLPK.Optimizer))
result =
C.constraint_values(c, optimized_vars(c, c.exchanges.biomass.value, GLPK.Optimizer))
result.exchanges

@test result.exchanges.oxygen < -19.0 #src
14 changes: 7 additions & 7 deletions docs/src/quadratic-optimization.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,8 @@
#
# ## Working with quadratic values and constraints
#
# Algebraically, you can construct `QuadraticValue`s simply by multiplying the linear
# `LinearValue`s:
# Algebraically, you can construct `QuadraticValue`s simply by multiplying the
# linear `LinearValue`s:

import ConstraintTrees as C

Expand All @@ -23,9 +23,9 @@ qv = system.x.value * (system.y.value + 2 * system.z.value)
@test qv.idxs == [(1, 2), (1, 3)] #src
@test qv.weights == [1.0, 2.0] #src

# As with `LinearValue`s, the `QuadraticValue`s can be easily combined, giving a nice way to
# specify e.g. weighted sums of squared errors with respect to various
# directions. We can thus represent common formulas for error values:
# As with `LinearValue`s, the `QuadraticValue`s can be easily combined, giving
# a nice way to specify e.g. weighted sums of squared errors with respect to
# various directions. We can thus represent common formulas for error values:
error_val =
C.squared(system.x.value + system.y.value - 1) +
C.squared(system.y.value + 5 * system.z.value - 3)
Expand All @@ -40,7 +40,7 @@ system = :vars^system * :error^C.Constraint(value = error_val, bound = (0.0, 100
# Let's pretend someone has solved the system, and see how much "error" the
# solution has:
solution = [1.0, 2.0, -1.0]
st = C.ValueTree(system, solution)
st = C.constraint_values(system, solution)
st.error

# ...not bad for a first guess.
Expand Down Expand Up @@ -125,7 +125,7 @@ end
# We can now load a suitable optimizer and solve the system by maximizing the
# negative squared error:
import Clarabel
st = C.ValueTree(s, optimized_vars(s, -s.objective.value, Clarabel.Optimizer))
st = C.constraint_values(s, optimized_vars(s, -s.objective.value, Clarabel.Optimizer))

# If the optimization worked well, we can nicely get out the position of the
# closest point to the line that is in the elliptical area:
Expand Down
5 changes: 5 additions & 0 deletions docs/src/reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,11 @@

## Values

```@autodocs
Modules = [ConstraintTrees]
Pages = ["src/value.jl"]
```

### Linear and affine values

```@autodocs
Expand Down
7 changes: 4 additions & 3 deletions src/ConstraintTrees.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,8 +32,9 @@ The package is structured as follows:
-- this forms the basis of the "tidy" algebra of constraints.
- A variable assignment, which is typically the "solution" for a given
constraint tree, can be combined with a [`ConstraintTree`](@ref) to create a
[`ValueTree`](@ref), which enables browsing of the optimization results in
the very same structure as the input [`ConstraintTree`](@ref).
"value tree" via [`constraint_values`](@ref), which enables browsing of the
optimization results in the very same structure as the input
[`ConstraintTree`](@ref).

You can follow the examples in documentation and the docstrings of package
contents for more details.
Expand All @@ -42,13 +43,13 @@ module ConstraintTrees

using DocStringExtensions

include("value.jl")
include("linear_value.jl")
include("quadratic_value.jl")
include("bound.jl")
include("constraint.jl")
include("tree.jl")
include("constraint_tree.jl")
include("value_tree.jl")
include("pretty.jl")

end # module ConstraintTrees
17 changes: 11 additions & 6 deletions src/constraint.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,17 +14,14 @@ becomes easily accessible for inspection and building other constraints.
# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef mutable struct Constraint{Value}
Base.@kwdef mutable struct Constraint{V}
"A value (typically a [`LinearValue`](@ref) or a [`QuadraticValue`](@ref))
that describes what the constraint constraints."
value::Value
value::V
"A bound that the `value` must satisfy."
bound::Bound = nothing

function Constraint(
v::T,
b::Bound = nothing,
) where {T<:Union{LinearValue,QuadraticValue}}
function Constraint(v::T, b::Bound = nothing) where {T<:Value}
new{T}(v, b)
end
end
Expand Down Expand Up @@ -57,3 +54,11 @@ Simple accessor for getting out the bound from the constraint that can be used
for broadcasting (as opposed to the dot-field access).
"""
bound(x::Constraint) = x.bound

"""
$(TYPEDSIGNATURES)

Substitute anything vector-like as variables into the constraint's value,
producing a constraint with the new value.
"""
substitute(x::Constraint, y) = Constraint(substitute(x.value, y), x.bound)
21 changes: 21 additions & 0 deletions src/constraint_tree.jl
Original file line number Diff line number Diff line change
Expand Up @@ -202,3 +202,24 @@ function variables(; keys::Vector{Symbol}, bounds = nothing)
((i, k), b) in zip(enumerate(keys), bs)
)
end

#
# Transforming the constraint trees
#

"""
$(TYPEDSIGNATURES)

Substitute variable values from `y` into the constraint tree's constraint's
values, getting a tree of "solved" constraint values for the given variable
assignment.
"""
constraint_values(x::ConstraintTree, y::Vector{Float64}) =
tree_map(x, c -> substitute(value(c), y), Float64)

"""
$(TYPEDSIGNATURES)

Fallback for [`constraint_values`](@ref) for a single constraint.
"""
constraint_values(x::Constraint, y::Vector{Float64}) = substitute(value(x), y)
10 changes: 5 additions & 5 deletions src/linear_value.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Multiplying two `LinearValue`s yields a quadratic form (in a [`QuadraticValue`](
# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct LinearValue
Base.@kwdef struct LinearValue <: Value
"""
Indexes of the variables used by the value. The indexes must always be
sorted in strictly increasing order. The affine element has index 0.
Expand Down Expand Up @@ -82,17 +82,17 @@ end
"""
$(TYPEDSIGNATURES)

Substitute anything vector-like as variable values into a [`LinearValue`](@ref) and
return the result.
Substitute anything vector-like as variable values into a [`LinearValue`](@ref)
and return the result.
"""
substitute(x::LinearValue, y) =
sum(idx == 0 ? x.weights[i] : x.weights[i] * y[idx] for (i, idx) in enumerate(x.idxs))

"""
$(TYPEDSIGNATURES)

Shortcut for making a [`LinearValue`](@ref) out of a linear combination defined by
the `SparseVector`.
Shortcut for making a [`LinearValue`](@ref) out of a linear combination defined
by the `SparseVector`.
"""
LinearValue(x::SparseVector{Float64}) =
let (idxs, weights) = findnz(x)
Expand Down
2 changes: 1 addition & 1 deletion src/quadratic_value.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ The cleanest way to construct a `QuadraticValue` is to multiply two [`LinearValu
# Fields
$(TYPEDFIELDS)
"""
Base.@kwdef struct QuadraticValue
Base.@kwdef struct QuadraticValue <: Value
"""
Indexes of variable pairs used by the value. The indexes must always be
sorted in strictly co-lexicographically increasing order, and the second
Expand Down
15 changes: 15 additions & 0 deletions src/tree.jl
Original file line number Diff line number Diff line change
Expand Up @@ -86,3 +86,18 @@ Base.merge(a::Base.Callable, d::Tree, others::Tree...) = mergewith(a, d, others.
function Base.mergewith(a::Base.Callable, d::Tree{X}, others::Tree...) where {X}
Tree{X}(elems = mergewith(a, elems(d), elems.(others)...))
end

#
# Transforming trees
#

"""
$(TYPEDSIGNATURES)

Run a function over everything in the tree. The resulting tree will contain
elements of type `output_type`. (This needs to be specified explicitly, because
the typesystem generally cannot guess the type correctly.)
"""
tree_map(x::Tree, f, output_type::DataType) = Tree{output_type}(
k => (v isa Tree ? tree_map(v, f, output_type) : f(v)) for (k, v) in x
)
7 changes: 7 additions & 0 deletions src/value.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@

"""
$(TYPEDEF)

Abstract type of all values usable in constraints, including [`LinearValue`](@ref) and [`QuadraticValue`](@ref).
"""
abstract type Value end
48 changes: 0 additions & 48 deletions src/value_tree.jl

This file was deleted.

18 changes: 12 additions & 6 deletions test/misc.jl
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,16 @@ end
@test C.bound(2 * -convert(C.Constraint, (C.variable(bound = 123.0))) / 2) == -123.0

x = C.variable().value
s = :a^C.Constraint(x) + :b^C.Constraint(x * x - x)
s = :a^C.Constraint(x, 5.0) + :b^C.Constraint(x * x - x, (4.0, 6.0))
@test C.value(s.a).idxs == [1]
@test C.value(s.b).idxs == [(0, 2), (2, 2)]
vars = [C.LinearValue([1], [1.0]), C.LinearValue([2], [1.0])]
@test C.substitute(s.a, vars).bound == s.a.bound
@test C.substitute(s.a, vars).value.idxs == s.a.value.idxs
@test C.substitute(s.a, vars).value.weights == s.a.value.weights
@test C.substitute(s.b, vars).bound == s.b.bound
@test C.substitute(s.b, vars).value.idxs == s.b.value.idxs
@test C.substitute(s.b, vars).value.weights == s.b.value.weights
end

@testset "Constraint tree operations" begin
Expand All @@ -66,11 +73,10 @@ end
@testset "Solution tree operations" begin
ct = C.variables(keys = [:a, :b])

@test_throws BoundsError C.ValueTree(ct, [1.0])
st = C.ValueTree(ct, [123.0, 321.0])
@test_throws BoundsError C.constraint_values(ct, [1.0])
st = C.constraint_values(ct, [123.0, 321.0])

@test isempty(C.ValueTree())
@test isempty(C.ValueTree(C.ConstraintTree(), Float64[]))
@test isempty(C.constraint_values(C.ConstraintTree(), Float64[]))
@test !isempty(st)
@test haskey(st, :a)
@test hasproperty(st, :a)
Expand All @@ -85,7 +91,7 @@ end
@test collect(keys(st)) == [:a, :b]
@test sum([v for (_, v) in st]) == 444.0
@test sum(values(st)) == 444.0
@test eltype(st) == Pair{Symbol,C.ValueTreeElem}
@test eltype(st) == Pair{Symbol,Union{C.Tree{Float64},Float64}}
end

@testset "Pretty-printing" begin
Expand Down