Skip to content

[FileFormats.MPS] fix scale factor in Gurobi's QCMATRIX #2628

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Feb 17, 2025
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
85 changes: 48 additions & 37 deletions src/FileFormats/MPS/MPS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -811,39 +811,40 @@
return
end
options = get_options(model)
if options.quadratic_format == kQuadraticFormatGurobi
println(io, "QUADOBJ")
elseif options.quadratic_format == kQuadraticFormatCPLEX
println(io, "QMATRIX")
else
@assert options.quadratic_format == kQuadraticFormatMosek
println(io, "QSECTION OBJ")
end
# Here we always write out QUADOBJ sections for the quadratic objective. All
# solvers can read these, even if CPLEX writes QMATRIX by default and Mosek
# writes QSECTION OBJ.
println(io, "QUADOBJ")

Check warning on line 817 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L817

Added line #L817 was not covered by tests
_write_q_matrix(
io,
model,
flip_obj,
f,
var_to_column;
duplicate_off_diagonal = options.quadratic_format ==
kQuadraticFormatCPLEX,
flip_coef = flip_obj,
generic_names = options.generic_names,
# In QUADOBJ, we need only to specific the ij term:
include_ij_and_ji = false,
# And all solvers interpret QUADOBJ to include /2:
include_div_2 = true,
)
return
end

function _write_q_matrix(
io::IO,
model::Model,
flip_obj::Bool,
f,
f::MOI.ScalarQuadraticFunction,
var_to_column;
duplicate_off_diagonal::Bool,
flip_coef::Bool,
generic_names::Bool,
include_ij_and_ji::Bool,
include_div_2::Bool,
)
options = get_options(model)
# Convert the quadratic terms into matrix form. We don't need to scale
# because MOI uses the same Q/2 format as Gurobi, but we do need to ensure
# we collate off-diagonal terms in the lower-triangular.
terms = Dict{Tuple{MOI.VariableIndex,MOI.VariableIndex},Float64}()
scale = flip_coef ? -1.0 : 1.0
if !include_div_2
scale /= 2

Check warning on line 846 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L844-L846

Added lines #L844 - L846 were not covered by tests
end
for term in f.quadratic_terms
x = term.variable_1
y = term.variable_2
Expand All @@ -861,14 +862,11 @@
collect(keys(terms)),
by = ((x, y),) -> (var_to_column[x], var_to_column[y]),
)
x_name = _var_name(model, x, var_to_column[x], options.generic_names)
y_name = _var_name(model, y, var_to_column[y], options.generic_names)
coef = terms[(x, y)]
if flip_obj
coef *= -1
end
x_name = _var_name(model, x, var_to_column[x], generic_names)
y_name = _var_name(model, y, var_to_column[y], generic_names)
coef = scale * terms[(x, y)]

Check warning on line 867 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L865-L867

Added lines #L865 - L867 were not covered by tests
println(io, Card(f2 = x_name, f3 = y_name, f4 = _to_string(coef)))
if x != y && duplicate_off_diagonal
if x != y && include_ij_and_ji

Check warning on line 869 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L869

Added line #L869 was not covered by tests
println(io, Card(f2 = y_name, f3 = x_name, f4 = _to_string(coef)))
end
end
Expand All @@ -890,20 +888,23 @@
)
for ci in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
name = MOI.get(model, MOI.ConstraintName(), ci)
if options.quadratic_format == kQuadraticFormatMosek
println(io, "QSECTION $name")
else
println(io, "QCMATRIX $name")
end
println(io, "QCMATRIX $name")

Check warning on line 891 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L891

Added line #L891 was not covered by tests
f = MOI.get(model, MOI.ConstraintFunction(), ci)
_write_q_matrix(
io,
model,
false, # flip_obj
f,
var_to_column;
duplicate_off_diagonal = options.quadratic_format !=
kQuadraticFormatMosek,
generic_names = options.generic_names,
# flip_coef is needed only for maximization objectives
flip_coef = false,
# All solvers interpret QCMATRIX to require both (i,j) and (j,i)
# terms.
include_ij_and_ji = true,
# In Gurobi's QCMATRIX there is no factor of /2. This is
# different to both CPLEX and Mosek.
include_div_2 = options.quadratic_format !=
kQuadraticFormatGurobi,
)
end
end
Expand Down Expand Up @@ -1372,11 +1373,21 @@
(i, coef) in data.A[j]
]
quad_terms = MOI.ScalarQuadraticTerm{Float64}[]
for (x, y, q) in data.qc_matrix[c_name]
push!(
quad_terms,
MOI.ScalarQuadraticTerm(q, variable_map[x], variable_map[y]),
options = get_options(model)
scale = if options.quadratic_format == kQuadraticFormatGurobi

Check warning on line 1377 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L1376-L1377

Added lines #L1376 - L1377 were not covered by tests
# Gurobi does NOT have a /2 as part of the quadratic matrix! Why oh why

Check failure on line 1378 in src/FileFormats/MPS/MPS.jl

View workflow job for this annotation

GitHub Actions / build

[vale] reported by reviewdog 🐶 [Google.Exclamation] Don't use exclamation points in text. Raw Output: {"message": "[Google.Exclamation] Don't use exclamation points in text.", "location": {"path": "src/FileFormats/MPS/MPS.jl", "range": {"start": {"line": 1378, "column": 62}}}, "severity": "ERROR"}
# would you break precedent with all other formats.
2.0

Check warning on line 1380 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L1380

Added line #L1380 was not covered by tests
else
@assert in(

Check warning on line 1382 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L1382

Added line #L1382 was not covered by tests
options.quadratic_format,
(kQuadraticFormatCPLEX, kQuadraticFormatMosek),
)
1.0

Check warning on line 1386 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L1386

Added line #L1386 was not covered by tests
end
for (x_name, y_name, q) in data.qc_matrix[c_name]
x, y = variable_map[x_name], variable_map[y_name]
push!(quad_terms, MOI.ScalarQuadraticTerm(scale * q, x, y))

Check warning on line 1390 in src/FileFormats/MPS/MPS.jl

View check run for this annotation

Codecov / codecov/patch

src/FileFormats/MPS/MPS.jl#L1388-L1390

Added lines #L1388 - L1390 were not covered by tests
end
f = MOI.ScalarQuadraticFunction(quad_terms, aff_terms, 0.0)
c = MOI.add_constraint(model, f, set)
Expand Down
85 changes: 78 additions & 7 deletions test/FileFormats/MPS/MPS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ function _test_model_equality(
MOI.Utilities.loadfromstring!(model, model_string)
io = IOBuffer()
write(io, model)
model_2 = MPS.Model()
model_2 = MPS.Model(; kwargs...)
seekstart(io)
read!(io, model_2)
MOI.Test.util_test_models_equal(
Expand Down Expand Up @@ -805,10 +805,9 @@ function test_quadobj_cplex()
BOUNDS
FR bounds x
FR bounds y
QMATRIX
QUADOBJ
x x 10
x y 2
y x 2
y y 2.4
ENDATA
""";
Expand Down Expand Up @@ -838,10 +837,10 @@ function test_quadcon_gurobi()
FR bounds x
FR bounds y
QCMATRIX c1
x x 10
x y 2
y x 2
y y 2.4
x x 5
x y 1
y x 1
y y 1.2
ENDATA
""",
)
Expand Down Expand Up @@ -1278,6 +1277,78 @@ function test_issue_2538()
return
end

function test_qcmatrix_read_gurobi()
file = """
NAME
ROWS
N OBJ
L c1
COLUMNS
x c1 1
y c1 1
RHS
rhs c1 1
RANGES
BOUNDS
FR bounds x
FR bounds y
QCMATRIX c1
x x 10
x y 2.0
y x 2.0
y y 2.0
ENDATA
"""
io = IOBuffer()
print(io, file)
seekstart(io)
model = MPS.Model()
read!(io, model)
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
@test isapprox(
MOI.get(model, MOI.ConstraintFunction(), c1),
1.0 * x + 1.0 * y + 10.0 * x * x + 4.0 * x * y + 2.0 * y * y,
)
return
end

function test_qcmatrix_read_cplex()
file = """
NAME
ROWS
N OBJ
L c1
COLUMNS
x c1 1
y c1 1
RHS
rhs c1 1
RANGES
BOUNDS
FR bounds x
FR bounds y
QCMATRIX c1
x x 1.0
x y 2.0
y x 2.0
y y 7.0
ENDATA
"""
io = IOBuffer()
print(io, file)
seekstart(io)
model = MPS.Model(; quadratic_format = MPS.kQuadraticFormatCPLEX)
read!(io, model)
x, y = MOI.get.(model, MOI.VariableIndex, ["x", "y"])
c1 = MOI.get(model, MOI.ConstraintIndex, "c1")
@test isapprox(
MOI.get(model, MOI.ConstraintFunction(), c1),
1.0 * x + 1.0 * y + 0.5 * x * x + 2.0 * x * y + 3.5 * y * y,
)
return
end

end # TestMPS

TestMPS.runtests()
Loading