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 programs, which have different ‘execution contexts’:

  1. Job-vehicle user programs (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 programs (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 programs (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 programs. 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 programs 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 program 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 Statements, control flow and programs

User functions are defined using formulae similar to Excel, and are embedded within user programs - a json structure allowing if-then-else type and forEach logic, where each node in the json structure is a statement object. Calls to user function are embedded within statement objects. Arrays of statements can be defined on vehicles, jobs, stops and globally. You can define callable programs, i.e. an array of statements that can be called by other statements. Callable programs are defined in model.configuration.problem.userFunctions.programs.

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"
}

An add costs statement 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')"
  ]
}

You can also use formulae instead of addCosts statements to add costs.

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 a statement

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.

You can also use formulae instead of addViolationCounts statements to add violations.

11.2.2 Combining multiple statements together with execution logic

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:

  • Control statements

    • 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.
    • init - {"init" : "idOfInit1", "do" : [...]}

      • init is only supported for SEQSTATE and ENROUTE contexts. It’s used to do a one-off initialisation of variables when the execution engine starts parsing a route.
      • The field init must be set to a unique id for the initialiser (which then gets saved as a boolean equal to 1 int the user variables after the init has been run.
      • init has an array do which contains a list of statements which will be run once.
    • runFuncs - { "runFuncs":[.....] }

      • runFuncs runs all the functions in its array.
      • e.g. { "runFuncs":["println('hello)", "println('world)"] } will print ‘hello’ and ’world on two different lines to the output.
    • runPrograms - { "runPrograms":[.....] }

      • Runs all the programs whose ids are stored as strings in the array.
    • forEach - for each can be used to loop over a set of objects. See later section for details.

    • break - {"break" : true}.

      • break can be used to break out of a forEach loop early. You must set break equal to true for it to work.
  • Cost and violation statements

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

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

  • User variable statements. The user variable statements are only supported for SEQSTATE and ENROUTE contexts.

    • 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.2.3 Defining programs

Rather than repeating the same JSON for each stop, job or vehicles, statements can be turned into programs and stored in:

model.configuration.problem.userFunctions.programs

In the JSON below we see a model where two programs are defined, with ids program1 and program2. The statements inside their statements arrays will be run when the respective programs are called:

{
  "data": {},
  "configuration": {
    "problem": {
      "userFunctions": {
        "programs": {
          "program1": {
            "description": "This program does something",
            "statements": [ ... ]
          },
          "program2": {
            "description": "This program does something else",
            "statements": [ ... ]
          }
        }
      }
    }
  }
}

Here we see a simple job object which has user functions defined for SEQSTATE in its stop, and will run the programs program1 and program2 when the stop is processed:

{
  "stops": [
    {
      "type": "SERVICE",
      "coordinate": { ... },
      "userFunctions": {
        "sequenceBasedState": [
          {
            "runPrograms": [
              "program1",
              "program2"
            ]
          }
        ]
      },
      "_id": "j4"
    }
  ],
  "_id": "j4"
}

Here are the same programs but called by a vehicle instead:

{
  "definition": {
    "start": {},
    "end": {},
    "userFunctions": {
      "sequenceBasedState": [
        {
          "runPrograms": [
            "program1",
            "program2"
          ]
        }
      ]
    }
  },
  "_id": "v1"
}

The example container optimisation model uses programs to avoid code duplication.

There are some rules to follow with programs:

  1. Never create an infinitely recursive loop by calling a program from itself (either directly on indirectly). ODL Live should throw an error if you do this.

  2. All variables - tmp, user and objects - have global scope (although the tmp variables scope doesn’t form part of the parse state, so isn’t available across stops). Variables defined in one program will be available from another.

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 Objects and foreach loops

In user functions, and object is basically a single level key-value map where the keys are strings and the values can be strings, real-valued numbers (doubles) or integers. Internally to ODL Live, all values are actually stored as doubles - strings are replaced by an id referencing the string, and integers are stored as doubles, but are automatically rounded when written to.

To define an object, you must first define the type of that object, which is stored in:

model.configuration.problem.userFunctions.programs.objectTypes

The example container optimisation model defines multiple object types:

{
  "data": {},
  "configuration": {
    "problem": {
      "userFunctions": {
        "objectTypes": [
          {
            "name": "container",
            "description": "Definition of a container. These are defined in the vehicle object and then get copied into the route's state, to be updated along the route.",
            "fields": [
              {
                "name": "fillOrderImportance", "description": "...", "type": "DOUBLE", "initialValue": 0.0
              },
              {
                "name": "capacity", "type": "INTEGER", "initialValue": 0.0
              },
              {
                "name": "free", "description": "...", "type": "INTEGER"
              },
              {
                "name": "containsProductType", "description": "...", "type": "STRING", "initialValue": -1.0
              }
            ]
          },
          {
            "name": "product",
            "description": "...",
            "fields": [
              {
                "name": "quantity", "type": "INTEGER", "initialValue": 0.0
              }
            ]
          }
        ]
      }
    }
  }
}

Each element in the array objectTypes defines an object type. An object type definition has the following fields:

  1. name - name/id of the object type.
  2. description - optional description of the objec type (ODL Live doesn’t read this, the description is only there to make the code more readable).
  3. fields - array of all fields on the object.

Each field definition in a type can have the following fields:

  1. name
  2. description - optional description of the field.
  3. type - this should be set to "DOUBLE", "INTEGER" or "STRING".
  4. initialValue - optional initial value of the field.

Read-only objects of a type can be defined on (a) jobs, (b) stops, (c) vehicles and (d) globally in:

model.configuration.problem.userFunctions.userObjs

Writable versions of objects can be saved in the parse state - the state which is created and maintained when ODL Live parses the route for SEQSTATE and ENROUTE contexts.

Here’s a job from the container optimisation example where 2 objects of type product are defined:

{
  "userFunctions": {
    "userObjs": {
      "byType": {
        "product": {
          "bio": {
            "quantity": 2350.0
          },
          "diesel": {
            "quantity": 1278.0
          }
        }
      }
    }
  },
  "stops": [ .... ],
  "_id": "j2"
}

Each object has an id, these two objects have the ids bio and diesel. The quantity field sets their quantity value. Wherever the userFunctions data structure appears in a model, you can define objects in userFunctions.userObjs. The object’s type must be defined first in model.configuration.problem.userFunctions.programs.objectTypes. For a type called ‘typeA’, its objects must be defined in the map userFunctions.userObjs.byType.typeA. Here was see an example vehicle object from the container optimisation problem where it has three container objects - C1, C2 and C3:

{
  "definition": {
    "start": {},
    "end": {},
    "userFunctions": {
      "userObjs": {
        "byType": {
          "container": {
            "C1": {
              "capacity": 1000.0, "fillOrderImportance": 0
            },
            "C2": {
              "capacity": 1000.0, "fillOrderImportance": 0
            },
            "C3": {
              "capacity": 1000.0, "fillOrderImportance": 0
            }
          }
        }
      }
    }
  },
  "_id": "v1"
}

The section object functions details which functions are available to read from objects, and write to state-based objects.

forEach statements can loop over a set of objects, allowing you to implement your own complex logic in a user program. Here’s a forEach loop (defined in a program) which is used in the container optimisation example:

{
  "description": "....",
  "statements": [
    {
      "forEach": {
        "description": "Loop over all products on the job which the pickup stop belongs to.",
        "objIdVarName": "p",
        "objsSource": "job",
        "objsType": "product",
        "do": [
          {
            "if": [ "not(objExists('product',p))" ],
            "then": [
              {
                "runFuncs": [ "createObj('product',p)" ]
              }
            ]
          },
          {
            "runFuncs": [...]
          }
        ]
      }
    },
    {
      "runPrograms": [... ]
    }
  ]
}

This is called from each pickup stop. Stops are owned by jobs, jobs can have product objects on them, and this forEach loops over all products on a job. The statements array for this program contains two statements - (1) the forEach statement and (2) a runPrograms statement.

A forEach statement has a field called forEach which defines a data structure holding the forEach logic. So a forEach statement has the JSON structure:

{
    "forEach":{
        ... for each logic defined here
    }
}

Inside the forEach the following fields are available:

  • description - optional description.
  • objsSource - the source of the objects we’re loop over, valid values are the read-only object sources "job", "stop", "veh", "problem", or leave blank to loop over objects in the parse state.
  • objsType, the type of the object, e.g. "product" for the product type defined above.
  • objIdVarName, a variable name, e.g. "p", which holds the id of the current object we’re looping over.
  • where - a set of functions to filter the objects we’re looping over.
  • orderBy - a set of functions to order the objects we’re looping object.
  • limit - an integer number defining the maximum number of objects to be looped over, e.g. set "limit":1 if you only want to loop over the first object.
  • do - an array of statements to execute within the forEach loop.
  • restartLoopEachTime - this allows you to continually select the first object returned in the loop (according to orderBy), restarting the loop from the beginning once this first object is processed, so it can be processed again if needed. This is useful if you need to process a set of objects and some objects might need processing more than once, according to some criteria.
  • maxRestartEachTimeLoopsUntilException - if restartLoopEachTime does not modify the objects it’s looping over, such that they’re not deleted or their values are modified so they won’t be selected by the where functions, you can get an infinite loop. As an infinite loop would lock ODL Live up, impacting all models on the system (not just the model running the loop), ODL Live throws an error if the restartLoopEachTime loop does more than maxRestartEachTimeLoopsUntilException loops. If you don’t set maxRestartEachTimeLoopsUntilException the default value is 64. The maximum allowed value is 1000, if you set maxRestartEachTimeLoopsUntilException to 1001, ODL will still use the limit 1000 internally.

The value of objIdVarName is important as you can refer to it within the forEach loop in the where functions, the orderBy functions and all statements within the do array. Here’s another example from the container optimisation example, where we initialise the parse route state by copying over the container objects stored on the vehicle object:

"forEach" : {
  "objIdVarName" : "c",
  "objsSource" : "veh",
  "objsType" : "container",
  "do" : [ {
    "runFuncs" : [
      "copyVehObjIntoState('container',c)",
      "setObjVar('container',c,'free',c.capacity)",
      "addToTmpVar('sumCap',c.capacity)"
    ]
  } ]
}

In this example, objIdVarName is c and we use this variable within the do statements - e.g. the function copyVehObjIntoState('container',c) which copies the container with id c from the vehicle object into the parse state. We can also refer to fields of the container object c stored in the vehicle by calling c.capacity, c.fillOrderImportance etc.

Here’s another example from the container optimisation model where we loop over all product type objects in the route’s parse state (i.e. the modifiable state). We don’t set objsSource because if objsSource is left blank, the source is the parse state. objIdVarName is pp and we print the current quantity of the product pp.quantity.

"forEach" : {
  "objIdVarName" : "pp",
  "objsType" : "product",
  "do" : [ {
    "runFuncs" : [
      "print(pp,'=',pp.quantity,' ')"
    ]
  } ]
}

Here’s a more complex example from the container optimisation model, where products which have been loaded but not yet assigned to containers are then assigned to containers. One product might have to be split across several containers, so we continually loop over the products in the parse state, setting restartLoopEachTime=true so the loop continually restarts and selecting products where p.quantity>0.

{
  "forEach": {
    "objIdVarName": "p",
    "objsType": "product",
    "where": [ "p.quantity>0 ],
    "orderBy": [ "-(p.quantity) ],
    "do": [...],
    "restartLoopEachTime": true,
    "maxRestartEachTimeLoopsUntilException": 1000
  }
}

When a product has been totally assigned to containers, it will have quantity=0 so it will no longer be selected by the loop (internally to the do statements, we also delete the product object in the parse state when it reaches quantity=0, so there’s no chance of it being selected).

The orderBy logic orders by smallest value first, so by sorting by -(p.quantity) we’re actually sorting by the product with the largest unassigned quantity first, not the smallest.

Both where and orderBy can have multiple functions, as we see in the following example which loops over containers in the parse state. If we have multiple where functions, each function must return a value >0 for the object to be selected. If we have multiple orderBy functions, we sort by the 1st function, then for cases where the first function is equal between 2 objects, we sort by the 2nd function and so-on, e.g. lexicographic order. The following example shows multiple where and orderBy functions in a forEach:

{
  "forEach": {
    "objIdVarName": "c",
    "objsType": "container",
    "where": [
      "(c.containsProductType==-1 ) || (c.containsProductType==p)",
      "c.free > 0"
    ],
    "orderBy": [
      "c.containsProductType!=p",
      "-c.fillOrderImportance",
      "-c.free"
    ],
    "do": [ ...],
    "limit": 1
  }
}

We also set limit:1 in this forEach so it only selects the first passing container.

11.5 Functions - operators, consts and math

11.5.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.5.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.5.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.6 Functions - vehicle, job and stop properties

11.6.1 Vehicle property accessors

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

  • isVehicleInGroup(groupId) - Returns true (1) is the vehicle is in the vehicle group, otherwise false (0).
  • vehNamedString(keyInNamedStrings) - Gets a named string from the key-value object vehicle.definition.namedStrings.
  • vehNamedStringExists(keyInNamedStrings) - Returns true if the named string exists in the key-value object vehicle.definition.namedStrings.
  • vehNamedStringOrDefault(keyInNamedStrings, defaultValueIfNotPresent) - Gets a named string from the key-value object vehicle.definition.namedStrings returning the default value set in the parameter if it doesn’t.
  • vehNamedVal(keyInNamedVals) - Gets a named value from the key-value object vehicle.definition.namedVals.
  • vehNamedValExists(keyInNamedVals) - Returns true if the named value exists in the key-value object vehicle.definition.namedVals.
  • vehNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent) - Gets a named value from the key-value object vehicle.definition.namedVals returning the default value set in the parameter if it doesn’t.

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

{
  "definition": {
    "start": {},
    "end": {},
    "namedVals": {
      "height": 2.5
    },
    "namedStrings": {
      "vehType": "tail-lift"
    },
    "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.6.2 Job property accessors

These functions access properties on the job objects:

  • isJobInGroup(groupId) - check if job is an a specific job group.
  • jobNamedString(keyInNamedStrings) - Gets a named string from the key-value object job.namedStrings.
  • jobNamedStringExists(keyInNamedStrings) - Returns true if the named string exists in the key-value object job.namedStrings.
  • jobNamedStringOrDefault(keyInNamedStrings, defaultValueIfNotPresent) - Gets a named string from the key-value object job.namedStrings returning the default value set in the parameter if it doesn’t.
  • jobNamedVal(keyInNamedVals) - Gets a named value from the key-value object job.namedVals.
  • jobNamedValExists(keyInNamedVals) - Returns true if the named value exists in the key-value object job.namedVals.
  • jobNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent) - Gets a named value from the key-value object job.namedVals returning the default value set in the parameter if it doesn’t.

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.6.3 Stop property accessors

These functions access properties on the stop objects:

  • Stop named string and value functions
    • stopNamedString(keyInNamedStrings) - Gets a named string from the key-value object job.stop[].namedStrings.
    • stopNamedStringExists(keyInNamedStrings) - Returns true if the named string exists in the key-value object job.stop[].namedStrings.
    • stopNamedStringOrDefault(keyInNamedStrings, defaultValueIfNotPresent) - Gets a named string from the key-value object job.stop[].namedStrings returning the default value set in the parameter if it doesn’t.
    • stopNamedVal(keyInNamedVals) - Gets a named value from the key-value object job.stop[].namedVals.
    • stopNamedValExists(keyInNamedVals) - Returns true if the named value exists in the key-value object job.stop[].namedVals.
    • stopNamedValOrDefault(keyInNamedVals, defaultValueIfNotPresent) - Gets a named value from the key-value object job.stop[].namedVals returning the default value set in the parameter if it doesn’t.
  • Stop type functions
    • isCustomStop() - Returns true (value 1) if stop is of type CUSTOM, otherwise false (value 0).
    • isDeliverStop() - Returns true (value 1) if stop is of type DELIVER, otherwise false (value 0).
    • isPickupStop() - Returns true (value 1) if stop is of type PICKUP, otherwise false (value 0).
    • isServiceStop() - Returns true (value 1) if stop is of type SERVICE, otherwise false (value 0).
    • isShipmentDeliveryStop() - Returns true (value 1) if stop is of type SHIPMENT_DELIVERY, otherwise false (value 0).
    • isShipmentPickupStop() - Returns true (value 1) if stop is of type SHIPMENT_PICKUP, otherwise false (value 0).
  • Misc. stop functions
    • isStopInGroup(groupId) - Returns true (1) is the stop is in the stop group, otherwise false (0).
    • stopChangeInOnboardJobs() - Returns +1 if the stop increases the number of jobs onboard the vehicle, -1 if it decreases them, otherwise 0.
    • stopChangeInOnboardQuantity(zeroBasedQuantityIndex) - Gets the change in quantity onboard the vehicle from servicing the stop, for the quantity dimension in the function parameter. Assuming the item in quest has non-zero quantity, a delivery would be a negative value and a pickup would be positive.
    • stopLocationId() - Returns an integer id which is identical for all stops with precisely the same latitude, longitude and road name.
  • Stop properties only available in SEQSTATE and ENROUTE functions. COMBO and STOPSET functions act only upon the jobs assigned to a route (in model.data.jobs). The following functions are primarily designed for inspecting preloaded stops on a route (defined in vehicle.definition.preloadedStops), or replenishes added to a route. These types of stops are not readable from COMBO and STOPSET functions.
    • isNoLocationReplenishStop() - Returns true (value=1) if the stop is a no-location replenish, otherwise returns false (value 0).
    • isNoLocationStop() - Returns true (value=1) if the stop doesn’t have a location (excluding no-location route start and ends), otherwise returns false (value 0).
    • isPreloadedBreakStop() - Returns true (value 1) if stop is of type BREAK, otherwise false (value 0).
    • isPreloadedStop() - Returns true (value=1) if the stop is a preloaded stop, otherwise returns false (value 0).
    • isPreloadedStopWithLocation() - Returns true (value=1) if the stop is a preloaded stop with a location, otherwise returns false (value 0).
    • isReplenishStop() - Returns true (value 1) if stop is of type REPLENISH, otherwise false (value 0).
    • isStop4Job() - Returns true (value=1) if the stop belongs to a job in model.data.jobs (so excluding preloaded stops, vehicle start/end, and replenish stops), otherwise returns false (value 0).

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"
}

11.7 Functions - tmp, user and object variables

11.7.1 Temp variables

Temp variables only exist within a single array of statements that are executed (i.e. their scope is the array), as well as within programs that array might call. So if you’re using SEQSTATE (for example) and you include SEQSTATE statements in both job.stop[].userFunctions.sequenceBasedState andvehicle.definition.userFunctions.sequenceBasedState, a temp variable defined in the vehicle SEQSTATE statements will not be readable in the stop SEQSTATE statements and vice-versa. Also when ODL Live is parsing a route, temp variables declared on one stop will not be readable on the next stop. Temp variables are supported for all contexts, not just for SEQSTATE and ENROUTE. A temp variable defined within the then of an if-then statement will also be available outside of the then.

The following functions get and set temp variables:

  • addToTmpVar(variableId, value) - Adds to a temp variable.
  • getTmpVar(variableId) - Gets a temp variable, returning 0 if it doesn’t exist yet.
  • getTmpVarExists(variableId) - Returns true if the temp variable exists (has been set already).
  • getTmpVarOrDefault(variableId, defaultValue) - Gets a temp variable, returning the default value if it doesn’t exist yet.
  • setTmpVar(variableId, value) - Sets a temp variable.
  • setTmpVarType(variableId, type) - Sets the type of the tmp variable. Allowed types are ‘DOUBLE’, ‘INTEGER’, ‘STRING’. If you set to type INTEGER, the field value will be rounded automatically and always contain a whole number only.
  • tmpVar(variableId) - Gets a temp variable, returning 0 if it doesn’t exist yet.
  • tmpVarExists(variableId) - Returns true if the temp variable exists (has been set already).
  • tmpVarOrDefault(variableId, defaultValue) - Gets a temp variable, returning the default value if it doesn’t exist yet.

Once a temp variable has been declared by calling either addToTmpVar or setTmpVar, you can also get its value by using the variable name directly in the code. The single example statement below runs two functions, the first declares the temp variable called tmpVar1 and the second reads the value of tmpVar1 just using its name, and adds this to the cost:

{
  "runFuncs": [
    "setTmpVar('tmpVar1', 7)",
    "addCost(tmpVar1)"
  ]
}

So for the temp variable tmpVar1, calling tmpVar1 is equivalent to calling getTmpVar('tmpVar1').

11.7.2 User variables

User variables exist in the parse state, and have global scope - they can be read/written to from different stops and from different arrays of statements. They are only supported for SEQSTATE and ENROUTE contexts. They are created when the route is parsed from start to finish, so a later stop can see a user variable defined on an earlier stop but not the other way round. SEQSTATE cannot see ENROUTE user variables though, and vice-versa.

The following functions get and set user variables:

  • addToVar(variableId, value) - Adds to a user variable.
  • getVar(variableId) - Gets a user variable, returning 0 if it doesn’t exist yet.
  • getVarExists(variableId) - Returns true if the user variable exists (has been set already).
  • getVarOrDefault(variableId, defaultValue) - Gets a user variable, returning the default value if it doesn’t exist yet.
  • setVar(variableId, value) - Sets a user variable.
  • var(variableId) - Gets a user variable, returning 0 if it doesn’t exist yet.
  • varExists(variableId) - Returns true if the user variable exists (has been set already).
  • varOrDefault(variableId, defaultValue) - Gets a user variable, returning the default value if it doesn’t exist yet.

User variables can also be set using the setVar and addToVar statements:

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

11.7.3 Object functions

Objects, like user variables, also exist in the parse state, and have the same scope rules as user variables. The only exception compared to user variables is that read-only objects declared in stops, jobs, vehicles and the problem can be read by COMBO and STOPSET execution contexts, but these contexts cannot create or write to objects because there is no parse-state in their execution context to write to. The section on objects shows how to define objects in stops, jobs, vehicles and the problem.

The following functions are available:

  • Reading objects in the parse state:
    • getObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • getObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • getObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
    • objExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • objVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • objVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
  • Writing to objects in the parse state:
    • addToObjVar(type, objectId, fieldId, value) - Adds to the value of the field on the object with the input type and input id. Creates the object if it doesn’t already exist. Returns the old value associated with the field.
    • copyJobObjIntoState(type, objectId) - Copies an object of the type and id from a job into the enroute or sequence-based state.
    • copyProblemObjIntoState(type, objectId) - Copies an object of the type and id from a problem into the enroute or sequence-based state.
    • copyStopObjIntoState(type, objectId) - Copies an object of the type and id from a stop into the enroute or sequence-based state.
    • copyVehObjIntoState(type, objectId) - Copies an object of the type and id from a veh into the enroute or sequence-based state.
    • createObj(type, objectId) - Creates an object of the input type within the user variables space
    • createObj(type, objectId, value…) - Creates an object of the input type within the user variables space. Extra parameters can be added, which set the fields in order they’re defined in.
    • deleteObj(type, objectId) - Deletes an object of the input type from the user variables space
    • setObjVar(type, objectId, fieldId, value) - Set the value of the field on the object with the input type and input id. Creates the object if it doesn’t already exist. Returns the old value associated with the field.
  • Reading objects defined in the job:
    • getJobObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • getJobObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • getJobObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
    • jobObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • jobObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • jobObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
  • Reading objects defined in the problem:
    • getProblemObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • getProblemObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • getProblemObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
    • problemObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • problemObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • problemObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
  • Reading objects defined in the stop:
    • getStopObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • getStopObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • getStopObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
    • stopObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • stopObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • stopObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
  • Reading objects defined in the vehicle:
    • getVehObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • getVehObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • getVehObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.
    • vehObjExists(type, objectId) - Returns true (1) if an object of the input type with the input id exists in the user variables space
    • vehObjVar(type, objectId, fieldId) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, default value for that type is returned.
    • vehObjVarOrDefault(type, objectId, fieldId, defaultValue) - Returns a value from object of input type, input id, using the input field id. If object doesn’t exist, the default value from the input parameter is returned.

In the example container optimisation model we use some of these functions (together with an init and forEach statement) to copy the container objects defined on the vehicle object into the parse state and to set the free field on each container in the parse state. As the containers objects in the parse state can be written to, they are used to track the contents and fill value of each container:

{
  "init": "initVeh",
  "do": [
    {
      "forEach": {
        "description": "This loops over all the containers stored in the vehicle object and copies them into the route's state. The variable c refers to the container's id.",
        "objIdVarName": "c",
        "objsSource": "veh",
        "objsType": "container",
        "do": [
          {
            "runFuncs": [
              "copyVehObjIntoState('container',c)",
              "setObjVar('container',c,'free',c.capacity)"
            ]
          }
        ]
      }
    }
  ]
}

11.8 Functions - add cost or violation

Functions are also available to add cost or violation (instead of using the addCosts and addViolationCounts statements):

  • addCost(costValue) - Add a cost to the solution cost.
  • addViolationCount(violationCount) - Report a hard constraint violation.

Here we see a single runFuncs statement which adds both a cost and a violation:

{
  "runFuncs": [
    "addCost(1)"
    "addViolationCount(2)",
  ]
}

11.9 Functions - print and reporting

The printing and reporting functions are currently only available for SEQSTATE. The following functions are only available:

  • isPrintAvailable() - Returns 1 if printing to output is available, otherwise 0.
  • print(values2Print…) - Print the values to the function output shown in the plan. print is only called when outputting to the plan, not when optimising, so assuming print output is small (less than a few MB) print has minimal impact on optimisation speed.
  • println(values2Print…) - Print the values to the function output shown in the plan. Add a new line after printing. println is only called when outputting to the plan, not when optimising, so assuming print output is small (less than a few MB) print has minimal impact on optimisation speed.
  • saveUserDataSnapshot(String with description of snapshot) - Save a snapshot of the user data state which will appear in the data model plan under plannedStops. Snapshot is not called during optimisation, only when exporting the plan at the end.

For each vehicle plan, the output of print appears in the string in:

plan.userFunctionOutput.sequenceBasedStatePrint

The output from parsing all stops is combined into this single string.

The parse state user variables and objects after each stop is processed are stored in plan.plannedStops[].sequenceBasedStateUserVarsAfter and plan.plannedStops[].sequenceBasedStateUserObjsAfter. If you’re doing some complex processing and need to output the objects state at a different point (e.g. halfway through processing a stop), you can call the function saveUserDataSnapshot and the state at that point will be stored in:

plan.plannedStops[].ssequenceBasedStateUserDataSnapshots

Print and saving the user state are not called when the optimisation algorithm is running, they are only called when the optimisation algorithm exports the route after optimising. As a result, they do not normally cause much processing overhead. However if you have complex logic, that is only used to called print statements (e.g. print statements within a forEach loop, where print is the only thing the forEach loop does), you are advised to wrap the logic in an if-then statement, like this to minimise any overhead:

{
  "if": [
    "isPrintAvailable()"
  ],
  "then": [ .... ]
}

11.10 Context - 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.11 Context - stops set (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.11.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.12 Context - sequence based state (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.

Sequence based state functions saved in vehicle.definition.userFunctions.sequenceBasedState are not executed for the vehicle end stop (e.g. the return to depot, defined in vehicle.definition.end). Only functions saved in vehicle.definition.end.userFunctions.sequenceBasedState are executed for the vehicle.definition.end stop.

In the output plan for a vehicle, the parse state user variables and objects after each stop is processed are stored in plan.plannedStops[].sequenceBasedStateUserVarsAfter and plan.plannedStops[].sequenceBasedStateUserObjsAfter.

11.12.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.12.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.12.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.12.4 Example model - container optimisation

The following directory has an example model which uses SEQSTATE to optimise fuel transport where each vehicle has multiple containers:

supporting-data-for-docs\example-models\container-optimisation

Each job has one or more fuel products which must be transported from the depot to the customer (e.g. ‘bio’, ‘diesel’). Containers can only contain one type of fuel at a type, e.g. ‘bio’ and ‘diesel’ fuels cannot be placed in the same container, unless the container has been filled, completely emptied with one and then filled with another.

11.13 Context - enroute - after travel

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.

Unlike SEQSTATE functions, enroute after travel functions are also executed for the end stop (in vehicle.definition.end). If you wish to filter out the end stop, use the function isStop4Job() in your formulae to check the stop isn’t vehicle.definition.end.

After travel functions will not be executed for stops without a location.

11.13.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.13.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"
}