Optional clients

In this short tutorial we introduce optional clients which offer a reward (a prize) when they are serviced on a route, but whose service is not required for feasibility. This routing variant is often called a prize-collecting vehicle routing problem, and PyVRP supports this out-of-the-box.

[1]:
import pyvrp
import pyvrp.plotting
import pyvrp.stop

We will set different prize values for different clients. This can be used to reflect different service priorities.

[2]:
COORDS = [(456, 320), (312, 418), (114, 80), (570, 160), (684, 240)]
PRIZES = [500, 300, 500, 300]

Important

When modelling optional clients, do not forget to provide both a reward (the prize argument to add_client), and to mark the client as optional by setting required=False!

[3]:
m = pyvrp.Model()

m.add_depot(x=COORDS[0][0], y=COORDS[0][1])
m.add_vehicle_type(1)

for idx in range(1, len(COORDS)):
    m.add_client(
        x=COORDS[idx][0],
        y=COORDS[idx][1],
        prize=PRIZES[idx - 1],
        required=False,
    )

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

res = m.solve(stop=pyvrp.stop.MaxRuntime(1))
PyVRP v0.14.0a0

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

    Iters    Time |      Current OK    Candidate OK         Best OK

Search terminated in 1.00s after 51962 iterations.
Best-found solution has cost 1560.

Solution results
================
    # routes: 1
     # trips: 1
   # clients: 3
   objective: 1560
    distance: 1260
    duration: 0
# iterations: 51962
    run-time: 1.00 seconds

PyVRP quickly finds a solution visiting three clients. Since the instance has four clients, one client is unvisited. Let’s visually investigate why:

[4]:
pyvrp.plotting.plot_solution(res.best, m.data(), plot_clients=True)
../_images/notebooks_optional_clients_7_0.png

The three visited clients are all close to the depot, but the one unvisited client is not. That’s why PyVRP has decided not to service that client: the prize value is not sufficiently high to warrant a detour.

We can incentivise PyVRP to service this client by setting a higher prize, or enforce service by making the visit required. Let’s try to increase the prize, and solve the model again:

[5]:
PRIZES[1] = 1_000
[6]:
m = pyvrp.Model()

m.add_depot(x=COORDS[0][0], y=COORDS[0][1])
m.add_vehicle_type(1)

for idx in range(1, len(COORDS)):
    m.add_client(
        x=COORDS[idx][0],
        y=COORDS[idx][1],
        prize=PRIZES[idx - 1],
        required=False,
    )

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

res = m.solve(stop=pyvrp.stop.MaxRuntime(1))
PyVRP v0.14.0a0

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

    Iters    Time |      Current OK    Candidate OK         Best OK

Search terminated in 1.00s after 48559 iterations.
Best-found solution has cost 1816.

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

With the prize increase, visiting all clients is now worthwhile:

[7]:
pyvrp.plotting.plot_solution(res.best, m.data(), plot_clients=True)
../_images/notebooks_optional_clients_13_0.png

Conclusion

In this tutorial you have learned about modelling optional clients using PyVRP, and incentivising the solver to visit optional clients using the client’s prize attribute. Optional clients are very helpful to ensure a feasible solution is always obtained, even when there are constraints that prevent PyVRP from servicing all clients. Furthermore, different prizes can be used to model different visit priorities: all else equal, PyVRP will prefer to schedule a higher-prize visit over one where it collects a lower prize.