Skip to content

Commit a7126bb

Browse files
authored
Route schedule statistics (PyVRP#716)
* Route visit/schedule attributes * Basic implementation, make the schedule data pickleable * Add tests * Add to notebook
1 parent 8fe1618 commit a7126bb

File tree

8 files changed

+245
-22
lines changed

8 files changed

+245
-22
lines changed

docs/source/api/pyvrp.rst

+3
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ This object can be used to obtain the best observed solution, and detailed runti
103103
.. autoclass:: ProblemData
104104
:members:
105105

106+
.. autoclass:: ScheduledVisit
107+
:members:
108+
106109
.. autoclass:: DynamicBitset
107110
:members:
108111
:special-members: __and__, __or__, __xor__, __getitem__, __setitem__,

examples/basic_vrps.ipynb

+54-7
Original file line numberDiff line numberDiff line change
@@ -323,9 +323,7 @@
323323
" for route in routes\n",
324324
"]\n",
325325
"\n",
326-
"header = list(data[0].keys())\n",
327-
"rows = [datum.values() for datum in data]\n",
328-
"tabulate(rows, header, tablefmt=\"html\")"
326+
"tabulate(data, headers=\"keys\", tablefmt=\"html\")"
329327
]
330328
},
331329
{
@@ -337,7 +335,6 @@
337335
"We can inspect the routes in more detail using the `plot_route_schedule` function.\n",
338336
"This will plot distance on the x-axis, and time on the y-axis, separating actual travel/driving time from waiting and service time.\n",
339337
"The clients visited are plotted as grey vertical bars indicating their time windows.\n",
340-
"We can see a jump to the start of the time window in the main (earliest) time line when a vehicle arrives early at a customer and has to wait.\n",
341338
"In some cases, there is slack in the route indicated by a semi-transparent region on top of the earliest time line.\n",
342339
"The grey background indicates the remaining load of the truck during the route, where the (right) y-axis ends at the vehicle capacity."
343340
]
@@ -350,7 +347,7 @@
350347
"outputs": [],
351348
"source": [
352349
"fig, axarr = plt.subplots(2, 2, figsize=(15, 9))\n",
353-
"for idx, (ax, route) in enumerate(zip(axarr.reshape(-1), routes)):\n",
350+
"for idx, (ax, route) in enumerate(zip(axarr.flatten(), routes)):\n",
354351
" plot_route_schedule(\n",
355352
" INSTANCE,\n",
356353
" route,\n",
@@ -362,6 +359,56 @@
362359
"fig.tight_layout()"
363360
]
364361
},
362+
{
363+
"cell_type": "markdown",
364+
"id": "de5c7ca0",
365+
"metadata": {},
366+
"source": [
367+
"Each route begins at a given `start_time`, that can be obtained as follows.\n",
368+
"Note that this start time is typically not zero, that is, routes do not have to start immediately at the beginning of the time horizon."
369+
]
370+
},
371+
{
372+
"cell_type": "code",
373+
"execution_count": null,
374+
"id": "dea67223",
375+
"metadata": {},
376+
"outputs": [],
377+
"source": [
378+
"solution = result.best\n",
379+
"shortest_route = min(solution.routes(), key=len)\n",
380+
"\n",
381+
"shortest_route.start_time()"
382+
]
383+
},
384+
{
385+
"cell_type": "markdown",
386+
"id": "88665bfb",
387+
"metadata": {},
388+
"source": [
389+
"Some of the statistics presented in the plots above can also be obtained from the route schedule, as follows:"
390+
]
391+
},
392+
{
393+
"cell_type": "code",
394+
"execution_count": null,
395+
"id": "9f882942",
396+
"metadata": {},
397+
"outputs": [],
398+
"source": [
399+
"data = [\n",
400+
" {\n",
401+
" \"start_service\": visit.start_service,\n",
402+
" \"end_service\": visit.end_service,\n",
403+
" \"service_duration\": visit.service_duration,\n",
404+
" \"wait_duration\": visit.wait_duration, # if vehicle arrives early\n",
405+
" }\n",
406+
" for visit in shortest_route.schedule()\n",
407+
"]\n",
408+
"\n",
409+
"tabulate(data, headers=\"keys\", tablefmt=\"html\")"
410+
]
411+
},
365412
{
366413
"attachments": {},
367414
"cell_type": "markdown",
@@ -455,7 +502,7 @@
455502
],
456503
"metadata": {
457504
"kernelspec": {
458-
"display_name": "Python 3 (ipykernel)",
505+
"display_name": ".venv",
459506
"language": "python",
460507
"name": "python3"
461508
},
@@ -469,7 +516,7 @@
469516
"name": "python",
470517
"nbconvert_exporter": "python",
471518
"pygments_lexer": "ipython3",
472-
"version": "3.9.13"
519+
"version": "3.11.9"
473520
}
474521
},
475522
"nbformat": 4,

pyvrp/_pyvrp.pyi

+11
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,16 @@ class ProblemData:
205205
def __getstate__(self) -> tuple: ...
206206
def __setstate__(self, state: tuple, /) -> None: ...
207207

208+
class ScheduledVisit:
209+
start_service: int
210+
end_service: int
211+
wait_duration: int
212+
time_warp: int
213+
@property
214+
def service_duration(self) -> int: ...
215+
def __getstate__(self) -> tuple: ...
216+
def __setstate__(self, state: tuple, /) -> None: ...
217+
208218
class Route:
209219
def __init__(
210220
self, data: ProblemData, visits: list[int], vehicle_type: int
@@ -239,6 +249,7 @@ class Route:
239249
def vehicle_type(self) -> int: ...
240250
def start_depot(self) -> int: ...
241251
def end_depot(self) -> int: ...
252+
def schedule(self) -> list[ScheduledVisit]: ...
242253
def __getstate__(self) -> tuple: ...
243254
def __setstate__(self, state: tuple, /) -> None: ...
244255

pyvrp/cpp/Route.cpp

+51-10
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
#include "LoadSegment.h"
44

55
#include <algorithm>
6+
#include <cassert>
67
#include <fstream>
78
#include <numeric>
89

@@ -14,6 +15,23 @@ using pyvrp::Route;
1415

1516
using Client = size_t;
1617

18+
Route::ScheduledVisit::ScheduledVisit(Duration startService,
19+
Duration endService,
20+
Duration waitDuration,
21+
Duration timeWarp)
22+
: startService(startService),
23+
endService(endService),
24+
waitDuration(waitDuration),
25+
timeWarp(timeWarp)
26+
{
27+
assert(startService <= endService);
28+
}
29+
30+
Duration Route::ScheduledVisit::serviceDuration() const
31+
{
32+
return endService - startService;
33+
}
34+
1735
Route::Route(ProblemData const &data, Visits visits, size_t const vehicleType)
1836
: visits_(std::move(visits)), centroid_({0, 0}), vehicleType_(vehicleType)
1937
{
@@ -24,28 +42,24 @@ Route::Route(ProblemData const &data, Visits visits, size_t const vehicleType)
2442
DurationSegment ds = {vehType, vehType.startLate};
2543
std::vector<LoadSegment> loadSegments(data.numLoadDimensions());
2644

27-
size_t prevClient = startDepot_;
28-
2945
auto const &distances = data.distanceMatrix(vehType.profile);
3046
auto const &durations = data.durationMatrix(vehType.profile);
3147

32-
for (size_t idx = 0; idx != size(); ++idx)
48+
for (size_t prevClient = startDepot_; auto const client : visits_)
3349
{
34-
auto const client = visits_[idx];
3550
ProblemData::Client const &clientData = data.location(client);
3651

3752
distance_ += distances(prevClient, client);
38-
travel_ += durations(prevClient, client);
3953
service_ += clientData.serviceDuration;
4054
prizes_ += clientData.prize;
4155

56+
auto const edgeDuration = durations(prevClient, client);
57+
travel_ += edgeDuration;
58+
ds = DurationSegment::merge(edgeDuration, ds, {clientData});
59+
4260
centroid_.first += static_cast<double>(clientData.x) / size();
4361
centroid_.second += static_cast<double>(clientData.y) / size();
4462

45-
auto const clientDS = DurationSegment(clientData);
46-
ds = DurationSegment::merge(
47-
durations(prevClient, client), ds, clientDS);
48-
4963
for (size_t dim = 0; dim != data.numLoadDimensions(); ++dim)
5064
{
5165
auto const clientLs = LoadSegment(clientData, dim);
@@ -81,6 +95,26 @@ Route::Route(ProblemData const &data, Visits visits, size_t const vehicleType)
8195
slack_ = ds.twLate() - ds.twEarly();
8296
timeWarp_ = ds.timeWarp(vehType.maxDuration);
8397
release_ = ds.releaseTime();
98+
99+
schedule_.reserve(size());
100+
auto now = startTime_;
101+
for (size_t prevClient = startDepot_; auto const client : visits_)
102+
{
103+
now += durations(prevClient, client);
104+
105+
ProblemData::Client const &clientData = data.location(client);
106+
auto const wait = std::max<Duration>(clientData.twEarly - now, 0);
107+
auto const timeWarp = std::max<Duration>(now - clientData.twLate, 0);
108+
109+
now += wait;
110+
now -= timeWarp;
111+
112+
schedule_.emplace_back(
113+
now, now + clientData.serviceDuration, wait, timeWarp);
114+
115+
now += clientData.serviceDuration;
116+
prevClient = client;
117+
}
84118
}
85119

86120
Route::Route(Visits visits,
@@ -103,8 +137,10 @@ Route::Route(Visits visits,
103137
std::pair<double, double> centroid,
104138
size_t vehicleType,
105139
size_t startDepot,
106-
size_t endDepot)
140+
size_t endDepot,
141+
std::vector<ScheduledVisit> schedule)
107142
: visits_(std::move(visits)),
143+
schedule_(std::move(schedule)),
108144
distance_(distance),
109145
distanceCost_(distanceCost),
110146
excessDistance_(excessDistance),
@@ -140,6 +176,11 @@ Route::Visits::const_iterator Route::end() const { return visits_.cend(); }
140176

141177
Route::Visits const &Route::visits() const { return visits_; }
142178

179+
std::vector<Route::ScheduledVisit> const &Route::schedule() const
180+
{
181+
return schedule_;
182+
}
183+
143184
Distance Route::distance() const { return distance_; }
144185

145186
Cost Route::distanceCost() const { return distanceCost_; }

pyvrp/cpp/Route.h

+50-2
Original file line numberDiff line numberDiff line change
@@ -18,12 +18,49 @@ namespace pyvrp
1818
*/
1919
class Route
2020
{
21+
public:
22+
/**
23+
* Simple object that stores some data about a client visit.
24+
*
25+
* Attributes
26+
* ----------
27+
* start_service : int
28+
* Time at which the client service begins.
29+
* end_service : int
30+
* Time at which the client service completes.
31+
* service_duration : int
32+
* Duration of the service.
33+
* wait_duration : int
34+
* If the vehicle arrives early, this is the duration it has to wait
35+
* until it can begin service.
36+
* time_warp : int
37+
* If the vehicle arrives late, this is the duration it has to 'travel
38+
* back in time' to begin service. Non-zero time warp indicates an
39+
* infeasible route.
40+
*/
41+
struct ScheduledVisit
42+
{
43+
Duration const startService = 0;
44+
Duration const endService = 0;
45+
Duration const waitDuration = 0;
46+
Duration const timeWarp = 0;
47+
48+
ScheduledVisit(Duration startService,
49+
Duration endService,
50+
Duration waitDuration,
51+
Duration timeWarp);
52+
53+
[[nodiscard]] Duration serviceDuration() const;
54+
};
55+
56+
private:
2157
using Client = size_t;
2258
using Depot = size_t;
2359
using VehicleType = size_t;
2460
using Visits = std::vector<Client>;
2561

26-
Visits visits_ = {}; // Client visits on this route
62+
Visits visits_ = {}; // Client visits on this route
63+
std::vector<ScheduledVisit> schedule_ = {}; // Client visit schedule data
2764
Distance distance_ = 0; // Total travel distance on this route
2865
Cost distanceCost_ = 0; // Total cost of travel distance
2966
Distance excessDistance_ = 0; // Excess travel distance
@@ -64,6 +101,16 @@ class Route
64101
*/
65102
[[nodiscard]] Visits const &visits() const;
66103

104+
/**
105+
* Statistics about each client visit and the overall route schedule.
106+
*
107+
* .. note::
108+
*
109+
* The schedule assumes the route starts at :meth:`~start_time`. Starting
110+
* later may be feasible, but shifts the schedule.
111+
*/
112+
[[nodiscard]] std::vector<ScheduledVisit> const &schedule() const;
113+
67114
/**
68115
* Total distance travelled on this route.
69116
*/
@@ -239,7 +286,8 @@ class Route
239286
std::pair<double, double> centroid,
240287
VehicleType vehicleType,
241288
Depot startDepot,
242-
Depot endDepot);
289+
Depot endDepot,
290+
std::vector<ScheduledVisit> schedule);
243291
};
244292
} // namespace pyvrp
245293

pyvrp/cpp/bindings.cpp

+32-2
Original file line numberDiff line numberDiff line change
@@ -454,6 +454,31 @@ PYBIND11_MODULE(_pyvrp, m)
454454
return data;
455455
}));
456456

457+
py::class_<Route::ScheduledVisit>(
458+
m, "ScheduledVisit", DOC(pyvrp, Route, ScheduledVisit))
459+
.def_readonly("start_service", &Route::ScheduledVisit::startService)
460+
.def_readonly("end_service", &Route::ScheduledVisit::endService)
461+
.def_readonly("wait_duration", &Route::ScheduledVisit::waitDuration)
462+
.def_readonly("time_warp", &Route::ScheduledVisit::timeWarp)
463+
.def_property_readonly("service_duration",
464+
&Route::ScheduledVisit::serviceDuration)
465+
.def(py::pickle(
466+
[](Route::ScheduledVisit const &visit) { // __getstate__
467+
return py::make_tuple(visit.startService,
468+
visit.endService,
469+
visit.waitDuration,
470+
visit.timeWarp);
471+
},
472+
[](py::tuple t) { // __setstate__
473+
Route::ScheduledVisit visit(
474+
t[0].cast<pyvrp::Duration>(), // start service
475+
t[1].cast<pyvrp::Duration>(), // end service
476+
t[2].cast<pyvrp::Duration>(), // wait duration
477+
t[3].cast<pyvrp::Duration>()); // time warp
478+
479+
return visit;
480+
}));
481+
457482
py::class_<Route>(m, "Route", DOC(pyvrp, Route))
458483
.def(py::init<ProblemData const &, std::vector<size_t>, size_t>(),
459484
py::arg("data"),
@@ -508,6 +533,7 @@ PYBIND11_MODULE(_pyvrp, m)
508533
.def("has_time_warp",
509534
&Route::hasTimeWarp,
510535
DOC(pyvrp, Route, hasTimeWarp))
536+
.def("schedule", &Route::schedule, DOC(pyvrp, Route, schedule))
511537
.def("__len__", &Route::size, DOC(pyvrp, Route, size))
512538
.def(
513539
"__iter__",
@@ -549,9 +575,12 @@ PYBIND11_MODULE(_pyvrp, m)
549575
route.centroid(),
550576
route.vehicleType(),
551577
route.startDepot(),
552-
route.endDepot());
578+
route.endDepot(),
579+
route.schedule());
553580
},
554581
[](py::tuple t) { // __setstate__
582+
using Schedule = std::vector<Route::ScheduledVisit>;
583+
555584
Route route(
556585
t[0].cast<std::vector<size_t>>(), // visits
557586
t[1].cast<pyvrp::Distance>(), // distance
@@ -573,7 +602,8 @@ PYBIND11_MODULE(_pyvrp, m)
573602
t[17].cast<std::pair<double, double>>(), // centroid
574603
t[18].cast<size_t>(), // vehicle type
575604
t[19].cast<size_t>(), // start depot
576-
t[20].cast<size_t>()); // end depot
605+
t[20].cast<size_t>(), // end depot
606+
t[21].cast<Schedule>()); // visit schedule
577607

578608
return route;
579609
}))

0 commit comments

Comments
 (0)