Skip to content
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
8 changes: 3 additions & 5 deletions docs/src/submodules/FileFormats/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 33 additions & 25 deletions src/FileFormats/MPS/MPS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -619,6 +617,7 @@ end

mutable struct TempMPSModel
name::String
is_minimization::Bool
obj_name::String
c::Vector{Float64}
col_lower::Vector{Float64}
Expand All @@ -639,6 +638,7 @@ end
function TempMPSModel()
return TempMPSModel(
"",
true,
"",
Float64[], # c
Float64[], # col_lower
Expand All @@ -660,6 +660,7 @@ end
@enum(
Headers,
HEADER_NAME,
HEADER_OBJSENSE,
HEADER_ROWS,
HEADER_COLUMNS,
HEADER_RHS,
Expand All @@ -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"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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}}(),
Expand Down
19 changes: 17 additions & 2 deletions test/FileFormats/MPS/MPS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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)
Expand Down Expand Up @@ -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!(
Expand Down Expand Up @@ -376,6 +388,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" *
Expand Down Expand Up @@ -421,6 +434,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" *
Expand Down Expand Up @@ -450,6 +464,7 @@ function test_sos_constraints()
)
@test sprint(write, model) ==
"NAME \n" *
"OBJSENSE MIN\n" *
"ROWS\n" *
" N OBJ\n" *
"COLUMNS\n" *
Expand Down
1 change: 1 addition & 0 deletions test/FileFormats/MPS/free_integer.mps
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
NAME
OBJSENSE MIN
ROWS
N obj
G con1
Expand Down
3 changes: 2 additions & 1 deletion test/FileFormats/MPS/stacked_data.mps
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
* 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)
* 4 <= 2x <= 8 con4 E == 8 + (-4)
* y ∈ {1, 2, 3, 4}
*2345678901234567890123456789012345678901234567890
NAME stacked_data
OBJSENSE MAX
ROWS
N obj
N blank_obj
Expand Down