feature/replay
Alinson S. Xavier 2 years ago
parent 1ea432fb57
commit 20d6570ea6
Signed by: isoron
GPG Key ID: 0DA8E4B9E1109DCA

@ -31,9 +31,9 @@ function print_progress(
node::Node; node::Node;
time_elapsed::Float64, time_elapsed::Float64,
print_interval::Int, print_interval::Int,
primal_update::Bool, primal_update::Bool
)::Nothing )::Nothing
if (pool.processed % print_interval == 0) || isempty(pool.pending) || primal_update if (pool.processed % print_interval == 0) || isempty(pool.pending)
if isempty(node.branch_vars) if isempty(node.branch_vars)
branch_var_name = "---" branch_var_name = "---"
branch_lb = "---" branch_lb = "---"

@ -11,7 +11,7 @@ function take(
suggestions::Array{Node}=[], suggestions::Array{Node}=[],
time_remaining::Float64, time_remaining::Float64,
gap_limit::Float64, gap_limit::Float64,
node_limit::Int, node_limit::Int
)::Union{Symbol,Node} )::Union{Symbol,Node}
t = threadid() t = threadid()
lock(pool.lock) do lock(pool.lock) do
@ -54,7 +54,7 @@ function offer(
parent_node::Union{Nothing,Node}, parent_node::Union{Nothing,Node},
child_nodes::Vector{Node}, child_nodes::Vector{Node},
time_elapsed::Float64=0.0, time_elapsed::Float64=0.0,
print_interval::Int = 100, print_interval::Int=100
)::Nothing )::Nothing
lock(pool.lock) do lock(pool.lock) do
primal_update = false primal_update = false
@ -101,6 +101,7 @@ function offer(
# Update branching variable history # Update branching variable history
branch_var = child_nodes[1].branch_vars[end] branch_var = child_nodes[1].branch_vars[end]
offset = findfirst(isequal(branch_var), parent_node.fractional_variables) offset = findfirst(isequal(branch_var), parent_node.fractional_variables)
if offset !== nothing
x = parent_node.fractional_values[offset] x = parent_node.fractional_values[offset]
obj_change_up = child_nodes[1].obj - parent_node.obj obj_change_up = child_nodes[1].obj - parent_node.obj
obj_change_down = child_nodes[2].obj - parent_node.obj obj_change_down = child_nodes[2].obj - parent_node.obj
@ -117,6 +118,7 @@ function offer(
pool.history.avg_pseudocost_down = pool.history.avg_pseudocost_down =
mean(vh.pseudocost_down for vh in values(pool.var_history)) mean(vh.pseudocost_down for vh in values(pool.var_history))
end end
end
for node in child_nodes for node in child_nodes
print_progress( print_progress(
@ -136,7 +138,7 @@ function _update_var_history(;
var::Variable, var::Variable,
x::Float64, x::Float64,
obj_change_down::Float64, obj_change_down::Float64,
obj_change_up::Float64, obj_change_up::Float64
)::Nothing )::Nothing
# Create new history entry # Create new history entry
if var keys(pool.var_history) if var keys(pool.var_history)

@ -17,9 +17,15 @@ function solve!(
initial_primal_bound::Float64=Inf, initial_primal_bound::Float64=Inf,
branch_rule::VariableBranchingRule=ReliabilityBranching(), branch_rule::VariableBranchingRule=ReliabilityBranching(),
enable_plunging=true, enable_plunging=true,
)::NodePool replay=nothing
)::Tuple{NodePool,ReplayInfo}
if replay === nothing
replay = ReplayInfo()
end
time_initial = time() time_initial = time()
pool = NodePool(mip = mip) pool = NodePool(mip=mip, next_index=replay.next_index)
pool.primal_bound = initial_primal_bound pool.primal_bound = initial_primal_bound
root_node = _create_node(mip) root_node = _create_node(mip)
@ -64,10 +70,25 @@ function solve!(
@assert status == :Optimal @assert status == :Optimal
_unset_node_bounds(node) _unset_node_bounds(node)
if node.index in keys(replay.node_decisions)
decision = replay.node_decisions[node.index]
ids = decision.ids
branch_var = decision.branch_var
var_value = decision.var_value
else
# Find branching variable # Find branching variable
ids = generate_indices(pool, 2) ids = generate_indices(pool, 2)
branch_var = find_branching_var(branch_rule, node, pool) branch_var = find_branching_var(branch_rule, node, pool)
# Query current fractional value
offset = findfirst(isequal(branch_var), node.fractional_variables)
var_value = node.fractional_values[offset]
# Update replay
decision = ReplayNodeDecision(; branch_var, var_value, ids)
replay.node_decisions[node.index] = decision
end
# Find current variable lower and upper bounds # Find current variable lower and upper bounds
offset = findfirst(isequal(branch_var), mip.int_vars) offset = findfirst(isequal(branch_var), mip.int_vars)
var_lb = mip.int_vars_lb[offset] var_lb = mip.int_vars_lb[offset]
@ -79,10 +100,6 @@ function solve!(
end end
end end
# Query current fractional value
offset = findfirst(isequal(branch_var), node.fractional_variables)
var_value = node.fractional_values[offset]
child_zero = _create_node( child_zero = _create_node(
mip, mip,
index=ids[2], index=ids[2],
@ -109,7 +126,8 @@ function solve!(
end end
end end
end end
return pool replay.next_index = pool.next_index
return pool, replay
end end
function _create_node( function _create_node(
@ -118,7 +136,7 @@ function _create_node(
parent::Union{Nothing,Node}=nothing, parent::Union{Nothing,Node}=nothing,
branch_var::Union{Nothing,Variable}=nothing, branch_var::Union{Nothing,Variable}=nothing,
branch_var_lb::Union{Nothing,Float64}=nothing, branch_var_lb::Union{Nothing,Float64}=nothing,
branch_var_ub::Union{Nothing,Float64} = nothing, branch_var_ub::Union{Nothing,Float64}=nothing
)::Node )::Node
if parent === nothing if parent === nothing
branch_vars = Variable[] branch_vars = Variable[]

@ -72,3 +72,14 @@ Base.@kwdef mutable struct NodePool
history::History = History() history::History = History()
var_history::Dict{Variable,VariableHistory} = Dict() var_history::Dict{Variable,VariableHistory} = Dict()
end end
Base.@kwdef struct ReplayNodeDecision
branch_var
var_value
ids
end
Base.@kwdef mutable struct ReplayInfo
node_decisions::Dict{Int,ReplayNodeDecision} = Dict()
next_index::Int = 1
end

@ -36,13 +36,14 @@ function _add_constrs(
end end
function _extract_after_load(model::JuMP.Model, h5) function _extract_after_load(model::JuMP.Model, h5)
@info "_extract_after_load"
if JuMP.objective_sense(model) == MOI.MIN_SENSE if JuMP.objective_sense(model) == MOI.MIN_SENSE
h5.put_scalar("static_sense", "min") h5.put_scalar("static_sense", "min")
else else
h5.put_scalar("static_sense", "max") h5.put_scalar("static_sense", "max")
end end
_extract_after_load_vars(model, h5) @time _extract_after_load_vars(model, h5)
_extract_after_load_constrs(model, h5) @time _extract_after_load_constrs(model, h5)
end end
function _extract_after_load_vars(model::JuMP.Model, h5) function _extract_after_load_vars(model::JuMP.Model, h5)
@ -117,10 +118,11 @@ function _extract_after_load_constrs(model::JuMP.Model, h5)
end end
function _extract_after_lp(model::JuMP.Model, h5) function _extract_after_lp(model::JuMP.Model, h5)
@info "_extract_after_lp"
h5.put_scalar("lp_wallclock_time", solve_time(model)) h5.put_scalar("lp_wallclock_time", solve_time(model))
h5.put_scalar("lp_obj_value", objective_value(model)) h5.put_scalar("lp_obj_value", objective_value(model))
_extract_after_lp_vars(model, h5) @time _extract_after_lp_vars(model, h5)
_extract_after_lp_constrs(model, h5) @time _extract_after_lp_constrs(model, h5)
end end
function _extract_after_lp_vars(model::JuMP.Model, h5) function _extract_after_lp_vars(model::JuMP.Model, h5)
@ -146,46 +148,46 @@ function _extract_after_lp_vars(model::JuMP.Model, h5)
end end
h5.put_array("lp_var_basis_status", to_str_array(basis_status)) h5.put_array("lp_var_basis_status", to_str_array(basis_status))
# Sensitivity analysis # # Sensitivity analysis
obj_coeffs = h5.get_array("static_var_obj_coeffs") # obj_coeffs = h5.get_array("static_var_obj_coeffs")
sensitivity_report = lp_sensitivity_report(model) # sensitivity_report = lp_sensitivity_report(model)
sa_obj_down, sa_obj_up = Float64[], Float64[] # sa_obj_down, sa_obj_up = Float64[], Float64[]
sa_lb_down, sa_lb_up = Float64[], Float64[] # sa_lb_down, sa_lb_up = Float64[], Float64[]
sa_ub_down, sa_ub_up = Float64[], Float64[] # sa_ub_down, sa_ub_up = Float64[], Float64[]
for (i, v) in enumerate(vars) # for (i, v) in enumerate(vars)
# Objective function # # Objective function
(delta_down, delta_up) = sensitivity_report[v] # (delta_down, delta_up) = sensitivity_report[v]
push!(sa_obj_down, delta_down + obj_coeffs[i]) # push!(sa_obj_down, delta_down + obj_coeffs[i])
push!(sa_obj_up, delta_up + obj_coeffs[i]) # push!(sa_obj_up, delta_up + obj_coeffs[i])
# Lower bound # # Lower bound
if has_lower_bound(v) # if has_lower_bound(v)
constr = LowerBoundRef(v) # constr = LowerBoundRef(v)
(delta_down, delta_up) = sensitivity_report[constr] # (delta_down, delta_up) = sensitivity_report[constr]
push!(sa_lb_down, lower_bound(v) + delta_down) # push!(sa_lb_down, lower_bound(v) + delta_down)
push!(sa_lb_up, lower_bound(v) + delta_up) # push!(sa_lb_up, lower_bound(v) + delta_up)
else # else
push!(sa_lb_down, -Inf) # push!(sa_lb_down, -Inf)
push!(sa_lb_up, -Inf) # push!(sa_lb_up, -Inf)
end # end
# Upper bound # # Upper bound
if has_upper_bound(v) # if has_upper_bound(v)
constr = JuMP.UpperBoundRef(v) # constr = JuMP.UpperBoundRef(v)
(delta_down, delta_up) = sensitivity_report[constr] # (delta_down, delta_up) = sensitivity_report[constr]
push!(sa_ub_down, upper_bound(v) + delta_down) # push!(sa_ub_down, upper_bound(v) + delta_down)
push!(sa_ub_up, upper_bound(v) + delta_up) # push!(sa_ub_up, upper_bound(v) + delta_up)
else # else
push!(sa_ub_down, Inf) # push!(sa_ub_down, Inf)
push!(sa_ub_up, Inf) # push!(sa_ub_up, Inf)
end # end
end # end
h5.put_array("lp_var_sa_obj_up", sa_obj_up) # 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_obj_down", sa_obj_down)
h5.put_array("lp_var_sa_ub_up", sa_ub_up) # 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_ub_down", sa_ub_down)
h5.put_array("lp_var_sa_lb_up", sa_lb_up) # h5.put_array("lp_var_sa_lb_up", sa_lb_up)
h5.put_array("lp_var_sa_lb_down", sa_lb_down) # h5.put_array("lp_var_sa_lb_down", sa_lb_down)
end end
@ -201,7 +203,7 @@ function _extract_after_lp_constrs(model::JuMP.Model, h5)
duals = Float64[] duals = Float64[]
basis_status = [] basis_status = []
constr_idx = 1 constr_idx = 1
sensitivity_report = lp_sensitivity_report(model) # sensitivity_report = lp_sensitivity_report(model)
for (ftype, stype) in JuMP.list_of_constraint_types(model) for (ftype, stype) in JuMP.list_of_constraint_types(model)
for constr in JuMP.all_constraints(model, ftype, stype) for constr in JuMP.all_constraints(model, ftype, stype)
length(JuMP.name(constr)) > 0 || continue length(JuMP.name(constr)) > 0 || continue
@ -219,21 +221,22 @@ function _extract_after_lp_constrs(model::JuMP.Model, h5)
error("Unknown basis status: $b") error("Unknown basis status: $b")
end end
# Sensitivity analysis # # Sensitivity analysis
(delta_down, delta_up) = sensitivity_report[constr] # (delta_down, delta_up) = sensitivity_report[constr]
push!(sa_rhs_down, rhs[constr_idx] + delta_down) # push!(sa_rhs_down, rhs[constr_idx] + delta_down)
push!(sa_rhs_up, rhs[constr_idx] + delta_up) # push!(sa_rhs_up, rhs[constr_idx] + delta_up)
constr_idx += 1 constr_idx += 1
end end
end end
h5.put_array("lp_constr_dual_values", duals) 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_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_up", sa_rhs_up)
h5.put_array("lp_constr_sa_rhs_down", sa_rhs_down) # h5.put_array("lp_constr_sa_rhs_down", sa_rhs_down)
end end
function _extract_after_mip(model::JuMP.Model, h5) function _extract_after_mip(model::JuMP.Model, h5)
@info "_extract_after_mip"
h5.put_scalar("mip_obj_value", objective_value(model)) h5.put_scalar("mip_obj_value", objective_value(model))
h5.put_scalar("mip_obj_bound", objective_bound(model)) h5.put_scalar("mip_obj_bound", objective_bound(model))
h5.put_scalar("mip_wallclock_time", solve_time(model)) h5.put_scalar("mip_wallclock_time", solve_time(model))
@ -254,11 +257,14 @@ end
function _fix_variables(model::JuMP.Model, var_names, var_values, stats) function _fix_variables(model::JuMP.Model, var_names, var_values, stats)
vars = [variable_by_name(model, v) for v in var_names] vars = [variable_by_name(model, v) for v in var_names]
for (i, var) in enumerate(vars) for (i, var) in enumerate(vars)
if isfinite(var_values[i])
fix(var, var_values[i], force=true) fix(var, var_values[i], force=true)
end end
end end
end
function _optimize(model::JuMP.Model) function _optimize(model::JuMP.Model)
@info "_optimize"
optimize!(model) optimize!(model)
flush(stdout) flush(stdout)
Libc.flush_cstdio() Libc.flush_cstdio()
@ -269,7 +275,7 @@ function _relax(model::JuMP.Model)
relax_integrality(relaxed) relax_integrality(relaxed)
# FIXME: Remove hardcoded optimizer # FIXME: Remove hardcoded optimizer
set_optimizer(relaxed, HiGHS.Optimizer) set_optimizer(relaxed, HiGHS.Optimizer)
set_silent(relaxed) # set_silent(relaxed)
return relaxed return relaxed
end end

@ -4,7 +4,9 @@ authors = ["Alinson S. Xavier <git@axavier.org>"]
version = "0.1.0" version = "0.1.0"
[deps] [deps]
CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b"
Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d" Clp = "e2554f3b-3117-50c0-817c-e040a3ddf72d"
DataFrames = "a93c6f00-e57d-5684-b7b6-d8193f3e46c0"
Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" Glob = "c27321d9-0574-5035-807b-f59d2c89b15c"
HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f" HDF5 = "f67ccb44-e63f-5c2f-98bd-6dc0ccc4ba2f"
HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b" HiGHS = "87dc4568-4c63-4d18-b0c0-bb2238e4078b"

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

File diff suppressed because one or more lines are too long

@ -10,6 +10,9 @@ using Test
using MIPLearn.BB using MIPLearn.BB
using MIPLearn using MIPLearn
using CSV
using DataFrames
basepath = @__DIR__ basepath = @__DIR__
function bb_run(optimizer_name, optimizer; large=true) function bb_run(optimizer_name, optimizer; large=true)
@ -132,3 +135,67 @@ function test_bb()
@time bb_run("HiGHS", optimizer_with_attributes(HiGHS.Optimizer)) @time bb_run("HiGHS", optimizer_with_attributes(HiGHS.Optimizer))
# @time bb_run("CPLEX", optimizer_with_attributes(CPLEX.Optimizer, "CPXPARAM_Threads" => 1)) # @time bb_run("CPLEX", optimizer_with_attributes(CPLEX.Optimizer, "CPXPARAM_Threads" => 1))
end end
function test_bb_replay()
rule_sb = BB.StrongBranching()
rule_rb = BB.ReliabilityBranching()
optimizer = optimizer_with_attributes(HiGHS.Optimizer)
filenames = [replace(f, ".h5" => "") for f in glob("test/fixtures/stab/*.h5")]
results_filename = "tmp.csv"
lk = ReentrantLock()
results = []
function push_result(r)
lock(lk) do
push!(results, r)
df = DataFrame()
for row in results
push!(df, row, cols=:union)
end
CSV.write(results_filename, df)
end
end
function solve(filename; replay=nothing, skip=false, rule)
has_replay = (replay !== nothing)
h5 = H5File("$filename.h5", "r")
mip_obj_bound = h5.get_scalar("mip_obj_bound")
@show filename
@show has_replay
h5.file.close()
mip = BB.init(optimizer)
BB.read!(mip, "$filename.mps.gz")
time_solve = @elapsed begin
pool, replay = BB.solve!(
mip,
initial_primal_bound=mip_obj_bound,
print_interval=100,
node_limit=1_000,
branch_rule=rule,
replay=replay,
)
end
if !skip
push_result(
Dict(
"Filename" => filename,
"Replay?" => has_replay,
"Solve time (s)" => time_solve,
"Relative MIP gap (%)" => round(pool.gap * 100, digits=3)
)
)
end
return replay
end
# Solve reference instance
replay = solve(filenames[1], skip=true, rule=rule_sb)
# Solve perturbations
for i in 2:6
solve(filenames[i], rule=rule_rb, replay=nothing)
solve(filenames[i], rule=rule_rb, replay=deepcopy(replay))
end
return
end
Loading…
Cancel
Save