diff --git a/src/Algorithm/Algorithm.jl b/src/Algorithm/Algorithm.jl index 17f1ea3f8..e40e8a4ce 100644 --- a/src/Algorithm/Algorithm.jl +++ b/src/Algorithm/Algorithm.jl @@ -49,7 +49,7 @@ include("divide.jl") # TODO: DivideInput and DivideOutput are already implementa include("branching/selectioncriteria.jl") include("branching/branchinggroup.jl") include("branching/branchingrule.jl") -include("branching/varbranching.jl") +include("branching/candidates.jl") include("branching/branchingalgo.jl") include("treesearch.jl") diff --git a/src/Algorithm/branching/branchingalgo.jl b/src/Algorithm/branching/branchingalgo.jl index 2ecae4543..7a7e14c6d 100644 --- a/src/Algorithm/branching/branchingalgo.jl +++ b/src/Algorithm/branching/branchingalgo.jl @@ -126,8 +126,8 @@ function exploits_primal_solutions(algo::StrongBranching) end function perform_strong_branching_with_phases!( - algo::StrongBranching, env::Env, reform::Reformulation, input::DivideInput, groups::Vector{BranchingGroup} -)::OptimizationState + algo::StrongBranching, env::Env, reform::Reformulation, input::DivideInput, candidates::Vector{C} +)::OptimizationState where {C<:AbstractBranchingCandidate} parent = getparent(input) exploitsprimalsolutions::Bool = exploits_primal_solutions(algo) @@ -144,12 +144,12 @@ function perform_strong_branching_with_phases!( # children for each branching candidate. if phase_index < length(algo.phases) nb_candidates_for_next_phase = algo.phases[phase_index + 1].max_nb_candidates - if phase_index > 1 && length(groups) <= nb_candidates_for_next_phase + if phase_index > 1 && length(candidates) <= nb_candidates_for_next_phase continue end # In phase 1, we make sure that the number of candidates for the next phase is # at least equal to the number of initial candidates - nb_candidates_for_next_phase = min(nb_candidates_for_next_phase, length(groups)) + nb_candidates_for_next_phase = min(nb_candidates_for_next_phase, length(candidates)) end conquer_units_to_restore = UnitsUsage() @@ -162,32 +162,35 @@ function perform_strong_branching_with_phases!( #for nice printing, we compute the maximum description length max_descr_length::Int64 = 0 - for group in groups - description = getdescription(group.candidate) + for candidate in candidates + description = getdescription(candidate) if (max_descr_length < length(description)) max_descr_length = length(description) end end - for (group_index,group) in enumerate(groups) + for (candidate_index, candidate) in enumerate(candidates) #TO DO: verify if time limit is reached + # TODO: start + if phase_index == 1 - generate_children!(group, env, reform, parent) + generate_children!(candidate, env, reform, parent) else - regenerate_children!(group, parent) + regenerate_children!(candidate, parent) end if phase_index > 1 - sort!(group.children, by = x -> get_lp_primal_bound(getoptstate(x))) + sort!(candidate.children, by = x -> get_lp_primal_bound(getoptstate(x))) end + # TODO: end # Here, we avoid the removal of pruned nodes at this point to let them # appear in the branching tree file - for (node_index, node) in enumerate(group.children) + for (node_index, node) in enumerate(candidate.children) if isverbose(current_phase.conquer_algo) print( "**** SB phase ", phase_index, " evaluation of candidate ", - group_index, " (branch ", node_index, " : ", node.branchdescription + candidate_index, " (branch ", node_index, " : ", node.branchdescription ) @printf "), value = %6.2f\n" getvalue(get_lp_primal_bound(getoptstate(node))) end @@ -215,28 +218,28 @@ function perform_strong_branching_with_phases!( if phase_index < length(algo.phases) # not the last phase, thus we compute the product score - group.score = product_score(group, getoptstate(parent)) + candidate.score = product_score(candidate.children, getoptstate(parent)) else # the last phase, thus we compute the tree size score - group.score = tree_depth_score(group, getoptstate(parent)) + candidate.score = tree_depth_score(candidate.children, getoptstate(parent)) end - print_bounds_and_score(group, phase_index, max_descr_length) + print_bounds_and_score(candidate, phase_index, max_descr_length) end - sort!(groups, rev = true, by = x -> (x.isconquered, x.score)) + sort!(candidates, rev = true, by = x -> (x.isconquered, x.score)) - if groups[1].isconquered + if candidates[1].isconquered nb_candidates_for_next_phase = 1 end # before deleting branching groups which are not kept for the next phase # we need to remove record kept in these nodes - for group_index = nb_candidates_for_next_phase + 1 : length(groups) - for (node_index, node) in enumerate(groups[group_index].children) + for candidate_index = nb_candidates_for_next_phase + 1 : length(candidates) + for (node_index, node) in enumerate(candidates[candidate_index].children) remove_records!(node.recordids) end end - resize!(groups, nb_candidates_for_next_phase) + resize!(candidates, nb_candidates_for_next_phase) end return sbstate end @@ -246,7 +249,7 @@ end # - stopping criterion # - what happens when original_solution or extended_solution are nothing function _select_candidates_with_branching_rule(rules, phases, selection_criterion, int_tol, parent_is_root, reform, env, original_solution, extended_solution) - kept_branch_groups = BranchingGroup[] + kept_branch_candidates = AbstractBranchingCandidate[] # We sort branching rules by their root/non-root priority. sorted_rules = sort(rules, rev = true, by = x -> getpriority(x, parent_is_root)) @@ -270,7 +273,7 @@ function _select_candidates_with_branching_rule(rules, phases, selection_criteri # Priority of the current branching rule. priority = getpriority(prioritised_rule, parent_is_root) - nb_candidates_found = length(kept_branch_groups) + nb_candidates_found = length(kept_branch_candidates) # Before selecting new candidates with the current branching rule, check if generation # of candidates stops. Generation of candidates stops when: @@ -295,7 +298,7 @@ function _select_candidates_with_branching_rule(rules, phases, selection_criteri local_id, int_tol, priority ) ) - append!(kept_branch_groups, output.groups) + append!(kept_branch_candidates, output.groups) local_id = output.local_id if projection_is_possible(getmaster(reform)) && !isnothing(extended_solution) @@ -305,15 +308,13 @@ function _select_candidates_with_branching_rule(rules, phases, selection_criteri local_id, int_tol, priority ) ) - append!(kept_branch_groups, output.groups) + append!(kept_branch_candidates, output.groups) local_id = output.local_id end - - select_candidates!(kept_branch_groups, selection_criterion, max_nb_candidates) - + select_candidates!(kept_branch_candidates, selection_criterion, max_nb_candidates) priority_of_last_generated_groups = priority end - return kept_branch_groups + return kept_branch_candidates end function run!(algo::StrongBranching, env::Env, reform::Reformulation, input::DivideInput)::DivideOutput @@ -341,21 +342,21 @@ function run!(algo::StrongBranching, env::Env, reform::Reformulation, input::Div end parent_is_root = iszero(getdepth(parent)) - kept_branch_groups = _select_candidates_with_branching_rule( + kept_branch_candidates = _select_candidates_with_branching_rule( algo.rules, algo.phases, algo.selection_criterion, algo.int_tol, parent_is_root, reform, env, original_solution, extended_solution ) - if isempty(kept_branch_groups) + if isempty(kept_branch_candidates) @logmsg LogLevel(0) "No branching candidates found. No children will be generated." return DivideOutput(Node[], optstate) end # in the case of simple branching, it remains to generate the children if isempty(algo.phases) - generate_children!(kept_branch_groups[1], env, reform, parent) - return DivideOutput(kept_branch_groups[1].children, OptimizationState(getmaster(reform))) + children = generate_children!(kept_branch_candidates[1], env, reform, parent) + return DivideOutput(children, OptimizationState(getmaster(reform))) end - sbstate = perform_strong_branching_with_phases!(algo, env, reform, input, kept_branch_groups) - return DivideOutput(kept_branch_groups[1].children, sbstate) + sbstate = perform_strong_branching_with_phases!(algo, env, reform, input, kept_branch_candidates) + return DivideOutput(kept_branch_candidates[1].children, sbstate) end diff --git a/src/Algorithm/branching/branchinggroup.jl b/src/Algorithm/branching/branchinggroup.jl index 24efc2bee..0e2c39dd9 100644 --- a/src/Algorithm/branching/branchinggroup.jl +++ b/src/Algorithm/branching/branchinggroup.jl @@ -3,42 +3,16 @@ ############################################################################################ -function BranchingGroup( - candidate::AbstractBranchingCandidate, local_id::Int64, lhs::Float64 -) - return BranchingGroup(candidate, local_id, lhs, SbNode[], false, typemin(Float64)) -end - -get_lhs_distance_to_integer(group::BranchingGroup) = - min(group.lhs - floor(group.lhs), ceil(group.lhs) - group.lhs) - -function generate_children!( - group::BranchingGroup, env::Env, reform::Reformulation, parent::Node -) - group.children = generate_children(group.candidate, group.lhs, env, reform, parent) - return -end - -# TODO : it does not look like a regeneration but more like a new vector where we -# reassign children -function regenerate_children!(group::BranchingGroup, parent::Node) - new_children = SbNode[] - for child in group.children - push!(new_children, SbNode(parent, child)) - end - group.children = new_children - return -end - # TODO : this method needs code documentation & context -function product_score(group::BranchingGroup, parent_optstate::OptimizationState) +# TODO : unit tests +function product_score(children::Vector{SbNode}, parent_optstate::OptimizationState) # TO DO : we need to mesure the gap to the cut-off value parent_lp_dual_bound = get_lp_dual_bound(parent_optstate) parent_delta = diff(get_ip_primal_bound(parent_optstate), parent_lp_dual_bound) all_branches_above_delta = true - deltas = zeros(Float64, length(group.children)) - for (i, node) in enumerate(group.children) + deltas = zeros(Float64, length(children)) + for (i, node) in enumerate(children) node_delta = diff(get_lp_primal_bound(getoptstate(node)), parent_lp_dual_bound) if node_delta < parent_delta all_branches_above_delta = false @@ -69,6 +43,7 @@ function product_score(group::BranchingGroup, parent_optstate::OptimizationState end # TODO : this method needs code documentation & context +# TODO ; unit tests function number_of_leaves(gap::Float64, deltas::Vector{Float64}) inf::Float64 = 0.0 sup::Float64 = 1e20 @@ -95,8 +70,9 @@ function number_of_leaves(gap::Float64, deltas::Vector{Float64}) end # TODO : this method needs code documentation & context -function tree_depth_score(group::BranchingGroup, parent_optstate::OptimizationState) - if length(group.children) == 0 +# TODO : this method needs unit tests +function tree_depth_score(children::Vector{SbNode}, parent_optstate::OptimizationState) + if iszero(length(children)) return 0.0 end @@ -104,9 +80,9 @@ function tree_depth_score(group::BranchingGroup, parent_optstate::OptimizationSt parent_lp_dual_bound = get_lp_dual_bound(parent_optstate) parent_delta = diff(get_ip_primal_bound(parent_optstate), parent_lp_dual_bound) - deltas = zeros(Float64, length(group.children)) + deltas = zeros(Float64, length(children)) nb_zero_deltas = 0 - for (i, node) in enumerate(group.children) + for (i, node) in enumerate(children) node_delta = diff(get_lp_primal_bound(getoptstate(node)), parent_lp_dual_bound) if node_delta < 1e-6 # TO DO : use tolerance here nb_zero_deltas += 1 @@ -134,16 +110,3 @@ function tree_depth_score(group::BranchingGroup, parent_optstate::OptimizationSt end return score end - -function print_bounds_and_score(group::BranchingGroup, phase_index::Int64, max_description_length::Int64) - lengthdiff = max_description_length - length(getdescription(group.candidate)) - print("SB phase ", phase_index, " branch on ", getdescription(group.candidate)) - @printf " (lhs=%.4f)" group.lhs - print(repeat(" ", lengthdiff), " : [") - for (node_index, node) in enumerate(group.children) - node_index > 1 && print(",") - @printf "%10.4f" getvalue(get_lp_primal_bound(getoptstate(node))) - end - @printf "], score = %10.4f\n" group.score - return -end diff --git a/src/Algorithm/branching/branchingrule.jl b/src/Algorithm/branching/branchingrule.jl index 45e397403..a37c93732 100644 --- a/src/Algorithm/branching/branchingrule.jl +++ b/src/Algorithm/branching/branchingrule.jl @@ -23,7 +23,7 @@ function run!( )::BranchingRuleOutput # variable branching works only for the original solution if !input.isoriginalsol - return BranchingRuleOutput(input.local_id, BranchingGroup[]) + return BranchingRuleOutput(input.local_id, []) end master = getmaster(reform) @@ -43,19 +43,19 @@ function run!( end if max_priority == -Inf - return BranchingRuleOutput(local_id, BranchingGroup[]) + return BranchingRuleOutput(local_id, []) end - groups = BranchingGroup[] + groups = SingleVarBranchingCandidate[] for (var_id, val) in input.solution continuous_var = getperenkind(master, var_id) == Continuous int_val = abs(round(val) - val) < input.int_tol br_priority = getbranchingpriority(master, var_id) if !continuous_var && !int_val && br_priority == max_priority # Description string of the candidate is the variable name - candidate = SingleVarBranchingCandidate(getname(master, var_id), var_id) local_id += 1 - push!(groups, BranchingGroup(candidate, local_id, val)) + candidate = SingleVarBranchingCandidate(getname(master, var_id), var_id, local_id, val) + push!(groups, candidate) end end diff --git a/src/Algorithm/branching/varbranching.jl b/src/Algorithm/branching/candidates.jl similarity index 53% rename from src/Algorithm/branching/varbranching.jl rename to src/Algorithm/branching/candidates.jl index ce4ab0816..d7fea0d11 100644 --- a/src/Algorithm/branching/varbranching.jl +++ b/src/Algorithm/branching/candidates.jl @@ -8,18 +8,48 @@ It is an implementation of AbstractBranchingCandidate. This is the type of branching candidates produced by the branching rule `SingleVarBranchingRule`. """ -struct SingleVarBranchingCandidate <: AbstractBranchingCandidate +mutable struct SingleVarBranchingCandidate <: AbstractBranchingCandidate varname::String varid::VarId + local_id::Int64 + lhs::Float64 + score::Float64 + children::Vector{SbNode} + isconquered::Bool + function SingleVarBranchingCandidate(varname::String, varid::VarId, local_id::Int64, lhs::Float64) + return new(varname, varid, local_id, lhs, 0.0, SbNode[], false) + end end getdescription(candidate::SingleVarBranchingCandidate) = candidate.varname -function generate_children( - candidate::SingleVarBranchingCandidate, lhs::Float64, env::Env, reform::Reformulation, +get_lhs(candidate::SingleVarBranchingCandidate) = candidate.lhs + +function get_lhs_distance_to_integer(candidate::SingleVarBranchingCandidate) + lhs = get_lhs(candidate) + return min(lhs - floor(lhs), ceil(lhs) - lhs) +end + +get_local_id(candidate::SingleVarBranchingCandidate) = candidate.local_id + + +# TODO : it does not look like a regeneration but more like a new vector where we +# reassign children +function regenerate_children!(candidate::SingleVarBranchingCandidate, parent::Node) + new_children = SbNode[] + for child in candidate.children + push!(new_children, SbNode(parent, child)) + end + candidate.children = new_children + return +end + +function generate_children!( + candidate::SingleVarBranchingCandidate, env::Env, reform::Reformulation, parent::Node ) master = getmaster(reform) + lhs = get_lhs(candidate) @logmsg LogLevel(-1) string( "Chosen branching variable : ", @@ -35,7 +65,6 @@ function generate_children( # adding the first branching constraints restore_from_records!(units_to_restore, copy_records(parent.recordids)) - TO.@timeit Coluna._to "Add branching constraint" begin setconstr!( master, string( "branch_geq_", getdepth(parent), "_", getname(master,candidate.varid) @@ -43,13 +72,11 @@ function generate_children( sense = Greater, rhs = ceil(lhs), loc_art_var_abs_cost = env.params.local_art_var_cost, members = Dict{VarId,Float64}(candidate.varid => 1.0) ) - end child1description = candidate.varname * ">=" * string(ceil(lhs)) child1 = SbNode(master, parent, child1description, store_records!(reform)) # adding the second branching constraints restore_from_records!(units_to_restore, copy_records(parent.recordids)) - TO.@timeit Coluna._to "Add branching constraint" begin setconstr!( master, string( "branch_leq_", getdepth(parent), "_", getname(master,candidate.varid) @@ -58,10 +85,23 @@ function generate_children( loc_art_var_abs_cost = env.params.local_art_var_cost, members = Dict{VarId,Float64}(candidate.varid => 1.0) ) - end child2description = candidate.varname * "<=" * string(floor(lhs)) child2 = SbNode(master, parent, child2description, store_records!(reform)) + candidate.children = [child1, child2] # TODO: remove. return [child1, child2] end +function print_bounds_and_score(candidate::SingleVarBranchingCandidate, phase_index::Int64, max_description_length::Int64) + lhs = get_lhs(candidate) + lengthdiff = max_description_length - length(getdescription(candidate)) + print("SB phase ", phase_index, " branch on ", getdescription(candidate)) + @printf " (lhs=%.4f)" lhs + print(repeat(" ", lengthdiff), " : [") + for (node_index, node) in enumerate(candidate.children) + node_index > 1 && print(",") + @printf "%10.4f" getvalue(get_lp_primal_bound(getoptstate(node))) + end + @printf "], score = %10.4f\n" candidate.score + return +end diff --git a/src/Algorithm/branching/interface.jl b/src/Algorithm/branching/interface.jl index 7ceba02ff..66475840d 100644 --- a/src/Algorithm/branching/interface.jl +++ b/src/Algorithm/branching/interface.jl @@ -9,6 +9,10 @@ getdescription(candidate::AbstractBranchingCandidate) = error("getdescription not defined for branching candidates of type $(typeof(candidate)).") +get_lhs(::AbstractBranchingCandidate) = nothing +get_lhs_distance_to_integer(::AbstractBranchingCandidate) = nothing +get_local_id(::AbstractBranchingCandidate) = nothing + # TODO: this method should not generate the children of the tree search algorithm. # However, AbstractBranchingCandidate should implement an interface to retrieve data to # generate a children. @@ -21,34 +25,6 @@ generate_children!( candidate::AbstractBranchingCandidate, ::Float64, ::Env, ::Reformulation, ::Node ) = error("generate_children not defined for branching candidates of type $(typeof(candidate)).") - -############################################################################################ -############################################################################################ -############################################################################################ -# TODO: need to see if this data struct must be part of the interface. -# I think this data structure should be replaced by an interface that will be implemented -# by AbstractBranchingCandidate. -############################################################################################ -############################################################################################ -############################################################################################ -""" -A branching group is the union of a branching candidate and additional information that are -computed during the execution of the branching algorithm (TODO : which one ?). -""" -mutable struct BranchingGroup - candidate::AbstractBranchingCandidate # the left-hand side in general. - local_id::Int64 - lhs::Float64 - children::Vector#{SbNode} - isconquered::Bool - score::Float64 -end -############################################################################################ -############################################################################################ -############################################################################################ -############################################################################################ -############################################################################################ - ############################################################################################ # Selection Criteria of branching candidates ############################################################################################ @@ -66,7 +42,7 @@ abstract type AbstractSelectionCriterion end Sort branching candidates according to the selection criterion and remove excess ones. """ -select_candidates!(::Vector{BranchingGroup}, selection::AbstractSelectionCriterion, ::Int) = +select_candidates!(::Vector{C}, selection::AbstractSelectionCriterion, ::Int) where {C <: AbstractBranchingCandidate} = error("select_candidates! not defined for branching selection rule $(typeof(selection)).") @@ -95,7 +71,7 @@ It contains the branching candidates generated and the updated local id value """ struct BranchingRuleOutput <: AbstractOutput local_id::Int64 - groups::Vector{BranchingGroup} + groups::Vector{AbstractBranchingCandidate} end abstract type AbstractBranchingRule <: AbstractAlgorithm end diff --git a/src/Algorithm/branching/selectioncriteria.jl b/src/Algorithm/branching/selectioncriteria.jl index 3e5efaeec..0cec89880 100644 --- a/src/Algorithm/branching/selectioncriteria.jl +++ b/src/Algorithm/branching/selectioncriteria.jl @@ -6,9 +6,9 @@ Select the branching candidates that have been generated first (sort by `local_i struct FirstFoundCriterion <: AbstractSelectionCriterion end function select_candidates!( - candidates::Vector{BranchingGroup}, ::FirstFoundCriterion, max_nb_candidates::Int -) - sort!(candidates, by = x -> x.local_id) + candidates::Vector{C}, ::FirstFoundCriterion, max_nb_candidates::Int +) where {C <: AbstractBranchingCandidate} + sort!(candidates, by = c -> get_local_id(c)) if length(candidates) > max_nb_candidates resize!(candidates, max_nb_candidates) end @@ -21,13 +21,10 @@ Select the most fractional branching candidates. """ struct MostFractionalCriterion <: AbstractSelectionCriterion end -_get_lhs_distance_to_integer(group::BranchingGroup) = - min(group.lhs - floor(group.lhs), ceil(group.lhs) - group.lhs) - function select_candidates!( - candidates::Vector{BranchingGroup}, ::MostFractionalCriterion, max_nb_candidates::Int -) - sort!(candidates, rev = true, by = x -> _get_lhs_distance_to_integer(x)) + candidates::Vector{C}, ::MostFractionalCriterion, max_nb_candidates::Int +) where {C <: AbstractBranchingCandidate} + sort!(candidates, rev = true, by = c -> get_lhs_distance_to_integer(c)) if length(candidates) > max_nb_candidates resize!(candidates, max_nb_candidates) end diff --git a/test/runtests.jl b/test/runtests.jl index 64b192b4f..6a539b8be 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -5,4 +5,4 @@ include("ColunaTests.jl") retest(Coluna, ColunaTests) # Run a specific test: -#retest(ColunaTests, "small instance") \ No newline at end of file +# retest(ColunaTests, "Capacitated Vehicle Routing") \ No newline at end of file