Skip to content

Commit

Permalink
Improve Printing for Disjunct/Logical Constraints and Disjunctions (#100
Browse files Browse the repository at this point in the history
)

* Add printing for constraints

* fix test function naming

* attempted linux fix

* replace fix attempt

* fix attempt 2

* fix coverage

* minor formatting fix
  • Loading branch information
pulsipher authored Dec 8, 2023
1 parent ccceb26 commit d7e8e7a
Show file tree
Hide file tree
Showing 6 changed files with 338 additions and 3 deletions.
1 change: 1 addition & 0 deletions src/DisjunctiveProgramming.jl
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ include("reformulate.jl")
include("bigm.jl")
include("hull.jl")
include("indicator.jl")
include("print.jl")

# Define additional stuff that should not be exported
const _EXCLUDE_SYMBOLS = [Symbol(@__MODULE__), :eval, :include]
Expand Down
11 changes: 8 additions & 3 deletions src/constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -171,6 +171,11 @@ function _check_expression(expr::AbstractVector)
return
end

# Extend JuMP.model_convert for _DisjunctConstraint
function JuMP.model_convert(model::JuMP.AbstractModel, con::_DisjunctConstraint)
return _DisjunctConstraint(JuMP.model_convert(model, con.constr), con.lvref)
end

"""
JuMP.build_constraint(
_error::Function,
Expand Down Expand Up @@ -450,7 +455,7 @@ end
_error::Function,
func::AbstractVector{T},
set::S
) where {T <: Union{LogicalVariableRef, _LogicalExpr}, S <: Union{Exactly, AtLeast, AtMost}}
) where {T <: LogicalVariableRef, S <: Union{Exactly, AtLeast, AtMost}}
Extend `JuMP.build_constraint` to add logical cardinality constraints to a [`GDPModel`](@ref).
This in combination with `JuMP.add_constraint` enables the use of
Expand All @@ -475,7 +480,7 @@ function JuMP.build_constraint( # Cardinality logical constraint
set::S # TODO: generalize to allow CP sets from MOI
) where {T <: LogicalVariableRef, S <: Union{Exactly{Int}, AtLeast{Int}, AtMost{Int}}}
new_set = _jump_to_moi_selector(set)(length(func) + 1)
new_func = Union{Number, LogicalVariableRef}[set.value, func...]
new_func = Union{Number, T}[set.value, func...]
return JuMP.VectorConstraint(new_func, new_set) # model_convert will make it an AbstractJuMPScalar
end
function JuMP.build_constraint( # Cardinality logical constraint
Expand All @@ -492,7 +497,7 @@ function JuMP.build_constraint( # Cardinality logical constraint
func::AbstractVector,
set::S # TODO: generalize to allow CP sets from MOI
) where {S <: Union{Exactly, AtLeast, AtMost}}
_error("Selector constraints can only be applied to a Vector or Container of LogicalVariableRefs.")
_error("Selector constraints can only be applied to a Vector or Container of LogicalVariableRefs or logical expressions.")
end

# Fallback for Affine/Quad expressions
Expand Down
172 changes: 172 additions & 0 deletions src/print.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
################################################################################
# PRINTING HELPERS
################################################################################
_wrap_in_math_mode(str::String) = "\$\$ $str \$\$"

# Get the appropriate symbols
_dor_symbol(::MIME"text/plain") = Sys.iswindows() ? "or" : ""
_dor_symbol(::MIME"text/latex") = "\\bigvee"
_imply_symbol(::MIME"text/plain") = Sys.iswindows() ? "-->" : ""
_left_pareth_symbol(::MIME"text/plain") = "("
_left_pareth_symbol(::MIME"text/latex") = "\\left("
_right_pareth_symbol(::MIME"text/plain") = ")"
_right_pareth_symbol(::MIME"text/latex") = "\\right)"

# Create the proper string for a cardinality function
_card_func_str(::MIME"text/plain", ::_MOIAtLeast) = "atleast"
_card_func_str(::MIME"text/latex", ::_MOIAtLeast) = "\\text{atleast}"
_card_func_str(::MIME"text/plain", ::_MOIAtMost) = "atmost"
_card_func_str(::MIME"text/latex", ::_MOIAtMost) = "\\text{atmost}"
_card_func_str(::MIME"text/plain", ::_MOIExactly) = "exactly"
_card_func_str(::MIME"text/latex", ::_MOIExactly) = "\\text{exactly}"

################################################################################
# CONSTRAINT PRINTING
################################################################################
# Return the string of a DisjunctConstraintRef
function JuMP.constraint_string(
mode::MIME,
cref::DisjunctConstraintRef;
in_math_mode = false
)
constr_str = JuMP.constraint_string(
mode,
JuMP.name(cref),
JuMP.constraint_object(cref);
in_math_mode = true
)
model = JuMP.owner_model(cref)
lvar = gdp_data(model).constraint_to_indicator[cref]
lvar_str = JuMP.function_string(mode, lvar)
if mode == MIME("text/latex")
constr_str *= ", \\; \\text{if } $(lvar_str) = \\text{True}"
if in_math_mode
return constr_str
else
return _wrap_in_math_mode(constr_str)
end
end
return constr_str * ", if $(lvar_str) = True"
end

# Give the constraint string for a logical constraint
function JuMP.constraint_string(
mode::MIME"text/latex",
con::JuMP.ScalarConstraint{<:_LogicalExpr, <:MOI.EqualTo}
)
return JuMP.function_string(mode, con) * " = \\text{True}"
end
function JuMP.constraint_string(
mode::MIME"text/plain",
con::JuMP.ScalarConstraint{<:_LogicalExpr, <:MOI.EqualTo}
)
return JuMP.function_string(mode, con) * " = True"
end
function JuMP.constraint_string(
mode, con::JuMP.VectorConstraint{F, <:AbstractCardinalitySet}
) where {F}
con_str = string(_card_func_str(mode, JuMP.moi_set(con)), _left_pareth_symbol(mode))
con_str *= join((JuMP.function_string(mode, ex) for ex in JuMP.jump_function(con)), ", ")
return con_str * _right_pareth_symbol(mode)
end

# Return the string of a LogicalConstraintRef
function JuMP.constraint_string(
mode::MIME,
cref::LogicalConstraintRef;
in_math_mode = false
)
constr_str = JuMP.constraint_string(
mode,
JuMP.name(cref),
JuMP.constraint_object(cref);
in_math_mode = in_math_mode
)
# temporary hack until JuMP provides a better solution for operator printing
# TODO improve the printing of implications (not recognized by JuMP as two-sided operators)
if mode == MIME("text/latex")
for p in ("&&" => "\\wedge", "||" => "\\vee", "\\textsf{!}" => "\\neg",
"==" => "\\iff", "\\textsf{=>}" => "\\implies")
constr_str = replace(constr_str, p)
end
return constr_str
else
constr_str = replace(constr_str, "&&" => Sys.iswindows() ? "and" : "")
constr_str = replace(constr_str, "||" => Sys.iswindows() ? "or" : "")
constr_str = replace(constr_str, "!" => Sys.iswindows() ? "!" : "¬")
constr_str = replace(constr_str, "==" => Sys.iswindows() ? "<-->" : "")
return replace(constr_str, "=>" => Sys.iswindows() ? "-->" : "")
end
end

# Return the string of a Disjunction for plain printing
function JuMP.constraint_string(
mode::MIME"text/plain",
d::Disjunction
)
model = JuMP.owner_model(first(d.indicators))
mappings = _indicator_to_constraints(model)
disjuncts = Vector{String}(undef, length(d.indicators))
for (i, lvar) in enumerate(d.indicators)
disjunct = string("[", JuMP.function_string(mode, lvar), " ", _imply_symbol(mode), " {")
cons = (JuMP.constraint_string(mode, JuMP.constraint_object(cref)) for cref in mappings[lvar])
disjuncts[i] = string(disjunct, join(cons, "; "), "}]")
end
return join(disjuncts, " $(_dor_symbol(mode)) ")
end

# Return the string of a Disjunction for latex printing
function JuMP.constraint_string(
mode::MIME"text/latex",
d::Disjunction
)
model = JuMP.owner_model(first(d.indicators))
mappings = _indicator_to_constraints(model)
disjuncts = Vector{String}(undef, length(d.indicators))
for (i, lvar) in enumerate(d.indicators)
disjunct = string("\\begin{bmatrix}\n ", JuMP.function_string(mode, lvar), "\\\\\n ")
cons = (JuMP.constraint_string(mode, JuMP.constraint_object(cref)) for cref in mappings[lvar])
disjuncts[i] = string(disjunct, join(cons, "\\\\\n "), "\\end{bmatrix}")
end
return join(disjuncts, " $(_dor_symbol(mode)) ")
end

# Return the string of a DisjunctionRef
function JuMP.constraint_string(
mode::MIME,
dref::DisjunctionRef;
in_math_mode::Bool = false
)
label = JuMP.name(dref)
n = isempty(label) ? "" : label * " : "
d = JuMP.constraint_object(dref)
constr_str = JuMP.constraint_string(mode, d)
if mode == MIME("text/latex")
# Do not print names in text/latex mode.
if in_math_mode
return constr_str
else
return _wrap_in_math_mode(constr_str)
end
end
return n * JuMP.constraint_string(mode, d)
end

# Overload Base.show as needed
for RefType in (:DisjunctionRef, :DisjunctConstraintRef, :LogicalConstraintRef)
@eval begin
function Base.show(io::IO, cref::$RefType)
return print(io, JuMP.constraint_string(MIME("text/plain"), cref))
end

function Base.show(io::IO, ::MIME"text/latex", cref::$RefType)
return print(io, JuMP.constraint_string(MIME("text/latex"), cref))
end
end
end

################################################################################
# SUMMARY PRINTING
################################################################################

# TODO create functions to print and summaries GDPModels (to show GDPData contains)
136 changes: 136 additions & 0 deletions test/print.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
function test_disjunct_constraint_printing()
# Set up the model
model = GDPModel()
@variable(model, x[1:2])
@variable(model, Y[1:2], Logical)
@constraint(model, c1, x[1]^2 >= 3.2, Disjunct(Y[1]))
c2 = @constraint(model, x[2] <= 2, Disjunct(Y[2]))

# Test plain printing
if Sys.iswindows()
show_test(MIME("text/plain"), c1, "c1 : x[1]² >= 3.2, if Y[1] = True")
show_test(MIME("text/plain"), c2, "x[2] <= 2, if Y[2] = True")
else
show_test(MIME("text/plain"), c1, "c1 : x[1]² ≥ 3.2, if Y[1] = True")
show_test(MIME("text/plain"), c2, "x[2] ≤ 2, if Y[2] = True")
end

# Test math mode string
c1_str = "x_{1}^2 \\geq 3.2, \\; \\text{if } Y_{1} = \\text{True}"
@test constraint_string(MIME("text/latex"), c1, in_math_mode = true) == c1_str
c2_str = "x_{2} \\leq 2, \\; \\text{if } Y_{2} = \\text{True}"
@test constraint_string(MIME("text/latex"), c2, in_math_mode = true) == c2_str

# Test LaTeX printing
show_test(MIME("text/latex"), c1, "\$\$ $(c1_str) \$\$")
show_test(MIME("text/latex"), c2, "\$\$ $(c2_str) \$\$")
end

function test_disjunction_printing()
# Set up the model
model = GDPModel()
@variable(model, x[1:2])
@variable(model, Y[1:2], Logical)
@constraint(model, 2x[1]^2 >= 1, Disjunct(Y[1]))
@constraint(model, x[2] - 1 == 2.1, Disjunct(Y[2]))
@constraint(model, 0 <= x[1] <= 1, Disjunct(Y[2]))
@disjunction(model, d1, Y)
d2 = disjunction(model, Y)

# Test plain printing
if Sys.iswindows()
str = "[Y[1] --> {2 x[1]² >= 1}] or [Y[2] --> {x[2] == 3.1; x[1] in [0, 1]}]"
show_test(MIME("text/plain"), d1, "d1 : " * str)
show_test(MIME("text/plain"), d2, str)
else
str = "[Y[1] ⟹ {2 x[1]² ≥ 1}] ⋁ [Y[2] ⟹ {x[2] = 3.1; x[1] ∈ [0, 1]}]"
show_test(MIME("text/plain"), d1, "d1 : " * str)
show_test(MIME("text/plain"), d2, str)
end

# Test math mode string
str = "\\begin{bmatrix}\n Y_{1}\\\\\n 2 x_{1}^2 \\geq 1\\end{bmatrix} \\bigvee \\begin{bmatrix}\n Y_{2}\\\\\n x_{2} = 3.1\\\\\n x_{1} \\in [0, 1]\\end{bmatrix}"
@test constraint_string(MIME("text/latex"), d1, in_math_mode = true) == str
@test constraint_string(MIME("text/latex"), d2, in_math_mode = true) == str

# Test LaTeX printing
show_test(MIME("text/latex"), d1, "\$\$ $(str) \$\$")
show_test(MIME("text/latex"), d2, "\$\$ $(str) \$\$")
end

function test_nested_disjunction_printing()
# Set up the model
m = GDPModel()
@variable(m, 1 x[1:2] 9)
@variable(m, Y[1:2], Logical)
@variable(m, W[1:2], Logical)
@objective(m, Max, sum(x))
@constraint(m, y1[i=1:2], [1,4][i] x[i] [3,6][i], Disjunct(Y[1]))
@constraint(m, w1[i=1:2], [1,5][i] x[i] [2,6][i], Disjunct(W[1]))
@constraint(m, w2[i=1:2], [2,4][i] x[i] [3,5][i], Disjunct(W[2]))
@constraint(m, y2[i=1:2], [8,1][i] x[i] [9,2][i], Disjunct(Y[2]))
@disjunction(m, inner, [W[1], W[2]], Disjunct(Y[1]))
@disjunction(m, outer, [Y[1], Y[2]])

# Test plain printing
if Sys.iswindows()
inner = "[W[1] --> {x[1] in [1, 2]; x[2] in [5, 6]}] or [W[2] --> {x[1] in [2, 3]; x[2] in [4, 5]}]"
str = "outer : [Y[1] --> {x[1] in [1, 3]; x[2] in [4, 6]; $(inner)}] or [Y[2] --> {x[1] in [8, 9]; x[2] in [1, 2]}]"
show_test(MIME("text/plain"), outer, str)
else
inner = "[W[1] ⟹ {x[1] ∈ [1, 2]; x[2] ∈ [5, 6]}] ⋁ [W[2] ⟹ {x[1] ∈ [2, 3]; x[2] ∈ [4, 5]}]"
str = "outer : [Y[1] ⟹ {x[1] ∈ [1, 3]; x[2] ∈ [4, 6]; $(inner)}] ⋁ [Y[2] ⟹ {x[1] ∈ [8, 9]; x[2] ∈ [1, 2]}]"
show_test(MIME("text/plain"), outer, str)
end
inner = "\\begin{bmatrix}\n W_{1}\\\\\n x_{1} \\in [1, 2]\\\\\n x_{2} \\in [5, 6]\\end{bmatrix} \\bigvee \\begin{bmatrix}\n W_{2}\\\\\n x_{1} \\in [2, 3]\\\\\n x_{2} \\in [4, 5]\\end{bmatrix}"
str = "\$\$ \\begin{bmatrix}\n Y_{1}\\\\\n x_{1} \\in [1, 3]\\\\\n x_{2} \\in [4, 6]\\\\\n $(inner)\\end{bmatrix} \\bigvee \\begin{bmatrix}\n Y_{2}\\\\\n x_{1} \\in [8, 9]\\\\\n x_{2} \\in [1, 2]\\end{bmatrix} \$\$"
show_test(MIME("text/latex"), outer, str)
end

function test_logic_constraint_printing()
# Set up the model
model = GDPModel()
@variable(model, x[1:2])
@variable(model, Y[1:2], Logical)
@constraint(model, c1, ¬(Y[1] && Y[2]) == (Y[1] || Y[2]) := true)
c2 = @constraint(model, ¬(Y[1] && Y[2]) == (Y[1] || Y[2]) := true)

# Test plain printing
if Sys.iswindows()
show_test(MIME("text/plain"), c1, "c1 : !(Y[1] and Y[2]) <--> (Y[1] or Y[2]) = True")
show_test(MIME("text/plain"), c2, "!(Y[1] and Y[2]) <--> (Y[1] or Y[2]) = True")
else
show_test(MIME("text/plain"), c1, "c1 : ¬(Y[1] ∧ Y[2]) ⟺ (Y[1] ∨ Y[2]) = True")
show_test(MIME("text/plain"), c2, "¬(Y[1] ∧ Y[2]) ⟺ (Y[1] ∨ Y[2]) = True")
end

# Test LaTeX printing
str = "\$\$ {\\neg\\left({{Y[1]} \\wedge {Y[2]}}\\right)} \\iff {\\left({Y[1]} \\vee {Y[2]}\\right)} = \\text{True} \$\$"
show_test(MIME("text/latex"), c1, str)
show_test(MIME("text/latex"), c2, str)

# Test cardinality constraints
for (Set, s) in ((Exactly, "exactly"), (AtMost, "atmost"), (AtLeast, "atleast"))
c3 = @constraint(model, Y in Set(1), base_name = "c3")
c4 = @constraint(model, Y in Set(1))
str = "$s(1, Y[1], Y[2])"
show_test(MIME("text/plain"), c3, "c3 : $(str)")
show_test(MIME("text/plain"), c4, str)
str = "\$\$ \\text{$s}\\left(1, Y_{1}, Y_{2}\\right) \$\$"
show_test(MIME("text/latex"), c3, str)
show_test(MIME("text/latex"), c4, str)
end
end

@testset "Printing" begin
@testset "Disjunct Constraints" begin
test_disjunct_constraint_printing()
end
@testset "Disjunctions" begin
test_disjunction_printing()
test_nested_disjunction_printing()
end
@testset "Logical Constraints" begin
test_logic_constraint_printing()
end
end
1 change: 1 addition & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,5 @@ include("constraints/bigm.jl")
include("constraints/hull.jl")
include("constraints/fallback.jl")
include("constraints/disjunction.jl")
include("print.jl")
include("solve.jl")
20 changes: 20 additions & 0 deletions test/utilities.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,26 @@ macro test_macro_throws(errortype, m)
end
end

# Helper function to test IO methods work correctly
function show_test(mode, obj, exp_str::String; repl=:both)
if mode == MIME("text/plain")
repl != :show && @test sprint(print, obj) == exp_str
repl != :print && @test sprint(show, obj) == exp_str
else
@test sprint(show, "text/latex", obj) == exp_str
end
end

# Helper function for IO methods with different possibilities
function show_test(mode, obj, exp_str::Vector{String}; repl=:both)
if mode == MIME("text/plain")
repl != :show && @test sprint(print, obj) in exp_str
repl != :print && @test sprint(show, obj) in exp_str
else
@test sprint(show, "text/latex", obj) in exp_str
end
end

# Helper functions to prepare variable bounds without reformulating
function prep_bounds(vref, model, method)
if requires_variable_bound_info(method)
Expand Down

0 comments on commit d7e8e7a

Please sign in to comment.