From 5638e3d7d9f23ecd942e79c2dda58b0c018bd6c1 Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Sun, 11 Aug 2019 14:11:04 +0100 Subject: [PATCH 1/9] initial tree interface --- Project.toml | 1 + src/Convex.jl | 1 + src/utilities/show.jl | 22 ++++++++++++++++++++-- src/utilities/tree_interface.jl | 28 ++++++++++++++++++++++++++++ 4 files changed, 50 insertions(+), 2 deletions(-) create mode 100644 src/utilities/tree_interface.jl diff --git a/Project.toml b/Project.toml index d6bb9d313..88338442b 100644 --- a/Project.toml +++ b/Project.toml @@ -3,6 +3,7 @@ uuid = "f65535da-76fb-5f13-bab9-19810c17039a" version = "0.12.4" [deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" MathProgBase = "fdba3010-5040-5b88-9595-932c9decdf73" OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" diff --git a/src/Convex.jl b/src/Convex.jl index 64dea6779..f6e484859 100644 --- a/src/Convex.jl +++ b/src/Convex.jl @@ -80,6 +80,7 @@ include("atoms/exp_+_sdp_cone/logdet.jl") ### utilities include("utilities/show.jl") +include("utilities/tree_interface.jl") include("utilities/iteration.jl") include("utilities/broadcast.jl") diff --git a/src/utilities/show.jl b/src/utilities/show.jl index 5cc5df055..116d4f60e 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -1,5 +1,23 @@ -import Base.show -export show +import Base.show, Base.summary +export show, summary + + +function Base.summary(io::IO, x::Variable) + sgn = sign(x) == ComplexSign() ? " complex " : " " + cst = vexity(x) == ConstVexity() ? " (fixed)" : "" + if size(x) == (1,1) + if sign(x) == ComplexSign() + print(io, "complex variable$(cst)") + else + print(io, "variable$(cst)") + end + elseif size(x,2) == 1 + print(io, "$(size(x,1))-element$(sgn)variable$(cst)") + else + print(io, "$(size(x,1))×$(size(x,2))$(sgn)variable$(cst)") + + end +end # A Constant is simply a wrapper around a native Julia constant # Hence, we simply display its value diff --git a/src/utilities/tree_interface.jl b/src/utilities/tree_interface.jl new file mode 100644 index 000000000..f3753a951 --- /dev/null +++ b/src/utilities/tree_interface.jl @@ -0,0 +1,28 @@ +import AbstractTrees + +function AbstractTrees.children(p::Problem) + return (p.objective, p.constraints) +end + +function AbstractTrees.children(e::AbstractExpr) + return e.children +end + +AbstractTrees.children(v::Variable) = () +AbstractTrees.children(c::Constant) = () + +AbstractTrees.children(C::Constraint) = (C.lhs, C.rhs) + +AbstractTrees.printnode(io::IO, node::AbstractExpr) = print(io,node.head) +AbstractTrees.printnode(io::IO, node::Constraint) = print(io,node.head) + +AbstractTrees.printnode(io::IO, node::Vector{Constraint}) = print(io, "constraints") + + +AbstractTrees.printnode(io::IO, node::Problem) = print(io,node.head) + +AbstractTrees.printnode(io::IO, node::Constant) = show(IOContext(io, :compact => true), node.value) + +AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) + + From 39f0f9b46fa7fbc004477115f391ab2b52828dff Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Sun, 1 Sep 2019 04:04:33 +0100 Subject: [PATCH 2/9] `show` via trees --- src/utilities/show.jl | 127 +++++++++++++++++--------------- src/utilities/tree_interface.jl | 31 +++++--- 2 files changed, 88 insertions(+), 70 deletions(-) diff --git a/src/utilities/show.jl b/src/utilities/show.jl index 116d4f60e..f23edc387 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -1,29 +1,51 @@ import Base.show, Base.summary export show, summary +import AbstractTrees function Base.summary(io::IO, x::Variable) - sgn = sign(x) == ComplexSign() ? " complex " : " " + hash_str = string(x.id_hash) + hash_str = hash_str[1:nextind(hash_str, 3)] + sgn = summary(sign(x)) cst = vexity(x) == ConstVexity() ? " (fixed)" : "" + cst = cst * " (id: " * hash_str * "...)" if size(x) == (1,1) - if sign(x) == ComplexSign() - print(io, "complex variable$(cst)") - else - print(io, "variable$(cst)") - end + print(io, "$(sgn) variable$(cst)") elseif size(x,2) == 1 - print(io, "$(size(x,1))-element$(sgn)variable$(cst)") + print(io, "$(size(x,1))-element $(sgn) variable$(cst)") else - print(io, "$(size(x,1))×$(size(x,2))$(sgn)variable$(cst)") + print(io, "$(size(x,1))×$(size(x,2)) $(sgn) variable$(cst)") end end +Base.summary(io::IO, ::AffineVexity) = print(io, "affine") +Base.summary(io::IO, ::ConvexVexity) = print(io, "convex") +Base.summary(io::IO, ::ConcaveVexity) = print(io, "concave") +Base.summary(io::IO, ::ConstVexity) = print(io, "constant") + +Base.summary(io::IO, ::Positive) = print(io, "positive") +Base.summary(io::IO, ::Negative) = print(io, "negative") +Base.summary(io::IO, ::NoSign) = print(io, "real") +Base.summary(io::IO, ::ComplexSign) = print(io, "complex") + +function Base.summary(io::IO, c::Constraint) + print(io, "$(c.head) constraint (") + summary(io, vexity(c)) + print(io, ")") +end + +function Base.summary(io::IO, e::AbstractExpr) + print(io, "$(e.head) (") + summary(io, vexity(e)) + print(io, "; ") + summary(io, sign(e)) + print(io, ")") +end + # A Constant is simply a wrapper around a native Julia constant # Hence, we simply display its value -function show(io::IO, x::Constant) - print(io, "$(x.value)") -end +show(io::IO, x::Constant) = print(io, x.value) # A variable, for example, Variable(3, 4), will be displayed as: # Variable of @@ -40,63 +62,46 @@ function show(io::IO, x::Variable) end end -# A constraint, for example, square(x) <= 4 will be displayed as: -# Constraint: -# <= constraint -# lhs: ... -# rhs: ... -function show(io::IO, c::Constraint) - print(io, """Constraint: - $(c.head) constraint - lhs: $(c.lhs) - rhs: $(c.rhs) - vexity: $(vexity(c))""") +struct ShowConstraint + constraint::Constraint end -# SDP constraints are displayed as: -# Constraint: -# sdp constraint -# expression: ... -function show(io::IO, c::SDPConstraint) - print(io, """Constraint: - $(c.head) constraint - expression: $(c.child) - """) +AbstractTrees.children(c::ShowConstraint) = AbstractTrees.children(c.constraint) +AbstractTrees.printnode(io::IO, c::ShowConstraint) = AbstractTrees.printnode(io, c.constraint) + +show(io::IO, c::Constraint) = AbstractTrees.print_tree(io, ShowConstraint(c)) + + + +struct ShowExpr + expr::AbstractExpr end -# An expression, for example, 2 * x, will be displayed as: -# AbstractExpr with -# head: * -# size: (1, 1) -# sign: NoSign() -# vexity: AffineVexity() -function show(io::IO, e::AbstractExpr) - print(io, """AbstractExpr with - head: $(e.head) - size: ($(e.size[1]), $(e.size[2])) - sign: $(sign(e)) - vexity: $(vexity(e)) - """) +AbstractTrees.children(c::ShowExpr) = AbstractTrees.children(c.expr) +AbstractTrees.printnode(io::IO, c::ShowExpr) = AbstractTrees.printnode(io, c.expr) + +show(io::IO, c::AbstractExpr) = AbstractTrees.print_tree(io, ShowExpr(c)) + + +struct ShowProblemObjective + head::Symbol + objective::AbstractExpr end -# A problem, for example, p = minimize(sum(x) + 3, [x >= 0, x >= 1, x <= 10]) -# will be displayed as follows: -# -# Problem: minimize `Expression` -# subject to -# Constraint: ... -# Constraint: ... -# Constraint: ... -# current status: not yet solved -# -# Once it is solved, the current status would look like: -# current status: solved with optimal value of 9.0 +AbstractTrees.children(p::ShowProblemObjective) = (p.objective,) +AbstractTrees.printnode(io::IO, p::ShowProblemObjective) = print(io,string(p.head)) + +struct ShowProblemConstraints + constraints::Vector{Constraint} +end + +AbstractTrees.children(p::ShowProblemConstraints) = tuple(p.constraints) +AbstractTrees.printnode(io::IO, p::ShowProblemConstraints) = print(io,"subject to") + + function show(io::IO, p::Problem) - print(io, """Problem: - $(p.head) $(p.objective) - subject to - """) - join(io, p.constraints, "\n\t\t") + AbstractTrees.print_tree(io, ShowProblemObjective(p.head, p.objective)) + AbstractTrees.print_tree(io, ShowProblemConstraints(p.constraints)) print(io, "\ncurrent status: $(p.status)") if p.status == "solved" print(io, " with optimal value of $(round(p.optval, digits=4))") diff --git a/src/utilities/tree_interface.jl b/src/utilities/tree_interface.jl index f3753a951..ad323a564 100644 --- a/src/utilities/tree_interface.jl +++ b/src/utilities/tree_interface.jl @@ -12,17 +12,30 @@ AbstractTrees.children(v::Variable) = () AbstractTrees.children(c::Constant) = () AbstractTrees.children(C::Constraint) = (C.lhs, C.rhs) - -AbstractTrees.printnode(io::IO, node::AbstractExpr) = print(io,node.head) -AbstractTrees.printnode(io::IO, node::Constraint) = print(io,node.head) - -AbstractTrees.printnode(io::IO, node::Vector{Constraint}) = print(io, "constraints") - +AbstractTrees.children(C::SDPConstraint) = (C.child,) +AbstractTrees.children(C::SOCConstraint) = C.children +AbstractTrees.children(C::SOCElemConstraint) = C.children +AbstractTrees.children(C::ExpConstraint) = C.children + +AbstractTrees.printnode(io::IO, node::AbstractExpr) = summary(io,node) +AbstractTrees.printnode(io::IO, node::Constraint) = summary(io, node) + +function AbstractTrees.printnode(io::IO, node::Vector{Constraint}) + if length(node) == 0 + print(io, "no constraints") + else + print(io, "constraints") + end +end AbstractTrees.printnode(io::IO, node::Problem) = print(io,node.head) -AbstractTrees.printnode(io::IO, node::Constant) = show(IOContext(io, :compact => true), node.value) +function AbstractTrees.printnode(io::IO, node::Constant) + if length(node.value) <= 3 + show(IOContext(io, :compact => true), node.value) + else + summary(io, node.value) + end +end AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) - - From 9f69c84497951098b5269e9f50dad5ea624c8df7 Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Sun, 1 Sep 2019 16:37:32 +0100 Subject: [PATCH 3/9] Fix redundant `constraints` in `show` --- src/utilities/show.jl | 6 ++++-- src/utilities/tree_interface.jl | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/utilities/show.jl b/src/utilities/show.jl index f23edc387..898c4cb53 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -95,13 +95,15 @@ struct ShowProblemConstraints constraints::Vector{Constraint} end -AbstractTrees.children(p::ShowProblemConstraints) = tuple(p.constraints) +AbstractTrees.children(p::ShowProblemConstraints) = p.constraints AbstractTrees.printnode(io::IO, p::ShowProblemConstraints) = print(io,"subject to") function show(io::IO, p::Problem) AbstractTrees.print_tree(io, ShowProblemObjective(p.head, p.objective)) - AbstractTrees.print_tree(io, ShowProblemConstraints(p.constraints)) + if !(isempty(p.constraints)) + AbstractTrees.print_tree(io, ShowProblemConstraints(p.constraints)) + end print(io, "\ncurrent status: $(p.status)") if p.status == "solved" print(io, " with optimal value of $(round(p.optval, digits=4))") diff --git a/src/utilities/tree_interface.jl b/src/utilities/tree_interface.jl index ad323a564..ee5510a0e 100644 --- a/src/utilities/tree_interface.jl +++ b/src/utilities/tree_interface.jl @@ -17,7 +17,7 @@ AbstractTrees.children(C::SOCConstraint) = C.children AbstractTrees.children(C::SOCElemConstraint) = C.children AbstractTrees.children(C::ExpConstraint) = C.children -AbstractTrees.printnode(io::IO, node::AbstractExpr) = summary(io,node) +AbstractTrees.printnode(io::IO, node::AbstractExpr) = summary(io, node) AbstractTrees.printnode(io::IO, node::Constraint) = summary(io, node) function AbstractTrees.printnode(io::IO, node::Vector{Constraint}) @@ -38,4 +38,4 @@ function AbstractTrees.printnode(io::IO, node::Constant) end end -AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) +AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) \ No newline at end of file From 7605ba52d68d0969648dbf4bce5513d54b95df6c Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Mon, 2 Sep 2019 16:16:06 +0100 Subject: [PATCH 4/9] Cleanup, add some comments, add tests --- src/utilities/show.jl | 125 ++++++++++++++++++++++++-------- src/utilities/tree_interface.jl | 12 +-- test/test_utilities.jl | 49 +++++++++++++ 3 files changed, 148 insertions(+), 38 deletions(-) diff --git a/src/utilities/show.jl b/src/utilities/show.jl index 898c4cb53..fba5f50d6 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -2,13 +2,45 @@ import Base.show, Base.summary export show, summary import AbstractTrees +""" + show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 4) -function Base.summary(io::IO, x::Variable) +Print a truncated version of the objects `id_hash` field. + +## Example + +```julia-repl +julia> x = Variable(); + +julia> Convex.show_id(stdout, x) +id: 6201... +``` +""" +show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 4) = print(io, show_id(x)) + +function show_id(x::Union{AbstractExpr, Constraint}; digits = 4) hash_str = string(x.id_hash) - hash_str = hash_str[1:nextind(hash_str, 3)] + hash_str = hash_str[1:nextind(hash_str, digits-1)] + return "id: " * hash_str * "..." +end + +""" + Base.summary(io::IO, x::Variable) + +Prints a one-line summary of a variable `x` to `io`. + +## Examples +```julia-repl +julia> x = ComplexVariable(3,2); + +julia> summary(stdout, x) +3×2 complex variable (id: 5455...) +``` +""" +function Base.summary(io::IO, x::Variable) sgn = summary(sign(x)) cst = vexity(x) == ConstVexity() ? " (fixed)" : "" - cst = cst * " (id: " * hash_str * "...)" + cst = cst * " (" * sprint(show_id, x) * ")" if size(x) == (1,1) print(io, "$(sgn) variable$(cst)") elseif size(x,2) == 1 @@ -48,62 +80,95 @@ end show(io::IO, x::Constant) = print(io, x.value) # A variable, for example, Variable(3, 4), will be displayed as: -# Variable of +# julia> Variable(3,4) +# Variable # size: (3, 4) -# sign: NoSign() -# vexity: AffineVexity() +# sign: real +# vexity: affine +# id: 7385... +# here, the `id` will change from run to run. function show(io::IO, x::Variable) - print(io, """Variable of - size: ($(x.size[1]), $(x.size[2])) - sign: $(x.sign) - vexity: $(x.vexity)""") + print(io, "Variable") + print(io, "\nsize: $(size(x))") + print(io, "\nsign: ") + summary(io, sign(x)) + print(io, "\nvexity: ") + summary(io, vexity(x)) + println(io) + show_id(io, x) if x.value !== nothing print(io, "\nvalue: $(x.value)") end end -struct ShowConstraint - constraint::Constraint -end - -AbstractTrees.children(c::ShowConstraint) = AbstractTrees.children(c.constraint) -AbstractTrees.printnode(io::IO, c::ShowConstraint) = AbstractTrees.printnode(io, c.constraint) +""" + print_tree_rstrip(io::IO, x) -show(io::IO, c::Constraint) = AbstractTrees.print_tree(io, ShowConstraint(c)) +Prints the results of `AbstractTrees.print_tree(io, x)` +without the final newline. Used for `show` methods which +invoke `print_tree`. +""" +function print_tree_rstrip(io::IO, x) + str = sprint(AbstractTrees.print_tree, x) + print(io, rstrip(str)) +end +# This object is used to work around the fact that +# Convex overloads booleans for AbstractExpr's +# in order to generate constraints. This is problematic +# for `AbstractTrees.print_tree` which wants to compare +# the root of the tree to itself at some point. +# By wrapping all tree roots in structs, this comparison +# occurs on the level of the `struct`, and `==` falls +# back to object equality (`===`), which is what we +# want in this case. +# +# The same construct is used below for other tree roots. +struct ConstraintRoot + constraint::Constraint +end +AbstractTrees.print_tree(io::IO, c::Constraint) = AbstractTrees.print_tree(io, ConstraintRoot(c)) +AbstractTrees.children(c::ConstraintRoot) = AbstractTrees.children(c.constraint) +AbstractTrees.printnode(io::IO, c::ConstraintRoot) = AbstractTrees.printnode(io, c.constraint) +show(io::IO, c::Constraint) = print_tree_rstrip(io, c) -struct ShowExpr +struct ExprRoot expr::AbstractExpr end +AbstractTrees.print_tree(io::IO, e::AbstractExpr) = AbstractTrees.print_tree(io, ExprRoot(e)) +AbstractTrees.children(e::ExprRoot) = AbstractTrees.children(e.expr) +AbstractTrees.printnode(io::IO, e::ExprRoot) = AbstractTrees.printnode(io, e.expr) -AbstractTrees.children(c::ShowExpr) = AbstractTrees.children(c.expr) -AbstractTrees.printnode(io::IO, c::ShowExpr) = AbstractTrees.printnode(io, c.expr) -show(io::IO, c::AbstractExpr) = AbstractTrees.print_tree(io, ShowExpr(c)) +show(io::IO, e::AbstractExpr) = print_tree_rstrip(io, e) -struct ShowProblemObjective +struct ProblemObjectiveRoot head::Symbol objective::AbstractExpr end -AbstractTrees.children(p::ShowProblemObjective) = (p.objective,) -AbstractTrees.printnode(io::IO, p::ShowProblemObjective) = print(io,string(p.head)) +AbstractTrees.children(p::ProblemObjectiveRoot) = (p.objective,) +AbstractTrees.printnode(io::IO, p::ProblemObjectiveRoot) = print(io, string(p.head)) -struct ShowProblemConstraints +struct ProblemConstraintsRoot constraints::Vector{Constraint} end -AbstractTrees.children(p::ShowProblemConstraints) = p.constraints -AbstractTrees.printnode(io::IO, p::ShowProblemConstraints) = print(io,"subject to") +AbstractTrees.children(p::ProblemConstraintsRoot) = p.constraints +AbstractTrees.printnode(io::IO, p::ProblemConstraintsRoot) = print(io, "subject to") -function show(io::IO, p::Problem) - AbstractTrees.print_tree(io, ShowProblemObjective(p.head, p.objective)) +function AbstractTrees.print_tree(io::IO, p::Problem) + AbstractTrees.print_tree(io, ProblemObjectiveRoot(p.head, p.objective)) if !(isempty(p.constraints)) - AbstractTrees.print_tree(io, ShowProblemConstraints(p.constraints)) + AbstractTrees.print_tree(io, ProblemConstraintsRoot(p.constraints)) end +end + +function show(io::IO, p::Problem) + AbstractTrees.print_tree(io, p) print(io, "\ncurrent status: $(p.status)") if p.status == "solved" print(io, " with optimal value of $(round(p.optval, digits=4))") diff --git a/src/utilities/tree_interface.jl b/src/utilities/tree_interface.jl index ee5510a0e..41eaba611 100644 --- a/src/utilities/tree_interface.jl +++ b/src/utilities/tree_interface.jl @@ -1,12 +1,8 @@ import AbstractTrees -function AbstractTrees.children(p::Problem) - return (p.objective, p.constraints) -end +AbstractTrees.children(p::Problem) = (p.objective, p.constraints) -function AbstractTrees.children(e::AbstractExpr) - return e.children -end +AbstractTrees.children(e::AbstractExpr) = e.children AbstractTrees.children(v::Variable) = () AbstractTrees.children(c::Constant) = () @@ -28,7 +24,7 @@ function AbstractTrees.printnode(io::IO, node::Vector{Constraint}) end end -AbstractTrees.printnode(io::IO, node::Problem) = print(io,node.head) +AbstractTrees.printnode(io::IO, node::Problem) = print(io, node.head) function AbstractTrees.printnode(io::IO, node::Constant) if length(node.value) <= 3 @@ -38,4 +34,4 @@ function AbstractTrees.printnode(io::IO, node::Constant) end end -AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) \ No newline at end of file +AbstractTrees.printnode(io::IO, node::Variable) = summary(io, node) diff --git a/test/test_utilities.jl b/test/test_utilities.jl index 8c5a7b75c..eae70f3b7 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -1,5 +1,54 @@ @testset "Utilities" begin + @testset "Show" begin + x = Variable() + @test sprint(show, x) == raw""" + Variable + size: (1, 1) + sign: real + vexity: affine + """ * "$(Convex.show_id(x))" + fix!(x, 1.0) + @test sprint(show, x) == raw""" + Variable + size: (1, 1) + sign: real + vexity: constant + """ * "$(Convex.show_id(x))" * + "\nvalue: 1.0" + + @test sprint(show, 2*x) == raw""" + * (constant; real) + ├─ 2 + └─ real variable (fixed) (""" * "$(Convex.show_id(x)))" + + free!(x) + p = maximize( log(x), x >= 1, x <= 3 ) + + @test sprint(show, p) == raw""" + maximize + └─ log (concave; real) + └─ real variable (""" * "$(Convex.show_id(x))" * raw""") + subject to + ├─ >= constraint (affine) + │ ├─ real variable (""" * "$(Convex.show_id(x))" * raw""") + │ └─ 1 + └─ <= constraint (affine) + ├─ real variable (""" * "$(Convex.show_id(x))" * raw""") + └─ 3 + + current status: not yet solved""" + + x = ComplexVariable(2,3) + @test sprint(show, x) == raw""" + Variable + size: (2, 3) + sign: complex + vexity: affine + """ * "$(Convex.show_id(x))" + + end + @testset "clearmemory" begin # solve a problem to populate globals x = Variable() From 61cf1372f8c23d8171a1274a07ba6e9e033f6bd7 Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Mon, 2 Sep 2019 16:30:27 +0100 Subject: [PATCH 5/9] Add docs --- Project.toml | 1 + docs/Project.toml | 1 + docs/src/advanced.md | 23 +++++++++++++++++++++++ 3 files changed, 25 insertions(+) diff --git a/Project.toml b/Project.toml index 88338442b..c373ba34c 100644 --- a/Project.toml +++ b/Project.toml @@ -10,6 +10,7 @@ OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d" SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [compat] +AbstractTrees = "^0.2.1" MathProgBase = "^0.7" OrderedCollections = "^1.0" julia = "^1.0" diff --git a/docs/Project.toml b/docs/Project.toml index 2f2e52c6a..5590afdad 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -1,4 +1,5 @@ [deps] +AbstractTrees = "1520ce14-60c1-5f80-bbc7-55ef81b5835c" Convex = "f65535da-76fb-5f13-bab9-19810c17039a" Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4" SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" diff --git a/docs/src/advanced.md b/docs/src/advanced.md index 3abff1903..d353bb307 100644 --- a/docs/src/advanced.md +++ b/docs/src/advanced.md @@ -98,3 +98,26 @@ for i=1:10 free!(x) end ``` + +Using the tree structure +------------------------ + +A Convex problem is structured as a *tree*, with the *root* being the +problem object, with branches to the objective and the set of constraints. +The objective is an `AbstractExpr` which itself is a tree, with each atom +being a node and having `children` which are other atoms, variables, or +constants. Convex provides `children` methods from +[AbstractTrees.jl](https://github.com/Keno/AbstractTrees.jl) so that the +tree-traversal functions of that package can be used with Convex.jl problems +and structures. This is what allows powers the printing of problems, expressions, +and constraints. This can also be used to analyze the structure of a Convex.jl +problem. For example, + +```@repl +using Convex, AbstractTrees +x = Variable() +p = maximize( log(x), x >= 1, x <= 3 ) +for leaf in AbstractTrees.Leaves(p) + println("Here's a leaf: $(summary(leaf))") +end +``` From 3b7884d40244396b2d900a413d41d5381bc11547 Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Mon, 2 Sep 2019 21:35:03 +0100 Subject: [PATCH 6/9] Vendor `print_tree` to respect `maxdepth` --- LICENSE.md | 30 ++++++++- src/Convex.jl | 3 +- src/utilities/show.jl | 26 +++++--- src/utilities/tree_interface.jl | 2 +- src/utilities/tree_print.jl | 108 ++++++++++++++++++++++++++++++++ test/test_utilities.jl | 31 +++++---- 6 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 src/utilities/tree_print.jl diff --git a/LICENSE.md b/LICENSE.md index 92654040c..b8af352a1 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -24,8 +24,9 @@ The Convex.jl package is licensed under the Simplified "2-clause" BSD License: > (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE > OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -The file benchmark/benchmarks/benchmark.jl contains some utilities copied from the MathOptInterface.jl package -(https://github.com/JuliaOpt/MathOptInterface.jl) which is licensed under the MIT License: +The file benchmark/benchmarks/benchmark.jl contains some utilities copied +from the MathOptInterface.jl package (https://github.com/JuliaOpt/MathOptInterface.jl) +which is licensed under the MIT License: >Copyright (c) 2017: Miles Lubin and contributors Copyright (c) 2017: Google Inc. > @@ -71,3 +72,28 @@ and .travis.yml contain code copied from the Transducers.jl package >LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, >OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE >SOFTWARE. + +The `TreePrint` module contains code from the AbstractTrees.jl package +(https://github.com/Keno/AbstractTrees.jl) which is licensed under the +MIT "Expat" License: + +> Copyright (c) 2015: Keno Fischer. +> +> Permission is hereby granted, free of charge, to any person obtaining +> a copy of this software and associated documentation files (the +> "Software"), to deal in the Software without restriction, including +> without limitation the rights to use, copy, modify, merge, publish, +> distribute, sublicense, and/or sell copies of the Software, and to +> permit persons to whom the Software is furnished to do so, subject to +> the following conditions: +> +> The above copyright notice and this permission notice shall be +> included in all copies or substantial portions of the Software. +> +> THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +> EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +> MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +> IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +> CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +> TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +> SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/src/Convex.jl b/src/Convex.jl index f6e484859..80c309989 100644 --- a/src/Convex.jl +++ b/src/Convex.jl @@ -79,8 +79,9 @@ include("atoms/exp_cone/relative_entropy.jl") include("atoms/exp_+_sdp_cone/logdet.jl") ### utilities -include("utilities/show.jl") +include("utilities/tree_print.jl") include("utilities/tree_interface.jl") +include("utilities/show.jl") include("utilities/iteration.jl") include("utilities/broadcast.jl") diff --git a/src/utilities/show.jl b/src/utilities/show.jl index fba5f50d6..37d25c68c 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -1,6 +1,14 @@ import Base.show, Base.summary export show, summary -import AbstractTrees +using AbstractTrees: AbstractTrees +using .TreePrint + +""" + const MAXDEPTH = Ref(3) + +Controls depth of tree printing globally for Convex.jl +""" +const MAXDEPTH = Ref(3) """ show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 4) @@ -104,12 +112,12 @@ end """ print_tree_rstrip(io::IO, x) -Prints the results of `AbstractTrees.print_tree(io, x)` +Prints the results of `TreePrint.print_tree(io, x)` without the final newline. Used for `show` methods which invoke `print_tree`. """ function print_tree_rstrip(io::IO, x) - str = sprint(AbstractTrees.print_tree, x) + str = sprint(TreePrint.print_tree, x, MAXDEPTH[]) print(io, rstrip(str)) end @@ -127,7 +135,7 @@ end struct ConstraintRoot constraint::Constraint end -AbstractTrees.print_tree(io::IO, c::Constraint) = AbstractTrees.print_tree(io, ConstraintRoot(c)) +TreePrint.print_tree(io::IO, c::Constraint, maxdepth = 5) = TreePrint.print_tree(io, ConstraintRoot(c), maxdepth) AbstractTrees.children(c::ConstraintRoot) = AbstractTrees.children(c.constraint) AbstractTrees.printnode(io::IO, c::ConstraintRoot) = AbstractTrees.printnode(io, c.constraint) @@ -136,7 +144,7 @@ show(io::IO, c::Constraint) = print_tree_rstrip(io, c) struct ExprRoot expr::AbstractExpr end -AbstractTrees.print_tree(io::IO, e::AbstractExpr) = AbstractTrees.print_tree(io, ExprRoot(e)) +TreePrint.print_tree(io::IO, e::AbstractExpr, maxdepth = 5) = TreePrint.print_tree(io, ExprRoot(e), maxdepth) AbstractTrees.children(e::ExprRoot) = AbstractTrees.children(e.expr) AbstractTrees.printnode(io::IO, e::ExprRoot) = AbstractTrees.printnode(io, e.expr) @@ -160,15 +168,15 @@ AbstractTrees.children(p::ProblemConstraintsRoot) = p.constraints AbstractTrees.printnode(io::IO, p::ProblemConstraintsRoot) = print(io, "subject to") -function AbstractTrees.print_tree(io::IO, p::Problem) - AbstractTrees.print_tree(io, ProblemObjectiveRoot(p.head, p.objective)) +function TreePrint.print_tree(io::IO, p::Problem, maxdepth = 5) + TreePrint.print_tree(io, ProblemObjectiveRoot(p.head, p.objective), maxdepth) if !(isempty(p.constraints)) - AbstractTrees.print_tree(io, ProblemConstraintsRoot(p.constraints)) + TreePrint.print_tree(io, ProblemConstraintsRoot(p.constraints), maxdepth) end end function show(io::IO, p::Problem) - AbstractTrees.print_tree(io, p) + TreePrint.print_tree(io, p, MAXDEPTH[]) print(io, "\ncurrent status: $(p.status)") if p.status == "solved" print(io, " with optimal value of $(round(p.optval, digits=4))") diff --git a/src/utilities/tree_interface.jl b/src/utilities/tree_interface.jl index 41eaba611..4573da529 100644 --- a/src/utilities/tree_interface.jl +++ b/src/utilities/tree_interface.jl @@ -1,4 +1,4 @@ -import AbstractTrees +using AbstractTrees: AbstractTrees AbstractTrees.children(p::Problem) = (p.objective, p.constraints) diff --git a/src/utilities/tree_print.jl b/src/utilities/tree_print.jl new file mode 100644 index 000000000..c030a9ea4 --- /dev/null +++ b/src/utilities/tree_print.jl @@ -0,0 +1,108 @@ +# This module is needed until AbstractTrees.jl#37 is fixed. +# (PR: https://github.com/Keno/AbstractTrees.jl/pull/38) +# because currently `print_tree` does not respect `maxdepth`. +# This just implements the changes in the above PR. +# Code in this file is modified from AbstractTrees.jl +# See LICENSE for a copy of its MIT license. +module TreePrint + +using AbstractTrees: printnode, treekind, IndexedTree, children + + +# Printing +struct TreeCharSet + mid + terminator + skip + dash + ellipsis +end + +# Default charset +TreeCharSet() = TreeCharSet('├','└','│','─','…') + +function print_prefix(io, depth, charset, active_levels) + for current_depth in 0:(depth-1) + if current_depth in active_levels + print(io,charset.skip," "^(textwidth(charset.dash)+1)) + else + print(io," "^(textwidth(charset.skip)+textwidth(charset.dash)+1)) + end + end +end + +@doc raw""" +# Usage +Prints an ASCII formatted representation of the `tree` to the given `io` object. +By default all children will be printed up to a maximum level of 5, though this +valud can be overriden by the `maxdepth` parameter. The charset to use in +printing can be customized using the `charset` keyword argument. + +# Examples +```julia +julia> print_tree(STDOUT,Dict("a"=>"b","b"=>['c','d'])) +Dict{String,Any}("b"=>['c','d'],"a"=>"b") +├─ b +│ ├─ c +│ └─ d +└─ a + └─ b + +julia> print_tree(STDOUT,Dict("a"=>"b","b"=>['c','d']); + charset = TreeCharSet('+','\\','|',"--")) +Dict{String,Any}("b"=>['c','d'],"a"=>"b") ++-- b +| +-- c +| \-- d +\-- a + \-- b +``` + +""" +print_tree + +function _print_tree(printnode::Function, io::IO, tree, maxdepth = 5; depth = 0, active_levels = Int[], + charset = TreeCharSet(), withinds = false, inds = [], from = nothing, to = nothing, roottree = tree) + nodebuf = IOBuffer() + isa(io, IOContext) && (nodebuf = IOContext(nodebuf, io)) + if withinds + printnode(nodebuf, tree, inds) + else + tree != roottree && isa(treekind(roottree), IndexedTree) ? + printnode(nodebuf, roottree[tree]) : + printnode(nodebuf, tree) + end + str = String(take!(isa(nodebuf, IOContext) ? nodebuf.io : nodebuf)) + for (i,line) in enumerate(split(str, '\n')) + i != 1 && print_prefix(io, depth, charset, active_levels) + println(io, line) + end + depth > maxdepth && return + c = isa(treekind(roottree), IndexedTree) ? + childindices(roottree, tree) : children(roottree, tree) + if c !== () + s = Iterators.Stateful(from === nothing ? pairs(c) : Iterators.Rest(pairs(c), from)) + while !isempty(s) + ind, child = popfirst!(s) + ind === to && break + active = false + child_active_levels = active_levels + print_prefix(io, depth, charset, active_levels) + if isempty(s) + print(io, charset.terminator) + else + print(io, charset.mid) + child_active_levels = push!(copy(active_levels), depth) + end + print(io, charset.dash, ' ') + print_tree(depth == maxdepth ? (io, val) -> print(io, charset.ellipsis) : printnode, + io, child, maxdepth; depth = depth + 1, + active_levels = child_active_levels, charset = charset, withinds=withinds, + inds = withinds ? [inds; ind] : [], roottree = roottree) + end + end +end +print_tree(f::Function, io::IO, tree, args...; kwargs...) = _print_tree(f, io, tree, args...; kwargs...) +print_tree(io::IO, tree, args...; kwargs...) = print_tree(printnode, io, tree, args...; kwargs...) +print_tree(tree, args...; kwargs...) = print_tree(stdout::IO, tree, args...; kwargs...) +end diff --git a/test/test_utilities.jl b/test/test_utilities.jl index eae70f3b7..46c6a22c2 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -2,51 +2,56 @@ @testset "Show" begin x = Variable() - @test sprint(show, x) == raw""" + @test sprint(show, x) == """ Variable size: (1, 1) sign: real vexity: affine - """ * "$(Convex.show_id(x))" + $(Convex.show_id(x))""" fix!(x, 1.0) - @test sprint(show, x) == raw""" + @test sprint(show, x) == """ Variable size: (1, 1) sign: real vexity: constant - """ * "$(Convex.show_id(x))" * - "\nvalue: 1.0" + $(Convex.show_id(x)) + value: 1.0""" - @test sprint(show, 2*x) == raw""" + @test sprint(show, 2*x) == """ * (constant; real) ├─ 2 - └─ real variable (fixed) (""" * "$(Convex.show_id(x)))" + └─ real variable (fixed) ($(Convex.show_id(x)))""" free!(x) p = maximize( log(x), x >= 1, x <= 3 ) - @test sprint(show, p) == raw""" + @test sprint(show, p) == """ maximize └─ log (concave; real) - └─ real variable (""" * "$(Convex.show_id(x))" * raw""") + └─ real variable ($(Convex.show_id(x))) subject to ├─ >= constraint (affine) - │ ├─ real variable (""" * "$(Convex.show_id(x))" * raw""") + │ ├─ real variable ($(Convex.show_id(x))) │ └─ 1 └─ <= constraint (affine) - ├─ real variable (""" * "$(Convex.show_id(x))" * raw""") + ├─ real variable ($(Convex.show_id(x))) └─ 3 current status: not yet solved""" x = ComplexVariable(2,3) - @test sprint(show, x) == raw""" + @test sprint(show, x) == """ Variable size: (2, 3) sign: complex vexity: affine - """ * "$(Convex.show_id(x))" + $(Convex.show_id(x))""" + # test `maxdepth` + x = Variable(2) + y = Variable(2) + p = minimize(sum(x), hcat(hcat(hcat(hcat(x,y), hcat(x,y)),hcat(hcat(x,y), hcat(x,y))),hcat(hcat(hcat(x,y), hcat(x,y)),hcat(hcat(x,y), hcat(x,y)))) == hcat(hcat(hcat(hcat(x,y), hcat(x,y)),hcat(hcat(x,y), hcat(x,y))),hcat(hcat(hcat(x,y), hcat(x,y)),hcat(hcat(x,y), hcat(x,y))))) + @test sprint(show, p) == "minimize\n└─ sum (affine; real)\n └─ 2-element real variable ($(Convex.show_id(x)))\nsubject to\n└─ == constraint (affine)\n ├─ hcat (affine; real)\n │ ├─ hcat (affine; real)\n │ │ ├─ …\n │ │ └─ …\n │ └─ hcat (affine; real)\n │ ├─ …\n │ └─ …\n └─ hcat (affine; real)\n ├─ hcat (affine; real)\n │ ├─ …\n │ └─ …\n └─ hcat (affine; real)\n ├─ …\n └─ …\n\ncurrent status: not yet solved" end @testset "clearmemory" begin From 787acad3e54ad82b31944cbb26f890a23c6dedcb Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Mon, 2 Sep 2019 22:00:53 +0100 Subject: [PATCH 7/9] Add tree iterators to docs --- docs/src/advanced.md | 38 +++++++++++++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/docs/src/advanced.md b/docs/src/advanced.md index d353bb307..13d154484 100644 --- a/docs/src/advanced.md +++ b/docs/src/advanced.md @@ -113,7 +113,7 @@ and structures. This is what allows powers the printing of problems, expressions and constraints. This can also be used to analyze the structure of a Convex.jl problem. For example, -```@repl +```@repl 1 using Convex, AbstractTrees x = Variable() p = maximize( log(x), x >= 1, x <= 3 ) @@ -121,3 +121,39 @@ for leaf in AbstractTrees.Leaves(p) println("Here's a leaf: $(summary(leaf))") end ``` + +We can also iterate over the problem in various orders. The following descriptions +are taken from the AbstractTrees.jl docstrings, which have more information. + +### PostOrderDFS + +Iterator to visit the nodes of a tree, guaranteeing that children +will be visited before their parents. + +```@repl 1 +for (i, node) in enumerate(AbstractTrees.PostOrderDFS(p)) + println("Here's node $i via PostOrderDFS: $(summary(node))") +end +``` + +### PreOrderDFS + +Iterator to visit the nodes of a tree, guaranteeing that parents +will be visited before their children. + +```@repl 1 +for (i, node) in enumerate(AbstractTrees.PreOrderDFS(p)) + println("Here's node $i via PreOrderDFS: $(summary(node))") +end +``` + +### StatelessBFS + +Iterator to visit the nodes of a tree, guaranteeing that all nodes of a level +will be visited before their children. + +```@repl 1 +for (i, node) in enumerate(AbstractTrees.StatelessBFS(p)) + println("Here's node $i via StatelessBFS: $(summary(node))") +end +``` From fc88f696958861291505eb4207b8f6fdb90d2c16 Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Mon, 9 Sep 2019 10:12:16 -0400 Subject: [PATCH 8/9] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Mathieu Besançon --- src/utilities/show.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utilities/show.jl b/src/utilities/show.jl index 37d25c68c..3a8c9d9b5 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -55,7 +55,6 @@ function Base.summary(io::IO, x::Variable) print(io, "$(size(x,1))-element $(sgn) variable$(cst)") else print(io, "$(size(x,1))×$(size(x,2)) $(sgn) variable$(cst)") - end end @@ -135,6 +134,7 @@ end struct ConstraintRoot constraint::Constraint end + TreePrint.print_tree(io::IO, c::Constraint, maxdepth = 5) = TreePrint.print_tree(io, ConstraintRoot(c), maxdepth) AbstractTrees.children(c::ConstraintRoot) = AbstractTrees.children(c.constraint) AbstractTrees.printnode(io::IO, c::ConstraintRoot) = AbstractTrees.printnode(io, c.constraint) From 8ce289398cafd95caa97b81d2f6f54e6188a0523 Mon Sep 17 00:00:00 2001 From: Eric <5846501+ericphanson@users.noreply.github.com> Date: Mon, 9 Sep 2019 10:29:10 -0400 Subject: [PATCH 9/9] Change `id` printing: show first and last 3 digits --- src/utilities/show.jl | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/src/utilities/show.jl b/src/utilities/show.jl index 3a8c9d9b5..e17d6142b 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -11,7 +11,7 @@ Controls depth of tree printing globally for Convex.jl const MAXDEPTH = Ref(3) """ - show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 4) + show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 3) Print a truncated version of the objects `id_hash` field. @@ -21,15 +21,14 @@ Print a truncated version of the objects `id_hash` field. julia> x = Variable(); julia> Convex.show_id(stdout, x) -id: 6201... +id: 163…906 ``` """ -show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 4) = print(io, show_id(x)) +show_id(io::IO, x::Union{AbstractExpr, Constraint}; digits = 3) = print(io, show_id(x; digits=digits)) -function show_id(x::Union{AbstractExpr, Constraint}; digits = 4) +function show_id(x::Union{AbstractExpr, Constraint}; digits = 3) hash_str = string(x.id_hash) - hash_str = hash_str[1:nextind(hash_str, digits-1)] - return "id: " * hash_str * "..." + return "id: " * first(hash_str, digits) * "…" * last(hash_str, digits) end """ @@ -42,7 +41,7 @@ Prints a one-line summary of a variable `x` to `io`. julia> x = ComplexVariable(3,2); julia> summary(stdout, x) -3×2 complex variable (id: 5455...) +3×2 complex variable (id: 732…737) ``` """ function Base.summary(io::IO, x::Variable) @@ -92,7 +91,7 @@ show(io::IO, x::Constant) = print(io, x.value) # size: (3, 4) # sign: real # vexity: affine -# id: 7385... +# id: 758…633 # here, the `id` will change from run to run. function show(io::IO, x::Variable) print(io, "Variable")