Совместимость узлов оптимизации маршрутизации OR-tools

Я пытаюсь решить проблему емкостной маршрутизации, когда у меня есть набор узлов, для которых требуется разное количество и разные типы элементов.
Кроме того, я хочу разрешить отбрасывание узлов, потому что все узлы с одним типом элементов могут по-прежнему превышать вместимость транспортного средства и, следовательно, не приведет к решению.
Однако в конечном итоге все узлы должны быть обслужены, поэтому я использую итеративный подход, при котором я рассматривал каждый тип элемента как отдельную проблему маршрутизации.
Но мне было интересно, можно ли использовать дизъюнкции или что-то подобное для решения «глобальной» проблемы маршрутизации. Любая помощь в том, возможно ли это, приветствуется.

Example:
Node 1 - item A - demand 10
Node 2 - item A - demand 10
Node 3 - item A - demand 12
Node 4 - item B - demand 10
Node 5 - item B - demand 10

vehicle I - capacity 20
vehicle II - capacity 10

Мой подход:
Сначала решите для элемента A: транспортное средство, которое я обслуживаю, узлы 1 и 2, узел 3 отброшен, сохраните отброшенные узлы для более поздней итерации
Затем решите для элемента B: транспортное средство, которое я обслуживаю узлы 4 и 5, транспортное средство II простаивает Решить для оставшегося узла 3: машина I обслуживает узел 3

ИЗМЕНИТЬ Я скорректировал свой подход, чтобы он соответствовал ответу @mizux. Ниже кода:

РЕДАКТИРОВАТЬ2. Исправлена ​​ошибка, из-за которой функция обратного вызова спроса из первых циклов по-прежнему ссылалась на переменную product_index и, таким образом, возвращала неправильное требование. Исправить с помощью functools.partial.

import functools
from ortools.constraint_solver import pywrapcp, routing_enums_pb2

class CVRP():
    def __init__(self, data):
        # assert all(data['demands'] < max(data['vehicle_capacities'])) # if any demand exceeds cap no solution possible
        self.data = data

        self.vehicle_names_internal = [f'{i}:{j}' for j in data['products'] for i in data['vehicle_names']]
        self.manager = pywrapcp.RoutingIndexManager(len(data['distance_matrix']), len(self.vehicle_names_internal), data['depot'])
        self.routing = pywrapcp.RoutingModel(self.manager)

        transit_callback_id = self.routing.RegisterTransitCallback(self._dist_callback)      
        self.routing.SetArcCostEvaluatorOfAllVehicles(transit_callback_id)
        
        # set up dimension for each product type for vehicle capacity constraint
        for product_index, product in enumerate(data['products']):
            dem_product_callback = functools.partial(self._dem_callback_generic, product_index=product_index)
            dem_callback_id = self.routing.RegisterUnaryTransitCallback(dem_product_callback)
            vehicle_product_capacity = [0 for i in range(len(self.vehicle_names_internal))]
            vehicle_product_capacity[product_index*data['num_vehicles']:product_index*data['num_vehicles']+data['num_vehicles']] = data['vehicle_capacities']
            print(product_index, product)
            print(self.vehicle_names_internal)
            print(vehicle_product_capacity)
            self.routing.AddDimensionWithVehicleCapacity(
                dem_callback_id,
                0,
                vehicle_product_capacity,
                True,
                f'capacity_{product}',
                )

        # disjunction (allow node drops)
        penalty = int(self.data['distance_matrix'].sum()+1) # penalty needs to be higher than total travel distance in order to only drop locations if not other feasible solution
        for field_pos_idx_arr in self.data['disjunctions']: 
            self.routing.AddDisjunction([self.manager.NodeToIndex(i) for i in field_pos_idx_arr], penalty)

        
    def _dist_callback(self, i, j):
        return self.data['distance_matrix'][self.manager.IndexToNode(i)][self.manager.IndexToNode(j)]
    
    def _dem_callback_generic(self, i, product_index):
        node = self.manager.IndexToNode(i)
        if node == self.data['depot']:
            return 0
        else:
            return self.data['demands'][node, product_index]

    def solve(self, verbose=False):    
        search_parameters = pywrapcp.DefaultRoutingSearchParameters()
        search_parameters.first_solution_strategy = (
            routing_enums_pb2.FirstSolutionStrategy.AUTOMATIC)
        search_parameters.local_search_metaheuristic = (
            routing_enums_pb2.LocalSearchMetaheuristic.AUTOMATIC)
        search_parameters.time_limit.FromSeconds(30)

        self.solution = self.routing.SolveWithParameters(search_parameters)

person the_man_in_black    schedule 19.02.2021    source источник
comment
В вашем примере общий спрос превышает общую вместимость ваших транспортных средств. Вы не найдете решения, которое обслуживает все узлы. Если вы можете отправлять автомобили в несколько туров, я предлагаю вам увеличить количество автомобилей, скопировав существующие. Можете ли вы добавить дополнительную информацию о дизъюнкции, которую вы ожидаете, например только один тип предметов на автомобиль?   -  person larsl    schedule 19.02.2021
comment
Я знаю, поэтому я разрешаю отбрасывать узлы (со штрафом) и создавать новую проблему с оставшимися, непосещенными узлами. Что касается дизъюнкций: каждое транспортное средство может загружать только один тип предметов, и не может быть маршрута с узлами, которые должны обслуживать разные предметы. Однако я не знаю, как это точно представить в виде кода.   -  person the_man_in_black    schedule 19.02.2021


Ответы (1)


  1. Вы должны создать два измерения емкости, по одному для каждого типа. В каждом месте вы увеличиваете соответствующее измерение.

  2. Вы можете продублировать свой автомобиль для каждого типа элемента, например:

    • v0, Vehicle 1 Type A with: capacity A: 20, capacity B: 0
    • v1, Автомобиль 1 Тип B с: вместимостью A: 0, вместимостью B: 20
    • v2, Автомобиль 2 типа A с: вместимостью A: 10, вместимостью B: 0
    • v3, Автомобиль 2 Тип B с: вместимостью A: 0, вместимостью B: 10

    примечание: вы можете воспроизвести его, чтобы разрешить несколько поездок

  3. Вы можете создать узел ворот, чтобы разрешить только одну конфигурацию транспортного средства. например Чтобы разрешить только v0 или v1 посетить

    v0_start = routing.Start(0)
    v0_end = routing.End(0)
    v1_start = routing.Start(1)
    v1_end = routing.End(1)
    gate_index = manager.NodeToIndex(gate_index)
    routing.NextVar(v0_start).setValues[gate_index, v0_end]
    routing.NextVar(v1_start).setValues[gate_index, v1_end]
    

    Поскольку узел можно посетить только один раз, одно транспортное средство из v0 и v1 может пройти мимо узла ворот, в то время как у другого нет другого выбора, кроме как перейти к конечному узлу, то есть пустому маршруту, который вы можете удалить при постобработке назначения.

  4. Вы также можете добавить автомобиль FixedCost в программу расчета стимулов, чтобы использовать автомобиль II, если он дешевле, чем автомобиль I и т. Д.

  5. Добавьте каждое местоположение в дизъюнкцию, чтобы решатель мог при необходимости отбросить их.

    location_index = manager.NodeToIndex(location_id)
    routing.AddDisjunction(
      [location_index], # locations
      penalty,
      max_cardinality=1 # you can omit it since it is already 1 by default
    )
    
person Mizux    schedule 21.02.2021
comment
Я предполагаю, что расположение ворот может быть произвольным, например. точно так же, как депо? - person the_man_in_black; 22.02.2021
comment
да, обычно у вас есть depot->gate: 0, gate->any == depot->any, поэтому при отображении решения вы можете удалить ворота и отобразить depot->Next(gate) - person Mizux; 22.02.2021
comment
Я скорректировал свой подход к вашему ответу. У меня есть одно измерение для каждого типа продукта, а возвращаемое значение обратного вызова спроса установлено на 0 для узлов, которым нужен другой тип продукта. Однако теперь решающая программа возвращает маршрут, который обслуживает все узлы с неправильным продуктом и, следовательно, с нулевой загрузкой. Нужно ли доводить их спрос на неподходящий продукт до бесконечности? - person the_man_in_black; 22.02.2021
comment
вы должны предоставить суть использования данных, чтобы мы могли их отладить, вы также можете связаться с нами в списке рассылки or-tools или в нашем Discord (значок чата на README.md проекта github) - person Mizux; 23.02.2021
comment
Теперь он работает благодаря исправлению, которое я упоминаю в EDIT2 - person the_man_in_black; 23.02.2021