Skip to content

Commit 5a4066d

Browse files
committed
[FileFormats.MPS] fix scale factor in Gurobi's QCMATRIX
1 parent 23f7469 commit 5a4066d

File tree

2 files changed

+126
-44
lines changed

2 files changed

+126
-44
lines changed

src/FileFormats/MPS/MPS.jl

Lines changed: 48 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -811,39 +811,40 @@ function write_quadobj(io::IO, model::Model, flip_obj::Bool, var_to_column)
811811
return
812812
end
813813
options = get_options(model)
814-
if options.quadratic_format == kQuadraticFormatGurobi
815-
println(io, "QUADOBJ")
816-
elseif options.quadratic_format == kQuadraticFormatCPLEX
817-
println(io, "QMATRIX")
818-
else
819-
@assert options.quadratic_format == kQuadraticFormatMosek
820-
println(io, "QSECTION OBJ")
821-
end
814+
# Here we always write out QUADOBJ sections for the quadratic objective. All
815+
# solvers can read these, even if CPLEX writes QMATRIX by default and Mosek
816+
# writes QSECTION OBJ.
817+
println(io, "QUADOBJ")
822818
_write_q_matrix(
823819
io,
824820
model,
825-
flip_obj,
826821
f,
827822
var_to_column;
828-
duplicate_off_diagonal = options.quadratic_format ==
829-
kQuadraticFormatCPLEX,
823+
flip_coef = flip_obj,
824+
generic_names = options.generic_names,
825+
# In QUADOBJ, we need only to specific the ij term:
826+
include_ij_and_ji = false,
827+
# And all solvers interpret QUADOBJ to include /2:
828+
include_div_2 = true,
830829
)
831830
return
832831
end
833832

834833
function _write_q_matrix(
835834
io::IO,
836835
model::Model,
837-
flip_obj::Bool,
838-
f,
836+
f::MOI.ScalarQuadraticFunction,
839837
var_to_column;
840-
duplicate_off_diagonal::Bool,
838+
flip_coef::Bool,
839+
generic_names::Bool,
840+
include_ij_and_ji::Bool,
841+
include_div_2::Bool,
841842
)
842-
options = get_options(model)
843-
# Convert the quadratic terms into matrix form. We don't need to scale
844-
# because MOI uses the same Q/2 format as Gurobi, but we do need to ensure
845-
# we collate off-diagonal terms in the lower-triangular.
846843
terms = Dict{Tuple{MOI.VariableIndex,MOI.VariableIndex},Float64}()
844+
scale = flip_coef ? -1.0 : 1.0
845+
if !include_div_2
846+
scale /= 2
847+
end
847848
for term in f.quadratic_terms
848849
x = term.variable_1
849850
y = term.variable_2
@@ -861,14 +862,11 @@ function _write_q_matrix(
861862
collect(keys(terms)),
862863
by = ((x, y),) -> (var_to_column[x], var_to_column[y]),
863864
)
864-
x_name = _var_name(model, x, var_to_column[x], options.generic_names)
865-
y_name = _var_name(model, y, var_to_column[y], options.generic_names)
866-
coef = terms[(x, y)]
867-
if flip_obj
868-
coef *= -1
869-
end
865+
x_name = _var_name(model, x, var_to_column[x], generic_names)
866+
y_name = _var_name(model, y, var_to_column[y], generic_names)
867+
coef = scale * terms[(x, y)]
870868
println(io, Card(f2 = x_name, f3 = y_name, f4 = _to_string(coef)))
871-
if x != y && duplicate_off_diagonal
869+
if x != y && include_ij_and_ji
872870
println(io, Card(f2 = y_name, f3 = x_name, f4 = _to_string(coef)))
873871
end
874872
end
@@ -890,20 +888,23 @@ function write_quadcons(io::IO, model::Model, var_to_column)
890888
)
891889
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
892890
name = MOI.get(model, MOI.ConstraintName(), ci)
893-
if options.quadratic_format == kQuadraticFormatMosek
894-
println(io, "QSECTION $name")
895-
else
896-
println(io, "QCMATRIX $name")
897-
end
891+
println(io, "QCMATRIX $name")
898892
f = MOI.get(model, MOI.ConstraintFunction(), ci)
899893
_write_q_matrix(
900894
io,
901895
model,
902-
false, # flip_obj
903896
f,
904897
var_to_column;
905-
duplicate_off_diagonal = options.quadratic_format !=
906-
kQuadraticFormatMosek,
898+
generic_names = options.generic_names,
899+
# flip_coef is needed only for maximization objectives
900+
flip_coef = false,
901+
# All solvers interpret QCMATRIX to require both (i,j) and (j,i)
902+
# terms.
903+
include_ij_and_ji = true,
904+
# In Gurobi's QCMATRIX there is no factor of /2. This is
905+
# different to both CPLEX and Mosek.
906+
include_div_2 = options.quadratic_format !=
907+
kQuadraticFormatGurobi,
907908
)
908909
end
909910
end
@@ -1372,11 +1373,21 @@ function _add_quad_constraint(model, data, variable_map, j, c_name, set)
13721373
(i, coef) in data.A[j]
13731374
]
13741375
quad_terms = MOI.ScalarQuadraticTerm{Float64}[]
1375-
for (x, y, q) in data.qc_matrix[c_name]
1376-
push!(
1377-
quad_terms,
1378-
MOI.ScalarQuadraticTerm(q, variable_map[x], variable_map[y]),
1376+
options = get_options(model)
1377+
scale = if options.quadratic_format == kQuadraticFormatGurobi
1378+
# Gurobi does NOT have a /2 as part of the quadratic matrix! Why oh why
1379+
# would you break precedent with all other formats.
1380+
2.0
1381+
else
1382+
@assert in(
1383+
options.quadratic_format,
1384+
(kQuadraticFormatCPLEX, kQuadraticFormatMosek),
13791385
)
1386+
1.0
1387+
end
1388+
for (x_name, y_name, q) in data.qc_matrix[c_name]
1389+
x, y = variable_map[x_name], variable_map[y_name]
1390+
push!(quad_terms, MOI.ScalarQuadraticTerm(scale * q, x, y))
13801391
end
13811392
f = MOI.ScalarQuadraticFunction(quad_terms, aff_terms, 0.0)
13821393
c = MOI.add_constraint(model, f, set)

test/FileFormats/MPS/MPS.jl

Lines changed: 78 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ function _test_model_equality(
4545
MOI.Utilities.loadfromstring!(model, model_string)
4646
io = IOBuffer()
4747
write(io, model)
48-
model_2 = MPS.Model()
48+
model_2 = MPS.Model(; kwargs...)
4949
seekstart(io)
5050
read!(io, model_2)
5151
MOI.Test.util_test_models_equal(
@@ -805,10 +805,9 @@ function test_quadobj_cplex()
805805
BOUNDS
806806
FR bounds x
807807
FR bounds y
808-
QMATRIX
808+
QUADOBJ
809809
x x 10
810810
x y 2
811-
y x 2
812811
y y 2.4
813812
ENDATA
814813
""";
@@ -838,10 +837,10 @@ function test_quadcon_gurobi()
838837
FR bounds x
839838
FR bounds y
840839
QCMATRIX c1
841-
x x 10
842-
x y 2
843-
y x 2
844-
y y 2.4
840+
x x 5
841+
x y 1
842+
y x 1
843+
y y 1.2
845844
ENDATA
846845
""",
847846
)
@@ -1278,6 +1277,78 @@ function test_issue_2538()
12781277
return
12791278
end
12801279

1280+
function test_qcmatrix_read_gurobi()
1281+
file = """
1282+
NAME
1283+
ROWS
1284+
N OBJ
1285+
L c1
1286+
COLUMNS
1287+
x c1 1
1288+
y c1 1
1289+
RHS
1290+
rhs c1 1
1291+
RANGES
1292+
BOUNDS
1293+
FR bounds x
1294+
FR bounds y
1295+
QCMATRIX c1
1296+
x x 10
1297+
x y 2.0
1298+
y x 2.0
1299+
y y 2.0
1300+
ENDATA
1301+
"""
1302+
io = IOBuffer()
1303+
print(io, file)
1304+
seekstart(io)
1305+
model = MPS.Model()
1306+
read!(io, model)
1307+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1308+
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
1309+
@test isapprox(
1310+
MOI.get(model, MOI.ConstraintFunction(), c1),
1311+
1.0 * x + 1.0 * y + 10.0 * x * x + 4.0 * x * y + 2.0 * y * y,
1312+
)
1313+
return
1314+
end
1315+
1316+
function test_qcmatrix_read_cplex()
1317+
file = """
1318+
NAME
1319+
ROWS
1320+
N OBJ
1321+
L c1
1322+
COLUMNS
1323+
x c1 1
1324+
y c1 1
1325+
RHS
1326+
rhs c1 1
1327+
RANGES
1328+
BOUNDS
1329+
FR bounds x
1330+
FR bounds y
1331+
QCMATRIX c1
1332+
x x 1.0
1333+
x y 2.0
1334+
y x 2.0
1335+
y y 7.0
1336+
ENDATA
1337+
"""
1338+
io = IOBuffer()
1339+
print(io, file)
1340+
seekstart(io)
1341+
model = MPS.Model(; quadratic_format = MPS.kQuadraticFormatCPLEX)
1342+
read!(io, model)
1343+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1344+
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
1345+
@test isapprox(
1346+
MOI.get(model, MOI.ConstraintFunction(), c1),
1347+
1.0 * x + 1.0 * y + 0.5 * x * x + 2.0 * x * y + 3.5 * y * y,
1348+
)
1349+
return
1350+
end
1351+
12811352
end # TestMPS
12821353

12831354
TestMPS.runtests()

0 commit comments

Comments
 (0)