Skip to content

Commit 3e8c77f

Browse files
authored
Merge pull request #270 from scipopt/heuristic-moi
heuristic callback in MOI
2 parents 583a582 + 93f4fcd commit 3e8c77f

File tree

5 files changed

+148
-49
lines changed

5 files changed

+148
-49
lines changed

src/MOI_wrapper.jl

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
3636
params::Dict{String,Any}
3737
start::Dict{VI,Float64} # can be partial
3838
moi_separator::Any # ::Union{CutCbSeparator, Nothing}
39+
moi_heuristic::Any # ::Union{HeuristicCb, Nothing}
3940
objective_sense::Union{Nothing,MOI.OptimizationSense}
4041
objective_function_set::Bool
4142

@@ -70,6 +71,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
7071
Dict(),
7172
nothing,
7273
nothing,
74+
nothing,
7375
false,
7476
)
7577
finalizer(free_scip, o)
@@ -246,6 +248,8 @@ function MOI.empty!(o::Optimizer)
246248
end
247249
o.objective_sense = nothing
248250
o.objective_function_set = false
251+
o.moi_separator = nothing
252+
o.moi_heuristic = nothing
249253
return nothing
250254
end
251255

src/MOI_wrapper/heuristic.jl

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,3 +27,85 @@ function include_heuristic(
2727
usessubscip=usessubscip,
2828
)
2929
end
30+
31+
mutable struct HeuristicCb <: Heuristic
32+
scipd::SCIPData
33+
heurcallback::Function
34+
end
35+
36+
# If no cut callback is given, the cut callback does nothing.
37+
HeuristicCb(scipd::SCIPData) = HeuristicCb(scipd, cb_data -> nothing)
38+
39+
"""
40+
Used for an argument to the heuristic callback, which in turn uses that argument to
41+
obtain the LP-solution via `MOI.get` and to add solutions via `MOI.submit`.
42+
"""
43+
mutable struct HeuristicCbData
44+
heur::HeuristicCb
45+
submit_called::Bool
46+
heurtiming::SCIP_HEURTIMING
47+
nodeinfeasible::Bool
48+
heur_ptr::Ptr{SCIP_HEUR}
49+
end
50+
51+
function find_primal_solution(scip, heur::HeuristicCb, heurtiming, nodeinfeasible, heur_ptr)
52+
cb_data = HeuristicCbData(heur, false, heurtiming, nodeinfeasible, heur_ptr)
53+
heur.heurcallback(cb_data)
54+
result = cb_data.submit_called ? SCIP_FOUNDSOL : SCIP_DIDNOTFIND
55+
return SCIP_OKAY, result
56+
end
57+
58+
#
59+
# MOI Interface for heuristic callbacks
60+
#
61+
62+
function MOI.get(o::Optimizer, ::MOI.CallbackVariablePrimal{HeuristicCbData}, vs::Vector{VI})
63+
return [SCIPgetSolVal(o, C_NULL, var(o, vi)) for vi in vs]
64+
end
65+
66+
function MOI.set(o::Optimizer, ::MOI.HeuristicCallback, cb::Function)
67+
if o.moi_heuristic === nothing
68+
o.moi_heuristic = HeuristicCb(o.inner, cb)
69+
include_heuristic(o, o.moi_heuristic, name="MOI_heuristic", description="Heuristic set in the MOI optimizer")
70+
else
71+
o.moi_heuristic.cutcallback = cb
72+
end
73+
return nothing
74+
end
75+
MOI.supports(::Optimizer, ::MOI.HeuristicCallback) = true
76+
77+
function MOI.submit(
78+
o::Optimizer,
79+
cb::MOI.HeuristicSolution{HeuristicCbData},
80+
x::Vector{MOI.VariableIndex},
81+
values::Vector{<:Real},
82+
)
83+
callback_data = cb.callback_data
84+
heuristic = callback_data.heur
85+
heur_ = o.inner.heuristic_storage[heuristic]
86+
87+
sol = create_empty_scipsol(o.inner.scip[], heur_)
88+
for idx in eachindex(x)
89+
SCIP.@SCIP_CALL SCIP.SCIPsetSolVal(
90+
o.inner.scip[],
91+
sol,
92+
var(o, x[idx]),
93+
values[idx],
94+
)
95+
end
96+
stored = Ref{SCIP_Bool}(SCIP.FALSE)
97+
@SCIP_CALL SCIPtrySolFree(
98+
o.inner.scip[],
99+
Ref(sol),
100+
SCIP.FALSE,
101+
SCIP.FALSE,
102+
SCIP.TRUE,
103+
SCIP.TRUE,
104+
SCIP.TRUE,
105+
stored,
106+
)
107+
if stored[] == SCIP.TRUE
108+
callback_data.submit_called = true
109+
end
110+
end
111+
MOI.supports(::Optimizer, ::MOI.HeuristicSolution{HeuristicCbData}) = true

src/heuristic.jl

Lines changed: 8 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -15,14 +15,13 @@ abstract type Heuristic end
1515
heurtiming::Heurtiming,
1616
nodeinfeasible::Bool,
1717
heur_ptr::Ptr{SCIP_HEUR},
18-
) -> (retcode, result, solutions)
18+
) -> (retcode, result)
1919
2020
It must attempt to find primal solution(s).
2121
`retcode` indicates whether the selection went well.
2222
A typical result would be `SCIP_SUCCESS`, and retcode `SCIP_OKAY`.
23-
`solutions` is a vector of added SCIP_SOL pointers.
24-
Use the methods `create_scipsol` and `SCIPsetSolVal` to build solutions.
25-
Do not add them to SCIP directly (i.e. do not call `SCIPtrySolFree`).
23+
Use the methods `create_empty_scipsol` and `SCIPsetSolVal` to build solutions.
24+
Submit it to SCIP with `SCIPtrySolFree`
2625
"""
2726
function find_primal_solution(scip, heur, heurtiming, nodeinfeasible, heur_ptr) end
2827

@@ -36,44 +35,16 @@ function _find_primal_solution_callback(
3635
heurdata::Ptr{SCIP_HEURDATA} = SCIPheurGetData(heur_)
3736
heur = unsafe_pointer_to_objref(heurdata)
3837
nodeinfeasible = nodeinfeasible_ == SCIP.TRUE
39-
(retcode, result, solutions) = find_primal_solution(
38+
(retcode, result) = find_primal_solution(
4039
scip,
4140
heur,
4241
heurtiming,
4342
nodeinfeasible,
4443
heur_,
45-
)::Tuple{SCIP_RETCODE,SCIP_RESULT,Vector{Ptr{SCIP_SOL}}}
44+
)::Tuple{SCIP_RETCODE,SCIP_RESULT}
4645
if retcode != SCIP_OKAY
4746
return retcode
4847
end
49-
if result == SCIP_FOUNDSOL
50-
@assert length(solutions) > 0
51-
end
52-
found_solution = false
53-
for sol in solutions
54-
stored = Ref{SCIP_Bool}(SCIP.FALSE)
55-
@SCIP_CALL SCIPtrySolFree(
56-
scip,
57-
Ref(sol),
58-
SCIP.FALSE,
59-
SCIP.FALSE,
60-
SCIP.TRUE,
61-
SCIP.TRUE,
62-
SCIP.TRUE,
63-
stored,
64-
)
65-
if stored[] != SCIP.TRUE
66-
@warn "Primal solution not feasible"
67-
else
68-
found_solution = true
69-
end
70-
end
71-
result = if found_solution
72-
SCIP_FOUNDSOL
73-
else
74-
SCIP_DIDNOTFIND
75-
end
76-
7748
unsafe_store!(result_, result)
7849
return retcode
7950
end
@@ -159,10 +130,10 @@ function include_heuristic(
159130
end
160131

161132
"""
162-
create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR}) -> Ptr{SCIP_SOL}
163-
Convenience wrapper to create a
133+
create_empty_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR}) -> Ptr{SCIP_SOL}
134+
Convenience wrapper to create an empty solution
164135
"""
165-
function create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR})
136+
function create_empty_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR})
166137
sol__ = Ref{Ptr{SCIP_SOL}}(C_NULL)
167138
@SCIP_CALL SCIPcreateSol(scip, sol__, heur_)
168139
return sol__[]

test/cutcallback.jl

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ end
117117
MOI.set(optimizer, MOI.UserCutCallback(), cutcallback)
118118

119119
# solve the problem
120-
SCIP.@SCIP_CALL SCIP.SCIPsolve(inner.scip[])
120+
MOI.optimize!(optimizer)
121121

122122
# The cut callback was called.
123123
@test calls >= 1
@@ -131,9 +131,6 @@ end
131131
rtol
132132
@test MOI.get(optimizer, MOI.VariablePrimal(), y) 1.0 atol = atol rtol =
133133
rtol
134-
135-
# free the problem
136-
finalize(inner)
137134
end
138135

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

186183
# solve the problem
187-
SCIP.@SCIP_CALL SCIP.SCIPsolve(inner.scip[])
184+
MOI.optimize!(optimizer)
188185

189186
# The cut callback was called.
190187
@test calls >= 1
@@ -198,7 +195,4 @@ end
198195
rtol
199196
@test MOI.get(optimizer, MOI.VariablePrimal(), y) 0.0 atol = atol rtol =
200197
rtol
201-
202-
# free the problem
203-
finalize(inner)
204198
end

test/heuristic.jl

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ end
99
function SCIP.find_primal_solution(scip, ::ZeroHeuristic, heurtiming, nodeinfeasible::Bool, heur_ptr)
1010
@assert SCIP.SCIPhasCurrentNodeLP(scip) == SCIP.TRUE
1111
result = SCIP.SCIP_DIDNOTRUN
12-
sol = SCIP.create_scipsol(scip, heur_ptr)
12+
sol = SCIP.create_empty_scipsol(scip, heur_ptr)
1313
vars = SCIP.SCIPgetVars(scip)
1414
nvars = SCIP.SCIPgetNVars(scip)
1515
var_vec = unsafe_wrap(Array, vars, nvars)
@@ -21,12 +21,27 @@ function SCIP.find_primal_solution(scip, ::ZeroHeuristic, heurtiming, nodeinfeas
2121
0.0,
2222
)
2323
end
24-
result = SCIP.SCIP_SUCCESS
25-
return (SCIP.SCIP_OKAY, result, [sol])
24+
stored = Ref{SCIP.SCIP_Bool}(SCIP.FALSE)
25+
SCIP.@SCIP_CALL SCIP.SCIPtrySolFree(
26+
scip,
27+
Ref(sol),
28+
SCIP.FALSE,
29+
SCIP.FALSE,
30+
SCIP.TRUE,
31+
SCIP.TRUE,
32+
SCIP.TRUE,
33+
stored,
34+
)
35+
result = if stored[] != SCIP.TRUE
36+
SCIP.SCIP_DIDNOTFIND
37+
else
38+
SCIP.SCIP_FOUNDSOL
39+
end
40+
return (SCIP.SCIP_OKAY, result)
2641
end
2742

2843
@testset "Basic heuristic properties" begin
29-
o = SCIP.Optimizer(; presolving_maxrounds=0)
44+
o = SCIP.Optimizer(; presolving_maxrounds=0, display_verblevel=0)
3045
name = "zero_heuristic"
3146
description = "description"
3247
priority = 1
@@ -59,5 +74,38 @@ end
5974

6075
MOI.optimize!(o)
6176
@test MOI.get(o, MOI.TerminationStatus()) == MOI.OPTIMAL
77+
end
6278

79+
@testset "Heuristic through MOI" begin
80+
o = SCIP.Optimizer(; presolving_maxrounds=0, display_verblevel=0)
81+
n = 10
82+
x = MOI.add_variables(o, n)
83+
MOI.add_constraint.(o, x, MOI.ZeroOne())
84+
MOI.add_constraint(
85+
o,
86+
sum(x; init=0.0),
87+
MOI.LessThan(3.0),
88+
)
89+
MOI.set(o, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), dot(rand(n), x))
90+
MOI.set(o, MOI.ObjectiveSense(), MOI.MAX_SENSE)
91+
ncalls = Ref(0)
92+
function heuristic_callback(callback_data::SCIP.HeuristicCbData)
93+
# LP solution
94+
x_frac = MOI.get(o, MOI.CallbackVariablePrimal(callback_data), x)
95+
# setting heuristic solution with three values at 1
96+
values = 0 * x_frac
97+
values[findmax(x_frac)[2]] = 1.0
98+
values[1] = 1.0
99+
values[2] = 1.0
100+
MOI.submit(
101+
o,
102+
MOI.HeuristicSolution(callback_data),
103+
x,
104+
values,
105+
)
106+
global ncalls[] +=1
107+
end
108+
MOI.set(o, MOI.HeuristicCallback(), heuristic_callback)
109+
MOI.optimize!(o)
110+
@test ncalls[] > 0
63111
end

0 commit comments

Comments
 (0)