5 Realtime planning walkthrough
The idea behind realtime modelling is simple - you add jobs when they become known and keep the state of the vehicles updated, ODL Live then automatically replans as needed. To process the complete lifecycle of a stop with realtime modelling, you must PUT these three different events, each when they occur and in the order shown below:
We demonstrate this lifecycle in the following walkthrough. You can also PUT GPS trace events, but these are not mandatory.
5.1 Create starting model
Warning, the example JSON data in the following section(s) should contain dates set in the future. ODL Live uses the current date and time in its calculations and will not schedule data in the past. If you are reading this some time after this document was created and the example data is not set in the future, you should change the dates. When you’re testing, you can also override the current time in the model configuration if you want to schedule jobs in past.
The following JSON defines a simple model with two vehicles and two service-type jobs:
{
"data" : {
"jobs" : [ {
"stops" : [ {
"type" : "SERVICE",
"coordinate" : { "latitude" : 51.5074, "longitude" : -0.1001 },
"openTime" : "2029-01-01T09:00",
"lateTime" : "2029-01-01T13:00",
"closeTime" : "2029-01-02T13:00",
"durationMillis" : 3600000,
"_id" : "TateModern1"
} ],
"_id" : "TateModern1" }, {
"stops" : [ {
"type" : "SERVICE",
"coordinate" : { "latitude" : 51.5229, "longitude" : -0.155 },
"openTime" : "2029-01-01T09:00",
"lateTime" : "2029-01-01T13:00",
"closeTime" : "2029-01-02T13:00",
"durationMillis" : 3600000,
"_id" : "MadameTussauds1"
} ],
"_id" : "MadameTussauds1"
} ],
"vehicles" : [ {
"definition" : {
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : { "latitude" : 51.5416, "longitude" : -0.1462 },
"openTime" : "2029-01-01T08:00" },
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : { "latitude" : 51.5416, "longitude" : -0.1462 },
"lateTime" : "2029-01-01T18:00",
"closeTime" : "2029-01-02T18:00" },
"costPerTravelHour" : 1.0,
"costPerWaitingHour" : 0.5,
"costPerServicingHour" : 1.0,
"costPerKm" : 1.0E-6,
"costFixed" : 1000.0,
"costPerStop" : 0.0 },
"_id" : "Vehicle1" }, {
"definition" : {
"start" : {
"type" : "START_AT_DEPOT",
"coordinate" : { "latitude" : 51.5416, "longitude" : -0.1462 },
"openTime" : "2029-01-01T08:00" },
"end" : {
"type" : "RETURN_TO_DEPOT",
"coordinate" : { "latitude" : 51.5416, "longitude" : -0.1462 },
"lateTime" : "2029-01-01T18:00",
"closeTime" : "2029-01-02T18:00" },
"costPerTravelHour" : 1.0,
"costPerWaitingHour" : 0.5,
"costPerServicingHour" : 1.0,
"costPerKm" : 1.0E-6,
"costFixed" : 100.0,
"costPerStop" : 0.0 },
"_id" : "Vehicle2"
} ] }
}
Now copy and paste the model JSON and use Postman to PUT this model to the following URL, as per the previous section(s).
my-base-URL/models/RealtimeModel1
So, in Postman you should:
- Change the URL.
- Set Basic Authentication username and password.
- Replace my-base-URL with your ODL Live URL.
- Change the HTTP method to PUT.
- Set the request body to the JSON above ensuring its type is JSON (application/JSON).
- Press Send.
You should receive the HTTP response 200 OK. If you GET the model using the same URL (but using the GET method) you will see this data again, with some additional properties assigned by ODL Live.
5.2 Dispatching
In the model, we set the fixed cost for Vehicle1 to be much higher than Vehicle2’s, so the optimiser should have assigned the jobs to Vehicle2. GET the plan, remembering you may need to repoll for a second or two for it to be updated:
GET my-base-URL/models/RealtimeModel1/optimiserstate/plan
In the ODLOptimiserPlan’s JSON returned by GET …/plan, the ODLVehiclePlan object with property vehicleId equal to Vehicle2 should have two ODLPlannedStop objects in its array plannedStops. Look at the first ODLPlannedStop in Vehicle2’s ODLVehiclePlan. You should see JSON similar to the following:
{
"stopId" : "TateModern1",
"timeEstimates" : {
"arrival" : "2029-01-01T08:03:42.048944800",
"start" : "2029-01-01T09:00",
"complete" : "2029-01-01T10:00"
},
"earliestDispatch" : "2029-01-01T08:26:17.951"
}
The property earliestDispatch is a recommended earliest time to dispatch the stop (send to the driver) based on the dispatch buffer in the configuration object. The dispatch buffer specifies the minimum amount of time (e.g. 10 minutes) between a driver receiving a stop and the driver having to depart for the stop. Having a larger dispatch buffer minimises the chances of communications breakdowns leading the driver to run out of work or start an old job, however it will also limit your opportunities to reassign the driver at late notice when more urgent jobs come in.
The optimiser uses dispatched stops to estimate the short-term future state of the vehicle - i.e. when and where it is next free for planning by the optimiser. It assumes dispatched stops are done in the specified order (i.e. it does not optimise them - they are a read-only input as far as the optimiser is concerned). In a standard ODL Live configuration, based on the planned stops you would create dispatch objects, which you then PUT to the vehicle’s objects history. This allows you to override ODL Live’s suggested dispatches as and when needed. Dispatches are therefore used to lock-down the short-term future of a driver, setting one or more stops as their next stops.
Now create a dispatch object to send to ODL Live, so within your own systems you can dispatch the stop to the driver knowing the optimiser will take account of it. Normally you would dispatch based on the optimiser plan but for this example, we will override the plan and dispatch to the more expensive Vehicle1 instead. Change Postman to a PUT request and enter the following JSON in the body:
{
"creationTime" : "2029-01-01T08:26:17.951",
"stopId" : "TateModern1"
}
This is an ODLDispatch. Dispatch records sit within the ODLVehicle record. We could either (a) PUT an updated version of ODLVehicle containing this dispatch or (b) PUT the dispatch directly on the ODLVehicle already held in ODL Live. We will do (b), so you should now PUT the JSON to the following URL:
my-base-URL/models/RealtimeModel1/vehicles/Vehicle1/dispatches/MyFirstDispatch
This adds the dispatch to the vehicle using a dispatch id of “MyFirstDispatch”. The dispatch becomes an element of the dispatches array in the vehicle and the vehicle itself is an element of the vehicles array in the model. This relationship is mirrored in the structure of the URL.
Now if you GET the vehicle:
GET my-base-URL/models/RealtimeModel1/vehicles/Vehicle1
you should see the new dispatch record in its list of dispatches. When we created the dispatch we also set its creationTime property (and pretended the time is actually 08:26:17.951 on 2029-01-01). You should always set creation time otherwise the system may incorrectly estimate the driver’s state. For example, if (a) the vehicle’s open time is 8 am, (b) the vehicle hasn’t been dispatched to any stops yet today and (c) the current time is 10 am, then if you create a new dispatch but don’t fill in the time, the system will wrongly assume the driver departed for the dispatch at 8 am.
GET the plan, remembering you may need to repoll for a second or two for it to be updated:
GET my-base-URL/models/RealtimeModel1/optimiserstate/plan
You should see the following data within the plan JSON (as well as some other data):
{
"vehiclePlans" : [ {
"vehicleId" : "Vehicle1",
"plannedStops" : [ {
"stopId" : "MadameTussauds1",
"timeEstimates" : {
"arrival" : "2029-01-01T10:03:06.633281344",
"start" : "2029-01-01T10:03:06.633281344",
"complete" : "2029-01-01T11:03:06.633281344"
},
"earliestDispatch" : "2029-01-01T09:50:00.000281344"
} ],
"planStartPoint" : {
"time" : "2029-01-01T10:00",
"coordinate" : {
"latitude" : 51.5074,
"longitude" : -0.1001
},
"type" : "ESTIMATED_BY_OPTIMISER"
},
"dispatchedIncompleteStops" : [ {
"stopId" : "TateModern1",
"timeEstimates" : {
"arrival" : "2029-01-01T08:29:59.999",
"start" : "2029-01-01T09:00",
"complete" : "2029-01-01T10:00"
}
} ]
} ]
}
Now Vehicle1 should be the only planned vehicle as the remaining non-dispatched stop has been moved from Vehicle2 to Vehicle1. It is now cheaper to use Vehicle1 for all stops because Vehicle1’s fixed cost cannot be avoided but Vehicle2’s can. You should also see the dispatched stop in Vehicle1’s dispatchedIncompleteStops list together with estimates for its times.
5.3 GPS
You can send GPS trace events to ODL Live but they are not mandatory - the real-time model will still function without them but if provided they will improve the accuracy of the system’s planning. Note that it is undesirable to send a GPS trace event when the driver is at stop, if the stop arrival event has not yet been sent, as the system will assume the driver has not started the stop yet (and is therefore running behind schedule). ODL Live only uses the last GPS trace for a vehicle. You should therefore do a PUT all to replace all GPS traces on a vehicle, by passing an array containing only the last GPS trace. This way you delete all existing GPS traces for the vehicle and add the new one, using a single API call. If instead you included all GPS traces in the vehicle object (particularly if the traces are generated often), this will slow down certain aspects of ODL Live’s operation and likely incur additional data transfer charges from the cloud hosting provider.
The object type for a GPS trace event is ODLTracePoint. We declare an array of ODLTracePoint holding a single trace as:
[ {
"time" : "2029-01-01T09:11",
"coordinate" : {
"latitude" : 51.5245,
"longitude" : -0.12315
},
"type" : "GPS_DEVICE"
} ]
Which we then PUT to the following URL:
GET my-base-URL/models/RealtimeModel1/vehicles/Vehicle1/gpstraces/
Remember to include the forward slash / on the end of the URL as you’re doing PUT to an array of objects. If you GET the ODLVehicle you should see this GPS trace is now present in its vehicleTraceEvents field. If you GET the ODLOptimiserPlan you should also see the timings have changed for the stops but the other stop should still be assigned to Vehicle1.
5.3.1 GPS sent before vehicle openTime
If you happen to send a GPS to the vehicle object with an event time
which is earlier than the vehicle’s defined starting time
(vehicle.definition.start.openTime), the location of the
GPS will be used as the vehicle’s starting location but the start time
will still be vehicle.definition.start.openTime.
5.4 Stop arrival events
ODLStopArrivalEvent events are mandatory and must be sent to ODL Live when performing real-time modelling. If you have very short stop durations - a minute or two - you can probably get away with just sending the stop arrival event at the same time you send a stop completion event. If you have longer durations, you must send the event or ODL Live will think your driver is running behind schedule. The following JSON is a ODLStopArrivalEvent for our dispatched stop:
{
"time" : "2029-01-01T12:48",
"stopId" : "TateModern1"
}
We choose to just send this arrival event rather than the entire updated vehicle. PUT the JSON to the following URL:
GET my-base-URL/models/RealtimeModel1/vehicles/Vehicle1/stoparrivals/ArriveTateModern1
Using the URL we gave the arrival event the id “ArriveTateModern1”. GET the plan, remembering you may need to repoll for a second or two for it to be updated:
GET my-base-URL/models/RealtimeModel1/optimiserstate/plan
In our arrival event, the arrival time was late compared to the previous plan - we only arrive a little before the lateTime of the stop. As a result, Vehicle1 can no longer serve the other stop without it being significantly late. As the cost associated with lateness is much higher than the fixed cost of a vehicle, the other stop should now be assigned to Vehicle2’s plan instead, as per the following JSON:
{
"vehiclePlans" : [ {
"vehicleId" : "Vehicle1",
"plannedStops" : [ ],
"dispatchedIncompleteStops" : [ {
"stopId" : "TateModern1",
"timeEstimates" : {
"arrival" : "2029-01-01T12:48",
"start" : "2029-01-01T12:48",
"complete" : "2029-01-01T13:48"
}
} ]
}, {
"vehicleId" : "Vehicle2",
"plannedStops" : [ {
"stopId" : "MadameTussauds1",
"timeEstimates" : {
"arrival" : "2029-01-01T08:01:36.931410936",
"start" : "2029-01-01T08:59:59.999999992",
"complete" : "2029-01-01T09:59:59.999999992"
},
"earliestDispatch" : "2029-01-01T08:28:23.069"
} ],
"dispatchedIncompleteStops" : [ ]
} ]
}
5.5 Stop completed events
ODLStopCompletedEvent events are mandatory and must be sent to ODL Live when performing real-time modelling. The following JSON is a ODLStopCompletedEvent for our arrived stop:
{
"time" : "2029-01-01T13:32",
"stopId" : "TateModern1"
}
PUT the JSON to the following URL:
my-base-URL/models/RealtimeModel1/vehicles/Vehicle1/stopcompleted/CompleteTateModern1
Using the URL we gave the completed event the id “CompleteTateModern1”. GET the plan, remembering you may need to repoll for a second or two for it to be updated:
GET my-base-URL/models/RealtimeModel1/optimiserstate/plan
Vehicle1 should no longer be listed as it has no planned stops or stops in its dispatchedIncompleteStops list
5.6 Data management and realtime modelling
When you’re doing realtime modelling, the vehicle record stores references to jobs - e.g. dispatched stop events and other events. Deleting a job or vehicle can therefore have undesirable effects unless done correctly. If you want to delete a job or vehicle, then PUT a new version of the whole model where you have ensured there are no hanging references. This way the vehicle and job data is updated atomically. Doing this non-atomically - for example DELETE job then PUT vehicle (or even the other way around) can cause issues in the system.
Important points are:
No hanging stop references Never have a stop reference within the vehicle (for example a dispatch) without the corresponding stop existing as part of a job. The API should prevent you doing this.
Deleting an active job. If you delete a job but the vehicle has already arrived at the stop’s location, replace the ArrivedAtStop event with a dummy GPS trace event to ensure both (a) referential integrity and (b) the vehicle’s last location is known by the system.
Disable don’t delete active vehicles If you delete a vehicle, the system will no longer know about its dispatches and completed stops events. As a result, already completed stops will be reassigned to different vehicles. If a vehicle breaks down, you should instead disable it by setting its close time to any point in the past (for example setting the open and close times to 12:00-12:01 year 2000). No new jobs will be planned for this vehicle but its dispatched and completed jobs will not be reallocated to a different vehicle; it is shutdown safely with its history intact.
Alternatively, you could PUT the whole model at once with the jobs corresponding the vehicle’s completed stops deleted as well.
Keep vehicle history and completed jobs until you delete the model. The simplest way to manage the data is therefore to keep the vehicle history (dispatches, stop arrival and completed) and completed jobs in the model, until you delete the model. Retaining history is also required so collections / pickups already on-board a vehicle are subtracted from its available capacity. You should only retain the last GPS trace however.
Create and delete models for different planning periods. Storing thousands or more complete jobs within a model will have an impact on the optimiser as it still has to fetch and hold this data. You should not therefore allow the data in a model to grow indefinitely; instead create a model for a specific planning period (day, week or month) and delete it when the period is finished.