11 User functions

User functions allow the users to define their own costs and constraints in the routing model, with a couple of limitations.

11.1 Overview of main types

There are four main types of user functions, which have different ‘execution contexts’:

  1. Job-vehicle user functions (COMBO). These define constraints and costs for the combination of a job on a vehicle. You can use these to ban specific jobs from a vehicle, or vehicles from a job, as well as add specific costs. They therefore perform a similar rule to skills and vehicle value-dependent cost functions, whilst being a lot more flexible.

  2. Stops set user functions (STOPSET). These define constraints and costs on the unordered set of stops assigned to a vehicle (i.e. independent of their actual sequence) and can be used to set rules like ‘no more than X stops of type Y on a vehicle’ or ‘if each stop has a value X, the sum of X over the vehicle should be less than Y’.

  3. Sequence based state user function (SEQSTATE). These define constraints and costs based on the sequence of stops on a vehicle (i.e. the ordering), but are independent of travel time, arrival time etc. The user functions can set and read user variables. The optimiser loops over all stops on a route from start to end, executing the user functions for each stop including setting/reading user variables. Many different types of constraints can be implemented using these, e.g. it would be easy to implement you own sequence constraint rule or basic quantities rules. First-in-last-out (FILO) type rules can be defined, rules which prevent a specific job being onboard at the same time as another, etc.

  4. Enroute - after travel user functions. These functions are executed as part of the vehicle’s enroute state and can access information like travel time. They can be used to defined costs based on the quantity onboard for example. They are also good for modelling things that can run out a route - e.g. the total travel hours or km limits constraints could easily be reimplemented using after travel user functions, similarly for the total km at stop cost.

The user functions are called at different places during the optimisation process. If you have a new constraint or cost you wish to add, always try to model it using the first feasible user function type in the previous list. This will make the optimiser run more efficiently, as it incrementally calculates the best insertion positions for a job on a route, and telling it earlier-on not to try to a certain combination will skip more calculations.

The next sections give a guide to the basic structure of the user functions, then look at some gotchas, before giving examples of each type.

11.2 Basic structure and syntax

User functions are defined using a formula similar to Excel, embedded within a json structure allowing if-then-else type logic. Each user function is held within a statement object. Statements can be defined on vehicles, jobs and stops. Here’s a very simple statement, which defines a cost of 1 which is added to the solution:

{
  "addCosts" : [ "1" ]
}

Here the formula is very simple "1", but it could be more complex, e.g. 

{
  "addCosts" : [ "sqrt(7) + max(1,3,5)" ]
}

The formula could also reference named values defined on the vehicle. For example, given the vehicle JSON definition:

{
  "definition" : {
    "start" : {},
    "end" : {},
    "namedVals" : {
      "x" : 2.0,
      "y" : 4.0
    }
  },
  "_id" : "v0"
}

A cost function could be defined in our statement which does:

{
  "addCosts" : [ "vehNamedVal('x') + vehNamedVal('y')" ]
}

where vehNamedVal('x') gets the named value x from the vehicle. addCosts is actually an array, so we could also write the following instead using two separate functions and it would give the exact same behaviour as before:

{
  "addCosts": [
    "vehNamedVal('x')",
    "vehNamedVal('y')"
  ]
}

Vehicles, jobs and stops can all have named values and groups (a set of strings), which can be referenced from the user functions. Depending on what type of user function is executing a statement, different values and properties will be available. For example when executing a job-vehicle function you have access to a job’s groups, the vehicle’s groups, but not stop-level groups.

11.2.1 Defining violations using user functions

As well as adding costs, you can instead define a hard constraint which returns a count of the number of violations:

{
  "addViolationCounts" : [ "1" ]
}

Any positive number (e.g. 0.1, 1, 100) returned from addViolationCounts is treated as a hard violation which ODL Live will not allow to be broken. Internally to ODL Live, the algorithm sometimes compares how big one violation is to another, or temporarily relaxes a violation. Therefore it’s good (but not essential) for ODL Live to know how big a violation is, rather than just having a binary true/false answer. Here’s some rules-of-thumb for setting the magnitude of the number you return from addViolationCounts:

  • If you have 1 stop or 1 job which is violating, return 1. Similarly if you have 2 stops violating, return 2.

  • If your violation is related to time instead, return the value in units of number of hours. So if you have a 30 minute violation, return 0.5.

  • If your violation can be described as a fraction of a vehicle used (e.g. using 150% of vehicle’s capacity so over by 50%), convert to a number of hours by assuming your vehicle operates for say 8 hours, so a 50% violation would be 4 hours and you return 4.

11.2.2 Combining multiple statements into a ‘program’

Statements themselves are always held in an array, so you can define multiple statements to be executed in-order. Here the jobVehicleCombination list holds 2 statements, which (a) add a cost of 1 and (b) add a violation of 2:

{
  "jobVehicleCombination": [
    {
      "addCosts": ["1"]
    },
    {
      "addViolationCounts": ["2"]
    }
  ]
}

Statements can use if-then-else structures. Here we say if the ‘x’ named value on the vehicle is less then 3, add a cost of 1 and a violation of 2:

{
  "jobVehicleCombination" : [ {
    "if" : [ "vehNamedVal('x')<3" ],
    "then" : [ {
      "addCosts" : [ "1" ]
    }, {
      "addViolationCounts" : [ "2" ]
    } ]
  } ]
}

Note that both the addCosts and addViolationCounts statements are held in the then array of the if-then statement. Statements can therefore be arranged hierarchically, with child statements held in the then and else lists of a parent statement. Each individual statement object must be in one (and only one) of the following forms:

  • addCosts - { "addCosts":[...] }, where the elements in addCosts must be strings defining functions.

  • addViolationCounts - { "addViolationCounts":[...] }, where the elements in addViolationCounts must be strings defining functions.

  • if-then - { "if":[...], "then":[...] }, where the elements in if must be strings defining functions and then is a list of statements to execute if the if predicate passes.

  • else-if - { "elseIf":[...], "then":[...] }

    • An else-if can only follow if-then or another else-if.
    • The elements in elseIf must be strings defining functions and then is a list of statements to execute if the elseIf predicate passes.
  • else - { "else":[...] }

    • An else can only follow if-then or else-if.
    • else is a list of statements to be executed.
  • addToVar - { "addToVar":"userVar1", "values":["max(1,3)","2"] }

    • This adds the values max(1,3) and 2 to the user variable called uservar1 (you can include 1 or more functions in the values array). The elements in values must be strings defining functions.
  • setVar - { "setVar":"userVar2", "value":"sqrt(3)"}

    • This sets the user variable userVar2 to the square route of 3.
    • value must be a single string defining a function.

User variables are only available for sequence-based state (SEQSTATE) and enroute user functions. If-then-else-if-else structures can be hierarchically nested to any depth, here’s a more complex example (where we assume the existence of some variables supplied by ODL Live named a and b):

[ {
  "if" : [ "a<b" ],
  "then" : [ {
    "addCosts" : [ "a+b" ]
  } ]
}, {
  "elseIf" : [ "a=b" ],
  "then" : [ {
    "addCosts" : [ "42.0" ]
  }, {
    "addViolationCounts" : [ "43.0" ]
  } ]
}, {
  "elseIf" : [ "b=100.0" ],
  "then" : [ {
    "addCosts" : [ "200.0" ]
  } ]
}, {
  "else" : [ {
    "addCosts" : [ "1.0", "2.0", "30.0" ]
  }, {
    "addViolationCounts" : [ "4.0", "5.0" ]
  } ]
} ]

As the if and elseif fields actually hold an array, they can hold multiple strings each defining a function, e.g.

{
  "if" : [ "a<b" , "a==b", "b>c" ],
  "then" : [ {
    "addCosts" : [ "a+b" ]
  } ]
}

If an if holds multiple functions, by default ODL Live treats this as true using OR logic (i.e. true if any one of the child functions in the array is true). You can control this by setting the ifLogic variable which can take values of OR, AND, NOR, NAND, i.e. the classic boolean logic gates from basic electronics / computer science:

{
  "ifLogic" : "AND",
  "if" : [ "a<b" , "a==b", "b>c"  ],
  "then" : [ {
    .....
  } ]
}

11.3 Gotchas (rules to follow)

For the optimiser to be able to work well with user functions, the functions need to be ‘aligned’ with how the optimiser works internally. Here’s a couple of rules you should follow when designing user functions:

  • Jobs are added to the routes one-by-one. User functions should work with both empty and full routes.

  • Adding a job to a route should not reduce the cost or violation being reported by a user function, i.e.  adding jobs should never make things cheaper.

  • User functions shouldn’t become cheaper if stops on a route are moved to a later position or served later in-time.

These gotchas mean certain types of constraints won’t work well, e.g. you can define a user function that a route shouldn’t have more than 5 stops but you can’t define a user function that it should have exactly 5 stops, because this breaks the first rule. If you try to encourage more stops to be added to a route that’s under 5 stops, by making smaller routes more expensive, this breaks the ‘adding jobs shouldn’t make things cheaper’ rule.

There’s also some gotchas specific to differen types of user functions:

  • Enroute user functions (ENROUTE) must cope with the case when a job is being inserted and not all of its stops are inserted into the route yet, e.g. imagine a pickup-delivery job where only the pickup is already inserted. They shouldn’t report a violation or high cost later in the route that will be ‘reversed’ by adding the delivery stop (you don’t need to worry about this when using the SEQSTATE user functions, only the ENROUTE user functions).

  • Although dispatching (i.e. realtime modelling) is supported with all user functions, once a stop has been dispatched the optimiser cannot place an undispatched stop before it, and it’s easy to define user functions (particularly SEQSTATE) which don’t work well with this.

11.4 Built-in function list

11.4.1 Operators

In a user function (defined in a json string), the following mathematical operators are always available:

  • != - Test if first value is not equal to second.
  • % - Remainder of dividing first value by second.
  • && - Boolean And function.
  • * - Multiply values together.
  • + - Add values together.
  • - - Subtract second value from first.
  • / - Divide one value by the other.
  • < - Test if first value is less than second.
  • <= - Test if first value is less than or equal to second.
  • <> - Test if first value is not equal to second.
  • = - Test if first value is equal to second.
  • == - Test if first value is equal to second.
  • > - Test if first value is greater than second.
  • >= - Test if first value is greater than or equal to second.
  • ^ - Return the value of the 1st number raised to the power of the 2nd number.
  • || - Boolean Or function.
  • - - Subtract second value from first.

11.4.2 Constants

The following mathematical constants are always available:

  • e - Constant value equal to 2.718281828459045
  • false - Constant value equal to 0.0
  • pi - Constant value equal to 3.141592653589793
  • true - Constant value equal to 1.0

1 and true can be used interchangeably, as can 0 and false. Any number greater than 0 is considered to be true.

11.4.3 Mathematical functions

The following mathematical functions are available:

  • abs(x) - Get the absolute of the value.
  • acos(x) - Inverse cosine function.
  • asin(x) - Inverse sine function.
  • atan(x) - Inverse tan function.
  • ceil(x) - Returns the smallest integer larger than or equal to the input number.
  • cos(x) - Cosine function.
  • floor(x) - Return the integer part of the number, e.g. 2.3 returns 2.
  • if(predicate,returnVal1,returnVal2) - Test the first value and if this is true return the second value, otherwise return the third.
  • lerp(a,b,c) - Linearly interpolate between value a and value b based on value c (which is in the range 0 to 1).
  • ln(x) - Natural logarithm.
  • log10(x) - Logarithm to base 10.
  • max(arg1, arg2, …) - Return the maximum of the input arguments (can have any number of arguments).
  • min(arg1, arg2, …) - Return the minimum of the input arguments (can have any number of arguments).
  • not(b) - Negates the input argument.
  • pow(a,b) - Return the value of the 1st number raised to the power of the 2nd number.
  • round(x) - Round to the nearest integer value.
  • sin(x) - Sine function.
  • sqrt(x) - Calculate the square root of the value.
  • tan(x) - Tan function.

11.4.4 Vehicle property accessors

Vehicle properties are always available for every type of user function. The following functions access vehicle properties:

  • isVehicleInGroup(groupId) - check if vehicle is an a specific vehicle group.
  • vehNamedVal(keyInNamedVals) - gets a named value corresponding to the parameter from the vehicle object.
  • vehNamedValExists(keyInNamedVals) - check if a vehicle has a named value corresponding to the parameter, return true if so.
  • vehNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent) - gets a named value corresponding to the 1st parameter from the vehicle object, returns the 2nd parameter if it doesn’t exist.

Here’s an example vehicle with some groups (defined in vehicleGroupIds) and a named value (defined in namedVals):

{
  "definition" : {
    "start" : {...},
    "end" : {...},
    "namedVals" : {
      "height" : 2.5
    },
    "vehicleGroupIds" : [ "London", "refrigerated" ]
  },
  "_id" : "v"
}

For this example vehicle JSON:

  • isVehicleInGroup('London') would return 1 (for true) but isVehicleInGroup('Paris') would return 0.

  • vehNamedVal('height') would return 2.5 but vehNamedVal('width') would return 0 (as it’s undefined).

  • vehNamedValExists('height') would return 1 and vehNamedValExists('width') would return 0 (as it’s undefined).

  • vehNamedValOrDefault('height',7) would return 2.5 whereas vehNamedValOrDefault('width',7) would return 7.

11.4.5 Job property accessors

These functions access properties on the job objects:

  • isJobInGroup(groupId) - check if job is an a specific job group.
  • jobNamedVal(keyInNamedVals) - gets a named value from the job.
  • jobNamedValExists(keyInNamedVals) - check if a job has a named value corresponding to the parameter, return true if so.
  • jobNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent) - gets a named value corresponding to the 1st parameter from the job object, returns the 2nd parameter if it doesn’t exist.

Here’s an example job JSON with named values (in namedVals) and groups (in jobGroupIds):

{
  "jobGroupIds" : [ "a", "b", "c" ],
  "namedVals" : {
    "x" : 1.0,
    "y" : 2.0
  },
  "stops" : [ {
    "type" : "SERVICE",
    "coordinate" : { "latitude" : 51.5073,"longitude" : -0.1657},
    "_id" : "s1"
  } ],
  "_id" : "j1",
}

These functions behave identically to the vehicle property accessors, except they may not be available in every type of user function.

11.4.6 Stop property accessors

These functions access properties on the stop objects:

  • isStopInGroup(groupId)
  • stopNamedVal(keyInNamedVals)
  • stopNamedValExists(keyInNamedVals)
  • stopNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent)

The following JSON shows a job whose stop has named values (in stops[].namedVals) and groups (in stops[].stopGroups):

{
  "lastModifiedTimestamp" : "2025-10-05T06:21:25.863",
  "stops" : [ {
    "type" : "SERVICE",
    "stopGroups" : [ "a", "b", "c" ],
    "coordinate" : {
      "latitude" : 51.5073,
      "longitude" : -0.1657
    },
    "namedVals" : {
      "x" : 1.0,
      "y" : 2.0
    },
    "_id" : "s1"
  } ],
  "_id" : "j1",
  "objectversion" : "1"
}

11.4.7 User variables accessors

User variables can be get and set in both (a) the sequence based state user functions (SEQSTATE) and (b) the enroute user functions (ENROUTE). SEQSTATE user variables and ENROUTE user variables do not share the same id space - a variable called ‘counter1’ set in SEQSTATE functions will be a different variable to one also called ‘counter1’ set in the ENROUTE functions.

The following functions are available to read the user variables:

  • var(variableName) - gets a user variable, returning 0 if it doesn’t exist yet.
  • varOrDefault(variableName,defaultValue) - gets a user variable, returning the default value if it doesn’t exist yet.
  • varExists(variableName) - returns true if the user variable exists (has been set already).

User variables are set using the setVar and addToVar statements:

{
  "setVar": "userVar1",
  "value": "sqrt(3)"
},
{
  "addToVar": "userVar2",
  "values": [
    "max(1,3)",
    "2"
  ]
}

11.5 Job-vehicle functions (COMBO)

The job-vehicle user functions define when a job-vehicle combination is compatible (i.e. job can be assigned to that vehicle), and can add different costs for different job-vehicle combinations. There’s an example model in the following directory:

supporting-data-for-docs\example-models\user-functions-job-vehicle-type

In the example model, jobs are given a named value called ‘width’:

{
  "namedVals": {
    "width": 1.0
  },
  "stops": [
    {
      "type": "DELIVER",
      "coordinate": {
        "latitude": 51.54792521152047,"longitude": 0.17358325662114982
      },
      "_id": "s0"
    }
  ],
  "_id": "j0"
}

The model contains a single vehicle, with a namedVal ‘maxwidth’ set to 10, and a jobVehicleCombination user function to reject any job whose width is greater than the vehicle’s maxwidth:

{
  "definition": {
    "start": { ... },
    "end": { ...},
    "capacities": [  99999],
    "namedVals": {
      "maxwidth": 10.0
    },
    "userFunctions": {
      "jobVehicleCombination": [
        {
          "addViolationCounts": [
            "jobNamedVal('width')>vehNamedVal('maxwidth')"
          ]
        }
      ]
    }
  },
  "_id": "v0"
}

The formula we use:

jobNamedVal('width')>vehNamedVal('maxwidth')

returns true if the job width is over the vehicle’s maxwidth. As true is equal to 1 in the user functions, this will add 1 to the violation counts if the vehicle and job are incompatible.

As each element in the jobVehicleCombination array is a statement, we could have used more complex if-then-else logic if needed. We could also have setup costs in addition to the violation count by adding an addCosts statement.

11.6 Stops set functions (STOPSET)

The stop set user functions are useful when restricting either (a) the maximum number of stops (potentially of a specific type or types) on a route, or (b) the sum of a property of the stops or jobs. The stops set functions are realtime-compatible and will take account of dispatched stops. Stops set functions are independent of the specific ordering of the stops, they only care what stops are onboard, not their sequence.

Special aggregate user functions are available, for example stopsSum which takes a function as its parameter and sums that function for each stop over the route. e.g. assume each stop has a named value called ‘price’, this function will sum ‘price’ over the route:

stopsSum(stopNamedVal('price'))

Also assume the vehicle has a named value called maxSumPrice, which is the maximum sum of price over the route. The following function will be positive if we go over that limit and negative if we go under:

stopsSum(stopNamedVal('price'))-vehNamedVal('maxSumPrice')

We use this formula in an example model stored in the following directory:

supporting-data-for-docs\example-models\user-functions-unordered-stops-set

In the model, stops in a job are in a stop group which is either A, B or C, and they have a named value called price, which is 0 for stop group A and set randomly in the range 0-5 for all other stops. Here’s an example job:

{
  "stops": [
    {
      "type": "SERVICE",
      "stopGroups": [
        "A"
      ],
      "coordinate": { ... },
      "namedVals": {
        "price": 0.0
      },
      "_id": "s0"
    }
  ],
  "_id": "j0"
}

There is only one vehicle object in the model, which has named values maxStopsA and maxSumPrice defining the limits for (a) number of stops of group A and (b) sum of price over all stops on the route. The vehicle also has two unorderedStopsSet functions which add to the violation counts. These functions separately sum (a) the stop A stops and (b) the price, over the route and subtract the respective limits defined in the vehicle’s named values. If a sum is under the limit, this will result in a negative value, and the addViolationCounts statement just ignores negative values. Here’s the vehicle JSON:

{
  "definition" : {
    "start" : { ... },
    "end" : { ... },
    "namedVals" : {
      "maxStopsA" : 2.0,
      "maxSumPrice" : 8.0
    },
    "userFunctions" : {
      "unorderedStopsSet" : [ {
        "addViolationCounts" : [ "stopsSum(stopNamedVal('price'))-vehNamedVal('maxSumPrice')" ]
      }, {
        "addViolationCounts" : [ "stopsCount(isStopInGroup('A'))-vehNamedVal('maxStopsA')" ]
      } ]
    }
  },
  "_id" : "v"
}

If you run this model, several jobs remain unassigned due to these rules. In the plan (also in this directory) we have a solution where just 2 stop group A stops were assigned (because vehicle.definition.namedVals.maxStopsA is 2) and the sum of price over the route is 7.08 (as close to the limit of 8 as the optimiser could get).

Note in this example price is unnormalised, ideally any violation you return should be normalised such that on average you would return a value of around 1 for each violating stop.

11.6.1 Extra available functions for stops sets type (aggregate functions)

The following aggregate functions are available when executing unordered stop sets (STOPSET) functions. Inside of an aggregate function, stops and jobs property accessors can be used (stop-level property accessors are not available in the aggregate functions that loop over jobs). Vehicle property accessors can be used inside and outside the aggregate functions.

  • Filtered jobs loop - these functions loop over all jobs on the route. They execute the first function (filterFunction) on the job and if it returns true (i.e. > 0), the second function (executeOnJobFunction) is executed, and its average, maximum, minimum or sum is returned. Jobs can have several stops on a route (e.g. a pickup-dropoff job has two stops), however the filterFunction and executeOnJobFunction will only be called once per job.
    • filteredJobsAvg(filterFunction, executeOnJobFunction)
    • filteredJobsMax(filterFunction, executeOnJobFunction)
    • filteredJobsMin(filterFunction, executeOnJobFunction)
    • filteredJobsSum(filterFunction, executeOnJobFunction)
  • Filtered stop loop - these functions loop over all stops on the route (including break stops), executing the second function if the first function returns true, and returning its average, maximum, minimum or sum.
    • filteredStopsAvg(filterFunction, executeOnStopFunction)
    • filteredStopsMax(filterFunction, executeOnStopFunction)
    • filteredStopsMin(filterFunction, executeOnStopFunction)
    • filteredStopsSum(filterFunction, executeOnStopFunction)
  • Unfiltered jobs loop - these are identical to the filtered jobs loop functions, except no filtering is performed.
    • jobsAvg(executeOnJobFunction)
    • jobsCount(filterFunction)
    • jobsMax(executeOnJobFunction)
    • jobsMin(executeOnJobFunction)
    • jobsSum(executeOnJobFunction)
  • Unfiltered stops loop - these are identical to the filtered stops loop functions, except no filtering is performed.
    • stopsAvg(executeOnStopFunction)
    • stopsCount(filterFunction)
    • stopsMax(executeOnStopFunction)
    • stopsMin(executeOnStopFunction)
    • stopsSum(executeOnStopFunction)

If you want to calculate the number of stops or jobs on a route, just use stopsCount(1) or jobsCount(1) respectively.

11.7 Sequence based state functions (SEQSTATE)

The sequence based state user functions loop forward over all stops on a route, and allow you to set and get user variables based on on those stops. This lets you implement more complex logic, putting constraints on the sequences of stops.

11.7.1 Extra available variables

A single extra variable position is available for the SEQSTATE functions. This is the position of the current stop, relative to the start of the route including dispatched stops. User variables can also be get and set in the SEQSTATE functions. Vehicle properties, stop properties and job properties (for the current stop) are also available.

11.7.2 Example model - max stops between a pickup and dropoff

The following directory has an example model using SEQSTATE functions:

supporting-data-for-docs\example-models\user-functions-sequence-based-state\max-stops-between-pickup-and-delivery

The model uses pickup-dropoff stops and sets maximum number of stops between each pickup and its corresponding dropoff. Here’s an example pickup stop from it:

{
  "type": "SHIPMENT_PICKUP",
  "coordinate": {... },
  "userFunctions": {
    "sequenceBasedState": [
      {
        "setVar": "P0",
        "value": "position"
      }
    ]
  },
  "_id": "P0"
}
    

In the pickup stop (with id ‘P0’), userFunctions.sequenceBasedState[0] sets a variable also called ‘P0’ and sets it equal to the position of the stop. The pickup stop’s position in the route is therefore saved in the state variables.

Here’s the corresponding dropoff stop in the job:

{
  "type": "SHIPMENT_DELIVERY",

  "coordinate": { ... },
  "userFunctions": {
    "sequenceBasedState": [
      {
        "addViolationCounts": [
          "position-var('P0')- 4"
        ]
      },
      {
        "addCosts": [
          "0.00000001 * pow(position-var('P0'),2)"
        ]
      }
    ]
  },
  "_id": "D0"
}

As dropoff stops must follow pickup stops, the sequenceBasedState user functions in the dropoff stop will always be executed after those in the pickup stop. The dropoff stop has two user functions. The first function adds position-var('P0')- 4 to the violation count, i.e. adds the position of the dropoff stop, minus the saved position of its pickup stop, minus 4. This will be negative or zero if there’s less than 4 stops between the pickup and the dropoff and will be greater than zero if there’s more. As the executor ignores negative violations counts, this therefore sets a rule that the gap between pickup and dropoff must be 4 or less. Violation counts should be approximately normalised so (roughly speaking) a violation of 1 is equivalent to 1 stop, by returning the excess number of positions we have exactly that.

We also add a cost function (see addCosts) which is set to be very small, and is the square of the number of positions between the pickup and the dropoff. For this type of problem where there’s a max number of positions between pickup and dropoff, we have found a cost like this to be beneficial in guiding the search. Without the cost function, the search can end up in a local minima where other jobs can’t be inserted easily because the dropoff stop has been placed at the maximum separation from its pickup stop.

11.7.3 Example model - FILO

The following directory has an example model using SEQSTATE functions to force FILO (first-in-last-out) ordering in a pickup-dropoff problem:

supporting-data-for-docs\example-models\user-functions-sequence-based-state\filo

A pickup stop saves the ‘depth’ of pickups on the vehicle when it’s served and also increments the depth counter. The job’s corresponding dropoff stop then uses this counter and the saved value in a constraint to ensure FILO ordering is preserved. The dropoff stop then decrements the depth counter afterwards.

11.8 Enroute - after travel functions

Note that in ODL Live version 2.0.4 and later, afterTravel costs have moved to this location:

vehicle.definition.userFunctions.enroute.afterTravel.costs

The new location is in userFunctions but if you set the old location instead, ODL Live will automatically copy the cost functions over to the new location.

The elements in definition.userFunctions.enroute.afterTravel.addCosts define formulae which are executed when the vehicle travels between stops (just after the travel has finished). The results of the formulae are added to the cost, i.e. they define penalty functions. These costs appear in the plan JSON, in plan.statistics.optimiserCostBreakdown.AFTER_TRAVEL_CUSTOM_FORMULA.

11.8.1 Extra available variables and functions with after travel

The following extra variables are available in afterTravel user functions (but not for other types of user function):

  • lastLocationWasStart - true if the last location was the start location.
  • lastKm - km driven to get the the current stop.
  • lastTravelHours - hours travelled to get to the current stop.
  • onboardQuantity0, onboardQuantity1, onboardQuantity2 etc. giving the onboard quantity for each quantity dimension, starting at 0.
  • newLocationIsEnd - true if the location we’ve just arrived at is the end.

Here are some example afterTravel cost formulae using these variables:

  • (lastKm * onboardQuantity0) + (5 * onboardQuantity0)

  • if(lastTravelHours > 0.001 && onboardQuantity0==0, 1000 * lastTravelHours, 0)

  • if(lastLocationWasStart, 100 * lastKm, 0)

Travel is calculated between one stop with a location (the FROM stop) and another (the TO stop). Stop and job property accessors available also available. To access properties of the FROM stop (which is the previous stop with a location, so breaks are not considered here) you can use the following special stop and job property accessors:

lastStopWithLoc.isJobInGroup(groupId)
lastStopWithLoc.isStopInGroup(groupId)
lastStopWithLoc.jobNamedVal(keyInNamedVals)
lastStopWithLoc.jobNamedValExists(keyInNamedVals)
lastStopWithLoc.jobNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent)
lastStopWithLoc.stopNamedVal(keyInNamedVals)
lastStopWithLoc.stopNamedValExists(keyInNamedVals)
lastStopWithLoc.stopNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent)

The function names are just the same as the standard ones, except it’s prefixed with lastStopWithLoc. The normal stop and job property accessors refer to the TO stop:

  • isJobInGroup(groupId)
  • isStopInGroup(groupId)
  • jobNamedVal(keyInNamedVals)
  • jobNamedValExists(keyInNamedVals)
  • jobNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent)
  • stopNamedVal(keyInNamedVals)
  • stopNamedValExists(keyInNamedVals)
  • stopNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent)

The normal vehicle property accessors are also available. User variables can also be get and set in the enroute functions.

11.8.2 Costs dependent on on-board quantity example

You can use the ‘enroute - after travel’ user function type to set costs dependent on the amount of quantity onboard a vehicle, so the optimiser will favour delivering heavy items first. An example model using custom formula is available in the following directory:

supporting-data-for-docs\example-models\custom-formulae-onboard-quantity-dependent-cost

The directory also contains a readme file explaining the model and the results you’ll get from running it. Here’s a vehicle JSON using the formula from this example:

{
  "definition": {
    "start": {
      "type": "START_AT_DEPOT",
      "coordinate": {
        "latitude": 51.536019,"longitude": -0.20274
      },
      "openTime": "2023-01-01T00:00:00",
      "_id": "Start1"
    },
    "end": {
      "type": "RETURN_TO_DEPOT",
      "coordinate": {
        "latitude": 51.536019,"longitude": -0.20274
      },
      "closeTime": "2024-01-01T00:00:00",
      "_id": "End1"
    },
    "capacities": [
      2
    ],
    "userFunctions": {
      "enroute": {
        "afterTravel": {
          "addCosts": [
            "if(lastTravelHours > 0.001 && onboardQuantity0==0, 1000*lastTravelHours, 0)"
          ]
        }
      }
    }
  },
  "_id": "Veh1"
}