jmarca/initial-solution

View on GitHub
src/model_run.py

Summary

Maintainability
B
4 hrs
Test Coverage
B
84%
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
import numpy as np
from functools import partial
import evaluators as E

def use_nodes(record,d):
    nodes = [0,record.origin,record.destination]
    nodes.extend(d.get_break_node_chain(0,record.origin))
    nodes.extend(d.get_break_node_chain(record.origin,record.destination))
    nodes.extend(d.get_break_node_chain(record.destination,0))
    return nodes


# globals.  does python do this?
time_dimension_name = 'time'
cap_dimension_name = 'cap'
count_dimension_name = 'count'
drive_dimension_name = 'drive'
short_break_dimension_name = 'break'



def get_route(v,assignment,routing,manager):
    index = routing.Start(v)
    initial_route = []
    while not routing.IsEnd(index):
        if index != routing.Start(v):
            node_idx = manager.IndexToNode(index)
            initial_route.append(node_idx)
        index = assignment.Value(routing.NextVar(index))
    return initial_route

def setup_params(args):
    parameters = pywrapcp.DefaultRoutingSearchParameters()
    parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PARALLEL_CHEAPEST_INSERTION)
        # routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
        # routing_enums_pb2.FirstSolutionStrategy.ALL_UNPERFORMED)
        # routing_enums_pb2.FirstSolutionStrategy.LOCAL_CHEAPEST_INSERTION)

    # Disabling path Large Neighborhood Search is the default behaviour.  enable
    parameters.local_search_operators.use_path_lns = pywrapcp.BOOL_TRUE
    parameters.local_search_operators.use_inactive_lns = pywrapcp.BOOL_TRUE
    # Routing: forbids use of TSPOpt neighborhood,
    # parameters.local_search_operators.use_tsp_opt = pywrapcp.BOOL_FALSE
    # set a time limit
    if args is not None:
        parameters.time_limit.seconds = args.timelimit * 60  # timelimit minutes
        if args.guided_local:
            print('including guided local search')
            parameters.local_search_metaheuristic = (
                routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
    else:
        parameters.time_limit.seconds = 60
    # sometimes helps with difficult solutions
    parameters.lns_time_limit.seconds = 10000  # 10000 milliseconds
    # i think this is the default
    # parameters.use_light_propagation = False
    # set to true to see the dump of search iterations
    parameters.log_search = pywrapcp.BOOL_TRUE

    return parameters

def unset_times(t,demand_subset):
    t = t.copy()
    l = len(t.index)
    index_mask = t.index.isin(demand_subset).reshape(l,1)
    column_mask = t.columns.isin(demand_subset)
    m = np.multiply(index_mask,column_mask)
    return t.where(m)



def breaks_at_nodes_constraints(d,
                                v,
                                time_matrix,
                                manager,
                                routing,
                                base_value):

    solver = routing.solver()
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    drive_dimension = routing.GetDimensionOrDie(drive_dimension_name)
    short_break_dimension = routing.GetDimensionOrDie(short_break_dimension_name)
    count_dimension = routing.GetDimensionOrDie(count_dimension_name)

    feasible_index = d.demand.feasible
    for idx in d.demand.index[feasible_index]:
        record = d.demand.loc[idx,:]
        # double check that is possible (in case just solving a limited set
        if np.isnan(time_matrix.loc[record.origin,record.destination]):
            continue
        d.break_constraint(record.origin,record.destination,
                           manager,routing,
                           drive_dimension,
                           short_break_dimension,
                           base_value
        )

    # constraints on return to depot, otherwise we just collect
    # break nodes on the way back and go deeply negative
    for veh in v:
        index = routing.End(veh.index)
        end_drive = drive_dimension.CumulVar(index)
        end_short = short_break_dimension.CumulVar(index)
        solver.AddConstraint(
            end_drive >= base_value)
        solver.AddConstraint(
            end_drive < base_value+(11*60))

        solver.AddConstraint(
            end_short >= base_value)

        solver.AddConstraint(
            end_short < base_value+(8*60))

def setup_model(d,t,v):
    # common to both with and without breaks
    num_nodes = len(t.index)
    manager = pywrapcp.RoutingIndexManager(
        num_nodes,
        len(v),
        v[0].depot_index)

    model_parameters = pywrapcp.DefaultRoutingModelParameters()
    model_parameters.max_callback_cache_size = 2 * num_nodes * num_nodes
    routing = pywrapcp.RoutingModel(manager, model_parameters)

    time_callback = E.create_time_callback2(t, d)
    demand_callback = E.create_demand_callback(t.index,d)

    transit_callback_index = routing.RegisterTransitCallback(
        partial(time_callback, manager)
    )

    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

    # count
    routing.AddConstantDimension(
        1, # increment by one every time
        num_nodes,  # max count is visit all the nodes
        True,  # set count to zero
        count_dimension_name)
    count_dimension = routing.GetDimensionOrDie(count_dimension_name)

    # time
    routing.AddDimension(
        transit_callback_index, # same "cost" evaluator as above
        # d.horizon,  # try infinite slack
        0, # try no slack
        d.horizon,  # max time is end of time horizon
        False,  # don't set time to zero...vehicles can wait at depot if necessary
        time_dimension_name)
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    # time_dimension.SetGlobalSpanCostCoefficient(100)

    # capacity/demand
    demand_evaluator_index = routing.RegisterUnaryTransitCallback(
        partial(demand_callback, manager))
    vehicle_capacities = [veh.capacity for veh in v]
    routing.AddDimensionWithVehicleCapacity(
        demand_evaluator_index,
        0,  # null capacity slack
        vehicle_capacities,
        True,  # start cumul to zero
        cap_dimension_name)
    cap_dimension = routing.GetDimensionOrDie(cap_dimension_name)

    return (num_nodes,manager,routing)

def break_nodes_time_windows(d,demand_subset,manager,routing):
    # Pickup & Delivery, plus time window
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    for node in demand_subset:
        # skip depot nodes---handled in vehicle time windows
        if node == 0:
            continue
        # also skip nodes with non zero demand (handled above)
        if d.get_demand(node) != 0:
            continue

        # this is a dummy node, not a pickup (demand = 1) not a dropoff (-1)
        index = manager.NodeToIndex(node)
        # set maximal time window
        time_dimension.CumulVar(index).SetRange(0,d.horizon)
        routing.AddToAssignment(time_dimension.SlackVar(index))


def pick_deliver_constraints(d,t,manager,routing):
    # Pickup & Delivery, plus time window
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    for idx in d.demand.index:
        record = d.demand.loc[idx]
        # print('origin to dest',record.origin,record.destination)

        if not record.feasible:
            continue
        if np.isnan(t.loc[record.origin,record.destination]):
            # print('origin to dest is nan',record.origin,record.destination,
            #      t.loc[record.origin,record.destination])
            continue
        pickup_index = manager.NodeToIndex(record.origin)
        delivery_index = manager.NodeToIndex(record.destination)
        routing.AddPickupAndDelivery(pickup_index, delivery_index)
        routing.solver().Add(
            routing.VehicleVar(pickup_index) ==
            routing.VehicleVar(delivery_index))
        routing.solver().Add(
            time_dimension.CumulVar(pickup_index) <=
            time_dimension.CumulVar(delivery_index))
        # [START time_window_constraint]
        early = int(record.early)
        late = int(record.late)
        time_dimension.CumulVar(pickup_index).SetRange(early, late)
        routing.AddToAssignment(time_dimension.SlackVar(pickup_index))
        # and  add simulation-wide time windows (slack) for delivery nodes,
        dropoff_index = manager.NodeToIndex(record.destination)
        # tt = t.loc[record.origin,record.destination]
        # just set dropoff time window same as 0, horizon
        early = 0
        late = d.horizon
        time_dimension.CumulVar(dropoff_index).SetRange(early, late)
        routing.AddToAssignment(time_dimension.SlackVar(dropoff_index))

def vehicle_time_constraints(v,manager,routing):
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    for vehicle in v:
        index = routing.Start(vehicle.index)
        time_dimension.CumulVar(index).SetRange(vehicle.time_window[0],
                                                vehicle.time_window[1])
        routing.AddToAssignment(time_dimension.SlackVar(index))

def vehicle_time_drive_constraints(v,base_value,manager,routing):
    time_dimension = routing.GetDimensionOrDie(time_dimension_name)
    short_break_dimension = routing.GetDimensionOrDie(short_break_dimension_name)
    drive_dimension = routing.GetDimensionOrDie(drive_dimension_name)
    for vehicle in v:
        index = routing.Start(vehicle.index)
        routing.solver().Add(drive_dimension.CumulVar(index)==base_value)
        routing.solver().Add(short_break_dimension.CumulVar(index)==base_value)
        time_dimension.CumulVar(index).SetRange(vehicle.time_window[0],
                                                vehicle.time_window[1])
        routing.AddToAssignment(time_dimension.SlackVar(index))



def model_run(d,t,v,base_value,
              demand_subset=None,
              initial_routes=None,
              args=None):

    # use demand_subset to pick out a subset of nodes
    if demand_subset != None:
        t = unset_times(t,demand_subset)
    else:
        demand_subset = t.index

    (num_nodes,manager,routing) = setup_model(d,t,v)

    drive_callback = partial(E.create_drive_callback(t, d, 11*60, 10*60), manager)
    drive_callback_index = routing.RegisterTransitCallback(drive_callback)
    routing.AddDimension(
        drive_callback_index, # same "cost" evaluator as above
        0,  # No slack for drive dimension? infinite slack?
        d.horizon,  # max drive is end of drive horizon
        False, # set to zero for each vehicle
        drive_dimension_name)
    drive_dimension = routing.GetDimensionOrDie(drive_dimension_name)

    short_break_callback = partial(E.create_short_break_callback(t, d, 8*60, 30), manager)
    short_break_callback_index = routing.RegisterTransitCallback(short_break_callback)
    routing.AddDimension(
        short_break_callback_index, # modified "cost" evaluator as above
        0,  # No slack
        d.horizon,  # max horizon is horizon
        False, # set to zero for each vehicle
        short_break_dimension_name)
    short_break_dimension = routing.GetDimensionOrDie(short_break_dimension_name)

    pick_deliver_constraints(d,t,manager,routing)
    break_nodes_time_windows(d,demand_subset,manager,routing)


    # vehicle constraints, time windows etc
    # constrain long break and short_break dimensions to be base_value at
    # start, so avoid negative numbers
    vehicle_time_drive_constraints(v,base_value,manager,routing)


    breaks_at_nodes_constraints(d, v, t,
                                manager,
                                routing,
                                base_value)

    parameters = setup_params(args)
    # add disjunctions to deliveries to make it not fail
    penalty = 1000000000  # The cost for dropping a demand node from the plan.
    break_penalty = 0  # The cost for dropping a break node from the plan.
    # all nodes are droppable, so add disjunctions

    droppable_nodes = [routing.AddDisjunction([manager.NodeToIndex(c)],
                                              penalty) for c in d.get_node_list()]
    breaknodes = np.setdiff1d(t.index,d.equivalence.index)
    # get rid of depot node
    breaknodes = np.delete(breaknodes,0)

    more_droppables = [routing.AddDisjunction([manager.NodeToIndex(c)],
                                              break_penalty) for c in breaknodes]
    assignment = run_solver(routing,parameters,initial_routes)
    return (assignment,routing,manager)

def run_solver(routing,parameters,initial_routes):
    if initial_routes:
        routing.CloseModelWithParameters(parameters)
        initial_solution = routing.ReadAssignmentFromRoutes(initial_routes,
                                                            True)
        assert initial_solution
        return routing.SolveFromAssignmentWithParameters(initial_solution, parameters)
    else:
        return routing.SolveWithParameters(parameters)



def model_run_nobreaks(d,t,v,demand_subset=None,initial_routes=None,args=None):

    # use demand_subset to pick out a subset of nodes
    if demand_subset != None:
        t = unset_times(t,demand_subset)
    else:
        demand_subset = t.index


    (num_nodes,manager,routing) = setup_model(d,t,v)

    pick_deliver_constraints(d,t,manager,routing)
    vehicle_time_constraints(v,manager,routing)

    parameters = setup_params(args)

    # add disjunctions to deliveries to make it not fail
    penalty = 10000000  # The cost for dropping a demand node from the plan.
    # all nodes are droppable, so add disjunctions

    droppable_nodes = [routing.AddDisjunction([manager.NodeToIndex(c)],
                                              penalty) for c in d.get_node_list()]
    assignment = run_solver(routing,parameters,initial_routes)
    return (assignment,routing,manager)