Tobgen
Tobgen

Reputation: 13

Google OR-tools VRP - Pickup/Dropoff at same node

I'm currently using Python and the Google-Or-Tools to solve an VRP problem, that I am not sure how exactly to model. (If someone has a solution with another library/tool, absolutely interested as well)

Problem Statement:

I have basically the classical CVRP problem also described and modelled on the documentation page, but with one addition. So the basic CVRP problem is, I have a depot where cargo is loaded and vehicles start from / end at. Also I have locations where goods are to be delivered to, e.g. they have a demand. Now the addition is, that I not only dropoff goods at the specific locations but also want to pickup goods at the same locations, and eventually dropping them off at the depot again.

Since it is possible for a location to have more goods to pickup than to dropoff, I need to explicitly check this, too ... but I can't find a way so far to do this.

Example: So say I have one Depot node 0 and two location nodes (A,B).

Now for a vehicle with max capcity 20 the possible solution would be:

First visiting A and then B would not work, since it would violate the capacity constraint in A.

What I've tried:

So to consider the basic dropoff capacity constraint I have the standard

routing.AddDimensionWithVehicleCapacity(
    dropoff_callback_index,
    0,  # null capacity slack
    data['vehicle_capacities'],  # vehicle maximum capacities
    True,  # start cumul to zero
    'Capacity')

However, for the pickup I can ofcourse add another one using the pickup demands. But obviously this will not suffice but I would need to combine those two. Since technically the model cumulates the volume over the nodes and does only know the amount of capacities a vehicle is starting from the depot after forming a complete trip, I can't build such a dimension that can check the constraint while running through the nodes but only after he found a complete trip.

So this is what I believe a possible way to do this, kinda check after a solution is found, if the then known volume a vehicle would load in the depot initially (i.e. the sum of dropoffs of a route) fits considering the pickups and does not violate the vehicle capacity. Not sure though, how exactly to do this. Does anyone have an idea here?

Also I tried modelling it using the pickup and delivery model, and splitting one shipment with pickup and delivery into two shipments, also duplicating the nodes to have two nodes instead of one (since one node cannot be pickup and delivery). However, since my tours are all starting from depot / to depot, I would also need to duplicate the depot nodes and then attach an pickup/dropoff demand for them, which doesn't make sense because I can't set this in advance, but the model should find a solution (hope this makes sense to you).

I tried searching for similar problems (here, googlegroups, github) but didn't manage to find anything helpful.

Upvotes: 1

Views: 1857

Answers (2)

Mizux
Mizux

Reputation: 9281

disclaimer: answer of @Tobgen question above:

With start_cumul_to_zero=False I assumed this is like the truck starts fully loaded.

If start_cumul_to_zero=True the solver will simply force start node to 0: equivakebt to:

for v in range(manager.GetNumberOfVehicles()):
  index = routing.Start(v)
  dim.CumulVar(index).SetValue(0)

otherwise it will keep its original range [0-capacity] (ed here [0,20]) I.e. at beginning all variables have a range but not a fixed value.

Actually the goal of the solver is to fix all variables to find a solution while respecting all constraint (thus the name constraint programming ;) )...

So supposing you visit first node A, solver will "think" ok, I have a delivery of 10 so I need at least 10 goods at start so I will update the range of the depot variable to [10,20], and A is now in range [0,10]. Then later trying to add B, ok I need at least 10 in A to deliver goods to B so new range of A is [10,10] which also means range of start is [20,20] and B is [0,0] etc...

It just says that for the start node (i.e. 0, since all vehicles start their) both dimensions shall have the same value. But isn't this already implied by the maximum vehicle capacity and the fact that start_cumul_to_zero=False ?

No start_cumul_to_zero=False only avoid to fix start to 0 otherwise solver is free to fix it to any value in the domain [0,capacity]

Without this constraint solver is free to use whatever it wants.
e.g. 20 for delivery since you have two -10 delivery (and CumulVar can't be negative !) But it is also free to set Total to 0 thus Start -> B -> A -> End become ok.

see:

Dimension Start B A End
Delivery 20 10 (-10) 0(-10) 0
TotalLoad 0 1(-10+11) 0(-10+9) 0

With the constraint, however, you'll have:

Dimension Start B
Delivery 20 10 (-10)
TotalLoad Delivery(Start) aka 20 21 PROBLEM! (-10+11)

ps: You can also join us on Discord (see chat badge on the README.md on github)

Upvotes: 1

Mizux
Mizux

Reputation: 9281

IIRC, already answered on the mailing list.
Here a gist with a sample: Mizux/vrp_collect_deliver.py

Basically you'll need two dimensions.

  • One to track only deliveries goods
  • One to track the total load (pickup and delivery)
  • One constraint on start nodes to make both dimension cumul vars equals.

Main idea: Having two dimensions instead of one, will avoid the solver to use pickup goods to perform deliveries.

Field Start A B End
Deliveries 0 -10 -10 0
Total 0 -10+11 -10+9 0
deliveries = [0, -10, -10, 0]
total = [0, +1, -1, 0]

...

# Add Deliveries constraint.
def delivery_callback(from_index):
    """Returns the demand of the node."""
    # Convert from routing variable Index to demands NodeIndex.
    from_node = manager.IndexToNode(from_index)
    return deliveries[from_node]
    
delivery_callback_index = routing.RegisterUnaryTransitCallback(delivery_callback)
routing.AddDimensionWithVehicleCapacity(
    delivery_callback_index,
    0,  # null capacity slack
    20,  # vehicle maximum capacities
    False, # start_cumul_to_zero=False since we start full of goods to deliver
    'Deliveries')

# Add Load constraint.
def load_callback(from_index):
    """Returns the load of the node."""
    # Convert from routing variable Index to demands NodeIndex.
    from_node = manager.IndexToNode(from_index)
    return total[from_node]

load_callback_index = routing.RegisterUnaryTransitCallback(load_callback)
routing.AddDimensionWithVehicleCapacity(
    load_callback_index,
    0,  # null capacity slack
    20,  # vehicle maximum capacities
    False,  # start_cumul_to_zero=False
    'Loads')

    # Add Constraint Both cumulVar are identical at start
    deliveries_dimension = routing.GetDimensionOrDie('Deliveries')
    loads_dimension = routing.GetDimensionOrDie('Loads')
    for vehicle_id in range(manager.GetNumberOfVehicles()):
      index = routing.Start(vehicle_id)
      routing.solver().Add(
          deliveries_dimension.CumulVar(index) == loads_dimension.CumulVar(index))

...

Upvotes: 2

Related Questions