diff --git a/src/FileFormats/LP/LP.jl b/src/FileFormats/LP/LP.jl index cc047d6fc5..603de5a4fe 100644 --- a/src/FileFormats/LP/LP.jl +++ b/src/FileFormats/LP/LP.jl @@ -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}}()) @@ -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) @@ -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]) @@ -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. diff --git a/test/FileFormats/LP/LP.jl b/test/FileFormats/LP/LP.jl index 17dc3e428e..620117ee43 100644 --- a/test/FileFormats/LP/LP.jl +++ b/test/FileFormats/LP/LP.jl @@ -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" * @@ -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) @@ -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", ] @@ -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") @@ -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 @@ -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_")