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)