9 Stop level features

The following subsections detail various advanced features you can enable for modelling stops. 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.

9.1 Contiguity constraint

Contiguity is a property of a stop within a job. If you set job[].stop[].contiguous = true on a stop, it will be part of the contiguity constraint. The contiguity constraint forces the optimiser to keep routes in separate non-overlapping areas.

The contiguity constraint has a similar effect to the centering penalty or a max separation penalty, in that it keeps routes in distinct areas. The difference is that the centering and max separation penalties are not guaranteed to keep routes in separate areas if other costs have a greater effect, or jobs can only be served by having overlapping areas. The contiguity constraint is a hard constraint, it will always keep routes in separate areas even if this means jobs are not assigned. It is actually recommended to use the contiguity constraint together with the centering penalty or a max separation penalty, as although the contiguity constraint stops routes overlapping, the routes may not be compact without centering or max separation being active as well.

Roughly speaking, the contiguity constraint works as follows:

  • Every stop (within a job) which has its contiguous field set to true participates in the constraint. Let’s call these contiguous stops.

  • If a route contains contiguous stops, then all contiguous stops on the route must be connected.

    • Two contiguous stops A and B on a route are connected if (roughly speaking) there is a clear line-of-sight between them, which isn’t blocked by any other contiguous stop in the problem, but not on the route.

    • Three contiguous stops A, B and C on a route are considered connected if there’s a clear line-of-sight between A and B, and another between B and C, even if there’s no direct line-of-sight between A and C. So contiguous stops can be connected together through intermediary contiguous stops on the same route.

    • As all contiguous stops on the route must be connected, these rules ensure that the contiguous stops on a route visually form a single connected ‘island’, i.e. there’s no contiguous stops on the route which don’t look ‘connected’ to the other contiguous stops on the route.

Within a single job, only a single stop should be marked as contiguous (or ODL Live will throw an error). Examples of this could be pickup-delivery jobs where the pickup corresponds to the depot and you only care about the delivery locations for a route being visually connected. In this case you would set contiguous=true on the delivery stops but leave the field out or set it to false for the pickup stops.

Also note:

  • It is not advised to use contiguous stops with alternative jobs, particularly alternative jobs with different contiguous locations. The contiguity constraint is not setup to work well with alternative jobs. It should also be avoided for multi-day problems where the same areas can be visited on each day.

  • When you have multiple contiguous stops at the same location (same lat-long) in a problem, the logic is slightly more complex. The contiguous stop with the lowest stop._id is treated normally and can act as an intermediary stop in the connection logic, but the other stops at the location can’t be intermediaries. This means multiple routes can have stops at this location, but only one route (containing the lowest id stop) can actually cross the location. This preserves the visual contiguity.

The supporting-data-for-docs directory contains an example model using contiguity at:

supporting-data-for-docs\example-models\contiguous-constraint\contiguous-constraint-example.json

Here’s a screenshot from this example:


This example model doesn’t use road network data and has timeOverride set, so it will run on your ODL Live instance without any modifications to the JSON. In the example, there’s not enough capacity on the vehicles to fit in all stops, and so the stops shown in purple don’t get loaded onto routes. Each stop has a different volume. Without the contiguity constraint, the 3 routes would criss-cross Singapore (messily overlapping each other), to find the combination of loaded stops on each route which maximises the total number of assigned stops. With the contiguity constraint on, the optimiser (1) prioritises keeping routes in their own area, then (2) maximises the loaded stops, and then (3) minimises the cost, which is based on both route compactness and travel.

9.2 Depot revisit rules (no-return with deliveries still on-board)

NOTE depot revisit rules are not currently evaluated from the last dispatched stop to the first planned (i.e. undispatched) stop.

Certain routing problems involve picking up goods from a depot, delivering them, and then going back to the depot to pickup some more goods to deliver. These are modelled using pickup-deliver jobs (i.e. jobs with two stops where the first stop is the depot). The optimiser will minimise the total solution cost, and this can be configured to minimise the number of visits to a depot by setting a high parking cost for the depot or using from stop rules or inter-stop costs. However, given a problem that requires several returns to the depot, you may sometimes see the vehicle (1) picking up items from the depot, (2) delivering some but not all of the items, (3) returning to the depot to pickup more items, and then (4) delivering all remaining items. In other words, the vehicle is returning to the depot to pickup more items when items for delivery still on-board.

The root cause of this is that the total travel cost for picking up items on the first depot visit or the second are the same, even for items not delivered until after the second depot visit. In some cases this can be solved by setting a penalty based on the amount of time an item is on-board (see on-board time penalty). However this should not be done for problems where the openTime of the delivery stop is later than the openTime of the pickup stop, as this can have adverse effects within the cost model (it can actually make it beneficial in the cost model to waste time before the openTime of the delivery stop, giving odd routes).

For cases where you have delivery openTime set later than pickup openTime, we recommend using the depot rules. The depot rules turn on special logic to deal with these cases. We support two types of depot rules: depotId4MultiTripRules, depotId2PreventRevisitWithOnBoards. Sequence based state user functions (SEQSTATE) could also be used to implement a depot revisit type rule if needs be (SEQSTATE functions are also evaluated correctly when stops have been dispatched).

9.2.1 Empty at all depots rule

The field depotId4MultiTripRules defines the more restrictive rule. This rule does the following:

  1. Prevents returning to a depot with delivery items still on-board.
  2. Does not allow picking up at one depot, then picking up at another depot, then delivering.
  3. For consecutive stops at the same depot location, delivery type stops are done first, then pickup type stops.
  4. The optimiser makes no attempt to optimise the ordering of consecutive stops at the same depot (apart from putting delivery stops at the depot first). This speeds up the optimiser but it should only be used if your depot stops are identical (e.g. don’t have different openTime values for example).

This rule is activated by setting the field stop.depotId4MultiTripRules to an identifier for the depot. All stops at this depot should have stop.depotId4MultiTripRules set to the same value. We show this for a single job in the following JSON:

{
  "quantities" : [ 1 ],
  "stops" : [ {
    "type" : "SHIPMENT_PICKUP",
    "durationMillis" : 600000,
    "depotId4MultiTripRules" : "MyDepot3",
    "coordinate" : {
      "latitude" : 51.5073,"longitude" : -0.1657
    },
    "_id" : "Pickup1"
  }, {
    "type" : "SHIPMENT_DELIVERY",
    "durationMillis" : 600000,
    "coordinate" : {
      "latitude" : 51.56026,"longitude" : -0.16067
    },
    "_id" : "Deliver1"
  } ],
  "_id" : "Job1"
}

9.2.2 Empty at same depot rule

The field depotId2PreventRevisitWithOnBoards defines the less restrictive rule. This rule does the following:

  1. Prevents returning to a depot with delivery items still on-board.
  2. Works for multi-depots, so it does allow picking up at one depot, then picking up at another depot, then delivering.
  3. Will order consecutive stops at the same depot by delivery first, then earliest openTime first, but does not attempt to otherwise optimise their order.

This rule is activated by setting the field stop.depotId2PreventRevisitWithOnBoards to an identifier for the depot. All stops at the same depot should have stop.depotId2PreventRevisitWithOnBoards set to the same value. Stops at a different depot should have a different value of depotId2PreventRevisitWithOnBoards for that depot. We show this for a single job in the following JSON:

{
  "quantities" : [ 1 ],
  "stops" : [ {
    "type" : "SHIPMENT_PICKUP",
    "durationMillis" : 600000,
    "depotId2PreventRevisitWithOnBoards" : "MyDepot3",
    "coordinate" : {
      "latitude" : 51.5073,"longitude" : -0.1657
    },
    "_id" : "Pickup1"
  }, {
    "type" : "SHIPMENT_DELIVERY",
    "durationMillis" : 600000,
    "coordinate" : {
      "latitude" : 51.56026,"longitude" : -0.16067
    },
    "_id" : "Deliver1"
  } ],
  "_id" : "Job1"
}

9.3 Duration types

The stop object sits within the job object, and the stop object has a field durationMillis which is the duration in milliseconds to serve the stop. We show this in the following JSON for a delivery job from a depot:

{
  "stops": [
    {
      "type": "DELIVER",
      "durationMillis": 60000,
      "coordinate": {
        "latitude": 51.5073,"longitude": -0.1657
      },
      "_id": "stop1"
    }
  ],
  "_id": "job1"
}

Once a vehicle arrives at a stop, it will wait until the stop’s openTime (if openTime is set) and then serve the stop, taking durationMillis.

If you have multiple stops (e.g. multiple deliveries) at the same location, you can use parking time to model that the stop takes a fixed amount of time to visit the location plus an extra time per stop.

If you want to model that certain vehicles (i.e. drivers / technicians etc) can serve a stop quicker than others, - for example an experienced technician can serve a stop 80% faster than an inexperienced one - see the section on overriding stop properties.

9.3.1 Duration added after the vehicle leaves a location

This following directory in supporting data contains a simple example model demonstrating the field job.stop[].durationSecondsAddedAfterLeavingLocation:

supporting-data-for-docs\example-models\duration-seconds-added-after-leaving-location

Normally stop duration (the time to service a stop) is set using the durationMillis field. Let’s assume we have three stops at the same location - stops A, B and C. Each stop takes 15 minutes to serve and there’s an identical time window on each stop of 10:00-10:20.

Time windows are evaluated based on the arrival time at a location, if the vehicle arrives before the time window’s open time, it will wait until the open time to start serving the stop. If the vehicle arrives after the time window’s close time, this is treated as a hard constraint violation (unless you’re using soft time windows).

Under our assumptions, if the vehicle arrives at or before 10:00, the stops will be served at these times:

  • Stop A at 10:00-10:15
  • Stop B at 10:15-10:30
  • Stop C at 10:30-10:45.

Stop C starts at 10:30, which breaks its 10:00-10:20 time window. Stop C will therefore be omitted from the plan because of the hard constraint violation.

The durationSecondsAddedAfterLeavingLocation field allows the duration to be added after leaving the location instead. Leaving the location is defined by having more than 1 metre between the previous and current stop latitude-longitudes. If instead of setting the 15 minutes duration to the durationMillis field, we set it to the durationSecondsAddedAfterLeavingLocation field, we’d have timings like these:

  • Stop A at 10:00-10:00
  • Stop B at 10:00-10:00
  • Stop C at 10:00-10:45.

So all the duration is added to the time we leave the location, which is the completion time of C. No time windows are broken.

The example model has multiple stops at the same location (e.g. stops Loc1S0 and Loc1S1 are at the same location, stops Loc3S0, Loc3S1, Loc3S2 and Loc3S3 are at the same location). We set both the durationMillis and durationSecondsAddedAfterLeavingLocation fields for these stops so some time is added whilst the stops are being served and some time is added on leaving the location:

{
  "stops": [
    {
      "type": "SERVICE",
      "durationMillis": 240000,
      "coordinate": {
        "latitude": 51.47755322571206,
        "longitude": -0.3969755678622481
      },
      "durationSecondsAddedAfterLeavingLocation": 300.0,
      "_id": "Loc3S2"
    }
  ],
  "_id": "Loc3S2"
}

Here’s an example schedule for the stops at location 3:

- Loc3S1 arrive=02:38:03 complete=02:41:03 durationMillis=3m durationSecondsAddedAfterLeavingLocation=1m
- Loc3S0 arrive=02:41:03 complete=02:42:03 durationMillis=1m durationSecondsAddedAfterLeavingLocation=4m
- Loc3S2 arrive=02:42:03 complete=02:46:03 durationMillis=4m durationSecondsAddedAfterLeavingLocation=5m
- Loc3S3 arrive=02:46:03 complete=02:59:03 durationMillis=1m durationSecondsAddedAfterLeavingLocation=2m

12 minutes is accumulated from the durationSecondsAddedAfterLeavingLocation fields as the stops are served, and this is added to the end time at Loc3S3.

If you override stop duration for specific vehicles, the overriding applies to both durationMillis and durationSecondsAddedAfterLeavingLocation if you’re using duration functions, so (for example) you could have a duration function that makes both 50% faster for a specific vehicle.

9.4 Enforcing lateness ordering

By default, ODL Live minimises the total of square lateness across all stops and breaks. This spreads lateness out across stops, as two stops which are late by 2 time units will have penalty = 22 + 22 = 8 whereas one stop which is late by 4 time units will have penalty 42 = 16. However when we consider the effects of travel time as well, there are circumstances where a stop that is more late can be served before a stop that is less late, if the total lateness penalty is still smaller this way.

Let’s imagine two stops A and B and ordering them either way:

  • A (lateness = 10), B (lateness = 5) gives penalty = 102 + 52 = 100 + 25 = 125

  • B (lateness = 1), A (lateness = 11) gives penalty = 12 + 112 = 1 + 121 = 122

So A is the most late stop, because if we serve A first it’s late by 10 time units, whereas if we serve B first, B is only late by 1 time unit. However B becomes 5 later if we serve it last whereas A becomes only 1 later. As a result the total of square lateness is actually minimised if we serve B first, even though B is not the most late stop and we are making A - the most late stop - even later. In other words we can serve two stops (‘most-late’ stop and ‘least-late’ stop) out-of-lateness-order, if the increase in lateness from serving ‘least-late’ second is a lot more than the increase in lateness from serving ‘most-late’ second.

This is an appropriate scheme for minimising lateness across a fleet of vehicles, however if you want to avoid consecutive stops in a single route which are out-of-lateness-order, a rule is available. We have set this in the following model JSON, setting the outOfOrderTWsAllowed in the configuration, problem object to false:

{
  "data" : {
    "jobs" : [ ],
    "vehicles" : [ ]
  },
  "configuration" : {
    "problem" : {
      "outOfOrderTWsAllowed" : false
    },
  },
}

This value is true by default, which keeps the rule inactive (i.e. if you do not specify the field outOfOrderTWsAllowed or set it to true the rule will not be active). If you set it to false ODL Live will enforce the rule that for two consecutive stops A and B, the open time + stop duration of stop A cannot be equal to or later than stop B’s late time. Stop A cannot be served until its open time starts, and as stop B is directly after stop A, stop B cannot be served until stop A is complete. As a result, we are preventing an ordering of stops that cannot be achieved without the second stop being late, even if there is zero travel time between the first and second stop (e.g. if they’re at the same physical location). This prevents a circumstance where a ‘most-late’ stop will be served after a ‘least-late’ stop.

This rule also applies in realtime modelling. If the ‘least-late’ stop has been dispatched, the ‘most-late’ stop will be not loaded after it; in a single vehicle problem this implies that the ‘most-late’ stop will not be loaded at all.

9.5 From stop rules

NOTE from stop rules are not currently evaluated from the last dispatched stop to the first planned (i.e. undispatched) stop.

From stop rules work in a similar manner to the inter-stop costs which are defined on the vehicle object. Stops can be set to be in stop groups. A from stop rule is defined on the stop object and adds a cost or time, depending on the group(s) the previous stop in the route was or wasn’t it.

In the following JSON we setup a pickup delivery job where the pickup stop is in a group called ‘Group1’ (defined in the array job.stops[0].stopGroups). It also has a from stop rule, defined in the array job.stops[0].fromStopCosts, which adds a cost of 10000 if the previous stop on the route (the ‘from’ stop) is not also in ‘Group1’:

{
  "stops" : [ {
    "type" : "SHIPMENT_PICKUP",
    "durationMillis" : 600000,
    "costFixed" : 0.0,
    "stopGroups" : [ "Group1" ],
    "coordinate" : {
      "latitude" : 51.81404415104371, "longitude" : -0.44252588718435115
    },
    "fromStopCosts" : [ {
      "fromStopGroupId" : "Group1",
      "notFrom" : true,
      "cost" : 10000.0,
      "timeSeconds" : 0.0,
      "skipNoLocationActs" : true
    } ],
    "_id" : "Job1Pickup"
  }, {
    "type" : "SHIPMENT_DELIVERY",
    "durationMillis" : 600000,
    "coordinate" : {
      "latitude" : 51.03731507255458,"longitude" : -0.9302501711270221
    },
    "_id" : "Job1Dropoff"
  } ],
  "_id" : "Job1"
}

Let’s imagine we have a routing model with many pickup delivery jobs, and each of these jobs has the same rules as shown above setup on their pickup stops, but for different stop groups (i.e. some jobs have their rules setup for ‘Group1’, others have them setup for ‘Group2’, others for ‘Group3’ etc). The optimiser reduces cost and will therefore try to do all Group1 pickups together (i.e. as consecutive stops on the same route) as this minimise the number of times the cost 10000 is added to the total solution cost. It will also follow the same logic for ‘Group2’, ‘Group3’ etc, putting pickup stops within the same group together in a row when it can. The only exception to this rule would be if putting the pickup stops within a group together breaks a hard rule or incurs another cost greater than 10000 if they’re put together (e.g. if it causes jobs to be served late).

Each element in the fromStopCosts array can have following fields:

  • fromStopGroupId. The ‘from’ stop group id to match to.

  • notFrom. If true, only match if the ‘from’ stop group id is not matched.

  • cost. Penalty cost.

  • timeSeconds. Additional time to add.

  • skipNoLocationActs. Ignore locationless activities (i.e. breaks) in the calculation, so only the stop groups of the previous stop with a location will be tested.

  • prohibited. If true, prevents the stop being placed in this position if matched. Use this with care as it can create problems which are hard for the engine to optimise - see section on preceeding enabler jobs for information on how to configure the engine to solve these sort of problems more effectively.

9.6 Lateness penalty functions

The ODL Live API uses penalty functions in various different places. Lateness penalty functions are a special type of penalty function only used within time windows, which have different field names and value types to the general penalty functions used in the API. However lateness penalty functions and general penalty functions use the exact same logic. You should therefore read the section on general penalty functions before reading this section, as this section assumes an understanding of general penalty functions.

We list the general penalty function fieldnames and their corresponding lateness penalty function fieldnames in the following table:

General p.f. name Lateness p.f. name General p.f. type Lateness p.f. type
inclusiveLowerLimit openTime Number Datetime
translate costZeroTime Number Datetime
join join Type of join Type of join
c0 cost Number Number
c1 costPerHour Number Number
c2 costPerHourSqd Number Number

So for example, the lateness penalties field openTime works the same way the general penalties field inclusiveLowerLimit and the join field works exactly the same for both general and lateness penalties.

ODL Live has a tool in the software developer’s dashboard that helps you configure lateness penalty functions. Go to ODL Live’s dashboard, click on the Model editor link, then click on the Tools link, and finally click on the View lateness functions link. Next click the View example button, and select the example named ‘1 window, 2 penalties (small and big)’.

The following JSON should appear on the left:

[
  {
    "openTime": "2018-01-02T01:33",
    "closeTime": "2018-01-03T01:33",
    "penalties": [
      {
        "openTime": "2018-01-02T01:33",
        "costPerHour": 5
      },
      {
        "openTime": "2018-01-02T02:18",
        "join": "EXACT",
        "costPerHour": 10000,
        "costPerHourSqd": 10000
      }
    ]
  }
]

This JSON is actually the content of the job.stop[].multiTWs object. It therefore contains an array of single time window objects, and inside each single time window object is an array of penalties.

If you use the zoom controls to zoom the graph on the appropriate region, you should see a graph similar to the following:

 

This graph is easy to interpret: Filled grey areas represent times when the optimiser will not allow the vehicle to start serving the stop. The lines in-between the filled grey areas show the optimiser cost that will be added if the vehicle arrives at that time.

We can see the following parts of this graph:

  1. The filled grey rectangle on the left represents the time before the time window opens, when the stop cannot be serviced.

  2. The pink-purple line from 1:33am to 2:18am corresponds to the first subfunction in the penalties array where we have a small costPerHour. If you zoom in on just this purple line, you can see that its y value is increasing at a rate of 5 per hour (corresponding to the costPerHour = 5 in the JSON).

  3. From 2:18am onwards we see a dark purple-blue line which curves upwards rapidly, corresponding to the 2nd subfunction which has costPerHour = 10000 and costPerHourSqd = 10000. The function curves upwards because we’re using the squared term (i.e. costPerHourSqd > 0). It increases rapidly because both costPerHour and costPerHourSqd are large. We’ve also set the join field on the 2nd subfunction so it starts at the end of the 1st subfunction (which is join type EXACT).

A subfunction (array element in the penalties array) is used if the arrival time at the stop occurs on or after the subfunction’s openTime and before the openTime of the next subfunction in the array. Each subfunction in a lateness penalty function is calculated using the following equation:

The fields which can be set in each subfunction are:

  • openTime. The time the subfunction applies from.

  • costZeroTime. The time to calculate the costPerHour and and costPerHourSqd terms relative to. If costZeroTime is not set in the JSON, we use the openTime of the subfunction instead (this logic is slightly different to general penalty functions, where if the translate field isn’t used the origin is assumed to be at x = 0).

  • cost. A fixed cost applied if the arrival time at the stop falls within the range of the subfunction.

  • costPerHour

  • costPerHourSqd

  • join. This has identical behaviour to the join field used in general penalty functions.

When using custom lateness penalties, ensure the following:

  • The subfunctions in the penalties array should be in order of openTime.

  • Penalties should always be increasing with time (use the join field to ensure this).

  • Don’t ever set costPerHour or costPerHourSqd to be negative.

  • If you want something to be delivered as soon as possible, the lateness penalty function should always increase with time, which means either costPerHour or costPerHourSqd should be greater than 0.

9.6.0.1 Lateness penalties and PLUS_CONST join type example

In the next example, we use the join type PLUS_CONST to setup a series of subfunctions where each subfunction starts at 5 cost above the end of the previous subfunction (see the section on general penalty functions for a detailed explanation about join types). When a PLUS_CONST join is used, the cost field in a subfunction specifies the start value of the subfunction relative to the end of the last function, so the subfunction starts at cost higher than the end of the last subfunction. The costPerHour still specifies the slope of each subfunction. The following screenshot shows the JSON and graph for this ‘stepped’ function example:

 

9.6.0.2 Lateness penalties across time windows for multiple days

The PLUS_CONST logic also applies across lateness penalty functions in different time window objects. In the following example we setup time windows that are open between 8:00 and 17:00 (in UTC) on 4 consecutive days. We use the PLUS_CONST logic to add 100 cost for each additional day the stop is not served, as well as having the cost increase by 2 for each hour during the allowed service time. See the following JSON:

[
  {
    "openTime": "2018-01-02T08:00",
    "closeTime": "2018-01-02T17:00",
    "penalties": [
      {
        "openTime": "2018-01-02T08:00",
        "costPerHour": 2
      }
    ]
  },
  {
    "openTime": "2018-01-03T08:00",
    "closeTime": "2018-01-03T17:00",
    "penalties": [
      {
        "openTime": "2018-01-03T08:00",
        "cost": 100,
        "costPerHour": 2,
        "join": "PLUS_CONST"
      }
    ]
  },
  {
    "openTime": "2018-01-04T08:00",
    "closeTime": "2018-01-04T17:00",
    "penalties": [
      {
        "openTime": "2018-01-04T08:00",
        "cost": 100,
        "costPerHour": 2,
        "join": "PLUS_CONST"
      }
    ]
  },
  {
    "openTime": "2018-01-05T08:00",
    "closeTime": "2018-01-05T17:00",
    "penalties": [
      {
        "openTime": "2018-01-05T08:00",
        "cost": 100,
        "costPerHour": 2,
        "join": "PLUS_CONST"
      }
    ]
  }
]

This JSON example can be selected in the viewer if you click the View example button (ensuring you’re on the View lateness functions tab and not the View penalty functions tab), and select the example named ‘Multi-day increasingly expensive’.

The following screenshot shows the graph of these time windows.

 

If we press the ‘Hide JSON’ button, we can get a zoomed-in picture of just the graph:

 

This example uses hard close times for jobs (so a job can never be served after a certain date or time). If you’re doing the same, you may also want to consider setting an unassigned penalty if the job is not assigned. This allows the optimiser to differentiate between high and low priority jobs - see section on unassigned job cost for more details.

9.6.0.3 Green, yellow, red zones delivery times example

For certain types of delivery operations, it is useful to define different penalty zones based on the delivery time, for example:

  1. ‘Green zone’ where delivery anytime within the green zone is considered equally ok, and the optimiser should choose the delivery time that minimises the amount of driving done.

  2. ‘Yellow zone’ where we want the delivery to be done as soon as possible, but the delivery is not considered to be late yet, i.e. it is still within the service level agreement (SLA) made with the customer.

  3. ‘Red zone’ where the delivery is considered late (outside the SLA agreed with the customer). Delivery must be done as soon as possible, taking priority over any deliveries which are expected to be completed in the ‘yellow’ zone.

The following JSON shows an example of this approach:

{
  "type" : "SERVICE",
  "multiTWs" : [ {
    "openTime" : "2019-01-01T09:00",
    "closeTime" : "2019-01-01T13:00",
    "penalties" : [ {
      "openTime" : "2019-01-01T11:00",
      "costPerHour" : 1.0,
      "join":"EXACT"
      }, {
      "openTime" : "2019-01-01T12:00",
      "cost" : 1.0,
      "costPerHourSqd" : 3600.0,
      "join":"EXACT"
    } ]
  } ],
  "_id" : "Stop1"
}

This screenshot shows a graph of this function using the dashboard’s View lateness functions tool:

 

We have 5 distinct regions:

  1. Before 9:00 when time window is not open yet (filled grey array on the left).

  2. Purple line at y = 0 from 9:00 until 11:00. The delivery is allowed to be made within this region and no optimiser cost is added (i.e. lateness penalty is 0). This is our ‘green zone’. This zone exists because multiTWs[0].openTime is set to 9:00 (i.e. the time window opens 9:00) but the open time on the 1st penalty subfunction (multiTWs[0].penalties[0].openTime) is not until 11:00, so there is a gap before the 1st penalty subfunction when delivery is allowed because the time window is already open. The API assumes that the cost value before the 1st subfunction is always 0.

  3. Green line from 11:00 until 12:00. This is actually our ‘yellow zone’. If you zoom in on just this line, you can see that the optimiser cost is increasing at a rate of 1 per hour. This corresponds to the first subfunction in the multiTWs[0].penalties array, which has an openTime of 11:00 and a costPerHour of 1.0. We’ve set its join to be EXACT to ensure its start point matches the end of the purple line.

  4. Brown-yellow line curving upwards sharply from 12:00, ending at 13:00. This is our ‘red zone’. The high costPerHourSqd value makes the cost function curve up sharply. Using a squared function will also encourage the optimiser to spread this lateness out over multiple stops where possible. Again we set join to EXACT, to ensure the subfunction’s start point matched the end-point of the ‘yellow zone’ subfunction.

  5. Filled grey area on the right, starting at 13:00. This is the close time of the time window (i.e. multiTWs[0].closeTime. Delivery cannot be done after this time.

9.7 Locating a stop at a coordinate defined on the vehicle

The example model in the directory:

supporting-data-for-docs\example-models\specialCoordinateType

demonstrates the use of the field:

job[].stop[].specialCoordinateType

The specialCoordinateType field allows a stop in a job to have a coordinate that’s defined on the vehicle it’s assigned to. This is useful if you’re modelling a problem where a delivery vehicle can go back to it’s own depot to reload, but jobs can be served by vehicles in different depots.

One way to model reloading at a depot is to use a pickup-delivery job, which has two stops, and the first stop stop is the depot location. This works well, but if reloading should always be at the vehicle’s base location, and you have different vehicle bases, you can’t easily set this up with a single job.

The field specialCoordinateType is used instead of the stop’s coordinate field and tells ODL Live to use a coordinate defined on the vehicle as the stop’s coordinate. specialCoordinateType can take the following values:

  • VEHICLE_START - places the stop at vehicle.definition.start.coordinate.
  • VEHICLE_END - places the stop at vehicle.definition.end.coordinate.
  • VEHICLE_SPECIAL_COORDINATE_FOR_STOPS - places the stop at vehicle.definition.specialCoordinateForStops

The following example job JSON has specialCoordinateType set so that the pickup stop will always happen at the vehicle’s start location:

{
  "quantities": [
    1
  ],
  "stops": [
    {
      "type": "SHIPMENT_PICKUP",
      "durationMillis": 1200000,
      "depotId2PreventRevisitWithOnBoards": "depot",
      "specialCoordinateType": "VEHICLE_START",
      "_id": "0P"
    },
    {
      "type": "SHIPMENT_DELIVERY",
      "durationMillis": 1200000,
      "coordinate": {
        "latitude": 51.567140870156294,
        "longitude": -0.0908223333609201
      },
      "_id": "0D"
    }
  ],
  "_id": "j0"
}

We have also set depotId2PreventRevisitWithOnBoards to prevent the vehicle returning to the depot with items still onboard.

9.8 Parking time / cost

The following JSON shows a pickup-delivery job where we’ve defined a parking object on the pickup stop:

{
  "stops": [
    {
      "type": "SHIPMENT_PICKUP",
      "durationMillis": 60000,
      "coordinate": {
        "latitude": 40.70440673828125,"longitude": -74.00933074951172
      },
      "parking": {
        "parkingTimeSeconds": 120,
        "parkingTimeMinSeparationMetres": 1,
        "cost": 1
      },

      "_id": "P1"
    },
    {
      "type": "SHIPMENT_DELIVERY",
      "durationMillis": 180000,
      "coordinate": {
        "latitude": 40.73283004760742,"longitude": -74.0078125
      },
      "_id": "D1"
    }
  ],
  "_id": "Job1"
}

The parking time and parking cost are applied directly before the arrival at the stop from another location which is further than parkingTimeMinSeparationMetres distant from the stop (in this case 1 metre). If we have multiple pickups at the same location, the parking time and cost will only be applied to the first pickup. You can therefore use parking to encourage the optimiser to group stops at the same location as much as possible. Parking can be added to any type of stop within a job, i.e. we could have added it to the delivery stop instead.

If you need the parking cost to be added after completion of the colocated stops instead, so ETAs etc. are not effected by it, use shared stop duration instead.

You can view parking time in the plan by setting

model.configuration.reporting.detailedStats4PlannedStops

to true. Arrival time at the stop before parking time is added will then appear in the field arrivalTimeB4ParkExclDistConfPark (which stands for ‘arrival time before parking, excluding parking defined in model.configuration.distances’). The following example JSON shows a small model with this enabled:

{
  "data" : {
    "jobs" : [ {
      "stops" : [ {
        "type" : "SERVICE",
        "coordinate" : {
          "latitude" : 51.54792521152047,"longitude" : 0.17358325662114982
        },
        "parking" : {
          "parkingTimeSeconds" : 120.0,
          "parkingTimeMinSeparationMetres" : 1.0
        },
        "_id" : "stop1"
      } ],
      "_id" : "job1"
    } ],
    "vehicles" : [ {
      "definition" : {
        "start" : {
          "type" : "START_AT_DEPOT",
          "coordinate" : {
            "latitude" : 51.497338282453036,"longitude" : -0.06861027643520673
          },
          "openTime" : "2001-01-01T08:00",
          "_id" : "KaNYxEleS4aUuPw-sfVfCQ=="
        },
        "end" : {
          "type" : "RETURN_TO_DEPOT",
          "coordinate" : {
            "latitude" : 51.497338282453036,"longitude" : -0.06861027643520673
          },
          "closeTime" : "2001-01-01T16:00",
          "_id" : "4SyRFjlIS9SwfQUiHW4-lg=="
        }
      },
      "_id" : "v0"
    } ]
  },
  "configuration" : {
    "reporting" : {
      "detailedStats4PlannedStops" : true,
      "outputBurstTiming" : true
    },
    "timeOverride" : {
      "override" : "2001-01-01T00:00",
      "overrideType" : "SCHEDULER"
    }
  },
  "_id" : "mymodel1"
}

From ODL Live version 1.5.12 and onwards, parking cost is reportedly separately in the optimiserCostBreakdown object in the statistics, in the plan JSON. Previously parking cost would be included in the travel cost (shown in the TRAVEL field) but from 1.5.12 it appears as a separate field called PARKING_COST. This is only for parking defined on the stop or vehicle objects, not for parking defined in the distances configuration. The reported parking cost is equal to (parking.parkingTimeSeconds × vehicle.definition.costPerTravelHour)/(60 × 60) + parking.cost, summed over all locations the vehicle visits which don’t have the same latitude-longitude as its preceding location.

9.9 Sequence constraint

You can place a sequence constraint on any stop, including:

  1. Normal stops in jobs.
  2. Preloaded stops on a vehicle including breaks.
  3. The stop object representing the vehicle start and/or end.

The sequence constraint is simply a number that’s placed on the stop. The optimiser will ensure that the sequence numbers on stops in a route are always increasing (or will unload jobs if this can’t be achieved).

Note that if the sequence constraint is not flexible enough, you might consider implementing your own logic using a sequence based state user function (SEQSTATE).

The following JSON shows a job with the sequence constraint defined:

 "jobs" : [ {
  "stops" : [ {
    "type" : "DELIVER",
    "coordinate" : {
      "latitude" : 51.54792521152047, "longitude" : 0.17358325662114982
    },
    "sequenceConstraint" : 42.5,
    "_id" : "Job1"
  } ],
  "_id" : "Job1"
}

The single stop in this job will never appear in a route earlier than any other stop with a sequence number less than 42.5. In other words the value of sequenceConstraint for stops in a route will always increase.

Other points of note:

  • sequenceConstraint can be used to fix the sequence of stops on a route (e.g. when combined with using skills to set a job to a specific route). For example, set sequenceConstraint to 1, 2, 3, 4 etc.

  • sequenceConstraint can be used in the same problem with other stops that don’t have sequenceConstraint defined. Imagine you have 3 stops S1, S2, S3 and A where S1, S2, S3 have sequenceConstraint set to 1, 2 and 3 respectively and A doesn’t have sequenceConstraint set. The following routes could therefore occur:

      - A, S1, S2, S3
    
      - S1, A, S2, S3
    
      - S1, S2, A, S3
    
      - S1, S2, S3, A
  • If you have dispatched stops with sequenceConstraint set, the optimiser will only assign planned (i.e. non-dispatched) stops to the route with either (a) no sequenceConstraint or (b) a sequenceConstraint greater than or equal to the maximum sequenceConstraint of the dispatched stops.

  • The sequenceConstraint doesn’t have to be a whole number, e.g. 5.2 is fine.

9.10 Shared stop cost / duration

The following directory contains an example model which demonstrates job.stops[].sharedCostDuration:

supporting-data-for-docs\example-models\shared-stop-duration

sharedCostDuration defines a cost and a duration which is shared between between consecutive stops at the same location, and only added once for each visit to the location. So if we have 1, 2 or 10 stops in a row at the same location, the cost and duration are only added once. Inside the sharedCostDuration object is a time field (sharedCostDuration.seconds) and a cost field (sharedCostDuration.fixedCost). sharedCostDuration is similar to job.stops[].parking except that parking time is added before arrival at the first stop in a series of colocated stops, whereas the time in sharedCostDuration is added directly after the last stop in the series. Parking time will therefore effect ETAs and whether a stop can make a time window (as time windows are based on the arrival time at the stop), but as sharedCostDuration.seconds is added after the completion of all colocated stops, it doesn’t effect ETAs etc. So given 3 stops A, B and C at the same location, if they each have sharedCostDuration.seconds equal to 1 hour and a value of stop.durationMillis equal to 1 minute, and we arrive at the first stop at 9:00, in the output plan you’ll see:

  • Stop A starts 9:00, completes 9:01.
  • Stop B starts 9:01, completes 9:02.
  • Stop C starts 9:02, completes 10:03.

Here we see a stop object with sharedCostDuration defined:

{
  "type": "SERVICE",
  "durationMillis": 60000,
  "sharedCostDuration": {
    "seconds": 3600.0,
    "fixedCost": 100.0
  },
  "coordinate": {
    "latitude": 51.547158,"longitude": -0.408914
  },
  "_id": "L3S0"
}

Similar to the parking object, sharedCostDuration also has a field minSeparationMetres which defines the minimum separation between stops for a new sharedCostDuration.seconds and sharedCostDuration.fixedCost to be defined (i.e. the distance defining when a location is different or the same).

If we have multiple stops at the same location with different values of sharedCostDuration.seconds and sharedCostDuration.fixedCost, ODL Live will take the maximum values of these fields and apply them when the vehicle leaves the location.

This example model uses straight line distances and overrides the current time, so you can run it directly in ODL Live without worrying about having road network data available etc.

9.11 Stop placement restrictions

The stop placement restriction allows you to specify that certain stops can only appear at the start or the end of the route. We have two models demonstrating its use in the following directory:

supporting-data-for-docs\example-models\stop-placement-restriction

These models demonstrate the placementRestriction field on a stop, combined with the depotId4MultiTripRules field. With the placementRestriction field you can specify that a stop can either:

  • Only appear at the START_OF_ROUTE (or following another stop that can only appear at START_OF_ROUTE),

  • or only appear at the END_OF_ROUTE (or come directly before another stop that can only appear at the END_OF_ROUTE).

If you have multiple START_OF_ROUTE stops on a route, they must therefore come at the very start of the route. Similarly if you have multiple END_OF_ROUTE stops on a route, they can only appear at the very end of the route. We demonstrate this using the two example models stop-placement-restriction-using-START_OF_ROUTE.json and stop-placement-restriction-using-END_OF_ROUTE.json. The models contain 6 pickup-delivery jobs each, where we use the depotId4MultiTripRules on the pickup stop of each job to specify that the stop either belongs to depot “0” or depot “1”:

  • Jobs Job0, Job1, Job2 have depotId4MultiTripRules = “0”

  • Jobs Job3, Job4, Job5 have depotId4MultiTripRules = “1”

depotId4MultiTripRules prevents the vehicle visiting the same or a different depot whilst stops are on-board. With depotId4MultiTripRules set but without the placementRestriction set, two possible routes could be created:

  • (Do pickups and dropoffs for Job0, Job1, Job2), (Do pickups and dropoffs for Job3, Job4, Job5)

  • or (Do pickups and dropoffs for Job3, Job4, Job5), (Do pickups and dropoffs for Job0, Job1, Job2)

In the example model stop-placement-restriction-using-START_OF_ROUTE.json, we set the placementRestriction on each pickup stop as follows:

{
  "type": "SHIPMENT_PICKUP",
  "depotId4MultiTripRules": "0",
  "coordinate": {
    "latitude": 51.54792521152047,
    "longitude": 0.17358325662114982
  },
  "placementRestriction": "START_OF_ROUTE",
  "_id": "P0"
}

This means the pickup stops can only go at the start of the route and so the vehicle can no longer do all pickups and dropoffs for one depot, and then do the pickups for the second depot, because the second depot’s pickups will not be at START_OF_ROUTE. Crucially we only set the placementRestriction on the pickup stops and not the dropoffs stops, which means the pickups at the second depot cannot follow the dropoffs at the first depot. The example model stop-placement-restriction-using-END_OF_ROUTE.json has similar logic to the first example model, but using placementRestriction END_OF_ROUTE applied to the dropoff stops instead.

As a side note, the same functionality could also be achieved by setting a fromStopCosts rule on the pickup stops, with prohibited=true, notFrom=true and fromStopGroupId being a stop group given to all pickups.

Note that if the stop placement rules are not flexible enough for your use case, you might consider implementing your own logic using a sequence based state user function (SEQSTATE).

9.12 Time windows

ODL Live supports two different sorts of time windows on a stop:

  • Simple version. A stop has openTime, lateTime and closeTime fields. A vehicle will wait at the stop until the openTime and will not serve the stop after the closeTime. openTime, lateTime and closeTime can all be null (i.e. not set) for a stop in a job, if desired. The stop can be served after the lateTime but this will incur a penalty.

    • Lateness for a stop is defined as the amount of time between the stop’s late time and the arrival time at the stop, so if late time is 10:00 am and the vehicle arrives at 10:10 am, it is 10 minutes late.

    • Lateness is penalised according to the lateness penalty object in the configuration. The penalty must be either lateness to the power 1 or to the power 2 (i.e. linear or squared). Using default cost parameters, ODL Live will seek to minimise lateness over and above travel cost or vehicle usage. Lateness naturally arises in real-time planning from unexpected delays.

    • Waiting time occurs when a vehicle waits until the stop openTime. In a real-time scenario, small amounts of waiting time before multiple stops is generally a good thing, as it gives slack in the routes which can be used to schedule new jobs, take breaks or help catch-up if the vehicle is running behind the planned schedule. Large amounts of time (e.g. hours) being wasted by waiting for a stop to open is obviously a bad thing though. Waiting time can be minimised by the engine using a cost parameter, providing the minimisation can be achieved by reassigning or reordering stops on routes.

  • Advanced version. A stop can have multiple time windows stored in an array in the stop’s multiTWs field. Each of these time windows is a special ‘custom time window’ object, which can have multiple different lateness penalty functions placed upon it. This allows, for example, setting up different lateness ‘zones’ with different penalty values.

You should use the advanced version of time windows if you (a) need multiple time windows or (b) want to customise the lateness penalty functions for a stop. Otherwise, use the simple version.

If the multiTWs array is set and has one or more elements, ODL Live will use this and will ignore the stop’s openTime, lateTime and closeTime fields. Otherwise ODL Live will use the openTime, lateTime and closeTime fields. ODL Live will not use the simple and advanced time windows at once; it will pick either one or the other.

If you enable detailed statistics in the reporting by setting the following field to true,

model.configuration.reporting.detailedStats4PlannedStops

then you can see how late each stop was and the associated cost for the lateness in the plan json.

9.12.1 Multiple hard time windows example

The following JSON shows a stop that can be served on mornings only, for two consecutive days:

{
  "type" : "SERVICE",
  "multiTWs" : [ {
    "openTime" : "2019-01-01T09:00",
    "closeTime" : "2019-01-01T13:00"
  }, {
    "openTime" : "2019-01-02T09:00",
    "closeTime" : "2019-01-02T13:00"
  } ],
  "_id" : "Stop1"
}

9.12.1.1 Multiple time windows for lunch breaks

If you are using multiple time windows to define that a stop cannot be visited during a lunch break, and this is a realtime (same day, not next day) problem, you are advised to set a lateness penalty function shortly before the start of the lunch break. An optimiser can (and often does) schedule an arrival time just before the end time window. For example, if deliveries can be done between 9:00 and 12:00, it’s not uncommon for stops to arrive at the very end of the window, say at 12:59. In a realtime problem, where the estimated arrival time is constantly being updated using GPS tracking, this can be a problem. Small deviations in timing could cause the estimated time to shift between 12:59 (which is OK) and 13:01 (which will cause the stop to unload). This would cause large-scale changes in the plan, as stops are unloaded and reassigned, and may impact solution quality.

The solution is to also set a custom penalty (see next section), to heavily penalise arrival times between say 12:55 and 13:00, whilst still allowing them. In other words arrival times which are ‘tight’ are penalised. The optimiser will then avoid, where possible, scheduling arrival times right before the end time window.