Skip to content

Commit c1142a6

Browse files
odowericphanson
andauthored
Add summary printing to show(::IO, ::Problem) (#650)
Co-authored-by: Eric Hanson <[email protected]>
1 parent cbc281b commit c1142a6

File tree

9 files changed

+278
-70
lines changed

9 files changed

+278
-70
lines changed

src/constraints/GenericConstraint.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ function GenericConstraint{S}(child::AbstractExpr) where {S<:MOI.AbstractSet}
1717
return GenericConstraint(child, set_with_size(S, size(child)))
1818
end
1919

20+
iscomplex(c::GenericConstraint) = iscomplex(c.child)
21+
2022
function set_with_size(
2123
::Type{S},
2224
sz::Tuple{Int,Int},

src/constraints/GeometricMeanHypoCone.jl

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,12 @@ mutable struct GeometricMeanHypoConeConstraint <: Constraint
115115
end
116116
end
117117

118+
function iscomplex(c::GeometricMeanHypoConeConstraint)
119+
return iscomplex(c.T) || iscomplex(c.cone)
120+
end
121+
122+
iscomplex(c::GeometricMeanHypoCone) = iscomplex(c.A) || iscomplex(c.B)
123+
118124
function head(io::IO, ::GeometricMeanHypoConeConstraint)
119125
return print(io, "∈(GeometricMeanHypoCone)")
120126
end
@@ -128,7 +134,7 @@ function AbstractTrees.children(constraint::GeometricMeanHypoConeConstraint)
128134
constraint.T,
129135
constraint.cone.A,
130136
constraint.cone.B,
131-
"t=$(constraint.cone.t)",
137+
constraint.cone.t,
132138
)
133139
end
134140

src/constraints/RelativeEntropyEpiCone.jl

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,12 @@ mutable struct RelativeEntropyEpiConeConstraint <: Constraint
106106
end
107107
end
108108

109+
function iscomplex(c::RelativeEntropyEpiConeConstraint)
110+
return iscomplex(c.τ) || iscomplex(c.cone)
111+
end
112+
113+
iscomplex(c::RelativeEntropyEpiCone) = iscomplex(c.X) || iscomplex(c.Y)
114+
109115
function head(io::IO, ::RelativeEntropyEpiConeConstraint)
110116
return print(io, "∈(RelativeEntropyEpiCone)")
111117
end

src/problems.jl

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,19 @@ function Base.getproperty(p::Problem, s::Symbol)
4141
return getfield(p, s)
4242
end
4343

44-
dual_status(p::Problem) = MOI.get(p.model, MOI.DualStatus())
44+
function dual_status(p::Problem)
45+
if p.model === nothing
46+
return MOI.NO_SOLUTION
47+
end
48+
return MOI.get(p.model, MOI.DualStatus())
49+
end
4550

46-
primal_status(p::Problem) = MOI.get(p.model, MOI.PrimalStatus())
51+
function primal_status(p::Problem)
52+
if p.model === nothing
53+
return MOI.NO_SOLUTION
54+
end
55+
return MOI.get(p.model, MOI.PrimalStatus())
56+
end
4757

4858
termination_status(p::Problem) = MOI.get(p.model, MOI.TerminationStatus())
4959

src/utilities/show.jl

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ end
197197
AbstractTrees.children(p::ProblemObjectiveRoot) = (p.objective,)
198198

199199
function AbstractTrees.printnode(io::IO, p::ProblemObjectiveRoot)
200-
return print(io, string(p.head))
200+
return print(io, " ", string(p.head))
201201
end
202202

203203
struct ProblemConstraintsRoot
@@ -207,7 +207,7 @@ end
207207
AbstractTrees.children(p::ProblemConstraintsRoot) = p.constraints
208208

209209
function AbstractTrees.printnode(io::IO, p::ProblemConstraintsRoot)
210-
return print(io, "subject to")
210+
return print(io, " subject to")
211211
end
212212

213213
function TreePrint.print_tree(io::IO, p::Problem, args...; kwargs...)
@@ -234,17 +234,40 @@ function TreePrint.print_tree(io::IO, p::Problem, args...; kwargs...)
234234
return
235235
end
236236

237+
function _to_underscore(n::Integer)
238+
x = Iterators.partition(digits(n), 3)
239+
return join(reverse(join.(reverse.(x))), '_')
240+
end
241+
242+
function _str_with_elements(x, y)
243+
xs, ys = _to_underscore(x), _to_underscore(y)
244+
return "$xs ($ys scalar elements)"
245+
end
246+
237247
function Base.show(io::IO, p::Problem)
238-
TreePrint.print_tree(io, p, MAXDEPTH[], MAXWIDTH[])
239-
if p.status == MOI.OPTIMIZE_NOT_CALLED
240-
print(io, "\nstatus: `solve!` not called yet")
241-
else
242-
print(io, "\ntermination status: $(p.status)")
243-
print(io, "\nprimal status: $(primal_status(p))")
244-
print(io, "\ndual status: $(dual_status(p))")
245-
end
246-
if p.status == "solved"
247-
print(io, " with optimal value of $(round(p.optval, digits=4))")
248+
# Print problem statistics
249+
counts = Counts(p)
250+
println(io, "Problem statistics")
251+
var_str = _str_with_elements(counts.n_variables, counts.n_scalar_variables)
252+
println(io, " number of variables : ", var_str)
253+
con_str =
254+
_str_with_elements(counts.n_constraints, counts.n_scalar_constraints)
255+
println(io, " number of constraints : ", con_str)
256+
println(io, " number of coefficients : ", counts.n_nonzeros)
257+
println(io, " number of atoms : ", counts.n_atoms)
258+
println(io)
259+
# Print solution summary
260+
println(io, "Solution summary")
261+
println(io, " termination status : ", p.status)
262+
println(io, " primal status : ", primal_status(p))
263+
println(io, " dual status : ", dual_status(p))
264+
if p.status == MOI.OPTIMAL && p.optval !== nothing
265+
println(io, " objective value : ", round(p.optval; digits = 4))
248266
end
267+
println(io)
268+
# Expression tree
269+
println(io, "Expression graph")
270+
# We offset the depth by 1 so things are printed with a three-space offset.
271+
TreePrint.print_tree(io, p, MAXDEPTH[] + 1, MAXWIDTH[]; depth = 1)
249272
return
250273
end

src/utilities/tree_interface.jl

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,3 +38,97 @@ function AbstractTrees.printnode(io::IO, node::Constant)
3838
end
3939

4040
AbstractTrees.printnode(io::IO, node::AbstractVariable) = summary(io, node)
41+
42+
mutable struct Counts
43+
seen::Base.IdSet
44+
n_variables::Int
45+
n_scalar_variables::Int
46+
n_constraints::Int
47+
n_scalar_constraints::Int
48+
n_atoms::Int
49+
n_nonzeros::Int
50+
Counts() = new(Base.IdSet(), 0, 0, 0, 0, 0, 0)
51+
end
52+
53+
function Counts(p::Problem)
54+
counts = Counts()
55+
_add_to_problem_count(counts, p)
56+
counts.n_atoms -= 1 # Don't count p as an atom
57+
return counts
58+
end
59+
60+
function _add_to_problem_count(counts::Counts, node::AbstractExpr)
61+
if node in counts.seen
62+
return
63+
end
64+
for n in AbstractTrees.PreOrderDFS(node)
65+
if n !== node && !(n in counts.seen)
66+
_add_to_problem_count(counts, n)
67+
end
68+
end
69+
counts.n_atoms += 1
70+
push!(counts.seen, node)
71+
return
72+
end
73+
74+
function _add_to_problem_count(counts::Counts, node::AbstractVariable)
75+
if node in counts.seen
76+
return
77+
end
78+
counts.n_variables += 1
79+
counts.n_scalar_variables += (iscomplex(node) ? 2 : 1) * length(node)
80+
push!(counts.seen, node)
81+
for c in get_constraints(node)
82+
_add_to_problem_count(counts, c)
83+
end
84+
return
85+
end
86+
87+
_add_to_problem_count(::Counts, ::Vector{Constraint}) = nothing
88+
89+
_add_to_problem_count(::Counts, ::Nothing) = nothing
90+
91+
function _add_to_problem_count(counts::Counts, node::GenericConstraint)
92+
counts.n_constraints += 1
93+
counts.n_scalar_constraints +=
94+
(iscomplex(node) ? 2 : 1) * MOI.dimension(node.set)
95+
_add_to_problem_count(counts, node.child)
96+
return
97+
end
98+
99+
function _add_to_problem_count(counts::Counts, node::Constraint)
100+
counts.n_constraints += 1
101+
# TODO(odow): we don't know the general case
102+
return
103+
end
104+
105+
function _add_to_problem_count(counts::Counts, node::Constant)
106+
_add_to_problem_count(counts, node.value)
107+
push!(counts.seen, node)
108+
return
109+
end
110+
111+
function _add_to_problem_count(counts::Counts, node::ComplexConstant)
112+
_add_to_problem_count(counts, node.real_constant)
113+
_add_to_problem_count(counts, node.imag_constant)
114+
push!(counts.seen, node)
115+
return
116+
end
117+
118+
function _add_to_problem_count(counts::Counts, node::Number)
119+
counts.n_nonzeros += iscomplex(node) ? 2 : 1
120+
return
121+
end
122+
123+
function _add_to_problem_count(counts::Counts, node::AbstractArray{<:Number})
124+
counts.n_nonzeros += length(node)
125+
return
126+
end
127+
128+
function _add_to_problem_count(
129+
counts::Counts,
130+
node::SparseArrays.SparseMatrixCSC,
131+
)
132+
counts.n_nonzeros += SparseArrays.nnz(node)
133+
return
134+
end

test/test_constraints.jl

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -348,6 +348,28 @@ function test_GeometricMeanEpiConeSquare()
348348
return
349349
end
350350

351+
### constraints/RelativeEntropyEpiConeConstraint
352+
353+
function test_constraints_RelativeEntropyEpiConeConstraint()
354+
t = Variable(2, 2)
355+
X = Variable(2, 2)
356+
Y = Variable(2, 2)
357+
c = t in RelativeEntropyEpiCone(X, Y)
358+
@test !Convex.iscomplex(c)
359+
return
360+
end
361+
362+
### constraints/GeometricMeanHypoConeConstraint
363+
364+
function test_constraints_GeometricMeanHypoConeConstraint()
365+
T = Variable(2, 2)
366+
A = Variable(2, 2)
367+
B = Variable(2, 2)
368+
c = T in GeometricMeanHypoCone(A, B, 1 // 2)
369+
@test !Convex.iscomplex(c)
370+
return
371+
end
372+
351373
end # TestConstraints
352374

353375
TestConstraints.runtests()

test/test_problem_depot.jl

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,12 +51,15 @@ function test_Clarabel_warmstarts()
5151
Convex.ProblemDepot.run_tests(;
5252
exclude = [r"mip", r"sdp_quantum_relative_entropy3_lowrank"],
5353
) do p
54-
return solve!(
54+
ret = solve!(
5555
p,
5656
Clarabel.Optimizer;
5757
silent_solver = true,
5858
warmstart = true,
5959
)
60+
# verify post-solve printing does not error
61+
@test sprint(show, p) isa AbstractString
62+
return ret
6063
end
6164
return
6265
end

0 commit comments

Comments
 (0)