Dynamic Replanning On the Go
Tour Planning allows you to get numerous advantages when planning tours not only for multiple vehicles but for single vehicles as well. One of the helpful options here is tour replanning - the ability for drivers to make corrections while executing the tour with live optimization of the whole tour at the moment of those corrections. This option is very useful when any unpredicted obstacles occur during the tour like the driver running late to the customer, traffic conditions changes, jobs cancellations during the tour, etc.
The normal replanning procedure will look as follows. A tour was planned and optimized (problem built) and a driver started executing the tour. After executing the first job, the driver proceeds to the next one, and since that moment, the driver’s current location becomes the shift’s start time for the remaining jobs. Accordingly, if the driver makes any changes to the tour - skips the location with the cancelled job, or changes the route because being late and forced by another job’s priority - the tour will be recalculated with the new constraints starting from the driver’s current location.
Let’s model a simple situation when we have a vehicle with a capacity of 10 items, and we need it to execute 6 jobs in different locations, one of which has high priority (priority = 1).
{
"fleet": {
"types": [
{
"id": "Vehicle_1",
"profile": "car_1",
"costs": {
"fixed": 9.0,
"distance": 0.004,
"time": 0.005
},
"shifts": [
{
"start": {
"time": "2021-08-27T08:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
},
"end": {
"time": "2021-08-27T18:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
}
}
],
"capacity": [
10
],
"amount": 1
}
],
"profiles": [
{
"type": "car",
"name": "car_1",
"departureTime": "2021-08-27T08:00:00Z"
}
]
},
"plan": {
"jobs": [
{
"id": "job_1",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T09:03:00Z",
"2021-08-27T18:03:00Z"
]
],
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372257
},
"duration": 360
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_2",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T11:03:00Z",
"2021-08-27T20:03:00Z"
]
],
"location": {
"lat": 52.43363386232821,
"lng": 13.403232562191313
},
"duration": 540
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_3",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_4",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.503321,
"lng": 13.299720
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_5",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.403321658918245,
"lng": 13.19972099097991
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_6",
"priority": 1,
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T14:03:00Z",
"2021-08-27T17:03:00Z"
]
],
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"duration": 1140
}
],
"demand": [
1
]
}
]
}
}
]
}
}
- After optimizing, the driver starts executing the tour. The initial solution for this problem will look as follows. The jobs are going to be executed in the sequence:
job_2
, job_5
, job_3
, job_4
, job_1
, job_6
. Note that job_6
has priority 1, but it is still settled at the end of the tour as at the moment driver has enough time to execute all jobs in time.
{
"statistic": {
"cost": 399.456,
"distance": 83354,
"duration": 11408,
"times": {
"driving": 7388,
"serving": 4020,
"waiting": 0,
"break": 0
}
},
"tours": [
{
"vehicleId": "Vehicle_1_1",
"typeId": "Vehicle_1",
"stops": [
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T08:03:00Z",
"departure": "2021-08-27T11:16:48Z"
},
"load": [
6
],
"activities": [
{
"jobId": "departure",
"type": "departure"
}
],
"distance": 0
},
{
"location": {
"lat": 52.43363386232821,
"lng": 13.403232562191311
},
"time": {
"arrival": "2021-08-27T11:41:25Z",
"departure": "2021-08-27T11:50:25Z"
},
"load": [
5
],
"activities": [
{
"jobId": "job_2",
"type": "delivery"
}
],
"distance": 14847
},
{
"location": {
"lat": 52.40332165891824,
"lng": 13.19972099097991
},
"time": {
"arrival": "2021-08-27T12:20:46Z",
"departure": "2021-08-27T12:31:46Z"
},
"load": [
4
],
"activities": [
{
"jobId": "job_5",
"type": "delivery"
}
],
"distance": 28933
},
{
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"time": {
"arrival": "2021-08-27T12:50:19Z",
"departure": "2021-08-27T13:01:19Z"
},
"load": [
3
],
"activities": [
{
"jobId": "job_3",
"type": "delivery"
}
],
"distance": 45017
},
{
"location": {
"lat": 52.503321,
"lng": 13.29972
},
"time": {
"arrival": "2021-08-27T13:10:48Z",
"departure": "2021-08-27T13:21:48Z"
},
"load": [
2
],
"activities": [
{
"jobId": "job_4",
"type": "delivery"
}
],
"distance": 62482
},
{
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
},
"time": {
"arrival": "2021-08-27T13:42:21Z",
"departure": "2021-08-27T13:48:21Z"
},
"load": [
1
],
"activities": [
{
"jobId": "job_1",
"type": "delivery"
}
],
"distance": 75917
},
{
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"time": {
"arrival": "2021-08-27T14:03:00Z",
"departure": "2021-08-27T14:22:00Z"
},
"load": [
0
],
"activities": [
{
"jobId": "job_6",
"type": "delivery"
}
],
"distance": 83354
},
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T14:26:56Z",
"departure": "2021-08-27T14:26:56Z"
},
"load": [
0
],
"activities": [
{
"jobId": "arrival",
"type": "arrival"
}
],
"distance": 85458
}
],
"statistic": {
"cost": 399.456,
"distance": 83354,
"duration": 11408,
"times": {
"driving": 7388,
"serving": 4020,
"waiting": 0,
"break": 0
}
},
"shiftIndex": 0
}
]
}
- Lets assume that the driver has executed the first job. After that, the driver decided to recalculate the tour to update his departure time to consider the traffic changes. Since that time, the tour is optimized regarding the changes and considering the current driver’s location as a new shift time starting location. Now the driver is executing the next job considering the new constraints. So now the problem constraints will be solved without
job_2
that was already executed. The starting location now is the location of job_2
, and starting time - the time of departure from job_2
. Now the problem will look as follows:
{
"fleet": {
"types": [
{
"id": "Vehicle_1",
"profile": "car_1",
"costs": {
"fixed": 9.0,
"distance": 0.004,
"time": 0.005
},
"shifts": [
{
"start": {
"time": "2021-08-27T11:50:17Z",
"location": {
"lat": 52.43363386232821,
"lng": 13.403232562191311
}
},
"end": {
"time": "2021-08-27T18:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
}
}
],
"capacity": [
10
],
"amount": 1
}
],
"profiles": [
{
"type": "car",
"name": "car_1",
"departureTime": "2021-08-27T11:50:17Z"
}
]
},
"plan": {
"jobs": [
{
"id": "job_1",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T09:03:00Z",
"2021-08-27T18:03:00Z"
]
],
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372257
},
"duration": 360
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_3",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_4",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.503321,
"lng": 13.299720
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_5",
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T10:03:00Z",
"2021-08-27T16:03:00Z"
]
],
"location": {
"lat": 52.403321658918245,
"lng": 13.19972099097991
},
"duration": 660
}
],
"demand": [
1
]
}
]
}
},
{
"id": "job_6",
"priority": 1,
"tasks": {
"deliveries": [
{
"places": [
{
"times": [
[
"2021-08-27T14:03:00Z",
"2021-08-27T17:03:00Z"
]
],
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"duration": 1140
}
],
"demand": [
1
]
}
]
}
}
]
}
}
The solution will now show that the consequence of executing the jobs will be the following: job_5
, job_3
, job_4
, job_1
, job_6
.
{
"statistic": {
"cost": 329.975,
"distance": 68505,
"duration": 9391,
"times": {
"driving": 5911,
"serving": 3480,
"waiting": 0,
"break": 0
}
},
"tours": [
{
"vehicleId": "Vehicle_1_1",
"typeId": "Vehicle_1",
"stops": [
{
"location": {
"lat": 52.43363386232821,
"lng": 13.403232562191311
},
"time": {
"arrival": "2021-08-27T11:50:25Z",
"departure": "2021-08-27T11:50:25Z"
},
"load": [
5
],
"activities": [
{
"jobId": "departure",
"type": "departure"
}
],
"distance": 0
},
{
"location": {
"lat": 52.40332165891824,
"lng": 13.19972099097991
},
"time": {
"arrival": "2021-08-27T12:20:46Z",
"departure": "2021-08-27T12:31:46Z"
},
"load": [
4
],
"activities": [
{
"jobId": "job_5",
"type": "delivery"
}
],
"distance": 14086
},
{
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"time": {
"arrival": "2021-08-27T12:50:19Z",
"departure": "2021-08-27T13:01:19Z"
},
"load": [
3
],
"activities": [
{
"jobId": "job_3",
"type": "delivery"
}
],
"distance": 30170
},
{
"location": {
"lat": 52.503321,
"lng": 13.29972
},
"time": {
"arrival": "2021-08-27T13:10:48Z",
"departure": "2021-08-27T13:21:48Z"
},
"load": [
2
],
"activities": [
{
"jobId": "job_4",
"type": "delivery"
}
],
"distance": 47635
},
{
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
},
"time": {
"arrival": "2021-08-27T13:42:21Z",
"departure": "2021-08-27T13:48:21Z"
},
"load": [
1
],
"activities": [
{
"jobId": "job_1",
"type": "delivery"
}
],
"distance": 61070
},
{
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"time": {
"arrival": "2021-08-27T14:03:00Z",
"departure": "2021-08-27T14:22:00Z"
},
"load": [
0
],
"activities": [
{
"jobId": "job_6",
"type": "delivery"
}
],
"distance": 68507
},
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T14:26:56Z",
"departure": "2021-08-27T14:26:56Z"
},
"load": [
0
],
"activities": [
{
"jobId": "arrival",
"type": "arrival"
}
],
"distance": 70611
}
],
"statistic": {
"cost": 329.975,
"distance": 68505,
"duration": 9391,
"times": {
"driving": 5911,
"serving": 3480,
"waiting": 0,
"break": 0
}
},
"shiftIndex": 0
}
]
}
- When executing
job_5
, the customer was not available at the specified location, so the packages were not delivered to a customer, and the driver has spent some additional time to deliver it to the nearby pick-up location. Since that time the tour is recalculated once again considering the new constraints - slightly different location and the departure time that will be later than expected. The constraints for the fleet will now look as follows:
{
"fleet": {
"types": [
{
"id": "Vehicle_1",
"profile": "car_1",
"costs": {
"fixed": 9.0,
"distance": 0.004,
"time": 0.005
},
"shifts": [
{
"start": {
"time": "2021-08-27T12:38:38Z",
"location": {
"lat": 52.40332165891824,
"lng": 13.19972099097991
}
},
"end": {
"time": "2021-08-27T18:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
}
}
],
"capacity": [
10
],
"amount": 1
}
],
"profiles": [
{
"type": "car",
"name": "car_1",
"departureTime": "2021-08-27T12:38:38Z"
}
]
}
As we can see from the solution, the rest of the jobs are now going to be executed in the order: job_3
, job_4
, job_1
, job_6
.
{
"statistic": {
"cost": 218.14999999999998,
"distance": 43650,
"duration": 6910,
"times": {
"driving": 4090,
"serving": 2820,
"waiting": 0,
"break": 0
}
},
"tours": [
{
"vehicleId": "Vehicle_1_1",
"typeId": "Vehicle_1",
"stops": [
{
"location": {
"lat": 52.40332165891824,
"lng": 13.19972099097991
},
"time": {
"arrival": "2021-08-27T12:40:46Z",
"departure": "2021-08-27T12:40:46Z"
},
"load": [
4
],
"activities": [
{
"jobId": "departure",
"type": "departure"
}
],
"distance": 0
},
{
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"time": {
"arrival": "2021-08-27T12:59:19Z",
"departure": "2021-08-27T13:10:19Z"
},
"load": [
3
],
"activities": [
{
"jobId": "job_3",
"type": "delivery"
}
],
"distance": 15600
},
{
"location": {
"lat": 52.503321,
"lng": 13.29972
},
"time": {
"arrival": "2021-08-27T13:19:48Z",
"departure": "2021-08-27T13:30:48Z"
},
"load": [
2
],
"activities": [
{
"jobId": "job_4",
"type": "delivery"
}
],
"distance": 20560
},
{
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
},
"time": {
"arrival": "2021-08-27T13:51:21Z",
"departure": "2021-08-27T13:57:21Z"
},
"load": [
1
],
"activities": [
{
"jobId": "job_1",
"type": "delivery"
}
],
"distance": 33995
},
{
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"time": {
"arrival": "2021-08-27T14:12:00Z",
"departure": "2021-08-27T14:31:00Z"
},
"load": [
0
],
"activities": [
{
"jobId": "job_6",
"type": "delivery"
}
],
"distance": 41432
},
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T14:35:56Z",
"departure": "2021-08-27T14:35:56Z"
},
"load": [
0
],
"activities": [
{
"jobId": "arrival",
"type": "arrival"
}
],
"distance": 43536
}
],
"statistic": {
"cost": 218.14999999999998,
"distance": 43650,
"duration": 6910,
"times": {
"driving": 4090,
"serving": 2820,
"waiting": 0,
"break": 0
}
},
"shiftIndex": 0
}
]
}
- Lets assume that when the driver was executing the follow up job, the traffic conditions have changed dramatically, or the vehicle had some issue that required some time for repair. As a result, the driver was running significantly late to execute
job_3
. The expected time for completing was 13:10, but the driver completed it at 16:15. Despite this, job_3
has been completed successfully, and the constraints were updated again.
{
"fleet": {
"types": [
{
"id": "Vehicle_1",
"profile": "car_1",
"costs": {
"fixed": 9.0,
"distance": 0.004,
"time": 0.005
},
"shifts": [
{
"start": {
"time": "2021-08-27T16:15:11Z",
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
}
},
"end": {
"time": "2021-08-27T18:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
}
}
],
"capacity": [
10
],
"amount": 1
}
],
"profiles": [
{
"type": "car",
"name": "car_1",
"departureTime": "2021-08-27T16:15:11Z"
}
]
}
Now when the tour was recalculated, job_6
with priority 1, has been moved to be executed before non-prioritized job_4
, because due to the new time constraints, the driver would not have enough time for executing them both. This update is immediately displayed in the driver's application, so the driver proceeds to executing job_1
, and then job_6
. job_4
is now unassigned from the tour as not prioritized.
{
"statistic": {
"cost": 134.247,
"distance": 26318,
"duration": 3995,
"times": {
"driving": 2495,
"serving": 1500,
"waiting": 0,
"break": 0
}
},
"tours": [
{
"vehicleId": "Vehicle_1_1",
"typeId": "Vehicle_1",
"stops": [
{
"location": {
"lat": 52.473321658918245,
"lng": 13.28972099097991
},
"time": {
"arrival": "2021-08-27T16:15:19Z",
"departure": "2021-08-27T16:15:19Z"
},
"load": [
2
],
"activities": [
{
"jobId": "departure",
"type": "departure"
}
],
"distance": 0
},
{
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
},
"time": {
"arrival": "2021-08-27T16:37:19Z",
"departure": "2021-08-27T16:43:19Z"
},
"load": [
1
],
"activities": [
{
"jobId": "job_1",
"type": "delivery"
}
],
"distance": 16775
},
{
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"time": {
"arrival": "2021-08-27T16:57:58Z",
"departure": "2021-08-27T17:16:58Z"
},
"load": [
0
],
"activities": [
{
"jobId": "job_6",
"type": "delivery"
}
],
"distance": 24212
},
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T17:21:54Z",
"departure": "2021-08-27T17:21:54Z"
},
"load": [
0
],
"activities": [
{
"jobId": "arrival",
"type": "arrival"
}
],
"distance": 26316
}
],
"statistic": {
"cost": 134.247,
"distance": 26318,
"duration": 3995,
"times": {
"driving": 2495,
"serving": 1500,
"waiting": 0,
"break": 0
}
},
"shiftIndex": 0
}
],
"unassigned": [
{
"jobId": "job_4",
"reasons": [
{
"code": "TIME_WINDOW_CONSTRAINT",
"description": "cannot be visited within time window"
}
]
}
]
}
- After executing
job_1
, the fleet constraints look as follows:
{
"fleet": {
"types": [
{
"id": "Vehicle_1",
"profile": "car_1",
"costs": {
"fixed": 9.0,
"distance": 0.004,
"time": 0.005
},
"shifts": [
{
"start": {
"time": "2021-08-27T16:43:20Z",
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
}
},
"end": {
"time": "2021-08-27T18:03:00Z",
"location": {
"lat": 52.530971,
"lng": 13.384915
}
}
}
],
"capacity": [
10
],
"amount": 1
}
],
"profiles": [
{
"type": "car",
"name": "car_1",
"departureTime": "2021-08-27T16:43:20Z"
}
]
},
The only job that remains to be executed is prioritized job_6
.
{
"statistic": {
"cost": 58.743,
"distance": 9542,
"duration": 2315,
"times": {
"driving": 1175,
"serving": 1140,
"waiting": 0,
"break": 0
}
},
"tours": [
{
"vehicleId": "Vehicle_1_1",
"typeId": "Vehicle_1",
"stops": [
{
"location": {
"lat": 52.59175589353722,
"lng": 13.350747750372255
},
"time": {
"arrival": "2021-08-27T16:43:19Z",
"departure": "2021-08-27T16:43:19Z"
},
"load": [
1
],
"activities": [
{
"jobId": "departure",
"type": "departure"
}
],
"distance": 0
},
{
"location": {
"lat": 52.54165532725351,
"lng": 13.365047170290309
},
"time": {
"arrival": "2021-08-27T16:57:58Z",
"departure": "2021-08-27T17:16:58Z"
},
"load": [
0
],
"activities": [
{
"jobId": "job_6",
"type": "delivery"
}
],
"distance": 7437
},
{
"location": {
"lat": 52.530971,
"lng": 13.384915
},
"time": {
"arrival": "2021-08-27T17:21:54Z",
"departure": "2021-08-27T17:21:54Z"
},
"load": [
0
],
"activities": [
{
"jobId": "arrival",
"type": "arrival"
}
],
"distance": 14991
}
],
"statistic": {
"cost": 58.743,
"distance": 9542,
"duration": 2315,
"times": {
"driving": 1175,
"serving": 1140,
"waiting": 0,
"break": 0
}
},
"shiftIndex": 0
}
],
"unassigned": [
{
"jobId": "job_4",
"reasons": [
{
"code": "TIME_WINDOW_CONSTRAINT",
"description": "cannot be visited within time window"
}
]
}
]
}