10 Vehicle features
The following subsections detail various advanced features you can enable for modelling vehicles. If you’re looking for a feature and you don’t find it in this section, it’s likely you can implement it with user functions instead.
10.1 Allowed job service times
For certain problems a vehicle might only be able to service jobs within certain times (e.g. 9am-5pm) but you may also allow the vehicle to drive to the first job or return home from the last job outside of these times. In these circumstances you want to setup job times on the vehicle, e.g.
vehicle.definition.start.earliestStartJobTime, the earliest time a vehicle can start a job. The vehicle is assumed to wait at the job before this time, until it opens.
vehicle.definition.end.finishLastJobTime, the latest time a vehicle can finish a job. This is treated as a hard end time; the optimiser will not allow a job to finish after this time.
vehicle.definition.end.finishLastJobTimePenalties, penalty functions defining costs based on the time jobs are completed.
The finishLastJobTimePenalties penalties are setup identically to the custom lateness penalty functions used with the multiple time windows definitions. See the section on multiple time windows for more information on their structure.
We set all of these fields in the following example vehicle JSON:
{
"definition" : {
"costPerTravelHour" : 1.0,
"costPerWaitingHour" : 0.5,
"costPerServicingHour" : 1.0,
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : { "latitude" : 51.470069, "longitude" : -0.454499 },
"openTime" : "2018-12-20T07:00",
"earliestStartJobTime" : "2018-12-20T09:00",
"_id" : "v1Start" },
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : { "latitude" : 51.470069, "longitude" : -0.454499 },
"lateTime" : "2018-12-20T18:00",
"closeTime" : "2018-12-20T19:00",
"finishLastJobTime" : "2018-12-20T17:00",
"finishLastJobTimePenalties" : [ {
"openTime" : "2018-12-20T16:00",
"closeTime" : "2018-12-20T17:00",
"cost" : 0.0,
"costPerHour" : 10.0,
"costPerHourSqd" : 0.0
} ],
"_id" : "v1End" }
},
"_id" : "v1"
}
This vehicle has the following times set:
Starts driving at 7:00 (openTime).
Can start servicing first job at 9:00 (earliestStartJobTime).
If the completion of a job falls between 16:00 and 17:00 a penalty function is applied, which increases as the square, towards 17:00 (finishLastJobTimePenalties).
No job will be allowed to finish after 17:00 (finishLastJobTime).
Late time penalties will be applied if the vehicle returns home after 18:00 (lateTime).
The vehicle is not allowed to return home after 19:00 (closeTime).
Note that for both finish last job time and return home time we have set a soft time followed by a hard time. Setting a soft time first is important for realtime problems as GPS updates etc may cause a vehicle’s estimated finish time to hover back and forth near the limit, and a hard limit would cause jobs to be unloaded when this happens.
10.2 A to B travel limits
You can place limits on the amount of travel a vehicle can do between two locations, for example drive no more than 2 km between locations. The distance is calculated using the road network if you have road networks enabled.
To keep things simple, when you’re modelling in realtime the travel distance or time is measured from the vehicle’s current location to its next planned stop. It does not include the travel the vehicle has already done from its last location (e.g. last dropoff) to its current location.
The limit can be set based on either a maximum travel hours between locations or a maximum kilometres (or both together). The following JSON shows a vehicle with a 30 km hard limit setup between locations, inside the field extraCostPerKm:
{
"definition" : {
"extraCostPerKm" : [ {
"inclusiveLowerLimit" : 30.0,
"prohibited" : true
} ],
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 51.5073,"longitude" : -0.1657
},
"openTime" : "2020-01-01T01:01",
"_id" : "start1"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 51.5073,"longitude" : -0.1657
},
"closeTime" : "2020-02-01T01:01",
"_id" : "end1"
}
},
"_id" : "vehicle1"
}
We look at extraCostPerKm in more detail:
"extraCostPerKm" : [ {
"inclusiveLowerLimit" : 30.0,
"prohibited" : true
} ]
This is actually the same kind of penalty function we use in various parts of the API. These penalties functions are defined in-detail in section penalty functions. The 30km limit is set in the field inclusiveLowerLimit, so if you wanted (for example) a 50 km limit instead you should set inclusiveLowerLimit = 50. You can also set a soft limit (i.e. penalty cost) if you wish, or a combination soft and hard limit - see the penalty functions section for details on how to do this.
The following JSON shows a 1.5 hour A to B travel time limit set instead, this time inside the field extraCostPerTravelHour:
{
"definition" : {
"extraCostPerTravelHour" : [ {
"inclusiveLowerLimit" : 1.5,
"prohibited" : true
} ],
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 51.5073,"longitude" : -0.1657
},
"openTime" : "2020-01-01T01:01",
"_id" : "start1"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 51.5073,"longitude" : -0.1657
},
"closeTime" : "2020-02-01T01:01",
"_id" : "end1"
}
},
"_id" : "vehicle1"
}
The format of the extraCostPerKm and extraCostPerTravelHour objects is identical (both use penalty functions), except for extraCostPerKm the inclusiveLowerLimit is in units of km and for extraCostPerTravelHour it is in units of hours. The fields are called extraCostPerKm and extraCostPerTravelHour because they can be used to add extra travel cost on top of the costs defined in vehicle.definition.costPerTravelHour and vehicle.definition.costPerKm.
10.3 Basic vehicle costs
The following basic costs are defined in the vehicle.definition object:
costPerTravelHour. Cost that is incurred for each hour the vehicle travels.
costPerWaitingHour. Cost that is incurred for each hour the vehicle is waiting at a stop until the stop’s openTime. This value should be small compared to costPerTravelHour otherwise you may get odd looking routes which drive extra distance just to avoid waiting (unless you use during-optimisation delays). See waiting time costs or limits for more information.
costPerServicingHour. Cost per hour for a vehicle servicing a stop, which is multiplied by the stop’s duration.
costPerKm. Cost that is incurred for each km the vehicle travels.
costFixed. A fixed cost which is applied when a vehicle is used, irrespective of the number of jobs assigned to it. If a vehicle has one or more dispatched stops, it is considered ‘already used’, and therefore its costFixed is no longer included in the cost model.
costPerStop. Cost for each stop the vehicle does.
The following JSON shows a vehicle object with these costs defined in its definition subobject.
{
"definition": {
"start": {
"type": "START_AT_DEPOT",
"coordinate": {
"latitude": 51.5138,
"longitude": -0.0984
},
"openTime": "2013-11-26T16:00",
"_id": "Vehicle1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {
"latitude": 51.5138,
"longitude": -0.0984
},
"lateTime": "2013-11-26T23:00",
"closeTime": "2013-11-27T23:00",
"_id": "Vehicle1End"
},
"costPerTravelHour": 1,
"costPerWaitingHour": 0.1,
"costPerServicingHour": 1,
"costPerKm": 0.000001,
"costFixed": 100,
"costPerStop": 2
},
"_id": "Vehicle1"
}
10.4 Capacity configurations
If your vehicle cannot be reconfigured internally for different passengers or loads, you only need to use a single capacity configuration. In this case, see quantities section under job features to explain vehicle capacities using the simpler vehicle.definition.capacities array.
Multiple capacity configurations are used to model a vehicle which can be reconfigured at any time during the route to have different internal capacities. For example, passenger seats could be left open to allow ambulatory passengers to sit, or folded back to allow stretchers instead. Given the different quantities onboard, the optimiser checks that at all points on the route, the onboard quantities must fit within at least one of the possible configurations. It does not model time for changing the configuration of the vehicle (i.e. folding away seats).
There’s an example model json and plan in the directory:
supporting-data-for-docs\example-models\multiple-capacity-configurations
The model has 3 jobs, all of which are pickup-dropoffs (i.e. point-to-point). The 1st and 2nd jobs have this quantities same array:
"quantities" : [ 1, 0 ]
The 3rd job has this quantities array:
"quantities" : [ 0, 1 ]
The vehicle is setup with 2 different capacities configurations in
the field vehicle.definition.multiCapacityConfigs:
{
"definition": {
"start": {
"type": "START_AT_DEPOT",
"openTime": "2025-01-01T00:00:00",
"_id": "ox8oWmUWTSWAkFiYPq6xfQ==Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"closeTime": "2025-02-01T00:00:00",
"_id": "ox8oWmUWTSWAkFiYPq6xfQ==End"
},
"multiCapacityConfigs": {
"configs": [
{
"capacities": [
3,
0
]
},
{
"capacities": [
0,
2
]
}
]
}
},
"_id": "ox8oWmUWTSWAkFiYPq6xfQ=="
}
If the multiCapacityConfigs field is present, ODL Live
will ignore the field vehicle.definition.capacities and use
the more flexible multiCapacityConfigs structure instead
(i.e. the advanced version). In this structure we’ve set-up 2
configurations with capacities [3,0] and [0,2]
respectively. This means the vehicle can have up to 3 of the 1st
quantity index onboard or up to 2 of the 2nd quantity index onboard but
no configuration lets quantities both the 1st and 2nd quantity be
onboard at once.
If we assume that the 1st quantity dimension is number of ambulatory (walking) passengers, and the 2nd quantity dimension is number of stretchers, we’re saying the vehicle can have up to 3 ambulatory people onboard together or up to 2 stretchers onboard together, but it can’t have both ambulatory people and stretchers onboard at the same time.
If we look at the plan JSON, we see the following route:
p0_10, p1_10, d0_10, d1_10, p2_01, d2_01
Here p or d stands for pickup or dropoff,
the number after the p or d is the 0-based job
index in the jobs array, and the _10 or _01
means "quantities" : [ 1, 0 ] or
"quantities" : [ 0, 1 ].
In terms of ambulatory and stretchers, this means we pickup the first ambulatory passenger, pickup the second ambulatory passenger, drop them both off and then pickup the stretcher passenger. So we can have both ambulatory passengers and stretchers onboard but not both at once.
10.5 Delayed start time and en-route delays (minimise waiting times)
If you set the appropriate value in the model configuration, ODL Live can calculate delays which can be inserted into the route to minimise waiting time. Exampples of these delays are:
ODL Live can calculate a delayed start time for the vehicle to minimise the amount of waiting time at either the first stop or the first stop with waiting time (i.e. where the vehicle arrives at the stop before its openTime).
ODL Live can calculate delayed leave times when no jobs are currently on-board, to minimise waiting for later stops. This is useful in passenger transport, imagine two pickup-dropoff jobs (P1,D1) and (P2,D2). Let’s assume a route sequence P1,P2,D1,D2 where the vehicle has to wait at P2 for 30 minutes until P2’s openTime with the passenger picked up at P1 already on-board and also having to wait. ODL Live can calculate a delayed pickup time for P1 which minimises the time the on-board passenger spends waiting at P2.
ODL Live can do this as (a) a post-optimisation process (i.e. after it has decided the routes) or (b) as a during-optimisation process (when it’s deciding the routes). Adding delays during the optimisation process will change the routes chooses - for example, without during-optimisation delays ODL Live may choose an inefficient route simply because this minimises waiting cost at a stop later in the route, but it won’t do this with during-optimisation delays. During-optimisation delays will however slow the optimiser down a little (in initial testing, we found around a 10% slowdown for one of our test cases).
We discuss the post-optimisation case first, as this introduces important concepts which are used for both post-optimisation and during-optimisation delays.
10.5.1 Minimise waiting using post-optimisation only delays
With post-optimisation delays, after optimising a route and deciding a sequence of stops, ODL Live calculates the delays. Constraints such as on-board time limits are therefore optimised without considering delays, and then delays are added after onto the final route. The delayed start time calculation is not applied if any of the following are true:
The vehicle has replenishes defined.
The vehicle has vehicle.definition.travelProcessingType set, and its value is not STANDARD.
A to B autobreaks are used.
To use the delay calculation, you specify in the optimiser configuration both (a) where delays can be added into the route and (b) where to look for waiting times which should be minimised. The optimiser then minimises these waiting times providing they don’t increase non-travel costs or break constraints (e.g. don’t increase lateness costs). Minimising waiting time (apart from at the start) will mean that some stops are served later to minimise waiting at the stops after them. If you don’t want the start time at a specific stop to be delayed after a time T, simply set a custom lateness penalty on the stop which applies a small linear penalty after time T.
As the delays are applied post-optimisation, they only affect the reporting of times in the output plan JSON. The costs and other statistics in the output plan still refer to the plan without delays added. The delay calculation is accurate to at least 1 minute, so for an openTime of 12:00 you might see an arrival time of 11:59:12 for example (see delay search tolerance). The calculation takes into account rush hours and breaks (defined in vehicle.definition.preloadedStops) which occur enroute.
By default ODL Live will delay the vehicle start time based on the wait time at the first stop only. You control this behaviour by setting values in the object model.configuration.optimiser.delays. The supporting-data-for-docs zip file which comes with ODL Live has an example model in the following location:
supporting-data-for-docs\example-models\delays\post-optimisation-delayed-pickups-model.json
This model is a single-route pickup-delivery problem where the route will do P0,D0,P1,D1,P2,D2 and there is waiting time before each stop due to the time windows. In this example model, we have also set the fields whereToLookForWaits and whereToAddDelays in the delays object as follows:
{
"data" : {
"jobs" : [ ... ],
"vehicles" : [ ...]
},
"configuration" : {
"optimiser" : {
"delays" : {
"whereToLookForWaits" : "ALL_STOPS",
"whereToAddDelays" : "NO_JOBS_ONBOARD"
}
},
"timeOverride" : { ... }
},
"_id" : "exampleModel"
}
As this model doesn’t use road network data, you can PUT it to your local ODL Live server and run it yourself. The following JSON shows the relevant fields from the model’s plan:
{
"vehicleId": "PFBoI2ElR9i5r5QSYFCcxg==",
"plannedStops": [
{
"stopId": "P0",
"timeEstimates": {
"arrival": "2001-01-01T04:00", "start": "2001-01-01T04:00", "complete": "2001-01-01T04:00"
}
},
{
"stopId": "D0",
"timeEstimates": {
"arrival": "2001-01-01T04:10:34.273892227", "start": "2001-01-01T06:00", "complete": "2001-01-01T06:00", "leaveAfter": "2001-01-01T08:39:48.402953315"
}
},
{
"stopId": "P1",
"timeEstimates": {
"arrival": "2001-01-01T09:00", "start": "2001-01-01T09:00", "complete": "2001-01-01T09:00"
}
},
{
"stopId": "D1",
"timeEstimates": {
"arrival": "2001-01-01T09:10:23.222334690","start": "2001-01-01T11:00", "complete": "2001-01-01T11:00", "leaveAfter": "2001-01-01T13:51:25.174049372"
}
},
{
"stopId": "P2",
"timeEstimates": {
"arrival": "2001-01-01T14:00","start": "2001-01-01T14:00", "complete": "2001-01-01T14:00"
}
},
{
"stopId": "D2",
"timeEstimates": {
"arrival": "2001-01-01T14:08:56.353229367", "start": "2001-01-01T16:00", "complete": "2001-01-01T16:00"
}
}
],
"planStartPoint": {
"time": "2001-01-01T03:57:13.107600830",
"coordinate": {"latitude": 51.5138,"longitude": -0.0984}
}
}
The vehicle in this model is scheduled to start work at “2001-01-01T00:00” (set in vehicle.definition.start.openTime) but the start has been delayed to minimise waiting at P0 (which doesn’t open until 03:00). The delayed start time is reported in plan.planStartPoint.time and ODL Live has calculated this as 03:57.
We’ve set whereToLookForWaits to ALL_STOPS so we look for waits before all stops and we’ve set whereToAddDelays to NO_JOBS_ONBOARD so delays can be added wherever there are no jobs (e.g. no passengers) on-board the vehicle. As a result a delay cannot be added after P0 (because P0 is now on-board) and therefore we still have waiting time at D0 (arrival at D0 is 04:10 but start is not until 06:00 giving a wait of nearly 2 hours). However the vehicle arrived at P0 at 04:00, which is actually closing the time of P0 (set in job.stops[0].closeTime) and so the vehicle arrived as late as possible at P0 (one hour after the openTime of P0), to minimise the waiting at D0. So waiting is still present at D0 because it’s unavoidable, but it has been reduced.
After dropping off D0 and before picking up P1 we have no jobs on-board, and a delay can therefore be added to minimise waiting at P1 and (as much as possible) D1. This is reported in the field timeEstimates.leaveAfter for D0. Stop D0 is completed at at 06:00 but leaveAfter is set to 08:39 and so the optimiser is recommending the vehicle wait at D0 after completing it, adding a delay of 08:39-06:00 = 2 hours 39 minutes between completing D0 and picking up D1.
The following values are supported for whereToAddDelays:
NOWHERE - no delays will be added.
START_ONLY - delays will only be added at the start of the route (so no delays can be added if there are already dispatched stops).
START_OR_AFTER_BREAK - delays can be added at the start of the route or after a break (including a replenish without a location). See this section for an example model using this.
NO_JOBS_ONBOARD - delays will be added wherever we have no jobs on-board. Single-stop deliveries are defined as being on-board from when the vehicle leaves the depot until they’re delivered. Single stop pickups are defined as being on-board from the point they’re picked-up until the vehicle reaches the end depot. Pickup-delivery jobs and custom jobs are defined as being on-board from the first until the last stop. Delays will be added at the closest ‘nothing on-board’ position before position(s) with waiting times and will attempt to mimimise waiting times for all stops before the next ‘nothing on-board’ position in the route.
START_MAX_NO_JOBS_ONBOARD - similar to NO_JOBS_ONBOARD except delays are added to the start first which attempt to minimise all waiting across the entire route, and then delays are attempted at the closest ‘nothing onboard’ position before each position which still has waiting time.
The following values are supported for whereToLookForWaits:
NOWHERE - no delays will be added.
FIRST_STOP - waiting will only be considered for the first stop (even if the first stop is a pickup stop at the same location as the vehicle’s start).
FIRST_WAIT - waiting will only be considered for the first stop in a route with waiting time. Waiting will not be minimised for stops after this.
FIRST_LOCATION - waiting will be considered up until (and including) the first stop that’s not at the depot location. So if you have pickup-delivery stops P1,P2,P3,D1,D2,D3 where P1,P2,P3 are actually at the vehicle start location, waiting will be minimised for all stops up to and including D1.
ALL_STOPS - waiting will be considered for all stops on the route.
To disable all delay calculations, you would set the delays object to:
"delays" : {
"whereToAddDelays" : "NOWHERE"
}
By default if there is no delays object set, the optimiser just delays the first stop. This is equivalent to:
"delays" : {
"whereToLookForWaits" : "FIRST_STOP",
"whereToAddDelays" : "START_ONLY"
}
If you want to delay the first waiting position instead, set the delays object to:
"delays" : {
"whereToLookForWaits" : "FIRST_WAIT",
"whereToAddDelays" : "START_ONLY"
}
If you are using custom jobs, you might want to add delays in-between stops of the custom job. You can control this by setting the field job.stops[].nothingOnBoard4JobAfterwards to tell ODL Live that delays can be added after a stop even if it’s not the last stop in the custom job. The following model in the directory supporting-data-for-docs demonstrates this, by having an AM pickup-dropoff and a PM pickup-dropoff in a single custom job, where delays can be added after the AM pickup-dropoff:
example-models\am-and-pm-schoolbus-routing-demostrating-delays-nothingOnBoard4JobAfterwards-field\model.json
10.5.2 Minimise waiting using during-optimisation delays
To turn on during-optimisation delays, you should configure your model as per the post-optimisation delays case, except you should include the field model.configuration.optimiser.delays and set it to true:
{
"data" : {... },
"configuration" : {
"optimiser" : {
"delays" : {
"whereToLookForWaits" : "ALL_STOPS",
"whereToAddDelays" : "NO_JOBS_ONBOARD",
"duringOptimisation" : true
}
},
"timeOverride" : { ... }
},
"_id" : "exampleModel"
}
The fields whereToLookForWaits and whereToAddDelays work exactly the same as post-optimisation case. Using during-optimisation delays you can model some types of problems where post-optimisation delays on their own could give you poorer results. These include:
Problems with waiting time costs or limits.
Pickup-delivery problems with on-board time limits, particularly where the openTime of the delivery stop is significantly later than the openTime of the pickup stop.
Problems with total work time limits.
There is a simple example model demonstrating pickup-delivery jobs with onboard time limits, custom lateness penalties and delay optimisation in the following directory:
supporting-data-for-docs\example-models\simple-pd-with-soft-onboard-limits-and-delay-optimisation
10.5.3 Accuracy of the delay calculation
By default, the delay calculation is accurate to 1 minute. For minimising delays at a stop which opens at 12:00, you may therefore see an arrival time of 11:59:03 (for example). You can set the delay search tolerance in the field searchToleranceSeconds:
"delays" : {
"whereToLookForWaits" : "ALL_STOPS",
"whereToAddDelays" : "NO_JOBS_ONBOARD",
"duringOptimisation" : true,
"searchToleranceSeconds" : 1
}
A smaller search tolerance will make the optimiser run slower.
10.5.4 Delays and dispatched stops
The optimiser assumes that once a stop is dispatched (added to the vehicle.dispatches list), it does not need to be delayed. Delays will therefore only be calculated after the last dispatched stop.
10.6 Incompatible quantities / product mix rules
In ODL Live you can specify multiple quantity dimensions (e.g. corresponding to weight, volume, passengers etc). You can also specify that if the on-board quantity for one dimension is greater than 0, another must be zero. This allows you to model problems with two product types that cannot be on-board at-once. If for example, you had a limit on weight as well, then you could setup 3 quantity dimensions corresponding to the following:
Quantity index 0 - total weight in kg
Quantity index 1 - number of product A on-board
Quantity index 2 - number of product B on-board
If the vehicle could hold a maximum of say 1000kg but no limits on the number of products, then you’d set its capacities array to [1000,999999,999999] where 999999 is just an arbitrarily large number that will never be exceeded (i.e. we’re turning off the limit).
For a product A job weighing 42 kg, you’d set its quantities to [42,1,0] as it has 42 weight, 1 of product A and 0 of product B. Similarly a product B job weighing 78 kg would have its quantities set to [78,0,1]. You’d then setup the vehicle with the incompatibleQuantityIndices list as follows, to set that quantity index 1 and 2 are incompatible and should not be on-board at once:
{
"definition" : {
"start" : {
...
},
"end" : {
...
},
"capacities" : [ 1000, 999999, 999999 ],
"incompatibleQuantityIndices" : [ {
"quantityIndx1" : 1,
"quantityIndx2" : 2
} ]
},
"_id" : "v1",
}
The incompatibleQuantityIndices can hold multiple entries, so you can prohibit multiple combinations of quantities being on-board at-once.
If you need more flexibility than incompatible quantities provides, you could implement your own logic with a sequence based state user function (SEQSTATE).
10.7 Interstop costs for the vehicle
NOTE interstop costs are not currently realtime compatible and will not be evaluated properly if you use them in realtime (i.e. live) problems where stops are marked as dispatched. They should only be used for non-realtime planning (e.g. next day).. You can define similar costs to the interstop costs using user functions (e.g. sequence based state user function (SEQSTATE) ), which are realtime compatible.
Interstop costs are defined on the vehicle object and allow you to add additional time or cost penalties between different types of stops, when served by that vehicle. The following JSON defines a pickup delivery job where its two stops are defined to be in the “Depot” and “NonDepot” stop groups respectively. A stop can be in multiple stops groups if needed, as the stopGroups field is an array.
{
"stops": [
{
"coordinate": {
"latitude": -33.7827777778,
"longitude": 150.8913888889
},
"type": "SHIPMENT_PICKUP",
"durationMillis": 600000,
"stopGroups": [
"Depot"
],
"_id": "#d2c0P"
},
{
"coordinate": {
"latitude": -33.71582551690086,
"longitude": 151.11229339244977
},
"type": "SHIPMENT_DELIVERY",
"durationMillis": 600000,
"stopGroups": [
"NonDepot"
],
"_id": "d2c0D"
}
],
"quantities": [
1
],
"_id": "d2c0"
}
The following JSON defines a vehicle with interstop costs designed to penalise returns to the depot.
{
"definition": {
"costPerTravelHour": 1,
"costPerWaitingHour": 0.1,
"costPerServicingHour": 0,
"costPerKm": 0,
"costFixed": 500,
"costPerStop": 0,
"interStopCosts": [
{
"fromStopGroupId": "Depot",
"toStopGroupId": "Depot",
"notFrom": true,
"notTo": false,
"cost": 100,
"timeSeconds": 0,
"skipNoLocationActs": true
}
],
"start": {
"coordinate": {
"latitude": -33.7827777778,
"longitude": 150.8913888889
},
"openTime": "2017-01-01T07:00",
"type": "START_AT_DEPOT",
"stopGroups": [
"Depot"
]
},
"end": {
"coordinate": {
"latitude": -33.7827777778,
"longitude": 150.8913888889
},
"closeTime": "2017-01-01T17:00",
"type": "RETURN_TO_DEPOT"
},
"capacities": [
7
]
},
"_id": "Vehicle1"
}
The interStopCosts field is an array with one or more interstop costs defined within it. Each element in the array has the following fields:
fromStopGroupId. The ‘from’ stop group id to match to.
toStopGroupId. The ‘to’ stop group id to match to.
notFrom. If true, only match if the ‘from’ stop group id is not matched.
notTo. If true, only match if the ‘to’ stop group id is not matched.
cost. Penalty cost.
zeroTravelCost. Set travel cost between the matching stops to zero.
timeSeconds. Additional time to add.
skipNoLocationActs. Ignore locationless activities (i.e. breaks) in the calculation.
10.8 Jobs allowed on vehicle
Skills are used to define what vehicles can perform what jobs. If you find the skills functionality is not sufficient, job-vehicle user functions can provide much more control on what combinations of jobs and vehicles are allowed.
Vehicle can have skills, which are a list of strings. Here is a vehicle object with two skills:
{
"definition" : {
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 51.511892,
"longitude" : -0.123313
},
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 51.511892,
"longitude" : -0.123313
},
},
"skills" : [ "Forklift" , "Refridgerated"],
},
"_id" : "John Smith",
}
Jobs can have required skills, which a vehicle must have to serve the job. Here is a job object which requires the Refridgerated skill:
{
"requiredSkills" : [ "Refridgerated" ],
"stops" : [ {
"type" : "DELIVER",
"coordinate" : {
"latitude" : 51.336,
"longitude" : 0.2674
},
"_id" : "Stop1"
} ],
"_id" : "Job1",
}
Vehicles and jobs can have any number of skills. Jobs can also have prohibited skills, which the vehicle cannot have. For example the following job cannot be served by a vehicle with the Refridgerated skill:
{
"prohibitedSkills" : [ "Refridgerated" ],
"stops" : [
...
],
"_id" : "Job1",
}
Skills can also be used to lock a job to a single vehicle, so the job will only be done by the vehicle (or not at all if the vehicle is unable to serve it). In this example we have a model with one job and one vehicle, where the job is locked to the vehicle:
{
"jobs" : [ {
"requiredSkills" : [ "John Smith" ],
"stops" : [ {
...
} ],
"_id" : "Job1"
} ],
"vehicles" : [ {
"definition" : {
"skills" : [ "John Smith" ],
...
},
"_id" : "John Smith",
} ]
}
Also see the section vehicle value-dependent cost functions which describes how to define numeric skills, which prohibit or allow a vehicle-job combination based on a vehicle’s numeric value (e.g. length, width).
10.8.1 Defining required and prohibited job groups on the vehicles
The directory:
supporting-data-for-docs\example-models\vehicle-constraints-on-job-groups
contains the following example models:
job-must-be-in-group-example.jsonjob-cannot-be-in-group-example.json
These demonstrate the use of the arrays
vehicle.definition.requiredJobGroups and
vehicle.definition.prohibitedJobGroups respectively, which
provide the same behaviour as requiredSkills and
prohibitedSkills on a job object, except they’re defined
from the vehicle’s point-of-view. Each model contains two jobs and one
vehicle. The jobs are the same in both models. This is the first
job:
{
"jobGroupIds": [
"group1"
],
"stops": [
{
"type": "SERVICE",
"coordinate": {
"latitude": 51.4826,
"longitude": 0.0
},
"_id": "s1"
}
],
"_id": "s1"
}
This job (s1) has been placed in a job group called “group1” (jobs can be in as many or few groups as you want). The other job (s2) is not in any group but is otherwise identical to s1, apart from ids.
In the example model job-must-be-in-group-example.json,
vehicle.definition.requiredJobGroups has been set as
follows:
"requiredJobGroups" : [ "group1" ]
Because jobs must be in the job group “group1”, and only job s1 is in this group, only s1 will be assigned if you optimise this model.
In the example model
job-cannot-be-in-group-example.json,
requiredJobGroups has been omitted and
vehicle.definition.prohibitedJobGroups has been set as
follows:
"prohibitedJobGroups" : [ "group1" ]
Now a job cannot be in “group1” and so only job s2 will be assigned instead.
10.9 Max parallel stop processing
The following directory:
supporting-data-for-docs\example-models\max-parallel-stop-processing
contains a simple model demonstrating the use of the
vehicle.definition.maxParallelStopProcessing field. When
you have stops at the same location which are served consecutively on
the same route, maxParallelStopProcessing tells the
optimiser that multiple stops can be processed (i.e. served) at the same
time. The primary use case for this is in patient transport scenarios
where it might take a long time to board or de-board a patient, and a
driver may have a driver’s helper who can also help board / de-board
patients at the same time as the driver.
The example model contains a single vehicle with its
maxParallelStopProcessing set to 2:
{
"definition": {
"start": {...},
"end": {...},
"capacities": [..],
"maxParallelStopProcessing": 2
}
}
The model has two pickup-dropoff jobs where the dropoff location of
the first job is equal to the pickup location of the second job. The
first job has durationMillis=3600000 (i.e. 1 hour) for its
dropoff stop and the second job has its pickup stop set to the same.
This model also has delay optimisation turned on, so the vehicle delays
its leave time to prevent waiting for the openTime of the
first job’s dropoff stop.
If you run this model, it will generate the route
pick1, drop1, pick2, drop2 with the following timings for
the stops:
- pick1 starts and ends at 2:38 (as it has zero duration).
- Vehicle arrives at drop1 at 3:00, starts serving at 3:00 and finishes at 4:00.
- Vehicle also starts serving pick2 at 3:00 and also finishes at 4:00 (i.e. it’s served in parallel with pickup1).
- Route is completed when vehicle arrives at drop2 at 4:08:59.
If instead you set maxParallelStopProcessing=1 or
removed the field, drop1 would have to be completed before pick2 starts,
and the vehicle wouldn’t finish pick2 until 5:00.
The vehicle considers stops with less than 1 metre between them to be at the same location. When using road network distances, a one metre difference could potentially lead to stops being served from different roads however (imagine two stops roughly equidistant between two roads), so if you want stops to be served at the same time like drop1 and pick2, it’s safest to give them exactly the same latitude-longitude.
Individual stop time windows and lateness functions are still correctly processed when using parallel stop processing.
When you’re using maxParallelStopProcessing, the
optimiser adds a data structure called parallelOps to each
planned stop in the vehicle plan JSON which, lists the index of the
operator serving a stop (where the index starts at 0) and the distinct
location number in the route (locationGroupIndex):
{
"vehicleId": "v",
"plannedStops": [
{
"stopId": "pick1",
"timeEstimates": {...},
"parallelOps": {
"operatorIndex": 0,
"locationGroupIndex": 0
}
},
{
"stopId": "drop1",
"timeEstimates": {...},
"parallelOps": {
"operatorIndex": 0,
"locationGroupIndex": 1
}
},
{
"stopId": "pick2",
"timeEstimates": {...},
"parallelOps": {
"operatorIndex": 1,
"locationGroupIndex": 1
}
},
{
"stopId": "drop2",
"timeEstimates": {...},
"parallelOps": {
"operatorIndex": 0,
"locationGroupIndex": 2
}
}
]
}
10.10 Maximum stop separation
NOTE these costs are not currently realtime compatible and will not be evaluated properly if you use them in realtime (i.e. live) problems where stops are marked as dispatched. They should only be used for non-realtime planning (e.g. next day).
Maximum separation costs allow you to set a penalty for the maximum travel time in hours or km between any two stops on the same route (the default is hours unless set otherwise). This applies to the stops even if they’re non-consecutive (i.e. at different places in the route). It therefore keeps routes compact (i.e. within a smaller area). You can also set a hard limit on maximum separation, so for example no two stops on the same route will be more than the maximum separation in travel time hours apart.
The following JSON defines a vehicle with max separation costs:
{
"definition": {
"costPerTravelHour": 1,
"maxSeparation": {
"includeRouteEnd": false,
"costs": [
{
"inclusiveLowerLimit": 0,
"c2": 100
},
{
"inclusiveLowerLimit": 2,
"prohibited": true
}]
},
"start": {
"type": "START_AT_DEPOT",
"coordinate": {
"latitude": 53.355615,
"longitude": -1.302941
},
"openTime": "2018-02-08T08:00",
"_id": "V2Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {
"latitude": 53.355615,
"longitude": -1.302941
},
"lateTime": "2018-02-08T17:00",
"closeTime": "2018-02-08T17:00",
"_id": "V2End"
},
},
"_id": "V2"
}
The field definition.maxSeparation.costs is an array storing a penalty function which defines optimiser costs based on the maximum travel time in hours between any two stops on this route. Please read the section on penalty functions for details of how these work.
The calculation works as follows. Assume x is the maximum travel time in hours between any two stops on the same route. For example given a route A, B, C, if travelling from A to C is 5 hours, but all other possible journeys (A to B, B to C etc) are only two hours, then x=5. x is then used as the parameter in the penalty function. If the field definition.maxSeparation.includeRouteEnd is set to true, the coordinate defined for the end of the route is also included as one of these stops when calculating the maximum travel time.
If we follow the instructions in the penalty functions section we can create a graph of this penalty function using the developer’s dashboard:

The pinkish-purple line curving upwards in the graph corresponds to the 1st subfunction (i.e. 1st element) in the penalty function array:
{
"inclusiveLowerLimit": 0,
"c2": 100
}
This defines the penalty cost as p(x) = 100 × x2 from x = 0 until the start of the next subfunction, where x is the maximum travel time in hours between any two stops on the route. The 2nd subfunction then defines a hard limit from x = 2 onwards:
{
"inclusiveLowerLimit": 2,
"prohibited": true
}
As a result, no stop on the route may be more than 2 hours drive away from any other stop on the route. This hard limit corresponds to the grey square on the right of the graph.
If we wanted to take the maximum separation between any two stops (including the end stop) down to say 1.5 hours instead, we’d use the following JSON:
"maxSeparation": {
"includeRouteEnd": false,
"costs": [
{
"inclusiveLowerLimit": 0,
"c2": 100
},
{
"inclusiveLowerLimit": 1.5,
"prohibited": true
}
]
}
Similar to the centering penalty, max separation keeps routes in small areas. If you want to be absolutely sure that routes do not overlap, you may want to combine the max separation or centering penalty with the hard contiguity constraint.
10.10.1 Turn off max separation for specific stops
You can turn max separation off for specific stops (for example if you’re picking up at a depot in a pickup-dropoff job), by setting the field stop.maxSeparation to false as shown in the following job JSON:
{
"stops": [
{
"type": "SHIPMENT_PICKUP",
"maxSeparation": false,
"coordinate": {
"latitude": 51.497338282453036,"longitude": -0.06861027643520673
},
"_id": "pick1"
},
{
"type": "SHIPMENT_DELIVERY",
"coordinate": {
"latitude": 51.55682810601671,"longitude": 0.10038226934439609
},
"_id": "drop1"
}
],
"_id": "job1"
}
Stops with maxSeparation set to false are not included in the max separation calculation.
10.10.2 Using travel hours or km for max separation
By default the max separation calculations are done in travel hours, but you can change them to km instead if you wish. The following JSON shows a vehicle with a max separation limit of 1.234 km:
{
"definition" : {
"maxSeparation" : {
"includeRouteEnd" : false,
"costs" : [ {
"inclusiveLowerLimit" : 1.234,
"prohibited" : true
} ],
"unit" : "KM"
},
"start" : {
...
},
"end" : {
...
}
},
"_id" : "veh1"
}
Valid values for the field vehicle.definition.maxSeparation.unit are ‘HOURS’ and ‘KM’.
10.10.3 Three equation max separation example
We can use any number of equations in the maxSeparation.costs array providing they follow the normal rules defining penalty functions.
In the next example we have 3 equations:
"maxSeparation": {
"includeRouteEnd": true,
"costs": [
{
"inclusiveLowerLimit": 0,
"c2": 10
},
{
"inclusiveLowerLimit": 2,
"c2": 1000,
"join" : "EXACT"
},
{
"inclusiveLowerLimit": 3,
"prohibited": true
}
]
},
If x is the maximum travel time in hours between any two stops on the same route including the end depot, then the elements do the following:
1st element. If x is between 0 and 2 hours we add an optimiser cost of 10 × x2. So we apply a mild penalty to the separation if it is under 2 hours.
2nd element. If x is between 2 hours and 3 hours we add a much larger optimiser cost of 1000 × x2. So we heavily penalise the separation if it is over 2 hours and less than 3. We’ve also set the join field so that the start of the 2nd subfunction joins to the end of the first (see penalty functions for details).
3rd element. We don’t allow x to be over 3 hours.
10.11 Overriding stop properties
Stops within jobs can be a member of any number of stop groups. For example, the following JSON shows a stop which is a member of stop groups A and B:
{
"coordinate": {
"latitude": -33.78,
"longitude": 150.89
},
"type": "DELIVER",
"durationMillis": 600000,
"costFixed": 1000,
"stopGroups": [
"A", "B"
],
"_id": "myStop"
}
Vehicles can override default properties on the stop, when the stop is assigned to them. They can override:
durationMillis, how long the stop takes to complete.
costFixed, an optimiser cost that is incurred by serving the stop.
extraTravelTimeHours, an additional travel time that is incurred by visiting the stop.
Within a vehicle’s definition object, it can have an array called stopGroupProperties which contains rules matching to stop groups. If a stop is assigned to a vehicle, the first matching rule in stopGroupProperties is used, if no match is found then the default values from the stop object are used instead.
10.11.1 Overriding stop duration
The following JSON defines a vehicle with several of these rules setup to modify stop times:
{
"definition" :
"costPerServicingHour" : 1.0,
"costPerTravelHour" : 1.0,
"start" : {
"type" : "START_AT_DEPOT",
"stopGroups" : [ "Depot" ],
"coordinate" : {
"latitude" : 52.1,
"longitude" : 2.4
},
"openTime" : "2010-01-01T09:00"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"stopGroups" : [ "Depot" ],
"coordinate" : {
"latitude" : 52.1,
"longitude" : 2.4
},
"closeTime" : "2010-01-01T17:00"
},
"stopGroupProperties" : [ {
"stopGroups" : [ "A" ],
"durationMillis" : 120000,
"matchType" : "ALL_MUST_MATCH"
}, {
"stopGroups" : [ "B", "C" ],
"durationMillis" : 240000,
"matchType" : "ALL_MUST_MATCH"
}, {
"stopGroups" : [ "Depot" ],
"durationMillis" : 360000,
"matchType" : "NONE_MUST_MATCH"
} ]
},
"_id" : "Vehicle1"
}
Inspecting the JSON we see:
- A stop within stop group A will get a durationMillis of 120000.
- A stop within group B or C will get a durationMillis of 240000.
- All other stops will get a durationMillis of 360000 except for stops in the Depot group.
The matchType must be set and has the available values:
- ALL_MUST_MATCH, all stop groups listed in the rule must be found on the stop, for the rule to pass.
- ONE_MUST_MATCH, at least one stop group listed in the rule must be found on the stop, for the rule to pass. The rule still passes if more than one stop group passes.
- NONE_MUST_MATCH, the rule only passes if none of the stop groups listed in the rule are found on the stop.
- MATCH_2_ANYTHING, the rule matches to anything irrespective of stop groups.
If you use a ‘catch-all’ rule designed to apply to all stops, if no earlier rule in the array matched, then you must also filter for the depot stops (the start and end stops), or the rule may also apply to them, giving them non-zero durations. Here we have defined the depot stops to be in group “Depot” and our ‘catch-all’ rule using matchType NONE_MUST_MATCH, passes everything except the stops in group “Depot”, so we don’t give depots a non-zero duration.
Setting the durationMillis can make a stop quicker for one vehicle to complete than another. It does not neccessarily follow that the quicker vehicle will always be assigned the stop though. Vehicles have a costPerServicingHour property and if costPerServicingHour=0, then there is no optimiser cost difference if the quicker or slower vehicle performs the stop (unless this means we save a vehicle, and vehicle costFixed is non-zero). Typically you would therefore want to use vehicle dependent stop durations in-conjunction with a non-zero costPerServicingHour.
You can also define duration functions within the stop group properties which calculate stop duration based on the stop’s quantities and original stop duration. See the section on duration functions for more details.
10.11.2 Overriding stop costs
You can also override the stop’s costFixed (fixed cost for performing the stop) in the stop group properties. Simply set the field costFixed in the rule JSON:
{
"stopGroups" : [ "Depot" ],
"durationMillis" : 360000,
"costFixed" : 100.0,
"matchType" : "NONE_MUST_MATCH"
}
A rule can have either costFixed set, or durationMillis set, or both set. If you don’t set durationMillis or costFixed within the rule object, these fields will not be automatically set and they will remain null (unlike the majority of number fields within the model).
If you want to set costs for stops which are different for different vehicles, you could also define a job-vehicle combination user function.
10.11.3 Overriding stop’s extra travel time / job ‘stickiness’
Stops can have a property extraTravelTimeHours, this is an extra travel time that is incurred by travelling to the stop from any location - even from a location with the same latitude and longitude as the stop. extraTravelTimeHours is included in the travel cost calculation, time window calculations and the late time penalties calculations. The following JSON is a stop with this property set:
{
"type" : "DELIVER",
"coordinate" : {
"latitude" : 55.9533,
"longitude" : -3.1883
},
"extraTravelTimeHours" : 0.05,
"stopGroups": [
"StopA"
],
"_id" : "Stop1"
}
Travelling to this stop from anywhere (even the same latitude and longitude) will take 3 minutes longer (0.05 hours) than the raw travel time calculated by the distances module. Normally the distances module does not take account of time for a vehicle to pull out, accelerate, decelerate and park, so adding an additional time could be useful. You could for example set the extra travel time to be higher for stops that where you know it will be difficult to park, to account for the extra time.
This extraTravelTimeHours field can be overridden by the vehicle definition’s stopGroupProperties in exactly the same way we overrode stop durationMillis and costFixed:
{
"definition" : {
"stopGroupProperties" : [ {
"stopGroups" : [ "StopA" ],
"extraTravelTimeHours" : 0,
"matchType" : "ALL_MUST_MATCH"
} ]
}
}
This can be useful for realtime pickup-delivery problems. Imagine a vehicle heading towards its next planned pickup stop. Typically you would dispatch this stop when the vehicle is about to depart for it, and then ODL Live will not schedule any new additional stops before the dispatched pickup stop. However if this assigned vehicle starts running too late, you may want to retain the ability to automatically move this pending pickup to another vehicle, even though the first vehicle is already on-route. Similarly you may want to encourage ridesharing or additional pickups before the first pickup. In these cases you wouldn’t dispatch the stop until shortly before the assigned vehicle arrives there.
The problem with this strategy is there may be multiple vehicles that have a similar amount of extra travelling time (i.e. extra fuel usage) to get to this next pickup stop, or can arrive there at a similar time. As these vehicles are travelling themselves and are subject to different traffic delays, small semi-random deviations in estimated travel time and arrival time will cause one vehicle or another to temporarily be the ‘best’ vehicle. GPS tracking itself can also add random deviations by a few metres, even for stationary vehicles waiting at the same depot. This would cause ODL Live to reassign this next pickup stop to a different vehicle, even if the difference in cost (travel cost or lateness cost) is marginal. Worse still, these random deviations could cause ODL Live to swap the job back and forth between one vehicle and another.
The solution is to encourage the optimiser to retain the first vehicle as the chosen vehicle for this pickup stop, unless it becomes a really bad vehicle (i.e. running very late). This can be achieved by using a modelling scheme like this:
Wait until the new job is assigned to a vehicle, and the new vehicle is close to departing for the pickup.
Use a PATCH to modify both the job and assigned vehicle at the same time:
Set extraTravelTimeHours in the stop object to 0.05 (3 minutes). Every vehicle will now take 3 minutes longer to arrive at the pickup.
Setup the stopGroupProperties in the assigned vehicle so you override extraTravelTimeHours for the pickup stop and set it to zero. Now the assigned vehicle will not take any longer to arrive at the pickup BUT all other vehicles will.
You may need to do the same procedure (in the same PATCH request) to the delivery stop as well, if this is also close to the late time. The stop (out of the pickup and delivery ends) that’s going to be late will normally dominate the solution cost, if you only modify extraTravelTimeHours on the pickup, but its the delivery that is going to be late, this scheme will have no effect.
So basically we’re adding a bit more time if a vehicle which isn’t the assigned one does the pickup. Intuitively adding a small amount of extra time makes sense, as the assigned vehicle is already driving to the stop but other vehicles will need to receive the new job instruction and start driving to the new stop.
As extraTravelTimeHours is included in both the lateness penalty calculations and the travel cost, this scheme works regardless of whether you’re (a) minimising travel cost (i.e. fuel consumption) as you’re due to arrive before the late time, or (b) minimising lateness as you’re due to arrive after the late time.
If a reassignment is still done to a different vehicle (presumably as the first vehicle is running more than three minutes late), you should swap the stopGroupProperties override onto the new vehicle. Never keep the stopGroupProperties override for a single job on two different vehicles, as ODL Live could still swap semi-randomly between the two vehicles.
10.11.4 Overriding stop’s duration using duration functions
The following JSON shows a vehicle with a stopGroupProperties that contains a duration function. See the section on overriding stop properties for more information on setting up stop group property rules within a vehicle.
{
"definition" : {
"start" : {
...
},
"end" : {
...
},
"stopGroupProperties" : [ {
"durationFunction" : {
"type" : "MAX",
"functions" : [ {
"millisFixed" : 60000,
"millisPerQuantity" : [ 60000.0, 0.0 ],
"millisPerQuantitySqd" : [ 100.0, 0.0 ],
"defaultMultiplier" : 0.2
}, {
"millisFixed" : 30000,
"millisPerQuantity" : [ 0.0, 39000.0 ],
"millisPerQuantitySqd" : [ 0.0, 250.0 ],
"defaultMultiplier" : 0.4
} ]
},
"matchType" : "MATCH_2_ANYTHING"
} ]
},
"_id" : "Vehicle1",
}
In this example the array stopGroupProperties contains only a single rule object with a matchType set to MATCH_2_ANYTHING so this rule will actually match to all stops, whatever their stop groups. The rule has a member variable called durationFunction, if durationFunction is set this overrides the rule’s durationMillis field, and durationMillis in the rule will be ignored.
The durationFunction object has two fields:
- type, this could be MAX, MIN, or DEFAULT_PLUS_MAX
- functions, child functions.
The child functions are executed and then the type field logic is applied to get the final result for the stop duration. If the type is:
- MAX the result is the maximum of all the child functions.
- MIN the result is the minimum of all the child functions.
- DEFAULT_PLUS_MAX the result is the maximum of all the child functions plus the stop’s original (default) duration.
The result is also rounded to the nearest millisecond and if it is negative, a value of zero will be used instead.
The child functions each have the following fields:
- millisFixed, a constant milliseconds that’s added to the duration.
- millisPerQuantity, an array containing multipliers corresponding to each quantity dimension in the stops. Milliseconds are added per unit of quantity multiplied by the corresponding multiplier.
- millisPerQuantitySqd, similar to millisPerQuantity but milliseconds are added per unit of quantity squared.
- defaultMultiplier, the stop’s original (default) duration is multiplied by this value and added to the duration.
Any one of these child function fields can be omitted and their values will default to zero, for example if you don’t want to set millisPerQuantitySqd just don’t include the field in the JSON.
For a single one of these child functions, the duration will be equal to:
where a is the millisFixed field, n is the number of quantities in the problem, bi is the ith value of millisPerQuantity, qi is the stop’s ith quantity value, ci is the ith value of millisPerQuantitySqd, d is the default multiplier and s is the stop’s original (default) duration milliseconds (as defined in the stop’s durationMillis field).
In the example JSON, the first child function includes terms based on the first quantity dimension and the second child function includes terms based on the second quantity function. We then take the MAX of these two functions, so we’re working out two separate durations each based on one quantity dimension and then taking the max of them as the final duration.
10.11.5 Overriding if a stop is the next stop on a vehicle - vehicleId~OptPlanStartPoint
In realtime optimisation scenarios you may want to encourage the optimiser to serve a stop as the next stop a vehicle does (i.e. after its dispatched and completed stops). This creates an issue as we override using stop groups, however you want a stop group that either:
- refers to the vehicle start location if the vehicle has no dispatched stops.
- refers to the last dispatched stop if the vehicle has dispatched stops (i.e. refers to the location where the optimiser can start planning the placement of stops).
To solve this, ODL Live creates an automatic stop group id corresponding to each vehicle’s optimiser plan start point. This stop group id is:
vehicleId~OptPlanStartPoint
So for a vehicle with its _id equal to large-truck-4, this stop group is automatically defined:
large-truck-4~OptPlanStartPoint
In the following JSON we have a job defined with extra travel time and cost defined on the stop if it doesn’t directly follow the start of vehicle v2. This cost will still work whether vehicle v2 has dispatched stops or not:
{
"stops": [
{
"type": "SERVICE",
"coordinate": {"latitude": 51.49733,"longitude": -0.06861027 },
"fromStopCosts": [
{
"fromStopGroupId": "v2~OptPlanStartPoint",
"notFrom": true,
"cost": 1000.0,
"timeSeconds": 60.0,
"skipNoLocationActs": true
}
],
"_id": "s1"
}
],
"_id": "j1"
}
For more details, see the example model and the readme.txt in the directory:
supporting-data-for-docs\example-models\stop-group-OptPlanStartPoint
10.12 Park and walking loop
With park-and-loop route optimisation, a driver can park their truck and deliver parcels on-foot in a loop. In dense urban areas where parking is tricky, this reduces delivery times, traffic, and CO2 footprint. The implementation in ODL Live doesn’t limit you to driving a car and then walking the loop on-foot either, you could ride a motorcycle and then walk on-foot, or drive a truck and then do a riding loop on a bicycle.
With park-and-loop optimisation, you have:
- the primary method of travel, which is the usual method defined on the vehicle object using either the default distance profile or the profile set in vehicle.definition.distancesProfileId, and
- the secondary method of travel, defined on vehicle.definition.secondaryTravel.
When ODL Live is optimising, for a given journey between two locations X and Y, it will decide whether it’s better to (a) use the primary method, or (b) to park at location X, use the secondary method to visit Y, and then return to the parking location X later. Depending on your settings, multiple stops can be served in a row using the secondary travel method. Typically the secondary travel method will actually be slower than the first - e.g. walking is slower than driving - unless we consider the time it takes to park. For example, if you have 2 stops that are 1 minute apart, without considering parking time it is still quicker to drive between them, however if you assume it takes 5 minutes to park, it’s better to park at one stop and serve both the stops on-foot. The values used for parking time and parking cost are therefore very important, without setting them correctly, the optimiser will not do park-and-loops.
The supporting-data-for-docs directory provided with your ODL Live release has an example park-and-loop model in it (see the folder example-models\park-and-loop and the readme doc). The following vehicle is taken from that model. This vehicle has a secondary travel mode defined, in the field vehicle.definition.secondaryTravel:
{
"definition" : {
"costPerTravelHour" : 1.0,
"costPerKm" : 1.0E-6,
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : -37.84152843715891,"longitude" : 144.93651339153962
},
"openTime" : "2021-04-01T08:00",
"_id" : "v1Start"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : -37.84152843715891,"longitude" : 144.93651339153962
},
"closeTime" : "2021-04-01T18:00",
"_id" : "v1End"
},
"secondaryTravel" : {
"costPerTravelHour" : 1.0,
"costPerKm" : 1.0E-6,
"distancesProfileId" : "walk"
}
},
"_id" : "v1"
}
The following fields are supported in the secondaryTravel object:
costPerTravelHour - see section on basic vehicle costs.
costPerKm - see section on basic vehicle costs.
extraCostPerKm - see section on A to B travel limits.
extraCostPerTravelHour - see section on A to B travel limits.
parking - defines parking time and cost for the secondary travel mode, see section on primary travel mode parking as the logic works the same. If parking time is defined on both the vehicle and the stop, the total parking time is just summed.
distancesProfileId - the distances profile (defined in model.distances.configuration.distances) the secondary mode should use.
travelTimeMultiplier - see section on travel time multiplier.
maxDistanceMetresFromParked - the maximum number of metres a stop served by secondary travel mode can be from the primary mode parking location. Use this if you want a rule like ‘do not serve any stops on-foot which are more than 200m from the parking location’.
maxNbStopsInSingleLoop - the maximum number of stops which can be served in a single park and loop without returning to the parking location first. Use this if you want to limit the number of stops that can be walked in a row (e.g. limit to 3 stops).
Fieldnames which are also found in the primary travel model (i.e. can also be defined in the vehicle.definition object), work exactly the same for primary and secondary modes.
The example vehicle uses the following distances configuration, which uses a road network graph with both “car” and “foot” vehicle types defined. The default profile is “car” and we have the “foot” profile defined with a distances profile id of “walk” in the model.data.configuration.distances.profiles object. vehicle.definition.secondaryTravel.distancesProfileId was set to use this “walk” profile:
{
"data": {},
"configuration": {
"distances": {
"graphDirectory": "C:\\australia-multi-vehicle-streaming",
"type": "ROADS",
"roadNetworkVehicleType": "car"
"profiles": {
"walk": {
"graphDirectory": "C:\\australia-multi-vehicle-streaming",
"type": "ROADS",
"roadNetworkVehicleType": "foot"
}
}
}
},
"_id": "myModelId"
}
The next JSON shows one of our jobs. Here we have two different parking objects defined in the stop object, parking for the main travel mode and secondaryTravelParking for the secondary mode:
{
"stops" : [ {
"type" : "DELIVER",
"durationMillis" : 60000,
"coordinate" : {
"latitude" : -37.81607973576584,"longitude" : 145.02550863331146
},
"parking" : {
"parkingTimeSeconds" : 300.0,
"parkingTimeMinSeparationMetres" : 1.0,
"cost" : 0.0
},
"secondaryTravelParking" : {
"parkingTimeSeconds" : 30.0,
"parkingTimeMinSeparationMetres" : 1.0,
"cost" : 0.0
},
"_id" : "s1"
} ],
"_id" : "j1"
}
If the stop is served by the primary travel mode, the parking object will be used, however if it is served by the secondary travel mode, the secondaryTravelParking object will be used instead. In this example job.stop[0].parking.parkingTimeSeconds is set to 5 minutes, so we’re saying it takes 5 minutes to park. job.stop[0].secondaryTravelParking.parkingTimeSeconds is set to just 30 seconds, which basically corresponds to ‘knocking on the door’ time as we’re already parked. As parking time can be defined on the stop level, if you know some stops have easier parking than others, you should set this appropriately in the parking time and cost. See section on stop parking for more details on how the parking objects work.
The vehicle plan contains some special fields which report on park-and-loop.
stayParkedAfter - found in both vehiclePlan.planStartPoint and the planned stop objects. If the field is shown and is true, it indicates the driver should stay parked at this stop (or at the start point) after serving it, and walk to the next stop. Here’s a planned stop with stayParkedAfter set:
{ "stopId": "s67", "timeEstimates": {.... }, "earliestDispatch": "2021-04-01T07:55:00", "stayParkedAfter": true }travelMode - found in the planned stop objects. If field is present and is set to “S”, this indicates the secondary travel mode was used to travel to the planned stop (e.g. you should walk to the stop). Here’s a planned stop with this set:
{ "stopId": "s224", "timeEstimates": {... }, "earliestDispatch": "2021-04-01T08:07:53", "travelMode": "S" }return2VehicleCoord - found in the planned stop objects, for special stops (which are not part of any jobs) that indicate the driver should return back to their parking location. This field contains the coordinate object defining the latitude and longitude the driver should walk back to. This coordinate object will be the same as job[].stop.coordinate for the stop where the driver first parked (i.e. which had stayParkedAfter=true). Here’s a ‘return to vehicle’ stop:
{ "stopId": "ret2Veh_-37.82751321189808_144.98943198662693", "timeEstimates": {...}, "travelMode": "S", "return2VehicleCoord": { "latitude": -37.82751321189808, "longitude": 144.98943198662693 } }
The software developer’s dashboard reads these fields and prints icons for ‘park’, ‘walk to stop’ and ‘return to car’ based on them, as shown in the following screenshot:

When you look at the map in the dashboard with park and loop activated, secondary travel segments are shown in dotted dark green, as in the following screenshot:

In this screenshot we’ve added the parameters geometry=RAW and noseq=true to the URL, so the road geometry is drawn accurately and we hide sequence numbers. By default, if you look at the map through the dashboard, the road geometry between stops is drawn simplified (for performance reasons) and when you zoom in to a park-and-loop route, the routes between stops will look odd as a result. Setting the option geometry=RAW is therefore advised so you see the true road network geometry that the optimiser is using internally for its calculations.
10.13 Parking time and cost
You can define parking time and cost on a vehicle in the same way you can define it on a stop - see section stop parking time for more details. The parking object is defined at vehicle.definition.parking, as shown below:
{
"definition" : {
"parking" : {
"parkingTimeSeconds" : 60.0,
"parkingTimeMinSeparationMetres" : 1.0,
"cost" : 0.0
},
"start" : {
...
},
"end" : {
...
}
},
"_id" : "v0"
}
When both the stop and vehicle have parking, the parking times and costs are summed.
10.14 Route centering / compact areas
10.14.1 Use cases
ODL Live optimises to maximise the number of loaded jobs and minimise the total cost - so minimising the sum of lateness penalty, travel cost etc. In some circumstances, vehicle routes that are efficient from the point of view of having minimal travel time, travel distance, lateness etc. can physically overlap each other. Such routes are still efficient but may not be visually appealing, particularly if drivers prefer to work in a small geographic area.
This type of issue can also occur when we consider road network geometry. For example, imagine we have a motorway running North-South and we have four stops to be served - one in the North-East, one in the North-West, one in the South-East and one in the South-West. Let’s imagine these stops must be served by two vehicles and the other constraints allow for only two possible solutions:
Possible solution 1. Route 1 serves South-East and North-East stops and route 2 serves South-West and North-West stops.
Possible solution 2. Route 1 serves South-East and North-West stops and route 2 serves South-West and North-East stops.
Solution 1 will lead to more visually appealing routes as there is no perceived cross-over between them, however if both possible solutions require both routes to traverse the North-South motorway, the actual travel time could be identical for both solutions. For identical travel times and without a centering penalty, the optimiser would choose randomly between the two solutions.
The centering penalty reduces the geographic spread of a planned route, trading this off against travel time, and can therefore create visually appealing routes which may have higher driver approval.
If you want to be absolutely sure that routes do not overlap, you may want to combine the centering penalty with the hard contiguity constraint.
10.14.2 How the penalty works
As of ODL Live version 1.4.4 onwards, dispatched stops are also included in the centering calculation.
For each planned route, the algorithm either (a) calculates the most central stop on that route - i.e. the stop with minimum travel cost to all other stops or (b) uses the fixed center you set in the vehicle JSON. Each stop on a route then incurs a centering penalty equal to the travel cost from the stop to the center location, multiplied by the costmultiplier for the vehicle. The penalty is summed over all stops in a solution. The strength of the centering can therefore be increased or decreased by changing the costmultiplier. For dispatched stops, for performance reasons a minor approximation is made using straight-line distances to identify the centroid of all dispatched stops, which is then used in the centering calculation together with the non-dispatched stops using road network distances (if set).
If you don’t want certain stops in a job to be considered in the centering calculation, you can control this by setting the boolean field job.stops[].centering, as shown in the following JSON where the pickup stop is included but the delivery stop is not:
{
"quantities" : [ 1 ],
"stops" : [ {
"type" : "SHIPMENT_PICKUP",
"durationMillis" : 60000,
"centering" : true,
"coordinate" : {
"latitude" : 51.5073,"longitude" : -0.1657
},
"_id" : "pickup1"
}, {
"type" : "SHIPMENT_DELIVERY",
"durationMillis" : 60000,
"centering" : false,
"coordinate" : {
"latitude" : 51.5416, "longitude" : -0.1462
},
"_id" : "delivery1"
} ],
"_id" : "job1"
}
10.14.3 Calibrating the centering penalty
Increasing the costmultiplier will increase the importance of the centering penalty relative to other penalties - for example balance or travel cost. You should therefore expect a small increase in travel cost (e.g. total travelling time or distance) when you apply the centering penalty. Ideally the total centering penalty cost for a solution (summed over all stops) should be roughly the same magnitude (within a factor of 2 or 3 say) as the total travel cost. This way the centering penalty will take affect but not totally dominate and impact on other cost factors - such as balancing via the fullness penalty - which may also be calibrated according to the travel cost.
If you GET the JSON of the optimiser plan (see walkthroughs etc.), then within the plan statistics you can find a breakdown of the various costs, as used by the optimiser. Inspect the travel cost and centering cost to see if they’re roughly the same order of magnitude. If you want centering to dominate the travel cost (i.e. you want very compact routes) but not affect fullness penalty, lateness etc., then try setting the costmultiplier so that the total centering cost is a few times the travel cost.
The centering object sits within the vehicle definition, as follows:
"definition" : {
"start" : {
....
},
"end" : {
....
},
...,
"centering" : {
"costMultiplier" : 1.0
}
},
10.14.4 Fixing the centre
You can fix the coordinate that the optimiser uses as the centre of the route, instead of letting the optimiser calculate the best centre from all stops on the route. The centering cost will then penalise stops on the route based on their travel cost from this fixed centre. If the costMultiplier is high enough, the optimiser will therefore allocate stops to the route which are as close to this fixed centre as possible.
You fix the center location by setting a lat-long on the centering.coordinate field:
{
"definition" : {
"centering" : {
"costMultiplier" : 1,
"coordinate" : {
"latitude" : 51.51729581633927,
"longitude" : -0.2940833461842728
}
},
"start" : {
...
},
"end" : {
...
}
},
"_id" : "v0"
}
10.15 Service radius / geofences
A service radius can be used to restrict the maximum travel distance or time between either (a) the vehicle’s start location and a stop or (b) a base coordinate that you set on the vehicle (different from the start location) and a stop. The start location is always the location defined in the start object in the vehicle definition, not (if you’re using real-time planning) the vehicle’s current location. For each vehicle, you can define both a soft radius and a hard radius. Assignments of stops outside of the soft radius are allowed, but incur a penalty. Assignments of stops outside of the hard radius are not allowed. The radius can be set to operate on either travel time in hours, or travel distance in kilometres.
Service radius is set on the vehicle object, and can therefore be set differently for each vehicle. The following JSON defines a vehicle with a service radius around its start location:
{
"definition" : {
"start" : {
"coordinate" : {
"latitude" : 51.511892,
"longitude" : -0.123313
},
"openTime" : "2017-07-18T12:45:00",
"type" : "START_AT_DEPOT",
},
"end" : {
"coordinate" : {
"latitude" : 51.511892,
"longitude" : -0.123313
},
"lateTime" : "2017-07-18T22:45:00",
"closeTime" : "2017-07-18T22:45:00",
"type" : "RETURN_TO_DEPOT",
},
"serviceRadius" : {
"type" : "KM",
"softRadius" : 100.0,
"hardRadius" : 200.0,
"costFixed" : 0.0,
"costPerViolation" : 1000.0,
"costPerSquareViolation" : 0.0
}
},
}
If you want to override the base location of the service radius, just set a coordinate object on the serviceRadius object:
"serviceRadius" : {
"type" : "KM",
"softRadius" : 100.0,
"hardRadius" : 200.0,
"costFixed" : 0.0,
"costPerViolation" : 1000.0,
"costPerSquareViolation" : 0.0,
"coordinate" : {
"latitude" : 51.4545,
"longitude" : -2.5879
},
}
The radius is set to type KM. The soft radius is set to 100, meaning stops more than 100 km from the start location will be penalised. The violation is defined as the travel KM or hours (dependent on the radius type) a stop is outside the radius. So a stop 102 km from the start with a 100 km soft radius will have:
violation = 102 - 100 = 2.
The penalty cost from a soft violation is defined as:
penalty = costFixed + (costPerViolation * violation)
+ (costPerSquareViolation * violation * violation)
The costFixed is therefore a constant added whenever a soft violation occurs whereas costPerViolation and costPerSquareViolation are the multipliers for violation and the square of violation. Having a fixed cost and multipliers gives the user flexibility to fine-tune the penalty cost. The penalties for different stops are added together - i.e. the total service radius penalty for a set of vehicle routes is the sum of the penalties for all assigned stops outside their vehicle’s service radius.
If you want to assign to the vehicle with least service radius violation, the simplest configuration is to set costPerViolation to a large number and keep costFixed and costPerSquareViolation set to zero. Remember if you want this penalty to dominate all other costs, the multipliers have to be large enough. In particular, if you’re using late time modelling as well, late time costs are by default set to dominate all other costs so you might want to (a) stop using late time modelling (only if your problem isn’t real-time) or (b) suitably reduce the late time penalty. If the late time cost still dominates the total solution cost, then minimising lateness will be prioritised above minimising service radius violation.
Assignments will not be made for a stop outside of the hard service radius - which was set to 200 km in the example JSON. A hard radius will be considered more important than the late time penalty (or any other penalty).
If you want the radius to be based on travel time instead, set the type to HOURS. The other values will then be in units of hours - so softRadius and hardRadius are defined in hours and costPerViolation is actually a cost per hour travel time outside the service radius.
The following image shows an example problem with an overridden base location for the service radius. Drivers start at a common central depot and drive out to their predefined patches, where they are familiar with the local area (and can therefore deliver more quickly). The triangles on the image shows the centre of each driver’s area. Areas will flex smaller and larger each day based on demand, e.g. if there’s a lot of deliveries in a driver’s area, their area will shrink as some deliveries will be automatically allocated to the neighbouring drivers.
10.15.1 Polygon hard service radius (geofence)
If you want to set a polygon area in which the vehicle is allowed to serve jobs, you can set a GeoJSON polygon as a service radius. We show this in the following example vehicle:
{
"definition" : {
"serviceRadius" : {
"hardGeoJSONPolygon" : {
"type" : "Polygon",
"coordinates" : [ [ [ -73.83359339215494, 40.62359497991594 ], [ -73.83361746755007, 40.62359428081693 ], [ -73.83362994278977, 40.62361117440607 ], [ -73.83363278247985, 40.623630987651396 ], [ -73.83363459028607, 40.62364076123104 ], [ -73.83363658292342, 40.62365153690172 ], [ -73.83365484362989, 40.6236655040863 ], [ -73.83367021064505, 40.6236665907997 ], [ -73.83368661569814, 40.623667751385284 ], [ -73.8336991074219, 40.62367804166611 ], [ -73.83370757495803, 40.62369368604875 ], [ -73.83370768871171, 40.62371253859068 ], [ -73.83369065729673, 40.62372132631638 ], [ -73.83366238668333, 40.623730081528244 ], [ -73.83363731573742, 40.623743986012485 ], [ -73.83363724441523, 40.62377260037096 ], [ -73.8336304335293, 40.62380047103179 ], [ -73.833606328792, 40.62381291019078 ], [ -73.83362092674392, 40.62383525087618 ], [ -73.83362931182866, 40.62386430349868 ], [ -73.83363504243655, 40.62388338698224 ], [ -73.8336503793823, 40.6239112908645 ], [ -73.83365224121476, 40.62393697274351 ], [ -73.83363582579899, 40.62395529191133 ], [ -73.83360212776157, 40.62395304181097 ], [ -73.83357711957946, 40.623942000629874 ], [ -73.83355923946421, 40.6239360345009 ], [ -73.83357550708051, 40.6235979172802 ], [ -73.83359339215494, 40.62359497991594 ] ] ]
},
"type" : "POLYGON"
},
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 40.7829,"longitude" : -73.9654
},
"openTime" : "2020-01-01T00:00",
"_id" : "LHMtHlA2RHe7Re7Z7CiLgQ=="
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 40.7829,"longitude" : -73.9654
},
"closeTime" : "2020-02-01T00:00",
"_id" : "_zGcKMPkQSWs4yS3Y08Myw=="
}
},
"_id" : "17"
}
The field vehicle.definition.serviceRadius.type should be set to type POLYGON, not HOURS or KM as in the previous examples. The field vehicle.definition.serviceRadius.hardGeoJSONPolygon should contain a valid GeoJSON polygon (and only a Polygon, other GeoJSON types such as LineString or MultiPolygon will create an error). The optimiser will not allow stops outside the polygon to be assigned to the vehicle (subject to the multi-stop job logic discussed below). You are advised to keep the number of points in your GeoJSON polygon small, as very complicated GeoJSON polygons can slow the optimiser down.
10.15.2 Service radius logic with multiple-stop jobs (e.g. pickup-delivery)
There are multiple options for how service radius is calculated when a job has multiple stops, for example for a pickup-dropoff job. These are set using the field multiStopLogic. This field can take the following values:
SUM, service radius costs and violations are summed over the multiple stops in a job. So if either the pickup or delivery stop has a hard violation, the job will not be served.
MIN, take the min of service radius costs and violations over the multiple stops in a job, so a pickup-delivery job can be served if either the pickup or the delivery are within the service radius.
The following example shows the serviceRadius object configured for a 200 KM hard radius that use the MIN logic:
"serviceRadius" : {
"type" : "KM",
"hardRadius" : 200.0,
"multiStopLogic" : "MIN"
}
You can also disable service radius for specific stops.
10.15.3 Disabling service radius for specific stops
You can disable the service radius for specific stops, so they are not considered by the service radius logic. This is useful if for example, you’re doing pickup-deliveries and you only want the service radius to be considered for the delivery stop, not the pickup stop. For self-hosting ODL Live subscribers the supporting-data-for-docs directory has the following example model:
supporting-data-for-docs\example-models\service-radius\disable-service-radius-for-stop.json
In this model we have a vehicle that starts in London, UK, then picks up an item also in London, UK and delivers it to Birmingham, UK. We have a soft service radius on the vehicle, and a different base location for the service radius (near to Birmingham). The radius is setup so that extra cost is added to the solution for every stop the vehicle serves, where the cost for each stop increases the further the stop is away from the base location. It therefore encourages the vehicle to only serve stops close to its service radius base location. We also disabled the service radius calculation for the pickup in London. With this disabled, only the delivery in Birmingham (which is near to the vehicle’s service radius base location) will contribute to the solution cost. In contrast, if the service radius is not disabled for the pickup in London, both the pickup in London (which is far from the vehicle’s service radius base location) and the delivery in Birmingham would be considered and the solution cost will be much higher.
Here is the job JSON where we’ve set job.stops[0].serviceRadius=false to disable service radius calculation for the pickup:
{
"stops": [
{
"type": "SHIPMENT_PICKUP",
"serviceRadius": false,
"coordinate": {
"latitude": 51.5073,"longitude": -0.1657
},
"_id": "P0"
},
{
"type": "SHIPMENT_DELIVERY",
"coordinate": {
"latitude": 52.4862,"longitude": -1.8904
},
"_id": "D0"
}
],
"_id": "J0"
}
Here is the vehicle where we’ve setup the soft service radius and the service radius base location:
{
"definition": {
"serviceRadius": {
"softRadius": 0,
"hardRadius": 99999,
"costPerViolation": 1,
"costPerSquareViolation": 1,
"coordinate": {
"latitude": 52.1936,"longitude": -2.2216
},
"type": "KM"
},
"start": {
"type": "START_AT_DEPOT",
"coordinate": {
"latitude": 51.56026,"longitude": -0.16067
},
"openTime": "2020-01-01T00:00",
"_id": "veh1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {
"latitude": 51.56026,"longitude": -0.16067
},
"closeTime": "2020-02-01T00:00",
"_id": "veh1End"
}
},
"_id": "veh1"
}
You can use a similar setup in your own models if (a) you have pickup-deliveries and (b) you want the deliveries to be as close as possible to a preferred location for the vehicle (e.g. at the centre of the vehicle’s normal operating area).
10.16 Start and end coordinates
Vehicles must always have vehicle.definition.start and vehicle.definition.end objects. The start and end lat-long coordinates are however optional, so you can:
Leave out the field vehicle.definition.start.coordinate, so vehicles start at their first stop.
Leave out the field vehicle.definition.end.coordinate, so vehicles finish at their last stop.
Leave out both.
The start and end objects must still have their type field set to START_AT_DEPOT and RETURN_TO_DEPOT respectively, even if the coordinates are not set.
In the following JSON we show a vehicle with no end coordinate defined:
{
"definition": {
"start": {
"type": "START_AT_DEPOT",
"coordinate": {
"latitude": 51.5138,
"longitude": -0.0984
},
"openTime": "2019-12-06T09:30:00",
"_id": "V0Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"closeTime": "2019-12-07T09:30:00",
"_id": "V0End"
},
"capacities": [
5
]
},
"_id": "V0"
}
10.17 Start / end times and lateness
Similarly to time windows for stops, vehicles can have time windows and custom lateness penalty function, however there are some limitations compared to stop time windows.
For the start ‘stop’ in a vehicle definition you must only set the openTime field (out of the open, late and close fields) and you cannot use the advanced time windows structure. The field vehicle.definition.openTime defines when the vehicle leaves its start coordinate.
For the end ‘stop’ in a vehicle definition you can either:
- Set vehicle.definition.end.closeTime (the hard time the vehicle must finish by) and optionally set vehicle.definition.end.lateTime (the soft time the vehicle should ideally finish by)
OR
- Use the multiTWs field instead of closeTime and lateTime, which allows you to set custom lateness penalties on the time the vehicle finishes. Although vehicle.definition.end.multiTWs is an array object (to keep compatibilty with the array object in job.stop[].multiTWs) you can only set one element in this array (i.e. one end time window for the vehicle). You can set multiple penalties functions within this one vehicle end time window though.
The following example JSON shows a simple vehicle with just an openTime and hard closeTime set:
{
"definition": {
"start": {
"type": "START_AT_DEPOT",
"coordinate": {...},
"openTime": "2019-12-06T09:30:00",
"_id": "V0Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"closeTime": "2019-12-07T09:30:00",
"_id": "V0End"
}
},
"_id": "V0"
}
10.18 Total km at stop cost (do stops together, earlier in route)
When you’re using road networks and have many stops in the same area, it can easily happen that a vehicle needs to go down the same road twice, e.g. once going North and once going South. In this situation, the cost of serving a stop on this road on the first pass or the second pass is usually the same, and so the optimiser will choose a random pass to serve the stop on. If you have multiple stops on this road, you may find that some stops are served on the first pass and some on the second, as this will have the same cost as serving all stops at-once. Most drivers would however prefer to do close-by stops together, and on the first time they pass them
If you want to ensure the stop(s) are served on the first pass (and so served together), you can set a cost on the vehicle which adds a cost when each stop is visited that’s proportional to the amount of km the vehicle has driven at that point. This makes it cheaper to serve stops on the first pass of a road instead of the second.
This cost is set in the field vehicle.definition.costPerTotalPostDispatchKmAtStop. When each stop is visited, this number is multiplied by the total km the vehicle has driven upon reaching the stop, excluding km driven for dispatched stops (so it’s ‘post-dispatch’ km only).
The following JSON shows a vehicle with this defined:
{
"definition" : {
"costPerTravelHour" : 1.0,
"costPerTotalPostDispatchKmAtStop" : 1.0E-5,
"start" : {
...
},
"end" : {
...
}
},
"_id" : "vehicle1"
}
You should first try setting costPerTotalPostDispatchKmAtStop to be very small compared to the other travel costs (e.g. 0.00001). If you’re modelling rush hours (e.g. using standard speeds) you may need to set it larger (e.g. 0.0001).
10.19 Total travel hours or km limits
NOTE from engine v1.0.28 onwards these costs are realtime compatible. If you dispatch stops and these costs are active, the travel time and distance from dispatched stops will be included in the calculations. Prior to version 1.0.28, the times and distances for dispatched stops were not included.
Total travel hours costs allow you to set a penalty for the total number of hours spent travelling on a route. Similarly total travel km costs allow you to set a penalty on the total number of km spent travelling. The two penalties work in exactly the same way. You can also set a hard limit on total travel hours or total travel km.
Total travel are defined using the same type of penalty functions used in various other parts of the ODL Live API. You should read the section on penalty functions before reading this section.
The following JSON defines a vehicle with total travel hours costs:
{
"definition": {
"costPerTravelHour": 1,
"totalTravelHours": [
{
"inclusiveLowerLimit": 4,
"c0": 10000,
"c1": 1000,
"c2": 1000,
"join" : "PLUS_CONST"
},
{
"inclusiveLowerLimit": 5,
"c0": 10000,
"c1": 10000,
"c2": 10000,
"join" : "PLUS_CONST"
},
{
"inclusiveLowerLimit": 5.5,
"prohibited": true
}
],
"start": {
"type": "START_AT_DEPOT",
"coordinate": {"latitude": 53.355615,"longitude": -1.302941
},
"openTime": "2018-02-08T08:00",
"_id": "V1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {"latitude": 53.355615,"longitude": -1.302941
},
"closeTime": "2018-02-08T17:00",
"_id": "V1End"
},
},
"_id": "V1"
}
The array in definition.totalTravelHours is a penalty function. If we follow the instructions in the penalty functions section, we can use the developer’s dashboard to create a graph of this function:
In this penalty function array the elements (i.e. subfunctions) do the following:
1st element (short green line on the graph). Assume x is total travel hours. If x is between 4 and 5 hours, have a starting optimiser cost of 10000, add an optimiser cost of (1000 × x) + (1000 × x2).
2nd element (steep dark blue line on the graph starting at x = 5). If x is between 5 and 5.5 hours, add a much larger optimiser cost of 10000 + (10000 × x) + (10000 × x2).
3rd element (grey area). Don’t allow optimiser cost to be above 5.5 hours as prohibited=true.
If we wanted to modify this JSON to prohibit total travel time over 5 hours we could instead set:
"totalTravelHours": [
{
"inclusiveLowerLimit": 4,
"c0": 10000,
"c1": 1000,
"c2": 1000,
"join" : "PLUS_CONST"
},
{
"inclusiveLowerLimit": 5,
"prohibited": true
}
]
So we’ve dropped the 3rd record and turned the 2nd record into a prohibited record. If we want a hard limit of 4.5 hours instead, we could instead use:
"totalTravelHours": [
{
"inclusiveLowerLimit": 4,
"c0": 10000,
"c1": 1000,
"c2": 1000,
"join" : "PLUS_CONST"
},
{
"inclusiveLowerLimit": 4.5,
"prohibited": true
}
]
When changing these values, you’re advised to check them using the penalty functions graph viewer.
If you wanted to set the limit on total travel km instead, change the field name from totalTravelHours to totalTravelKm. The following JSON shows a vehicle with a soft totalTravelKm limit of 100 km and a hard limit of 150 km:
{
"definition": {
"costPerTravelHour": 1,
"totalTravelKm": [
{
"inclusiveLowerLimit": 100,
"c0": 1000,
"c1": 1000,
"c2": 1000,
"join" : "PLUS_CONST"
},
{
"inclusiveLowerLimit": 150,
"prohibited": true
}
],
"start": {
"type": "START_AT_DEPOT",
"coordinate": {"latitude": 53.355615,"longitude": -1.302941
},
"openTime": "2018-02-08T08:00",
"_id": "V1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {"latitude": 53.355615,"longitude": -1.302941
},
"closeTime": "2018-02-08T17:00",
"_id": "V1End"
},
},
"_id": "V1"
}
The following screenshot shows the graph of this penalty function:
The same vehicle can have both a totalTravelHours limit and a totalTravelKm limit (i.e. the two can be combined).
10.20 Total work time limit
To use the total work time limit, all stops in your problem must have openTime defined.
The maximum worktime limit lets you place a limit on the total time from the vehicle starting work until the vehicle ending work, and accounts for the case when the vehicle’s first stop might not open some time after the vehicle itself starts (and so in-reality the vehicle’s start time would be delayed).
Let’s consider an example. We have a vehicle which starts at 7am (so vehicle.definition.start.openTime is set to 7am on the day we’re modelling). The vehicle is assigned a single job containing a single stop, but the stop’s open time isn’t until 12pm. Driving to the stop only takes 1 hour, so internally to its calculation, the model assumes the vehicle drives to the stop at 7am, gets there at 8am and then waits 4 hours for the stop to actually open. In real-life the driver would simply leave home later, at around 11am instead. The stop itself takes 5 hours to serve.
If we tried to enforce a maximum worktime limit using vehicle.definition.end.closeTime, and we wanted the driver to work no more than 8 hours, we’d set vehicle.definition.end.closeTime to 3pm based on the vehicle open time of 7am. The stop therefore becomes unservable, as the stop can’t be served until 12pm and takes 5 hours, so the driver won’t finish the stop until 5pm and won’t return home until 6pm. However this doesn’t take account of the fact that in real-life, the driver would just leave home later.
The work time limit does this differently. Inside its calculation, it works out an approximate ‘latest leave home’ time and then calculates the work time limit relative to this. So for our example it would work out a leave home time of 11am, so the driver doesn’t wait for the stop to open, and then the driver can return home by 6pm which is within their 8 hour limit.
The latest leave home time is calculated internally by taking the waiting time at the first stop assuming the driver leaves home at their open time, and then adding this waiting time to the vehicle’s current departure time. It does not currently account for the difference in the travel time to the first stop between rush hour and non-rush hour. The calculation will not work properly unless all stops have an open time.
The maximum work limit is defined using the same type of penalty functions used in various other parts of the ODL Live API. You should read the section on penalty functions before reading this section. The following vehicle JSON has a hard maximum work time limit set on it, where the penalty function is stored in definition.workTimeHours:
{
"definition" : {
"workTimeHours" : [ {
"inclusiveLowerLimit" : 9.0,
"prohibited" : true
} ],
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 51.5138,"longitude" : -0.0984
},
"openTime" : "2001-01-01T00:00",
"_id" : "vehicle1Start"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 51.5138,"longitude" : -0.0984
},
"closeTime" : "2002-01-01T00:00",
"_id" : "vehicle1End"
}
},
"_id" : "vehicle1",
}
If we check the penalty function on the penalty functions graph viewer we get:
Where we see cost is zero below 9 hours and then prohibited (the grey area) after 9 hours.
As we’ve defined the work time limit using a penalty function, you can setup either a hard time limit (as in our JSON example) or a soft limit that incurs cost penalties if exceeded, or even both together.
10.20.1 Work time limit and realtime updates
The following rules are used to calculate the start work time used in the work time limit constraint, when the vehicle has realtime updates (i.e. dispatched events):
If we have a dispatched stop, the maximum of the dispatch
creationTimefield and the vehicle start time are used as the start work time. So the vehicle starts later if its first stop is dispatched (i.e. sent to it) later.GPS values are not used as the start work time (as a driver could be sending a GPS value when they’re still parked at home).
The rules when breaks (or no-location replenish activities) are dispatched first on a route before any stop with a location are a little different.
If only breaks are dispatched, but no with-location stops are dispatched, the start work time will be calculated as the maximum of the current time and the break completion time.
If breaks have been dispatched followed by one or more dispatched stops with locations:
- If the optimiser calculates driving should have been done towards the with-location stops before the break(s) are started, the start driving time is used as the start work time.
- If no driving time could have been done before the break(s) started, the break completion times are done.
There are some use cases where this set of rules might not give
what’s considered the correct start time, so you can also set the
recordedStartedWorkTime field in the vehicle which
explicitly tells ODL Live what time to use for (a) the work time limit
and (b) replenish logic. See following vehicle definition:
{
"definition" : {
"workTimeHours" : [ ... ],
"start" : {...},
"end" : {...}
},
"dispatches" : [ ... ],
"stopArrivedEvents" : [ ... ],
"recordedStartedWorkTime" : "2025-01-01T00:07:45",
"_id" : "vehicle1"
}
10.20.2 Work time excluding preloaded breaks, delays and waits (totalServePlusTravelHours)
You can also see a limit on the amount of work time excluding preloaded breaks (i.e. preloaded stops without locations), added delays and waiting time. This is set using the field vehicle.definition.totalServePlusTravelHours. totalServePlusTravelHours is realtime compatible, it will take account of dispatched stops. The following vehicle has a hard limit of 6 hours on its ‘service + travel’ time:
{
"definition": {
"totalServePlusTravelHours": [
{
"inclusiveLowerLimit": 6.0,
"prohibited": true
}
],
"start": {
"type": "START_AT_DEPOT",
"coordinate": {
"latitude": 51.5073,
"longitude": -0.1657
},
"openTime": "2022-01-01T00:00:00",
"_id": "V1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {
"latitude": 51.5073,
"longitude": -0.1657
},
"closeTime": "2022-02-01T00:00:00",
"_id": "V1End"
}
},
"_id": "V1"
}
Soft, hard or combination limits can be used as totalServePlusTravelHours is defined using a penalty function.
The unplanned analysis code to indicate a job hasn’t loaded because of the totalServePlusTravelHours rule is servePlusTravelLimit.
10.21 Travel time multiplier (per-vehicle)
You can specify a multiplier for travel time for a vehicle, that is applied to all travel the vehicle does regardless of how the travel is calculated (i.e. the field works for both road network travel times and Euclidean travel times).
In the following example vehicle object we set travelTimeMultiplier = 2, as a result all travel times for the vehicle will be multiplied by 2 and the vehicle will be twice as slow:
{
"definition" : {
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : {
"latitude" : 51.5073, "longitude" : -0.1657
},
"openTime" : "2001-01-01T01:01",
"_id" : "Start1"
},
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : {
"latitude" : 51.5073, "longitude" : -0.1657
},
"closeTime" : "2001-01-01T02:01",
"_id" : "End1"
},
"travelTimeMultiplier" : 2.0
},
"_id" : "vehicle1"
}
In contrast, if we set travelTimeMultiplier = 0.5 instead, the vehicle will be twice as fast.
10.22 Waiting time costs and limits
If a vehicle arrives at a stop at 9:00, but the openTime field on the stop is set to 10:00, the vehicle will have to wait at the stop for an hour until the stop can be served. Unless the field vehicle.definition.costPerWaitingHour is set to zero, this waiting time will create a cost, and the optimiser will assign stops to minimise this cost (unless other costs dominate, e.g. travel, cost of using a vehicle). The following JSON shows a vehicle with costPerWaitingHour set to 0.15:
{
"definition": {
"costPerTravelHour" : 1.0,
"costPerWaitingHour" : 0.15,
"costFixed" : 1000.0,
"start": {
"type": "START_AT_DEPOT",
"coordinate": {...},
"openTime": "2021-01-01T00:00",
"_id": "veh1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {...},
"closeTime": "2021-02-01T00:00",
"_id": "veh1End"
}
},
"_id": "veh1"
}
If the waiting costs in your model are significant, you may need to turn on during optimisation delays so the optimiser can add delays to the route (e.g. delay vehicle start) which minimise waiting costs.
Waiting cost can either be defined using costPerWaitingHour or using a penalty function set on the field vehicle.definition.costPerWaitingHourFunc. If costPerWaitingHourFunc is set, the value of costPerWaitingHour is ignored. costPerWaitingHourFunc can also be used to define a hard limit on waiting time. You should only use a hard waiting time limit if you have during optimisation delays turned on.
In the following vehicle JSON, we use costPerWaitingHourFunc to tell the vehicle it cannot wait at any stop for more than 5 minutes (0.1167 hours):
{
"definition": {
"costPerWaitingHourFunc": [
{
"inclusiveLowerLimit": 0.11666666666666667,
"prohibited": true
}
],
"start": {
"type": "START_AT_DEPOT",
"coordinate": {...},
"openTime": "2021-01-01T00:00",
"_id": "veh1Start"
},
"end": {
"type": "RETURN_TO_DEPOT",
"coordinate": {...},
"closeTime": "2021-02-01T00:00",
"_id": "veh1End"
}
},
"_id": "veh1"
}
Imagine a problem with just one vehicle and one single-stop job, where vehicle.definition.start.openTime is 8am, the stop openTime is 10am, and the stop is only 1 hour drive from the vehicle start location. Without during-optimisation delays, the vehicle will start at 8am, arrive at the stop at 9am and then have to wait for 1 hour, which will break the hard limit on waiting time. As a result the job will be not assigned. Using during-optimisation delays, when ODL Live is assigning that job, it will also calculate the delayed start time for the vehicle. It will then delay the vehicle start by an one hour, and the job will be assigned.
If you are using a hard wait time limit, be sure to set the limit to be greater than the delay search tolerance, e.g. at least twice the delay search tolerance, or the optimiser may fail to optimise the problem properly.
10.23 Workload balancing penalty
Workload balancing enables work to be spread more evenly across your planned routes, which helps to (a) keep your staff content and (b) retain capacity across your fleet for future jobs. In ODL Live, balancing works using a penalty cost which penalises ‘fuller’ vehicles. This makes it more cost-efficient (in optimiser cost, not real-life monetary cost) to spread work out over multiple vehicles instead of having one or two full vehicles and the rest empty. The fullness penalty therefore indirectly forces the optimiser to balance work.
We define fullness based on how much stop time and driving time a vehicle has left in its plan, as a proportion of its remaining working time. So for example, a full vehicle is one that’s going to be driving and serving stops for all of its remaining work time. This definition has the advantage of still working even if you have staggered vehicle starting times or vehicles with different length shifts.
The penalty cost is designed to be around the same size as a vehicle’s costs-per-hour (service time, travel time and waiting time), so we can suitably model the trade-off between minimising travel time and balancing work. It is best to model this as a trade-off, because there are situations where if you totally prioritised balance over routing, you would get very inefficient routes. The penalty cost is therefore defined as a dimensionless weight which is multiplied by the mean of the vehicle’s various costs-per-hour; it will therefore only function correctly if the vehicle’s cost per km is set to be much less important than the various costs-per-hour and may not function if the routing cost is dominated by lateness costs.
It is assumed that balance should always be less important than minimising lateness - i.e. you would never choose to deliver late to a customer purely to ensure the routes are more balanced (and there will be times when these are conflicting goals). Lateness cost should therefore always be set to dominate balance cost. We note that when planned stops are going to be served late, the optimiser already has some ability to spread this lateness between stops (as opposed to spreading between routes), so in ‘peak day’ situations where many stops are running late, some spreading would already be active.
As the penalty is more effective the ‘fuller’ a vehicle is, it won’t spread the work out as much if you only have a couple of vehicles in your plan and all these vehicles are half or quarter-full - i.e. if the only vehicles being used still have lots of time available to do more work. This is unlikely to be a problem in real life though if your main aim in balancing is to ensure vehicles have spare capacity. The fullness penalty won’t prevent stops being planned, as the ‘cost’ for not assigning stops is effectively infinite in the model.
Workload balancing is set in the model.configuration.problem.fullnessPenalty object and is turned off by default. The following JSON shows a model with this set:
{
"data": {
...
},
"configuration": {
"distances": {
...
},
"problem": {
"fullnessPenalty": {
"weight": 10,
"power": 2
}
},
"timeOverride": {
...
}
}
}
You are advised to try weight = 10 as a starting point and then reduce it (for example to 1) if the travel time is impacted too greatly, or increase it if the balance doesn’t seem enough. You should keep power = 2.