Skip to content

Commit 8159c80

Browse files
authored
[FileFormats.LP] add support for indicator constraints (#2483)
1 parent f89036a commit 8159c80

File tree

2 files changed

+179
-3
lines changed

2 files changed

+179
-3
lines changed

src/FileFormats/LP/LP.jl

Lines changed: 101 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,18 +27,41 @@ function _print_shortest(io::IO, x::Float64)
2727
return
2828
end
2929

30+
const _ILT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}}
31+
const _IGT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.GreaterThan{T}}
32+
const _IET1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.EqualTo{T}}
33+
const _ILT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.LessThan{T}}
34+
const _IGT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.GreaterThan{T}}
35+
const _IET0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.EqualTo{T}}
36+
3037
MOI.Utilities.@model(
3138
Model,
3239
(MOI.ZeroOne, MOI.Integer),
3340
(MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval),
3441
(),
35-
(MOI.SOS1, MOI.SOS2),
42+
(MOI.SOS1, MOI.SOS2, _ILT1, _IET1, _IGT1, _ILT0, _IGT0, _IET0),
3643
(),
3744
(MOI.ScalarQuadraticFunction, MOI.ScalarAffineFunction),
3845
(MOI.VectorOfVariables,),
39-
()
46+
(MOI.VectorAffineFunction,)
4047
)
4148

49+
function MOI.supports_constraint(
50+
::Model{T},
51+
::Type{MOI.VectorAffineFunction{T}},
52+
::Type{MOI.SOS1{T}},
53+
) where {T}
54+
return false
55+
end
56+
57+
function MOI.supports_constraint(
58+
::Model{T},
59+
::Type{MOI.VectorAffineFunction{T}},
60+
::Type{MOI.SOS2{T}},
61+
) where {T}
62+
return false
63+
end
64+
4265
struct Options
4366
maximum_length::Int
4467
warn::Bool
@@ -98,6 +121,7 @@ function _write_function(
98121
::Model,
99122
func::MOI.ScalarAffineFunction{Float64},
100123
variable_names::Dict{MOI.VariableIndex,String};
124+
print_one::Bool = true,
101125
kwargs...,
102126
)
103127
is_first_item = true
@@ -108,7 +132,9 @@ function _write_function(
108132
for term in func.terms
109133
if !(term.coefficient 0.0)
110134
if is_first_item
111-
_print_shortest(io, term.coefficient)
135+
if print_one || !isone(term.coefficient)
136+
_print_shortest(io, term.coefficient)
137+
end
112138
is_first_item = false
113139
else
114140
print(io, term.coefficient < 0 ? " - " : " + ")
@@ -338,6 +364,62 @@ function _write_constraint(
338364
return
339365
end
340366

367+
function _write_indicator_constraints(
368+
io,
369+
model,
370+
::Type{S},
371+
variable_names,
372+
) where {S}
373+
F = MOI.VectorAffineFunction{Float64}
374+
for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO)
375+
Set = MOI.Indicator{A,S}
376+
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}())
377+
_write_constraint(
378+
io,
379+
model,
380+
index,
381+
variable_names;
382+
write_name = true,
383+
)
384+
end
385+
end
386+
F = MOI.VectorOfVariables
387+
for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO)
388+
Set = MOI.Indicator{A,S}
389+
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}())
390+
_write_constraint(
391+
io,
392+
model,
393+
index,
394+
variable_names;
395+
write_name = true,
396+
)
397+
end
398+
end
399+
return
400+
end
401+
402+
function _write_constraint(
403+
io::IO,
404+
model::Model{T},
405+
index::MOI.ConstraintIndex{F,MOI.Indicator{A,S}},
406+
variable_names::Dict{MOI.VariableIndex,String};
407+
write_name::Bool = true,
408+
) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},A,S}
409+
func = MOI.get(model, MOI.ConstraintFunction(), index)
410+
set = MOI.get(model, MOI.ConstraintSet(), index)
411+
if write_name
412+
print(io, MOI.get(model, MOI.ConstraintName(), index), ": ")
413+
end
414+
z, f = MOI.Utilities.scalarize(func)
415+
flag = A == MOI.ACTIVATE_ON_ONE ? 1 : 0
416+
_write_function(io, model, z, variable_names; print_one = false)
417+
print(io, " = ", flag, " -> ")
418+
_write_function(io, model, f, variable_names)
419+
_write_constraint_suffix(io, set.set)
420+
return
421+
end
422+
341423
"""
342424
Base.write(io::IO, model::FileFormats.LP.Model)
343425
@@ -364,6 +446,7 @@ function Base.write(io::IO, model::Model)
364446
println(io, "subject to")
365447
for S in _SCALAR_SETS
366448
_write_constraints(io, model, S, variable_names)
449+
_write_indicator_constraints(io, model, S, variable_names)
367450
end
368451
println(io, "Bounds")
369452
CI = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}
@@ -456,6 +539,7 @@ mutable struct _ReadCache
456539
num_constraints::Int
457540
name_to_variable::Dict{String,MOI.VariableIndex}
458541
has_default_bound::Set{MOI.VariableIndex}
542+
indicator::Union{Nothing,Pair{MOI.VariableIndex,MOI.ActivationCondition}}
459543
function _ReadCache()
460544
return new(
461545
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0),
@@ -466,6 +550,7 @@ mutable struct _ReadCache
466550
0,
467551
Dict{String,MOI.VariableIndex}(),
468552
Set{MOI.VariableIndex}(),
553+
nothing,
469554
)
470555
end
471556
end
@@ -684,6 +769,14 @@ function _parse_section(
684769
cache.constraint_name = "R$(cache.num_constraints)"
685770
end
686771
end
772+
if cache.indicator === nothing
773+
if (m = match(r"\s*(.+?)\s*=\s*(0|1)\s*->(.+)", line)) !== nothing
774+
z = _get_variable_from_name(model, cache, String(m[1]))
775+
cond = m[2] == "0" ? MOI.ACTIVATE_ON_ZERO : MOI.ACTIVATE_ON_ONE
776+
cache.indicator = z => cond
777+
line = String(m[3])
778+
end
779+
end
687780
if occursin("^", line)
688781
# Simplify parsing of constraints with ^2 terms by turning them into
689782
# explicit " ^ 2" terms. This avoids ambiguity when parsing names.
@@ -723,13 +816,18 @@ function _parse_section(
723816
cache.constraint_function.constant,
724817
)
725818
end
819+
if cache.indicator !== nothing
820+
f = MOI.Utilities.operate(vcat, Float64, cache.indicator[1], f)
821+
constraint_set = MOI.Indicator{cache.indicator[2]}(constraint_set)
822+
end
726823
c = MOI.add_constraint(model, f, constraint_set)
727824
MOI.set(model, MOI.ConstraintName(), c, cache.constraint_name)
728825
cache.num_constraints += 1
729826
empty!(cache.constraint_function.terms)
730827
empty!(cache.quad_terms)
731828
cache.constraint_function.constant = 0.0
732829
cache.constraint_name = ""
830+
cache.indicator = nothing
733831
end
734832
return
735833
end

test/FileFormats/LP/LP.jl

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,6 +388,41 @@ c: 1.1 * x + 1.2 * y + -1.1 * x * x + 1.5*x*y + 1.3 in Interval(-1.1, 1.4)
388388
return
389389
end
390390

391+
function test_write_indicator()
392+
model = LP.Model()
393+
MOI.Utilities.loadfromstring!(
394+
model,
395+
"""
396+
variables: x, z
397+
c1: [z, x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0))
398+
c2: [z, x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0))
399+
c3: [z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2))
400+
401+
c4: [z, 2.0 * x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0))
402+
c5: [z, 3.0 * x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0))
403+
c6: [1.0 * z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2))
404+
z in ZeroOne()
405+
""",
406+
)
407+
MOI.write_to_file(model, LP_TEST_FILE)
408+
@test read(LP_TEST_FILE, String) ==
409+
"minimize\n" *
410+
"obj: \n" *
411+
"subject to\n" *
412+
"c4: z = 1 -> 2 x <= 0\n" *
413+
"c1: z = 1 -> x <= 0\n" *
414+
"c5: z = 0 -> 3 x >= 2\n" *
415+
"c2: z = 0 -> x >= 2\n" *
416+
"c6: z = 1 -> 1 x = 1.2\n" *
417+
"c3: z = 1 -> x = 1.2\n" *
418+
"Bounds\n" *
419+
"x free\n" *
420+
"Binary\n" *
421+
"z\n" *
422+
"End\n"
423+
return
424+
end
425+
391426
###
392427
### Read tests
393428
###
@@ -976,6 +1011,49 @@ function test_read_variable_bounds()
9761011
return
9771012
end
9781013

1014+
function test_read_indicator()
1015+
io = IOBuffer("""
1016+
minimize
1017+
obj: 1 x
1018+
subject to
1019+
c: z = 1 -> x >= 0
1020+
d: z = 0 -> x - y <= 1.2
1021+
bounds
1022+
x free
1023+
z free
1024+
binary
1025+
z
1026+
end
1027+
""")
1028+
model = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP)
1029+
read!(io, model)
1030+
io = IOBuffer()
1031+
write(io, model)
1032+
seekstart(io)
1033+
@test read(io, String) == """
1034+
minimize
1035+
obj: 1 x
1036+
subject to
1037+
d: z = 0 -> 1 x - 1 y <= 1.2
1038+
c: z = 1 -> 1 x >= 0
1039+
Bounds
1040+
x free
1041+
y >= 0
1042+
Binary
1043+
z
1044+
End
1045+
"""
1046+
return
1047+
end
1048+
1049+
function test_VectorAffineFunction_SOS()
1050+
model = MOI.FileFormats.LP.Model()
1051+
F = MOI.VectorAffineFunction{Float64}
1052+
@test !MOI.supports_constraint(model, F, MOI.SOS1{Float64})
1053+
@test !MOI.supports_constraint(model, F, MOI.SOS2{Float64})
1054+
return
1055+
end
1056+
9791057
function runtests()
9801058
for name in names(@__MODULE__, all = true)
9811059
if startswith("$(name)", "test_")

0 commit comments

Comments
 (0)