def column_generation_loop(instance, instance_manager): # a signle loop of column generation : adds all available types of cuts to the instance # also verifies if a feasible integer solution is found #0) check if solution is integer and has valid paths if managing.solution_is_integer(instance): instance0 = instance.clone() managing.integerize_solution(instance0) if paths_are_feasible(instance0): log.subtitle( "!!!!! feasible integer solution found with objective value of " + str(round(instance0.objective_value, 2)), instance0.id) assertion = " " if instance_manager.upper_bound > instance0.objective_value else " not " log.write( "new solution is" + assertion + "better than one previously found, this branch is dropped and its solution is" + assertion + "recorded") instance_manager.record_feasible_integer_solution(instance0) return True, False if column_input == "python": raise Exception( "hand implemented heuristics have been commented out in cap_constraint.py for more clarity in code reading, these methods are outdated in comparaison to the c++ library. If you whish to test them, uncomment them and comment this exception out" ) #1) adding capacity cuts #success, count = cap.add_c_cap(instance) #log.write_awaiting_answer("capacity cuts: " +str(count)) #2),3),4) ... # other types of cuts to be added elif column_input == "c++": success = cpp.generate_and_add_columns(instance) else: raise NameError("Enter a valide column input type") remove_inactive_constraints(instance) return False, success
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 full_graph(instance, locations, status, bypass=False, show_only=False): if graphing or bypass: log.write("saving " + status + " solution to " + log.name + "_" + status + "_solution_graph.png in current folder") g = Graph(instance, locations) g.update_with_x(instance.x) fig = plt.figure(figsize=(15, 15)) plt.axis('off') plt.title("graph of " + status + " solution found for CVRP solving with \n" + str(instance.n.value) + " nodes, " + str(instance.number_of_vehicles.value) + " vehicles with capacity of " + str(instance.capacity.value)) g.show() # fig.show() if show_only: fig.show() return fig.savefig(log.name + "/" + status + "_solution_graph.png", bbox_inches='tight', dpi=fig.dpi * 2) log.write_timed("finished saving graph") plt.close(fig) del (fig)
def initialize_instance(instance): # takes an instsanciated pyomo concrete model and adds all the custom attributes that will be used later in the process # among others things it will : define the variables, add the constraint containers, delete the variables that are not usefull (we only keep the strictly inferior trianble of the X matrix), # delete the locations matrix for it is not necessary, we keep it as a seperate variable that will be used only by the graphing functions if instance.entry_type.value == "COORD": ''' we need to retrieve the locations and delete them from all instances for smaller size''' locations = to_list_locations( instance) #global parameter used for graphing of solutions del (instance.locations) # WORKS! else: #entry type is necessarily "WEIGHT" ''' we need to generate random coordinates for the plotting ''' locations = [(random() * 20, random() * 20) for i in range(instance.n.value)] max_cost = 0 for k in instance.costs.values(): if k > max_cost: max_cost = k instance.max_cost = max_cost #managing.normalize_costs(instance) ''' define variable x ''' instance.x = pyo.Var(instance.nodes, instance.nodes, bounds=set_bounds) #deleting uneused variables (i<=j) for i in instance.nodes: for j in instance.nodes: if i <= j: del (instance.x[i, j]) # instance.x[i,j].deactivate() ''' define flow variable ''' instance.flow = pyo.Var(instance.nodes, instance.nodes, bounds=set_bounds_flow) ''' define the objective function ''' instance.objective = pyo.Objective(expr=sum( instance.costs[i, j] * instance.x[i, j] for i in instance.nodes for j in instance.nodes if i > j)) ''' define degree constraints ''' instance.c_deg = pyo.Constraint(instance.nodes, rule=rule_deg) ''' define flow constraints ''' if flow_constraints: instance.c_flow = pyo.Constraint(instance.nodes, rule=rule_flow) instance.c_flow_deg = pyo.Constraint(instance.nodes, instance.nodes, rule=rule_flow_deg) instance.c_flow_deg_indirect = pyo.Constraint( instance.nodes, instance.nodes, rule=rule_flow_deg_indirect) ''' define capacity constraints as an empty list for now ''' instance.c_cap = pyo.ConstraintList( ) # on utilise cette structure pour pouvoir ajouter et supprimer des contraintes par la suite instance.c_mstar = pyo.ConstraintList() instance.c_glm = pyo.ConstraintList() instance.c_fci = pyo.ConstraintList() instance.c_sci = pyo.ConstraintList() instance.c_hti = pyo.ConstraintList() #liste vide pour commencer ''' defining dual values ''' instance.dual = pyo.Suffix(direction=pyo.Suffix.IMPORT) ''' definig list of values that will be fixed by branching (used for problem reduction) ''' ''' and the list of constraints used for branching if constraint branching is used ''' instance.branching_indexes = [] if cpp_constraint_branching: instance.c_branch = pyo.ConstraintList() instance.branching_sets = [] if not (index_branching) and not (integer_branching) and not ( cpp_constraint_branching): log.subtitle( "instance generation failed because no valid branching type is given" ) raise NameError("enter a valid branching type in globals settings") ''' defining wether or not the instance can be subject to constraint deactivation ''' if disable_constraint_deactivation or integer_branching or integer_simplifications: log.write( "disabling constraint deactivation because integer variables are activated (integer branching or simplifications)" ) instance.disable_constraint_deactivation = True instance.constraints_inactivity = {} else: instance.disable_constraint_deactivation = False instance.constraints_inactivity = {} ''' defining others parameters based on those in global.py ''' instance.reduction = 1 instance.id = [0] instance.depth = 0 if reduce_problem: managing.reduce_problem(instance, initial_problem_reduction) #managing.reduce_problem_neighboors(instance,5) if force_integer_to_depot: managing.integerize_edges_to_depot(instance) log.write("integerized " + str(instance.n.value) + " edges to depot " + str(len(list(instance.x.keys()))) + " variables") if force_all_edges_integer: managing.integerize_all_edges(instance) if force_closest_neighboors_integer: integerized = managing.integerize_edges( instance, max_dist=relative_distance_closest_neighboor) log.write("integerized " + str(len(integerized)) + " shortest edges out of " + str(len(list(instance.x.keys()))) + " variables with a relative distance of " + str(relative_distance_closest_neighboor * 100) + '%' + " of max_distance") '''solving the initial instance in order to initialize instance.x values ''' opt.solve(instance) instance.objective_value = pyo.value( instance.objective ) #recording this as attribute to save computation time log.write_timed("finished constructing instance model") return instance, locations
def rule_flow_deg(instance, i, j): return pyo.Constraint.Skip if j >= i else ( instance.x[i, j] >= instance.flow[i, j]) def rule_flow_deg_indirect(instance, i, j): return pyo.Constraint.Skip if j >= i else ( instance.x[i, j] >= instance.flow[j, i]) def to_list_locations(instance): d = [] for i in instance.nodes: d.append((instance.locations[i, 0], instance.locations[i, 1])) return d if __name__ == "main": ''' instanciate model with inbound data ''' model = initialize_model() if len(sys.argv) == 1: file = "test3.dat" instance = model.create_instance(file) elif len(sys.argv) == 2: file = sys.argv[1] instance = construct_instance(model, file) else: raise "erroneous entry arguments" log.write("using input file " + file) instance, locations = initialize_instance(instance)
elif "carp" in sys.argv[1]: raise "Carp problems (without specified number of vehicles) are not yet supported" else: instance,locations = init.full_init(sys.argv[1]) log.title("starting CVRP solving for "+str(instance.n.value)+" nodes, "+str(instance.number_of_vehicles.value)+" vehicles with capacity of "+str(instance.capacity.value)) ''''initialise instance manager ''' instance_manager = managing.instance_manager(instance) full_graph(instance_manager.best_feasible_integer_solution,locations,"Google Opt solution") '''computing lower_bound and adding to queue for column generation and branching ''' managing.set_lower_bound(instance) instance_manager.add(instance) log.write("initial upper bound of cvrp problem is "+str(instance_manager.upper_bound)) ''' printing initial value of objective function ''' log.write("initial value of objective function "+str(round(instance.objective_value,2))+" and is "+str(managing.integer_percent(instance))+"% integer") '''saving initial graph ''' full_graph(instance,locations,"initial") ''' list of old instances used for the graping of the search Tree ''' old_nodes = [] ''' actual branch and cut ''' instance = instance_manager.pop() while instance!=None and max_time_not_reached() and instance.depth<=max_depth: log.subtitle("starting processing of new instance with lower_bound of "+str(instance.lower_bound)+" ( upper_bound is "+str(instance_manager.upper_bound)+") ,depth: "+ str(instance.depth) +", global time: "+str(global_time()),instance.id)
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
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 create_dat(in_name, out_name): with open(in_name, "r") as f: """reading .vrp data file""" ''' supported formats : all formats from http://vrp.atd-lab.inf.puc-rio.br/index.php/en/ letters must all be put to capital, all seperation characters (except . for commas) will be treated equally excepted arguments are : dimension, number of vehicles, capacity, locations or weights, demands ''' lines = f.readlines() for i in range(len(lines)): line = sanitize(lines[i], " ") line = line.split(" ") while "" in line: line.remove("") lines[i] = line #expected_headers = ["NAME","COMMENT","TYPE","CVRP","DIMENSION","CAPACITY","DISTANCE","VEHICLES","EDGE WEIGHT TYPE","EDGE WEIGHT FORMAT","DISPLAY DATA TYPE","NODE COORD TYPE","NODE COORD SECTION","EDGE WEIGHT SECTION","DEMAND SECTION","DEPOT SECTION"] #dimension found = False for line in lines: if line[0] == "DIMENSION": found = True try: dimension = int(float(line[-1])) except: raise NameError(line) break if not (found): raise "Could not locate DIMENSION parameter" #capacity found = False for line in lines: if line[0] == "CAPACITY": found = True try: capacity = int(float(line[-1])) except: raise NameError(line) break if not (found): raise "Could not locate DIMENSION parameter" #demands found = False start_index_demand = 0 for i in range(len(lines)): line = lines[i] if line[0] == "DEMAND" and line[1] == "SECTION": start_index_demand = i + 1 found = True break if not (found): raise "Could not locate DEMAND SECTION parameter" #we want depot to be index 0 (needed for the coord/weights section) depot_set_apart = int(float(lines[start_index_demand][1])) > 0 indexing_offset = int(float( lines[start_index_demand][0])) - (1 if depot_set_apart else 0) demands = [0] for i in range(0 if depot_set_apart else 1, dimension): line = lines[start_index_demand + i] try: demands.append(int(float(sanitize(line[1])))) except ValueError: print("error while adding demands with : ") print(start_index_demand + i) print(line) print(line[1]) print(float(sanitize(line[1]))) except IndexError: print("error while adding demands with : ") print(start_index_demand + i) print(line) print(line[1]) print(float(sanitize(line[1]))) #locations/weights found = False start_index = 0 for i in range(len(lines)): line = lines[i] if line[0] == "NODE" and line[1] == "COORD" and line[ 2] == "SECTION": found = True start_index = i + 1 break if found: input_type = "COORD" locations = {} line = lines[start_index] for i in range(dimension): line = lines[start_index + i] try: n, i, j = int(float( sanitize(line[0]))) - indexing_offset, float( sanitize(line[1])), float(sanitize(line[2])) except IndexError: print("mauvaise indentation dans locations avec : ") print(line) print(n) print(i) print(j) print(len(line)) locations[(n, 0)] = i locations[(n, 1)] = j if depot_set_apart: depot_found = False depot_index = 0 for i in range(len(lines)): line = lines[i] if line[0] == "DEPOT" and line[1] == "SECTION": depot_index = i + 1 break if not (depot_found): raise "could not locate DEPOT SECTION paremeter" line = lines[depot_index] locations[(0, 0)] = float(line[0]) locations[(0, 1)] = float(line[1]) else: raise Exception( "WEIGHT INPUT not yet implemented, must take into account the different types of wieghts input (lower,higher,complete etc.)" ) for i in range(len(lines)): line = lines[i] if line[0] == "EDGE" and line[1] == "WEIGHT" and line[ 2] == "SECTION": start_index = i + 1 found = True break if not (found): raise "Could locate neither EDGE WEIGTH SECTION nor NODE COORD SECTION parameters" input_type = "WEIGHT" weights = {} for i in range(dimension): line = lines[start_index + i] for j in range(len(line)): try: weights[(i, j)] = float(line[j]) except: raise Exception( "Error in writing of weights with paremeters " + str(i) + " " + str(j) + " " + str(start_index)) #number of vehicles found = False for i in range(len(lines)): line = lines[i] if line[0] == "VEHICLES": found = True number_of_vehicles = int(float(line[-1])) break if not (found): for i in range(len(lines)): line = lines[i] if line[0] == "NAME" and line[-1][0] == "k": found = True number_of_vehicles = int(float(line[-1][1:])) break if not (found): number_of_vehicles = int( sum(demand for demand in demands) / capacity) + 4 log.write( "could not locate VEHICLES. We calculated that we could use " + str(number_of_vehicles) + "vehicles") ''' constructing distances matrix if needed ''' weights = {} if input_type == "COORD": for i in range(dimension): for j in range(dimension): weights[i, j] = sqrt((locations[(i, 0)] - locations[(j, 0)])**2 + (locations[(i, 1)] - locations[(j, 1)])**2) '''writing those variables in the right format''' with open(out_name, "w") as f: #number of vehicles f.write("param number_of_vehicles := " + str(number_of_vehicles) + ";\n") f.write("\n") #dimension f.write("param n := " + str(dimension) + ";\n") f.write("\n") #capacity f.write("param capacity := " + str(capacity) + ";\n") f.write("\n") #type of entry f.write("param entry_type := " + input_type + ";\n") f.write("\n") if input_type == "COORD": f.write("param locations := \n") for k in locations.keys(): f.write( str(k[0]) + " " + str(k[1]) + " " + str(locations[k]) + "\n") f.write(";\n") f.write("\n") f.write("param costs := \n") for k in weights.keys(): f.write( str(k[0]) + " " + str(k[1]) + " " + str(weights[k]) + "\n") f.write(";\n") f.write("\n") elif input_type == "WEIGHT": f.write("param locations := \n") for i in range(dimension): f.write(str(i) + " 0 -1" + "\n") f.write(str(i) + " 1 -1" + "\n") f.write(";\n") f.write("\n") f.write("param costs := \n") for k in weights.keys(): f.write( str(k[0]) + " " + str(k[1]) + " " + str(weights[k]) + "\n") f.write(";\n") f.write("\n") else: raise "We screwed up : input_type not recognized at time of writing" #demands f.write("param demands := \n") for i in range(dimension): f.write(str(i) + " " + str(demands[i]) + "\n") f.write(";\n")