From 68a2d25137ae0d50cd95e91e5a158d9d0bae9ae0 Mon Sep 17 00:00:00 2001 From: odow Date: Fri, 18 Jun 2021 13:39:17 +1200 Subject: [PATCH 1/3] [FileFormats.MPS] Add support for OBJSENSE --- src/FileFormats/MPS/MPS.jl | 58 +++++++++++++++------------ test/FileFormats/MPS/MPS.jl | 7 +++- test/FileFormats/MPS/free_integer.mps | 1 + test/FileFormats/MPS/stacked_data.mps | 3 +- 4 files changed, 41 insertions(+), 28 deletions(-) diff --git a/src/FileFormats/MPS/MPS.jl b/src/FileFormats/MPS/MPS.jl index c0e7d3a72f..9476cc6485 100644 --- a/src/FileFormats/MPS/MPS.jl +++ b/src/FileFormats/MPS/MPS.jl @@ -171,6 +171,11 @@ function Base.write(io::IO, model::Model) names[x] = n end write_model_name(io, model) + if MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE + println(io, "OBJSENSE MAX") + else + println(io, "OBJSENSE MIN") + end write_rows(io, model) write_columns(io, model, ordered_names, names) write_rhs(io, model) @@ -246,19 +251,15 @@ function list_of_integer_variables(model::Model, names) return integer_variables end -function extract_terms( +function _extract_terms( v_names::Dict{MOI.VariableIndex,String}, coefficients::Dict{String,Vector{Tuple{String,Float64}}}, row_name::String, func::MOI.ScalarAffineFunction, - multiplier::Float64 = 1.0, ) for term in func.terms variable_name = v_names[term.variable] - push!( - coefficients[variable_name], - (row_name, multiplier * term.coefficient), - ) + push!(coefficients[variable_name], (row_name, term.coefficient)) end return end @@ -275,7 +276,7 @@ function _collect_coefficients( ) row_name = MOI.get(model, MOI.ConstraintName(), index) func = MOI.get(model, MOI.ConstraintFunction(), index) - extract_terms(v_names, coefficients, row_name, func) + _extract_terms(v_names, coefficients, row_name, func) end return end @@ -289,14 +290,11 @@ function write_columns(io::IO, model::Model, ordered_names, names) _collect_coefficients(model, S, names, coefficients) end # Build objective - # MPS doesn't support maximization so we flip the sign on the objective - # coefficients. - s = MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE ? -1.0 : 1.0 obj_func = MOI.get( model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), ) - extract_terms(names, coefficients, "OBJ", obj_func, s) + _extract_terms(names, coefficients, "OBJ", obj_func) integer_variables = list_of_integer_variables(model, names) println(io, "COLUMNS") int_open = false @@ -619,6 +617,7 @@ end mutable struct TempMPSModel name::String + is_minimization::Bool obj_name::String c::Vector{Float64} col_lower::Vector{Float64} @@ -639,6 +638,7 @@ end function TempMPSModel() return TempMPSModel( "", + true, "", Float64[], # c Float64[], # col_lower @@ -660,6 +660,7 @@ end @enum( Headers, HEADER_NAME, + HEADER_OBJSENSE, HEADER_ROWS, HEADER_COLUMNS, HEADER_RHS, @@ -671,27 +672,21 @@ end ) # Headers(s) gets called _alot_ (on every line), so we try very hard to be -# efficient.] +# efficient. function Headers(s::AbstractString) N = length(s) - if N > 7 || N < 3 - return HEADER_UNKNOWN - elseif N == 3 - x = first(s) + x = first(s) + if N == 3 if (x == 'R' || x == 'r') && uppercase(s) == "RHS" return HEADER_RHS elseif (x == 'S' || x == 's') && uppercase(s) == "SOS" return HEADER_SOS end elseif N == 4 - x = first(s) if (x == 'R' || x == 'r') && uppercase(s) == "ROWS" return HEADER_ROWS end - elseif N == 5 - return HEADER_UNKNOWN elseif N == 6 - x = first(s) if (x == 'R' || x == 'r') && uppercase(s) == "RANGES" return HEADER_RANGES elseif (x == 'B' || x == 'b') && uppercase(s) == "BOUNDS" @@ -700,10 +695,13 @@ function Headers(s::AbstractString) return HEADER_ENDATA end elseif N == 7 - x = first(s) if (x == 'C' || x == 'c') && (uppercase(s) == "COLUMNS") return HEADER_COLUMNS end + elseif N == 12 + if (x == 'O' || x == 'o') && startswith(uppercase(s), "OBJSENSE") + return HEADER_OBJSENSE + end end return HEADER_UNKNOWN end @@ -731,12 +729,18 @@ function Base.read!(io::IO, model::Model) continue # Skip blank lines and comments. end h = Headers(line) - if h != HEADER_UNKNOWN + if h == HEADER_OBJSENSE + items = line_to_items(line) + @assert length(items) == 2 + sense = uppercase(items[2]) + @assert sense == "MAX" || sense == "MIN" + data.is_minimization = sense == "MIN" + continue + elseif h != HEADER_UNKNOWN header = h continue - else - # Carry on with the previous header end + # Otherwise, carry on with the previous header # TODO: split into hard fields based on column indices. items = line_to_items(line) if header == HEADER_NAME @@ -819,7 +823,11 @@ function _add_variable(model, data, variable_map, i, name) end function _add_objective(model, data, variable_map) - MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + if data.is_minimization + MOI.set(model, MOI.ObjectiveSense(), MOI.MIN_SENSE) + else + MOI.set(model, MOI.ObjectiveSense(), MOI.MAX_SENSE) + end MOI.set( model, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), diff --git a/test/FileFormats/MPS/MPS.jl b/test/FileFormats/MPS/MPS.jl index 10249572ff..c2a36a1801 100644 --- a/test/FileFormats/MPS/MPS.jl +++ b/test/FileFormats/MPS/MPS.jl @@ -114,7 +114,7 @@ function test_maximization() MOI.SingleVariable(x), ) @test sprint(MPS.write_columns, model, ["x"], Dict(x => "x")) == - "COLUMNS\n x OBJ -1\n" + "COLUMNS\n x OBJ 1\n" end function test_stacked_data() @@ -125,7 +125,7 @@ function test_stacked_data() model_2, """ variables: x, y, z -minobjective: x + y + z +maxobjective: x + y + z con1: 1.0 * x in Interval(1.0, 5.0) con2: 1.0 * x in Interval(2.0, 6.0) con3: 1.0 * x in Interval(3.0, 7.0) @@ -376,6 +376,7 @@ a_really_long_name <= 2.0 MOI.write_to_file(model, MPS_TEST_FILE) @test read(MPS_TEST_FILE, String) == "NAME \n" * + "OBJSENSE MIN\n" * "ROWS\n" * " N OBJ\n" * "COLUMNS\n" * @@ -421,6 +422,7 @@ function test_names_with_spaces() MOI.set(model, MOI.ConstraintName(), c, "c c") @test sprint(write, model) == "NAME \n" * + "OBJSENSE MIN\n" * "ROWS\n" * " N OBJ\n" * " E c_c\n" * @@ -450,6 +452,7 @@ function test_sos_constraints() ) @test sprint(write, model) == "NAME \n" * + "OBJSENSE MIN\n" * "ROWS\n" * " N OBJ\n" * "COLUMNS\n" * diff --git a/test/FileFormats/MPS/free_integer.mps b/test/FileFormats/MPS/free_integer.mps index 84c3c8021e..7c30609050 100644 --- a/test/FileFormats/MPS/free_integer.mps +++ b/test/FileFormats/MPS/free_integer.mps @@ -1,4 +1,5 @@ NAME +OBJSENSE MIN ROWS N obj G con1 diff --git a/test/FileFormats/MPS/stacked_data.mps b/test/FileFormats/MPS/stacked_data.mps index dbe89a2d16..ecc07ccf31 100644 --- a/test/FileFormats/MPS/stacked_data.mps +++ b/test/FileFormats/MPS/stacked_data.mps @@ -1,4 +1,4 @@ -* min x + y +* max x + y * s.t. 1 <= x <= 5 con1 G == 1 + (-4) * 2 <= x <= 6 con2 L == 6 + (+4) * 3 <= x <= 7 con3 E == 3 + (4) @@ -6,6 +6,7 @@ * y ∈ {1, 2, 3, 4} *2345678901234567890123456789012345678901234567890 NAME stacked_data +OBJSENSE MAX ROWS N obj N blank_obj From 032888d35227fde903cb719676f7d0e0517ec4ce Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 18 Jun 2021 13:46:40 +1200 Subject: [PATCH 2/3] Update overview.md --- docs/src/submodules/FileFormats/overview.md | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/src/submodules/FileFormats/overview.md b/docs/src/submodules/FileFormats/overview.md index f6ad498b88..be44b28eaf 100644 --- a/docs/src/submodules/FileFormats/overview.md +++ b/docs/src/submodules/FileFormats/overview.md @@ -188,18 +188,16 @@ A Mathematical Programming System (MPS) model julia> MOI.copy_to(dest, src) MathOptInterface.Utilities.IndexMap with 0 entries -julia> io = IOBuffer() -IOBuffer(data=UInt8[...], readable=true, writable=true, seekable=true, append=false, size=0, maxsize=Inf, ptr=1, mark=-1) +julia> io = IOBuffer(); julia> write(io, dest) -julia> seekstart(io) -IOBuffer(data=UInt8[...], readable=true, writable=true, seekable=true, append=false, size=61, maxsize=Inf, ptr=1, mark=-1) +julia> seekstart(io); julia> src_2 = MOI.FileFormats.Model(format = MOI.FileFormats.FORMAT_MPS) A Mathematical Programming System (MPS) model -julia> read!(io, src_2) +julia> read!(io, src_2); ``` ## Validating MOF files From 7875dc93a044f4b5f83e9e13ec9e82fb7c874f15 Mon Sep 17 00:00:00 2001 From: Oscar Dowson Date: Fri, 18 Jun 2021 14:32:07 +1200 Subject: [PATCH 3/3] Update MPS.jl --- test/FileFormats/MPS/MPS.jl | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/test/FileFormats/MPS/MPS.jl b/test/FileFormats/MPS/MPS.jl index c2a36a1801..bb869e90ee 100644 --- a/test/FileFormats/MPS/MPS.jl +++ b/test/FileFormats/MPS/MPS.jl @@ -277,6 +277,18 @@ c1: 1.1 * x in Interval(1.0, 2.0) ) end +function test_objsense_max() + return _test_model_equality( + """ +variables: x +maxobjective: 1.2x +c1: 1.0 * x >= 0.0 +""", + ["x"], + ["c1"], + ) +end + function test_MARKER_INT() model = MPS.Model() MOIU.loadfromstring!(