Skip to content

Commit

Permalink
Improve printing of problems, constraints, and variables (#325)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* Change `id` printing: show first and last 3 digits
  • Loading branch information
ericphanson authored Sep 10, 2019
1 parent 0a46d84 commit b4f89f3
Show file tree
Hide file tree
Showing 9 changed files with 450 additions and 64 deletions.
30 changes: 28 additions & 2 deletions LICENSE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
>
Expand Down Expand Up @@ -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.
2 changes: 2 additions & 0 deletions Project.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
1 change: 1 addition & 0 deletions docs/Project.toml
Original file line number Diff line number Diff line change
@@ -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"
59 changes: 59 additions & 0 deletions docs/src/advanced.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
```
2 changes: 2 additions & 0 deletions src/Convex.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
221 changes: 159 additions & 62 deletions src/utilities/show.jl
Original file line number Diff line number Diff line change
@@ -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))")
Expand Down
Loading

0 comments on commit b4f89f3

Please sign in to comment.