Modelling with PyVRP

This quickstart notebook is intended as a first entrypoint to working with PyVRP. We will model and solve several vehicle routing problems, using PyVRP’s high level Model interface. This interface makes it easy to define routing problems directly, using familiar routing terminology of clients, depots, vehicle types, and edges.

We start by modelling the routing problem of a small delivery company with a single depot and a few clients. As the business grows and constraints become more realistic, we adapt our model step-by-step. After the quickstart, you will be able to solve routing problems using PyVRP and are ready for the more focused tutorials presented elsewhere in our documentation.

Note

The problem scale of this notebook is intentionally kept small. In practice, PyVRP can solve routing problems with thousands of clients, often in just a few minutes.

Starting small

Our delivery company starts simple: initially it just wants to deliver a number of boxes to each client to meet the client demand. The boxes are stored at the company’s depot. It has two vehicles available to deliver the boxes with, and each vehicle can carry twenty boxes.

[1]:
import pyvrp
import pyvrp.plotting
import pyvrp.stop
[2]:
DEPOT_COORDINATES = (51.0, 51.0)
COORDINATES = [(39, 76.5), (64.2, 47), (73, 61), (32, 38.8)]
DEMANDS = [5, 3, 7, 2]

Caution

PyVRP automatically converts all numeric input values to integers, except for client and depot (x, y) coordinates. If your data has decimal values, you must scale and convert them to integers first to avoid unexpected behaviour.

Using the COORDINATES and DEMANDS above for all locations, we can specify and solve a simple model as follows. We will use simplified Manhattan distances for the examples. Normally, distances (and durations) need to be obtained using real-world map data, but that is outside the scope of this example.

[3]:
m = pyvrp.Model()

depot = m.add_depot(
    x=DEPOT_COORDINATES[0],
    y=DEPOT_COORDINATES[1],
    name="Depot",
)

vehicles = m.add_vehicle_type(num_available=2, capacity=20)

for idx in range(len(COORDINATES)):
    m.add_client(
        x=COORDINATES[idx][0],
        y=COORDINATES[idx][1],
        delivery=DEMANDS[idx],
        name=f"Client {idx + 1}",
    )

for frm in m.locations:
    for to in m.locations:
        dist = int(abs(frm.x - to.x) + abs(frm.y - to.y))
        m.add_edge(frm, to, distance=dist)

res = m.solve(stop=pyvrp.stop.MaxRuntime(1))  # one second of runtime
PyVRP v0.14.0a0

Solving an instance with:
    1 depot
    4 clients
    2 vehicles (1 vehicle type)

    Iters    Time |      Current OK    Candidate OK         Best OK

Search terminated in 1.00s after 41472 iterations.
Best-found solution has cost 163.

Solution results
================
    # routes: 1
     # trips: 1
   # clients: 4
   objective: 163
    distance: 163
    duration: 0
# iterations: 41472
    run-time: 1.00 seconds

We have quickly obtained a solution to this small problem. PyVRP recommends using a single vehicle:

[4]:
for idx, route in enumerate(res.best.routes()):
    print(f"Route #{idx}:")

    for visit in route.schedule():
        loc = m.locations[visit.location]
        print(f" - At {loc}.")
Route #0:
 - At Depot.
 - At Client 4.
 - At Client 1.
 - At Client 3.
 - At Client 2.
 - At Depot.

PyVRP provides much more information about routes, schedules, and visits. We will investigate that later, once we take into account travel duration and associated constraints. For now, let’s first explore what these routes look like visually:

[5]:
pyvrp.plotting.plot_solution(res.best, m.data())
../_images/notebooks_quick_start_11_0.png

We have succesfully solved our first, simple routing problem: the delivery company now knows what to do.

Improving client and driver satisfaction

Some time later, the company again approaches us. To improve client satisfaction, the business has agreed to deliver during specific time windows. Further, the drivers were unhappy with long shifts, and demand that they only work a maximum shift duration.

To adhere to these new business constraints, we need to update our simple model to include TIME_WINDOWS and a SHIFT_DURATION.

[6]:
TIME_WINDOWS = [(0, 60), (60, 120), (0, 60), (60, 120)]
SHIFT_DURATION = 120

We need to update our model to account for the time windows and vehicle shift duration. That suffices to model the new business constraints.

[7]:
m = pyvrp.Model()

depot = m.add_depot(
    x=DEPOT_COORDINATES[0],
    y=DEPOT_COORDINATES[1],
    name="Depot",
)

vehicles = m.add_vehicle_type(
    num_available=2,
    capacity=20,
    shift_duration=SHIFT_DURATION,
)

for idx in range(len(COORDINATES)):
    m.add_client(
        x=COORDINATES[idx][0],
        y=COORDINATES[idx][1],
        delivery=DEMANDS[idx],
        tw_early=TIME_WINDOWS[idx][0],
        tw_late=TIME_WINDOWS[idx][1],
        name=f"Client {idx + 1}",
    )

for frm in m.locations:
    for to in m.locations:
        dist = int(abs(frm.x - to.x) + abs(frm.y - to.y))
        m.add_edge(frm, to, distance=dist, duration=dist)

res = m.solve(stop=pyvrp.stop.MaxRuntime(1))  # one second of runtime
PyVRP v0.14.0a0

Solving an instance with:
    1 depot
    4 clients
    2 vehicles (1 vehicle type)

    Iters    Time |      Current OK    Candidate OK         Best OK

Search terminated in 1.00s after 35629 iterations.
Best-found solution has cost 183.

Solution results
================
    # routes: 2
     # trips: 2
   # clients: 4
   objective: 183
    distance: 183
    duration: 183
# iterations: 35629
    run-time: 1.00 seconds

PyVRP found a new solution, this time using both vehicles to spread the work. Let’s investigate the route plan:

[8]:
for idx, route in enumerate(res.best.routes()):
    print(f"Route #{idx} [duration = {route.duration()}]:")

    for visit in route.schedule():
        loc = m.locations[visit.location]
        print(f" - [t = {visit.start_service:>03}] At {loc}.")
Route #0 [duration = 71]:
 - [t = 006] At Depot.
 - [t = 038] At Client 3.
 - [t = 060] At Client 2.
 - [t = 077] At Depot.
Route #1 [duration = 112]:
 - [t = 000] At Depot.
 - [t = 037] At Client 1.
 - [t = 081] At Client 4.
 - [t = 112] At Depot.

These routes respect the client time windows, as well as the driver’s shift durations.

[9]:
pyvrp.plotting.plot_solution(res.best, m.data())
../_images/notebooks_quick_start_20_0.png

PyVRP supports many more time and duration constraints beyond time windows and shift durations. These include latest start constraints and overtime costs for vehicles, release times for client deliveries, service durations at clients and depots, and time windows for depots.

A business expansion

Both clients and drivers are happy with the routing decisions made using our updated model. Business is doing well, and a second depot location was recently opened to meet additional demand from new clients. The new depot initially has a single vehicle available. The company again approaches us, to help answer which clients should be serviced from each depot.

Luckily, PyVRP can easily solve routing problems with multiple depots. We simply extend the previous model with new data:

[10]:
NEW_COORDINATES = [(30, 80), (50, 90), (38.5, 82)]
NEW_DEMANDS = [5, 3, 7]
NEW_TIME_WINDOWS = [(0, 80), (0, 80), (80, 120)]
[11]:
# We extend the previous model with the new data.
new_depot = m.add_depot(x=40.0, y=70.0, name="New depot")
new_vehicles = m.add_vehicle_type(
    num_available=1,
    capacity=20,
    start_depot=new_depot,
    end_depot=new_depot,
    shift_duration=SHIFT_DURATION,
)

for idx in range(len(NEW_COORDINATES)):
    m.add_client(
        x=NEW_COORDINATES[idx][0],
        y=NEW_COORDINATES[idx][1],
        delivery=NEW_DEMANDS[idx],
        tw_early=NEW_TIME_WINDOWS[idx][0],
        tw_late=NEW_TIME_WINDOWS[idx][1],
        name=f"New client {idx + 1}",
    )

for frm in m.locations:
    for to in m.locations:
        dist = int(abs(frm.x - to.x) + abs(frm.y - to.y))
        m.add_edge(frm, to, distance=dist, duration=dist)

res = m.solve(stop=pyvrp.stop.MaxRuntime(1))  # one second of runtime
PyVRP v0.14.0a0

Solving an instance with:
    2 depots
    7 clients
    3 vehicles (2 vehicle types)

    Iters    Time |      Current OK    Candidate OK         Best OK

Search terminated in 1.00s after 24467 iterations.
Best-found solution has cost 214.

Solution results
================
    # routes: 3
     # trips: 3
   # clients: 7
   objective: 214
    distance: 214
    duration: 214
# iterations: 24467
    run-time: 1.00 seconds

PyVRP found a solution using three vehicles:

[12]:
for idx, route in enumerate(res.best.routes()):
    print(f"Route #{idx} [duration = {route.duration()}]:")

    for visit in route.schedule():
        loc = m.locations[visit.location]
        print(f" - [t = {visit.start_service:>03}] At {loc}.")
Route #0 [duration = 62]:
 - [t = 029] At Depot.
 - [t = 060] At Client 4.
 - [t = 091] At Depot.
Route #1 [duration = 71]:
 - [t = 006] At Depot.
 - [t = 038] At Client 3.
 - [t = 060] At Client 2.
 - [t = 077] At Depot.
Route #2 [duration = 81]:
 - [t = 012] At New depot.
 - [t = 019] At Client 1.
 - [t = 031] At New client 1.
 - [t = 061] At New client 2.
 - [t = 080] At New client 3.
 - [t = 093] At New depot.

Let’s have a look:

[13]:
pyvrp.plotting.plot_solution(res.best, m.data())
../_images/notebooks_quick_start_28_0.png

Additionally, PyVRP supports vehicles starting and ending their routes at different depots, as well as reloading along the route.

Conclusion

In this quickstart notebook we have solved the business case of a small delivery company. You have learned how to specify depots, vehicle types, and clients using PyVRP’s modelling interface, and learned how to model time and duration constraints, as well as multiple depots. With these fundamentals, you now have a solid grasp of modelling routing problems with PyVRP.

Hint

Along the way, we have hinted at various additional constraints and features that PyVRP supports. These features are treated in more detail in the tutorials.

Hint

The API reference contains a complete reference of our implementation.