mirror of
https://github.com/ANL-CEEESA/MIPLearn.jl.git
synced 2025-12-06 08:28:52 -06:00
Start implementing JumpSolver
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
|
||||
using CPLEX
|
||||
using JuMP
|
||||
using HDF5
|
||||
|
||||
Base.@kwdef struct CplexBlackBoxCuts
|
||||
threads::Int = 1
|
||||
@@ -26,10 +25,7 @@ function _add_mip_start!(env, lp, x::Vector{Float32})
|
||||
rval == 0 || error("CPXaddmipstarts failed: $rval")
|
||||
end
|
||||
|
||||
function collect(
|
||||
mps_filename::String,
|
||||
method::CplexBlackBoxCuts,
|
||||
)::Nothing
|
||||
function collect(mps_filename::String, method::CplexBlackBoxCuts)::Nothing
|
||||
tempdir = mktempdir()
|
||||
isfile(mps_filename) || error("file not found: $mps_filename")
|
||||
h5_filename = replace(mps_filename, ".mps.gz" => ".h5")
|
||||
@@ -47,8 +43,8 @@ function collect(
|
||||
CPXsetintparam(env, CPX_PARAM_PREDUAL, -1)
|
||||
CPXsetintparam(env, CPX_PARAM_PRESLVND, -1)
|
||||
|
||||
# Parameter: Enable logging
|
||||
CPXsetintparam(env, CPX_PARAM_SCRIND, 1)
|
||||
# Parameter: Disable logging
|
||||
CPXsetintparam(env, CPX_PARAM_SCRIND, 0)
|
||||
|
||||
# Parameter: Stop processing at the root node
|
||||
CPXsetintparam(env, CPX_PARAM_NODELIM, 0)
|
||||
@@ -68,7 +64,7 @@ function collect(
|
||||
CPXreadcopyprob(env, lp, mps_filename, "mps")
|
||||
|
||||
# Load warm start
|
||||
h5 = Hdf5Sample(h5_filename)
|
||||
h5 = H5File(h5_filename)
|
||||
var_values = h5.get_array("mip_var_values")
|
||||
h5.file.close()
|
||||
_add_mip_start!(env, lp, var_values)
|
||||
@@ -80,13 +76,17 @@ function collect(
|
||||
CPXwriteprob(env, nodelp_p[1], "$tempdir/root.mps", C_NULL)
|
||||
return 0
|
||||
end
|
||||
c_solve_callback = @cfunction($solve_callback, Cint, (
|
||||
CPXENVptr, # env
|
||||
Ptr{Cvoid}, # cbdata
|
||||
Cint, # wherefrom
|
||||
Ptr{Cvoid}, # cbhandle
|
||||
Ptr{Cint}, # useraction_p
|
||||
))
|
||||
c_solve_callback = @cfunction(
|
||||
$solve_callback,
|
||||
Cint,
|
||||
(
|
||||
CPXENVptr, # env
|
||||
Ptr{Cvoid}, # cbdata
|
||||
Cint, # wherefrom
|
||||
Ptr{Cvoid}, # cbhandle
|
||||
Ptr{Cint}, # useraction_p
|
||||
)
|
||||
)
|
||||
CPXsetsolvecallbackfunc(env, c_solve_callback, C_NULL)
|
||||
|
||||
# Run optimization
|
||||
@@ -96,18 +96,20 @@ function collect(
|
||||
model = JuMP.read_from_file("$tempdir/root.mps")
|
||||
|
||||
function select(cr)
|
||||
return name(cr)[begin] in ['i', 'f', 'm', 'r', 'L', 'z', 'v'] && isdigit(name(cr)[begin+1])
|
||||
return name(cr)[begin] in ['i', 'f', 'm', 'r', 'L', 'z', 'v'] &&
|
||||
isdigit(name(cr)[begin+1])
|
||||
end
|
||||
|
||||
# Parse cuts
|
||||
constraints = all_constraints(model, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64})
|
||||
constraints =
|
||||
all_constraints(model, GenericAffExpr{Float64,VariableRef}, MOI.LessThan{Float64})
|
||||
nvars = num_variables(model)
|
||||
ncuts = length([cr for cr in constraints if select(cr)])
|
||||
cuts_lhs = spzeros(ncuts, nvars)
|
||||
cuts_rhs = Float64[]
|
||||
cuts_var_names = String[]
|
||||
|
||||
for i in 1:nvars
|
||||
for i = 1:nvars
|
||||
push!(cuts_var_names, name(VariableRef(model, MOI.VariableIndex(i))))
|
||||
end
|
||||
|
||||
@@ -121,20 +123,18 @@ function collect(
|
||||
if (idx < 1 || idx > nvars)
|
||||
error("invalid index: $idx")
|
||||
end
|
||||
cuts_lhs[offset, idx - 1] = val
|
||||
cuts_lhs[offset, idx-1] = val
|
||||
end
|
||||
push!(cuts_rhs, cset.upper)
|
||||
offset += 1
|
||||
end
|
||||
end
|
||||
|
||||
@info "Storing $(length(cuts_rhs)) CPLEX cuts..."
|
||||
h5 = Hdf5Sample(h5_filename)
|
||||
|
||||
h5 = H5File(h5_filename)
|
||||
h5.put_sparse("cuts_cpx_lhs", cuts_lhs)
|
||||
h5.put_array("cuts_cpx_rhs", cuts_rhs)
|
||||
h5.put_array("cuts_cpx_var_names", to_str_array(cuts_var_names))
|
||||
h5.file.close()
|
||||
|
||||
h5.close()
|
||||
return
|
||||
end
|
||||
|
||||
|
||||
@@ -7,44 +7,15 @@ module MIPLearn
|
||||
using PyCall
|
||||
using SparseArrays
|
||||
|
||||
global miplearn = PyNULL()
|
||||
global Hdf5Sample = PyNULL()
|
||||
|
||||
to_str_array(values) = py"to_str_array"(values)
|
||||
|
||||
from_str_array(values) = py"from_str_array"(values)
|
||||
|
||||
function __init__()
|
||||
copy!(miplearn, pyimport("miplearn"))
|
||||
copy!(Hdf5Sample, miplearn.features.sample.Hdf5Sample)
|
||||
|
||||
py"""
|
||||
import numpy as np
|
||||
|
||||
def to_str_array(values):
|
||||
if values is None:
|
||||
return None
|
||||
return np.array(values, dtype="S")
|
||||
|
||||
def from_str_array(values):
|
||||
return [v.decode() for v in values]
|
||||
"""
|
||||
end
|
||||
|
||||
function convert(::Type{SparseMatrixCSC}, o::PyObject)
|
||||
I, J, V = pyimport("scipy.sparse").find(o)
|
||||
return sparse(I .+ 1, J .+ 1, V, o.shape...)
|
||||
end
|
||||
|
||||
function PyObject(m::SparseMatrixCSC)
|
||||
pyimport("scipy.sparse").csc_matrix(
|
||||
(m.nzval, m.rowval .- 1, m.colptr .- 1),
|
||||
shape = size(m),
|
||||
).tocoo()
|
||||
end
|
||||
|
||||
include("problems/setcover.jl")
|
||||
include("io.jl")
|
||||
include("solvers/jump.jl")
|
||||
include("Cuts/BlackBox/cplex.jl")
|
||||
|
||||
export Hdf5Sample
|
||||
function __init__()
|
||||
__init_problems_setcover__()
|
||||
__init_io__()
|
||||
__init_solvers_jump__()
|
||||
end
|
||||
|
||||
end # module
|
||||
end # module
|
||||
|
||||
35
src/io.jl
Normal file
35
src/io.jl
Normal file
@@ -0,0 +1,35 @@
|
||||
global H5File = PyNULL()
|
||||
|
||||
to_str_array(values) = py"to_str_array"(values)
|
||||
|
||||
from_str_array(values) = py"from_str_array"(values)
|
||||
|
||||
function __init_io__()
|
||||
copy!(H5File, pyimport("miplearn.h5").H5File)
|
||||
|
||||
py"""
|
||||
import numpy as np
|
||||
|
||||
def to_str_array(values):
|
||||
if values is None:
|
||||
return None
|
||||
return np.array(values, dtype="S")
|
||||
|
||||
def from_str_array(values):
|
||||
return [v.decode() for v in values]
|
||||
"""
|
||||
end
|
||||
|
||||
function convert(::Type{SparseMatrixCSC}, o::PyObject)
|
||||
I, J, V = pyimport("scipy.sparse").find(o)
|
||||
return sparse(I .+ 1, J .+ 1, V, o.shape...)
|
||||
end
|
||||
|
||||
function PyObject(m::SparseMatrixCSC)
|
||||
pyimport("scipy.sparse").csc_matrix(
|
||||
(m.nzval, m.rowval .- 1, m.colptr .- 1),
|
||||
shape = size(m),
|
||||
).tocoo()
|
||||
end
|
||||
|
||||
export H5File
|
||||
28
src/problems/setcover.jl
Normal file
28
src/problems/setcover.jl
Normal file
@@ -0,0 +1,28 @@
|
||||
global SetCoverData = PyNULL()
|
||||
global SetCoverGenerator = PyNULL()
|
||||
|
||||
using JuMP
|
||||
using HiGHS
|
||||
|
||||
function __init_problems_setcover__()
|
||||
copy!(SetCoverData, pyimport("miplearn.problems.setcover").SetCoverData)
|
||||
copy!(SetCoverGenerator, pyimport("miplearn.problems.setcover").SetCoverGenerator)
|
||||
end
|
||||
|
||||
function build_setcover_model(data; optimizer = HiGHS.Optimizer)
|
||||
model = Model(optimizer)
|
||||
set_silent(model)
|
||||
n_elements, n_sets = size(data.incidence_matrix)
|
||||
E = 0:n_elements-1
|
||||
S = 0:n_sets-1
|
||||
@variable(model, x[S], Bin)
|
||||
@objective(model, Min, sum(data.costs .* x))
|
||||
@constraint(
|
||||
model,
|
||||
eqs[e in E],
|
||||
sum(data.incidence_matrix[e+1, s+1] * x[s] for s in S) >= 1
|
||||
)
|
||||
return JumpModel(model)
|
||||
end
|
||||
|
||||
export SetCoverData, SetCoverGenerator, build_setcover_model
|
||||
290
src/solvers/jump.jl
Normal file
290
src/solvers/jump.jl
Normal file
@@ -0,0 +1,290 @@
|
||||
using JuMP
|
||||
using HiGHS
|
||||
|
||||
global JumpModel = PyNULL()
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
function _add_constrs(
|
||||
model::JuMP.Model,
|
||||
var_names,
|
||||
constrs_lhs,
|
||||
constrs_sense,
|
||||
constrs_rhs,
|
||||
stats,
|
||||
) end
|
||||
|
||||
function _extract_after_load(model::JuMP.Model, h5)
|
||||
if JuMP.objective_sense(model) == MOI.MIN_SENSE
|
||||
h5.put_scalar("static_sense", "min")
|
||||
else
|
||||
h5.put_scalar("static_sense", "max")
|
||||
end
|
||||
_extract_after_load_vars(model, h5)
|
||||
_extract_after_load_constrs(model, h5)
|
||||
end
|
||||
|
||||
function _extract_after_load_vars(model::JuMP.Model, h5)
|
||||
vars = JuMP.all_variables(model)
|
||||
lb = [
|
||||
JuMP.is_binary(v) ? 0.0 : JuMP.has_lower_bound(v) ? JuMP.lower_bound(v) : -Inf
|
||||
for v in vars
|
||||
]
|
||||
ub = [
|
||||
JuMP.is_binary(v) ? 1.0 : JuMP.has_upper_bound(v) ? JuMP.upper_bound(v) : Inf
|
||||
for v in vars
|
||||
]
|
||||
types = [JuMP.is_binary(v) ? "B" : JuMP.is_integer(v) ? "I" : "C" for v in vars]
|
||||
obj = objective_function(model, AffExpr)
|
||||
obj_coeffs = [v ∈ keys(obj.terms) ? obj.terms[v] : 0.0 for v in vars]
|
||||
h5.put_array("static_var_names", to_str_array(JuMP.name.(vars)))
|
||||
h5.put_array("static_var_types", to_str_array(types))
|
||||
h5.put_array("static_var_lower_bounds", lb)
|
||||
h5.put_array("static_var_upper_bounds", ub)
|
||||
h5.put_array("static_var_obj_coeffs", obj_coeffs)
|
||||
h5.put_scalar("static_obj_offset", obj.constant)
|
||||
end
|
||||
|
||||
function _extract_after_load_constrs(model::JuMP.Model, h5)
|
||||
names = String[]
|
||||
senses, rhs = String[], Float64[]
|
||||
lhs_rows, lhs_cols, lhs_values = Int[], Int[], Float64[]
|
||||
|
||||
constr_index = 1
|
||||
for (ftype, stype) in JuMP.list_of_constraint_types(model)
|
||||
for constr in JuMP.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)
|
||||
|
||||
name = JuMP.name(constr)
|
||||
length(name) > 0 || continue
|
||||
push!(names, name)
|
||||
|
||||
# LHS, RHS and sense
|
||||
if ftype == VariableRef
|
||||
# nop
|
||||
elseif ftype == AffExpr
|
||||
if stype == MOI.EqualTo{Float64}
|
||||
rhs_c = cset.value
|
||||
push!(senses, "=")
|
||||
elseif stype == MOI.LessThan{Float64}
|
||||
rhs_c = cset.upper
|
||||
push!(senses, "<")
|
||||
elseif stype == MOI.GreaterThan{Float64}
|
||||
rhs_c = cset.lower
|
||||
push!(senses, ">")
|
||||
else
|
||||
error("Unsupported set: $stype")
|
||||
end
|
||||
push!(rhs, rhs_c)
|
||||
for term in cf.terms
|
||||
push!(lhs_cols, term.variable.value)
|
||||
push!(lhs_rows, constr_index)
|
||||
push!(lhs_values, term.coefficient)
|
||||
end
|
||||
constr_index += 1
|
||||
else
|
||||
error("Unsupported constraint type: ($ftype, $stype)")
|
||||
end
|
||||
end
|
||||
end
|
||||
lhs = sparse(lhs_rows, lhs_cols, lhs_values, length(rhs), JuMP.num_variables(model))
|
||||
h5.put_sparse("static_constr_lhs", lhs)
|
||||
h5.put_array("static_constr_rhs", rhs)
|
||||
h5.put_array("static_constr_sense", to_str_array(senses))
|
||||
h5.put_array("static_constr_names", to_str_array(names))
|
||||
end
|
||||
|
||||
function _extract_after_lp(model::JuMP.Model, h5)
|
||||
h5.put_scalar("lp_wallclock_time", solve_time(model))
|
||||
h5.put_scalar("lp_obj_value", objective_value(model))
|
||||
_extract_after_lp_vars(model, h5)
|
||||
_extract_after_lp_constrs(model, h5)
|
||||
end
|
||||
|
||||
function _extract_after_lp_vars(model::JuMP.Model, h5)
|
||||
# Values and reduced costs
|
||||
vars = all_variables(model)
|
||||
h5.put_array("lp_var_values", JuMP.value.(vars))
|
||||
h5.put_array("lp_var_reduced_costs", reduced_cost.(vars))
|
||||
|
||||
# Basis status
|
||||
basis_status = []
|
||||
for var in vars
|
||||
bstatus = MOI.get(model, MOI.VariableBasisStatus(), var)
|
||||
if bstatus == MOI.BASIC
|
||||
bstatus_v = "B"
|
||||
elseif bstatus == MOI.NONBASIC_AT_LOWER
|
||||
bstatus_v = "L"
|
||||
elseif bstatus == MOI.NONBASIC_AT_UPPER
|
||||
bstatus_v = "U"
|
||||
else
|
||||
error("Unknown basis status: $(bstatus)")
|
||||
end
|
||||
push!(basis_status, bstatus_v)
|
||||
end
|
||||
h5.put_array("lp_var_basis_status", to_str_array(basis_status))
|
||||
|
||||
# Sensitivity analysis
|
||||
obj_coeffs = h5.get_array("static_var_obj_coeffs")
|
||||
sensitivity_report = lp_sensitivity_report(model)
|
||||
sa_obj_down, sa_obj_up = Float64[], Float64[]
|
||||
sa_lb_down, sa_lb_up = Float64[], Float64[]
|
||||
sa_ub_down, sa_ub_up = Float64[], Float64[]
|
||||
for (i, v) in enumerate(vars)
|
||||
# Objective function
|
||||
(delta_down, delta_up) = sensitivity_report[v]
|
||||
push!(sa_obj_down, delta_down + obj_coeffs[i])
|
||||
push!(sa_obj_up, delta_up + obj_coeffs[i])
|
||||
|
||||
# Lower bound
|
||||
if has_lower_bound(v)
|
||||
constr = LowerBoundRef(v)
|
||||
(delta_down, delta_up) = sensitivity_report[constr]
|
||||
push!(sa_lb_down, lower_bound(v) + delta_down)
|
||||
push!(sa_lb_up, lower_bound(v) + delta_up)
|
||||
else
|
||||
push!(sa_lb_down, -Inf)
|
||||
push!(sa_lb_up, -Inf)
|
||||
end
|
||||
|
||||
# Upper bound
|
||||
if has_upper_bound(v)
|
||||
constr = JuMP.UpperBoundRef(v)
|
||||
(delta_down, delta_up) = sensitivity_report[constr]
|
||||
push!(sa_ub_down, upper_bound(v) + delta_down)
|
||||
push!(sa_ub_up, upper_bound(v) + delta_up)
|
||||
else
|
||||
push!(sa_ub_down, Inf)
|
||||
push!(sa_ub_up, Inf)
|
||||
end
|
||||
end
|
||||
h5.put_array("lp_var_sa_obj_up", sa_obj_up)
|
||||
h5.put_array("lp_var_sa_obj_down", sa_obj_down)
|
||||
h5.put_array("lp_var_sa_ub_up", sa_ub_up)
|
||||
h5.put_array("lp_var_sa_ub_down", sa_ub_down)
|
||||
h5.put_array("lp_var_sa_lb_up", sa_lb_up)
|
||||
h5.put_array("lp_var_sa_lb_down", sa_lb_down)
|
||||
end
|
||||
|
||||
|
||||
function _extract_after_lp_constrs(model::JuMP.Model, h5)
|
||||
# Slacks
|
||||
lhs = h5.get_sparse("static_constr_lhs")
|
||||
rhs = h5.get_array("static_constr_rhs")
|
||||
x = h5.get_array("lp_var_values")
|
||||
slacks = abs.(lhs * x - rhs)
|
||||
h5.put_array("lp_constr_slacks", slacks)
|
||||
|
||||
sa_rhs_up, sa_rhs_down = Float64[], Float64[]
|
||||
duals = Float64[]
|
||||
basis_status = []
|
||||
constr_idx = 1
|
||||
sensitivity_report = lp_sensitivity_report(model)
|
||||
for (ftype, stype) in JuMP.list_of_constraint_types(model)
|
||||
for constr in JuMP.all_constraints(model, ftype, stype)
|
||||
length(JuMP.name(constr)) > 0 || continue
|
||||
|
||||
# Duals
|
||||
push!(duals, JuMP.dual(constr))
|
||||
|
||||
# Basis status
|
||||
b = MOI.get(model, MOI.ConstraintBasisStatus(), constr)
|
||||
if b == MOI.NONBASIC
|
||||
push!(basis_status, "N")
|
||||
elseif b == MOI.BASIC
|
||||
push!(basis_status, "B")
|
||||
else
|
||||
error("Unknown basis status: $b")
|
||||
end
|
||||
|
||||
# Sensitivity analysis
|
||||
(delta_down, delta_up) = sensitivity_report[constr]
|
||||
push!(sa_rhs_down, rhs[constr_idx] + delta_down)
|
||||
push!(sa_rhs_up, rhs[constr_idx] + delta_up)
|
||||
|
||||
constr_idx += 1
|
||||
end
|
||||
end
|
||||
h5.put_array("lp_constr_dual_values", duals)
|
||||
h5.put_array("lp_constr_basis_status", to_str_array(basis_status))
|
||||
h5.put_array("lp_constr_sa_rhs_up", sa_rhs_up)
|
||||
h5.put_array("lp_constr_sa_rhs_down", sa_rhs_down)
|
||||
end
|
||||
|
||||
function _extract_after_mip(model::JuMP.Model, h5)
|
||||
h5.put_scalar("mip_obj_value", objective_value(model))
|
||||
h5.put_scalar("mip_obj_bound", objective_bound(model))
|
||||
h5.put_scalar("mip_wallclock_time", solve_time(model))
|
||||
h5.put_scalar("mip_gap", relative_gap(model))
|
||||
|
||||
# Values
|
||||
vars = all_variables(model)
|
||||
x = JuMP.value.(vars)
|
||||
h5.put_array("mip_var_values", x)
|
||||
|
||||
# Slacks
|
||||
lhs = h5.get_sparse("static_constr_lhs")
|
||||
rhs = h5.get_array("static_constr_rhs")
|
||||
slacks = abs.(lhs * x - rhs)
|
||||
h5.put_array("mip_constr_slacks", slacks)
|
||||
end
|
||||
|
||||
function _fix_variables(model::JuMP.Model, var_names, var_values, stats) end
|
||||
|
||||
function _optimize(model::JuMP.Model)
|
||||
optimize!(model)
|
||||
end
|
||||
|
||||
function _relax(model::JuMP.Model)
|
||||
relaxed, _ = copy_model(model)
|
||||
relax_integrality(relaxed)
|
||||
# FIXME: Remove hardcoded optimizer
|
||||
set_optimizer(relaxed, HiGHS.Optimizer)
|
||||
set_silent(relaxed)
|
||||
return relaxed
|
||||
end
|
||||
|
||||
function _set_warm_starts(model::JuMP.Model, var_names, var_values, stats) end
|
||||
|
||||
function _write(model::JuMP.Model, filename) end
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
function __init_solvers_jump__()
|
||||
@pydef mutable struct Class
|
||||
|
||||
function __init__(self, inner)
|
||||
self.inner = inner
|
||||
end
|
||||
|
||||
add_constrs(self, var_names, constrs_lhs, constrs_sense, constrs_rhs, stats) =
|
||||
_add_constrs(
|
||||
self.inner,
|
||||
var_names,
|
||||
constrs_lhs,
|
||||
constrs_sense,
|
||||
constrs_rhs,
|
||||
stats,
|
||||
)
|
||||
|
||||
extract_after_load(self, h5) = _extract_after_load(self.inner, h5)
|
||||
|
||||
extract_after_lp(self, h5) = _extract_after_lp(self.inner, h5)
|
||||
|
||||
extract_after_mip(self, h5) = _extract_after_mip(self.inner, h5)
|
||||
|
||||
fix_variables(self, var_names, var_values, stats) =
|
||||
_fix_variables(self.inner, var_names, var_values, stats)
|
||||
|
||||
optimize(self) = _optimize(self.inner)
|
||||
|
||||
relax(self) = Class(_relax(self.inner))
|
||||
|
||||
set_warm_starts(self, var_names, var_values, stats) =
|
||||
_set_warm_starts(self.inner, var_names, var_values, stats)
|
||||
|
||||
write(self, filename) = _write(self.inner, filename)
|
||||
end
|
||||
copy!(JumpModel, Class)
|
||||
end
|
||||
Reference in New Issue
Block a user