Skip to content

Commit 6317c5a

Browse files
authored
[FileFormats.MPS] fix scale factor in Gurobi's QCMATRIX (#2628)
1 parent 23f7469 commit 6317c5a

File tree

2 files changed

+228
-44
lines changed

2 files changed

+228
-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: 180 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,180 @@ function test_issue_2538()
12781277
return
12791278
end
12801279

1280+
function test_qmatrix_objective()
1281+
file = """
1282+
NAME
1283+
ROWS
1284+
N OBJ
1285+
COLUMNS
1286+
x OBJ 1
1287+
y OBJ 1
1288+
RHS
1289+
RANGES
1290+
BOUNDS
1291+
FR bounds x
1292+
FR bounds y
1293+
QMATRIX
1294+
x x 10
1295+
x y 2.0
1296+
y x 2.0
1297+
y y 2.0
1298+
ENDATA
1299+
"""
1300+
io = IOBuffer()
1301+
print(io, file)
1302+
seekstart(io)
1303+
model = MPS.Model()
1304+
read!(io, model)
1305+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1306+
F = MOI.ScalarQuadraticFunction{Float64}
1307+
@test isapprox(
1308+
MOI.get(model, MOI.ObjectiveFunction{F}()),
1309+
1.0 * x + 1.0 * y + 5.0 * x * x + 2.0 * x * y + 1.0 * y * y,
1310+
)
1311+
return
1312+
end
1313+
1314+
function test_qsection_objective()
1315+
file = """
1316+
NAME
1317+
ROWS
1318+
N OBJ
1319+
COLUMNS
1320+
x OBJ 1
1321+
y OBJ 1
1322+
RHS
1323+
RANGES
1324+
BOUNDS
1325+
FR bounds x
1326+
FR bounds y
1327+
QSECTION OBJ
1328+
x x 10
1329+
x y 2.0
1330+
y y 2.0
1331+
ENDATA
1332+
"""
1333+
io = IOBuffer()
1334+
print(io, file)
1335+
seekstart(io)
1336+
model = MPS.Model()
1337+
read!(io, model)
1338+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1339+
F = MOI.ScalarQuadraticFunction{Float64}
1340+
@test isapprox(
1341+
MOI.get(model, MOI.ObjectiveFunction{F}()),
1342+
1.0 * x + 1.0 * y + 5.0 * x * x + 2.0 * x * y + 1.0 * y * y,
1343+
)
1344+
return
1345+
end
1346+
1347+
function test_qsection_row()
1348+
file = """
1349+
NAME
1350+
ROWS
1351+
N OBJ
1352+
L c1
1353+
COLUMNS
1354+
x c1 1
1355+
y c1 1
1356+
RHS
1357+
rhs c1 1
1358+
RANGES
1359+
BOUNDS
1360+
FR bounds x
1361+
FR bounds y
1362+
QSECTION c1
1363+
x x 10
1364+
x y 2.0
1365+
y y 2.0
1366+
ENDATA
1367+
"""
1368+
io = IOBuffer()
1369+
print(io, file)
1370+
seekstart(io)
1371+
model = MPS.Model()
1372+
read!(io, model)
1373+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1374+
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
1375+
@test isapprox(
1376+
MOI.get(model, MOI.ConstraintFunction(), c1),
1377+
1.0 * x + 1.0 * y + 10.0 * x * x + 4.0 * x * y + 2.0 * y * y,
1378+
)
1379+
return
1380+
end
1381+
1382+
function test_qcmatrix_read_gurobi()
1383+
file = """
1384+
NAME
1385+
ROWS
1386+
N OBJ
1387+
L c1
1388+
COLUMNS
1389+
x c1 1
1390+
y c1 1
1391+
RHS
1392+
rhs c1 1
1393+
RANGES
1394+
BOUNDS
1395+
FR bounds x
1396+
FR bounds y
1397+
QCMATRIX c1
1398+
x x 10
1399+
x y 2.0
1400+
y x 2.0
1401+
y y 2.0
1402+
ENDATA
1403+
"""
1404+
io = IOBuffer()
1405+
print(io, file)
1406+
seekstart(io)
1407+
model = MPS.Model()
1408+
read!(io, model)
1409+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1410+
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
1411+
@test isapprox(
1412+
MOI.get(model, MOI.ConstraintFunction(), c1),
1413+
1.0 * x + 1.0 * y + 10.0 * x * x + 4.0 * x * y + 2.0 * y * y,
1414+
)
1415+
return
1416+
end
1417+
1418+
function test_qcmatrix_read_cplex()
1419+
file = """
1420+
NAME
1421+
ROWS
1422+
N OBJ
1423+
L c1
1424+
COLUMNS
1425+
x c1 1
1426+
y c1 1
1427+
RHS
1428+
rhs c1 1
1429+
RANGES
1430+
BOUNDS
1431+
FR bounds x
1432+
FR bounds y
1433+
QCMATRIX c1
1434+
x x 1.0
1435+
x y 2.0
1436+
y x 2.0
1437+
y y 7.0
1438+
ENDATA
1439+
"""
1440+
io = IOBuffer()
1441+
print(io, file)
1442+
seekstart(io)
1443+
model = MPS.Model(; quadratic_format = MPS.kQuadraticFormatCPLEX)
1444+
read!(io, model)
1445+
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
1446+
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
1447+
@test isapprox(
1448+
MOI.get(model, MOI.ConstraintFunction(), c1),
1449+
1.0 * x + 1.0 * y + 0.5 * x * x + 2.0 * x * y + 3.5 * y * y,
1450+
)
1451+
return
1452+
end
1453+
12811454
end # TestMPS
12821455

12831456
TestMPS.runtests()

0 commit comments

Comments
 (0)