def greedy_assignment(route, buses): routes = [] picked_up = [False for stop in route.stops] while False in picked_up: bus = None for bus in buses[::-1]: #Found an unassigned bus if bus.route == None: break if bus == None: print("Out of buses. Terminating bus assignment.") break route_creating = Route() for i in range(len(route.stops)): if not picked_up[i]: route_creating.add_stop(route.stops[i]) picked_up[i] = True if (not bus.can_handle(route_creating)): route_creating.remove_stop(route.stops[i]) picked_up[i] = False for bus in buses: if bus.can_handle(route_creating): bus.assign(route_creating) break if route_creating.bus != None: routes.append(route_creating) continue #If no bus was large enough, should be for one stop. assert (len(route_creating.stops) == 1), str(len(route_creating.stops)) for bus in buses[::-1]: if bus.route == None: break bus.assign(route_creating) routes.append(route_creating) return routes
def assign_lift(route, buses, picked_up): new_route = Route() for stop_ind, stop in enumerate(route.stops): if picked_up[stop_ind]: continue #If this is a wheelchair stop, see whether we can add it to the route if stop.count_needs("W") > 0 or stop.count_needs("L") > 0: new_route.add_stop(stop) picked_up[stop_ind] = True possible = False for bus in buses: if bus.route == None and bus.lift and bus.can_handle( new_route): possible = True break if not possible: new_route.remove_stop(stop) picked_up[stop_ind] = False #Now as many wheelchair students as can fit on one #bus have been picked up. Add other students if possible for stop_ind, stop in enumerate(route.stops): if picked_up[stop_ind]: continue if stop.count_needs("W") == 0 and stop.count_needs("L") == 0: if not new_route.add_stop(stop): continue picked_up[stop_ind] = True possible = False for bus in buses: if bus.route == None and bus.lift and bus.can_handle( new_route): possible = True break if not possible: new_route.remove_stop(stop) picked_up[stop_ind] = False for bus in buses: if bus.lift and bus.can_handle(new_route): assert bus.assign(new_route) break #Keep stops in the same order. This ensures that the travel time #doesn't increase, violating regulations. new_route.stops.sort(key=lambda s: route.stops.index(s)) new_route.recompute_length() buses.remove(new_route.bus) #If we failed to pick up all wheelchair students, #need to continue assigning wheelchair buses. recursive_routes = [] for stop_ind, stop in enumerate(route.stops): if (not picked_up[stop_ind] and (stop.count_needs("W") > 0 or stop.count_needs("L") > 0)): recursive_routes = assign_lift(route, buses, picked_up) recursive_routes.append(new_route) assert new_route.feasibility_check(verbose=True) return recursive_routes
def apply_partial_route_plan(partial_route_plan, all_stops, new_route_plan): for route in partial_route_plan: new_route = Route() new_route_plan.add(new_route) for stop in route.stops: isomorphic_stop = None for search_stop in all_stops: if (search_stop.school.school_name == stop.school.school_name and search_stop.tt_ind == stop.tt_ind): isomorphic_stop = search_stop break if isomorphic_stop == None: print("Stop not found") new_route.add_stop(isomorphic_stop) isomorphic_stop.school.unrouted_stops.remove(isomorphic_stop) all_stops.remove(isomorphic_stop) for stop in isomorphic_stop.school.unrouted_stops: stop.update_value(isomorphic_stop) print("Done applying partial route plan")
def try_uncrossing(r1, r2, i1, i2): new_r1 = Route() new_r2 = Route() for i in range(i1): new_r1.add_stop(r1.stops[i]) for i in range(i2, len(r2.stops)): new_r1.add_stop(r2.stops[i]) for i in range(i2): new_r2.add_stop(r2.stops[i]) for i in range(i1, len(r1.stops)): new_r2.add_stop(r1.stops[i]) #If the school combination was invalid, not #all stops will make it on if len(new_r1.stops) + len(new_r2.stops) < len(r1.stops) + len(r2.stops): return -1 savings = mstt([r1, r2]) - mstt([new_r1, new_r2]) #If the new routes don't save time on average, no point #Note: savings <= 0 sometimes allows trivial swaps to #sneak by due to floating point error, so use savings <= 1 if savings <= 1: return -1 #Determine by how much (if at all) the time constraints are violated worst_extra_time_ratio = max(new_r1.length / new_r1.max_time, new_r2.length / new_r2.max_time) #Determine whether the uncrossing mixes elementary #and high school students elem_high_mixed = ((new_r1.e_no_h and new_r1.h_no_e) or (new_r2.e_no_h and new_r2.h_no_e)) #In the special ed case, buses are not assigned, so we return #zeros for the capacity parts. if r1.bus == None or r2.bus == None: return (savings, worst_extra_time_ratio, 0, 0, elem_high_mixed, new_r1, new_r2) #Determine by how much (if at all) the capacity constraints are violated #for each of the two possible assignments of the buses to the routes bus1_for_newr1 = r1.bus.can_handle(new_r1, True, True) bus2_for_newr2 = r2.bus.can_handle(new_r2, True, True) worst_cap_ratio_1 = max(bus1_for_newr1, bus2_for_newr2) bus1_for_newr2 = r1.bus.can_handle(new_r2, True, True) bus2_for_newr1 = r2.bus.can_handle(new_r1, True, True) worst_cap_ratio_2 = max(bus1_for_newr2, bus2_for_newr1) return (savings, worst_extra_time_ratio, worst_cap_ratio_1, worst_cap_ratio_2, elem_high_mixed, new_r1, new_r2)
def check_possibilities(route, buses_using, partial_routes, picked_up, route_ind, min_stop_ind, starts): global start_time if process_time() > start_time + constants.BUS_SEARCH_TIME: return (False, None, 1e10) #If we're out of buses, return infeasibility if route_ind == len(buses_using) and False in picked_up: return (False, None, 1e10) #If the bus has too many students and multiple stops, return infeasibility if (not buses_using[route_ind].can_handle(partial_routes[route_ind]) and len(partial_routes[route_ind].stops) > 1): return (False, None, 1e10) #If the last bus has passed a stop that needs to be picked up, return infeasibility if route_ind == len(buses_using) - 1 and False in picked_up[:min_stop_ind]: return (False, None, 1e10) #If only one capacity remains and a bus with such capacity has passed #a stop that needs to be picked up before starting the route, return #infeasibility to avoid duplicating work. if (buses_using[route_ind] == buses_using[-1] and len(partial_routes[route_ind].stops) == 0 and False in picked_up[:min_stop_ind]): return (False, None, 1e10) #If all stops have been picked up, this is a feasible solution. if False not in picked_up: trav_times = [] #Determine the total travel time for route in partial_routes: trav_times.extend(route.student_travel_times()) total_trav_time = sum(trav_times) completed_routes = [] for route in partial_routes: new_route = Route() for stop in route.stops: new_route.add_stop(stop) completed_routes.append(new_route) return (True, completed_routes, total_trav_time) best = (False, None, 1e10) #First, check completion of the current route. out = check_possibilities(route, buses_using, partial_routes, picked_up, route_ind + 1, 0, starts) if out[2] < best[2]: best = out for stop_ind in range(min_stop_ind, len(picked_up)): if not picked_up[stop_ind]: #Case where we would be duplicating work #We're starting a route at an earlier time than #another bus with the same capacity. if len(partial_routes[route_ind].stops) == 0: duplicating = False for start in starts: if start[0] == buses_using[ route_ind] and stop_ind < start[1]: duplicating = True if duplicating: continue partial_routes[route_ind].add_stop(route.stops[stop_ind]) picked_up[stop_ind] = True if len(partial_routes[route_ind].stops) == 1: starts.append((buses_using[route_ind], stop_ind)) out = check_possibilities(route, buses_using, partial_routes, picked_up, route_ind, stop_ind + 1, starts) if len(partial_routes[route_ind].stops) == 1: del starts[-1] picked_up[stop_ind] = False if out[2] < best[2]: best = out partial_routes[route_ind].remove_stop(route.stops[stop_ind]) #Another case where we would be duplicating work #If all remaining buses have the same capacity, we may #as well start at the first untaken stop. So break after #trying the first untaken stop. if (buses_using[route_ind] == buses_using[-1] and len(partial_routes[route_ind].stops) == 0): break return best
def assign_buses(routes, buses): global start_time buses.sort(key=lambda x: x.capacity) routes = list(routes) routes.sort(key=lambda x: x.occupants) new_routes = [] for route_ind, route in enumerate(routes): #Reporting if len(buses) == 0: new_routes.append(route) continue picked_up = [False for i in range(len(route.stops))] #Before entering the recursive procedure, assign buses for #wheelchair students if any exist. for stud in route.special_ed_students: if stud.has_need("W") or stud.has_need("L"): l_routes = assign_lift(route, buses, picked_up) new_routes.extend(l_routes) break #It's possible that the wheelchair buses were #able to pick up all of the students. if False not in picked_up: continue #Due to checks in the brute force bus assignment #procedure that rely on a certain order of processing #for possible permutations, it's necessary to pass #in a route none of whose stops have been picked up. #Therefore, create a virtual route with all of the #unvisited stops. virtual_route = Route() for (stop_ind, stop) in enumerate(route.stops): if not picked_up[stop_ind]: virtual_route.add_stop(stop) picked_up = [False for i in range(len(virtual_route.stops))] num_buses = 0 out = None start_time = process_time() while False in picked_up: num_buses += 1 out = try_hold(virtual_route, num_buses, buses, picked_up) if out[0]: break if process_time() - start_time > constants.BUS_SEARCH_TIME: #No solution was found by search. Punt out = None break if out == None: greedy_routes = greedy_assignment(virtual_route, buses) for subroute in greedy_routes: new_routes.append(subroute) continue for subroute in out[1]: #If no bus is big enough to take the stop, just use the #biggest remaining one. #So first figure out what that is. biggest_bus = None for bus in buses[::-1]: if bus.route == None: biggest_bus = bus for bus in buses: #Acceptable either if the capacity is satisifed OR we #take the largest remaining bus and only pick up one stop if (bus.can_handle(subroute) or len(subroute.stops) == 1 and bus == biggest_bus): subroute.bus = bus bus.route = subroute subroute.bus = bus new_routes.append(subroute) break buses.remove(subroute.bus) return new_routes
def generate_routes(schools, permutation=None, partial_route_plan=None, sped=False): all_stops = [] for school in schools: all_stops.extend(school.unrouted_stops) #Initialize stop values for stop in all_stops: stop.update_value(None) #We will process the stops in order of distance #from their schools #The second and part of the tuple improves #determinism of the sorting algorithm. all_stops = sorted(all_stops, key=lambda s: (-trav_time(s, s.school), s.school.school_name)) if len(all_stops) == 0: return [] if permutation != None: all_stops = [all_stops[i] for i in permutation] routes = [] near_schools = determine_school_proximities(schools) if partial_route_plan != None: apply_partial_route_plan(partial_route_plan, all_stops, routes) while len(all_stops) > 0: current_route = Route() #Pick up the most distant stop init_stop = all_stops[0] root_school = init_stop.school root_school.unrouted_stops.remove(init_stop) all_stops.remove(init_stop) #Figure out which schools can be mixed with the stop admissible_schools = near_schools[root_school] if sped: #special ed: no mixed load routing admissible_schools = set([root_school]) current_route.add_stop(init_stop) e_no_h = False h_no_e = False if init_stop.e > 0 and init_stop.h == 0: e_no_h = True if init_stop.h > 0 and init_stop.e == 0: h_no_e = True #Now we will try to add a stop while True: oldlength = current_route.length current_route.backup("generation") #best_score = -100000 best_score = constants.EVALUATION_CUTOFF best_stop = None for school in admissible_schools: for stop in school.unrouted_stops: #Not feasibile with respect to age types if (e_no_h and stop.h > 0 and stop.e == 0 or h_no_e and stop.e > 0 and stop.h == 0): continue if current_route.insert_mincost(stop): #Stop was successfully inserted. #Determine the score of the stop #We want to penalize large time #increases while rewarding collecting #faraway stops. time_cost = current_route.length - oldlength value = stop.value score = value - time_cost #stop in the same place, but different age if time_cost == 0: score = 100000 if score > best_score: best_score = score best_stop = stop current_route.restore("generation") if best_stop == None: break msg = "Failed to insert stop after insertion was verified" assert current_route.insert_mincost(best_stop), msg best_stop.school.unrouted_stops.remove(best_stop) all_stops.remove(best_stop) for stop in best_stop.school.unrouted_stops: stop.update_value(best_stop) if best_stop.e > 0 and best_stop.h == 0: e_no_h = True if best_stop.h > 0 and best_stop.e == 0: h_no_e = True routes.append(current_route) return list(routes)