mirror of
https://github.com/ANL-CEEESA/MIPLearn.jl.git
synced 2025-12-06 16:38:51 -06:00
Make compatible with MIPLearn 0.2.0
This commit is contained in:
@@ -8,67 +8,56 @@ import Base: dump
|
||||
to_model(instance) =
|
||||
error("not implemented: to_model")
|
||||
|
||||
get_instance_features(instance) =
|
||||
error("not implemented: get_instance_features")
|
||||
get_instance_features(instance) = [0.0]
|
||||
|
||||
get_variable_features(instance, var, index) =
|
||||
error("not implemented: get_variable_features")
|
||||
get_variable_features(instance, varname) = [0.0]
|
||||
|
||||
get_variable_category(instance, var, index) = "default"
|
||||
get_variable_category(instance, varname) = "default"
|
||||
|
||||
find_violated_lazy_constraints(instance, model) = []
|
||||
|
||||
build_lazy_constraint(instance, model, v) = nothing
|
||||
|
||||
dump(instance::PyCall.PyObject, filename) = @pycall instance.dump(filename)
|
||||
load!(instance::PyCall.PyObject, filename) = @pycall instance.load(filename)
|
||||
|
||||
macro Instance(klass)
|
||||
quote
|
||||
@pydef mutable struct Wrapper <: Instance
|
||||
function __init__(self, args...; kwargs...)
|
||||
self.data = $(esc(klass))(args...; kwargs...)
|
||||
end
|
||||
|
||||
function dump(self, filename)
|
||||
prev_data = self.data
|
||||
self.data = JSON2.write(prev_data)
|
||||
Instance.dump(self, filename)
|
||||
self.data = prev_data
|
||||
end
|
||||
|
||||
function load(self, filename)
|
||||
Instance.load(self, filename)
|
||||
self.data = JSON2.read(self.data, $(esc(klass)))
|
||||
self.training_data = []
|
||||
self.features = miplearn.Features()
|
||||
end
|
||||
|
||||
to_model(self) =
|
||||
$(esc(:to_model))(self.data)
|
||||
|
||||
get_instance_features(self) =
|
||||
get_instance_features(self.data)
|
||||
$(esc(:get_instance_features))(self.data)
|
||||
|
||||
get_variable_features(self, var, index) =
|
||||
get_variable_features(self.data, var, index)
|
||||
get_variable_features(self, varname) =
|
||||
$(esc(:get_variable_features))(self.data, varname)
|
||||
|
||||
get_variable_category(self, var, index) =
|
||||
get_variable_category(self.data, var, index)
|
||||
get_variable_category(self, varname) =
|
||||
$(esc(:get_variable_category))(self.data, varname)
|
||||
|
||||
function find_violated_lazy_constraints(self, model)
|
||||
find_violated_lazy_constraints(self, model) =
|
||||
find_violated_lazy_constraints(self.data, model)
|
||||
end
|
||||
|
||||
function build_lazy_constraint(self, model, v)
|
||||
build_lazy_constraint(self, model, v) =
|
||||
build_lazy_constraint(self.data, model, v)
|
||||
end
|
||||
|
||||
load(self) = nothing
|
||||
|
||||
flush(self) = nothing
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
export get_instance_features,
|
||||
get_variable_features,
|
||||
get_variable_category,
|
||||
find_violated_lazy_constraints,
|
||||
build_lazy_constraint,
|
||||
to_model,
|
||||
dump,
|
||||
load!,
|
||||
@Instance
|
||||
@@ -9,23 +9,13 @@ using TimerOutputs
|
||||
|
||||
|
||||
mutable struct JuMPSolverData
|
||||
basename_idx_to_var
|
||||
var_to_basename_idx
|
||||
varname_to_var
|
||||
optimizer
|
||||
instance
|
||||
model
|
||||
bin_vars
|
||||
solution::Union{Nothing,Dict{String,Dict{String,Float64}}}
|
||||
time_limit::Union{Nothing, Float64}
|
||||
end
|
||||
|
||||
|
||||
function varname_split(varname::String)
|
||||
m = match(r"([^[]*)\[(.*)\]", varname)
|
||||
if m == nothing
|
||||
return varname, ""
|
||||
end
|
||||
return m.captures[1], m.captures[2]
|
||||
solution::Union{Nothing, Dict{String,Float64}}
|
||||
cname_to_constr
|
||||
end
|
||||
|
||||
|
||||
@@ -59,29 +49,24 @@ function optimize_and_capture_output!(model; tee::Bool=false)
|
||||
end
|
||||
|
||||
|
||||
function solve(data::JuMPSolverData; tee::Bool=false)
|
||||
function solve(
|
||||
data::JuMPSolverData;
|
||||
tee::Bool=false,
|
||||
iteration_cb,
|
||||
)::Dict
|
||||
instance, model = data.instance, data.model
|
||||
if data.time_limit != nothing
|
||||
JuMP.set_time_limit_sec(model, data.time_limit)
|
||||
end
|
||||
wallclock_time = 0
|
||||
found_lazy = []
|
||||
log = ""
|
||||
while true
|
||||
log *= optimize_and_capture_output!(model, tee=tee)
|
||||
wallclock_time += JuMP.solve_time(model)
|
||||
violations = instance.find_violated_lazy_constraints(model)
|
||||
if length(violations) == 0
|
||||
if iteration_cb !== nothing
|
||||
iteration_cb() || break
|
||||
else
|
||||
break
|
||||
end
|
||||
append!(found_lazy, violations)
|
||||
for v in violations
|
||||
instance.build_lazy_constraint(data.model, v)
|
||||
end
|
||||
end
|
||||
update_solution!(data)
|
||||
instance.found_violated_lazy_constraints = found_lazy
|
||||
instance.found_violated_user_cuts = []
|
||||
primal_bound = JuMP.objective_value(model)
|
||||
dual_bound = JuMP.objective_bound(model)
|
||||
if JuMP.objective_sense(model) == MOI.MIN_SENSE
|
||||
@@ -93,13 +78,15 @@ function solve(data::JuMPSolverData; tee::Bool=false)
|
||||
lower_bound = primal_bound
|
||||
upper_bound = dual_bound
|
||||
end
|
||||
return Dict("Lower bound" => lower_bound,
|
||||
"Upper bound" => upper_bound,
|
||||
"Sense" => sense,
|
||||
"Wallclock time" => wallclock_time,
|
||||
"Nodes" => 1,
|
||||
"Log" => log,
|
||||
"Warm start value" => nothing)
|
||||
return Dict(
|
||||
"Lower bound" => lower_bound,
|
||||
"Upper bound" => upper_bound,
|
||||
"Sense" => sense,
|
||||
"Wallclock time" => wallclock_time,
|
||||
"Nodes" => 1,
|
||||
"MIP log" => log,
|
||||
"Warm start value" => nothing,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
@@ -116,95 +103,270 @@ function solve_lp(data::JuMPSolverData; tee::Bool=false)
|
||||
for var in bin_vars
|
||||
JuMP.set_binary(var)
|
||||
end
|
||||
return Dict("Optimal value" => obj_value,
|
||||
"Log" => log)
|
||||
return Dict(
|
||||
"LP value" => obj_value,
|
||||
"LP log" => log,
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
function update_solution!(data::JuMPSolverData)
|
||||
var_to_basename_idx, model = data.var_to_basename_idx, data.model
|
||||
solution = Dict{String,Dict{String,Float64}}()
|
||||
for var in JuMP.all_variables(model)
|
||||
var in keys(var_to_basename_idx) || continue
|
||||
basename, idx = var_to_basename_idx[var]
|
||||
if !haskey(solution, basename)
|
||||
solution[basename] = Dict{String,Float64}()
|
||||
end
|
||||
solution[basename][idx] = JuMP.value(var)
|
||||
end
|
||||
data.solution = solution
|
||||
end
|
||||
|
||||
|
||||
function get_variables(data::JuMPSolverData)
|
||||
var_to_basename_idx, model = data.var_to_basename_idx, data.model
|
||||
variables = Dict()
|
||||
for var in JuMP.all_variables(model)
|
||||
var in keys(var_to_basename_idx) || continue
|
||||
basename, idx = var_to_basename_idx[var]
|
||||
if !haskey(variables, basename)
|
||||
variables[basename] = []
|
||||
end
|
||||
push!(variables[basename], idx)
|
||||
end
|
||||
return variables
|
||||
data.solution = Dict(
|
||||
JuMP.name(var) => JuMP.value(var)
|
||||
for var in JuMP.all_variables(data.model)
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
function set_instance!(data::JuMPSolverData, instance, model)
|
||||
data.instance = instance
|
||||
data.model = model
|
||||
data.var_to_basename_idx = Dict(var => varname_split(JuMP.name(var))
|
||||
for var in JuMP.all_variables(model))
|
||||
data.basename_idx_to_var = Dict(varname_split(JuMP.name(var)) => var
|
||||
for var in JuMP.all_variables(model))
|
||||
data.bin_vars = [var
|
||||
for var in JuMP.all_variables(model)
|
||||
if JuMP.is_binary(var)]
|
||||
if data.optimizer != nothing
|
||||
data.bin_vars = [
|
||||
var
|
||||
for var in JuMP.all_variables(data.model)
|
||||
if JuMP.is_binary(var)
|
||||
]
|
||||
data.varname_to_var = Dict(
|
||||
JuMP.name(var) => var
|
||||
for var in JuMP.all_variables(data.model)
|
||||
)
|
||||
if data.optimizer !== nothing
|
||||
JuMP.set_optimizer(model, data.optimizer)
|
||||
end
|
||||
end
|
||||
data.cname_to_constr = Dict()
|
||||
for (ftype, stype) in JuMP.list_of_constraint_types(model)
|
||||
for constr in JuMP.all_constraints(model, ftype, stype)
|
||||
name = JuMP.name(constr)
|
||||
length(name) > 0 || continue
|
||||
data.cname_to_constr[name] = constr
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function fix!(data::JuMPSolverData, solution)
|
||||
count = 0
|
||||
for (basename, subsolution) in solution
|
||||
for (idx, value) in subsolution
|
||||
value != nothing || continue
|
||||
var = data.basename_idx_to_var[basename, idx]
|
||||
JuMP.fix(var, value, force=true)
|
||||
count += 1
|
||||
end
|
||||
for (varname, value) in solution
|
||||
value !== nothing || continue
|
||||
var = data.varname_to_var[varname]
|
||||
JuMP.fix(var, value, force=true)
|
||||
end
|
||||
@info "Fixing $count variables"
|
||||
end
|
||||
|
||||
|
||||
function set_warm_start!(data::JuMPSolverData, solution)
|
||||
count = 0
|
||||
for (basename, subsolution) in solution
|
||||
for (idx, value) in subsolution
|
||||
value != nothing || continue
|
||||
var = data.basename_idx_to_var[basename, idx]
|
||||
JuMP.set_start_value(var, value)
|
||||
count += 1
|
||||
end
|
||||
for (varname, value) in solution
|
||||
value !== nothing || continue
|
||||
var = data.varname_to_var[varname]
|
||||
JuMP.set_start_value(var, value)
|
||||
end
|
||||
@info "Setting warm start values for $count variables"
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function get_variable_names(data::JuMPSolverData)
|
||||
return [JuMP.name(var) for var in JuMP.all_variables(data.model)]
|
||||
end
|
||||
|
||||
|
||||
function is_infeasible(data::JuMPSolverData)
|
||||
return JuMP.termination_status(data.model) == MOI.INFEASIBLE
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_ids(data::JuMPSolverData)
|
||||
return [cname for cname in keys(data.cname_to_constr)]
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_rhs(data::JuMPSolverData, cname)
|
||||
constr = data.cname_to_constr[cname]
|
||||
return get_constraint_rhs(constr)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_lhs(data::JuMPSolverData, cname)
|
||||
constr = data.cname_to_constr[cname]
|
||||
return get_constraint_lhs(constr)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_sense(data::JuMPSolverData, cname)
|
||||
constr = data.cname_to_constr[cname]
|
||||
return get_constraint_sense(constr)
|
||||
end
|
||||
|
||||
|
||||
# Constraints: ScalarAffineFunction, LessThan
|
||||
# -------------------------------------------------------------------------
|
||||
function get_constraint_rhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.LessThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::T where T
|
||||
return MOI.get(
|
||||
constr.model.moi_backend,
|
||||
MOI.ConstraintSet(),
|
||||
constr.index,
|
||||
).upper
|
||||
end
|
||||
|
||||
|
||||
function _terms_dict(constr)
|
||||
terms = MOI.get(
|
||||
constr.model.moi_backend,
|
||||
MOI.ConstraintFunction(),
|
||||
constr.index,
|
||||
).terms
|
||||
return Dict(
|
||||
MOI.get(
|
||||
constr.model.moi_backend,
|
||||
MOI.VariableName(),
|
||||
term.variable_index
|
||||
) => term.coefficient
|
||||
for term in terms
|
||||
)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_lhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.LessThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::Dict{String, T} where T
|
||||
return _terms_dict(constr)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_sense(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.LessThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::String where T
|
||||
return "<"
|
||||
end
|
||||
|
||||
|
||||
# Constraints: ScalarAffineFunction, GreaterThan
|
||||
# -------------------------------------------------------------------------
|
||||
function get_constraint_rhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.GreaterThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::T where T
|
||||
return MOI.get(
|
||||
constr.model.moi_backend,
|
||||
MOI.ConstraintSet(),
|
||||
constr.index,
|
||||
).lower
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_lhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.GreaterThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::Dict{String, T} where T
|
||||
return _terms_dict(constr)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_sense(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.GreaterThan{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::String where T
|
||||
return ">"
|
||||
end
|
||||
|
||||
|
||||
# Constraints: ScalarAffineFunction, EqualTo
|
||||
# -------------------------------------------------------------------------
|
||||
function get_constraint_rhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.EqualTo{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::T where T
|
||||
return MOI.get(
|
||||
constr.model.moi_backend,
|
||||
MOI.ConstraintSet(),
|
||||
constr.index,
|
||||
).value
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_lhs(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.EqualTo{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::Dict{String, T} where T
|
||||
return _terms_dict(constr)
|
||||
end
|
||||
|
||||
|
||||
function get_constraint_sense(
|
||||
constr::ConstraintRef{
|
||||
Model,
|
||||
MathOptInterface.ConstraintIndex{
|
||||
MathOptInterface.ScalarAffineFunction{T},
|
||||
MathOptInterface.EqualTo{T},
|
||||
},
|
||||
ScalarShape,
|
||||
},
|
||||
)::String where T
|
||||
return "="
|
||||
end
|
||||
|
||||
|
||||
@pydef mutable struct JuMPSolver <: miplearn.solvers.internal.InternalSolver
|
||||
function __init__(self; optimizer)
|
||||
self.data = JuMPSolverData(nothing, # basename_idx_to_var
|
||||
nothing, # var_to_basename_idx
|
||||
optimizer,
|
||||
nothing, # instance
|
||||
nothing, # model
|
||||
nothing, # bin_vars
|
||||
nothing, # solution
|
||||
nothing, # time limit
|
||||
)
|
||||
self.data = JuMPSolverData(
|
||||
nothing, # varname_to_var
|
||||
optimizer,
|
||||
nothing, # instance
|
||||
nothing, # model
|
||||
nothing, # bin_vars
|
||||
nothing, # solution
|
||||
nothing, # cname_to_constr
|
||||
)
|
||||
end
|
||||
|
||||
set_warm_start(self, solution) =
|
||||
@@ -216,8 +378,17 @@ end
|
||||
set_instance(self, instance, model) =
|
||||
set_instance!(self.data, instance, model)
|
||||
|
||||
solve(self; tee=false) =
|
||||
solve(self.data, tee=tee)
|
||||
solve(
|
||||
self;
|
||||
tee=false,
|
||||
iteration_cb,
|
||||
lazy_cb,
|
||||
user_cut_cb,
|
||||
) = solve(
|
||||
self.data,
|
||||
tee=tee,
|
||||
iteration_cb=iteration_cb,
|
||||
)
|
||||
|
||||
solve_lp(self; tee=false) =
|
||||
solve_lp(self.data, tee=tee)
|
||||
@@ -228,26 +399,40 @@ end
|
||||
get_variables(self) =
|
||||
get_variables(self.data)
|
||||
|
||||
set_time_limit(self, time_limit) =
|
||||
self.data.time_limit = time_limit
|
||||
|
||||
set_gap_tolerance(self, gap_tolerance) =
|
||||
@warn "JuMPSolver: set_gap_tolerance not implemented"
|
||||
|
||||
set_node_limit(self) =
|
||||
@warn "JuMPSolver: set_node_limit not implemented"
|
||||
|
||||
set_threads(self, threads) =
|
||||
@warn "JuMPSolver: set_threads not implemented"
|
||||
|
||||
set_branching_priorities(self, priorities) =
|
||||
@warn "JuMPSolver: set_branching_priorities not implemented"
|
||||
|
||||
add_constraint(self, constraint) = nothing
|
||||
add_constraint(self, constraint) =
|
||||
nothing
|
||||
|
||||
clear_warm_start(self) =
|
||||
error("JuMPSolver.clear_warm_start should never be called")
|
||||
get_variable_names(self) =
|
||||
get_variable_names(self.data)
|
||||
|
||||
is_infeasible(self) =
|
||||
is_infeasible(self.data)
|
||||
|
||||
get_constraint_ids(self) =
|
||||
get_constraint_ids(self.data)
|
||||
|
||||
get_constraint_rhs(self, cname) =
|
||||
get_constraint_rhs(self.data, cname)
|
||||
|
||||
get_constraint_lhs(self, cname) =
|
||||
get_constraint_lhs(self.data, cname)
|
||||
|
||||
get_constraint_sense(self, cname) =
|
||||
get_constraint_sense(self.data, cname)
|
||||
|
||||
clone(self) = self
|
||||
|
||||
add_cut(self) = error("not implemented")
|
||||
extract_constraint(self) = error("not implemented")
|
||||
is_constraint_satisfied(self) = error("not implemented")
|
||||
set_constraint_sense(self) = error("not implemented")
|
||||
relax(self) = error("not implemented")
|
||||
get_inequality_slacks(self) = error("not implemented")
|
||||
get_dual(self) = error("not implemented")
|
||||
get_sense(self) = error("not implemented")
|
||||
end
|
||||
|
||||
export JuMPSolver, solve!, fit!, add!
|
||||
export JuMPSolver, solve!, fit!, add!
|
||||
|
||||
@@ -10,19 +10,18 @@ function LearningSolver(;
|
||||
optimizer,
|
||||
kwargs...,
|
||||
)::LearningSolver
|
||||
py = @pycall miplearn.LearningSolver(;
|
||||
kwargs...,
|
||||
solver=JuMPSolver(optimizer=optimizer))
|
||||
py = miplearn.LearningSolver(
|
||||
;
|
||||
kwargs...,
|
||||
solver=JuMPSolver(optimizer=optimizer),
|
||||
)
|
||||
return LearningSolver(py)
|
||||
end
|
||||
|
||||
solve!(solver::LearningSolver, instance; kwargs...) =
|
||||
@pycall solver.py.solve(instance; kwargs...)
|
||||
solver.py.solve(instance; kwargs...)
|
||||
|
||||
fit!(solver::LearningSolver, instances; kwargs...) =
|
||||
@pycall solver.py.fit(instances; kwargs...)
|
||||
|
||||
add!(solver::LearningSolver, component; kwargs...) =
|
||||
@pycall solver.py.add(component; kwargs...)
|
||||
solver.py.fit(instances; kwargs...)
|
||||
|
||||
export LearningSolver
|
||||
Reference in New Issue
Block a user