diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..004af2b --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +.vscode/settings.json +Manifest.toml diff --git a/Project.toml b/Project.toml index b08760e..2e4ab45 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "BlockDecomposition" uuid = "6cde8614-403a-11e9-12f1-c10d0f0caca0" authors = ["Guillaume Marques", "Vitor Nesello", "François Vanderbeck"] -version = "1.14.1" +version = "1.15.0" [deps] Combinatorics = "861a8166-3701-5b0c-9a16-15d98fcdc6aa" diff --git a/src/BlockDecomposition.jl b/src/BlockDecomposition.jl index c1e8748..a7e5c54 100644 --- a/src/BlockDecomposition.jl +++ b/src/BlockDecomposition.jl @@ -29,11 +29,11 @@ include("subproblem_representative.jl") include("checker.jl") include("decomposition.jl") include("objective.jl") +include("customdata.jl") include("callbacks.jl") include("automatic_dantzig_wolfe.jl") include("utils.jl") include("branchingpriority.jl") -include("customdata.jl") include("soldisaggregation.jl") function model_factory(::Val{true}, optimizer; kw...)::JuMP.Model diff --git a/src/annotations.jl b/src/annotations.jl index a42c44f..b874183 100644 --- a/src/annotations.jl +++ b/src/annotations.jl @@ -21,6 +21,7 @@ mutable struct Annotation{T, F<:Formulation, D<:Decomposition} axis_index_value::T lower_multiplicity::Float64 upper_multiplicity::Float64 + branching_priority::Float64 optimizer_builders::Vector end @@ -31,24 +32,26 @@ getdecomposition(a::Annotation) = a.decomposition getlowermultiplicity(a::Annotation) = a.lower_multiplicity getuppermultiplicity(a::Annotation) = a.upper_multiplicity getoptimizerbuilders(a::Annotation) = a.optimizer_builders +getbranchingpriority(a::Annotation) = a.branching_priority setlowermultiplicity!(a::Annotation, lm::Real) = a.lower_multiplicity = lm setuppermultiplicity!(a::Annotation, um::Real) = a.upper_multiplicity = um +setbranchingpriority!(a::Annotation, bp::Real) = a.branching_priority = bp emptyoptimizerbuilders!(a::Annotation) = empty!(a.optimizer_builders) pushoptimizerbuilder!(a::Annotation, f::MOI.AbstractOptimizer) = push!(a.optimizer_builders, f) pushoptimizerbuilder!(a::Annotation, f::Function) = push!(a.optimizer_builders, f) pushoptimizerbuilder!(a::Annotation, f::AbstractCustomOptimizer) = push!(a.optimizer_builders, f) -OriginalAnnotation() = Annotation(0, 0, Original, NoDecomposition, 0, 1.0, 1.0, []) +OriginalAnnotation() = Annotation(0, 0, Original, NoDecomposition, 0, 1.0, 1.0, 1.0, []) function MasterAnnotation(tree, D::Type{<:Decomposition}) uid = generateannotationid(tree) - return Annotation(uid, 0, Master, D, 0, 1.0, 1.0, []) + return Annotation(uid, 0, Master, D, 0, 1.0, 1.0, 1.0, []) end function Annotation(tree, F::Type{<:Formulation}, D::Type{<:Decomposition}, v) uid = generateannotationid(tree) - return Annotation(uid, 0, F, D, v, 1.0, 1.0, []) + return Annotation(uid, 0, F, D, v, 1.0, 1.0, 1.0, []) end function Base.show(io::IO, a::Annotation) @@ -60,6 +63,8 @@ function Base.show(io::IO, a::Annotation) print(io, getlowermultiplicity(a)) print(io, ", um = ") print(io, getuppermultiplicity(a)) + print(io, ", bp = ") + print(io, getbranchingpriority(a)) print(io, ", id = ") print(io, getid(a)) print(io, ")") diff --git a/src/branchingpriority.jl b/src/branchingpriority.jl index dcd7211..3568636 100644 --- a/src/branchingpriority.jl +++ b/src/branchingpriority.jl @@ -10,6 +10,8 @@ The idea is to have both "soft" and "hard" branching priorities. For instance : - if the number of branching candidates with priority 4.0 is less than the maximum number of candidates considered, no branching candidates with priority 3.0 will be considered - if the number of branching candidates with priority 3.5 is less than the maximum number of candidates considered, then some branching candidates with priority 3.0 will be considered (to bring the total number to the maximum) - if the number of branching candidates with priority 3.5 is not less than the maximum number, then no branching candidates with priority 3.0 will be considered + +Branching priority is also used in rounding and diving heuristics to determine which variables should be fixed first. """ branchingpriority!(x::JuMP.VariableRef, priority) = MOI.set( x.model, VarBranchingPriority(), x, priority diff --git a/src/callbacks.jl b/src/callbacks.jl index 0c3d9b3..dd33ef1 100644 --- a/src/callbacks.jl +++ b/src/callbacks.jl @@ -108,17 +108,6 @@ function callback_ub(cbdata, x::JuMP.VariableRef) ) end -""" -Every custom data assigned to a pricing variable or a user cut should inherit from it. -""" -abstract type AbstractCustomData end - -function MOI.submit( - model, cb, con, custom_data -) - return MOI.submit(JuMP.backend(model), cb, JuMP.moi_function(con.func), con.set, custom_data) -end - """ A callback to provide initial columns to the optimizer before starting the optimization. """ diff --git a/src/customdata.jl b/src/customdata.jl index 7642e2c..b2ce96a 100644 --- a/src/customdata.jl +++ b/src/customdata.jl @@ -1,13 +1,65 @@ +""" + AbstractCustomData + +Left for compatibility with BlockDecomposition versions 1.14.1 and below. +One should use [AbstractCustomVarData](@ref) or [AbstractCustomConstrData](@ref) instead. +""" +abstract type AbstractCustomData end + +""" + AbstractCustomVarData + +Every custom data associated to a solution passed in [PricingSolution](@ref) +should inherit from AbstractCustomVarData. + +This data is used to +- Determine the coefficient of the corresponding column in non-robust constraints +- Store the information about pricing solution not expressed with subproblem variables + (and thus not used in the master formulation); this information can then be retrieved + using [customdata(info)]@ref by the user. +- determine the branching priority of the corresponding column + (if [branchingpriority(::AbstractCustomVarData)] is defined for the concrete type). +""" +abstract type AbstractCustomVarData <: AbstractCustomData end + +branchingpriority(::Nothing) = nothing + +""" + branchingpriority(<:AbstractCustomVarData) + +This function should be redefined for a concrete type which inherits from AbstractCustomVarData +if a custom branching priority is defined for columns associated with this data type. +If this function is not redefined, the branching priority of each column equals to the +branching priority of the pricing problem which generated it. +""" +branchingpriority(::AbstractCustomVarData) = nothing + + +""" + AbstractCustomConstrData + +Every custom data associated to a non-robust constraint should inherit from AbstractCustomConstrData. + +This data is used to determine the coefficient of the columns in non-robust constraints. +""" +abstract type AbstractCustomConstrData <: AbstractCustomData end + +function MOI.submit( + model, cb, con::JuMP.AbstractConstraint, custom_data::AbstractCustomConstrData +) + return MOI.submit(JuMP.backend(model), cb, JuMP.moi_function(con.func), con.set, custom_data) +end + struct CustomVars <: MOI.AbstractModelAttribute end struct CustomConstrs <: MOI.AbstractModelAttribute end """ - customvars!(model, customvar::Type{AbstractCustomData}) - customvars!(model, customvars::Vector{Type{<:AbstractCustomData}}) + customvars!(model, customvar::Type{<:AbstractCustomVarData}) + customvars!(model, customvars::Vector{Type{<:AbstractCustomVarData}}) Set the possible custom data types of variables in a model. """ -customvars!(model, customvar::DataType) = MOI.set( +customvars!(model, customvar::Type{<:AbstractCustomData}) = MOI.set( model, CustomVars(), [customvar] ) customvars!(model, customvars::Vector{DataType}) = MOI.set( @@ -15,12 +67,12 @@ customvars!(model, customvars::Vector{DataType}) = MOI.set( ) """ - customconstrs!(model, customconstr::DataType) - customconstrs!(model, customconstrs::Vector{DataType}) + customconstrs!(model, customconstr::Type{AbstractCustomConstrData}) + customconstrs!(model, customconstrs::Vector{Type{AbstractCustomConstrData}}) Set the possible custom data types of constraints in a model. """ -customconstrs!(model, customconstr::DataType) = MOI.set( +customconstrs!(model, customconstr::Type{<:AbstractCustomData}) = MOI.set( model, CustomConstrs(), [customconstr] ) customconstrs!(model, customconstrs::Vector{DataType}) = MOI.set( @@ -146,12 +198,12 @@ function MOI.get(dest::MOIU.UniversalFallback, attribute::CustomConstrValue, ci: end MathOptInterface.Utilities.map_indices( - variable_map::MathOptInterface.Utilities.IndexMap, x::AbstractCustomData + ::MathOptInterface.Utilities.IndexMap, x::AbstractCustomData ) = x MathOptInterface.Utilities.map_indices( - variable_map::MathOptInterface.Utilities.IndexMap, x::Vector{AbstractCustomData} + ::MathOptInterface.Utilities.IndexMap, x::Vector{AbstractCustomData} ) = x # added for compatibility with MathOptInterface v1.23 -MathOptInterface.Utilities.map_indices(f::Function, x::AbstractCustomData) = x -MathOptInterface.Utilities.map_indices(f::Function, x::Vector{AbstractCustomData}) = x \ No newline at end of file +MathOptInterface.Utilities.map_indices(::Function, x::AbstractCustomData) = x +MathOptInterface.Utilities.map_indices(::Function, x::Vector{AbstractCustomData}) = x diff --git a/src/formulations.jl b/src/formulations.jl index 370133e..be786c6 100644 --- a/src/formulations.jl +++ b/src/formulations.jl @@ -72,16 +72,22 @@ end subproblem, lower_multiplicity = 1, upper_multiplicity = 1, - solver = nothing + solver = nothing, + branching_priority = 1 ) Method that allows the user to specify additional property of the subproblems. -The multiplicity of `subproblem` is the number of times that the same independant +The multiplicity of `subproblem` is the number of times that the same independent block shaped by the subproblem in the coefficient matrix appears in the model. It is equivalent to the number of solutions to the subproblem that can appear in the solution of the original problem. +Branching priority of a subproblem is equal to the branching priority of the associated integer variable +(the number of columns from this subproblem in the global solution). It also determines +the default branching priority of columns generated by this subproblem. Branching priority is also used +in rounding and diving heuristics to prioritize which variables and columns to fix the first. + The solver of the subproblem is the way the subproblem will be optimized. It can be either a function (pricing callback), an optimizer of MathOptInterface (e.g. `Gurobi.Optimizer`, `CPLEX.Optimizer`, `Glpk.Optimizer`... with attributes), @@ -98,10 +104,12 @@ buffered to all solvers. So you may degrade performances if you use a lot of sol """ function specify!( sp::SubproblemForm; lower_multiplicity::Real = 1, - upper_multiplicity::Real = 1, solver = nothing + upper_multiplicity::Real = 1, solver = nothing, + branching_priority::Real = 1 ) setlowermultiplicity!(sp.annotation, lower_multiplicity) setuppermultiplicity!(sp.annotation, upper_multiplicity) + setbranchingpriority!(sp.annotation, branching_priority) emptyoptimizerbuilders!(sp.annotation) _specify!(sp, solver) return diff --git a/test/customdata.jl b/test/customdata.jl index f615888..a0f566e 100644 --- a/test/customdata.jl +++ b/test/customdata.jl @@ -1,19 +1,34 @@ -struct MyCustomVarData1 <: BlockDecomposition.AbstractCustomData +struct MyCustomVarData1 <: BlockDecomposition.AbstractCustomVarData nb_items::Int + branching_priority::Float64 end -struct MyCustomVarData2 <: BlockDecomposition.AbstractCustomData +BD.branchingpriority(data::MyCustomVarData1) = data.branching_priority + +struct MyCustomVarData2 <: BlockDecomposition.AbstractCustomVarData nb_items::Float64 end -struct MyCustomCutData1 <: BlockDecomposition.AbstractCustomData +struct MyCustomCutData1 <: BlockDecomposition.AbstractCustomConstrData min_items::Int end -struct MyCustomCutData2 <: BlockDecomposition.AbstractCustomData +struct MyCustomCutData2 <: BlockDecomposition.AbstractCustomConstrData min_items::Float64 end +struct CutCallbackContext end + +function MOI.submit( + ::MockOptimizer, + ::MOI.UserCut{CutCallbackContext}, + ::MOI.ScalarAffineFunction{Float64}, + ::Union{MOI.LessThan{Float64}, MOI.GreaterThan{Float64}, MOI.EqualTo{Float64}}, + ::BD.AbstractCustomConstrData +) + return nothing +end + function test_custom_data() model = Model() customvars!(model, [MyCustomVarData1, MyCustomVarData2]) @@ -53,19 +68,27 @@ function test_custom_data() return end + + function test_attach_custom_data() - model = Model() + model = Model(MockOptimizer) @variable(model, x[1:2]) @constraint(model, con, x[1] + x[2] <= 1) @testset "attach custom data to variable from unregistered custom data family" begin - @test_throws UnregisteredCustomDataFamily customdata!(x[1], MyCustomVarData1(1)) + @test_throws UnregisteredCustomDataFamily customdata!(x[1], MyCustomVarData1(1, 2.0)) + @test_throws UnregisteredCustomDataFamily customdata!(con, MyCustomCutData1(1)) end @testset "attach custom data to a variable" begin customvars!(model, MyCustomVarData1) - customdata!(x[1], MyCustomVarData1(1)) - @test customdata(x[1]) == MyCustomVarData1(1) + customvars!(model, MyCustomVarData2) + customdata!(x[1], MyCustomVarData1(1, 2.0)) + @test customdata(x[1]) == MyCustomVarData1(1, 2.0) + @test branchingpriority(customdata(x[1])) == 2.0 + customdata!(x[2], MyCustomVarData2(2)) + @test customdata(x[2]) == MyCustomVarData2(2) + @test branchingpriority(customdata(x[2])) === nothing end @testset "attach custom data to a constraint" begin @@ -73,4 +96,12 @@ function test_attach_custom_data() customdata!(con, MyCustomCutData1(1)) @test customdata(con) == MyCustomCutData1(1) end + + @testset "submit a cut with attached custom data " begin + MOI.submit( + model, MOI.UserCut(CutCallbackContext()), + JuMP.ScalarConstraint(JuMP.AffExpr(0.0), MOI.LessThan(1.0)), MyCustomCutData1(2) + ) + @test_throws UnregisteredCustomDataFamily JuMP.optimize!(model) + end end diff --git a/test/dantzigwolfe.jl b/test/dantzigwolfe.jl index b6d0123..f052ae1 100644 --- a/test/dantzigwolfe.jl +++ b/test/dantzigwolfe.jl @@ -20,7 +20,7 @@ function test_dantzig_wolfe_different() subproblems = getsubproblems(dec) @test repr(subproblems[1]) == "Subproblem formulation for Machines = 1 contains :\t 0.0 <= multiplicity <= 1.0\n" @test gettree(model) == gettree(dec) - @test repr(dec) == "Root - Annotation(BlockDecomposition.Master, BlockDecomposition.DantzigWolfe, lm = 1.0, um = 1.0, id = 2) with 2 subproblems :\n\t 2 => Annotation(BlockDecomposition.DwPricingSp, BlockDecomposition.DantzigWolfe, lm = 0.0, um = 1.0, id = 4) \n\t 1 => Annotation(BlockDecomposition.DwPricingSp, BlockDecomposition.DantzigWolfe, lm = 0.0, um = 1.0, id = 3) \n" + @test repr(dec) == "Root - Annotation(BlockDecomposition.Master, BlockDecomposition.DantzigWolfe, lm = 1.0, um = 1.0, bp = 1.0, id = 2) with 2 subproblems :\n\t 2 => Annotation(BlockDecomposition.DwPricingSp, BlockDecomposition.DantzigWolfe, lm = 0.0, um = 1.0, bp = 2.0, id = 4) \n\t 1 => Annotation(BlockDecomposition.DwPricingSp, BlockDecomposition.DantzigWolfe, lm = 0.0, um = 1.0, bp = 1.0, id = 3) \n" end d = GapToyData(30, 10) diff --git a/test/formulations.jl b/test/formulations.jl index 4e1cd01..c7effe3 100644 --- a/test/formulations.jl +++ b/test/formulations.jl @@ -27,9 +27,10 @@ function generalized_assignement(d::GapData) sum(d.costs[j, m] * x[j, m] for j in d.jobs, m in Machines)) @dantzig_wolfe_decomposition(model, decomposition, Machines) - master = getmaster(decomposition) subproblems = getsubproblems(decomposition) - specify!.(subproblems, lower_multiplicity = 0, upper_multiplicity = 1) + for (i,_) in enumerate(Machines) + specify!(subproblems[i], lower_multiplicity = 0, upper_multiplicity = 1, branching_priority = i) + end return model, x, cov, knp, decomposition end @@ -67,9 +68,8 @@ function generalized_assignement_penalties(d::GapData) @dantzig_wolfe_decomposition(model, decomposition, Machines) - master = getmaster(decomposition) subproblems = getsubproblems(decomposition) - for (i,m) in enumerate(Machines) + for (i,_) in enumerate(Machines) specify!(subproblems[i], lower_multiplicity = 0, upper_multiplicity = 1) end return model, x, y, z, cov, knp, lim, decomposition diff --git a/test/runtests.jl b/test/runtests.jl index 96ef287..7e4d6f4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -12,13 +12,14 @@ MOI.optimize!(::MockOptimizer) = nothing # Helper to tests annotations function test_annotation(ann::BD.Annotation, F::Type{<:BD.Formulation}, - D::Type{<:BD.Decomposition}, minmult, maxmult) + D::Type{<:BD.Decomposition}, minmult, maxmult, branchpr = 1.0) @test BD.getformulation(ann) == F @test BD.getdecomposition(ann) == D @test BD.getlowermultiplicity(ann) == minmult @test BD.getuppermultiplicity(ann) == maxmult + @test BD.getbranchingpriority(ann) == branchpr id = BD.getid(ann) - @test repr(ann) == "Annotation($(F), $(D), lm = $(float(minmult)), um = $(float(maxmult)), id = $(id))" + @test repr(ann) == "Annotation($(F), $(D), lm = $(float(minmult)), um = $(float(maxmult)), bp = $(float(branchpr)), id = $(id))" return end