From b4f89f3a9eab3537a16e5b64ab334d6f73f1ee3a Mon Sep 17 00:00:00 2001 From: Eric Hanson <5846501+ericphanson@users.noreply.github.com> Date: Tue, 10 Sep 2019 17:20:12 -0400 Subject: [PATCH] Improve printing of problems, constraints, and variables (#325) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * initial tree interface * `show` via trees * Fix redundant `constraints` in `show` * Cleanup, add some comments, add tests * Add docs * Vendor `print_tree` to respect `maxdepth` * Add tree iterators to docs * Apply suggestions from code review Co-Authored-By: Mathieu Besançon * Change `id` printing: show first and last 3 digits --- LICENSE.md | 30 ++++- Project.toml | 2 + docs/Project.toml | 1 + docs/src/advanced.md | 59 +++++++++ src/Convex.jl | 2 + src/utilities/show.jl | 221 +++++++++++++++++++++++--------- src/utilities/tree_interface.jl | 37 ++++++ src/utilities/tree_print.jl | 108 ++++++++++++++++ test/test_utilities.jl | 54 ++++++++ 9 files changed, 450 insertions(+), 64 deletions(-) create mode 100644 src/utilities/tree_interface.jl 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/Project.toml b/Project.toml index d6bb9d313..c373ba34c 100644 --- a/Project.toml +++ b/Project.toml @@ -3,12 +3,14 @@ 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" 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..13d154484 100644 --- a/docs/src/advanced.md +++ b/docs/src/advanced.md @@ -98,3 +98,62 @@ 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 1 +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 +``` + +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 +``` diff --git a/src/Convex.jl b/src/Convex.jl index 64dea6779..80c309989 100644 --- a/src/Convex.jl +++ b/src/Convex.jl @@ -79,6 +79,8 @@ include("atoms/exp_cone/relative_entropy.jl") include("atoms/exp_+_sdp_cone/logdet.jl") ### utilities +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 5cc5df055..e17d6142b 100644 --- a/src/utilities/show.jl +++ b/src/utilities/show.jl @@ -1,84 +1,181 @@ -import Base.show -export show +import Base.show, Base.summary +export show, summary +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 = 3) + +Print a truncated version of the objects `id_hash` field. + +## Example + +```julia-repl +julia> x = Variable(); + +julia> Convex.show_id(stdout, x) +id: 163…906 +``` +""" +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 = 3) + hash_str = string(x.id_hash) + return "id: " * first(hash_str, digits) * "…" * last(hash_str, digits) +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: 732…737) +``` +""" +function Base.summary(io::IO, x::Variable) + sgn = summary(sign(x)) + cst = vexity(x) == ConstVexity() ? " (fixed)" : "" + cst = cst * " (" * sprint(show_id, x) * ")" + if size(x) == (1,1) + print(io, "$(sgn) variable$(cst)") + 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 + +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 +# julia> Variable(3,4) +# Variable # size: (3, 4) -# sign: NoSign() -# vexity: AffineVexity() +# sign: real +# vexity: affine +# id: 758…633 +# 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 -# 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))""") +""" + print_tree_rstrip(io::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(TreePrint.print_tree, x, MAXDEPTH[]) + print(io, rstrip(str)) 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) - """) +# 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 -# 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)) - """) +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) + +show(io::IO, c::Constraint) = print_tree_rstrip(io, c) + +struct ExprRoot + expr::AbstractExpr +end +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) + + +show(io::IO, e::AbstractExpr) = print_tree_rstrip(io, e) + + +struct ProblemObjectiveRoot + head::Symbol + objective::AbstractExpr +end + +AbstractTrees.children(p::ProblemObjectiveRoot) = (p.objective,) +AbstractTrees.printnode(io::IO, p::ProblemObjectiveRoot) = print(io, string(p.head)) + +struct ProblemConstraintsRoot + constraints::Vector{Constraint} +end + +AbstractTrees.children(p::ProblemConstraintsRoot) = p.constraints +AbstractTrees.printnode(io::IO, p::ProblemConstraintsRoot) = print(io, "subject to") + + +function TreePrint.print_tree(io::IO, p::Problem, maxdepth = 5) + TreePrint.print_tree(io, ProblemObjectiveRoot(p.head, p.objective), maxdepth) + if !(isempty(p.constraints)) + TreePrint.print_tree(io, ProblemConstraintsRoot(p.constraints), maxdepth) + end 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 function show(io::IO, p::Problem) - print(io, """Problem: - $(p.head) $(p.objective) - subject to - """) - join(io, p.constraints, "\n\t\t") + 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 new file mode 100644 index 000000000..4573da529 --- /dev/null +++ b/src/utilities/tree_interface.jl @@ -0,0 +1,37 @@ +using AbstractTrees: AbstractTrees + +AbstractTrees.children(p::Problem) = (p.objective, p.constraints) + +AbstractTrees.children(e::AbstractExpr) = e.children + +AbstractTrees.children(v::Variable) = () +AbstractTrees.children(c::Constant) = () + +AbstractTrees.children(C::Constraint) = (C.lhs, C.rhs) +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) + +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) 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 8c5a7b75c..46c6a22c2 100644 --- a/test/test_utilities.jl +++ b/test/test_utilities.jl @@ -1,5 +1,59 @@ @testset "Utilities" begin + @testset "Show" begin + x = Variable() + @test sprint(show, x) == """ + Variable + size: (1, 1) + sign: real + vexity: affine + $(Convex.show_id(x))""" + fix!(x, 1.0) + @test sprint(show, x) == """ + Variable + size: (1, 1) + sign: real + vexity: constant + $(Convex.show_id(x)) + value: 1.0""" + + @test sprint(show, 2*x) == """ + * (constant; real) + ├─ 2 + └─ real variable (fixed) ($(Convex.show_id(x)))""" + + free!(x) + p = maximize( log(x), x >= 1, x <= 3 ) + + @test sprint(show, p) == """ + maximize + └─ log (concave; real) + └─ real variable ($(Convex.show_id(x))) + subject to + ├─ >= constraint (affine) + │ ├─ real variable ($(Convex.show_id(x))) + │ └─ 1 + └─ <= constraint (affine) + ├─ real variable ($(Convex.show_id(x))) + └─ 3 + + current status: not yet solved""" + + x = ComplexVariable(2,3) + @test sprint(show, x) == """ + Variable + size: (2, 3) + sign: complex + vexity: affine + $(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 # solve a problem to populate globals x = Variable()