Skip to content

Commit 5d07102

Browse files
authored
[FileFormats.MPS] add Indicator support (#1957)
1 parent ab897af commit 5d07102

File tree

3 files changed

+250
-5
lines changed

3 files changed

+250
-5
lines changed

src/FileFormats/MPS/MPS.jl

Lines changed: 151 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,18 +25,60 @@ function print_shortest(io::IO, x::Real)
2525
return
2626
end
2727

28+
const IndicatorLessThanTrue{T} =
29+
MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}}
30+
31+
const IndicatorGreaterThanTrue{T} =
32+
MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.GreaterThan{T}}
33+
34+
const IndicatorLessThanFalse{T} =
35+
MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.LessThan{T}}
36+
37+
const IndicatorGreaterThanFalse{T} =
38+
MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.GreaterThan{T}}
39+
2840
MOI.Utilities.@model(
2941
Model,
3042
(MOI.ZeroOne, MOI.Integer),
3143
(MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval),
3244
(),
33-
(MOI.SOS1, MOI.SOS2),
45+
(
46+
MOI.SOS1,
47+
MOI.SOS2,
48+
IndicatorLessThanTrue,
49+
IndicatorLessThanFalse,
50+
IndicatorGreaterThanTrue,
51+
IndicatorGreaterThanFalse,
52+
),
3453
(),
3554
(MOI.ScalarAffineFunction, MOI.ScalarQuadraticFunction),
3655
(MOI.VectorOfVariables,),
37-
()
56+
(MOI.VectorAffineFunction,)
3857
)
3958

59+
function MOI.supports_constraint(
60+
::Model{T},
61+
::Type{MOI.VectorAffineFunction{T}},
62+
::Type{<:Union{MOI.SOS1{T},MOI.SOS2{T}}},
63+
) where {T}
64+
return false
65+
end
66+
67+
function MOI.supports_constraint(
68+
::Model{T},
69+
::Type{MOI.VectorOfVariables},
70+
::Type{
71+
<:Union{
72+
IndicatorLessThanTrue{T},
73+
IndicatorLessThanFalse{T},
74+
IndicatorGreaterThanTrue{T},
75+
IndicatorGreaterThanFalse{T},
76+
},
77+
},
78+
) where {T}
79+
return false
80+
end
81+
4082
@enum(
4183
QuadraticFormat,
4284
kQuadraticFormatCPLEX,
@@ -216,7 +258,8 @@ function Base.write(io::IO, model::Model)
216258
flip_obj = MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
217259
end
218260
write_rows(io, model)
219-
obj_const = write_columns(io, model, flip_obj, ordered_names, names)
261+
obj_const, indicators =
262+
write_columns(io, model, flip_obj, ordered_names, names)
220263
write_rhs(io, model, obj_const)
221264
write_ranges(io, model)
222265
write_bounds(io, model, ordered_names, names)
@@ -230,6 +273,7 @@ function Base.write(io::IO, model::Model)
230273
# CPLEX needs qcons _after_ SOS.
231274
write_quadcons(io, model, ordered_names, var_to_column)
232275
end
276+
write_indicators(io, indicators)
233277
println(io, "ENDATA")
234278
return
235279
end
@@ -294,6 +338,11 @@ function write_rows(io::IO, model::Model)
294338
_write_rows(io, model, F, set_type, sense_char)
295339
end
296340
end
341+
F = MOI.VectorAffineFunction{Float64}
342+
_write_rows(io, model, F, IndicatorLessThanTrue{Float64}, "L")
343+
_write_rows(io, model, F, IndicatorLessThanFalse{Float64}, "L")
344+
_write_rows(io, model, F, IndicatorGreaterThanTrue{Float64}, "G")
345+
_write_rows(io, model, F, IndicatorGreaterThanFalse{Float64}, "G")
297346
return
298347
end
299348

@@ -363,6 +412,25 @@ function _collect_coefficients(
363412
return
364413
end
365414

415+
_activation_condition(::Type{<:MOI.Indicator{A}}) where {A} = A
416+
417+
function _collect_indicator(model, S, names, coefficients, indicators)
418+
F = MOI.VectorAffineFunction{Float64}
419+
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
420+
row_name = MOI.get(model, MOI.ConstraintName(), index)
421+
func = MOI.get(model, MOI.ConstraintFunction(), index)
422+
funcs = MOI.Utilities.eachscalar(func)
423+
z = convert(MOI.VariableIndex, funcs[1])
424+
_extract_terms(names, coefficients, row_name, funcs[2])
425+
condition = _activation_condition(S)
426+
push!(
427+
indicators,
428+
(row_name, MOI.get(model, MOI.VariableName(), z), condition),
429+
)
430+
end
431+
return
432+
end
433+
366434
function _get_objective(model)
367435
F = MOI.get(model, MOI.ObjectiveFunctionType())
368436
f = MOI.get(model, MOI.ObjectiveFunction{F}())
@@ -373,6 +441,7 @@ function _get_objective(model)
373441
end
374442

375443
function write_columns(io::IO, model::Model, flip_obj, ordered_names, names)
444+
indicators = Tuple{String,String,MOI.ActivationCondition}[]
376445
coefficients = Dict{String,Vector{Tuple{String,Float64}}}(
377446
n => Tuple{String,Float64}[] for n in ordered_names
378447
)
@@ -382,6 +451,14 @@ function write_columns(io::IO, model::Model, flip_obj, ordered_names, names)
382451
_collect_coefficients(model, F, S, names, coefficients)
383452
end
384453
end
454+
for S in (
455+
IndicatorLessThanTrue{Float64},
456+
IndicatorLessThanFalse{Float64},
457+
IndicatorGreaterThanTrue{Float64},
458+
IndicatorGreaterThanFalse{Float64},
459+
)
460+
_collect_indicator(model, S, names, coefficients, indicators)
461+
end
385462
# Build objective
386463
obj_func = _get_objective(model)
387464
_extract_terms(names, coefficients, "OBJ", obj_func, flip_obj)
@@ -413,7 +490,7 @@ function write_columns(io::IO, model::Model, flip_obj, ordered_names, names)
413490
)
414491
end
415492
end
416-
return obj_func.constant
493+
return obj_func.constant, indicators
417494
end
418495

419496
# ==============================================================================
@@ -423,6 +500,7 @@ end
423500
_value(set::MOI.LessThan) = set.upper
424501
_value(set::MOI.GreaterThan) = set.lower
425502
_value(set::MOI.EqualTo) = set.value
503+
_value(set::MOI.Indicator) = _value(set.set)
426504

427505
function _write_rhs(io, model, F, S)
428506
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
@@ -467,6 +545,11 @@ function write_rhs(io::IO, model::Model, obj_const)
467545
_write_rhs(io, model, F, set_type)
468546
end
469547
end
548+
F = MOI.VectorAffineFunction{Float64}
549+
_write_rhs(io, model, F, IndicatorLessThanTrue{Float64})
550+
_write_rhs(io, model, F, IndicatorLessThanFalse{Float64})
551+
_write_rhs(io, model, F, IndicatorGreaterThanTrue{Float64})
552+
_write_rhs(io, model, F, IndicatorGreaterThanFalse{Float64})
470553
# Objective constants are added to the RHS as a negative offset.
471554
# https://www.ibm.com/docs/en/icos/20.1.0?topic=standard-records-in-mps-format
472555
if !iszero(obj_const)
@@ -786,6 +869,25 @@ function write_sos(io::IO, model::Model, names)
786869
return
787870
end
788871

872+
# ==============================================================================
873+
# INDICATORS
874+
# ==============================================================================
875+
876+
function write_indicators(io::IO, indicators)
877+
if isempty(indicators)
878+
return
879+
end
880+
println(io, "INDICATORS")
881+
for (row, var, condition) in indicators
882+
if condition == MOI.ACTIVATE_ON_ONE
883+
println(io, Card(f1 = "IF", f2 = row, f3 = var, f4 = "1"))
884+
else
885+
println(io, Card(f1 = "IF", f2 = row, f3 = var, f4 = "0"))
886+
end
887+
end
888+
return
889+
end
890+
789891
# ==============================================================================
790892
#
791893
# Base.read!
@@ -861,6 +963,7 @@ mutable struct TempMPSModel
861963
quad_obj::Vector{Tuple{String,String,Float64}}
862964
qc_matrix::Dict{String,Vector{Tuple{String,String,Float64}}}
863965
current_qc_matrix::String
966+
indicators::Dict{String,Tuple{String,MOI.ActivationCondition}}
864967
end
865968

866969
function TempMPSModel()
@@ -886,6 +989,7 @@ function TempMPSModel()
886989
Tuple{String,String,Float64}[],
887990
Dict{String,Vector{Tuple{String,String,Float64}}}(),
888991
"",
992+
Dict{String,Tuple{String,MOI.ActivationCondition}}(),
889993
)
890994
end
891995

@@ -905,6 +1009,7 @@ end
9051009
HEADER_QMATRIX,
9061010
HEADER_QCMATRIX,
9071011
HEADER_QSECTION,
1012+
HEADER_INDICATORS,
9081013
)
9091014

9101015
# Headers(s) gets called _alot_ (on every line), so we try very hard to be
@@ -941,6 +1046,10 @@ function Headers(s::AbstractString)
9411046
return HEADER_QMATRIX
9421047
end
9431048
end
1049+
elseif N == 10
1050+
if (x == 'I' || x == 'i') && uppercase(s) == "INDICATORS"
1051+
return HEADER_INDICATORS
1052+
end
9441053
elseif N == 12
9451054
if (x == 'O' || x == 'o') && startswith(uppercase(s), "OBJSENSE")
9461055
return HEADER_OBJSENSE
@@ -1028,6 +1137,8 @@ function Base.read!(io::IO, model::Model)
10281137
parse_qcmatrix_line(data, items)
10291138
elseif header == HEADER_QSECTION
10301139
parse_qsection_line(data, items)
1140+
elseif header == HEADER_INDICATORS
1141+
parse_indicators_line(data, items)
10311142
else
10321143
@assert header == HEADER_ENDATA
10331144
break
@@ -1119,12 +1230,30 @@ end
11191230
function _add_constraint(model, data, variable_map, j, c_name, set)
11201231
if haskey(data.qc_matrix, c_name)
11211232
_add_quad_constraint(model, data, variable_map, j, c_name, set)
1233+
elseif haskey(data.indicators, c_name)
1234+
_add_indicator_constraint(model, data, variable_map, j, c_name, set)
11221235
else
11231236
_add_linear_constraint(model, data, variable_map, j, c_name, set)
11241237
end
11251238
return
11261239
end
11271240

1241+
function _add_indicator_constraint(model, data, variable_map, j, c_name, set)
1242+
z, activate = data.indicators[c_name]
1243+
terms = MOI.VectorAffineTerm{Float64}[MOI.VectorAffineTerm(
1244+
1,
1245+
MOI.ScalarAffineTerm(1.0, variable_map[z]),
1246+
),]
1247+
for (i, coef) in data.A[j]
1248+
scalar = MOI.ScalarAffineTerm(coef, variable_map[data.col_to_name[i]])
1249+
push!(terms, MOI.VectorAffineTerm(2, scalar))
1250+
end
1251+
f = MOI.VectorAffineFunction(terms, [0.0, 0.0])
1252+
c = MOI.add_constraint(model, f, MOI.Indicator{activate}(set))
1253+
MOI.set(model, MOI.ConstraintName(), c, c_name)
1254+
return
1255+
end
1256+
11281257
function _add_linear_constraint(model, data, variable_map, j, c_name, set)
11291258
terms = MOI.ScalarAffineTerm{Float64}[
11301259
MOI.ScalarAffineTerm(coef, variable_map[data.col_to_name[i]]) for
@@ -1579,4 +1708,22 @@ function parse_qsection_line(data, items)
15791708
return
15801709
end
15811710

1711+
# ==============================================================================
1712+
# INDICATORS
1713+
# ==============================================================================
1714+
1715+
function parse_indicators_line(data, items)
1716+
if length(items) != 4
1717+
error("Malformed INDICATORS line: $(join(items, " "))")
1718+
end
1719+
condition = if items[4] == "0"
1720+
MOI.ACTIVATE_ON_ZERO
1721+
else
1722+
@assert items[4] == "1"
1723+
MOI.ACTIVATE_ON_ONE
1724+
end
1725+
data.indicators[items[2]] = (items[3], condition)
1726+
return
1727+
end
1728+
15821729
end

test/FileFormats/MPS/MPS.jl

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,13 @@ const MOIU = MOI.Utilities
1414
const MPS = MOI.FileFormats.MPS
1515
const MPS_TEST_FILE = "test.mps"
1616

17-
function _test_model_equality(model_string, variables, constraints; kwargs...)
17+
function _test_model_equality(
18+
model_string,
19+
variables,
20+
constraints,
21+
args...;
22+
kwargs...,
23+
)
1824
model = MPS.Model(; kwargs...)
1925
MOIU.loadfromstring!(model, model_string)
2026
MOI.write_to_file(model, MPS_TEST_FILE)
@@ -25,6 +31,7 @@ function _test_model_equality(model_string, variables, constraints; kwargs...)
2531
model_2,
2632
variables,
2733
constraints,
34+
args...,
2835
)
2936
end
3037

@@ -870,6 +877,81 @@ c1: 1.2x + 2.1 * x * x + 1.2 * x * y + 0.2 * y * x + 0.5 * y * y <= 1.0
870877
return
871878
end
872879

880+
function test_round_trip_indicator_lessthan()
881+
_test_model_equality(
882+
"""
883+
variables: x, y, z
884+
minobjective: 1.0 * x + y + z
885+
z in ZeroOne()
886+
c1: [z, 1.0 * x + 1.0 * y] in Indicator{ACTIVATE_ON_ONE}(LessThan(1.0))
887+
""",
888+
["x", "y", "z"],
889+
["c1"],
890+
[("z", MOI.ZeroOne())],
891+
)
892+
return
893+
end
894+
895+
function test_round_trip_indicator_greaterthan()
896+
_test_model_equality(
897+
"""
898+
variables: x, y, z
899+
minobjective: 1.0 * x + y + z
900+
z in ZeroOne()
901+
c1: [z, 1.0 * x + 1.0 * y] in Indicator{ACTIVATE_ON_ONE}(GreaterThan(1.0))
902+
""",
903+
["x", "y", "z"],
904+
["c1"],
905+
[("z", MOI.ZeroOne())],
906+
)
907+
return
908+
end
909+
910+
function test_round_trip_indicator_lessthan_false()
911+
_test_model_equality(
912+
"""
913+
variables: x, y, z
914+
minobjective: 1.0 * x + y + z
915+
z in ZeroOne()
916+
c1: [z, 1.0 * x + 1.0 * y] in Indicator{ACTIVATE_ON_ZERO}(LessThan(1.0))
917+
""",
918+
["x", "y", "z"],
919+
["c1"],
920+
[("z", MOI.ZeroOne())],
921+
)
922+
return
923+
end
924+
925+
function test_round_trip_indicator_greaterthan_false()
926+
_test_model_equality(
927+
"""
928+
variables: x, y, z
929+
minobjective: 1.0 * x + y + z
930+
z in ZeroOne()
931+
c1: [z, 1.0 * x + 1.0 * y] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(1.0))
932+
""",
933+
["x", "y", "z"],
934+
["c1"],
935+
[("z", MOI.ZeroOne())],
936+
)
937+
return
938+
end
939+
940+
function test_vector_supports_constraint()
941+
model = MPS.Model()
942+
@test !MOI.supports_constraint(
943+
model,
944+
MOI.VectorOfVariables,
945+
MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{Float64}},
946+
)
947+
@test !MOI.supports_constraint(
948+
model,
949+
MOI.VectorAffineFunction{Float64},
950+
MOI.SOS1{Float64},
951+
)
952+
return
953+
end
954+
873955
function runtests()
874956
for name in names(@__MODULE__, all = true)
875957
if startswith("$(name)", "test_")

0 commit comments

Comments
 (0)