Re-add Cuts module

master
Alinson S. Xavier 3 years ago
parent db6456dbaa
commit d8b80f00ca
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

@ -10,13 +10,17 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"
JuMP = "4076af6c-e467-56ae-b986-b466b2749572"
KLU = "ef3ab10e-7fda-4108-b977-705223b18434"
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee"
OrderedCollections = "bac558e1-5e72-5ebc-8fee-abe8a469f55d"
Printf = "de0858da-6303-5e67-8744-51eddeeeb8d7"
PyCall = "438e738f-606a-5dbb-bf0a-cddfbfd45ab0"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Requires = "ae029012-a4dd-5104-9daa-d747884805df"
SparseArrays = "2f01184e-e22b-5df5-ae63-d93ebab69eaf"
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
TimerOutputs = "a759f4b9-e2f1-59dc-863e-4aeb61b1ea8f"
[compat]
JuMP = "1"

@ -0,0 +1,14 @@
module Cuts
import ..to_str_array
include("tableau/structs.jl")
include("blackbox/cplex.jl")
include("tableau/collect.jl")
include("tableau/gmi.jl")
include("tableau/moi.jl")
include("tableau/tableau.jl")
include("tableau/transform.jl")
end # module

@ -0,0 +1,201 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
import ..H5File
using OrderedCollections
function collect_gmi(
mps_filename;
optimizer,
max_rounds=10,
max_cuts_per_round=100,
)
@info mps_filename
reset_timer!()
# Open HDF5 file
h5_filename = replace(mps_filename, ".mps.gz" => ".h5")
h5 = H5File(h5_filename)
# Read optimal solution
sol_opt_dict = Dict(
zip(
h5.get_array("static_var_names"),
convert(Array{Float64}, h5.get_array("mip_var_values")),
)
)
# Read optimal value
obj_mip = h5.get_scalar("mip_lower_bound")
if obj_mip === nothing
obj_mip = h5.get_scalar("mip_obj_value")
end
obj_lp = nothing
h5.file.close()
# Define relative MIP gap
gap(v) = 100 * abs(obj_mip - v) / abs(v)
# Initialize stats
stats_obj = []
stats_gap = []
stats_ncuts = []
stats_time_convert = 0
stats_time_solve = 0
stats_time_select = 0
stats_time_tableau = 0
stats_time_gmi = 0
all_cuts = nothing
# Read problem
model = read_from_file(mps_filename)
for round in 1:max_rounds
@info "Round $(round)..."
stats_time_convert = @elapsed begin
# Extract problem data
data = ProblemData(model)
# Construct optimal solution vector (with correct variable sequence)
sol_opt = [sol_opt_dict[n] for n in data.var_names]
# Assert optimal solution is feasible for the original problem
@assert all(data.constr_lb .- 1e-3 .<= data.constr_lhs * sol_opt)
@assert all(data.constr_lhs * sol_opt .<= data.constr_ub .+ 1e-3)
# Convert to standard form
data_s, transforms = convert_to_standard_form(data)
model_s = to_model(data_s)
set_optimizer(model_s, optimizer)
relax_integrality(model_s)
# Convert optimal solution to standard form
sol_opt_s = forward(transforms, sol_opt)
# Assert converted solution is feasible for standard form problem
@assert data_s.constr_lhs * sol_opt_s data_s.constr_lb
end
# Optimize standard form
optimize!(model_s)
stats_time_solve += solve_time(model_s)
obj = objective_value(model_s) + data_s.obj_offset
if obj_lp === nothing
obj_lp = obj
push!(stats_obj, obj)
push!(stats_gap, gap(obj))
push!(stats_ncuts, 0)
end
if termination_status(model_s) != MOI.OPTIMAL
return
end
# Select tableau rows
basis = get_basis(model_s)
sol_frac = get_x(model_s)
stats_time_select += @elapsed begin
selected_rows = select_gmi_rows(
data_s,
basis,
sol_frac,
max_rows=max_cuts_per_round,
)
end
# Compute selected tableau rows
stats_time_tableau += @elapsed begin
tableau = compute_tableau(
data_s,
basis,
sol_frac,
rows=selected_rows,
)
# Assert tableau rows have been computed correctly
@assert tableau.lhs * sol_frac tableau.rhs
@assert tableau.lhs * sol_opt_s tableau.rhs
end
# Compute GMI cuts
stats_time_gmi += @elapsed begin
cuts_s = compute_gmi(data_s, tableau)
# Assert cuts have been generated correctly
try
assert_cuts_off(cuts_s, sol_frac)
assert_does_not_cut_off(cuts_s, sol_opt_s)
catch
@warn "Invalid cuts detected. Discarding round $round cuts and aborting."
break
end
# Abort if no cuts are left
if length(cuts_s.lb) == 0
@info "No cuts generated. Aborting."
break
end
end
# Add GMI cuts to original problem
cuts = backwards(transforms, cuts_s)
assert_does_not_cut_off(cuts, sol_opt)
constrs = add_constraint_set(model, cuts)
# Optimize original form
set_optimizer(model, optimizer)
undo_relax = relax_integrality(model)
optimize!(model)
obj = objective_value(model)
push!(stats_obj, obj)
push!(stats_gap, gap(obj))
# Store useful cuts; drop useless ones from the problem
useful = [
abs(shadow_price(c)) > 1e-3
for c in constrs
]
drop = findall(useful .== false)
keep = findall(useful .== true)
delete.(model, constrs[drop])
if all_cuts === nothing
all_cuts = cuts
else
all_cuts.lhs = [all_cuts.lhs; cuts.lhs[keep, :]]
all_cuts.lb = [all_cuts.lb; cuts.lb[keep]]
all_cuts.lb = [all_cuts.lb; cuts.lb[keep]]
end
push!(stats_ncuts, length(all_cuts.lb))
undo_relax()
end
# Store cuts
if all_cuts !== nothing
@info "Storing $(length(all_cuts.ub)) GMI cuts..."
h5 = H5File(h5_filename)
h5.put_sparse("cuts_lhs", all_cuts.lhs)
h5.put_array("cuts_lb", all_cuts.lb)
h5.put_array("cuts_ub", all_cuts.ub)
h5.file.close()
end
return OrderedDict(
"instance" => mps_filename,
"max_rounds" => max_rounds,
"rounds" => length(stats_obj) - 1,
"time_convert" => stats_time_convert,
"time_solve" => stats_time_solve,
"time_tableau" => stats_time_tableau,
"time_gmi" => stats_time_gmi,
"obj_mip" => obj_mip,
"obj_lp" => obj_lp,
"stats_obj" => stats_obj,
"stats_gap" => stats_gap,
"stats_ncuts" => stats_ncuts,
)
end
export collect_gmi

@ -0,0 +1,92 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using SparseArrays
using TimerOutputs
@inline frac(x::Float64) = x - floor(x)
function select_gmi_rows(data, basis, x; max_rows=10, atol=0.001)
candidate_rows = [
r
for r in 1:length(basis.var_basic)
if (data.var_types[basis.var_basic[r]] != 'C') && (frac(x[basis.var_basic[r]]) > atol)
]
candidate_vals = frac.(x[basis.var_basic[candidate_rows]])
score = abs.(candidate_vals .- 0.5)
perm = sortperm(score)
return [candidate_rows[perm[i]] for i in 1:min(length(perm), max_rows)]
end
function compute_gmi(
data::ProblemData,
tableau::Tableau,
tol=1e-8,
)::ConstraintSet
nrows, ncols = size(tableau.lhs)
ub = Float64[Inf for _ in 1:nrows]
lb = Float64[0.999 for _ in 1:nrows]
tableau_I, tableau_J, tableau_V = findnz(tableau.lhs)
lhs_I = Int[]
lhs_J = Int[]
lhs_V = Float64[]
@timeit "Compute coefficients" begin
for k in 1:nnz(tableau.lhs)
i::Int = tableau_I[k]
v::Float64 = 0.0
alpha_j = frac(tableau_V[k])
beta = frac(tableau.rhs[i])
if data.var_types[i] == "C"
if alpha_j >= 0
v = alpha_j / beta
else
v = alpha_j / (1 - beta)
end
else
if alpha_j <= beta
v = alpha_j / beta
else
v = (1 - alpha_j) / (1 - beta)
end
end
if abs(v) > tol
push!(lhs_I, i)
push!(lhs_J, tableau_J[k])
push!(lhs_V, v)
end
end
lhs = sparse(lhs_I, lhs_J, lhs_V, nrows, ncols)
end
return ConstraintSet(; lhs, ub, lb)
end
function assert_cuts_off(
cuts::ConstraintSet,
x::Vector{Float64},
tol=1e-6
)
for i in 1:length(cuts.lb)
val = cuts.lhs[i, :]' * x
if (val <= cuts.ub[i] - tol) && (val >= cuts.lb[i] + tol)
throw(ErrorException("inequality fails to cut off fractional solution"))
end
end
end
function assert_does_not_cut_off(
cuts::ConstraintSet,
x::Vector{Float64};
tol=1e-6
)
for i in 1:length(cuts.lb)
val = cuts.lhs[i, :]' * x
ub = cuts.ub[i]
lb = cuts.lb[i]
if (val >= ub) || (val <= lb)
throw(ErrorException("inequality $i cuts off integer solution ($lb <= $val <= $ub)"))
end
end
end
export compute_gmi, frac, select_gmi_rows, assert_cuts_off, assert_does_not_cut_off

@ -0,0 +1,177 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using JuMP
function ProblemData(model::Model)::ProblemData
vars = all_variables(model)
# Objective function
obj = objective_function(model)
obj = [v keys(obj.terms) ? obj.terms[v] : 0.0 for v in vars]
# Variable types, lower bounds and upper bounds
var_lb = [is_binary(v) ? 0.0 : has_lower_bound(v) ? lower_bound(v) : -Inf for v in vars]
var_ub = [is_binary(v) ? 1.0 : has_upper_bound(v) ? upper_bound(v) : Inf for v in vars]
var_types = [is_binary(v) || is_integer(v) ? 'I' : 'C' for v in vars]
var_names = [name(v) for v in vars]
# Constraints
constr_lb = Float64[]
constr_ub = Float64[]
constr_lhs_rows = Int[]
constr_lhs_cols = Int[]
constr_lhs_values = Float64[]
constr_index = 1
for (ftype, stype) in list_of_constraint_types(model)
for constr in all_constraints(model, ftype, stype)
cset = MOI.get(constr.model.moi_backend, MOI.ConstraintSet(), constr.index)
cf = MOI.get(
constr.model.moi_backend,
MOI.ConstraintFunction(),
constr.index,
)
if ftype == VariableRef
var_idx = cf.value
if stype == MOI.Integer || stype == MOI.ZeroOne
# nop
elseif stype == MOI.EqualTo{Float64}
var_lb[var_idx] = max(var_lb[var_idx], cset.value)
var_ub[var_idx] = min(var_ub[var_idx], cset.value)
elseif stype == MOI.LessThan{Float64}
var_ub[var_idx] = min(var_ub[var_idx], cset.upper)
elseif stype == MOI.GreaterThan{Float64}
var_lb[var_idx] = max(var_lb[var_idx], cset.lower)
elseif stype == MOI.Interval{Float64}
var_lb[var_idx] = max(var_lb[var_idx], cset.lower)
var_ub[var_idx] = min(var_ub[var_idx], cset.upper)
else
error("Unsupported set: $stype")
end
elseif ftype == AffExpr
if stype == MOI.EqualTo{Float64}
push!(constr_lb, cset.value)
push!(constr_ub, cset.value)
elseif stype == MOI.LessThan{Float64}
push!(constr_lb, -Inf)
push!(constr_ub, cset.upper)
elseif stype == MOI.GreaterThan{Float64}
push!(constr_lb, cset.lower)
push!(constr_ub, Inf)
elseif stype == MOI.Interval{Float64}
push!(constr_lb, cset.lower)
push!(constr_ub, cset.upper)
else
error("Unsupported set: $stype")
end
for term in cf.terms
push!(constr_lhs_cols, term.variable.value)
push!(constr_lhs_rows, constr_index)
push!(constr_lhs_values, term.coefficient)
end
constr_index += 1
else
error("Unsupported constraint type: ($ftype, $stype)")
end
end
end
n = length(vars)
m = constr_index - 1
constr_lhs = sparse(
constr_lhs_rows,
constr_lhs_cols,
constr_lhs_values,
m,
n,
)
@assert length(obj) == n
@assert length(var_lb) == n
@assert length(var_ub) == n
@assert length(var_types) == n
@assert length(var_names) == n
@assert length(constr_lb) == m
@assert length(constr_ub) == m
return ProblemData(
obj_offset=0.0;
obj,
constr_lb,
constr_ub,
constr_lhs,
var_lb,
var_ub,
var_types,
var_names
)
end
function to_model(data::ProblemData, tol=1e-6)::Model
model = Model()
# Variables
nvars = length(data.obj)
@variable(model, x[1:nvars])
for i = 1:nvars
set_name(x[i], data.var_names[i])
if data.var_types[i] == 'B'
set_binary(x[i])
elseif data.var_types[i] == 'I'
set_integer(x[i])
end
if isfinite(data.var_lb[i])
set_lower_bound(x[i], data.var_lb[i])
end
if isfinite(data.var_ub[i])
set_upper_bound(x[i], data.var_ub[i])
end
set_objective_coefficient(model, x[i], data.obj[i])
end
# Constraints
lhs = data.constr_lhs * x
for (j, lhs_expr) in enumerate(lhs)
lb = data.constr_lb[j]
ub = data.constr_ub[j]
if abs(lb - ub) < tol
@constraint(model, lb == lhs_expr)
elseif isfinite(lb) && !isfinite(ub)
@constraint(model, lb <= lhs_expr)
elseif !isfinite(lb) && isfinite(ub)
@constraint(model, lhs_expr <= ub)
else
@constraint(model, lb <= lhs_expr <= ub)
end
end
return model
end
function add_constraint_set(model::JuMP.Model, cs::ConstraintSet)
vars = all_variables(model)
nrows, _ = size(cs.lhs)
constrs = []
for i in 1:nrows
c = nothing
if isinf(cs.ub[i])
c = @constraint(model, cs.lb[i] <= dot(cs.lhs[i, :], vars))
elseif isinf(cs.lb[i])
c = @constraint(model, dot(cs.lhs[i, :], vars) <= cs.ub[i])
else
c = @constraint(model, cs.lb[i] <= dot(cs.lhs[i, :], vars) <= cs.ub[i])
end
push!(constrs, c)
end
return constrs
end
function set_warm_start(model::JuMP.Model, x::Vector{Float64})
vars = all_variables(model)
for (i, xi) in enumerate(x)
set_start_value(vars[i], xi)
end
end
export to_model, ProblemData, add_constraint_set, set_warm_start

@ -0,0 +1,39 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using SparseArrays
Base.@kwdef mutable struct ProblemData
obj::Vector{Float64}
obj_offset::Float64
constr_lb::Vector{Float64}
constr_ub::Vector{Float64}
constr_lhs::SparseMatrixCSC
var_lb::Vector{Float64}
var_ub::Vector{Float64}
var_types::Vector{Char}
var_names::Vector{String}
end
Base.@kwdef mutable struct Tableau
obj
lhs
rhs
z
end
Base.@kwdef mutable struct Basis
var_basic
var_nonbasic
constr_basic
constr_nonbasic
end
Base.@kwdef mutable struct ConstraintSet
lhs::SparseMatrixCSC
ub::Vector{Float64}
lb::Vector{Float64}
end
export ProblemData, Tableau, Basis, ConstraintSet

@ -0,0 +1,130 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using KLU
using TimerOutputs
function get_basis(model::JuMP.Model)::Basis
var_basic = Int[]
var_nonbasic = Int[]
constr_basic = Int[]
constr_nonbasic = Int[]
# Variables
for (i, var) in enumerate(all_variables(model))
bstatus = MOI.get(model, MOI.VariableBasisStatus(), var)
if bstatus == MOI.BASIC
push!(var_basic, i)
elseif bstatus == MOI.NONBASIC_AT_LOWER
push!(var_nonbasic, i)
else
error("Unknown basis status: $bstatus")
end
end
# Constraints
constr_index = 1
for (ftype, stype) in list_of_constraint_types(model)
for constr in all_constraints(model, ftype, stype)
if ftype == VariableRef
# nop
elseif ftype == AffExpr
bstatus = MOI.get(model, MOI.ConstraintBasisStatus(), constr)
if bstatus == MOI.BASIC
push!(constr_basic, constr_index)
elseif bstatus == MOI.NONBASIC
push!(constr_nonbasic, constr_index)
else
error("Unknown basis status: $bstatus")
end
constr_index += 1
else
error("Unsupported constraint type: ($ftype, $stype)")
end
end
end
return Basis(; var_basic, var_nonbasic, constr_basic, constr_nonbasic)
end
function get_x(model::JuMP.Model)
return JuMP.value.(all_variables(model))
end
function compute_tableau(
data::ProblemData,
basis::Basis,
x::Vector{Float64};
rows::Union{Vector{Int},Nothing}=nothing,
tol=1e-8
)::Tableau
@timeit "Split data" begin
nrows, ncols = size(data.constr_lhs)
lhs_slacks = sparse(I, nrows, nrows)
lhs_b = [data.constr_lhs[:, basis.var_basic] lhs_slacks[:, basis.constr_basic]]
obj_b = [data.obj[basis.var_basic]; zeros(length(basis.constr_basic))]
if rows === nothing
rows = 1:nrows
end
end
@timeit "Factorize basis matrix" begin
factor = klu(sparse(lhs_b'))
end
@timeit "Compute tableau LHS" begin
tableau_lhs_I = Int[]
tableau_lhs_J = Int[]
tableau_lhs_V = Float64[]
for k in 1:length(rows)
@timeit "Prepare inputs" begin
i = rows[k]
e = zeros(nrows)
e[i] = 1.0
end
@timeit "Solve" begin
sol = factor \ e
end
@timeit "Multiply" begin
row = sol' * data.constr_lhs
end
@timeit "Sparsify & copy" begin
for (j, v) in enumerate(row)
if abs(v) < tol
continue
end
push!(tableau_lhs_I, k)
push!(tableau_lhs_J, j)
push!(tableau_lhs_V, v)
end
end
end
tableau_lhs = sparse(
tableau_lhs_I,
tableau_lhs_J,
tableau_lhs_V,
length(rows),
ncols,
)
end
@timeit "Compute tableau RHS" begin
tableau_rhs = [x[basis.var_basic]; zeros(length(basis.constr_basic))][rows]
end
@timeit "Compute tableau objective row" begin
sol = factor \ obj_b
tableau_obj = -data.obj' + sol' * data.constr_lhs
tableau_obj[abs.(tableau_obj).<tol] .= 0
end
return Tableau(
obj=tableau_obj,
lhs=tableau_lhs,
rhs=tableau_rhs,
z=dot(data.obj, x),
)
end
export get_basis, get_x, compute_tableau

@ -0,0 +1,314 @@
# MIPLearn: Extensible Framework for Learning-Enhanced Mixed-Integer Optimization
# Copyright (C) 2020-2022, UChicago Argonne, LLC. All rights reserved.
# Released under the modified BSD license. See COPYING.md for more details.
using LinearAlgebra
using TimerOutputs
abstract type Transform end
function _isbounded(x)
isfinite(x) || return false
abs(x) < 1e15 || return false
return true
end
function backwards!(transforms::Vector{Transform}, m::ConstraintSet; tol=1e-8)
for t in reverse(transforms)
backwards!(t, m)
end
for (idx, val) in enumerate(m.lhs.nzval)
if abs(val) < tol
m.lhs.nzval[idx] = 0
end
end
end
function backwards(transforms::Vector{Transform}, m::ConstraintSet; tol=1e-8)
m2 = deepcopy(m)
backwards!(transforms, m2; tol)
return m2
end
function forward(transforms::Vector{Transform}, p::Vector{Float64})::Vector{Float64}
for t in transforms
p = forward(t, p)
end
return p
end
# -----------------------------------------------------------------------------
Base.@kwdef mutable struct ShiftVarLowerBoundsToZero <: Transform
lb::Vector{Float64} = []
end
function forward!(t::ShiftVarLowerBoundsToZero, data::ProblemData)
t.lb = copy(data.var_lb)
data.obj_offset += dot(data.obj, t.lb)
data.var_ub -= t.lb
data.var_lb -= t.lb
data.constr_lb -= data.constr_lhs * t.lb
data.constr_ub -= data.constr_lhs * t.lb
end
function backwards!(t::ShiftVarLowerBoundsToZero, c::ConstraintSet)
c.lb += c.lhs * t.lb
c.ub += c.lhs * t.lb
end
function forward(t::ShiftVarLowerBoundsToZero, p::Vector{Float64})::Vector{Float64}
return p - t.lb
end
# -----------------------------------------------------------------------------
Base.@kwdef mutable struct MoveVarUpperBoundsToConstrs <: Transform end
function forward!(t::MoveVarUpperBoundsToConstrs, data::ProblemData)
_, ncols = size(data.constr_lhs)
data.constr_lhs = [data.constr_lhs; I]
data.constr_lb = [data.constr_lb; [-Inf for _ in 1:ncols]]
data.constr_ub = [data.constr_ub; data.var_ub]
data.var_ub .= Inf
end
function backwards!(::MoveVarUpperBoundsToConstrs, ::ConstraintSet)
# nop
end
function forward(t::MoveVarUpperBoundsToConstrs, p::Vector{Float64})::Vector{Float64}
return p
end
# -----------------------------------------------------------------------------
Base.@kwdef mutable struct AddSlackVariables <: Transform
M1::SparseMatrixCSC = spzeros(0)
M2::Vector{Float64} = []
ncols_orig::Int = 0
GE::Int = 0
LE::Int = 0
lhs_ge::SparseMatrixCSC = spzeros(0)
lhs_le::SparseMatrixCSC = spzeros(0)
rhs_le::Vector{Float64} = []
rhs_ge::Vector{Float64} = []
end
function forward!(t::AddSlackVariables, data::ProblemData)
nrows, ncols = size(data.constr_lhs)
isequality = abs.(data.constr_ub .- data.constr_lb) .< 1e-6
eq = [i for i in 1:nrows if isequality[i]]
ge = [i for i in 1:nrows if isfinite(data.constr_lb[i]) && !isequality[i]]
le = [i for i in 1:nrows if isfinite(data.constr_ub[i]) && !isequality[i]]
EQ, GE, LE = length(eq), length(ge), length(le)
t.M1 = [
I spzeros(ncols, GE + LE)
data.constr_lhs[ge, :] spzeros(GE, GE + LE)
-data.constr_lhs[le, :] spzeros(LE, GE + LE)
]
t.M2 = [
zeros(ncols)
data.constr_lb[ge]
-data.constr_ub[le]
]
t.ncols_orig = ncols
t.GE, t.LE = GE, LE
t.lhs_ge = data.constr_lhs[ge, :]
t.lhs_le = data.constr_lhs[le, :]
t.rhs_ge = data.constr_lb[ge]
t.rhs_le = data.constr_ub[le]
data.constr_lhs = [
data.constr_lhs[eq, :] spzeros(EQ, GE) spzeros(EQ, LE)
data.constr_lhs[ge, :] -I spzeros(GE, LE)
data.constr_lhs[le, :] spzeros(LE, GE) I
]
data.obj = [data.obj; zeros(GE + LE)]
data.var_lb = [data.var_lb; zeros(GE + LE)]
data.var_ub = [data.var_ub; [Inf for _ = 1:(GE+LE)]]
data.var_names = [data.var_names; ["__s$i" for i in 1:(GE+LE)]]
data.var_types = [data.var_types; ['C' for _ in 1:(GE+LE)]]
data.constr_lb = [
data.constr_lb[eq]
data.constr_lb[ge]
data.constr_ub[le]
]
data.constr_ub = copy(data.constr_lb)
end
function backwards!(t::AddSlackVariables, c::ConstraintSet)
c.lb += c.lhs * t.M2
c.ub += c.lhs * t.M2
c.lhs = (c.lhs*t.M1)[:, 1:t.ncols_orig]
end
function forward(t::AddSlackVariables, x::Vector{Float64})::Vector{Float64}
return [
x
t.lhs_ge * x - t.rhs_ge
t.rhs_le - t.lhs_le * x
]
end
# -----------------------------------------------------------------------------
Base.@kwdef mutable struct SplitFreeVars <: Transform
F::Int = 0
B::Int = 0
free::Vector{Int}=[]
others::Vector{Int}=[]
end
function forward!(t::SplitFreeVars, data::ProblemData)
lhs = data.constr_lhs
_, ncols = size(lhs)
free = [i for i in 1:ncols if !isfinite(data.var_lb[i]) && !isfinite(data.var_ub[i])]
others = [i for i in 1:ncols if isfinite(data.var_lb[i]) || isfinite(data.var_ub[i])]
t.F = length(free)
t.B = length(others)
t.free, t.others = free, others
data.obj = [
data.obj[others]
data.obj[free]
-data.obj[free]
]
data.constr_lhs = [lhs[:, others] lhs[:, free] -lhs[:, free]]
data.var_lb = [
data.var_lb[others]
[0.0 for _ in free]
[0.0 for _ in free]
]
data.var_ub = [
data.var_ub[others]
[Inf for _ in free]
[Inf for _ in free]
]
data.var_types = [
data.var_types[others]
data.var_types[free]
data.var_types[free]
]
data.var_names = [
data.var_names[others]
["$(v)_p" for v in data.var_names[free]]
["$(v)_m" for v in data.var_names[free]]
]
end
function backwards!(t::SplitFreeVars, c::ConstraintSet)
# Convert GE constraints into LE
nrows, _ = size(c.lhs)
ge = [i for i in 1:nrows if isfinite(c.lb[i])]
c.ub[ge], c.lb[ge] = -c.lb[ge], -c.ub[ge]
c.lhs[ge, :] *= -1
# Assert only LE constraints are left (EQ constraints are not supported)
@assert all(c.lb .== -Inf)
# Take minimum (weakest) coefficient
B, F = t.B, t.F
for i in 1:F
c.lhs[:, B + i] = min.(c.lhs[:, B + i], -c.lhs[:, B + F + i])
end
c.lhs = c.lhs[:, 1:(B+F)]
end
function forward(t::SplitFreeVars, p::Vector{Float64})::Vector{Float64}
return [
p[t.others]
max.(p[t.free], 0)
max.(-p[t.free], 0)
]
end
# -----------------------------------------------------------------------------
Base.@kwdef mutable struct FlipUnboundedBelowVars <: Transform
flip_idx::Vector{Int} = []
end
function forward!(t::FlipUnboundedBelowVars, data::ProblemData)
_, ncols = size(data.constr_lhs)
for i in 1:ncols
if isfinite(data.var_lb[i])
continue
end
data.obj[i] *= -1
data.constr_lhs[:, i] *= -1
data.var_lb[i], data.var_ub[i] = -data.var_ub[i], -data.var_lb[i]
push!(t.flip_idx, i)
end
end
function backwards!(t::FlipUnboundedBelowVars, c::ConstraintSet)
for i in t.flip_idx
c.lhs[:, i] *= -1
end
end
function forward(t::FlipUnboundedBelowVars, p::Vector{Float64})::Vector{Float64}
p2 = copy(p)
p2[t.flip_idx] *= -1
return p2
end
# -----------------------------------------------------------------------------
function _assert_standard_form(data::ProblemData)
# Check sizes
nrows, ncols = size(data.constr_lhs)
@assert length(data.constr_lb) == nrows
@assert length(data.constr_ub) == nrows
@assert length(data.obj) == ncols
@assert length(data.var_lb) == ncols
@assert length(data.var_ub) == ncols
@assert length(data.var_names) == ncols
@assert length(data.var_types) == ncols
# Check standard form
@assert all(data.var_lb .== 0.0)
@assert all(data.var_ub .== Inf)
@assert all(data.constr_lb .== data.constr_ub)
end
function convert_to_standard_form!(data::ProblemData)::Vector{Transform}
transforms = []
function _apply!(t)
push!(transforms, t)
forward!(t, data)
end
@timeit "Split free vars" begin
_apply!(SplitFreeVars())
end
@timeit "Flip unbounded-below vars" begin
_apply!(FlipUnboundedBelowVars())
end
@timeit "Shift var lower bounds to zero" begin
_apply!(ShiftVarLowerBoundsToZero())
end
@timeit "Move var upper bounds to constrs" begin
_apply!(MoveVarUpperBoundsToConstrs())
end
@timeit "Add slack vars" begin
_apply!(AddSlackVariables())
end
_assert_standard_form(data)
return transforms
end
function convert_to_standard_form(data::ProblemData)::Tuple{ProblemData,Vector{Transform}}
data2 = deepcopy(data)
transforms = convert_to_standard_form!(data2)
return (data2, transforms)
end
export convert_to_standard_form!,
convert_to_standard_form,
forward!,
backwards!,
backwards,
AddSlackVariables,
SplitFreeVars,
forward

@ -26,6 +26,6 @@ function __init__()
end
include("BB/BB.jl")
include("Cuts/BlackBox/cplex.jl")
include("Cuts/Cuts.jl")
end # module

Binary file not shown.

@ -3,7 +3,7 @@
# Released under the modified BSD license. See COPYING.md for more details.
using HDF5
using MIPLearn
using MIPLearn.Cuts
function test_cuts_blackbox_cplex()
# Prepare filenames
@ -11,7 +11,7 @@ function test_cuts_blackbox_cplex()
h5_filename = replace(mps_filename, ".mps.gz" => ".h5")
# Run collector
MIPLearn.collect(mps_filename, CplexBlackBoxCuts())
Cuts.collect(mps_filename, CplexBlackBoxCuts())
# Read HDF5 file
h5 = H5File(h5_filename)

Loading…
Cancel
Save