1

python 3.6, or-tools 9.3.10497

For example, I have a node-vehicle matrix that records the weights or scores, like:

Vehicle1 Vehicle2 Vehicle3 Vehicle4
Node1 4 3 2 1
Node2 4 2 1 3
Node3 1 2 3 4
Node4 3 2 4 1
Node5 2 4 1 3
Node6 2 1 4 3

I want to maximize the sum of total weights or scores while meeting the time window.

How to create dimension, maximize constraint, callback function and register it?

I've been searching for days and still can't find a way to do it.

邓瑞颖
  • 138
  • 5
  • 1
    What code have you tried? – Michael S. Jul 28 '22 at 03:42
  • @MichaelS. Well I think the problem is that I can't find a routing.RegisterXXXCallback with from_node_index and vehicle_index as callback function's parameters. Because I need to associate the vehicle with the node. Now I only find routing.RegisterTransitCallback which with from_node_index and to_node_index as callback function's parameters. – 邓瑞颖 Jul 28 '22 at 07:25

1 Answers1

1
  1. AddDimension() can take a array of registered transit callback. so you just need to register one callback per vehicle.
    ref: https://github.com/google/or-tools/blob/e47afb41ead75dd52d66879a553cec2728d76de8/ortools/constraint_solver/routing.h#L545-L547
  2. Solver will try to minimize the objective function so to maximize a score (prize collecting problem) you need to use the opposite value and offset them e.g. here all your values N are in range [1,4] so I've used 5 - N.
#!/usr/bin/env python3
"""
Try to maximise the score by using the correct vehicle
to visit each node.

vehicle  | node1 node2 node3 node4 node5 node6
vehicle 0|     4     4     1     3     2     2
vehicle 1|     3     2     2     2     4     1
vehicle 2|     2     1     3     4     1     4
vehicle 3|     1     3     4     1     3     3
"""

from functools import partial
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['v0'] = [0, 4, 4, 1, 3, 2, 2]
    data['v1'] = [0, 3, 2, 2, 2, 4, 1]
    data['v2'] = [0, 2, 1, 3, 4, 1, 4]
    data['v3'] = [0, 1, 3, 4, 1, 3, 3]
    data['num_locations'] = len(data['v0'])
    data['num_vehicles'] = 4
    data['depot'] = 0
    return data


def print_solution(data, manager, routing, solution):
    """Prints solution on console."""
    print(f'Objective: {solution.ObjectiveValue()}')
    total_distance = 0
    total_score = 0
    for vehicle_id in range(manager.GetNumberOfVehicles()):
        index = routing.Start(vehicle_id)
        plan_output = f'Route for vehicle {vehicle_id}:\n'
        route_distance = 0
        route_score = 0
        while not routing.IsEnd(index):
            node = manager.IndexToNode(index)
            score = data[f'v{vehicle_id}'][node]
            plan_output += f' {node} ({score}) ->'
            previous_index = index
            index = solution.Value(routing.NextVar(index))
            route_distance += 1
            route_score += score
        plan_output += ' {}\n'.format(manager.IndexToNode(index))
        plan_output += 'Distance of the route: {}m\n'.format(route_distance)
        plan_output += 'Score of the route: {}\n'.format(route_score)
        print(plan_output)
        total_distance += route_distance
        total_score += route_score
    print('Total Distance of all routes: {}m'.format(total_distance))
    print('Total Score of all routes: {}'.format(total_score))


def main():
    """Entry point of the program."""
    # Instantiate the data problem.
    data = create_data_model()

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

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

    # Create and register a transit callback.
    def distance_callback(from_index, to_index):
        """Returns the distance between the two nodes."""
        # Convert from routing variable Index to distance matrix NodeIndex.
        from_node = manager.IndexToNode(from_index)
        to_node = manager.IndexToNode(to_index)
        return 1
    transit_callback_index = routing.RegisterTransitCallback(distance_callback)

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

    # Add Scoring constraint.
    def score_callback(from_index, vehicle):
        """Returns the demand of the node."""
        # Convert from routing variable Index to demands NodeIndex.
        from_node = manager.IndexToNode(from_index)
        if from_node == 0:
            return 0
        # note: since the solver will try to minimize the objective but we want
        # to maximise the score we need to pass the opposite value offset by 5
        # to be positive
        return (5 - data[vehicle][from_node]) * 100

    score_callback_indices = []
    for v in ['v0', 'v1', 'v2', 'v3']:
        score_callback_indices.append(routing.RegisterUnaryTransitCallback(
            partial(score_callback, vehicle=v)))

    routing.AddDimensionWithVehicleTransits(
        score_callback_indices,
        0,  # no slack
        1000,  # vehicle maximum score capacities (large enough)
        True,  # start cumul to zero
        'Score')
    score_dimension = routing.GetDimensionOrDie('Score')
    score_dimension.SetSpanCostCoefficientForAllVehicles(1)

    # 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.log_search = True
    search_parameters.time_limit.FromSeconds(1)

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

    # Print solution on console.
    if solution:
        print_solution(data, manager, routing, solution)
    else:
        print('No solution found !')


if __name__ == '__main__':
    main()

possible output:

%./vrp.py
Objective: 610
Route for vehicle 0:
 0 (0) -> 1 (4) -> 2 (4) -> 0
Distance of the route: 3m
Score of the route: 8

Route for vehicle 1:
 0 (0) -> 5 (4) -> 0
Distance of the route: 2m
Score of the route: 4

Route for vehicle 2:
 0 (0) -> 4 (4) -> 6 (4) -> 0
Distance of the route: 3m
Score of the route: 8

Route for vehicle 3:
 0 (0) -> 3 (4) -> 0
Distance of the route: 2m
Score of the route: 4

Total Distance of all routes: 10m
Total Score of all routes: 24
Mizux
  • 8,222
  • 7
  • 32
  • 48
  • OMG it works! Thank you so much for helping me solve this problem. Using functools.partial is so clever. Meanwhile I have another question. In def score_callback(from_index, vehicle), when I delete "if from_node == 0: return 0" (it will return 50 when from_node=0) or I set it to return a slightly larger value, will get wrong solutions. When I set it to return a slightly smaller value, will get correct solutions. My doubt is that the depot is a place which must be passed for every vehicle, and every vehicle will get the same score, why different scores at depot affect final solutions? – 邓瑞颖 Aug 01 '22 at 08:36