diff --git a/src/Algorithm/Algorithm.jl b/src/Algorithm/Algorithm.jl index 502454334..570c2d05f 100644 --- a/src/Algorithm/Algorithm.jl +++ b/src/Algorithm/Algorithm.jl @@ -46,6 +46,7 @@ include("branching/varbranching.jl") include("branching/branchingalgo.jl") include("treesearch.jl") +include("branchcutprice.jl") # Algorithm should export only methods usefull to define & parametrize algorithms, and # data structures from utilities. diff --git a/src/Algorithm/branchcutprice.jl b/src/Algorithm/branchcutprice.jl new file mode 100644 index 000000000..0e8ad2742 --- /dev/null +++ b/src/Algorithm/branchcutprice.jl @@ -0,0 +1,166 @@ +""" + + Coluna.Algorithm.BranchCutAndPriceAlgorithm(; + maxnumnodes::Int = 100000, + opt_atol::Float64 = Coluna.DEF_OPTIMALITY_ATOL, + opt_rtol::Float64 = Coluna.DEF_OPTIMALITY_RTOL, + restmastipheur_timelimit::Int = 600, + restmastipheur_frequency::Int = 1, + restmastipheur_maxdepth::Int = 1000, + max_nb_cut_rounds::Int = 3, + colgen_stabilization::Float64 = 0.0, + colgen_cleanup_threshold::Int = 10000, + colgen_stages_pricing_solvers::Vector{Int} = [1], + stbranch_phases_num_candidates::Vector{Int} = Int[], + stbranch_intrmphase_stages::Vector{NamedTuple{(:userstage, :solverid, :maxiters), Tuple{Int64, Int64, Int64}}} + ) + +Alias for a simplified parameterisation +of the branch-cut-and-price algorithm. + +Parameters : +- `maxnumnodes` : maximum number of nodes explored by the algorithm +- `opt_atol` : optimality absolute tolerance +- `opt_rtol` : optimality relative tolerance +- `restmastipheur_timelimit` : time limit in seconds for the restricted master heuristic + (if <= 0 then the heuristic is disabled) +- `restmastipheur_frequency` : frequency of calls to the restricted master heuristic +- `restmastipheur_maxdepth` : maximum depth of the search tree when the restricted master heuristic is called +- `max_nb_cut_rounds` : maximum number of cut generation rounds in every node of the search tree +- `colgen_stabilization` : parameterisation of the dual price smoothing stabilization of column generation + 0.0 - disabled, 1.0 - automatic, ∈(0.0,1.0) - fixed smoothing parameter +- `colgen_cleanup_threshold` : threshold (number of active columns) to trigger the restricted master LP clean up +- `colgen_stages_pricing_solvers` : vector of pricing solver ids for every column generation stage, + pricing solvers should be specified using argument `solver` of `BlockDecomposition.specify!()`, + the number of column generation stages is equal to the length of this vector, + column generation stages are executed in the reverse order, + the first stage should be exact to ensure the optimality of the BCP algorithm +- `stbranch_phases_num_candidates` : maximum number of candidates for each strong branching phase, + strong branching is activated if this vector is not empty, + the number of phases in strong branching is equal to min{3, length(stbranch_phases_num_candidates)}, + in the last phase, the standard column-and-cut generation procedure is run, + in the first phase (if their number is >= 2), only the restricted master LP is resolved, + in the second (intermediate) phase (if their number is >= 3), usually a heuristic pricing is used + or the number of column generation iterations is limited, this is parameterised with the three + next parameters, cut separation is not called in the intermediate strong branching phase, + if the lenth of this vector > 3, then all values except first, second, and last ones are ignored +- `stbranch_intrmphase_stages` : the size of this vector is the number of column generation stages in the intemediate phase of strong branching + each element of the vector is the named triple (userstage, solver, maxiters). "userstage" is the + value of "stage" parameter passed to the pricing callback on this stage, "solverid" is the solver id on this stage, + and "maxiters" is the maximum number of column generation iterations on this stage +""" + + + +function BranchCutAndPriceAlgorithm(; + maxnumnodes::Int = 100000, + branchingtreefile::Union{Nothing, String} = nothing, + opt_atol::Float64 = Coluna.DEF_OPTIMALITY_ATOL, + opt_rtol::Float64 = Coluna.DEF_OPTIMALITY_RTOL, + restmastipheur_timelimit::Int = 600, + restmastipheur_frequency::Int = 1, + restmastipheur_maxdepth::Int = 1000, + max_nb_cut_rounds::Int = 3, + colgen_stabilization::Float64 = 0.0, + colgen_cleanup_threshold::Int = 10000, + colgen_stages_pricing_solvers::Vector{Int64} = [1], + stbranch_phases_num_candidates::Vector{Int64} = Int[], + stbranch_intrmphase_stages::Vector{NamedTuple{(:userstage, :solverid, :maxiters), Tuple{Int64, Int64, Int64}}} = [(userstage=1, solverid=1, maxiters=100)] +) + heuristics = ParameterisedHeuristic[] + if restmastipheur_timelimit > 0 + heuristic = ParameterisedHeuristic( + SolveIpForm(moi_params = MoiOptimize( + get_dual_bound = false, + time_limit = restmastipheur_timelimit + )), + 1.0, 1.0, restmastipheur_frequency, + restmastipheur_maxdepth, "Restricted Master IP" + ) + push!(heuristics, heuristic) + end + + colgen_stages = ColumnGeneration[] + + for (stage, solver_id) in enumerate(colgen_stages_pricing_solvers) + colgen = ColumnGeneration( + pricing_prob_solve_alg = SolveIpForm( + optimizer_id = solver_id, + user_params = UserOptimize(stage = stage), + moi_params = MoiOptimize( + deactivate_artificial_vars = false, + enforce_integrality = false + ) + ), + smoothing_stabilization = colgen_stabilization, + cleanup_threshold = colgen_cleanup_threshold, + opt_atol = opt_atol, + opt_rtol = opt_rtol + ) + push!(colgen_stages, colgen) + end + + conquer = ColCutGenConquer( + stages = colgen_stages, + max_nb_cut_rounds = max_nb_cut_rounds, + primal_heuristics = heuristics, + opt_atol = opt_atol, + opt_rtol = opt_rtol + ) + + branching = NoBranching() + branching_rules = PrioritisedBranchingRule[PrioritisedBranchingRule(VarBranchingRule(), 1.0, 1.0)] + + if !isempty(stbranch_phases_num_candidates) + branching_phases = BranchingPhase[] + if length(stbranch_phases_num_candidates) >= 2 + push!(branching_phases, + BranchingPhase(first(stbranch_phases_num_candidates), RestrMasterLPConquer()) + ) + if length(stbranch_phases_num_candidates) >= 3 + intrmphase_stages = ColumnGeneration[] + for tuple in stbranch_intrmphase_stages + colgen = ColumnGeneration( + pricing_prob_solve_alg = SolveIpForm( + optimizer_id = tuple.solverid, + user_params = UserOptimize(stage = tuple.userstage), + moi_params = MoiOptimize( + deactivate_artificial_vars = false, + enforce_integrality = false + ) + ), + smoothing_stabilization = colgen_stabilization, + cleanup_threshold = colgen_cleanup_threshold, + max_nb_iterations = tuple.maxiters, + opt_atol = opt_atol, + opt_rtol = opt_rtol + ) + push!(intrmphase_stages, colgen) + end + intrmphase_conquer = ColCutGenConquer( + stages = intrmphase_stages, + max_nb_cut_rounds = 0, + primal_heuristics = [], + opt_atol = opt_atol, + opt_rtol = opt_rtol + ) + push!(branching_phases, + BranchingPhase(stbranch_phases_num_candidates[2], intrmphase_conquer) + ) + end + end + push!(branching_phases, BranchingPhase(last(stbranch_phases_num_candidates), conquer)) + branching = StrongBranching(rules = branching_rules, phases = branching_phases) + else + branching = StrongBranching(rules = branching_rules) + end + + return TreeSearchAlgorithm( + conqueralg = conquer, + dividealg = branching, + maxnumnodes = maxnumnodes, + branchingtreefile = branchingtreefile, + opt_atol = opt_atol; + opt_rtol = opt_rtol + ) +end \ No newline at end of file diff --git a/src/Algorithm/branching/varbranching.jl b/src/Algorithm/branching/varbranching.jl index 71d83fce9..5b94bbedc 100644 --- a/src/Algorithm/branching/varbranching.jl +++ b/src/Algorithm/branching/varbranching.jl @@ -83,13 +83,27 @@ function run!( !input.isoriginalsol && return BranchingRuleOutput(input.local_id, Vector{BranchingGroup}()) master = getmaster(reform) - groups = Vector{BranchingGroup}() local_id = input.local_id + max_priority = -Inf for (var_id, val) in input.solution # Do not consider continuous variables as branching candidates getperenkind(master, var_id) == Continuous && continue - getbranchingpriority(master, var_id) < input.minimum_priority && continue if !isinteger(val, input.int_tol) + brpriority = getbranchingpriority(master, var_id) + if max_priority < brpriority + max_priority = brpriority + end + end + end + + if max_priority == -Inf + return BranchingRuleOutput(local_id, BranchingGroup[]) + end + + groups = BranchingGroup[] + for (var_id, val) in input.solution + getperenkind(master, var_id) == Continuous && continue + if !isinteger(val, input.int_tol) && getbranchingpriority(master, var_id) == max_priority #description string is just the variable name candidate = VarBranchingCandidate(getname(master, var_id), var_id) local_id += 1 diff --git a/test/bound_callback_tests.jl b/test/bound_callback_tests.jl index 75d17c781..d856741e4 100644 --- a/test/bound_callback_tests.jl +++ b/test/bound_callback_tests.jl @@ -9,7 +9,7 @@ function bound_callback_tests() coluna = JuMP.optimizer_with_attributes( CL.Optimizer, "default_optimizer" => GLPK.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm(maxnumnodes = 2)) + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm(maxnumnodes = 2)) ) model, x, dec = CLD.GeneralizedAssignment.model_without_knp_constraints(data, coluna) diff --git a/test/full_instances_tests.jl b/test/full_instances_tests.jl index f6263011f..e9adab5bc 100644 --- a/test/full_instances_tests.jl +++ b/test/full_instances_tests.jl @@ -15,7 +15,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm( + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm( branchingtreefile = "playgap.dot" )), "default_optimizer" => GLPK.Optimizer @@ -39,7 +39,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -56,24 +56,15 @@ function generalized_assignment_tests() @testset "gap - strong branching" begin data = CLD.GeneralizedAssignment.data("mediumgapcuts3.txt") - conquer_with_small_cleanup_threshold = ClA.ColCutGenConquer( - stages = [ClA.ColumnGeneration(cleanup_threshold = 150, smoothing_stabilization = 1.0)] - ) - - branching = ClA.StrongBranching( - phases = [ClA.BranchingPhase(5, ClA.RestrMasterLPConquer()), - ClA.BranchingPhase(1, conquer_with_small_cleanup_threshold)], - rules = [ClA.PrioritisedBranchingRule(ClA.VarBranchingRule(), 2.0, 2.0), - ClA.PrioritisedBranchingRule(ClA.VarBranchingRule(), 1.0, 1.0)] - ) - coluna = JuMP.optimizer_with_attributes( CL.Optimizer, "params" => CL.Params( - solver = ClA.TreeSearchAlgorithm( - conqueralg = conquer_with_small_cleanup_threshold, - dividealg = branching, - maxnumnodes = 300 + solver = ClA.BranchCutAndPriceAlgorithm( + maxnumnodes = 300, + colgen_stabilization = 1.0, + colgen_cleanup_threshold = 150, + stbranch_phases_num_candidates = [10, 3, 1], + stbranch_intrmphase_stages = [(userstage=1, solverid=1, maxiters=2)] ) ), "default_optimizer" => GLPK.Optimizer @@ -105,7 +96,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( CL.Optimizer, "params" => CL.Params( - solver = ClA.TreeSearchAlgorithm( + solver = ClA.BranchCutAndPriceAlgorithm( maxnumnodes = 5 ) ), @@ -151,7 +142,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -166,7 +157,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -181,7 +172,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -197,7 +188,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -212,7 +203,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -250,10 +241,8 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( CL.Optimizer, "params" => CL.Params( - solver = ClA.TreeSearchAlgorithm( - conqueralg = ClA.ColCutGenConquer( - stages = [ClA.ColumnGeneration(smoothing_stabilization = 1.0)] - ), + solver = ClA.BranchCutAndPriceAlgorithm( + colgen_stabilization = 1.0, maxnumnodes = 300 ) ), @@ -273,7 +262,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -289,7 +278,7 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()) + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()) ) problem, x, dec = CLD.GeneralizedAssignment.model(data, coluna) @@ -308,8 +297,8 @@ function generalized_assignment_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm( - conqueralg = ClA.ColCutGenConquer(max_nb_cut_rounds = 1000) + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm( + max_nb_cut_rounds = 1000 )), "default_optimizer" => GLPK.Optimizer ) @@ -393,7 +382,7 @@ function capacitated_lot_sizing_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -428,7 +417,7 @@ function cutting_stock_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm()), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm()), "default_optimizer" => GLPK.Optimizer ) @@ -445,7 +434,7 @@ function cvrp_tests() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm( + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm( maxnumnodes = 10000, branchingtreefile = "cvrp.dot" )), diff --git a/test/optimizer_with_attributes_test.jl b/test/optimizer_with_attributes_test.jl index 6200e5ca9..f97f1d8d7 100644 --- a/test/optimizer_with_attributes_test.jl +++ b/test/optimizer_with_attributes_test.jl @@ -7,7 +7,7 @@ function optimizer_with_attributes_test() println(GLPK.Optimizer) coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm( + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm( branchingtreefile = "playgap.dot" )), "default_optimizer" => JuMP.optimizer_with_attributes(GLPK.Optimizer, "tm_lim" => 60 * 1_100, "msg_lev" => GLPK.GLP_MSG_OFF) diff --git a/test/pricing_callback_tests.jl b/test/pricing_callback_tests.jl index de2e442f5..18ad451a6 100644 --- a/test/pricing_callback_tests.jl +++ b/test/pricing_callback_tests.jl @@ -7,18 +7,8 @@ function pricing_callback_tests() CL.Optimizer, "default_optimizer" => GLPK.Optimizer, "params" => CL.Params( - solver = ClA.TreeSearchAlgorithm( - conqueralg = ClA.ColCutGenConquer( - stages = [ClA.ColumnGeneration( - pricing_prob_solve_alg = ClA.SolveIpForm( - optimizer_id=2, user_params = ClA.UserOptimize(stage=1) - )), - ClA.ColumnGeneration( - pricing_prob_solve_alg = ClA.SolveIpForm( - optimizer_id=2, user_params = ClA.UserOptimize(stage=2) - )) - ] - ) + solver = ClA.BranchCutAndPriceAlgorithm( + colgen_stages_pricing_solvers = [2, 2] ) ) ) diff --git a/test/subproblem_solvers_tests.jl b/test/subproblem_solvers_tests.jl index cf220993f..22cce261c 100644 --- a/test/subproblem_solvers_tests.jl +++ b/test/subproblem_solvers_tests.jl @@ -4,9 +4,7 @@ function subproblem_solvers_test() coluna = JuMP.optimizer_with_attributes( Coluna.Optimizer, - "params" => CL.Params(solver = ClA.TreeSearchAlgorithm( - conqueralg = ClA.ColCutGenConquer(max_nb_cut_rounds = 1000) - )), + "params" => CL.Params(solver = ClA.BranchCutAndPriceAlgorithm(max_nb_cut_rounds = 1000)), "default_optimizer" => GLPK.Optimizer )