def select_best_candidate(branching_candidates,initial_instance): # heuristics to select "best" candidate # we want to avoid having too many integer branchings to avoid too great calculation time. # first ranking factor : minimum boost for candidate in branching_candidates: log.write_awaiting_answer(candidate["method"]+":" ) for instance in candidate["instances"]: log.write_awaiting_answer(str(instance.id[-1])+", boost : "+str(instance.objective_value-initial_instance.objective_value)) log.write_answer("") if len(branching_candidates)==1: return branching_candidates[0] for candidate in branching_candidates: for inst in candidate["instances"]: if inst.objective_value-initial_instance.objective_value<precision: branching_candidates.remove(candidate) break if len(branching_candidates)==1: return branching_candidates[0] clear_winner_thresh = 2 min_boosts = {branching_candidate["method"]:(min( branching_candidate["instances"], key = lambda instance : instance.objective_value - initial_instance.objective_value ).objective_value-initial_instance.objective_value) for branching_candidate in branching_candidates} branching_candidates = sorted( branching_candidates, key = lambda candidate : min_boosts[candidate["method"]], reverse = True ) #sorts them in decreasing order of smallest boost # we have at least 2 try: if min_boosts[branching_candidates[0]["method"]] / min_boosts[branching_candidates[1]["method"]] >= clear_winner_thresh: log.write("biggest smallest boost is clear winner") return branching_candidates[0] # if we get here, the two "best" candidates are not distinguishable by their smallest boost ("best" as in biggest smallest boost) if len(branching_candidates) == 3 and min_boosts[branching_candidates[2]["method"]] / min_boosts[branching_candidates[0]["method"]] >= clear_winner_thresh: # checking if the last candidate holds a chance branching_candidates.pop(2) except ZeroDivisionError: raise Exception(str([inst.objective_value for inst in branching_candidates[0]["instances"]]) + " " + str([inst.objective_value for inst in branching_candidates[1]["instances"]]) + " " + str([inst.objective_value for inst in branching_candidates[2]["instances"]]) + " " + str(initial_instance.objective_value) ) # at this point, all remaining candidates have a similar smallest boost, other factors must be taken into account # second class ranking factors : number of instances created and their respective boost # we like having only one instance, but that considerably slows down solving process (integer fixing): we must find a trade-off branching_candidates = sorted( branching_candidates, key = lambda candidate : len(candidate["instances"]) ) for candidate in branching_candidates: if len(candidate["instances"])==1 and candidate["method"]!= "integer branching": log.write("not intger branching with cut branches wins") return candidate # smallest amount of instances without slowing down solving time: perfect! (happens for instance when index branching has one of its branches unfeasible) m = sum( 1 for b in initial_instance.id if b==-1 ) # number of fixed integers if m<=6 and random()>0.6: #we can still fix integers without too much trouble for candidate in branching_candidates: if len(candidate["instances"])==1: log.write("not too many integer branching") return candidate # this will always only return integer_branching if it is still in the remaining candidates # now we will simply assess if the remaining candidates have highly unbalanced instances, which is good. If so, we choose them, else we will simply take that with least amount of instances highly_unbalanced_candidates = [] for candidate in branching_candidates: if len(candidate["instances"])>1 and max( (abs(candidate["instances"][j].objective_value-initial_instance.objective_value))/max((candidate["instances"][i].objective_value-initial_instance.objective_value),0.00001) for i in range(len(candidate["instances"])) for j in range(len(candidate["instances"])) if i!=j ) > 10 : highly_unbalanced_candidates.append(candidate) if len(highly_unbalanced_candidates)==1: log.write("unique highly unbalanced is winner") return highly_unbalanced_candidates[0] if len(highly_unbalanced_candidates)>=2: #can only be equal to 1 or 2 log.write("smallest highly unbalanced is winner") return min( highly_unbalanced_candidates, key = lambda candidate : len(candidate["instances"]) ) log.write("smallest balanced is winner") return branching_candidates[0]
def branch(instance,instance_manager): #branches instance problem into complementary sub problem instances following various rules #adds new instances that have been correctly initialised to instance_manager #returns success of branching log.subtitle("entering branching",instance.id) initial_time = time() branching_candidates = [] if index_branching: partial_time = time() log.write_awaiting_answer("index branching--> ") candidate = index_branching_candidates(instance) if candidate["instances"] != None: branching_candidates.append(candidate) log.write_answer(", score: "+str(branching_candidates[-1]["score"])+", time: "+str(round(time()-partial_time,2))) log.write_answer(", score: "+str(branching_candidates[-1]["score"])+", time: "+str(round(time()-partial_time,2))) if integer_branching: partial_time = time() log.write_awaiting_answer("integer branching--> ") candidate = integer_branching_candidates(instance) if candidate["instances"] != None: branching_candidates.append(candidate) log.write(", score: "+str(branching_candidates[-1]["score"])+", time: "+str(round(time()-partial_time,2))) if cpp_constraint_branching: partial_time = time() log.write_awaiting_answer("c++ constraint branching--> ") candidate = cpp_constraint_branching_candidate(instance) if candidate["instances"] != None: branching_candidates.append(candidate) log.write(", score: "+str(branching_candidates[-1]["score"])+", time: "+str(round(time()-partial_time,2))) if branching_candidates==[]: return False #coefficients = {"integer branching":1.1,"constraint branching":0.8,"index branching":1} #the bigger the coefficient, the lyklier it will be selected #best_candidate = sorted( branching_candidates, key = lambda candidate : candidate["score"]*coefficients[candidate["method"]], reverse=True )[0] best_candidate = select_best_candidate(branching_candidates,instance) log.write("BEST STRATEGY: "+str(best_candidate["method"])) #adding new instances to solving queue for instance in best_candidate["instances"]: set_lower_bound(instance) log.write("value of objective function is " +str(round(instance.objective_value,2)) + " for new instance " +list_to_string(instance.id)+" with lower bound "+str(instance.lower_bound)[:-1]) instance_manager.add(instance) log.end_subtitle("end of branching, method: "+best_candidate["method"]+", smallest boost: "+str(best_candidate["score"]) +", time: "+str(round(time()-initial_time,2))+" seconds") return True
def cpp_constraint_branching_candidate(instance,branching_strategy=constraint_branching_strategy): NoOfCustomers,Demand,CAP,NoOfEdges,EdgeX,EdgeHead,EdgeTail,QMin = cpp.convert_for_cvrpsep(instance) sets = cpp.cvrpsep.branching(NoOfCustomers,Demand,CAP,NoOfEdges,EdgeX,EdgeHead,EdgeTail) sets = order_best_sets(sets,instance) if sets == []: log.write_answer("no branching set were found") return {"instances":None,"score":None, "method":"constraint branching"} log.write_awaiting_answer("candidate sets: "+str(len(sets))) if branching_strategy == "simple": branch_set = sets[0] instance.branching_sets.append(branch_set) log.write_awaiting_answer(", best set: "+str(branch_set)+", demand: "+str(round(sum(instance.demands[i] for i in branch_set)/instance.capacity.value,4))+", coboundary: "+str(coboundary(branch_set,instance))) candidate = branch_over_constraint(branch_set,instance) if candidate == []: return {"instances":None,"score":None, "method":"constraint branching"} increment_depth_and_id(candidate,"constraint branching") return {"instances":candidate,"score":candidate_score(candidate,instance.objective_value), "method":"constraint branching"} if branching_strategy == "exhaustive": branch_sets = sets[0:max_constraint_branching_candidates] best_candidate,best_set,best_score = [],0,0 for branch_set in branch_sets: candidate = branch_over_constraint(branch_set,instance) score = candidate_score(candidate,instance.objective_value) if score > best_score: best_candidate,best_set,best_score = candidate,branch_set,score else: for inst in candidate: del(inst) if best_candidate == []: log.write_answer("no branching over constraint could be found") return {"instances":None,"score":None, "method":"constraint branching"} for inst in best_candidate: inst.branching_sets.append(best_set) increment_depth_and_id(best_candidate,"constraint branching") log.write_awaiting_answer("branching set: "+str(best_set)+", demand: "+str(round(sum(instance.demands[i] for i in branch_set)/instance.capacity.value,4))+", coboundary: "+str(coboundary(best_set,instance))) return {"instances":best_candidate, "score":best_score, "method":"constraint branching"} raise NameError("enter a valid constraint_branching_strategy")
def integer_branching_candidates(instance,strategy = integer_branching_strategy): # returns candidates for integer_branching indexes = order_best_indexes(instance) if len(indexes)==0: return {"instances":None,"score":None, "method":"integer branching"} if strategy == "simple": index = indexes[0] log.write_awaiting_answer("branching index: "+str(index)+", value "+str(round(instance.x[index].value,4))) instance0 = instance.clone() instance0.branching_indexes.append(index) instance0.x[index].domain = pyo.NonNegativeIntegers if not(solve(instance0,silent=True)): return {"instances":None,"score":None, "method":"integer branching"} candidate = [instance0] score = candidate_score(candidate,instance.objective_value) increment_depth_and_id(candidate,"integer branching") return {"instances":candidate, "score":score, "method":"integer branching"} if strategy == "exhaustive": candidate_indexes = indexes[:max_index_branching_candidates] best_score,best_instance,best_index = 0,None, (-1,-1) for ind in candidate_indexes: instance0 = instance.clone() instance0.x[ind].domain = pyo.NonNegativeIntegers if not(solve(instance0,silent=True)): continue score = candidate_score([instance0],instance.objective_value) if score > best_score: best_score,best_instance,best_index = score,instance0.clone(),ind del(instance0) if best_index == (-1,-1): #should never happen log.write_answer("no branching index found, branch must be cut") return {"instances":None,"score":None, "method":"integer branching"} log.write_awaiting_answer("branching index: "+str(best_index)+", value "+str(round(instance.x[best_index].value,4))) best_instance.branching_indexes.append(best_index) increment_depth_and_id([best_instance],"integer branching") return {"instances":[best_instance],"score":best_score, "method":"integer branching"}
def column_generation(instance, instance_manager): # general method that will apply all available cut finding algorithms in a loop whose parameters are defined in globals.py log.subtitle("entering column generation", instance.id) initial_time, feasible_integer_found, loop_count, unmoving_count, solver_success, failure_count, obj_val_old, obj_val_old_global = round( time(), 2 ), False, 1, 0, True, 0, instance.objective_value, instance.objective_value while instance.objective_value < instance_manager.upper_bound and loop_count <= ( max_column_generation_count if len(instance.id) > 1 else max_column_generation_count_first_instance) and unmoving_count <= ( max_unmoving_count if len(instance.id) > 1 else max_unmoving_count_first_instance) and not ( feasible_integer_found) and solver_success: log.write_awaiting_answer("loop " + str(loop_count) + "--> ", instance.id) # adding cuts feasible_integer_found, cuts_found = column_generation_loop( instance, instance_manager) #solving updated instance if cuts_found: solver_success = managing.solve(instance) #different cases are verified # we verify if the adding cuts method is a failure if not (cuts_found) and not (feasible_integer_found): log.write_awaiting_answer("all heurisitcs have failed") if not (feasible_integer_found): log.write_answer("improvement: " + str( round( 100 * (instance.objective_value - obj_val_old) / obj_val_old, 5)) + '%' + ", objective: " + str(round(instance.objective_value, 5)) + ", " + str(managing.integer_percent(instance)) + '%' + " integer, unmoving count:" + str(unmoving_count) + ", reduction: " + str(instance.reduction)) # we verify if the objective value is increasing, if not : the cuts found have no effect if (instance.objective_value - obj_val_old) / obj_val_old < unmoving_constraint_threshold: unmoving_count += 1 if unmoving_count >= (max_unmoving_count if instance.depth > 0 else max_unmoving_count_first_instance): log.write( "no evolution in column_generation, moving on to branching", instance.id) break else: unmoving_count = 0 # we verifiy if the objective value is decreasing : if it is, we have probably dropped constraints that were usefull if (instance.objective_value - obj_val_old) / obj_val_old < -0.0001: log.write( "we are losing objective function value! useful constraints were dropped", instance.id) # we count the number of consecutive total failure if not (cuts_found): failure_count += 1 if failure_count > max_failure_count_cuts: log.write( "no evolution in column_generation, moving on to branching", instance.id) break else: failure_count = 0 obj_val_old = instance.objective_value loop_count += 1 log.end_subtitle( "end of column generation, gain " + str(instance.objective_value - obj_val_old_global) + ", objective: " + str(instance.objective_value) + ", time: " + str(round(time() - initial_time, 2)), instance.id) return feasible_integer_found, solver_success