Skip to content

Commit

Permalink
Merge pull request #245 from scipopt/cutsel
Browse files Browse the repository at this point in the history
added cut selector plugin
  • Loading branch information
matbesancon authored Oct 4, 2022
2 parents 6032603 + c5667f3 commit 31948bf
Show file tree
Hide file tree
Showing 10 changed files with 272 additions and 18 deletions.
7 changes: 4 additions & 3 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
name = "SCIP"
uuid = "82193955-e24f-5292-bf16-6f2c5261a85f"
version = "0.11.5"
version = "0.11.6"

[deps]
Ipopt_jll = "9cc047cb-c261-5740-88fc-0cf96f7bdcc7"
Expand All @@ -18,8 +18,9 @@ SCIP_jll = "0.2"
julia = "1.6"

[extras]
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"
OpenBLAS32_jll = "656ef2d0-ae68-5445-9ca0-591084a874a2"
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

[targets]
test = ["Test", "OpenBLAS32_jll"]
test = ["Test", "OpenBLAS32_jll", "Random"]
4 changes: 2 additions & 2 deletions src/MOI_wrapper.jl
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ mutable struct Optimizer <: MOI.AbstractOptimizer
@SCIP_CALL SCIPincludeDefaultPlugins(scip[])
@SCIP_CALL SCIP.SCIPcreateProbBasic(scip[], "")

scip_data = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), [])
scip_data = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), Dict(), [])

o = new(scip_data, PtrMap(), ConsTypeMap(), Dict(), Dict(), Dict(), nothing, MOI.MIN_SENSE)
finalizer(free_scip, o)
Expand Down Expand Up @@ -198,7 +198,7 @@ function MOI.empty!(o::Optimizer)
@SCIP_CALL SCIPincludeDefaultPlugins(scip[])
@SCIP_CALL SCIP.SCIPcreateProbBasic(scip[], "")
# create a new problem
o.inner = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), [])
o.inner = SCIPData(scip, Dict(), Dict(), 0, 0, Dict(), Dict(), Dict(), Dict(), [])
# reapply parameters
for pair in o.params
set_parameter(o.inner, pair.first, pair.second)
Expand Down
6 changes: 5 additions & 1 deletion src/MOI_wrapper/conshdlr.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
#
# Adding constraint handlers and constraints to SCIP.Optimizer.
# Adding constraint handlers, constraints, and cut selectors to SCIP.Optimizer.
#

"""
Expand Down Expand Up @@ -67,3 +67,7 @@ function add_constraint(o::Optimizer, ch::CH, c::C;
_local=_local, modifiable=modifiable, dynamic=dynamic,
removable=removable, stickingatnode=stickingatnode)
end

function include_cutsel(o::Optimizer, cutsel::CS; name = "", description = "", priority=10000) where {CS <: AbstractCutSelector}
return include_cutsel(o.inner.scip[], cutsel, o.inner.cutsel_storage; name=name, description=description, priority=priority)
end
11 changes: 7 additions & 4 deletions src/SCIP.jl
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,17 @@ include("wrapper.jl")
# memory management
include("scip_data.jl")

# constraints from nonlinear expressions
include("nonlinear.jl")
# separators
include("sepa.jl")

# cut selectors
include("cut_selector.jl")

# constraint handlers
include("conshdlr.jl")

# separators
include("sepa.jl")
# constraints from nonlinear expressions
include("nonlinear.jl")

# implementation of MOI
include("MOI_wrapper.jl")
Expand Down
113 changes: 113 additions & 0 deletions src/cut_selector.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
# cut selection interface
# it is recommended to check https://scipopt.org/doc/html/CUTSEL.php for key concepts and interface

"""
Abstract class for cut selector.
A cut selector must implement `select_cuts`.
It also stores all user data that must be available to select cuts.
"""
abstract type AbstractCutSelector end

"""
select_cuts(cutsel, scip, cuts, forced_cuts, root, maxnslectedcuts) -> (retcode, nselectedcuts, result)
It must operate the selection of cuts by placing the selected cuts first in the selected cut vector.
`nselectedcuts` must be the number of selected cuts, retcode indicates whether the selection went well.
A typical result would be `SCIP_SUCCESS`, and retcode `SCIP_OKAY`.
`forced_cuts` is a vector of cuts that will be added to the problem, they should not be tampered with by the function.
"""
function select_cuts(cutsel::AbstractCutSelector, scip, cuts::Vector{Ptr{SCIP_ROW}}, forced_cuts::Vector{Ptr{SCIP_ROW}}, root, maxnslectedcuts)
end

function _select_cut_callback(scip::Ptr{SCIP_}, cutsel_::Ptr{SCIP_CUTSEL}, cuts_::Ptr{Ptr{SCIP_ROW}}, ncuts::Cint, forced_cuts_::Ptr{Ptr{SCIP_ROW}}, nforced_cuts::Cint, root_::SCIP_Bool, maxnslectedcuts::Cint, nselectedcuts_::Ptr{Cint}, result_::Ptr{SCIP_RESULT})
cutseldata::Ptr{SCIP_CUTSELDATA} = SCIPcutselGetData(cutsel_)
cutsel = unsafe_pointer_to_objref(cutseldata)
cuts = unsafe_wrap(Vector{Ptr{SCIP_ROW}}, cuts_, ncuts)
@assert length(cuts) == ncuts
forced_cuts = unsafe_wrap(Vector{Ptr{SCIP_ROW}}, forced_cuts_, nforced_cuts)
@assert length(forced_cuts) == nforced_cuts
root = root_ == SCIP.TRUE
(retcode, nselectedcuts, result) = select_cuts(cutsel, scip, cuts, forced_cuts, root, maxnslectedcuts)::Tuple{SCIP_RETCODE, Integer, SCIP_RESULT}
if retcode != SCIP_OKAY
return retcode
end
if nselectedcuts > maxnslectedcuts
error("$nselectedcuts cuts selected by cut selected, maximum $maxnslectedcuts allowed")
end
unsafe_store!(nselectedcuts_, Cint(nselectedcuts))
unsafe_store!(result_, result)
return retcode
end

function _cutselfree(::Ptr{SCIP_}, cutsel::Ptr{SCIP_CUTSEL})
# just like sepa, free the data on the SCIP side,
# the Julia GC will take care of the objects
SCIPcutselSetData(cutsel, C_NULL)
return SCIP_OKAY
end

"""
Includes a cut selector in SCIP and stores it in cutsel_storage.
"""
function include_cutsel(scip::Ptr{SCIP_}, cutsel::CS, cutsel_storage::Dict{Any, Ptr{SCIP_CUTSEL}}; name = "", description = "", priority=10000) where {CS <: AbstractCutSelector}

# ensure a unique name for the cut selector
if name == ""
name = "cutselector_$(string(CS))"
end

cutsel__ = Ref{Ptr{SCIP_CUTSEL}}(C_NULL)
if !ismutable(cutsel)
throw(ArgumentError("The cut selector structure must be a mutable type"))
end

cutseldata_ = pointer_from_objref(cutsel)
cutselselect_callback = @cfunction(
_select_cut_callback, SCIP_RETCODE,
(Ptr{SCIP_}, Ptr{SCIP_CUTSEL}, Ptr{Ptr{SCIP_ROW}}, Cint, Ptr{Ptr{SCIP_ROW}}, Cint, SCIP_Bool, Cint, Ptr{Cint}, Ptr{SCIP_RESULT}),
)
@SCIP_CALL SCIPincludeCutselBasic(scip, cutsel__, name, description, priority, cutselselect_callback, cutseldata_)
@assert cutsel__[] != C_NULL

@SCIP_CALL SCIPsetCutselFree(
scip, cutsel__[],
@cfunction(_cutselfree, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CUTSEL})),
)

# store cut selector (avoids GC-ing it)
cutsel_storage[cutsel] = cutsel__[]
end

"""
Extracts information from a SCIP_ROW into a form:
`lhs ≤ aᵀ x + b ≤ rhs`
The row is returned as a named tuple
"""
function get_row_information(scip::SCIPData, row::Ptr{SCIP_ROW})
rhs = SCIProwGetRhs(row)
lhs = SCIProwGetLhs(row)
b = SCIProwGetConstant(row)
n = SCIProwGetNNonz(row)
a_ptr = SCIProwGetVals(row)
a = unsafe_wrap(Vector{Cdouble}, a_ptr, n)
col_ptr = SCIProwGetCols(row)
cols = unsafe_wrap(Vector{Ptr{SCIP_COL}}, col_ptr, n)
x = map(cols) do col
var_ptr = SCIPcolGetVar(col)
var_ref = findfirst(==(var_ptr), scip.vars)
@assert var_ref !== nothing
var_ref
end
return (;rhs, lhs, b, a, x)
end

"""
Utility function to get scores from cut that can be used to evaluate it.
Returns a named tuple with three commonly used scores `(; integer_support, efficacy, objective_parallelism)`.
"""
function get_row_scores(scip::Ptr{SCIP_}, row::Ptr{SCIP_ROW})
integer_support = SCIPgetRowNumIntCols(scip, row) / SCIProwGetNNonz(row)
efficacy = SCIPgetCutEfficacy(scip, C_NULL, row)
objective_parallelism = SCIPgetRowObjParallelism(scip, cuts[i])
return (; integer_support, efficacy, objective_parallelism)
end
3 changes: 3 additions & 0 deletions src/scip_data.jl
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,9 @@ mutable struct SCIPData
# corresponding SCIP objects.
sepas::Dict{Any, Ptr{SCIP_SEPA}}

# User-defined cut selector
cutsel_storage::Dict{Any, Ptr{SCIP_CUTSEL}}

# to store expressions for release
nonlinear_storage::Vector{NonlinExpr}
end
Expand Down
6 changes: 3 additions & 3 deletions src/sepa.jl
Original file line number Diff line number Diff line change
Expand Up @@ -125,12 +125,11 @@ end
)
Include a user defined separator `sepa` to the SCIP instance `scip`.
"""
function include_sepa(scip::Ptr{SCIP_}, sepas::Dict{Any, Ptr{SCIP_SEPA}}, sepa::SEPA;
name="", description="", priority=0, freq=1,
maxbounddist=0.0, usessubscip=false,
delay=false) where SEPA <: AbstractSeparator
delay=false) where {SEPA <: AbstractSeparator}
# Get C function pointers from Julia functions
_execlp = @cfunction(_sepaexeclp, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_SEPA}, Ptr{SCIP_RESULT}, SCIP_Bool))
_execsol = C_NULL
Expand Down Expand Up @@ -190,7 +189,8 @@ associated to the separator `sepa`.
- modifiable: is row modifiable during node processing (subject to column generation)?
- removable: should the row be removed from the LP due to aging or cleanup?
"""
function add_cut_sepa(scip::Ptr{SCIP_}, vars::Dict{VarRef, Ref{Ptr{SCIP_VAR}}}, sepas::Dict{Any, Ptr{SCIP_SEPA}}, sepa::SEPA, varrefs, coefs, lhs, rhs;
function add_cut_sepa(scip::Ptr{SCIP_}, vars::Dict{VarRef, Ref{Ptr{SCIP_VAR}}}, sepas::Dict{Any, Ptr{SCIP_SEPA}},
sepa::SEPA, varrefs, coefs, lhs, rhs;
islocal=false, modifiable=false, removable=true
) where {SEPA <: AbstractSeparator}
@assert length(varrefs) == length(coefs)
Expand Down
11 changes: 6 additions & 5 deletions test/conshdlr.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
@testset "dummy conshdlr (always satisfied, no constraint)" begin
# create an empty problem
o = SCIP.Optimizer()
MOI.set(o, MOI.Silent(), true)
SCIP.set_parameter(o.inner, "display/verblevel", 0)

# add the constraint handler
Expand All @@ -21,7 +22,7 @@ end
@testset "dummy conshdlr (always satisfied, with constraint)" begin
# create an empty problem
o = SCIP.Optimizer()
SCIP.set_parameter(o.inner, "display/verblevel", 0)
MOI.set(o, MOI.Silent(), true)

# add the constraint handler
ch = Dummy.DummyConsHdlr()
Expand All @@ -44,7 +45,7 @@ end
@testset "dummy conshdlr (always satisfied, no constraint, but needs it)" begin
# create an empty problem
o = SCIP.Optimizer()
SCIP.set_parameter(o.inner, "display/verblevel", 0)
MOI.set(o, MOI.Silent(), true)

# add the constraint handler
ch = Dummy.DummyConsHdlr()
Expand All @@ -64,7 +65,7 @@ end
@testset "never satisfied conshdlr (does not need constraint)" begin
# create an empty problem
o = SCIP.Optimizer()
SCIP.set_parameter(o.inner, "display/verblevel", 0)
MOI.set(o, MOI.Silent(), true)

# add the constraint handler
ch = NeverSatisfied.NSCH()
Expand All @@ -84,7 +85,7 @@ end
@testset "never satisfied conshdlr (needs constraint but does not have it)" begin
# create an empty problem
o = SCIP.Optimizer()
SCIP.set_parameter(o.inner, "display/verblevel", 0)
MOI.set(o, MOI.Silent(), true)

# add the constraint handler
ch = NeverSatisfied.NSCH()
Expand All @@ -104,7 +105,7 @@ end
@testset "never satisfied conshdlr (needs constraint and has one)" begin
# create an empty problem
o = SCIP.Optimizer()
SCIP.set_parameter(o.inner, "display/verblevel", 0)
MOI.set(o, MOI.Silent(), true)

# add the constraint handler
ch = NeverSatisfied.NSCH()
Expand Down
Loading

2 comments on commit 31948bf

@matbesancon
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@JuliaRegistrator register()

@JuliaRegistrator
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Registration pull request created: JuliaRegistries/General/69510

After the above pull request is merged, it is recommended that a tag is created on this repository for the registered package version.

This will be done automatically if the Julia TagBot GitHub Action is installed, or can be done manually through the github interface, or via:

git tag -a v0.11.6 -m "<description of version>" 31948bf9129f35b9cd6903d1b4ae70507dbd34b9
git push origin v0.11.6

Please sign in to comment.