Lin
Lin

Reputation: 107

OR-Tools VRP - Minimize number of late deliveries with soft time windows

I am trying to solve a vehicle routing problem where I wish to minimize the number of late deliveries. Through the use of SetCumulVarSoftUpperBound, I was able to set soft time windows to allow for late deliveries. However, since the penalty given by SetCumulVarSoftUpperBound is proportional to how much the bound has been exceeded, the solver will produce a route where multiple deliveries will be slightly late.

For example, given the following time matrix and windows:

data['time_matrix'] = [
    [0, 2, 2, 2, 2], # depot
    [2, 0, 2, 2, 2], # 1
    [2, 2, 0, 2, 2], # 2
    [2, 2, 2, 0, 2], # 3
    [2, 2, 2, 2, 0], # 4
]
data['time_windows'] = [
    (0, 0),  # depot
    (0, 2),  # 1
    (0, 3),  # 2
    (0, 4),  # 3 
    (0, 6),  # 4
]

The solver will return the solution of: 0 -> 1 (On Time) -> 2 (Late by 1) -> 3 (Late by 2) -> 4 (Late by 2). What I am looking for is a route that is closer to 0 -> 1 (On Time) -> 3 (On Time) -> 4 (On Time) -> 2 (Late by 5), where the number of late deliveries are kept to a minimum.

Any help would be much appreciated.


EDIT: Example program to illustrate the issue

import sys
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp

def create_data_model(return_to_depot = False):
    """Stores the data for the problem."""
    data = {}
    data['time_matrix'] = [
        [0, 2, 2, 2, 2], # depot
        [2, 0, 2, 2, 2],
        [2, 2, 0, 2, 2],
        [2, 2, 2, 0, 2],
        [2, 2, 2, 2, 0],
    ]
    data['time_windows'] = [
        (0, 0),  # depot
        (0, 2),  
        (0, 3),  
        (0, 4),  
        (0, 6),
    ]
    data['num_vehicles'] = 1
    data['depot'] = 0

    return data


def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    time_dimension = routing.GetDimensionOrDie('Time')
    total_time = 0
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        plan_output = 'Route for vehicle {}:\n'.format(vehicle_id)
        while not routing.IsEnd(index):
            time_var = time_dimension.CumulVar(index)
            plan_output += '{0} (A:{1}, D:{2}, L:{3}) -> '.format(
                manager.IndexToNode(index), 
                solution.Min(time_var),
                solution.Max(time_var),
                solution.Min(time_var) - data['time_windows'][manager.IndexToNode(index)][1])
            index = solution.Value(routing.NextVar(index))
        time_var = time_dimension.CumulVar(index)

        plan_output += '\nTime of the route: {}min\n'.format(
            solution.Min(time_var))
        print(plan_output)
        total_time += solution.Min(time_var)


def main():
    """Solve the VRP with time windows."""
    # Instantiate the data problem.
    data = create_data_model()

    # Initialize the penalty value
    penalty = sum([sum(i) for i in data['time_matrix']]) + 1

    # Create the routing index manager.
    manager = pywrapcp.RoutingIndexManager(len(data['time_matrix']),
                                           data['num_vehicles'], data['depot'])

    # Create Routing Model.
    routing = pywrapcp.RoutingModel(manager)

    # Create and register a transit callback.
    def time_callback(from_index, to_index):
        """Returns the travel time between the two nodes."""
        # Convert from routing variable Index to time matrix NodeIndex.
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return data['time_matrix'][from_node][to_node]

    transit_callback_index = routing.RegisterTransitCallback(time_callback)

    # Define cost of each arc.
    routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

    # Add Time Windows constraint.
    time = 'Time'
    routing.AddDimension(
        transit_callback_index,
        sys.maxsize,  # maximum waiting time
        sys.maxsize,  # maximum travel time per vehicle
        False,  # Don't force start cumul to zero.
        time)
    time_dimension = routing.GetDimensionOrDie(time)

    # Add time window constraints for each location except depot.
    for location_idx, time_window in enumerate(data['time_windows']):
        if location_idx == data['depot']:
            continue
        index = manager.NodeToIndex(location_idx)
        time_dimension.CumulVar(index).SetMin(time_window[0])
        time_dimension.SetCumulVarSoftUpperBound(index, time_window[1], penalty)
        
    # Add time window constraints for each vehicle start node.
    depot_idx = data['depot']
    for vehicle_id in range(data['num_vehicles']):
        index = routing.Start(vehicle_id)
        time_dimension.CumulVar(index).SetRange(
            data['time_windows'][depot_idx][0],
            data['time_windows'][depot_idx][1])

    # Instantiate route start and end times to produce feasible times.
    for i in range(data['num_vehicles']):
        routing.AddVariableMinimizedByFinalizer(
            time_dimension.CumulVar(routing.Start(i)))
        routing.AddVariableMinimizedByFinalizer(
            time_dimension.CumulVar(routing.End(i)))
    
    # Setting first solution heuristic.
    search_parameters = pywrapcp.DefaultRoutingSearchParameters()
    search_parameters.first_solution_strategy = (
        routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)
    search_parameters.local_search_metaheuristic = (
        routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)
    search_parameters.time_limit.seconds = 1

    # Solve the problem.
    solution = routing.SolveWithParameters(search_parameters)

    # Print solution on console.
    if solution:
        print_solution(data, manager, routing, solution)
    else:
        print("\nNo solutions founds")


if __name__ == '__main__':
    main()

Upvotes: 3

Views: 1267

Answers (2)

Lin
Lin

Reputation: 107

I have managed to solve this issue by implementing a fixed penalty in addition to the proportional cost for violating time windows. By imposing a high fixed cost and a low proportional cost, the solver will then be more incentivized to delay an already late job than to be late for another job.

Implementation details can be found in this discussion thread over on the or-tools Github page. The brief rundown of the implementation is as follows:

  1. Split all existing locations into two separate nodes; an 'on-time' node with a hard time window (i.e. setRange) and a 'late' node with a soft time window (i.e. SetCumulVarSoftUpperBound)
  2. Add a disjunction so that the solver is required to visit only one of the two nodes
  3. Add another disjunction to the 'on-time' node so that a fixed cost has to be paid for dropping it. You will want to ensure that the fixed cost is high enough so that the solver will drop the 'on-time' node (i.e. be late for a job) only when it is absolutely necessary.

Upvotes: 1

Phoenix
Phoenix

Reputation: 982

I think the issue is that you are perhaps using the wrong tool for the job.

You mentioned:

I am trying to solve a vehicle routing problem where I wish to minimize the number of late deliveries.

In that case, it seems like rather than trying to solve this problem using a constraint solver, you should use linear or nonlinear programming (depending on if all of your constraints are linear). Your objective would be to minimize the number of late deliveries.

From Wikipedia:

Linear programming (LP, also called linear optimization) is a method to achieve the best outcome (such as maximum profit or lowest cost) in a mathematical model whose requirements are represented by linear relationships. Linear programming is a special case of mathematical programming (also known as mathematical optimization).

More formally, linear programming is a technique for the optimization of a linear objective function, subject to linear equality and linear inequality constraints. Its feasible region is a convex polytope, which is a set defined as the intersection of finitely many half spaces, each of which is defined by a linear inequality. Its objective function is a real-valued affine (linear) function defined on this polyhedron. A linear programming algorithm finds a point in the polytope where this function has the smallest (or largest) value if such a point exists.

It seems like you are using Google's OR Tools, which includes linear & mixed integer programming solvers: https://developers.google.com/optimization/lp/mpsolver

Upvotes: 1

Related Questions