Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[FileFormats.LP] add support for indicator constraints #2483

Merged
merged 8 commits into from
Apr 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading