Skip to content

Commit

Permalink
[FileFormats.LP] fix reading file with free variable and explicit bou…
Browse files Browse the repository at this point in the history
…nds (#2225)
  • Loading branch information
odow authored Jun 25, 2023
1 parent 0ba620d commit 5afc08b
Show file tree
Hide file tree
Showing 2 changed files with 109 additions and 60 deletions.
76 changes: 45 additions & 31 deletions src/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -298,15 +298,6 @@ function _write_constraints(io, model, S, variable_names)
return
end

function _write_bounds(io, model, S, variable_names, free_variables)
F = MOI.VariableIndex
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
delete!(free_variables, MOI.VariableIndex(index.value))
_write_constraint(io, model, index, variable_names; write_name = false)
end
return
end

function _write_sos_constraints(io, model, variable_names)
T, F = Float64, MOI.VectorOfVariables
sos1_indices = MOI.get(model, MOI.ListOfConstraintIndices{F,MOI.SOS1{T}}())
Expand Down Expand Up @@ -375,19 +366,31 @@ function Base.write(io::IO, model::Model)
_write_constraints(io, model, S, variable_names)
end
println(io, "Bounds")
for S in _SCALAR_SETS
_write_bounds(io, model, S, variable_names, free_variables)
end
# If a variable is binary, it should not be listed as `free` in the bounds
# section.
attr = MOI.ListOfConstraintIndices{MOI.VariableIndex,MOI.ZeroOne}()
for index in MOI.get(model, attr)
delete!(free_variables, MOI.VariableIndex(index.value))
end
# By default, variables have bounds of [0, ∞), so we need to explicitly
# declare variables as free.
for variable in sort(collect(free_variables), by = x -> x.value)
println(io, variable_names[variable], " free")
CI = MOI.ConstraintIndex{MOI.VariableIndex,MOI.ZeroOne}
for x in MOI.get(model, MOI.ListOfVariableIndices())
lb, ub = MOI.Utilities.get_bounds(model, Float64, x)
if lb == -Inf && ub == Inf
if MOI.is_valid(model, CI(x.value))
# If a variable is binary, it should not be listed as `free` in
# the bounds section.
continue
end
print(io, variable_names[x], " free")
elseif lb == ub
print(io, variable_names[x], " = ")
_print_shortest(io, lb)
elseif lb == -Inf
print(io, "-infinity <= ", variable_names[x], " <= ")
_print_shortest(io, ub)
elseif ub == Inf
print(io, variable_names[x], " >= ")
_print_shortest(io, lb)
else
_print_shortest(io, lb)
print(io, " <= ", variable_names[x], " <= ")
_print_shortest(io, ub)
end
println(io)
end
_write_integrality(io, model, "General", MOI.Integer, variable_names)
_write_integrality(io, model, "Binary", MOI.ZeroOne, variable_names)
Expand Down Expand Up @@ -759,7 +762,7 @@ function _parse_section(
_delete_default_lower_bound_if_present(model, cache, x)
return
end
lb, ub, name = -Inf, Inf, ""
lb, ub, name = nothing, nothing, ""
if length(tokens) == 5
name = tokens[3]
if _is_less_than(tokens[2]) && _is_less_than(tokens[4])
Expand Down Expand Up @@ -810,16 +813,27 @@ function _parse_section(
error("Unable to parse bound: $(line)")
end
x = _get_variable_from_name(model, cache, name)
if lb == ub
_delete_default_lower_bound_if_present(model, cache, x)
MOI.add_constraint(model, x, MOI.EqualTo(lb))
elseif -Inf < lb < ub < Inf
_delete_default_lower_bound_if_present(model, cache, x)
MOI.add_constraint(model, x, MOI.Interval(lb, ub))
elseif -Inf < lb
if lb !== nothing && ub !== nothing
if lb == ub
_delete_default_lower_bound_if_present(model, cache, x)
MOI.add_constraint(model, x, MOI.EqualTo(lb))
return
elseif -Inf < lb < ub < Inf
_delete_default_lower_bound_if_present(model, cache, x)
MOI.add_constraint(model, x, MOI.Interval(lb, ub))
return
elseif lb == -Inf
_delete_default_lower_bound_if_present(model, cache, x)
if ub == Inf
return # Explicitly free variable
end
end
end
if lb !== nothing && -Inf < lb
_delete_default_lower_bound_if_present(model, cache, x)
MOI.add_constraint(model, x, MOI.GreaterThan(lb))
else
end
if ub !== nothing && ub < Inf
if ub < 0
# We only need to delete the default lower bound if the upper bound
# is less than 0.
Expand Down
93 changes: 64 additions & 29 deletions test/FileFormats/LP/LP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,10 @@ c12: [x, y, z] in SOS2{Float64}([3.3, 1.1, 2.2])
"c7: 1.6 + 1.5 a = 0.2\n" *
"c8: 0.3 <= 1.8 + 1.7 a <= 0.4\n" *
"Bounds\n" *
"x <= 2\n" *
"x >= -1\n" *
"a free\n" *
"-1 <= x <= 2\n" *
"y = 3\n" *
"4 <= z <= 5\n" *
"a free\n" *
"General\n" *
"y\n" *
"Binary\n" *
Expand Down Expand Up @@ -465,15 +464,14 @@ function test_read_model1_tricky()
@test occursin("CON1: 1 V1 >= 0", file)
@test occursin("CON5: [ 1 Var4 ^ 2 - 1.2 V5 * V1 ] <= 0", file)
@test occursin("R1: 1 V2 >= 2", file)
@test occursin("V1 <= 3", file)
@test occursin("-infinity <= V1 <= 3", file)
@test occursin("Var4 >= 5.5", file)
@test occursin("V3 >= -3", file)
@test occursin("V5 = 1", file)
@test occursin("V2 <= 3", file)
@test occursin("V2 >= 0", file)
@test occursin("0 <= V2 <= 3", file)
@test occursin("V6 free", file)
@test occursin("0 <= V7 <= 1", file)
@test occursin("0 <= V8 <= 1", file)
@test occursin("V6 free", file)
@test occursin("\nVar4\n", file)
@test occursin("\nV5\n", file)
@test occursin("\nV6\n", file)
Expand Down Expand Up @@ -718,15 +716,15 @@ end
function test_wrong_way_bounds()
for (case, result) in [
"x >= 2" => "x >= 2",
"x <= 2" => "x <= 2\nx >= 0",
"x <= 2" => "0 <= x <= 2",
"x == 2" => "x = 2",
"x > 2" => "x >= 2",
"x < 2" => "x <= 2\nx >= 0",
"x < 2" => "0 <= x <= 2",
"x = 2" => "x = 2",
"2 >= x" => "x <= 2\nx >= 0",
"2 >= x" => "0 <= x <= 2",
"2 <= x" => "x >= 2",
"2 == x" => "x = 2",
"2 > x" => "x <= 2\nx >= 0",
"2 > x" => "0 <= x <= 2",
"2 < x" => "x >= 2",
"2 = x" => "x = 2",
]
Expand Down Expand Up @@ -789,19 +787,19 @@ function test_reading_bounds()
_test_round_trip("0 < x", "Bounds\nx >= 0\nEnd")
_test_round_trip("-1 < x", "Bounds\nx >= -1\nEnd")
# Test upper bound
_test_round_trip("x <= 1", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("x <= 0", "Bounds\nx <= 0\nx >= 0\nEnd")
_test_round_trip("x <= -1", "Bounds\nx <= -1\nEnd")
_test_round_trip("x < 1", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("x < 0", "Bounds\nx <= 0\nx >= 0\nEnd")
_test_round_trip("x < -1", "Bounds\nx <= -1\nEnd")
_test_round_trip("x <= 1", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("x <= 0", "Bounds\nx = 0\nEnd")
_test_round_trip("x <= -1", "Bounds\n-infinity <= x <= -1\nEnd")
_test_round_trip("x < 1", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("x < 0", "Bounds\nx = 0\nEnd")
_test_round_trip("x < -1", "Bounds\n-infinity <= x <= -1\nEnd")
# Test reversed upper bound
_test_round_trip("1 >= x", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("0 >= x", "Bounds\nx <= 0\nx >= 0\nEnd")
_test_round_trip("-1 >= x", "Bounds\nx <= -1\nEnd")
_test_round_trip("1 > x", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("0 > x", "Bounds\nx <= 0\nx >= 0\nEnd")
_test_round_trip("-1 > x", "Bounds\nx <= -1\nEnd")
_test_round_trip("1 >= x", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("0 >= x", "Bounds\nx = 0\nEnd")
_test_round_trip("-1 >= x", "Bounds\n-infinity <= x <= -1\nEnd")
_test_round_trip("1 > x", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("0 > x", "Bounds\nx = 0\nEnd")
_test_round_trip("-1 > x", "Bounds\n-infinity <= x <= -1\nEnd")
# Test equality
_test_round_trip("x == 1", "Bounds\nx = 1\nEnd")
_test_round_trip("x == 0", "Bounds\nx = 0\nEnd")
Expand All @@ -822,13 +820,13 @@ function test_reading_bounds()
_test_round_trip("0 <= x <= 0", "Bounds\nx = 0\nEnd")
_test_round_trip("-2 <= x <= -2", "Bounds\nx = -2\nEnd")
# Test upper then lower
_test_round_trip("x <= 1\nx >= 0", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("x <= 2\nx >= 1", "Bounds\nx <= 2\nx >= 1\nEnd")
_test_round_trip("x <= 2\nx >= -1", "Bounds\nx <= 2\nx >= -1\nEnd")
_test_round_trip("x <= 1\nx >= 0", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("x <= 2\nx >= 1", "Bounds\n1 <= x <= 2\nEnd")
_test_round_trip("x <= 2\nx >= -1", "Bounds\n-1 <= x <= 2\nEnd")
# Test lower then upper
_test_round_trip("x >= 0\nx <= 1", "Bounds\nx <= 1\nx >= 0\nEnd")
_test_round_trip("x >= 1\nx <= 2", "Bounds\nx <= 2\nx >= 1\nEnd")
_test_round_trip("x >= -1\nx <= 2", "Bounds\nx <= 2\nx >= -1\nEnd")
_test_round_trip("x >= 0\nx <= 1", "Bounds\n0 <= x <= 1\nEnd")
_test_round_trip("x >= 1\nx <= 2", "Bounds\n1 <= x <= 2\nEnd")
_test_round_trip("x >= -1\nx <= 2", "Bounds\n-1 <= x <= 2\nEnd")
return
end

Expand Down Expand Up @@ -942,6 +940,43 @@ function test_read_newline_breaks()
return
end

function test_read_variable_bounds()
io = IOBuffer("""
maximize
obj: 1 x1
subject to
bounds
-infinity <= x1 <= +infinity
-infinity <= x2 <= 1
-infinity <= x3 <= -1
-1 <= x4 <= +infinity
1 <= x5 <= +infinity
-1 <= x6 <= 1
1 <= x7 <= 1
end
""")
model = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_LP)
read!(io, model)
io = IOBuffer()
write(io, model)
seekstart(io)
@test read(io, String) == """
maximize
obj: 1 x1
subject to
Bounds
x1 free
-infinity <= x2 <= 1
-infinity <= x3 <= -1
x4 >= -1
x5 >= 1
-1 <= x6 <= 1
x7 = 1
End
"""
return
end

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

0 comments on commit 5afc08b

Please sign in to comment.