Skip to content

[FileFormats.MPS]: avoid creating list of variable names #2426

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 5 commits into from
Feb 14, 2024
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
137 changes: 69 additions & 68 deletions src/FileFormats/MPS/MPS.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ module MPS
import ..FileFormats

import MathOptInterface as MOI
import DataStructures: OrderedDict

const _NUM_TO_STRING = [string(i) for i in -10:10]

Expand Down Expand Up @@ -209,7 +210,8 @@ Write `model` to `io` in the MPS file format.
function Base.write(io::IO, model::Model)
options = get_options(model)
if options.generic_names
FileFormats.create_generic_names(model)
# Generic variable names handled in this writer.
FileFormats.create_generic_constraint_names(model)
else
FileFormats.create_unique_names(
model;
Expand All @@ -218,11 +220,8 @@ function Base.write(io::IO, model::Model)
)
end
variables = MOI.get(model, MOI.ListOfVariableIndices())
ordered_names = Vector{String}(undef, length(variables))
var_to_column = Dict{MOI.VariableIndex,Int}()
var_to_column = OrderedDict{MOI.VariableIndex,Int}()
for (i, x) in enumerate(variables)
n = MOI.get(model, MOI.VariableName(), x)
ordered_names[i] = n
var_to_column[x] = i
end
write_model_name(io, model)
Expand All @@ -237,20 +236,19 @@ function Base.write(io::IO, model::Model)
flip_obj = MOI.get(model, MOI.ObjectiveSense()) == MOI.MAX_SENSE
end
write_rows(io, model)
obj_const, indicators =
write_columns(io, model, flip_obj, ordered_names, var_to_column)
obj_const, indicators = write_columns(io, model, flip_obj, var_to_column)
write_rhs(io, model, obj_const)
write_ranges(io, model)
write_bounds(io, model, ordered_names, var_to_column)
write_quadobj(io, model, ordered_names, var_to_column)
write_bounds(io, model, var_to_column)
write_quadobj(io, model, var_to_column)
if options.quadratic_format != kQuadraticFormatCPLEX
# Gurobi needs qcons _after_ quadobj and _before_ SOS.
write_quadcons(io, model, ordered_names, var_to_column)
write_quadcons(io, model, var_to_column)
end
write_sos(io, model, ordered_names, var_to_column)
write_sos(io, model, var_to_column)
if options.quadratic_format == kQuadraticFormatCPLEX
# CPLEX needs qcons _after_ SOS.
write_quadcons(io, model, ordered_names, var_to_column)
write_quadcons(io, model, var_to_column)
end
write_indicators(io, indicators)
println(io, "ENDATA")
Expand Down Expand Up @@ -388,7 +386,7 @@ function list_of_integer_variables(model::Model, var_to_column)
end

function _extract_terms(
var_to_column::Dict{MOI.VariableIndex,Int},
var_to_column::OrderedDict{MOI.VariableIndex,Int},
coefficients::Vector{Vector{Tuple{String,Float64}}},
row_name::String,
func::MOI.ScalarAffineFunction,
Expand All @@ -403,7 +401,7 @@ function _extract_terms(
end

function _extract_terms(
var_to_column::Dict{MOI.VariableIndex,Int},
var_to_column::OrderedDict{MOI.VariableIndex,Int},
coefficients::Vector{Vector{Tuple{String,Float64}}},
row_name::String,
func::MOI.ScalarQuadraticFunction,
Expand All @@ -421,7 +419,7 @@ function _collect_coefficients(
model,
::Type{F},
::Type{S},
var_to_column::Dict{MOI.VariableIndex,Int},
var_to_column::OrderedDict{MOI.VariableIndex,Int},
coefficients::Vector{Vector{Tuple{String,Float64}}},
) where {F,S}
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
Expand All @@ -441,6 +439,7 @@ function _collect_indicator(
coefficients,
indicators,
) where {S}
options = get_options(model)
F = MOI.VectorAffineFunction{Float64}
for index in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
row_name = MOI.get(model, MOI.ConstraintName(), index)
Expand All @@ -449,10 +448,8 @@ function _collect_indicator(
z = convert(MOI.VariableIndex, funcs[1])
_extract_terms(var_to_column, coefficients, row_name, funcs[2])
condition = _activation_condition(S)
push!(
indicators,
(row_name, MOI.get(model, MOI.VariableName(), z), condition),
)
var_name = _var_name(model, z, var_to_column[z], options.generic_names)
push!(indicators, (row_name, var_name, condition))
end
return
end
Expand All @@ -468,16 +465,24 @@ function _extract_terms_objective(model, var_to_column, coefficients, flip_obj)
return obj_func.constant
end

function write_columns(
io::IO,
function _var_name(
model::Model,
flip_obj,
ordered_names,
var_to_column,
)
variable::MOI.VariableIndex,
column::Int,
generic_name::Bool,
)::String
if generic_name
return "C$column"
else
return MOI.get(model, MOI.VariableName(), variable)
end
end

function write_columns(io::IO, model::Model, flip_obj, var_to_column)
options = get_options(model)
indicators = Tuple{String,String,MOI.ActivationCondition}[]
coefficients = Vector{Tuple{String,Float64}}[
Tuple{String,Float64}[] for _ in ordered_names
Tuple{String,Float64}[] for _ in 1:length(var_to_column)
]
# Build constraint coefficients
# The functions and sets are given explicitly so that this function is
Expand Down Expand Up @@ -511,7 +516,8 @@ function write_columns(
integer_variables = list_of_integer_variables(model, var_to_column)
println(io, "COLUMNS")
int_open = false
for (column, variable) in enumerate(ordered_names)
for (variable, column) in var_to_column
var_name = _var_name(model, variable, column, options.generic_names)
is_int = column in integer_variables
if is_int && !int_open
println(io, Card(f2 = "MARKER", f3 = "'MARKER'", f5 = "'INTORG'"))
Expand All @@ -523,13 +529,13 @@ function write_columns(
if length(coefficients[column]) == 0
# Every variable must appear in the COLUMNS section. Add a 0
# objective coefficient instead.
println(io, Card(f2 = variable, f3 = "OBJ", f4 = "0"))
println(io, Card(f2 = var_name, f3 = "OBJ", f4 = "0"))
end
for (constraint, coefficient) in coefficients[column]
println(
io,
Card(
f2 = variable,
f2 = var_name,
f3 = constraint,
f4 = _to_string(coefficient),
),
Expand Down Expand Up @@ -750,9 +756,10 @@ function _collect_bounds(bounds, model, ::Type{S}, var_to_column) where {S}
return
end

function write_bounds(io::IO, model::Model, ordered_names, var_to_column)
function write_bounds(io::IO, model::Model, var_to_column)
options = get_options(model)
println(io, "BOUNDS")
bounds = [(-Inf, Inf, VTYPE_CONTINUOUS) for _ in ordered_names]
bounds = [(-Inf, Inf, VTYPE_CONTINUOUS) for _ in 1:length(var_to_column)]
@_unroll for S in (
MOI.LessThan{Float64},
MOI.GreaterThan{Float64},
Expand All @@ -762,7 +769,8 @@ function write_bounds(io::IO, model::Model, ordered_names, var_to_column)
)
_collect_bounds(bounds, model, S, var_to_column)
end
for (column, var_name) in enumerate(ordered_names)
for (variable, column) in var_to_column
var_name = _var_name(model, variable, column, options.generic_names)
lower, upper, vtype = bounds[column]
if vtype == VTYPE_BINARY
println(io, Card(f1 = "BV", f2 = "bounds", f3 = var_name))
Expand All @@ -782,7 +790,7 @@ end
# QUADRATIC OBJECTIVE
# ==============================================================================

function write_quadobj(io::IO, model::Model, ordered_names, var_to_column)
function write_quadobj(io::IO, model::Model, var_to_column)
f = _get_objective(model)
if isempty(f.quadratic_terms)
return
Expand All @@ -798,8 +806,8 @@ function write_quadobj(io::IO, model::Model, ordered_names, var_to_column)
end
_write_q_matrix(
io,
model,
f,
ordered_names,
var_to_column;
duplicate_off_diagonal = options.quadratic_format ==
kQuadraticFormatCPLEX,
Expand All @@ -809,18 +817,20 @@ end

function _write_q_matrix(
io::IO,
model::Model,
f,
ordered_names,
var_to_column;
duplicate_off_diagonal::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{Int,Int},Float64}()
terms = Dict{Tuple{MOI.VariableIndex,MOI.VariableIndex},Float64}()
for term in f.quadratic_terms
x, y = var_to_column[term.variable_1], var_to_column[term.variable_2]
if x > y
x = term.variable_1
y = term.variable_2
if var_to_column[x] > var_to_column[y]
x, y = y, x
end
if haskey(terms, (x, y))
Expand All @@ -830,23 +840,20 @@ function _write_q_matrix(
end
end
# Use sort for reproducibility, and so the Q matrix is given in order.
for (x, y) in sort!(collect(keys(terms)))
for (x, y) in sort!(
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)
println(
io,
Card(
f2 = ordered_names[x],
f3 = ordered_names[y],
f4 = _to_string(terms[(x, y)]),
),
Card(f2 = x_name, f3 = y_name, f4 = _to_string(terms[(x, y)])),
)
if x != y && duplicate_off_diagonal
println(
io,
Card(
f2 = ordered_names[y],
f3 = ordered_names[x],
f4 = _to_string(terms[(x, y)]),
),
Card(f2 = y_name, f3 = x_name, f4 = _to_string(terms[(x, y)])),
)
end
end
Expand All @@ -857,7 +864,7 @@ end
# QUADRATIC CONSTRAINTS
# ==============================================================================

function write_quadcons(io::IO, model::Model, ordered_names, var_to_column)
function write_quadcons(io::IO, model::Model, var_to_column)
options = get_options(model)
F = MOI.ScalarQuadraticFunction{Float64}
for S in (
Expand All @@ -876,8 +883,8 @@ function write_quadcons(io::IO, model::Model, ordered_names, var_to_column)
f = MOI.get(model, MOI.ConstraintFunction(), ci)
_write_q_matrix(
io,
model,
f,
ordered_names,
var_to_column;
duplicate_off_diagonal = options.quadratic_format !=
kQuadraticFormatMosek,
Expand All @@ -891,22 +898,22 @@ end
# SOS
# ==============================================================================

function write_sos_constraint(
io::IO,
model::Model,
index,
ordered_names,
var_to_column,
)
function write_sos_constraint(io::IO, model::Model, index, var_to_column)
options = get_options(model)
func = MOI.get(model, MOI.ConstraintFunction(), index)
set = MOI.get(model, MOI.ConstraintSet(), index)
for (variable, weight) in zip(func.variables, set.weights)
column = var_to_column[variable]
println(io, Card(f2 = ordered_names[column], f3 = _to_string(weight)))
var_name = _var_name(
model,
variable,
var_to_column[variable],
options.generic_names,
)
println(io, Card(f2 = var_name, f3 = _to_string(weight)))
end
end

function write_sos(io::IO, model::Model, ordered_names, var_to_column)
function write_sos(io::IO, model::Model, var_to_column)
sos1_indices = MOI.get(
model,
MOI.ListOfConstraintIndices{MOI.VectorOfVariables,MOI.SOS1{Float64}}(),
Expand All @@ -921,13 +928,7 @@ function write_sos(io::IO, model::Model, ordered_names, var_to_column)
for (sos_type, indices) in enumerate([sos1_indices, sos2_indices])
for index in indices
println(io, Card(f1 = "S$(sos_type)", f2 = "SOS$(idx)"))
write_sos_constraint(
io,
model,
index,
ordered_names,
var_to_column,
)
write_sos_constraint(io, model, index, var_to_column)
idx += 1
end
end
Expand Down
32 changes: 26 additions & 6 deletions src/FileFormats/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -39,19 +39,39 @@ Rename all variables and constraints in `model` to have generic names.
This is helpful for users with proprietary models to avoid leaking information.
"""
function create_generic_names(model::MOI.ModelLike)
create_generic_variable_names(model)
create_generic_constraint_names(model)
return
end

function create_generic_variable_names(model::MOI.ModelLike)
for (i, x) in enumerate(MOI.get(model, MOI.ListOfVariableIndices()))
MOI.set(model, MOI.VariableName(), x, "C$i")
end
return
end

function create_generic_constraint_names(model::MOI.ModelLike)
i = 1
for (F, S) in MOI.get(model, MOI.ListOfConstraintTypesPresent())
if F == MOI.VariableIndex
continue # VariableIndex constraints do not need a name.
end
for c in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
MOI.set(model, MOI.ConstraintName(), c, "R$i")
i += 1
if F != MOI.VariableIndex
i = create_generic_constraint_names(model, F, S, i)
end
end
return
end

function create_generic_constraint_names(
model::MOI.ModelLike,
::Type{F},
::Type{S},
i::Int,
) where {F,S}
for c in MOI.get(model, MOI.ListOfConstraintIndices{F,S}())
MOI.set(model, MOI.ConstraintName(), c, "R$i")
i += 1
end
return i
end

function _replace(s::String, replacements::Vector{Function})
Expand Down
Loading