7 Types of jobs and stops

ODL Live supports various types of jobs and stops, which are detailed in the next sections.

7.1 Deliver (from central depot)

With a deliver job, an item is collected from the depot at the start of the route (i.e. the vehicle’s start location) and delivered to the stop location. Deliver jobs will not be assigned to vehicle once the vehicle has left the depot and any assigned deliveries in the plan will be unassigned when the vehicle leaves, unless you have locked them to the vehicle. This is because a vehicle can’t load items at the depot once it has left the depot.

Here’s a JSON example of a deliver job, which has one stop, with type DELIVER:

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

7.2 Pickup (return to depot)

With a pickup job, an item is collected from the stop location and delivered back to the depot (i.e. the vehicle’s end location). Here’s a JSON example of a pickup job, which has one stop, with type PICKUP:

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

7.3 Pickup-delivery (move item from point A to point B)

With a pickup-delivery job, an item is moved from one location to a different location, where neither location is the vehicle’s start or end coordinate. Pickup-delivery jobs are useful for taxi services, on-demand e-commerce, restaurant food deliveries and modelling reloading at a depot (i.e. when a vehicle can’t hold all deliveries and so goes back to reload). Pickup-delivery jobs have 2 stops, with stop type SHIPMENT_PICKUP and SHIPMENT_DELIVERY:

{
  "quantities" : [ 1 ],
  "stops" : [ {
    "type" : "SHIPMENT_PICKUP",
    "durationMillis" : 600000,
    "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"
}

You can also setup jobs which have alternative pickup locations, so ODL Live chooses the best pickup. See section on alternative jobs for more details.

Pickup-delivery jobs are also useful for depot-based problems where you need to go back to the depot mid-route and reload (i.e. because truck capacity is not large enough to hold an entire route’s worth of items). In this case you would use pickup-delivery jobs where the pickup stop corresponds to the depot. You should also use the depot revisit rules, because if you don’t use them (a) the optimiser can be slow and (b) you can return to the depot with items still on-board.

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

7.4 Service (visit location)

With a service job, a location is served but no item is moved. These are typically used for field service management type applications, e.g. when a technician needs to visit a location to fix a machine. Service jobs have one stop with type SERVICE.

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

7.5 Alternative jobs

With the alternative jobs functionality, you can define multiple jobs and tell ODL Live that only one of the jobs should be assigned. A typical use case for this is if you’re doing deliveries, and you have multiple possible pickup locations for an item. You therefore want to ODL Live to choose the best pickup location. To do this, given say 3 possible pickup locations, you could set up 3 alternative jobs, each with a different pickup location but the same delivery location. ODL Live will then select just one of these alternative jobs, based on minimising solution cost.

Alternative jobs are defined within the normal job object. You have a top level ‘container’ job object, which you can use with the normal endpoints (PUT job, DELETE job, etc) and which sits (as normal) in the model.data.jobs array. The top level container job object should not have any stops within its job.stops array and it contains one or more child jobs, which are stored within its altJobs array. ODL Live will assign one (and one only) of its child jobs.

In the supporting-data-for-docs directory (which comes with a self-hosting ODL Live), there’s a file:

supporting-data-for-docs\example-models\alternative-pickups-model\small.json

small.json contains a tiny model with 1 job, which has 3 alternative jobs inside it corresponding to 3 alternative pickup locations. The structure of small.json is as follows:

{
  "data" : {
    "jobs" : [ 
    
    {
      ... This is the top-level container job
      
      "altJobs" : [ 
       {
        ... this is the 1st alternative job
        "stops" : [ {
          "type" : "SHIPMENT_PICKUP",
          "coordinate" : {
            "latitude" : 51.55682810601671, "longitude" : 0.10038226934439609
          },
          "_id" : "J0M0P"
        }, {
          "type" : "SHIPMENT_DELIVERY",
          "coordinate" : {
            "latitude" : 51.54792521152047, "longitude" : 0.17358325662114982
          },
          "_id" : "J0M0D"
        } ],
        "_id" : "J0M0"
      }, 
      {
        ... this is the 2nd alternative job
        "stops" : [ 
            ... pickup-dropoff stops for 2nd alternative job
         ],
        "_id" : "J0M1"
      }, 
      {
        ... this is the 3rd alternative job
        "stops" : [ 
            ... pickup-dropoff stops for 3rd alternative job
        ],
        "_id" : "J0M2"
      } ],
      
      ... This is the stops array for the top-level container job, it is empty
      "stops" : [ ],
      "_id" : "J0"
    } ],
    "vehicles" : [ {
      ... single vehicle definition
    } ]
  },
  "configuration" : {
    ....
  },
  "_id" : "AltPickupsModel"
}

If you run this model (by doing PUT model to your ODL Live instance), ODL Live will schedule the job and it will choose the least-cost alternative job within the model (i.e. for this example, the least-cost pickup point).

Other points of note:

  • All stops in an alternative job must have unique ids (i.e. even if the delivery location is the same, you must have a different id for each delivery stop, the same stop id cannot be used in multiple alternative jobs).

  • For realtime problems, you dispatch these stops as you normally would. Once you dispatch a pickup stop, only the alternative job containing that pickup stop will be scheduled (i.e. you’re locked to a single alternative job).

  • Alternative jobs do not have to be pickup-delivery jobs (i.e. 2 stop jobs), they can actually be any type of job, however pickup deliveries with alternative pickup or dropoff locations is the most obvious use case for this functionality.

  • Skills, quantities etc. should be defined on the alternative job objects, not the top-level container job. If you define skills or quantities in the top-level job, these will be ignored.

7.6 Breaks

Several sorts of breaks are supported by ODL Live.

  • Scheduled / preloaded breaks. These are breaks which are scheduled to happen at set times (e.g. lunchbreak, overnight break).

  • Dynamic breaks using replenishments. These are breaks added dynamically using replenishments based on working time - see this example model for details.

  • A-to-B Autobreaks. Currently we support adding automatic break time between A to B journeys based on total travel time. These can be combined with preloaded breaks.

7.6.1 Scheduled / preloaded breaks

ODL Live allows multiple work breaks (i.e. periods where the vehicle is not working) to be defined and updated in real-time. A break is a special type of stop without a location, but which can still have (a) an open time (the earliest break can start), (b) a late time (when the break should start by), (c) a close time (the latest time the break is allowed to start) and (d) a duration (how long the break lasts). Breaks are defined within the vehicle’s definition object in an array with the property name preloadedStops. Multiple scheduled breaks can be added to a vehicle (e.g. mid-morning break, lunch, mid-afternoon break). See the section on preloaded stop for more details.

7.6.1.1 Defining breaks in the JSON

Breaks are defined within the vehicle’s definition object. The following JSON is for a vehicle object with one break:

{
  "definition" : {
    "start" : {
      "coordinate" : {
        "latitude" : 51.511892,"longitude" : -0.123313
      },
      "openTime" : "2017-09-08T14:55:44.662",
      "type" : "START_AT_DEPOT",
      "_id" : "StartVehicle1"
    },
    "end" : {
      "coordinate" : {
        "latitude" : 51.511892,"longitude" : -0.123313
      },
      "lateTime" : "2017-09-09T00:55",
      "closeTime" : "2017-09-09T00:55",
      "type" : "RETURN_TO_DEPOT",
      "_id" : "EndVehicle1"
    },
    "preloadedStops" : [ {
      "openTime" : "2017-09-08T19:55",
      "lateTime" : "2017-09-08T20:55",
      "closeTime" : "2017-09-09T14:55",
      "type" : "BREAK",
      "durationMillis" : 3600000,
      "_id" : "Vehicle1Break1"
    } ]
  },
  "_id" : "vehicle1"
}

As breaks are a type of stop, they need to have their type property set as above. As the preloadedStops property in the vehicle’s definition is an array, you can add multiple breaks here.

Always make sure a break’s id field is unique throughout the entire model. No other vehicle can have a break with the same id. Crucially no stop should have the same id as a break either, as breaks are treated as stops.

7.6.1.2 Real-time planning with breaks

Breaks are treated exactly the same as stops with regards to the vehicle’s plan and the dispatch list. Breaks will appear as planned stops within a vehicle’s plan, as in the following planned stop JSON:

{
  "stopId" : "break1",
  "timeEstimates" : {
    "arrival" : "2017-09-08T14:47:35.085",
    "start" : "2017-09-08T14:47:35.085",
    "complete" : "2017-09-08T14:57:35.085"
  },
  "earliestDispatch" : "2017-09-08T14:37:35.001"
}

For real-time planning you must dispatch breaks in the same way you dispatch other stops, adding a dispatch object to the vehicle’s dispatch list for the break and setting the dispatch object’s stopId property to the break object’s id. Here we have the dispatch object for our break:

{
  "creationTime" : "2017-09-08T14:50:04.408",
  "id" : "dispatch-for-break1",
  "stopId" : "break1"
}

Once a break is dispatched it will no longer appear in the vehicle’s plan (after the plan has next been updated). The break object must still be kept in the vehicle definition because the dispatch object refers to the break object and ODL Live will need to know the break’s opening time and duration (from the original break object) to calculate the vehicle’s next free state after the break.

7.6.1.3 Preventing jumps in ETAs when dispatching breaks

Imagine a route with stops A and B, and a preloaded break called brk, which is in this sequence - A, brk, B. If brk has an openTime which is after the departure from A but before the arrival at B, then travel from A to B will be done both before and after the break - i.e. the break will be taken enroute.

Now let’s imagine that we dispatch stop A and the brk stop, but not stop B, and we dispatch them before the vehicle is travelling so there’s no GPS location being sent to ODL Live. As ODL Live is allowed to change/move/unassign any stop after the dispatched stops A and brk, ODL Live cannot assume the vehicle was travelling to stop B when it took the brk. As a result, if no GPS has been added, it will assume that the break was taken at the location of stop A and that all driving from A to B must be done after the break. This causes the ETA for stop B to erroneously jump later in time.

To solve this, we can explicitly give ODL Live the location where the break will be taken when we dispatch the break. The model in the following directory has an example problem where this is done:

supporting-data-for-docs\example-models\coordinate-override-for-dispatched-breaks

If we look at the vehicle object in the model, we see that for the brk’s dispatch object, we’ve set the coordinateOverride object to the location where we expect the break to be taken:

{
  "definition": { ... },
  "dispatches": [
    {
      "creationTime": "2025-01-01T00:00:00",
      "stopId": "A"
    },
    {
      "creationTime": "2025-01-01T00:00:00",
      "stopId": "brk",
      "coordinateOverride": {
        "latitude": 51.99781064731704,
        "longitude": -1.0299187286011735
      }
    }
  ]
}

If you run this model without any dispatches, stop B will start at 03:59. If you add the dispatches for A and brk but don’t set coordinateOverride, B will jump later in time and its start time becomes 04:59. Setting coordinateOverride corrects this and brings its start time back to 03:59.

In summary, if you’re (a) dispatching a break but not the stop after it with a location, (b) you’re dispatching the stops/breaks before the vehicle arrives at them (so no GPS available) and (c) the break time window would allow travel before the break, you should set coordinateOverride to the expected location where the break will be taken (e.g. motorway service station).

7.6.1.4 Preventing breaks between a pickup and dropoff

By default breaks are allowed between a pickup and drop-off as for courier-type operations, there is no reason a work break can’t be taken with a job still on-board the vehicle. For taxi-type operations, obviously the driver can’t take a break with passengers still on-board. To prevent this happening, you should set the breakAllowedBetweenStops field in the job to false, as shown in this example job JSON:

{
  "breakAllowedBetweenStops" : false,
  "stops" : [ {
    "type" : "SHIPMENT_PICKUP",
    "durationMillis" : 1800000,
    "coordinate" : {
      "latitude" : 51.54792521152047,
      "longitude" : 0.17358325662114982
    },
    "_id" : "J1Pick"
  }, {
    "type" : "SHIPMENT_DELIVERY",
    "durationMillis" : 1800000,
    "coordinate" : {
      "latitude" : 51.497338282453036,
      "longitude" : -0.06861027643520673
    },
    "_id" : "J1Del"
  } ],
  "_id" : "Job1",
}

7.6.1.5 Pitfalls to avoid with break modelling

The following subsections detail issues which can arise with break modelling and how to avoid them.

Break priority

Breaks take priority over all other jobs. The optimiser will therefore ensure breaks are loaded before considering any other high-priority jobs, such as those locked to the vehicle. If you’re modelling a pickup-delivery problem where the pickup is already loaded onto a route, and you’re modelling breaks as well, it is therefore important to use late time rather than hard close times, so on-board stops cannot be unloaded in-favour of breaks.

Breaks and waiting

Some problems have a natural ‘pressure’ to serve stops earlier, for example:

  • If you have a high fixed vehicle cost and more stops then can be done within a single vehicle’s operating hours.

  • If stops are quite constrained by time windows.

  • If late time encourages the stops to be done earlier.

Not all problems are setup like this however and some very simple problems might lack this ‘time pressure’ to serve stops earlier. Imagine a single vehicle problem where quantity constraints limit the number of stops a vehicle can do in a day, but the vehicle’s available operating hours are left very open and stops have no time windows. In this situation there may be no difference in optimiser cost between these two solutions:

  • Solution I - break, stop1, stop2, stop3, stop4

  • Solution II - stop1, stop2, break, stop3, stop4

Assuming the break open time is set some time after the vehicle starts work, for solution I the vehicle will have to wait (i.e. do nothing) until the break open time starts and the break can be ‘served’. In solution II, the vehicle may still have to wait, depending on when stop 2 finishes relative to the break open time, however it is going to be waiting a lot less time than in solution I. Solution II is clearly preferable to solution I from a real-world perspective, but without any ‘time pressure’ the solver won’t distinguish between the two.

This can be solved by applying a cost to the time spent waiting for the break to ‘open’. The relevant field to set is costPerWaitingHour in the vehicle’s definition. If you currently have a zero costPerWaitingHour and your problem lacks ‘time pressure’, you are advised to set it to a small non-zero amount (e.g. 1% of costPerTravelHour). If waiting cost is too high it can distort the routes, basically making a vehicle do extra driving just to avoid waiting. Waiting time cost should therefore always be used with caution and always be smaller than costPerTravelHour.

Physically impossible breaks

Any number of breaks can be defined, which means you can also define overlapping breaks which are not physically possible without breaking the close time (e.g. a one hour break starting between 12:00 and 13:00 and another one hour break starting between 12:30 to 13:00).

The optimiser will not optimise a vehicle route with physically impossible breaks and will not allow non-break jobs to be added to it. If the vehicle is physically impossible and has preloadedStops with locations assigned to it (not just breaks), it will still appear in the plan object with the field impossibleVehicle set to true:

{
  "vehiclePlans" : [ {
    "vehicleId" : "vehicle1",
    "plannedStops" : [ 
        ... break ETAs will be available here
    ]
    "impossibleVehicle" : true
  } ]
}

7.6.2 Dynamic breaks using replenishments

See the section in replenishments detailing the example model for details.

7.6.3 A-to-B Autobreaks

A to B autobreaks allow you to add extra time to the time travelling between two locations, based on the time travelled. For example, if you’re on a 15 hour journey travelling from Paris to Rome you could say:

  • 1st break - take a 30 minutes autobreak when I’ve been driving for 2 hours.
  • 2nd break - take a 1 hour autobreak when I’ve been driving for 5 hours.
  • 3rd break - take a 9 hours autobreak when I’ve been driving for 10 hours.
  • Last break - take a 1 hour hour autobreak when I’ve been driving for 13 hours.

Autobreaks are summed, so in the above example a drive of 13.5 hours total between two locations would incur a total autobreak of 30 minutes + 1 hour + 9 hours + 1 hour, so the total journey would take 13.5 hours driving + 11.5 hours breaks.

Warning - autobreaks are not yet 100% realtime compatible; in realtime once a vehicle has completed stop A and is enroute to stop B the autobreak time will not be calculated properly if you have GPS traces. If you require this feature to be realtime compatible, please tell Open Door Logistics.

If you’ve also defined scheduled breaks (i.e. breaks at set times) by adding preloadedStops of type BREAK to the vehicle, then the autobreak time will be sensibly combined with the scheduled break time. If an autobreak will be completed before a scheduled break, 100% of the autobreak time is added. Conversely if the autobreak time would enter into the scheduled break’s time (i.e. some of the autobreak time would be after the scheduled break’s openTime), then the scheduled break and autobreak time are merged. The scheduled break will then end at the latest time out of (a) when the autobreak was due to finish or (b) when the scheduled break was due to finish. In other words the scheduled break time can be counted as part of the autobreak time.

You are advised to only use autobreaks if (a) you’re modelling with full loads - where a vehicle can only hold one job on-board at once or (b) you’re not using soft time windows (i.e. no late times). This is because A to B autobreaks have an interesting property - given a route A -> B -> C the travel times A->B and B->C might be too short to trigger an autobreak, however if you remove stop B, then A->C might trigger an autobreak. Therefore removing a stop from a solution can potentially make the solution more costly, in terms of soft time violation etc. This makes it harder for the optimiser to find good solutions, particularly for problems which aren’t full loads and/or have soft time windows.

The definition of autobreaks is based on the piecewise functions used elsewhere in the ODL Live API although, to make the internal calculations simpler, we currently only support adding constant values based on time travelled (e.g. we don’t support ‘add 10% extra to travel time if travel time above X’, but we may support this in the future). The following vehicle has autobreaks defined in the list vehicle.definition.autobreakHours4A2BTravelHours. It has a 1 hour autobreak defined which is taken after 4 hours travel time and an 10 hour autobreak which is taken at 8 hours travel time. No further autobreaks are taken after 8 hours, so any A to B journey of 8 or more hours has 11 hours autobreak total:

{
  "definition" : {
    "costPerTravelHour" : 1.0,
    ....
    "autobreakHours4A2BTravelHours" : [ {
      "inclusiveLowerLimit" : 4.0,
      "exclusiveUpperLimit" : 8.0,
      "c0" : 1.0,
      }, 
      {
      "inclusiveLowerLimit" : 8.0,
      "exclusiveUpperLimit" : "Infinity",
      "c0" : 10.0,
    } 
    ],
    "start" : {
        ....
    "end" : {
        ....
     },
    },
  "_id" : "myVehicleId"
}

The piecewise functions in autobreakHours4A2BTravelHours follow the same convention as the rest of the API, there should be no gaps between the exclusiveUpperLimit of the previous function and the inclusiveLowerLimit of the next one. Also the exclusiveUpperLimit of the last function should be set to “Infinity”.

To help understand the assignment of autobreak time, there are some additional reporting fields in the plan JSON object, however these fields are turned off by default (to reduce the size of the HTTP body). To enable these fields you need to set model.configuration.reporting.detailedStats4PlannedStop to true, like we’ve done in the following model configuration object:

{
  "distances" : {
    ....
  },
  "optimiser" : {
    .....
  },
  "reporting" : {
    "detailedStats4PlannedStops" : true
  },
  "timeOverride" : {
    ......
  }
}

The additional fields appear in (a) the timeEstimates sub-object of the plannedStops objects, in each vehicle’s plan and (b) the planEndPoint object in the vehicle’s plan. The fields are:

  • A2BAutobreakHoursBefore - the autobreak hours taken before the stop (i.e. enroute to the stop).

  • A2BAutobreakHoursCarriedOver - this is only present for scheduled (preloaded) break stops and is the amount of time from an autobreak that has been added / carried over into the scheduled break. For example if a 4.5 hour autobreak is due, but after 1 hour of the autobreak a scheduled break starts, the scheduled break will have A2BAutobreakHoursCarriedOver = 4.5 − 1 = 3.5.

  • TravelHoursBefore - the total time spent travelling between this stop and the preceding stop.

To save space in the JSON, if the value of any these fields is zero, the field is not included in the JSON. So if the field isn’t present, it’s value is zero.

7.7 Custom jobs

Custom jobs can have any number of stops. For example, instead of just defining a delivery job as ‘pickup from location A, deliver to location B’, you can define many-stop jobs like ‘pickup from location A, deliver to locations B, C and D’, or ‘pickup from A, B, C, then deliver to D’, or even ‘visit A, B, C in that order, then visit X, Y, Z in the most efficient order’

These are the basics of custom job behaviour:

  • Combinable with alternative jobs. Custom jobs can also be combined with alternative jobs, so you could have a job which is for example one pickup, and two dropoffs, and the pickup has three different possible locations.

  • Combinable with other job types. Custom jobs can be used in same model and on the same route as normal jobs (custom jobs can actually be configured to act like all the other job types).

  • No half-loaded jobs. The stops in a custom job will either be all assigned by the optimiser or none will be assigned (i.e. a custom job cannot be half-loaded).

  • Optimisation speed. Custom jobs are (generally speaking) slower to optimiser then normal jobs. When using custom jobs, you may need to tweak some of the optimiser settings (see section on custom job optimiser performance).

  • Quantities defined differently. Quantities are setup differently with custom jobs. Instead of specifying the quantity at the job level, you have to specify the change in quantity on-board a vehicle which happens when the stop is served. For example, if your quantity corresponds to weight, and you have an item for delivery which is 10kg, when you pickup the item you have a quantity change of +10 (as 10kg is added to the vehicle weight) whereas when you dropoff the item you have a quantity change of -10 (as 10kg is removed from the vehicle weight). Any net quantity change across all stops in a custom job is assumed to correspond to either a pickup at the vehicle start depot or a dropoff at the vehicle end depot. For example, a custom job with 3 pickups of 10kg each going back to the depot could have 3 stops, each with a quantity change of -10.

  • Realtime modelling. Custom jobs are fully compatible with realtime modelling, and you would dispatch custom job stops like you dispatch any other type of stop.

  • Supported rules and constraints. Custom jobs support all normal rules and constraints. On-board time penalties have to be defined slightly differently for custom jobs (see this section for more details).

We will look at an example custom job in the next section. There are several fields you must set and data formatting rules you must follow when using custom jobs:

  • job.assignableAtDepotOnly. If a custom job is the kind of job that’s loaded at the depot before the vehicle starts working, you should set job.assignableAtDepotOnly=true and then the custom job will not be assigned to the vehicle after it’s left the depot. You should use delivery locking to ensure the job stays assigned to the vehicle when it leaves the depot.

  • stop.onboardQuantityChange. The on-board quantity change is an array of integers which is set to the field job[].stops[].onboardQuantityChange. Custom jobs should not set quantities on the job level in the field job.quantities.

  • stop.relativeSequenceConstraint. This an integer field which lets you place constraints on the ordering of stops in the custom job. Given job.stops (the array of stops in the job), if the stop at index i in the job.stops array has a relativeSequenceConstraint value which is the same as the stop at index i − 1, stop i can appear before or after stop i − 1 when it’s inserted into a route. If relativeSequenceConstraint for stop i is greater than relativeSequenceConstraint for stop i − 1, stop i can only appear after stop i − 1 when inserted into a route. So given 3 stops A,B,C in a job, if they all have the same number for relativeSequenceConstraint, they can go in any order. Conversely if A,B,C have relativeSequenceConstraint 1,2,2 then A must go first on the route and then we can have either B,C or C,B. There are some other rules you need to follow when using this field:

    • You should either set relativeSequenceConstraint on all stops in a job or not set it on all stops, mixing set and not-set will produce an error.
    • If you don’t set relativeSequenceConstraint on stops in a job, the optimiser assumes they have relativeSequenceConstraint 1,2,3, etc, i.e. they must be inserted onto the route in the sequence they’re defined in the job.
    • relativeSequenceConstraint should always be positive (i.e. greater or equal to zero), and should always be monotonically increasing with stop index in the job. For example, given stops A,B,C,D defined in a job, you could have relativeSequenceConstraint 1,1,1,1, 1,2,2,3 or 1,2,2,2 but you cannot have 2,1,1,1.
    • Relative sequence is ignored for dispatched stops, so you should dispatch in order of relative sequence if you want to preserve this ordering.
  • stop.type. All stops in a custom job must have their type field set to CUSTOM. Jobs which have stops with a mix of CUSTOM and other types will not be scheduled by the optimiser. Any job with more than 2 stops must have type=CUSTOM on all stops.

7.7.1 Example custom jobs problem

The supporting-data-for-docs directory provided with ODL Live contains this example model JSON:

supporting-data-for-docs\example-models\single-pickup-multi-dropoff\singlePickupMultiDropoffExample.json

This is a standalone model using straight line distances (so it doesn’t need road network data), containing several custom jobs. Each job has 1 pickup stop and multiple drop-off stops. Here’s the JSON for one of the jobs:

{
  "stops": [
    {
      "type": "CUSTOM",
      "durationMillis": 3600000,
      "coordinate": {
        "latitude": 0.003248675740970697,"longitude": 0.007405404179770004
      },
      "relativeSequenceConstraint": 0,
      "onboardQuantityChange": [3],
      "_id": "P0"
    },
    {
      "type": "CUSTOM",
      "durationMillis": 3600000,
      "coordinate": {
        "latitude": 0.0020285112654858327,"longitude": 0.009119457313233735
      },
      "relativeSequenceConstraint": 1,
      "onboardQuantityChange": [-1],
      "_id": "D0s1"
    },
    {
      "type": "CUSTOM",
      "durationMillis": 3600000,
      "coordinate": {
        "latitude": 0.004568703754831217,"longitude": 0.008615085414442737
      },
      "relativeSequenceConstraint": 1,
      "onboardQuantityChange": [-1],
      "_id": "D0s2"
    },
    {
      "type": "CUSTOM",
      "durationMillis": 3600000,
      "coordinate": {
        "latitude": 0.001995362658742622,"longitude": 0.00559342071583724
      },
      "relativeSequenceConstraint": 1,
      "onboardQuantityChange": [-1],
      "_id": "D0s3"
    }
  ],
  "_id": "j0"
}

We have a pickup stop followed by 3 delivery stops. The relativeSequenceConstraint enforces that the pickup stop must come first but the delivery stops can be in any order after the pickup. As defined in the onboardQuantityChange field, we pickup 3 items at the pickup stop, and then drop-off each item at each delivery stop (picking up is +quantity because we add to what’s on-board, dropping off is -quantity because we remove from what’s on-board).

If you want to have stops in a fixed order on the route and with no other stops (from other jobs) in-between them, you would use both (a) relativeSequenceConstraint and (b) from stop rules. For the from stop rules, you would put custom stop i in the job in its own stop group, and then place a rule on stop i + 1 where the fromStopGroupId field is set to stop i’s group, notFrom=true and prohibited=true. Stop i + 1 will then only be allowed to follow stop i.

7.7.2 Tweaking optimisation performance for custom jobs

Custom jobs are slower to optimise then normal jobs. If you only have a couple of custom jobs in a problem that’s mainly normal jobs, you are unlikely to notice the speed difference. if you have a problem that’s dominated by custom jobs, the speed difference will be more noticeable. The time it takes to optimise custom jobs depends on (a) the number of stops in the job, (b) if they can be in any sequence or a fixed sequence and (c) if they have time windows or other restrictions which means there’s less possible positions the optimiser needs to consider.

When ODL Live tries to add a custom job into a route, it tries to find the best possible positions for all stops. If the algorithm finds these quickly, it will then insert the job and go on to consider the next job. If the algorithm doesn’t find the best possible positions quickly, it will keep searching, finding the best positions it can, until it ‘timeouts’ when it reaches a maximum amount of ‘work’. When the insertion algorithm times out, it either (a) uses the best positions found already, or (b) leaves the job unassigned if it couldn’t find any allowed set of positions.

The criteria ODL Live uses for timing out is called the search depth. If you find that optimisation with custom jobs is too slow, you can reduce the search depth by overriding the default depth for each job. Conversely if you find that custom jobs are not being assigned when they should be, or you’re not getting what you believe should be the best positions selected, you might want to override the default and increase the search depth. (Generally speaking, you should only need to increase the search depth if you have a lot of stops in your job, and you have an odd setup for costs and rules, which the optimiser finds harder to deal with).

ODL Live supports the following search depth values: HIGHEST, VERY_HIGH, HIGH3, HIGH2, HIGH1, MEDIUM_HIGH3, MEDIUM_HIGH2, MEDIUM_HIGH1, MEDIUM, MEDIUM_LOW3, MEDIUM_LOW2, MEDIUM_LOW1, LOW, VERY_LOW, LOWEST.

Here’s a simple example job where we’ve overridden the search depth and set it to HIGH1 instead:

{
  "searchDepth" : "HIGH1",
  "stops" : [ {
        ....
  } ],
  "_id" : "job1"
}

By default if you don’t set a search depth on the job, ODL Live uses depth HIGH1 for jobs with only 1 or 2 stops, and MEDIUM for all other jobs (i.e. custom jobs).

7.8 Preloaded stops

A vehicle can have preloaded stops on it. These are stops that are loaded onto the vehicle before any other jobs are loaded and therefore take priority. Work breaks are a type of preloaded stop, which have no location (as a work break can happen anywhere). The other allowed type of preloaded stops are SERVICE stops, which basically means a stop with a location that isn’t a delivery or pickup (and therefore doesn’t have any associated quantities).

The following JSON shows a vehicle record with both a preloaded SERVICE stop and a lunchbreak, defined in the vehicle.definition.preloadedStops array:

{
  "definition" : {
    "start" : {
      "type" : "START_AT_DEPOT",
      "coordinate" : {
        "latitude" : 51.470069,"longitude" : -0.454499
      },
      "openTime" : "2020-01-01T08:00",
      "_id" : "Veh1Start"
    },
    "end" : {
      "type" : "RETURN_TO_DEPOT",
      "coordinate" : {
        "latitude" : 51.470069,"longitude" : -0.454499
      },
      "lateTime" : "2020-01-01T17:00",
      "_id" : "Veh1End"
    },
    "preloadedStops" : [ {
      "type" : "SERVICE",
      "durationMillis" : ‭3600000‬,
      "coordinate" : {
        "latitude" : 51.5073,
        "longitude" : -0.1657
      },
      "_id" : "Preloaded1"
    }, {
      "type" : "BREAK",
      "durationMillis" : 3600000,
      "openTime" : "2020-01-01T12:00",
      "lateTime" : "2020-01-01T13:00",
      "_id" : "break1"
    } ]
  },
  "_id" : "Veh1"
}

The optimiser assumes preloaded stops are performed in the sequence shown in the preloadedStops array; it will maintain this order in the vehicle plan (but can still add other jobs between the preloaded stops). In the example, the preloaded SERVICE stop will therefore be done before the lunchtime break. Both stops have a duration of 1 hour. The normal fields supported on all other stops are also supported on preloaded stops (e.g. multiTWs, parking, stopGroups etc).

7.8.1 What if preloaded stops violate hard constraints?

As preloaded stops have been defined on the vehicle object, the optimiser assumes that loading the preloaded stops should override hard constraints and therefore will never unload them even if a constraint is broken. If a vehicle has preloaded stops with locations (i.e. not just breaks), it will always appear in the routing plan with updated ETAs for the stops.

There are however some hard constraints that the optimiser cannot work with if they are broken. If the preloaded stops break one of these constraints, the vehicle will be considered ‘impossible’ and although the optimiser will not unload the preloaded stops, it will not load any normal jobs onto the vehicle either. Hard constraints therefore sit in one of two groups dependent on how they interact with preloaded stops:

  • Constraints which can create an impossible vehicle. Constraints that are dependent on all the stops in the route can be broken by preloaded stops, creating an ‘impossible’ vehicle. These include time window constraints, total work time and total travel km or travel time. (All these hard constraints can be reformulated as penalty functions instead though, so they are no longer breakable and would not create an ‘impossible’ vehicle).

  • Constraints ignored by preloaded stops. Loosely speaking, constraints which are just dependent on the preloaded stop and the vehicle itself will be ignored for preloaded stops. These include skills, service radius and maximum separation.

If the optimiser decides the vehicle is impossible, it will not optimise it and will not allow normal jobs to be added to it. If the vehicle is physically impossible and has preloadedStops with locations assigned to it (not just breaks), it will still appear in the plan object, with the field impossibleVehicle set to true:

{
  "vehiclePlans" : [ {
    "vehicleId" : "vehicle1",
    "plannedStops" : [ 
        ... break ETAs will be available here
    ]
    "impossibleVehicle" : true
  } ]
}

7.8.2 Real-time planning with preloaded stops

Preloaded stops are treated exactly the same as normal stops within jobs, with regards to the vehicle’s plan and the dispatch list. Preloaded stops will appear as planned stops within a vehicle’s plan, as in the following planned stop JSON:

{
  "stopId" : "preloaded1",
  "timeEstimates" : {
    "arrival" : "2020-09-08T14:47:35.085",
    "start" : "2020-09-08T14:47:35.085",
    "complete" : "2020-09-08T14:57:35.085"
  },
  "earliestDispatch" : "2020-09-08T14:37:35.001"
}

For real-time planning you must dispatch preloaded stops in the same way you dispatch other stops, adding a dispatch object to the vehicle’s dispatch list for the preloaded stop and setting the dispatch object’s stopId property to the preloaded stop object’s id. Here we have the dispatch object for our preloaded stop:

{
  "creationTime" : "2020-09-08T14:50:04.408",
  "id" : "dispatch-for-preloaded-1",
  "stopId" : "preloaded1"
}

Once a preloaded is dispatched it will no longer appear in the vehicle’s plan (after the plan has next been updated). The preloaded stop must still be kept in the vehicle definition because the dispatch object refers to the preloaded stop and ODL Live will need to know the stop’s opening time and duration (from the original stop object) to calculate the vehicle’s next free state after the preloaded stop.

7.9 Replenishments (dynamic breaks, recharging, refills, …)

Replenishments are only realtime compatible for ODL Live version 2.1 and later (prior to 2.1 the calculation does not take account of dispatched stops). Models with replenishment jobs may be significantly slower than normal models.

7.9.1 Overview

Replenishment stops are stops which the optimiser adds to the route when a ‘replenishment value’ is running out. Replenishment stops are defined within replenishment job objects. Replenishment jobs allow a vehicle to visit a location when it needs to refuel / recharge / replenish something before completing more jobs. Possible use cases are:

  • Electric vehicle charging where the vehicle needs to go to a location to recharge during the route.

  • Fuel delivery problems, where a vehicle (e.g. tanker) may deliver fuel to multiple locations and then need to refill itself.

  • Jobs requiring a certain piece of equipment to be picked-up first, where some vehicles might already have the equipment, other vehicles don’t and the distinct types of equipment is small (e.g. no more than 3).

  • Mobile shops, e.g. a van selling goods which needs to pull into one or more warehouses en-route to restock.

  • Waste disposal, where waste is picked-up and multiple pickups need to be dropped off at the best disposal site.

  • Modelling dynamic breaks (replenishments don’t have to have a location). See this example model.

Some of these use cases overlap with cases that can be solved using pickup-delivery problems where the ‘pickup’ stop is actually a reload-at-depot (see section on depot logic for more information). Generally speaking, if you can model your problem using pickup-deliveries you are advised to do this rather than use replenishments. Some problems however - e.g. electric vehicle recharging - cannot be solved with pickup-deliveries though. Also for problems where you use alternative pickup or dropoff locations (see section on alternative jobs) and multiple jobs will be picked-up or dropped-off at the same time/location, you may find the optimiser gets lower-cost results if you formulate this as a replenishment jobs problem instead. If you have a problem and you’re not sure whether to model it using replenishments or a different formulation, speak to your support contact at Open Door Logistics.

Currently the calculation time for optimising with replenishments scales linearly with the number of replenishment jobs you have defined, you are therefore advised to only define a small number of replenishment jobs in your problem (e.g. 10 or 20, not hundreds).

7.9.2 Example recharging model

For self-hosting subscribers an example model is available at:

supporting-data-for-docs\example-models\replenishments\simple-recharge-demo.json

This model uses straight line distances, so you don’t have to worry about setting up road network data. In this model a single truck serves 20 deliveries around the UK. The truck is electric and can travel a maximum of 250 km without recharging. 15 recharge locations are available around the UK for the truck to recharge at, and these are named recharge0, to recharge14. The jobs are named j0 to j19. A typical route generated by this problem is:

j18,j7,j4,recharge12,j9,recharge1,j2,j3,recharge2,j17,j1,recharge13,j19,recharge12,j11,recharge0,j8,j10,recharge4,j14,j15,j12,recharge6,j0,j16,recharge8,j5,j13,recharge8,j6

Our vehicle object is relatively normal except that it has a new sub-object, vehicle.definition.replenishes:

{
  "definition": {
    "start": {
      "type": "START_AT_DEPOT",
      "coordinate": {
        "latitude": 53.56923099825719,"longitude": -2.1280959357427465
      },
      "openTime": "2020-04-01T00:00",
      "_id": "v0Start"
    },
    "end": {
      "type": "RETURN_TO_DEPOT",
      "coordinate": {
        "latitude": 53.56923099825719,"longitude": -2.1280959357427465
      },
      "closeTime": "2021-04-01T00:00",
      "_id": "v0End"
    },
    "capacities": [42],
    "replenishes": [
      {
        "replenishTypeId": "charge",
        "usagePerTravelKm": 1,
        "minValue": 10,
        "maxValue": 250,
        "replenishPerHour": 200,
        "targetFullness4NextStopAfter": 0.5
      }
    ]
  },
  "_id": "v0"
}

replenishes is an array, so a vehicle can have more than one value which needs replenishing (but here we use only one). Inside this example replenishes array we have a single replenish object which defines the vehicle’s replenishment requirements. It can have the following fields:

  • replenishTypeId. The name of the value that must be replenished. This is an electric vehicle charging problem and so we called this ‘charge’. You can use any string value here.

  • usagePerTravelKm. The amount of the replenish value that gets used for each km driven.

  • minValue and maxValue. The minimum level of the replenish value (i.e. charge) allowed and the maximum value. The maximum value is set at the vehicle start and the vehicle is refilled to maximum whenever it visits the replenishment stop. As we set usagePerTravelKm in this example to be 1, the minValue and maxValue actually refer to km driving range. minValue is therefore the minimum amount of charge in km allowed on the vehicle, i.e. we never let its remaining range get below 10km in this example. maxValue is its range corresponding to a full charge.

  • replenishPerHour is the amount of replenish value that gets filled after an hour spent at the replenish stop. As this is 200 and our maxValue is 250, it takes 250/200 = 1.25 hours to fully charge from empty. If this field is omitted / set to null, or set to infinity, the algorithm assumes it takes no time to refill.

  • targetFullness4NextStopAfter. If you have configured replenishments for an electric car charging problem, you are advised to keep this number set at 0.5 (if you omit the field, the optimiser will default to 0.5). When choosing which replenish stop to put between two normal stops, the optimiser algorithm tries to ensure we have at least targetFullness4NextStopAfter of the maximum charge when we get to the next stop and will only consider the cost model after that. For problems where replenishments are being used to model pickups or dropoffs at stops instead, disable this by setting targetFullness4NextStopAfter to a negative number instead, e.g. set it to -1.

  • usagePerTravelHour (not shown in the example). Amount of replenish value used for 1 hour driving.

  • usagePerStop (not shown in the example). Amount of replenish value used for visiting a stop.

  • initialValue (not shown in the example). Initial amount of replenish value on-board the vehicle (this is assumed to be equal to maxValue if not set).

We must also define the possible replenish locations. These are defined in model.data.replenishJobs as follows:

{
  "data": {
    "jobs": [ ... ],
    "vehicles": [ ....],
    "replenishJobs": [
      {
        "stops": [
          {
            "type": "REPLENISH",
            "durationMillis": 0,
            "costFixed": 1000,
            "coordinate": {
              "latitude": 53.470005770115996,
              "longitude": -0.6195496483286584
            },
            "_id": "recharge0"
          }
        ],
        "replenishTypeId": "charge",
        "_id": "recharge0"
      },
      ...
    ]
  },
  "configuration": {},
  "_id": "modelId"
}

A replenish job looks very similar to a normal single-stop job except:

  • The replenishJob.stops[0].type value must be set to REPLENISH

  • The field replenishJob.replenishTypeId must be set to the same name as the replenish type that’s set in vehicle.definition.replenishes[].replenishTypeId.

If you want to include a fixed amount of time for the replenish stop (e.g. 10 minutes plus the time to charge) then set replenishJob.stops[0].durationMillis appropriately. If you only want a fixed time for each replenish (i.e. no variable time dependent on how empty the vehicle is) then omit the field vehicle.definition.replenishes[].replenishPerHour or set to Infinity and only replenishJob.stops[0].durationMillis will be included in the stop time at the replenish. The following example element from a vehicle.definition.replenishes[] array shows replenishPerHour set to infinity:

{
  "replenishTypeId" : "charge",
  "usagePerTravelKm" : 1.0,
  "minValue" : 10.0,
  "maxValue" : 250.0,
  "replenishPerHour" : "Infinity",
  "targetFullness4NextStopAfter" : 0.5
}

7.9.3 Example waste disposal model

For self-hosting subscribers an example model is available at:

supporting-data-for-docs\example-models\replenishments\waste-disposal-return-empty-demo.json

In this model we route a waste disposal problem. We define a replenish value called “available-capacity-percentage” which is the percentage of available volume left free on the truck. Its maximum is therefore 100 and its minimum is 0. At each stop, a truck collects either 25% or 50% of its volume (note in a real-life problem you should probably define replenishment in terms of cubic metres, litres or similar instead - this example just uses percentage to make it simple). The truck visits disposal sites to offload its contents as it needs to. We also set an additional rule that the truck must return to its end location empty, so it has to visit an additional disposal site on the way home.

Here’s an example job JSON, where the vehicle picks up 25% of its volume:

{
  "stops": [
    {
      "type": "PICKUP",
      "durationMillis": 0,
      "coordinate": {
        "latitude": 50.86576747971346,
        "longitude": -0.5528191194879581
      },
      "replenishUsage": [
        {
          "valueUsed": 25,
          "replenishTypeId": "available-capacity-percentage"
        }
      ],
      "_id": "j20"
    }
  ],
  "_id": "j20"
}

This is a normal job set in model.data.jobs (like all other jobs), except it has the new sub-object job.stops[0].replenishUsage. The replenishUsage object has 3 fields, although only 2 are used in this example:

  • replenishTypeId - the name of the replenish value the stop is using.

  • valueUsed - the amount of the replenish value that is used at the stop. Here we use 25% of the truck’s volume when we collect at the stop.

  • minValue - a minimum value of the replenish value that the truck must have when it arrives at the stop. If this field is left out, no minimum value is enforced.

Next we show the vehicle JSON:

{
  "definition": {
    "start": {
      "type": "START_AT_DEPOT",
      "coordinate": {
        "latitude": 51.51531017312055,"longitude": -1.4639024652128316
      },
      "openTime": "2020-04-01T00:00",
      "_id": "v7Start"
    },
    "end": {
      "type": "RETURN_TO_DEPOT",
      "coordinate": {
        "latitude": 51.51531017312055,"longitude": -1.4639024652128316
      },
      "closeTime": "2021-04-01T00:00",
      "replenishUsage": [
        {
          "valueUsed": 0,
          "minValue": 100,
          "replenishTypeId": "available-capacity-percentage"
        }
      ],
      "_id": "v7End"
    },
    "replenishes": [
      {
        "replenishTypeId": "available-capacity-percentage",
        "minValue": 0,
        "maxValue": 100,
        "replenishPerHour": 100,
        "targetFullness4NextStopAfter": 0.5
      }
    ]
  },
  "_id": "v7"
} 

At the replenish value is a percentage, minValue and maxValue on vehicle.definition.replenishes[0] are set to 0 and 100 respectively.

The vehicle also has replenishUsage defined in its end stop vehicle.definition.end.replenishUsage, this has valueUsed=0 because going to the vehicle’s end location doesn’t use up any space on-board. The vehicle end replenishUsage object does however have minValue set to 100, which enforces that the vehicle must visit a disposal site before going home, i.e. must not go home empty.

The definition of the possible replenish locations in model.data.replenishJobs is straightforward and follows the same pattern as the previous example:

{
  "stops": [
    {
      "type": "REPLENISH",
      "durationMillis": 0,
      "costFixed": 1000,
      "coordinate": {
        "latitude": 51.748484948995994,
        "longitude": -1.7281965822220022
      },
      "_id": "Disposal0"
    }
  ],
  "replenishTypeId": "available-capacity-percentage",
  "_id": "Disposal0"
}

Using the same naming convention as the previous example, a typical route for this example would be:

j32,j90,j42,Disposal3,j39,j82,Disposal3,j79,j73,Disposal3,j65,j28,j20,Disposal1 

where the last stop is always a replenish stop.

7.9.4 Example problem with different replenish costs for different locations

Different replenish locations can have different costs for replenishing a vehicle. There is a small model demonstrating this at the following location:

supporting-data-for-docs\example-models\replenishments\costPerReplenishUnit.json

This model doesn’t use road network data, so you can run it without setting the road network file. It contains a single stop job which is setup so that the vehicle must do a replenishment before visiting the stop. There are 10 different possible replenishment locations, we show the first two in the JSON snippet below:

{
  "data": {
    "jobs": [ ... ],
    "vehicles": [ ... ],
    "replenishJobs": [
      {
        "stops": [
          {
            "type": "REPLENISH",
            "coordinate": {
              "latitude": 51.534785147202975,"longitude": 0.19396434656012718
            },
            "replenishFilling": {
              "costFixed": 1.2664655278719565,
              "costPerReplenishUnit": 4.261602592649618
            },
            "_id": "rep0"
          },
          {
            "type": "REPLENISH",
            "coordinate": {
              "latitude": 51.54700751418152,"longitude": 0.14021711025600964
            },
            "replenishFilling": {
              "costFixed": 3.580242556498624,
              "costPerReplenishUnit": 0.503419157455702
            },
            "_id": "rep1"
          },
          ...
        ],
        "replenishTypeId": "replenishType1",
        "_id": "replenishJobs1"
      }
    ]
  },
  "configuration": { ... },
  "_id": "REPLENISH_MODEL"
}

Each replenish stop has a sub-object called replenishFilling which has the following fields:

  • costFixed - a fixed cost for refilling at this location.

  • costPerReplenishUnit - a cost for every replenishment unit filled at this location. The replenishment value for a vehicle will be filled up to its vehicle.definition.replenishes[].maxValue at the replenishment stop, so if (1) the vehicle has a replenishment value of 3 when it arrives at the replenishment stop, (2) the maxValue is 10 and (3) costPerReplenishUnit is 2, then a cost of (10 − 3) × 2 = 14 will be incurred.

The optimiser minimises cost, and so when it chooses the replenishment location between two stops, it will choose the location that incurs the minimum cost, so including both travel cost and the replenish cost itself.

7.9.5 Example breaks using replenishes with delays model

The following directory contains an example model which uses replenishments to model dynamic breaks:

supporting-data-for-docs\example-models\breaks-using-replenishes-with-delays

This is one of the vehicle objects in the example model:

{
  "definition": {
    "workTimeHours": [
      {
        "inclusiveLowerLimit": 10.0,
        "prohibited": true
      }
    ],
    "start": {...},
    "end": {...},
    "replenishes": [
      {
        "replenishTypeId": "break",
        "usagePerHour": 1.0,
        "minValue": 0.0,
        "maxValue": 4.5,
        "replenishPerHour": 9.9999999999E10,
        "initialValue": 4.5,
        "maxNbReplenishes": 1
      }
    ]
  },
  "_id": "v0"
}

We’ve placed a rule on it (in workTimeHours) that it can’t work for more than 10 hours, so no more than 10 hours between the time it leaves home and time it returns home. We’ve also used replenishes to set a rule that it can’t work more than 4.5 hours without a break. Let’s look at the fields in the replenish object:

  • replenishTypeId - this id links to the replenish job defining the break, which we’ll discuss below.
  • usagePerHour - the replenish is used to model working time, so 1 unit of replenish gets used in 1 hour.
  • minValue - we can’t have less than 0 hours left available.
  • maxValue - the maximum number of hours we can go without a break, i.e. 4.5.
  • replenishPerHour - we effectively disable this value by setting it to an arbitrarily large number, as this will add extra time (which we don’t want adding) to the schedule when the replenish break happens, so by setting to a very big number, the extra time added becomes tiny. Alternatively, we could have just omitted the replenishPerHour field and it would have default to infinite internally.
  • initialValue - when the vehicle starts we assume that they have the maximum 4.5 hours available, so we set this here.
  • maxNbReplenishes - we don’t allow more than one replenish (if this field is omitted, there is no limit).

We also define the break job in the array model.data.replenishJobs:

{
  "stops": [
    {
      "type": "REPLENISH",
      "durationMillis": 1800000,
      "costFixed": 100.0,
      "_id": "break"
    }
  ],
  "replenishTypeId": "break",
  "_id": "break"
}

Let’s look at the various fields:

  • _id (in both places), this can be anything but we set it to ‘break’ to keep things clear.
  • type - must be REPLENISH.
  • durationMillis - how long the break is, corresponding to 30 minutes.
  • costFixed - the cost of taking this break. Set to higher to minimise the number of breaks taken.
  • replenishTypeId - this must be equal to the field vehicle.definition.replenishes[].replenishTypeId, as this id is used to link the replenish job to the replenish constraint on the vehicle.

Crucially the replenish stop object does not have a coordinate field, as the break can happen anywhere rather than at a specific location. If you have a replenish defined with no coordinate, the algorithm will always choose that rather than any other replenish job with the same replenishTypeId (unless you use the explicit search mode). So if you’re not using explicit search for a specific replenishTypeId, if you have a replenish job without a coordinate, that should be the only replenish job for that replenishTypeId.

The normal jobs in the problem are straightforward service-type jobs with time windows, so we won’t cover them here. The configuration for delay optimisation is however of interest. In this example problem, we set that vehicles can start work anytime in a 24 hour period but can only work a maximum of 10 hours, so we set the delays configuration (in model.configuration.optimiser.delays) to work during the optimisation and to delay the start time if needed. Importantly, we also want the delay optimisation to be able to extend the length of a break when required. The optimal solution requires this extension as (a) we only allow one break, (b) we do not allow working for more than 4.5 hours without a break and (c) the time windows on some of the jobs mean time can be wasted waiting for stops to open. By extending the breaks, we can remove some waiting time at jobs and stay within the 4.5 hours limit more easily.

Here’s our delays configuration:

  "delays" : {
    "whereToLookForWaits" : "ALL_STOPS",
    "whereToAddDelays" : "START_OR_AFTER_BREAK",
    "duringOptimisation" : true
  }
  

We set whereToLookForWaits to ALL_STOPS as we want to look for waiting time before any stop. We set whereToAddDelays to START_OR_AFTER_BREAK as we want to both delay the start and extend breaks (by adding a delay after them). Lastly we set duringOptimisation to activate this logic when the algorithm is designing the routes. See section on delays for more details.

If you’re modelling a problem where a vehicle is driving long-distance and can either take a replenish based break on the route or by returning home, then assuming you’re modelling the enroute breaks as not having a coordinate, you need to tell the optimiser to explicitly search for both replenishment types. Without the explicit search, the optimiser will always choose the replenish job which doesn’t have a coordinate set.

The example model in the following directory demonstrates how to use this:

supporting-data-for-docs\example-models\replenish-explicit-search-with-home-or-out-overnights

Each replenish job (in model.data.replenishJobs) is placed in a job group using the field jobGroupIds:

{
  "jobGroupIds": [ "outgroup" ],
  "stops": [
    {
      "type": "REPLENISH",
      "multiTWs": [ ... ],
      "_id": "outstop"
    }
  ],
  "replenishTypeId": "sleep",
  "_id": "outjob"
}

The definition of the vehicle’s replenish logic (in vehicle.definition.replenishes[]) then sets the field replenishJobGroups4ExplicitSearch will tells the optimiser to search using jobs in both those job groups:

{
  "replenishTypeId": "sleep",
  "usagePerHour": 1.0,
  "minValue": 0.0,
  "maxValue": 12.0,
  "initialValue": 12.0,
  "replenishJobGroups4ExplicitSearch": [ "homegroup", "outgroup"  ]
}