Skip to content

Commit

Permalink
[FileFormats.LP] add support for indicator constraints (#2483)
Browse files Browse the repository at this point in the history
  • Loading branch information
odow authored Apr 15, 2024
1 parent f89036a commit 8159c80
Show file tree
Hide file tree
Showing 2 changed files with 179 additions and 3 deletions.
104 changes: 101 additions & 3 deletions src/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,41 @@ function _print_shortest(io::IO, x::Float64)
return
end

const _ILT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.LessThan{T}}
const _IGT1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.GreaterThan{T}}
const _IET1{T} = MOI.Indicator{MOI.ACTIVATE_ON_ONE,MOI.EqualTo{T}}
const _ILT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.LessThan{T}}
const _IGT0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.GreaterThan{T}}
const _IET0{T} = MOI.Indicator{MOI.ACTIVATE_ON_ZERO,MOI.EqualTo{T}}

MOI.Utilities.@model(
Model,
(MOI.ZeroOne, MOI.Integer),
(MOI.EqualTo, MOI.GreaterThan, MOI.LessThan, MOI.Interval),
(),
(MOI.SOS1, MOI.SOS2),
(MOI.SOS1, MOI.SOS2, _ILT1, _IET1, _IGT1, _ILT0, _IGT0, _IET0),
(),
(MOI.ScalarQuadraticFunction, MOI.ScalarAffineFunction),
(MOI.VectorOfVariables,),
()
(MOI.VectorAffineFunction,)
)

function MOI.supports_constraint(
::Model{T},
::Type{MOI.VectorAffineFunction{T}},
::Type{MOI.SOS1{T}},
) where {T}
return false
end

function MOI.supports_constraint(
::Model{T},
::Type{MOI.VectorAffineFunction{T}},
::Type{MOI.SOS2{T}},
) where {T}
return false
end

struct Options
maximum_length::Int
warn::Bool
Expand Down Expand Up @@ -98,6 +121,7 @@ function _write_function(
::Model,
func::MOI.ScalarAffineFunction{Float64},
variable_names::Dict{MOI.VariableIndex,String};
print_one::Bool = true,
kwargs...,
)
is_first_item = true
Expand All @@ -108,7 +132,9 @@ function _write_function(
for term in func.terms
if !(term.coefficient 0.0)
if is_first_item
_print_shortest(io, term.coefficient)
if print_one || !isone(term.coefficient)
_print_shortest(io, term.coefficient)
end
is_first_item = false
else
print(io, term.coefficient < 0 ? " - " : " + ")
Expand Down Expand Up @@ -338,6 +364,62 @@ function _write_constraint(
return
end

function _write_indicator_constraints(
io,
model,
::Type{S},
variable_names,
) where {S}
F = MOI.VectorAffineFunction{Float64}
for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO)
Set = MOI.Indicator{A,S}
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}())
_write_constraint(
io,
model,
index,
variable_names;
write_name = true,
)
end
end
F = MOI.VectorOfVariables
for A in (MOI.ACTIVATE_ON_ONE, MOI.ACTIVATE_ON_ZERO)
Set = MOI.Indicator{A,S}
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,Set}())
_write_constraint(
io,
model,
index,
variable_names;
write_name = true,
)
end
end
return
end

function _write_constraint(
io::IO,
model::Model{T},
index::MOI.ConstraintIndex{F,MOI.Indicator{A,S}},
variable_names::Dict{MOI.VariableIndex,String};
write_name::Bool = true,
) where {T,F<:Union{MOI.VectorOfVariables,MOI.VectorAffineFunction{T}},A,S}
func = MOI.get(model, MOI.ConstraintFunction(), index)
set = MOI.get(model, MOI.ConstraintSet(), index)
if write_name
print(io, MOI.get(model, MOI.ConstraintName(), index), ": ")
end
z, f = MOI.Utilities.scalarize(func)
flag = A == MOI.ACTIVATE_ON_ONE ? 1 : 0
_write_function(io, model, z, variable_names; print_one = false)
print(io, " = ", flag, " -> ")
_write_function(io, model, f, variable_names)
_write_constraint_suffix(io, set.set)
return
end

"""
Base.write(io::IO, model::FileFormats.LP.Model)
Expand All @@ -364,6 +446,7 @@ function Base.write(io::IO, model::Model)
println(io, "subject to")
for S in _SCALAR_SETS
_write_constraints(io, model, S, variable_names)
_write_indicator_constraints(io, model, S, variable_names)
end
println(io, "Bounds")
CI = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}
Expand Down Expand Up @@ -456,6 +539,7 @@ mutable struct _ReadCache
num_constraints::Int
name_to_variable::Dict{String,MOI.VariableIndex}
has_default_bound::Set{MOI.VariableIndex}
indicator::Union{Nothing,Pair{MOI.VariableIndex,MOI.ActivationCondition}}
function _ReadCache()
return new(
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm{Float64}[], 0.0),
Expand All @@ -466,6 +550,7 @@ mutable struct _ReadCache
0,
Dict{String,MOI.VariableIndex}(),
Set{MOI.VariableIndex}(),
nothing,
)
end
end
Expand Down Expand Up @@ -684,6 +769,14 @@ function _parse_section(
cache.constraint_name = "R$(cache.num_constraints)"
end
end
if cache.indicator === nothing
if (m = match(r"\s*(.+?)\s*=\s*(0|1)\s*->(.+)", line)) !== nothing
z = _get_variable_from_name(model, cache, String(m[1]))
cond = m[2] == "0" ? MOI.ACTIVATE_ON_ZERO : MOI.ACTIVATE_ON_ONE
cache.indicator = z => cond
line = String(m[3])
end
end
if occursin("^", line)
# Simplify parsing of constraints with ^2 terms by turning them into
# explicit " ^ 2" terms. This avoids ambiguity when parsing names.
Expand Down Expand Up @@ -723,13 +816,18 @@ function _parse_section(
cache.constraint_function.constant,
)
end
if cache.indicator !== nothing
f = MOI.Utilities.operate(vcat, Float64, cache.indicator[1], f)
constraint_set = MOI.Indicator{cache.indicator[2]}(constraint_set)
end
c = MOI.add_constraint(model, f, constraint_set)
MOI.set(model, MOI.ConstraintName(), c, cache.constraint_name)
cache.num_constraints += 1
empty!(cache.constraint_function.terms)
empty!(cache.quad_terms)
cache.constraint_function.constant = 0.0
cache.constraint_name = ""
cache.indicator = nothing
end
return
end
Expand Down
78 changes: 78 additions & 0 deletions test/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
return
end

function test_write_indicator()
model = LP.Model()
MOI.Utilities.loadfromstring!(
model,
"""
variables: x, z
c1: [z, x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0))
c2: [z, x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0))
c3: [z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2))
c4: [z, 2.0 * x] in Indicator{ACTIVATE_ON_ONE}(LessThan(0.0))
c5: [z, 3.0 * x] in Indicator{ACTIVATE_ON_ZERO}(GreaterThan(2.0))
c6: [1.0 * z, x] in Indicator{ACTIVATE_ON_ONE}(EqualTo(1.2))
z in ZeroOne()
""",
)
MOI.write_to_file(model, LP_TEST_FILE)
@test read(LP_TEST_FILE, String) ==
"minimize\n" *
"obj: \n" *
"subject to\n" *
"c4: z = 1 -> 2 x <= 0\n" *
"c1: z = 1 -> x <= 0\n" *
"c5: z = 0 -> 3 x >= 2\n" *
"c2: z = 0 -> x >= 2\n" *
"c6: z = 1 -> 1 x = 1.2\n" *
"c3: z = 1 -> x = 1.2\n" *
"Bounds\n" *
"x free\n" *
"Binary\n" *
"z\n" *
"End\n"
return
end

###
### Read tests
###
Expand Down Expand Up @@ -976,6 +1011,49 @@ function test_read_variable_bounds()
return
end

function test_read_indicator()
io = IOBuffer("""
minimize
obj: 1 x
subject to
c: z = 1 -> x >= 0
d: z = 0 -> x - y <= 1.2
bounds
x free
z free
binary
z
end
""")
model = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP)
read!(io, model)
io = IOBuffer()
write(io, model)
seekstart(io)
@test read(io, String) == """
minimize
obj: 1 x
subject to
d: z = 0 -> 1 x - 1 y <= 1.2
c: z = 1 -> 1 x >= 0
Bounds
x free
y >= 0
Binary
z
End
"""
return
end

function test_VectorAffineFunction_SOS()
model = MOI.FileFormats.LP.Model()
F = MOI.VectorAffineFunction{Float64}
@test !MOI.supports_constraint(model, F, MOI.SOS1{Float64})
@test !MOI.supports_constraint(model, F, MOI.SOS2{Float64})
return
end

function runtests()
for name in names(@__MODULE__, all = true)
if startswith("$(name)", "test_")
Expand Down

0 comments on commit 8159c80

Please sign in to comment.