Skip to content

Commit 583a582

Browse files
authored
Merge pull request #269 from scipopt/heuristic
Heuristic plugin
2 parents 0014004 + 057e552 commit 583a582

File tree

9 files changed

+275
-10
lines changed

9 files changed

+275
-10
lines changed

.codecov.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,3 +2,4 @@ comment: false
22
ignore:
33
- "**/compat.jl"
44
- "**/wrapper/*.jl"
5+
- "**LibSCIP.jl"

src/MOI_wrapper.jl

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
5757
Dict(),
5858
Dict(),
5959
Dict(),
60+
Dict(),
6061
[],
6162
)
6263

@@ -238,7 +239,7 @@ function MOI.empty!(o::Optimizer)
238239
@SCIP_CALL SCIP.SCIPcreateProbBasic(scip[], "")
239240
# create a new problem
240241
o.inner =
241-
SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), Dict(), Dict(), [])
242+
SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), Dict(), Dict(), Dict(), [])
242243
# reapply parameters
243244
for pair in o.params
244245
set_parameter(o.inner, pair.first, pair.second)
@@ -371,3 +372,4 @@ include(joinpath("MOI_wrapper", "objective.jl"))
371372
include(joinpath("MOI_wrapper", "results.jl"))
372373
include(joinpath("MOI_wrapper", "conshdlr.jl"))
373374
include(joinpath("MOI_wrapper", "sepa.jl"))
375+
include(joinpath("MOI_wrapper", "heuristic.jl"))

src/MOI_wrapper/heuristic.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
2+
function include_heuristic(
3+
o::Optimizer,
4+
heuristic::HT;
5+
name="",
6+
description="",
7+
dispchar='_',
8+
priority=10000,
9+
frequency=1,
10+
frequency_offset=0,
11+
maximum_depth=-1,
12+
timing_mask=SCIP_HEURTIMING_BEFORENODE,
13+
usessubscip=false,
14+
) where {HT}
15+
return include_heuristic(
16+
o.inner.scip[],
17+
heuristic,
18+
o.inner.heuristic_storage;
19+
name=name,
20+
description=description,
21+
dispchar=dispchar,
22+
priority=priority,
23+
frequency=frequency,
24+
frequency_offset=frequency_offset,
25+
maximum_depth=maximum_depth,
26+
timing_mask=timing_mask,
27+
usessubscip=usessubscip,
28+
)
29+
end

src/SCIP.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ module SCIP
33
# assorted utility functions
44
include("util.jl")
55

6+
67
# load deps, version check
78
include("init.jl")
89

@@ -12,13 +13,12 @@ include("wrapper.jl")
1213
# memory management
1314
include("scip_data.jl")
1415

15-
# separators
1616
include("sepa.jl")
1717

18-
# cut selectors
1918
include("cut_selector.jl")
2019

21-
# branching rule
20+
include("heuristic.jl")
21+
2222
include("branching_rule.jl")
2323

2424
# constraint handlers

src/heuristic.jl

Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
# heuristic interface
2+
# it is recommended to check https://scipopt.org/doc/html/HEUR.php for key concepts and interface
3+
4+
"""
5+
Abstract class for Heuristic.
6+
A heuristic must implement `find_primal_solution`.
7+
It also stores all user data that must be available to run the heuristic.
8+
"""
9+
abstract type Heuristic end
10+
11+
"""
12+
find_primal_solution(
13+
scip::Ptr{SCIP_},
14+
heur::Heuristic,
15+
heurtiming::Heurtiming,
16+
nodeinfeasible::Bool,
17+
heur_ptr::Ptr{SCIP_HEUR},
18+
) -> (retcode, result, solutions)
19+
20+
It must attempt to find primal solution(s).
21+
`retcode` indicates whether the selection went well.
22+
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`).
26+
"""
27+
function find_primal_solution(scip, heur, heurtiming, nodeinfeasible, heur_ptr) end
28+
29+
function _find_primal_solution_callback(
30+
scip::Ptr{SCIP_},
31+
heur_::Ptr{SCIP_HEUR},
32+
heurtiming::SCIP_HEURTIMING,
33+
nodeinfeasible_::SCIP_Bool,
34+
result_::Ptr{SCIP_RESULT},
35+
)
36+
heurdata::Ptr{SCIP_HEURDATA} = SCIPheurGetData(heur_)
37+
heur = unsafe_pointer_to_objref(heurdata)
38+
nodeinfeasible = nodeinfeasible_ == SCIP.TRUE
39+
(retcode, result, solutions) = find_primal_solution(
40+
scip,
41+
heur,
42+
heurtiming,
43+
nodeinfeasible,
44+
heur_,
45+
)::Tuple{SCIP_RETCODE,SCIP_RESULT,Vector{Ptr{SCIP_SOL}}}
46+
if retcode != SCIP_OKAY
47+
return retcode
48+
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+
77+
unsafe_store!(result_, result)
78+
return retcode
79+
end
80+
81+
function _heurfree(::Ptr{SCIP_}, heur::Ptr{SCIP_HEUR})
82+
# just like sepa, free the data on the SCIP side,
83+
# the Julia GC will take care of the objects
84+
SCIPheurSetData(heur, C_NULL)
85+
return SCIP_OKAY
86+
end
87+
88+
"""
89+
Includes a heuristic plugin in SCIP and stores it in heuristic_storage.
90+
"""
91+
function include_heuristic(
92+
scip::Ptr{SCIP_},
93+
heuristic::HT,
94+
heuristic_storage::Dict{Any,Ptr{SCIP_HEUR}};
95+
name="",
96+
description="",
97+
dispchar='_',
98+
priority=10000,
99+
frequency=1,
100+
frequency_offset=0,
101+
maximum_depth=-1,
102+
timing_mask=SCIP_HEURTIMING_BEFORENODE,
103+
usessubscip=false,
104+
) where {HT<:Heuristic}
105+
106+
# ensure a unique name for the cut selector
107+
if name == ""
108+
name = "heuristic_$(string(HT))"
109+
end
110+
if dispchar == '_'
111+
dispchar = name[end]
112+
end
113+
114+
heur__ = Ref{Ptr{SCIP_HEUR}}(C_NULL)
115+
if !ismutable(heuristic)
116+
throw(
117+
ArgumentError("The heuristic structure must be a mutable type"),
118+
)
119+
end
120+
121+
heurdata_ = pointer_from_objref(heuristic)
122+
heur_callback = @cfunction(
123+
_find_primal_solution_callback,
124+
SCIP_RETCODE,
125+
(
126+
Ptr{SCIP_},
127+
Ptr{SCIP_HEUR},
128+
SCIP_HEURTIMING,
129+
SCIP_Bool,
130+
Ptr{SCIP_RESULT},
131+
),
132+
)
133+
@SCIP_CALL SCIPincludeHeurBasic(
134+
scip,
135+
heur__,
136+
name,
137+
description,
138+
dispchar,
139+
priority,
140+
frequency,
141+
frequency_offset,
142+
maximum_depth,
143+
timing_mask,
144+
usessubscip,
145+
heur_callback,
146+
heurdata_,
147+
)
148+
149+
@assert heur__[] != C_NULL
150+
151+
@SCIP_CALL SCIPsetHeurFree(
152+
scip,
153+
heur__[],
154+
@cfunction(_heurfree, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_HEUR})),
155+
)
156+
157+
# store heuristic in storage (avoids GC-ing it)
158+
heuristic_storage[heuristic] = heur__[]
159+
end
160+
161+
"""
162+
create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR}) -> Ptr{SCIP_SOL}
163+
Convenience wrapper to create a
164+
"""
165+
function create_scipsol(scip::Ptr{SCIP_}, heur_::Ptr{SCIP_HEUR})
166+
sol__ = Ref{Ptr{SCIP_SOL}}(C_NULL)
167+
@SCIP_CALL SCIPcreateSol(scip, sol__, heur_)
168+
return sol__[]
169+
end

src/scip_data.jl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ mutable struct SCIPData
4646
# User-defined cut selectors and branching rules
4747
cutsel_storage::Dict{Any,Ptr{SCIP_CUTSEL}}
4848
branchrule_storage::Dict{Any,Ptr{SCIP_BRANCHRULE}}
49-
49+
heuristic_storage::Dict{Any,Ptr{SCIP_HEUR}}
50+
5051
# to store expressions for release
5152
nonlinear_storage::Vector{NonlinExpr}
5253
end

test/cutcallback.jl

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -100,10 +100,7 @@ end
100100
MOI.set(
101101
optimizer,
102102
MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(),
103-
MOI.ScalarAffineFunction(
104-
MOI.ScalarAffineTerm.([1.0, 1.0], [x, y]),
105-
0.0,
106-
),
103+
1.0 * x + 1.0 * y,
107104
)
108105
MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE)
109106

@@ -112,7 +109,7 @@ end
112109
MOI.submit(
113110
optimizer,
114111
MOI.UserCut{SCIP.CutCbData}(cb_data),
115-
MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([1.0], [x]), 0.0),
112+
1.0 * x,
116113
MOI.LessThan(0.0),
117114
)
118115
calls += 1

test/heuristic.jl

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import MathOptInterface as MOI
2+
using SCIP
3+
using LinearAlgebra
4+
using Test
5+
6+
mutable struct ZeroHeuristic <: SCIP.Heuristic
7+
end
8+
9+
function SCIP.find_primal_solution(scip, ::ZeroHeuristic, heurtiming, nodeinfeasible::Bool, heur_ptr)
10+
@assert SCIP.SCIPhasCurrentNodeLP(scip) == SCIP.TRUE
11+
result = SCIP.SCIP_DIDNOTRUN
12+
sol = SCIP.create_scipsol(scip, heur_ptr)
13+
vars = SCIP.SCIPgetVars(scip)
14+
nvars = SCIP.SCIPgetNVars(scip)
15+
var_vec = unsafe_wrap(Array, vars, nvars)
16+
for var in var_vec
17+
SCIP.@SCIP_CALL SCIP.SCIPsetSolVal(
18+
scip,
19+
sol,
20+
var,
21+
0.0,
22+
)
23+
end
24+
result = SCIP.SCIP_SUCCESS
25+
return (SCIP.SCIP_OKAY, result, [sol])
26+
end
27+
28+
@testset "Basic heuristic properties" begin
29+
o = SCIP.Optimizer(; presolving_maxrounds=0)
30+
name = "zero_heuristic"
31+
description = "description"
32+
priority = 1
33+
heur = ZeroHeuristic()
34+
SCIP.include_heuristic(o, heur, name=name, description=description, priority=priority)
35+
36+
heur_pointer = o.inner.heuristic_storage[heur]
37+
@test unsafe_string(SCIP.LibSCIP.SCIPheurGetName(heur_pointer)) == name
38+
@test unsafe_string(SCIP.LibSCIP.SCIPheurGetDesc(heur_pointer)) ==
39+
description
40+
@test SCIP.LibSCIP.SCIPheurGetPriority(heur_pointer) == priority
41+
@test SCIP.LibSCIP.SCIPheurGetData(heur_pointer) ==
42+
pointer_from_objref(heur)
43+
44+
x = MOI.add_variables(o, 10)
45+
MOI.add_constraint.(o, x, MOI.Integer())
46+
MOI.add_constraint.(o, x, MOI.GreaterThan(-0.1))
47+
MOI.add_constraint.(o, x, MOI.LessThan(2.3))
48+
MOI.add_constraint(o, sum(x; init=0.0), MOI.LessThan(12.5))
49+
for _ in 1:5
50+
MOI.add_constraint(
51+
o,
52+
2.0 * dot(rand(10), x),
53+
MOI.LessThan(10.0 + 2 * rand()),
54+
)
55+
end
56+
func = -dot(rand(10), x)
57+
MOI.set(o, MOI.ObjectiveFunction{typeof(func)}(), func)
58+
MOI.set(o, MOI.ObjectiveSense(), MOI.MIN_SENSE)
59+
60+
MOI.optimize!(o)
61+
@test MOI.get(o, MOI.TerminationStatus()) == MOI.OPTIMAL
62+
63+
end

test/runtests.jl

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,9 @@ end
4444
@testset "branching rule" begin
4545
include("branchrule.jl")
4646
end
47+
@testset "heuristic" begin
48+
include("heuristic.jl")
49+
end
4750

4851
const MOI_BASE_EXCLUDED = [
4952
"Indicator_LessThan", # indicator must be binary error in SCIP

0 commit comments

Comments
 (0)