Supporting new VRP variants

On this page we describe some guidelines and ‘gotchas’ to think about while adding support for new VRP variants to PyVRP. These are given as hints. To make these guidelines concrete, we take examples from our experience adding support for prize-collecting VRPs, which was added to PyVRP in version 0.3.0. For reference, the pull request implementing prize-collecting is available here.

Note

If you want to merge your work into PyVRP itself, please first open an issue in the repository. It is also recommended to open an early draft pull request. How to set everything up for development is documented on the contributing page.

Adding support for new VRP variants starts with thinking about where to add the new problem data needed to solve it. In the case of prize-collecting, this meant adding the prize: int and required: bool fields to the Client object:

  • The prize field indicates the prize obtained for visiting the client;

  • The required field indicates whether the client must be part of a feasible solution, regardless of the prize.

Adding such data requires modifying the Client class, which is written in C++. After changing C++ functionality, don’t forget to update the Python bindings and type stub files with the new changes!

Hint

When adding or modifying C++ functionality, don’t forget to update the Python bindings and type stub files!

When adding new data fields, it is particularly important to think about sane default values. For example, the standard CVRP and VRPTW do not consider prizes, and all clients must be visited in those problem settings. It thus makes sense to make prize-collecting an opt-in feature, with defaults prize = 0 and required = True.

Hint

Think about sane default values for any new data fields!

With the required problem data now available, the next step is to think about what needs to change in terms of the objective, and how to support its evaluation. This is not always required: for example, if your VRP variant only introduces additional constraints, you can typically skip this step. In the case of prize-collecting, however, the prizes must be added to the objective.

This requires modifying the Solution’s and CostEvaluator’s implementation of the cost components. In particular, we updated the Solution to also compute the total value of all uncollected prizes, and changed cost() to compute

\[\overbrace{\sum_{(i, j)} d_{ij} x_{ij}}^{\text{distance}} + \overbrace{\sum_{i} p_i (1 - y_i)}^{\text{uncollected prizes}},\]

where \(x_{ij} \in \{0, 1\}\) indicates if arc \((i, j)\) is used, and \(y_i \in \{0, 1\}\) indicates if client \(i\) is visited.

Hint

Think about whether you need to modify the objective function. If you do, a good starting point is to look at the implementations of the Solution and the CostEvaluator.

The next step is to modify the search methods in pyvrp.search to respect the constraints of the new VRP variant. Depending on the VRP variant in question, this might be simple, or it could be a lot of work: if you are unsure whether your VRP variant will be a lot of work to support, please consider opening an issue for discussion first.

Hint

Open an issue if you are unsure about how to proceed implementing your VRP variant.

In the pyvrp.search module, either changes need to be made to the operators, or the LocalSearch object. In the case of prize-collecting, it was the latter: we added support for evaluating (and applying) moves that inserted a client into the solution, or removed a client from it. The required evaluation logic was easy to write by looking at the implementation of Exchange10.

With those changes in place, a basic implementation supporting the new VRP variant is typically already functional. This is more than sufficient for an initial patch, so please open a pull request around this time. To get that pull request merged, two more things are required:

  • Tests exercising the new or modified code. These tests should check edge cases that are not supported (are errors raised when they should?), and ensure the basic functionality is correct.

  • Benchmark results, on existing benchmark instances (see benchmarking for an explanation of how we benchmark), and possibly on benchmark instances for the new VRP variant. Benchmarks on existing instances ensures the new code does not cause a performance regression, and benchmarks on new instances ensures we have a baseline for the newly supported VRP variant. New benchmark instances are not strictly necessary if the changes are very small (e.g., only adding a single new constraint) - this will be decided on a case-by-case basis during review of your pull request.

Hint

A successful pull request adds tests and shows benchmark results!

With a basic implementation in place, PyVRP should now be able to find solutions for the new VRP variant. Of course, it might be possible to further improve the implementation. In the case of prize-collecting, after the initial implementation, we also:

  • Modified the computation of the granular neighbourhood in compute_neighbours() to take prizes into account.

  • Updated various statistics to display the number of clients in a solution.

  • Changed neighbours() to return None in case a client is not in the solution.

Such changes may come about later, as we further improve support for a new VRP variant: the pull request adding initial support should ideally be kept as simple as possible.

Hint

Keep it simple. It is always possible to further improve support for a VRP variant in later pull requests.

We hope that the guidelines on this page will prove helpful when adding support for a new VRP variant.

Note

For further inspiration, you may want to look at the pull requests that added: