diff --git a/NEWS.md b/NEWS.md index ecac74ee..dbd3cdc6 100644 --- a/NEWS.md +++ b/NEWS.md @@ -1,5 +1,9 @@ # News +## v0.9.1 + +- Add a Julia wrapper for constraint handlers. [109](https://github.com/SCIP-Interfaces/SCIP.jl/pull/109) + ## v0.9.0 - support MOI v0.9 [#126](https://github.com/SCIP-Interfaces/SCIP.jl/pull/126) diff --git a/README.md b/README.md index 96a8aa70..83e66126 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,8 @@ The goal is to support [JuMP](https://github.com/JuliaOpt/JuMP.jl) (from version Currently, we support LP, MIP and QCP problems, as well as some nonlinear constraints, both through `MOI` sets (e.g., for second-order cones) as well as for expression graphs (see below). -We still have feature loss in the area of callbacks compared to previous versions. +It is now possible to implement SCIP constraint handlers in Julia. Other plugin +types are not yet supported. ## Getting Started diff --git a/src/MOI_wrapper.jl b/src/MOI_wrapper.jl index e09052fa..a6c07dfc 100644 --- a/src/MOI_wrapper.jl +++ b/src/MOI_wrapper.jl @@ -91,7 +91,7 @@ end "Go back from solved stage to problem modification stage, invalidating results." function allow_modification(o::Optimizer) - if SCIPgetStage(o) != SCIP_STAGE_PROBLEM + if !(SCIPgetStage(o) in (SCIP_STAGE_PROBLEM, SCIP_STAGE_SOLVING)) @SC SCIPfreeTransform(o) end return nothing @@ -202,3 +202,4 @@ include(joinpath("MOI_wrapper", "indicator_constraints.jl")) include(joinpath("MOI_wrapper", "nonlinear_constraints.jl")) include(joinpath("MOI_wrapper", "objective.jl")) include(joinpath("MOI_wrapper", "results.jl")) +include(joinpath("MOI_wrapper", "conshdlr.jl")) diff --git a/src/MOI_wrapper/conshdlr.jl b/src/MOI_wrapper/conshdlr.jl new file mode 100644 index 00000000..c22c4d32 --- /dev/null +++ b/src/MOI_wrapper/conshdlr.jl @@ -0,0 +1,69 @@ +# +# Adding constraint handlers and constraints to SCIP.Optimizer. +# + +""" + include_conshdlr( + o::Optimizer, + ch::CH; + name::String, + description::String, + enforce_priority::Int, + check_priority::Int, + eager_frequency::Int, + needs_constraints::Bool + ) + +Include a user defined constraint handler `ch` to SCIP optimizer instance `o`. + +All parameters have default values that can be set as keyword arguments. +In particular, note the boolean `needs_constraints`: +* If set to `true`, then the callbacks are only called for the constraints that + were added explicitly using `add_constraint`. +* If set to `false`, the callback functions will always be called, even if no + corresponding constraint was added. It probably makes sense to set + `misc/allowdualreds` to `FALSE` in this case. +""" +function include_conshdlr(o::Optimizer, ch::CH; + name="", description="", enforce_priority=-15, + check_priority=-7000000, eager_frequency=100, + needs_constraints=true) where CH <: AbstractConstraintHandler + include_conshdlr(o.mscip, ch, name=name, description=description, + enforce_priority=enforce_priority, + check_priority=check_priority, + eager_frequency=eager_frequency, + needs_constraints=needs_constraints) +end + +""" + add_constraint( + o::Optimizer, + ch::CH, + c::C; + initial=true, + separate=true, + enforce=true, + check=true, + propagate=true, + _local=false, + modifiable=false, + dynamic=false, + removable=false, + stickingatnode=false + )::ConsRef + + +Add constraint `c` belonging to user-defined constraint handler `ch` to model. +Returns constraint reference. + +All keyword arguments are passed to the `SCIPcreateCons` call. +""" +function add_constraint(o::Optimizer, ch::CH, c::C; + initial=true, separate=true, enforce=true, check=true, + propagate=true, _local=false, modifiable=false, + dynamic=false, removable=false, stickingatnode=false) where {CH <:AbstractConstraintHandler, C <: AbstractConstraint{CH}} + return add_constraint(o.mscip, ch, c, initial=initial, separate=separate, + enforce=enforce, check=check, propagate=propagate, + _local=_local, modifiable=modifiable, dynamic=dynamic, + removable=removable, stickingatnode=stickingatnode) +end diff --git a/src/MOI_wrapper/results.jl b/src/MOI_wrapper/results.jl index 978306b4..c82e07f9 100644 --- a/src/MOI_wrapper/results.jl +++ b/src/MOI_wrapper/results.jl @@ -41,13 +41,25 @@ end "Make sure that SCIP is currently in one of the allowed stages." function assert_stage(o::Optimizer, stages) - if !(SCIPgetStage(o) in stages) - error("SCIP is wrong stage, can not query results!") + stage = SCIPgetStage(o) + if !(stage in stages) + error("SCIP is wrong stage ($stage, need $stages), can not query results!") end end "Make sure that the problem was solved (SCIP is in SOLVED stage)." -assert_solved(o::Optimizer) = assert_stage(o, [SCIP_STAGE_SOLVED]) +function assert_solved(o::Optimizer) + # SCIP's stage is SOLVING when stopped by user limit! + assert_stage(o, [SCIP_STAGE_SOLVING, SCIP_STAGE_SOLVED]) + + # Check for invalid status (when stage is SOLVING). + status = SCIPgetStage(o) + if status in (SCIP_STATUS_UNKNOWN, + SCIP_STATUS_USERINTERRUPT, + SCIP_STATUS_TERMINATE) + error("SCIP's solving was interrupted, but not by a user-given limit!") + end +end "Make sure that: TRANSFORMED ≤ stage ≤ SOLVED." assert_after_prob(o::Optimizer) = assert_stage(o, SCIP_Stage.(3:10)) diff --git a/src/MOI_wrapper/variable.jl b/src/MOI_wrapper/variable.jl index 8b421117..9a43eaa7 100644 --- a/src/MOI_wrapper/variable.jl +++ b/src/MOI_wrapper/variable.jl @@ -13,8 +13,8 @@ MOI.get(o::Optimizer, ::MOI.NumberOfVariables) = length(o.mscip.vars) MOI.get(o::Optimizer, ::MOI.ListOfVariableIndices) = [VI(k.val) for k in keys(o.mscip.vars)] MOI.is_valid(o::Optimizer, vi::VI) = haskey(o.mscip.vars, VarRef(vi.value)) -function MOI.get(o::Optimizer, ::MOI.VariableName, vi::VI) - return GC.@preserve o SCIPvarGetName(var(o, vi)) +function MOI.get(o::Optimizer, ::MOI.VariableName, vi::VI)::String + return GC.@preserve o unsafe_string(SCIPvarGetName(var(o, vi))) end function MOI.set(o::Optimizer, ::MOI.VariableName, vi::VI, name::String) diff --git a/src/SCIP.jl b/src/SCIP.jl index 22639ed4..ebfddf86 100644 --- a/src/SCIP.jl +++ b/src/SCIP.jl @@ -15,9 +15,15 @@ include("managed_scip.jl") # constraints from nonlinear expressions include("nonlinear.jl") +# constraint handlers +include("conshdlr.jl") + # implementation of MOI include("MOI_wrapper.jl") +# convenience functions +include("convenience.jl") + # warn about rewrite include("compat.jl") diff --git a/src/conshdlr.jl b/src/conshdlr.jl new file mode 100644 index 00000000..4e652733 --- /dev/null +++ b/src/conshdlr.jl @@ -0,0 +1,415 @@ +# +# Wrappers for implementing SCIP constraint handlers in Julia. +# +# Please study the corresponding SCIP documentation first, to become familiar +# with basic concepts and terms: https://scip.zib.de/doc-6.0.2/html/CONS.php +# +# The basic idea is that you create a new subtype of `AbstractConstraintHandler` +# to store the constraint handler data and implement the fundamental callbacks +# by adding methods to the functions `check`, `enforce_lp_sol`, +# `enforce_pseudo_sol` and `lock`. +# +# If your constraint handler uses constraint objects (`needs_constraints`), you +# create a subtype of the parametrized `AbstractConstraint`. +# +# +# Current limitations: +# - We now use fixed values for some properties: +# - SEPAPRIORITY = 0 +# - SEPAFREQ = -1 +# - DELAYSEPA = FALSE +# - PROPFREQ = -1 +# - DELAYPROP = -1 +# - PROP_TIMING = SCIP_PROPTIMING_BEFORELP +# - PRESOLTIMING = SCIP_PRESOLTIMING_MEDIUM +# - MAXPREROUNDS = -1 +# - We don't support these optional methods: CONSHDLRCOPY, CONSFREE, CONSINIT, +# CONSEXIT, CONSINITPRE, CONSEXITPRE, CONSINITSOL, CONSEXITSOL, CONSDELETE, +# CONSTRANS, CONSINITLP, CONSSEPALP, CONSSEPASOL, CONSENFORELAX, CONSPROP, +# CONSPRESOL, CONSRESPROP, CONSACTIVE, CONSDEACTIVE, CONSENABLE, CONSDISABLE, +# CONSDELVARS, CONSPRINT, CONSCOPY, CONSPARSE, CONSGETVARS, CONSGETNVARS, +# CONSGETDIVEBDCHGS +# - We don't support linear or nonlinear constraint upgrading. +# + + +# +# Abstract Supertypes: +# + +""" + AbstractConstraintHandler + +Abstract supertype for objects that store all user data belonging to the +constraint handler. This object is also used for method dispatch with the +callback functions. + +From SCIP's point-of-view, this objects corresponds to the SCIP_CONSHDLRDATA, +but its memory is managed by Julia's GC. + +It's recommended to store a reference to your instance of `ManagedSCIP` or +`SCIP.Optimizer` here, so that you can use it within your callback methods. +""" +abstract type AbstractConstraintHandler end + +""" + AbstractConstraint{Handler} + +Abstract supertype for objects that store all user data belonging to an +individual constraint. It's parameterized by the type of constraint handler. + +From SCIP's point-of-view, this objects corresponds to the SCIP_CONSDATA, +but its memory is managed by Julia's GC. +""" +abstract type AbstractConstraint{Handler} end + + +# +# Fundamental Callbacks for Julia. +# + +""" + check( + constraint_handler::CH, + constraints::Array{Ptr{SCIP_CONS}}, + sol::Ptr{SCIP_SOL}, + checkintegrality::Bool, + checklprows::Bool, + printreason::Bool, + completely::Bool + )::SCIP_RESULT + +Check whether the solution candidate given by `sol` satisfies all constraints in +`constraints`. + +Use the functions `user_constraint` and `sol_values` to access your +constraint-specific user data and solution values, respectively. + +Acceptable result values are: +* `SCIP_FEASIBLE`: The solution candidate satisfies all constraints. +* `SCIP_INFEASIBLE`: The solution candidate violates at least one constraint. + +There is nothing else to do for the user in terms of dealing with the violation. +""" +function check end + +""" + enforce_lp_sol( + constraint_handler::CH, + constraints::Array{Ptr{SCIP_CONS}}, + nusefulconss::Cint, + solinfeasible::Bool + )::SCIP_RESULT + +Enforce the current solution for the LP relaxation. That is, check all given +constraints for violation and deal with it in some way (see below). + +Use the functions `user_constraint` and `sol_values` to access your +constraint-specific user data and solution values, respectively. + +Acceptable result values are: +* `SCIP_FEASIBLE`: The solution candidate satisfies all constraints. +* `SCIP_CUTOFF`: The current subproblem is infeasible. +* `SCIP_CONSADDED`: Added a constraint that resolves the infeasibility. +* `SCIP_REDUCEDDOM`: Reduced the domain of a variable. +* `SCIP_SEPARATED`: Added a cutting plane. +* `SCIP_BRANCHED`: Performed a branching. +* `SCIP_INFEASIBLE`: The solution candidate violates at least one constraint. +""" +function enforce_lp_sol end + +""" + enforce_pseudo_sol( + constraint_handler::CH, + constraints::Array{Ptr{SCIP_CONS}}, + nusefulconss::Cint, + solinfeasible::Bool + )::SCIP_RESULT + +Enforce the current pseudo solution (the LP relaxation was not solved). That is, +check all given constraints for violation and deal with it in some way (see +below). + +Use the functions `user_constraint` and `sol_values` to access your +constraint-specific user data and solution values, respectively. + +Acceptable result values are: +* `SCIP_FEASIBLE`: The solution candidate satisfies all constraints. +* `SCIP_CUTOFF`: The current subproblem is infeasible. +* `SCIP_CONSADDED`: Added a constraint that resolves the infeasibility. +* `SCIP_REDUCEDDOM`: Reduced the domain of a variable. +* `SCIP_SEPARATED`: Added a cutting plane. +* `SCIP_BRANCHED`: Performed a branching. +* `SCIP_SOLVELP`: Force the solving of the LP relaxation. +* `SCIP_INFEASIBLE`: The solution candidate violates at least one constraint. +""" +function enforce_pseudo_sol end + +""" + lock( + constraint_handler::CH, + constraint::Ptr{SCIP_CONS}, + locktype::SCIP_LOCKTYPE, + nlockspos::Cint, + nlocksneg::Cint + ) + +Define the variable locks that the given constraint implies. For each related +variable, call `SCIPaddVarLocksType` to let SCIP know whether rounding the value +up or down might lead to constraint violation. +""" +function lock end + + +# +# Fundamental callback functions with SCIP's C API. +# +# There is only a single, generic implementation for each of these, which are +# passed to all user-defined SCIP constraint handlers, but they will call the +# user's method in the function body. +# + +""" +Generic `check` function, matching the signature from SCIP's C API. +""" +function _conscheck(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}, + conss::Ptr{Ptr{SCIP_CONS}}, nconss::Cint, + sol::Ptr{SCIP_SOL}, checkintegrality::SCIP_Bool, + checklprows::SCIP_Bool, printreason::SCIP_Bool, + completely::SCIP_Bool, result::Ptr{SCIP_RESULT}) + # get Julia object out of constraint handler data + conshdlrdata::Ptr{SCIP_CONSHDLRDATA} = SCIPconshdlrGetData(conshdlr) + constraint_handler = unsafe_pointer_to_objref(conshdlrdata) + + # get Julia array from C pointer + constraints = unsafe_wrap(Array{Ptr{SCIP_CONS}}, conss, nconss) + + # call user method via dispatch + res = check(constraint_handler, constraints, sol, + convert(Bool, checkintegrality), + convert(Bool, checklprows), + convert(Bool, printreason), + convert(Bool, completely)) + unsafe_store!(result, res) + + return SCIP_OKAY +end + +""" +Generic `enfolp` function, matching the signature from SCIP's C API. +""" +function _consenfolp(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}, + conss::Ptr{Ptr{SCIP_CONS}}, nconss::Cint, + nusefulconss::Cint, solinfeasible::SCIP_Bool, + result::Ptr{SCIP_RESULT}) + # get Julia object out of constraint handler data + conshdlrdata::Ptr{SCIP_CONSHDLRDATA} = SCIPconshdlrGetData(conshdlr) + constraint_handler = unsafe_pointer_to_objref(conshdlrdata) + + # get Julia array from C pointer + constraints = unsafe_wrap(Array{Ptr{SCIP_CONS}}, conss, nconss) + + # call user method via dispatch + res = enforce_lp_sol(constraint_handler, constraints, nusefulconss, + convert(Bool, solinfeasible)) + unsafe_store!(result, res) + + return SCIP_OKAY +end + +""" +Generic `enfops` function, matching the signature from SCIP's C API. +""" +function _consenfops(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}, + conss::Ptr{Ptr{SCIP_CONS}}, nconss::Cint, + nusefulconss::Cint, solinfeasible::SCIP_Bool, + objinfeasible::SCIP_Bool, result::Ptr{SCIP_RESULT}) + # get Julia object out of constraint handler data + conshdlrdata::Ptr{SCIP_CONSHDLRDATA} = SCIPconshdlrGetData(conshdlr) + constraint_handler = unsafe_pointer_to_objref(conshdlrdata) + + # get Julia array from C pointer + constraints = unsafe_wrap(Array{Ptr{SCIP_CONS}}, conss, nconss) + + # call user method via dispatch + res = enforce_pseudo_sol(constraint_handler, constraints, nusefulconss, + convert(Bool, solinfeasible), + convert(Bool, objinfeasible)) + unsafe_store!(result, res) + + return SCIP_OKAY +end + +""" +Generic `lock` function, matching the signature from SCIP's C API. +""" +function _conslock(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}, + cons::Ptr{SCIP_CONS}, locktype::SCIP_LOCKTYPE, + nlockspos::Cint, nlocksneg::Cint) + # get Julia object out of constraint handler data + conshdlrdata::Ptr{SCIP_CONSHDLRDATA} = SCIPconshdlrGetData(conshdlr) + constraint_handler = unsafe_pointer_to_objref(conshdlrdata) + + # call user method via dispatch + lock(constraint_handler, cons, locktype, nlockspos, nlocksneg) + + return SCIP_OKAY +end + + +# +# Additional callback functions for memory management. +# +# The implementation should work for all constraint handlers defined in Julia, +# so there is no method for the user to implement. +# + +""" +Generic `free` function, matching the signature from SCIP's C API. +""" +function _consfree(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}) + # Here, we should free the constraint handler data. But because this is an + # object created and owned by Julia, we will let GC do it. + # Instead, we will just set the pointer to NULL, so that SCIP will think + # that it is taken care of. + SCIPconshdlrSetData(conshdlr, C_NULL) + + return SCIP_OKAY +end + +""" +Generic `delete` function, matching the signature from SCIP's C API. +""" +function _consdelete(scip::Ptr{SCIP_}, conshdlr::Ptr{SCIP_CONSHDLR}, + cons::Ptr{SCIP_CONS}, consdata::Ptr{Ptr{SCIP_CONSDATA}}) + # Here, we should free the constraint data. But because this is an object + # created and owned by Julia, we will let GC do it. + # Instead, we will just set the pointer to NULL, so that SCIP will think + # that it is taken care of. + unsafe_store!(consdata, C_NULL) + + return SCIP_OKAY +end + + +# +# Adding constraint handlers and constraints to ManagedSCIP. +# + +""" + include_conshdlr( + mscip::ManagedSCIP, + ch::CH; + name::String, + description::String, + enforce_priority::Int, + check_priority::Int, + eager_frequency::Int, + needs_constraints::Bool + ) + +Include a user defined constraint handler `ch` to the SCIP instance `mscip`. + +All parameters have default values that can be set as keyword arguments. +In particular, note the boolean `needs_constraints`: +* If set to `true`, then the callbacks are only called for the constraints that + were added explicitly using `add_constraint`. +* If set to `false`, the callback functions will always be called, even if no + corresponding constraint was added. It probably makes sense to set + `misc/allowdualreds` to `FALSE` in this case. +""" +function include_conshdlr(mscip::ManagedSCIP, ch::CH; + name="", description="", enforce_priority=-15, + check_priority=-7000000, eager_frequency=100, + needs_constraints=true) where CH <: AbstractConstraintHandler + # Get C function pointers from Julia functions + _enfolp = @cfunction(_consenfolp, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}, Ptr{Ptr{SCIP_CONS}}, Cint, Cint, SCIP_Bool, Ptr{SCIP_RESULT})) + _enfops = @cfunction(_consenfops, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}, Ptr{Ptr{SCIP_CONS}}, Cint, Cint, SCIP_Bool, SCIP_Bool, Ptr{SCIP_RESULT})) + _check = @cfunction(_conscheck, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}, Ptr{Ptr{SCIP_CONS}}, Cint, Ptr{SCIP_SOL}, SCIP_Bool, SCIP_Bool, SCIP_Bool, SCIP_Bool, Ptr{SCIP_RESULT})) + _lock = @cfunction(_conslock, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}, Ptr{SCIP_CONS}, SCIP_LOCKTYPE, Cint, Cint)) + + # Store pointer to SCIP structure (for future C API calls) + conshdlr__ = Ref{Ptr{SCIP_CONSHDLR}}(C_NULL) + + # Hand over Julia object as constraint handler data: + conshdlrdata_ = pointer_from_objref(ch) + + # Try to create unique name, or else SCIP will complain! + if name == "" + name = "__ch__$(length(mscip.conshdlrs))" + end + + # Register constraint handler with SCIP instance. + @SC SCIPincludeConshdlrBasic(mscip, conshdlr__, name, description, + enforce_priority, check_priority, + eager_frequency, needs_constraints, + _enfolp, _enfops, _check, _lock, + conshdlrdata_) + + # Sanity checks + @assert conshdlr__[] != C_NULL + + # Set additional callbacks. + @SC SCIPsetConshdlrFree( + mscip, conshdlr__[], + @cfunction(_consfree, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}))) + @SC SCIPsetConshdlrDelete( + mscip, conshdlr__[], + @cfunction(_consdelete, SCIP_RETCODE, (Ptr{SCIP_}, Ptr{SCIP_CONSHDLR}, Ptr{SCIP_CONS}, Ptr{Ptr{SCIP_CONSDATA}}))) + + # Register constraint handler (for GC-protection and mapping). + mscip.conshdlrs[ch] = conshdlr__[] +end + +""" + add_constraint( + mscip::ManagedSCIP, + ch::CH, + c::C; + initial=true, + separate=true, + enforce=true, + check=true, + propagate=true, + _local=false, + modifiable=false, + dynamic=false, + removable=false, + stickingatnode=false + )::ConsRef + + +Add constraint `c` belonging to user-defined constraint handler `ch` to model. +Returns constraint reference. + +All keyword arguments are passed to the `SCIPcreateCons` call. +""" +function add_constraint(mscip::ManagedSCIP, ch::CH, c::C; + initial=true, separate=true, enforce=true, check=true, + propagate=true, _local=false, modifiable=false, + dynamic=false, removable=false, stickingatnode=false) where {CH <:AbstractConstraintHandler, C <: AbstractConstraint{CH}} + # Find matching SCIP constraint handler plugin. + conshdlr_::Ptr{SCIP_CONSHDLR} = get(mscip.conshdlrs, ch, C_NULL) + conshdlr_ != C_NULL || error("No matching constraint handler registered!") + + # Hand over Julia object as constraint data: + consdata_ = pointer_from_objref(c) + + # Create SCIP constraint (and attach constraint data). + cons__ = Ref{Ptr{SCIP_CONS}}(C_NULL) + @SC SCIPcreateCons(mscip, cons__, "", conshdlr_, consdata_, + initial, separate, enforce, check, propagate, + _local, modifiable, dynamic, removable, stickingatnode) + + # Sanity check. + @assert cons__[] != C_NULL + + # Register constraint data (for GC-protection). + mscip.conshdlrconss[c] = cons__[] + + # Add constraint to problem. + @SC SCIPaddCons(mscip, cons__[]) + + # Register constraint and return reference. + return store_cons!(mscip, cons__) +end diff --git a/src/convenience.jl b/src/convenience.jl new file mode 100644 index 00000000..3c412ccf --- /dev/null +++ b/src/convenience.jl @@ -0,0 +1,17 @@ +## Related to constraint handlers + +""" +Recover Julia object which is attached to user constraint (via constraint data). +""" +function user_constraint(cons_::Ptr{SCIP_CONS}) + @assert cons_ != C_NULL + consdata_::Ptr{SCIP.SCIP_CONSDATA} = SCIPconsGetData(cons_) + return unsafe_pointer_to_objref(consdata_) +end + +""" +Extract solution values for given variables. +""" +function sol_values(o::Optimizer, vars::AbstractArray{VI}, sol::Ptr{SCIP_SOL}=C_NULL) + return [SCIPgetSolVal(o, sol, var(o, vi)) for vi in vars] +end diff --git a/src/managed_scip.jl b/src/managed_scip.jl index 1a15c968..d3ee38a3 100644 --- a/src/managed_scip.jl +++ b/src/managed_scip.jl @@ -16,6 +16,13 @@ mutable struct ManagedSCIP var_count::Int64 cons_count::Int64 + # Map from user-defined Julia types (keys are <: AbstractConstraintHandler + # or <: AbstractConstraint, respectively) to the corresponding SCIP objects. + # The reverse mapping is handled by SCIP itself. + # This also serves to prevent premature GC. + conshdlrs::Dict{Any, Ptr{SCIP_CONSHDLR}} + conshdlrconss::Dict{Any, Ptr{SCIP_CONS}} + function ManagedSCIP() scip = Ref{Ptr{SCIP_}}(C_NULL) @SC SCIPcreate(scip) @@ -23,7 +30,7 @@ mutable struct ManagedSCIP @SC SCIPincludeDefaultPlugins(scip[]) @SC SCIP.SCIPcreateProbBasic(scip[], "") - mscip = new(scip, Dict(), Dict(), 0, 0) + mscip = new(scip, Dict(), Dict(), 0, 0, Dict(), Dict()) finalizer(free_scip, mscip) end end diff --git a/test/MOI_conshdlr.jl b/test/MOI_conshdlr.jl new file mode 100644 index 00000000..21f860b0 --- /dev/null +++ b/test/MOI_conshdlr.jl @@ -0,0 +1,191 @@ +using MathOptInterface +const MOI = MathOptInterface + +@testset "NaiveAllDiff (two binary vars)" begin + optimizer = SCIP.Optimizer(display_verblevel=0) + atol, rtol = 1e-6, 1e-6 + + # add two binary variables: x, y + x, y = MOI.add_variables(optimizer, 2) + MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne()) + + # maximize 2x + y + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], [x, y]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraint all-diff(x, y) + alldiffch = NaiveAllDiff.NADCH(optimizer) + SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true) + + alldiffcons = NaiveAllDiff.NADCons([x, y]) + cr = SCIP.add_constraint(optimizer, alldiffch, alldiffcons) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + + @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 2.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 0.0 atol=atol rtol=rtol +end + +@testset "NaiveAllDiff (3 bin.vars, 2 pairwise conss)" begin + optimizer = SCIP.Optimizer(display_verblevel=0) + atol, rtol = 1e-6, 1e-6 + + # add three binary variables + x, y, z = MOI.add_variables(optimizer, 3) + MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne()) + + # maximize 2x + 3y + 2z + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0], + [x, y, z]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraints all-diff(x, y), all-diff(y, x) + alldiffch = NaiveAllDiff.NADCH(optimizer) + SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true) + + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y])) + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([y, z])) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + + @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 4.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 0.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), z) ≈ 1.0 atol=atol rtol=rtol +end + +@testset "NaiveAllDiff (3 bin.vars, 3 pairwise conss)" begin + optimizer = SCIP.Optimizer(display_verblevel=0) + atol, rtol = 1e-6, 1e-6 + + # add three binary variables + x, y, z = MOI.add_variables(optimizer, 3) + MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne()) + + # maximize 2x + 3y + 2z + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0], + [x, y, z]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraints all-diff(x, y, z) + alldiffch = NaiveAllDiff.NADCH(optimizer) + SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true) + + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y])) + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, z])) + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([y, z])) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION + + @test MOI.get(optimizer, MOI.NodeCount()) >= 8 # complete enumeration +end + +@testset "NaiveAllDiff (3 bin.vars, 1 cons with all)" begin + optimizer = SCIP.Optimizer(display_verblevel=0) + atol, rtol = 1e-6, 1e-6 + + # add three binary variables + x, y, z = MOI.add_variables(optimizer, 3) + MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(z), MOI.ZeroOne()) + + # maximize 2x + 3y + 2z + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 3.0, 2.0], + [x, y, z]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraints all-diff(x, y, z) + alldiffch = NaiveAllDiff.NADCH(optimizer) + SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true) + + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y, z])) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION + + @test MOI.get(optimizer, MOI.NodeCount()) >= 8 # complete enumeration +end + +@testset "NaiveAllDiff (3 int.vars, 1 cons with all)" begin + optimizer = SCIP.Optimizer(display_verblevel=0) + atol, rtol = 1e-6, 1e-6 + + # add three integer variables, in {0, 1, 2} + x, y, z = MOI.add_variables(optimizer, 3) + for v in [x, y, z] + MOI.add_constraint(optimizer, MOI.SingleVariable(v), MOI.Integer()) + MOI.add_constraint(optimizer, MOI.SingleVariable(v), MOI.Interval(0.0, 2.0)) + end + + # maximize 2x + y + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], + [x, y]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraints all-diff(x, y, z) + alldiffch = NaiveAllDiff.NADCH(optimizer) + SCIP.include_conshdlr(optimizer, alldiffch; needs_constraints=true) + + SCIP.add_constraint(optimizer, alldiffch, NaiveAllDiff.NADCons([x, y, z])) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.OPTIMAL + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.FEASIBLE_POINT + + @test MOI.get(optimizer, MOI.ObjectiveValue()) ≈ 5.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), x) ≈ 2.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), y) ≈ 1.0 atol=atol rtol=rtol + @test MOI.get(optimizer, MOI.VariablePrimal(), z) ≈ 0.0 atol=atol rtol=rtol +end + +@testset "NoGoodCounter (2 binary vars)" begin + optimizer = SCIP.Optimizer(display_verblevel=0, + misc_allowdualreds=SCIP.FALSE) + atol, rtol = 1e-6, 1e-6 + + # add binary variables + x, y = MOI.add_variables(optimizer, 2) + MOI.add_constraint(optimizer, MOI.SingleVariable(x), MOI.ZeroOne()) + MOI.add_constraint(optimizer, MOI.SingleVariable(y), MOI.ZeroOne()) + + # maximize 2x + y + MOI.set(optimizer, MOI.ObjectiveFunction{MOI.ScalarAffineFunction{Float64}}(), + MOI.ScalarAffineFunction(MOI.ScalarAffineTerm.([2.0, 1.0], + [x, y]), 0.0)) + MOI.set(optimizer, MOI.ObjectiveSense(), MOI.MAX_SENSE) + + # add constraint handler with constraints + counter = NoGoodCounter.Counter(optimizer, [x, y]) + SCIP.include_conshdlr(optimizer, counter; needs_constraints=false) + + # solve problem and query result + MOI.optimize!(optimizer) + @test MOI.get(optimizer, MOI.TerminationStatus()) == MOI.INFEASIBLE + @test MOI.get(optimizer, MOI.PrimalStatus()) == MOI.NO_SOLUTION + + @test length(counter.solutions) == 4 +end diff --git a/test/conshdlr.jl b/test/conshdlr.jl new file mode 100644 index 00000000..b79ee20b --- /dev/null +++ b/test/conshdlr.jl @@ -0,0 +1,125 @@ +@testset "dummy conshdlr (always satisfied, no constraint)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = Dummy.DummyConsHdlr() + SCIP.include_conshdlr(mscip, ch; needs_constraints=false) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called >= 1 + @test ch.enfo_called == 0 + @test ch.lock_called == 1 + + # free the problem + finalize(mscip) +end + +@testset "dummy conshdlr (always satisfied, with constraint)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = Dummy.DummyConsHdlr() + SCIP.include_conshdlr(mscip, ch; needs_constraints=true) + + # add dummy constraint + cr = SCIP.add_constraint(mscip, ch, Dummy.DummyCons()) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called >= 1 + @test ch.enfo_called == 0 + @test ch.lock_called == 1 + + # free the problem + finalize(mscip) +end + +@testset "dummy conshdlr (always satisfied, no constraint, but needs it)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = Dummy.DummyConsHdlr() + SCIP.include_conshdlr(mscip, ch; needs_constraints=true) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called == 0 + @test ch.enfo_called == 0 + @test ch.lock_called == 0 + + # free the problem + finalize(mscip) +end + +@testset "never satisfied conshdlr (does not need constraint)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = NeverSatisfied.NSCH() + SCIP.include_conshdlr(mscip, ch; needs_constraints=false) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called >= 1 + @test ch.enfo_called == 1 + @test ch.lock_called == 1 + + # free the problem + finalize(mscip) +end + +@testset "never satisfied conshdlr (needs constraint but does not have it)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = NeverSatisfied.NSCH() + SCIP.include_conshdlr(mscip, ch; needs_constraints=true) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called == 0 + @test ch.enfo_called == 0 + @test ch.lock_called == 0 + + # free the problem + finalize(mscip) +end + +@testset "never satisfied conshdlr (needs constraint and has one)" begin + # create an empty problem + mscip = SCIP.ManagedSCIP() + SCIP.set_parameter(mscip, "display/verblevel", 0) + + # add the constraint handler + ch = NeverSatisfied.NSCH() + SCIP.include_conshdlr(mscip, ch; needs_constraints=true) + + # add one constraint + cr = SCIP.add_constraint(mscip, ch, NeverSatisfied.Cons()) + + # solve the problem + SCIP.@SC SCIP.SCIPsolve(mscip.scip[]) + + @test ch.check_called >= 1 + @test ch.enfo_called == 1 + @test ch.lock_called == 1 + + # free the problem + finalize(mscip) +end diff --git a/test/conshdlr_support.jl b/test/conshdlr_support.jl new file mode 100644 index 00000000..67f726f7 --- /dev/null +++ b/test/conshdlr_support.jl @@ -0,0 +1,238 @@ +module Dummy + +using SCIP + +# Define a minimal no-op constraint handler. +# Needs to be mutable for `pointer_from_objref` to work. +mutable struct DummyConsHdlr <: SCIP.AbstractConstraintHandler + check_called::Int64 + enfo_called::Int64 + lock_called::Int64 + + DummyConsHdlr() = new(0, 0, 0) +end + +# Implement only the fundamental callbacks: +function SCIP.check(ch::DummyConsHdlr, + constraints::Array{Ptr{SCIP.SCIP_CONS}}, + sol::Ptr{SCIP.SCIP_SOL}, + checkintegrality::Bool, + checklprows::Bool, + printreason::Bool, + completely::Bool) + ch.check_called += 1 + return SCIP.SCIP_FEASIBLE +end + +function SCIP.enforce_lp_sol(ch::DummyConsHdlr, + constraints::Array{Ptr{SCIP.SCIP_CONS}}, + nusefulconss::Cint, + solinfeasible::Bool) + ch.enfo_called += 1 + return SCIP.SCIP_FEASIBLE +end + +function SCIP.enforce_pseudo_sol(ch::DummyConsHdlr, + constraints::Array{Ptr{SCIP.SCIP_CONS}}, + nusefulconss::Cint, + solinfeasible::Bool, + objinfeasible::Bool) + ch.enfo_called += 1 + return SCIP.SCIP_FEASIBLE +end + +function SCIP.lock(ch::DummyConsHdlr, + constraint::Ptr{SCIP.SCIP_CONS}, + locktype::SCIP.SCIP_LOCKTYPE, + nlockspos::Cint, + nlocksneg::Cint) + ch.lock_called += 1 +end + +# Corresponding, empty constraint (data) object +mutable struct DummyCons <: SCIP.AbstractConstraint{DummyConsHdlr} end + +end # module Dummy + + + +module NeverSatisfied + +using SCIP + +mutable struct NSCH <: SCIP.AbstractConstraintHandler + check_called::Int64 + enfo_called::Int64 + lock_called::Int64 + + NSCH() = new(0, 0, 0) +end + +function SCIP.check(ch::NSCH, constraints, sol, checkintegrality, + checklprows, printreason, completely) + ch.check_called += 1 + return SCIP.SCIP_INFEASIBLE +end + +function SCIP.enforce_lp_sol(ch::NSCH, constraints, nusefulconss, solinfeasible) + ch.enfo_called += 1 + return SCIP.SCIP_INFEASIBLE +end + +function SCIP.enforce_pseudo_sol(ch::NSCH, constraints, nusefulconss, + solinfeasible, objinfeasible) + ch.enfo_called += 1 + return SCIP.SCIP_INFEASIBLE +end + +function SCIP.lock(ch::NSCH, constraint, locktype, nlockspos, nlocksneg) + ch.lock_called += 1 +end + +# Corresponding, empty constraint (data) object +mutable struct Cons <: SCIP.AbstractConstraint{NSCH} end + +end # module AlwaysSatisfied + + + +module NaiveAllDiff + +using MathOptInterface +using SCIP + +const MOI = MathOptInterface + +mutable struct NADCH <: SCIP.AbstractConstraintHandler + scip::SCIP.Optimizer # for SCIP* and var maps +end + +# Constraint data, referencing variables of a single constraint. +mutable struct NADCons <: SCIP.AbstractConstraint{NADCH} + variables::Array{MOI.VariableIndex} +end + +# Helper function used in several callbacks +function anyviolated(ch, constraints, sol=C_NULL) + for cons_::Ptr{SCIP.SCIP_CONS} in constraints + cons::NADCons = SCIP.user_constraint(cons_) + + # extract solution values + values = SCIP.sol_values(ch.scip, cons.variables, sol) + + # check for constraint violation + if !allunique(values) + return true + end + end + return false +end + +function SCIP.check(ch::NADCH, constraints, sol, checkintegrality, + checklprows, printreason, completely) + if anyviolated(ch, constraints, sol) + return SCIP.SCIP_INFEASIBLE + else + return SCIP.SCIP_FEASIBLE + end +end + +function SCIP.enforce_lp_sol(ch::NADCH, constraints, nusefulconss, solinfeasible) + if anyviolated(ch, constraints, C_NULL) + return SCIP.SCIP_INFEASIBLE + else + return SCIP.SCIP_FEASIBLE + end +end + +function SCIP.enforce_pseudo_sol(ch::NADCH, constraints, nusefulconss, + solinfeasible, objinfeasible) + if anyviolated(ch, constraints, C_NULL) + return SCIP.SCIP_INFEASIBLE + else + return SCIP.SCIP_FEASIBLE + end +end + +function SCIP.lock(ch::NADCH, constraint, locktype, nlockspos, nlocksneg) + cons::NADCons = SCIP.user_constraint(constraint) + + # look all variables in both directions + for vi in cons.variables + # TODO: understand why lock is called during SCIPfree, after the + # constraint should have been deleted already. Does it mean we should + # implement CONSTRANS? + var_::Ptr{SCIP.SCIP_VAR} = SCIP.var(ch.scip, vi) + var_ != C_NULL || continue # avoid segfault! + + SCIP.@SC SCIP.SCIPaddVarLocksType( + ch.scip, var_, locktype, nlockspos + nlocksneg, nlockspos + nlocksneg) + end +end + +end # module NaiveAllDiff + + + +module NoGoodCounter + +using MathOptInterface +using SCIP + +const MOI = MathOptInterface + +mutable struct Counter <: SCIP.AbstractConstraintHandler + scip::SCIP.Optimizer # for SCIP* and var maps + variables::Array{MOI.VariableIndex} + solutions::Set{Array{Float64}} + + Counter(scip, variables) = new(scip, variables, Set()) +end + +function SCIP.check(ch::Counter, constraints, sol, checkintegrality, + checklprows, printreason, completely) + return SCIP.SCIP_INFEASIBLE +end + +function enforce(ch::Counter) + values = SCIP.sol_values(ch.scip, ch.variables) + + # Store solution + if values in ch.solutions + # Getting the same solution twice? + return SCIP.SCIP_INFEASIBLE + end + push!(ch.solutions, values) + + # Add no-good constraint: sum_zeros(x) + sum_ones(1-x) >= 1 + zeros = values .≈ 0.0 + ones = values .≈ 1.0 + others = .!zeros .& .!ones + if any(others) + error("Found non-binary solution value for $(vars[others])") + end + + terms = vcat([MOI.ScalarAffineTerm( 1.0, vi) for vi in ch.variables[zeros]], + [MOI.ScalarAffineTerm(-1.0, vi) for vi in ch.variables[ones]]) + ci = MOI.add_constraint(ch.scip, MOI.ScalarAffineFunction(terms, 0.0), + MOI.GreaterThan(1.0 - sum(ones))) + + return SCIP.SCIP_CONSADDED +end + +function SCIP.enforce_lp_sol(ch::Counter, constraints, nusefulconss, + solinfeasible) + @assert length(constraints) == 0 + return enforce(ch) +end + +function SCIP.enforce_pseudo_sol(ch::Counter, constraints, nusefulconss, + solinfeasible, objinfeasible) + @assert length(constraints) == 0 + return enforce(ch) +end + +function SCIP.lock(ch::Counter, constraint, locktype, nlockspos, nlocksneg) +end + +end # module NoGoodCounter diff --git a/test/runtests.jl b/test/runtests.jl index 9c02d5e6..3808ffd1 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -9,6 +9,13 @@ end include("managed_scip.jl") end +# new type definitions in module (needs top level) +include("conshdlr_support.jl") + +@testset "constraint handlers" begin + include("conshdlr.jl") +end + @testset "MathOptInterface tests (bridged)" begin include("MOI_wrapper_bridged.jl") end @@ -28,3 +35,7 @@ end @testset "MathOptInterface nonlinear expressions" begin include("MOI_nonlinear_exprs.jl") end + +@testset "constraint handlers (with MOI)" begin + include("MOI_conshdlr.jl") +end