Skip to content

Commit

Permalink
heuristic MOI
Browse files Browse the repository at this point in the history
  • Loading branch information
matbesancon committed Jul 1, 2023
1 parent 057e552 commit 93f4fcd
Show file tree
Hide file tree
Showing 5 changed files with 148 additions and 49 deletions.
4 changes: 4 additions & 0 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
params::Dict{String,Any}
start::Dict{VI,Float64} # can be partial
moi_separator::Any # ::Union{CutCbSeparator, Nothing}
moi_heuristic::Any # ::Union{HeuristicCb, Nothing}
objective_sense::Union{Nothing,MOI.OptimizationSense}
objective_function_set::Bool

Expand Down Expand Up @@ -70,6 +71,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
Dict(),
nothing,
nothing,
nothing,
false,
)
finalizer(free_scip, o)
Expand Down Expand Up @@ -246,6 +248,8 @@ function MOI.empty!(o::Optimizer)
end
o.objective_sense = nothing
o.objective_function_set = false
o.moi_separator = nothing
o.moi_heuristic = nothing
return nothing
end

Expand Down
82 changes: 82 additions & 0 deletions src/MOI_wrapper/heuristic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,85 @@ function include_heuristic(
usessubscip=usessubscip,
)
end

mutable struct HeuristicCb <: Heuristic
scipd::SCIPData
heurcallback::Function
end

# If no cut callback is given, the cut callback does nothing.
HeuristicCb(scipd::SCIPData) = HeuristicCb(scipd, cb_data -> nothing)

Check warning on line 37 in src/MOI_wrapper/heuristic.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_wrapper/heuristic.jl#L37

Added line #L37 was not covered by tests

"""
Used for an argument to the heuristic callback, which in turn uses that argument to
obtain the LP-solution via `MOI.get` and to add solutions via `MOI.submit`.
"""
mutable struct HeuristicCbData
heur::HeuristicCb
submit_called::Bool
heurtiming::SCIP_HEURTIMING
nodeinfeasible::Bool
heur_ptr::Ptr{SCIP_HEUR}
end

function find_primal_solution(scip, heur::HeuristicCb, heurtiming, nodeinfeasible, heur_ptr)
cb_data = HeuristicCbData(heur, false, heurtiming, nodeinfeasible, heur_ptr)
heur.heurcallback(cb_data)
result = cb_data.submit_called ? SCIP_FOUNDSOL : SCIP_DIDNOTFIND
return SCIP_OKAY, result
end

#
# MOI Interface for heuristic callbacks
#

function MOI.get(o::Optimizer, ::MOI.CallbackVariablePrimal{HeuristicCbData}, vs::Vector{VI})
return [SCIPgetSolVal(o, C_NULL, var(o, vi)) for vi in vs]
end

function MOI.set(o::Optimizer, ::MOI.HeuristicCallback, cb::Function)
if o.moi_heuristic === nothing
o.moi_heuristic = HeuristicCb(o.inner, cb)
include_heuristic(o, o.moi_heuristic, name="MOI_heuristic", description="Heuristic set in the MOI optimizer")
else
o.moi_heuristic.cutcallback = cb

Check warning on line 71 in src/MOI_wrapper/heuristic.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_wrapper/heuristic.jl#L71

Added line #L71 was not covered by tests
end
return nothing
end
MOI.supports(::Optimizer, ::MOI.HeuristicCallback) = true

Check warning on line 75 in src/MOI_wrapper/heuristic.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_wrapper/heuristic.jl#L75

Added line #L75 was not covered by tests

function MOI.submit(
o::Optimizer,
cb::MOI.HeuristicSolution{HeuristicCbData},
x::Vector{MOI.VariableIndex},
values::Vector{<:Real},
)
callback_data = cb.callback_data
heuristic = callback_data.heur
heur_ = o.inner.heuristic_storage[heuristic]

sol = create_empty_scipsol(o.inner.scip[], heur_)
for idx in eachindex(x)
SCIP.@SCIP_CALL SCIP.SCIPsetSolVal(
o.inner.scip[],
sol,
var(o, x[idx]),
values[idx],
)
end
stored = Ref{SCIP_Bool}(SCIP.FALSE)
@SCIP_CALL SCIPtrySolFree(
o.inner.scip[],
Ref(sol),
SCIP.FALSE,
SCIP.FALSE,
SCIP.TRUE,
SCIP.TRUE,
SCIP.TRUE,
stored,
)
if stored[] == SCIP.TRUE
callback_data.submit_called = true
end
end
MOI.supports(::Optimizer, ::MOI.HeuristicSolution{HeuristicCbData}) = true

Check warning on line 111 in src/MOI_wrapper/heuristic.jl

View check run for this annotation

Codecov / codecov/patch

src/MOI_wrapper/heuristic.jl#L111

Added line #L111 was not covered by tests
45 changes: 8 additions & 37 deletions src/heuristic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,13 @@ abstract type Heuristic end
heurtiming::Heurtiming,
nodeinfeasible::Bool,
heur_ptr::Ptr{SCIP_HEUR},
) -> (retcode, result, solutions)
) -> (retcode, result)
It must attempt to find primal solution(s).
`retcode` indicates whether the selection went well.
A typical result would be `SCIP_SUCCESS`, and retcode `SCIP_OKAY`.
`solutions` is a vector of added SCIP_SOL pointers.
Use the methods `create_scipsol` and `SCIPsetSolVal` to build solutions.
Do not add them to SCIP directly (i.e. do not call `SCIPtrySolFree`).
Use the methods `create_empty_scipsol` and `SCIPsetSolVal` to build solutions.
Submit it to SCIP with `SCIPtrySolFree`
"""
function find_primal_solution(scip, heur, heurtiming, nodeinfeasible, heur_ptr) end

Expand All @@ -36,44 +35,16 @@ function _find_primal_solution_callback(
heurdata::Ptr{SCIP_HEURDATA} = SCIPheurGetData(heur_)
heur = unsafe_pointer_to_objref(heurdata)
nodeinfeasible = nodeinfeasible_ == SCIP.TRUE
(retcode, result, solutions) = find_primal_solution(
(retcode, result) = find_primal_solution(
scip,
heur,
heurtiming,
nodeinfeasible,
heur_,
)::Tuple{SCIP_RETCODE,SCIP_RESULT,Vector{Ptr{SCIP_SOL}}}
)::Tuple{SCIP_RETCODE,SCIP_RESULT}
if retcode != SCIP_OKAY
return retcode
end
if result == SCIP_FOUNDSOL
@assert length(solutions) > 0
end
found_solution = false
for sol in solutions
stored = Ref{SCIP_Bool}(SCIP.FALSE)
@SCIP_CALL SCIPtrySolFree(
scip,
Ref(sol),
SCIP.FALSE,
SCIP.FALSE,
SCIP.TRUE,
SCIP.TRUE,
SCIP.TRUE,
stored,
)
if stored[] != SCIP.TRUE
@warn "Primal solution not feasible"
else
found_solution = true
end
end
result = if found_solution
SCIP_FOUNDSOL
else
SCIP_DIDNOTFIND
end

unsafe_store!(result_, result)
return retcode
end
Expand Down Expand Up @@ -159,10 +130,10 @@ function include_heuristic(
end

"""
create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR}) -> Ptr{SCIP_SOL}
Convenience wrapper to create a
create_empty_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR}) -> Ptr{SCIP_SOL}
Convenience wrapper to create an empty solution
"""
function create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR})
function create_empty_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR})
sol__ = Ref{Ptr{SCIP_SOL}}(C_NULL)
@SCIP_CALL SCIPcreateSol(scip, sol__, heur_)
return sol__[]
Expand Down
10 changes: 2 additions & 8 deletions test/cutcallback.jl
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ end
MOI.set(optimizer, MOI.UserCutCallback(), cutcallback)

# solve the problem
SCIP.@SCIP_CALL SCIP.SCIPsolve(inner.scip[])
MOI.optimize!(optimizer)

# The cut callback was called.
@test calls >= 1
Expand All @@ -131,9 +131,6 @@ end
rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), y) 1.0 atol = atol rtol =
rtol

# free the problem
finalize(inner)
end

# Test, whether adding cuts within cut callbacks via `submit` works [2/2].
Expand Down Expand Up @@ -184,7 +181,7 @@ end
MOI.set(optimizer, MOI.UserCutCallback(), cutcallback)

# solve the problem
SCIP.@SCIP_CALL SCIP.SCIPsolve(inner.scip[])
MOI.optimize!(optimizer)

# The cut callback was called.
@test calls >= 1
Expand All @@ -198,7 +195,4 @@ end
rtol
@test MOI.get(optimizer, MOI.VariablePrimal(), y) 0.0 atol = atol rtol =
rtol

# free the problem
finalize(inner)
end
56 changes: 52 additions & 4 deletions test/heuristic.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ end
function SCIP.find_primal_solution(scip, ::ZeroHeuristic, heurtiming, nodeinfeasible::Bool, heur_ptr)
@assert SCIP.SCIPhasCurrentNodeLP(scip) == SCIP.TRUE
result = SCIP.SCIP_DIDNOTRUN
sol = SCIP.create_scipsol(scip, heur_ptr)
sol = SCIP.create_empty_scipsol(scip, heur_ptr)
vars = SCIP.SCIPgetVars(scip)
nvars = SCIP.SCIPgetNVars(scip)
var_vec = unsafe_wrap(Array, vars, nvars)
Expand All @@ -21,12 +21,27 @@ function SCIP.find_primal_solution(scip, ::ZeroHeuristic, heurtiming, nodeinfeas
0.0,
)
end
result = SCIP.SCIP_SUCCESS
return (SCIP.SCIP_OKAY, result, [sol])
stored = Ref{SCIP.SCIP_Bool}(SCIP.FALSE)
SCIP.@SCIP_CALL SCIP.SCIPtrySolFree(
scip,
Ref(sol),
SCIP.FALSE,
SCIP.FALSE,
SCIP.TRUE,
SCIP.TRUE,
SCIP.TRUE,
stored,
)
result = if stored[] != SCIP.TRUE
SCIP.SCIP_DIDNOTFIND
else
SCIP.SCIP_FOUNDSOL
end
return (SCIP.SCIP_OKAY, result)
end

@testset "Basic heuristic properties" begin
o = SCIP.Optimizer(; presolving_maxrounds=0)
o = SCIP.Optimizer(; presolving_maxrounds=0, display_verblevel=0)
name = "zero_heuristic"
description = "description"
priority = 1
Expand Down Expand Up @@ -59,5 +74,38 @@ end

MOI.optimize!(o)
@test MOI.get(o, MOI.TerminationStatus()) == MOI.OPTIMAL
end

@testset "Heuristic through MOI" begin
o = SCIP.Optimizer(; presolving_maxrounds=0, display_verblevel=0)
n = 10
x = MOI.add_variables(o, n)
MOI.add_constraint.(o, x, MOI.ZeroOne())
MOI.add_constraint(
o,
sum(x; init=0.0),
MOI.LessThan(3.0),
)
MOI.set(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), dot(rand(n), x))
MOI.set(o, MOI.ObjectiveSense(), MOI.MAX_SENSE)
ncalls = Ref(0)
function heuristic_callback(callback_data::SCIP.HeuristicCbData)
# LP solution
x_frac = MOI.get(o, MOI.CallbackVariablePrimal(callback_data), x)
# setting heuristic solution with three values at 1
values = 0 * x_frac
values[findmax(x_frac)[2]] = 1.0
values[1] = 1.0
values[2] = 1.0
MOI.submit(
o,
MOI.HeuristicSolution(callback_data),
x,
values,
)
global ncalls[] +=1
end
MOI.set(o, MOI.HeuristicCallback(), heuristic_callback)
MOI.optimize!(o)
@test ncalls[] > 0
end

0 comments on commit 93f4fcd

Please sign in to comment.