利用or-tools实现带时间窗口车辆路径规划(VRPTW)

之前解释了TSP、VRP、带容量限制的VRP、取货送货VRP ,今天我们再来介绍一种带时间限制的VPR,所谓带时间限制的VRP顾名思义是指车辆需要访问的每一个地点都有一个时间窗口,车辆必须在指定的时间窗口内到达指定的地点,这个时间窗口对于我们的算法来说是一个硬约束是不可用违法的。

带时间限制的 VRP 示例

这里的地点和之前我们介绍的CVRP案例中的地点相同,只不过在此基础上我们在每一个地点上方都加上了一个时间窗口,车辆必须在指定的时间窗口内到达该地点。下图显示了蓝色的点代表车辆需要访问地点和黑色的点代表所有车辆的出发点(起始点和终点)。

 这里需要说明一下,为了将问题简单化,我们这里的时间窗口没有特定的时间单位比如[7,12] 可以理解为车辆必须第7个时刻至第12个时刻的时间范围内访问第1号地点。

与之前的最小化车辆总行驶路程的优化目标不同的是,这里我们需要优化的目标是最小化车辆的总行程时间。

使用 OR-Tools 解决 VRPTW 示例

以下部分描述了如何使用 OR-Tools 解决 VRPTW 示例。

创建数据模型

以下函数为问题创建数据。

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


def create_data_model():
    """Stores the data for the problem."""
    data = {}
    data['time_matrix'] = [
        [0, 6, 9, 8, 7, 3, 6, 2, 3, 2, 6, 6, 4, 4, 5, 9, 7],
        [6, 0, 8, 3, 2, 6, 8, 4, 8, 8, 13, 7, 5, 8, 12, 10, 14],
        [9, 8, 0, 11, 10, 6, 3, 9, 5, 8, 4, 15, 14, 13, 9, 18, 9],
        [8, 3, 11, 0, 1, 7, 10, 6, 10, 10, 14, 6, 7, 9, 14, 6, 16],
        [7, 2, 10, 1, 0, 6, 9, 4, 8, 9, 13, 4, 6, 8, 12, 8, 14],
        [3, 6, 6, 7, 6, 0, 2, 3, 2, 2, 7, 9, 7, 7, 6, 12, 8],
        [6, 8, 3, 10, 9, 2, 0, 6, 2, 5, 4, 12, 10, 10, 6, 15, 5],
        [2, 4, 9, 6, 4, 3, 6, 0, 4, 4, 8, 5, 4, 3, 7, 8, 10],
        [3, 8, 5, 10, 8, 2, 2, 4, 0, 3, 4, 9, 8, 7, 3, 13, 6],
        [2, 8, 8, 10, 9, 2, 5, 4, 3, 0, 4, 6, 5, 4, 3, 9, 5],
        [6, 13, 4, 14, 13, 7, 4, 8, 4, 4, 0, 10, 9, 8, 4, 13, 4],
        [6, 7, 15, 6, 4, 9, 12, 5, 9, 6, 10, 0, 1, 3, 7, 3, 10],
        [4, 5, 14, 7, 6, 7, 10, 4, 8, 5, 9, 1, 0, 2, 6, 4, 8],
        [4, 8, 13, 9, 8, 7, 10, 3, 7, 4, 8, 3, 2, 0, 4, 5, 6],
        [5, 12, 9, 14, 12, 6, 6, 7, 3, 3, 4, 7, 6, 4, 0, 9, 2],
        [9, 10, 18, 6, 8, 12, 15, 8, 13, 9, 13, 3, 4, 5, 9, 0, 9],
        [7, 14, 9, 16, 14, 8, 5, 10, 6, 5, 4, 10, 8, 6, 2, 9, 0],
    ]
    data['time_windows'] = [
        (0, 5),  # depot
        (7, 12),  # 1
        (10, 15),  # 2
        (16, 18),  # 3
        (10, 13),  # 4
        (0, 5),  # 5
        (5, 10),  # 6
        (0, 4),  # 7
        (5, 10),  # 8
        (0, 3),  # 9
        (10, 16),  # 10
        (10, 15),  # 11
        (0, 5),  # 12
        (5, 10),  # 13
        (7, 8),  # 14
        (10, 15),  # 15
        (11, 15),  # 16
    ]
    data['num_vehicles'] = 4
    data['depot'] = 0
    return data

data = create_data_model()

这里,我们的数据模型和之前的有所不同,之前我们都是定义的距离矩阵distance_matrix,而在这里我们定义的是一个时间矩阵:time_matrix,它的元素表示车辆经过任务两个地点所需要的时间。主要数据结果包括:

  • data['time_matrix']:地点之间的行驶时间数组。请注意,这与之前使用距离矩阵的示例不同。如果所有车辆以相同的速度行驶,则使用距离矩阵或时间矩阵将得到相同的解,因为行驶距离是行驶时间的常数倍。
  • data['time_windows']:地点的时间窗口数组,您可以将其视为访问的请求时间。车辆必须在其时间窗口内访问一个位置。 
  • data['num_vehicles']:车辆数量。
  • data['depot']: 起始点和终点的索引。

创建路由模型

        以下代码在程序的主要部分创建了索引管理器(manager)和路由模型(routing)。 manager主要根据time_matrix来管理各个地点的索引,而routing用来计算和存储访问路径。

manager = pywrapcp.RoutingIndexManager(len(data['time_matrix']),
                                       data['num_vehicles'], 
                                       data['depot'])
routing = pywrapcp.RoutingModel(manager)

创建时间回调函数

这里我们定义了一个时间回调函数用来从time_matrix中返回给定的两个地点之间的时间,接下来我们还要设置路由成本(routing.SetArcCostEvaluatorOfAllVehicles)它将告诉求解器如何计算任意两个地点的路线成本—这里我们的成本指的是任意两个地点之间的时间

def time_callback(from_index, to_index):
    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)

routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_index)

添加维度变量

time = 'Time'
routing.AddDimension(
    transit_callback_index,
    30,  # allow waiting time
    30,  # maximum time per vehicle
    False,  # Don't force start cumul to zero.
    time)
time_dimension = routing.GetDimensionOrDie(time)

 和之前VPR的案例中创建距离维度类似,不过在这里我们需要创建一个时间的维度变量,时间维度中的参数:

  • transit_callback_index: 行程时间的回调索引
  • 第一个30:表示车辆到达每个地点后可以休息的最大时间,在之前的案例中我们都将它设置为0,当设置为0时表示车辆达到该地点后必须立即前往下一个地点中间不休息。但在当前这个案例中每一个地点都有一个时间窗口,车辆不能早到,也不能晚到,必须要在时间窗口的设置的时间范围内到达,因此车辆在到达前一个地点后,很有可能必须要休息一段时间后再前往下一个地点,这样才能保证在规定的时间窗口内到达下一个地点。所以必须要设置一个休息或者等待时间。
  • 第二个30:每辆车行驶路程的总时间的上限,这里需要指定一个总时间,用来限制每辆车的走完自己的所有行驶路径后的总时间。
  • False: 这个参数可以参考官网的解释,它的字面含义是:fix_start_cumulative_to_zero,表示我们的统计量是否是从0开始统计,在之前的案例中我们都将它设置为0,因为不存在车辆休息或者等待的情况,而在当前的案例中每个地点都有一个时间窗口,如果车辆的行驶速度是确定的,那么车辆很有可能在到达第一个地点之前需要等待一段时间后,才能按时在第一个地点的时间窗口内到底第一个地点,因此我们在计算车辆行驶总时间时不能从第0个时刻开始计算总时间,所以我们必须将这个参数设置为False.
  • dimension_name:表示维度名称的标识,它是算法调用维度变量的一个句柄。

为地点添加时间窗口约束条件

我们需要给每个地点添加预先设置好的时间窗口约束,这里我们使用维度变量time_dimension的CumulVar(index).SetRange方法来为地点index添加时间窗口。

#除起始点以外,给每个地点添加时间窗口约束
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).SetRange(time_window[0], time_window[1])

为车辆车出发地点添加时间窗口约束条件

前面我们为除出发地点(depot)以外的所有地点都添加了时间窗口,接下来我们还需要为每辆车的出发地点也添加时间窗口约束。

# 为每个车辆出发地点添加时间窗口约束
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])

实例化路线开始和结束时间以产生可行的时间

为了最小化车辆的总行程时间,我们需要最小化每辆车的起点和终点的累积时间,更详细的说明请参考官方文档

for i in range(data['num_vehicles']):
    routing.AddVariableMinimizedByFinalizer(
        time_dimension.CumulVar(routing.Start(i)))
    routing.AddVariableMinimizedByFinalizer(
        time_dimension.CumulVar(routing.End(i)))

设置搜索策略

类似于之前TSP应用中的设置,这里我们也需要设置 first_solution_strategy:PATH_CHEAPEST_ARC

search_parameters = pywrapcp.DefaultRoutingSearchParameters()
search_parameters.first_solution_strategy = (
    routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)

定义打印输出函数

打印输出函数最终输出经过求解以后得到的每辆车的行驶路线以及车辆到达每个地点的实际时间窗口,输出路径信息的步骤是:

  1. 获取维度变量:time_dimension。
  2. 遍历每一辆车,得到每辆车访问的起始地点的id,输出车辆id。
  3. 遍历车辆访问过的所有地点,通过维度变量得到地点的累积时间变量time_var, 然后解析出车辆访问该地点的实际时间窗口的最小值时刻和最大值时刻。
  4. 输出最终地点的地点id和实际时间窗口的最小值和最大值。
  5. 输出每辆车行驶路线的总时间,该总时间为最终地点的实际时间窗口的最小值。
def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    print(f'Objective: {solution.ObjectiveValue()}')
    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} Time({1},{2}) -> '.format(
                manager.IndexToNode(index), solution.Min(time_var),
                solution.Max(time_var))
            index = solution.Value(routing.NextVar(index))
        time_var = time_dimension.CumulVar(index)
        plan_output += '{0} Time({1},{2})\n'.format(manager.IndexToNode(index),
                                                    solution.Min(time_var),
                                                    solution.Max(time_var))
        plan_output += 'Time of the route: {}min\n'.format(
            solution.Min(time_var))
        print(plan_output)
        total_time += solution.Min(time_var)
    print('Total time of all routes: {}min'.format(total_time))

这里需要说明一点的是,如果time_var的最小值和最大值相同,则表示实际时间窗口是单个时间点,这意味着车辆一到达就必须离开该位置。另一方面,如果最小值小于最大值,则车辆可以在出发前等待。

定义保存路径信息函数

打印输出车辆路径信息的作用只是为了显示路径信息,不过有时候我们需要将所有的路径信息和实际时间窗口保存到变量中,以供以后使用。

def get_cumul_data(solution, routing, dimension):
  """Get cumulative data from a dimension and store it in an array."""
  # Returns an array cumul_data whose i,j entry contains the minimum and
  # maximum of CumulVar for the dimension at the jth node on route :
  # - cumul_data[i][j][0] is the minimum.
  # - cumul_data[i][j][1] is the maximum.

  cumul_data = []
  for route_nbr in range(routing.vehicles()):
    route_data = []
    index = routing.Start(route_nbr)
    dim_var = dimension.CumulVar(index)
    route_data.append([solution.Min(dim_var), solution.Max(dim_var)])
    while not routing.IsEnd(index):
      index = solution.Value(routing.NextVar(index))
      dim_var = dimension.CumulVar(index)
      route_data.append([solution.Min(dim_var), solution.Max(dim_var)])
    cumul_data.append(route_data)
  return cumul_data

求解并输出结果

这里我们需要说明一下求解的目的:我们需要找到每辆车到达每个地点的最合适的时间窗口,因此车辆到达该地点的实际时间窗口应该在该地点定义的时间窗口范围内,如某个地点定义的时间窗口是[10,15],那么车辆到达该地点的实际时间窗口必须在该地点定义的时间窗口范围内如:[10,14]、[11,13]、[12,15]等都是可行的,另外我们还需要考虑到任意地点之间的时间间隔,因此我们最终的优化目标是为车辆找到访问每个地点的最佳的时间窗口以使车辆行驶的总时间最短。

solution = routing.SolveWithParameters(search_parameters)

if solution:
    print_solution(data, manager, routing, solution)

print()
print("actual time window:")
get_cumul_data(solution, routing, time_dimension)

 

 这里我们需要对输出的路径信息做出说明:

0 Time(0,0) -> 9 Time(2,3) -> 14 Time(7,8) -> 16 Time(11,11) -> 0 Time(18,18)

车辆从地点0出发到达地点 9 处时的求解窗口是Time(2,3),这意味着车辆必须在时间 2 和 3 之间到达地点9。请注意,我们求解窗口包含在该位置的约束时间窗口中,data['time_windows'][9]=(0, 3),因此求解窗口(2,3)处在约束的时间窗口(0,3)的范围内。那为什么求解窗口要从时间 2 开始?这是因为从出发点0到地点 9需要 2 个时间单位,data['time_matrix'][0][9]=2。

为什么车辆可以在 2 到 3 之间的任何时间离开地点 9?原因是由于从地点9到地点14的行程时间为3,如果车辆在3点之前离开,那么它会在6点之前到达地点14,这对于它的访问来说为时过早。因此车辆必须在某处等待,因此如果驾驶员愿意,他可以在位置 9 等待直到时间 3再去访问地点14,但是即使等到时间3过后再去访问地点14,因为9到地点14的行程时间为3这意味着至少需要3个时间单位,那如果路上司机开的慢一点,也可以做到准时到达地点14。如果发现会提早到达某一地点,那意味着时间有余量,此时司机可以通过放慢行驶速度或到别处休息一下的方式来保证准时到达规定的地点。但是晚到则是不被允许的。

您可能已经注意到,一些解决方案的时间窗口(例如在地点9 和 14)具有不同的开始和结束时间,但其他(例如在路线 2 和 3 上)具有相同的开始和结束时间。在前一种情况下,车辆可以等到窗口的尽头再出发,而在后一种情况下,车辆必须一到就离开。

总结

时间窗口约束比之前的VRP案例要稍微复杂一些,车辆必须在规定的时间窗口到达规定的地点,不能早到,也不能晚到, 如果发现会早到,则说明时间有余量,司机可以选择放慢行驶速度或休息一段时间方式来规避早到,但每次休息的时间也不能超过在维度变量中设置的休息时间的上限。

猜你喜欢

转载自blog.csdn.net/weixin_42608414/article/details/122100737