【问题标题】:OR-Tools VRP with multiple depots only works for dummy cases具有多个仓库的 OR-Tools VRP 仅适用于虚拟案例
【发布时间】:2022-06-07 17:27:51
【问题描述】:

问题

我已经使用 OR-Tools 实现了一个 MDVRP(多仓库 VRP)来生成多车辆的交付路线。但是,求解器仅在交付数量非常少(< 15)时才能找到解决方案。我想知道这是因为实现中的错误还是求解器功能的限制。

模型定义

  • 我有D包含在路线中的交货(都需要发货)
  • 我有W可以使用的仓库(每条路线都在同一个仓库开始和结束)
  • 每个交货都有一个预先分配的仓库(运送的车辆必须在预先指定的仓库中开始和结束路线)
  • 我有每辆车最多可运送 M 辆货物的车辆
  • 我想生成最小化距离总和的路线

多个仓库的实现

鉴于默认情况下 OR-Tools 仅允许使用 1 个仓库,我已完成以下更改以拥有多个仓库:

包括虚拟节点

  • 对于每辆车 (vᵢ) 和仓库 (wⱼ):

    • 我已经创建了一个仓库起始节点开始_vᵢ_wⱼ
    • 我已经创建了一个仓库结束节点结束_vᵢ_wⱼ
  • 对于每辆车(vᵢ):

    • 我为该车辆 vᵢ 的所有起始节点添加了析取,因此每辆车仅启动一次:
      Disjunction(start_vᵢ_w₁, start_vᵢ_w₂, ..., start_vᵢ_w<sub>W</sub>)
    • 我为该车辆的所有结束节点添加了析取 vᵢ 只完成一次:
      Disjunction(end_vᵢ_w₁, end_vᵢ_w₂, ..., end_vᵢ_w<sub>W</sub>)

    这就是我设置析取的方式:

    routing.AddDisjunction([manager.NodeToIndex(index) for index in indices])
    

矩阵值

使用额外的节点,需要调整距离矩阵。这些是我遵循的规则:

原始仓库

  • 从原始站点到任何起始节点的距离为 0。
  • 从原始仓库到任何其他节点的距离为 INF(路线必须始终从起始节点开始)

启动节点

  • 从仓库 i 的任何起始节点到分配给仓库 i 的交货节点的距离是位置之间的距离
  • 从仓库 i 的任何起始节点到分配给任何其他仓库的交货节点的距离为 INF
  • 起始节点start_vᵢ_wⱼ到结束节点end_vᵢ_wⱼ的距离为0(路线可以为空).
  • 从起始节点start_vᵢ_wⱼ 到任何其他结束节点的距离为 INF。

交付节点

  • 同一仓库的任​​何交货到任何其他交货的距离是位置之间的距离。对于不同仓库的交货,距离为 INF。
  • 从任何交货到任何起始节点或原始仓库的距离为 INF。
  • 从交货到同一仓库的末端节点的距离是位置之间的距离。到不同仓库的末端节点的距离是INF。

结束节点

  • 从任何结束节点到任何起始节点、到任何交付或任何其他结束节点的距离都是 INF。
  • 任何末端节点到原站的距离为 0(路线总是在原站结束)

2 辆车、2 个仓库和 3 次交货的虚拟案例的距离/成本示例。

每个节点显示节点的类型和距离矩阵中的索引。对于开始和结束节点,显示了车辆和仓库。对于交货,还显示分配的仓库。未显示的任何其他连接的成本是 INF。

Python 实现

我附上了 Python 中的实现草案。使用当前输入(3 个交货、2 个仓库和 2 个车辆),它可以正常工作。但是,例如,将交付数量增加到 15 个时,它没有找到解决方案。既不增加执行时间。

import random
from typing import List
from ortools.constraint_solver import routing_enums_pb2
from ortools.constraint_solver import pywrapcp
from math import radians, cos, sin, asin, sqrt
from enum import Enum
from random import uniform
import tabulate

random.seed(0)

INF = 1e15


class RT(Enum):
    DEPOT = 1
    START = 2
    END = 3
    DELIVERY = 4


class BasicElements(Enum):
    WAREHOUSE = 1
    DELIVERY = 2


class RoutingElement:

    def __init__(self, warehouse: int, routing_type: RT, vehicle, index):
        self.warehouse: int = warehouse
        self.routing_type: RT = routing_type
        self.vehicle = vehicle
        self.index = index

    def calculate_matrix_value_between_elements(self, other, matrix: list):

        # FROM AND TO: Original Depot Cases
        if self.routing_type == RT.DEPOT:
            if other.routing_type == RT.START:
                return 0
            else:
                return INF
        if other.routing_type == RT.DEPOT:
            if self.routing_type == RT.END:
                return 0
            else:
                return INF

        # FROM: Real Warehouse Start
        if self.routing_type == RT.START:
            if other.routing_type == RT.START:
                return INF
            if other.routing_type == RT.END:
                if self.vehicle == other.vehicle and self.warehouse == other.warehouse:
                    return 0
                else:
                    return INF
            if other.routing_type == RT.DELIVERY:
                if self.warehouse == other.warehouse:
                    return matrix[self.index][other.index]
                else:
                    return INF
            else:
                raise Exception

        # FROM: Real Warehouse End
        if self.routing_type == RT.END:
            return INF

        # FROM: Delivery
        if self.routing_type == RT.DELIVERY:
            if other.routing_type == RT.START:
                return INF
            if self.warehouse != other.warehouse:
                return INF
            else:
                return matrix[self.index][other.index]
        raise Exception

    def equals(self, other):
        return self.routing_type == other.routing_type \\
               and self.warehouse == other.warehouse \\
               and self.index == other.index \\
               and self.vehicle == other.vehicle


def haversine(lon1, lat1, lon2, lat2):
    lon1, lat1, lon2, lat2 = map(radians, [lon1, lat1, lon2, lat2])
    dlon = lon2 - lon1
    dlat = lat2 - lat1
    a = sin(dlat / 2) ** 2 + cos(lat1) * cos(lat2) * sin(dlon / 2) ** 2
    c = 2 * asin(sqrt(a))
    r = 6371 * 1000
    return int(c * r)


def get_distance_matrix(latitudes, longitudes):
    return [
        [
            haversine(longitudes[i], latitudes[i], longitudes[j], latitudes[j]) for i in range(len(latitudes))
        ]
        for j in range(len(latitudes))
    ]


def convert_routing_elements(elements, n_vehicles):
    routing_elements = [RoutingElement(-1, RT.DEPOT, None, None)]
    for element_id, element in enumerate(elements):
        if element[1] == BasicElements.WAREHOUSE:
            for vehicle_id in range(n_vehicles):
                routing_elements.append(
                    RoutingElement(element[0], RT.START, vehicle_id, element_id)
                )
            for vehicle_id in range(n_vehicles):
                routing_elements.append(
                    RoutingElement(element[0], RT.END, vehicle_id, element_id)
                )
        elif element[1] == BasicElements.DELIVERY:
            routing_elements.append(
                RoutingElement(element[0], RT.DELIVERY, None, element_id)
            )
        else:
            raise Exception
    return routing_elements


def transform_matrix(matrix: List[List[float]], routing_elements: List[RoutingElement]):
    new_matrix = []
    for i1, e1 in enumerate(routing_elements):
        new_row = []
        for i2, e2 in enumerate(routing_elements):
            new_row.append(0 if i1 == i2 else e1.calculate_matrix_value_between_elements(e2, matrix))
        new_matrix.append(new_row)
    return new_matrix


def print_solution(data, manager, routing, solution):
    for vehicle_id in range(data[\'num_vehicles\']):
        index = routing.Start(vehicle_id)
        plan_output = \'Route for vehicle {}:\\n\'.format(vehicle_id)
        route_distance = 0
        route_stops = 0
        while not routing.IsEnd(index):
            route_stops += 1
            plan_output += \' {} -> \'.format(manager.IndexToNode(index))
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += data[\"distance_matrix\"][manager.IndexToNode(previous_index)][manager.IndexToNode(index)]
        plan_output += \'{}\\n\'.format(manager.IndexToNode(index))
        plan_output += \'Distance of the route: {}m\\n\'.format(route_distance)
        if route_stops > 3:
            print(plan_output)


def print_matrix(distance_matrix, routing_elements):
    headers = [f\"({i}) {x.routing_type}\" for i, x in enumerate(routing_elements)]
    matrix_with_row_names = [[headers[i]] + d for i, d in enumerate(distance_matrix)]
    print(tabulate.tabulate(matrix_with_row_names, headers=headers))


def main():

    # INPUT #
    n_vehicles = 2
    max_deliveries_per_vehicle = 10

    # Use 2 warehouses
    warehouses = [
        [\"W_1\", 41.2, 2.2, 1, BasicElements.WAREHOUSE],
        [\"W_2\", 41.4, 2.3, 2, BasicElements.WAREHOUSE]
    ]

    # Create \"total_deliveries\" with half assigned to warehouse 1 and the other half to warehouse 2
    total_deliveries = 3

    deliveries = [
        [f\"D_{i}\", uniform(41.0, 41.5), uniform(2.1, 2.4), 1 if i < total_deliveries / 2 else 2, BasicElements.DELIVERY]
        for i in range(total_deliveries)
    ]

    # END INPUT #

    deliveries_and_warehouses = warehouses + deliveries
    distance_matrix = get_distance_matrix(
        [element[1] for element in deliveries_and_warehouses], [element[2] for element in deliveries_and_warehouses]
    )

    # Create all the needed elements to solve the problem with multiple pickups
    routing_elements: List[RoutingElement] = convert_routing_elements(
        [[element[3], element[4]] for element in deliveries_and_warehouses], n_vehicles
    )
    distance_matrix = transform_matrix(distance_matrix, routing_elements)
    if len(deliveries_and_warehouses) < 6:
        print_matrix(distance_matrix, routing_elements)

    # Instantiate the routing elements
    data = {\"distance_matrix\": distance_matrix, \"num_vehicles\": n_vehicles, \"depot\": 0}
    manager = pywrapcp.RoutingIndexManager(len(data[\'distance_matrix\']), data[\'num_vehicles\'], data[\'depot\'])
    routing = pywrapcp.RoutingModel(manager)

    def distance_callback(from_index, to_index):
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return data[\'distance_matrix\'][from_node][to_node]

    transit_callback_index = routing.RegisterTransitCallback(distance_callback)
    routing.AddDimension(transit_callback_index, 0, 1000000000, True, \'Distance\')

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

    def max_deliveries_callback(from_index):
        from_node = manager.IndexToNode(from_index)
        return 0 if from_node < n_vehicles * len(warehouses) * 2 + 1 else 1

    deliveries_per_route_callback_index = routing.RegisterUnaryTransitCallback(max_deliveries_callback)
    routing.AddDimension(deliveries_per_route_callback_index, 0, max_deliveries_per_vehicle, True, \"Max_deliveries\")

    # Create disjunctions between all the start nodes (one per vehicle) of a given warehouse
    for i in range(n_vehicles * 2):
        indices = [i + 1 + j * n_vehicles * 2 for j in range(len(warehouses))]
        routing.AddDisjunction([manager.NodeToIndex(index) for index in indices])

    # Minimize number of vehicles used
    routing.SetFixedCostOfAllVehicles(100000000)

    # 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 = 10
    #search_parameters.log_search = True

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

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

    else:
        print(f\"**********************                **********************\")
        print(\"********************** NO SOLUTION FOUND **********************\")
        print(f\"**********************                **********************\")


if __name__ == \'__main__\':
    main()


    标签: or-tools


    【解决方案1】:

    我已经能够使用更简单的方法解决问题,现在求解器能够非常快速地为数百个交付提出解决方案。

    关键是我事先知道每次交付的对应仓库,所以我不需要 START 和 END 节点,因此也不需要它们之间的分离。

    在构建距离矩阵时,从仓库(索引 0)到每个交货的距离是该交货的相应仓库到交货地点的距离。然后,将不同仓库交货之间的距离设置为 INF,该值高于距离维度中允许的最大距离。

    这样,只有分配到同一仓库的交货才能一起发送。最后,在构建解决方案时,我还考虑了每次交付的预分配仓库,因此我在每条路线中使用正确的仓库。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2021-10-15
      • 2011-08-21
      • 2021-04-06
      • 1970-01-01
      • 2019-11-29
      • 2021-12-29
      • 2021-06-14
      • 1970-01-01
      相关资源
      最近更新 更多