Skip to content

Concepts

Drivers, Vehicles, and Theoretical Runs

The Driver is a very flexible concept in Tarot Routing.

It can be used to represent:

  • an actual (human, for now) Driver, or
  • a vehicle, or
  • a predetermined theoretical run, or
  • several of the above at the same time.

It is used to represent the Human Driver when:

  1. the Driver owns their vehicle, and comes to work in it
  2. the Driver has restrictions on the hours they can work, or pauses they must take

It is used to represent a Vehicle when

  1. the Capacity of the vehicle is important
  2. the Type of the vehicle is important
  3. different Drivers drive the same vehicle

It is used to represent a Theoretical Run when:

  1. Jobs are pre-allocated to delivery zones
  2. you will decide later which human driver will drive this Run

A Driver can work one shift in a single-day request, or several shifts across a planning horizon. See Multi-Shift.

ETA, ETD, and Duration

ETA: Estimated Time of Arrival at a Job — when the Driver reaches the stop, before parking.

ETD: Estimated Time of Departure after the on-site stop (parking, if applied, plus service).

Duration: The time the Driver expects to spend performing a Job after they are already on site. Often called the service time — loading, unloading, walking to/from the vehicle, signing paperwork. It is applied once per Job.

Parking Duration: The time spent parking or docking after arriving at a location and before finishing service. If driving time from the previous stop — including the Driver's start — is zero, parking is waived. duration is still applied on every Job.

The job.duration and optional job.parking_duration are set by you, and taken into account by the optimisation algorithm.

The algorithm then sets the job.eta during the optimisation, based on the optimised route it calculates. ETA is arrival at the Job — before parking.

The job.etd is job.etd = job.eta + parking + duration when parking applies at that stop, and job.eta + duration when it is waived. Parking and service are one contiguous on-site block.

Optimisation Objective

The optimisation objective controls what the algorithm tries to minimise. You can set this via settings.optimisation_objective.

Time (default)

optimisation_objective = "time" minimises total driving time across all drivers. This is the default and works well when driver time is your primary concern.

Distance

optimisation_objective = "distance" minimises total distance travelled. Use this when fuel costs or vehicle wear are more important than driver time.

Cost

optimisation_objective = "cost" minimises total cost based on driver-specific cost coefficients. This is the most flexible option, allowing you to model complex cost structures.

When using cost optimisation, you provide each driver with their own cost rates:

  • Per-km cost: Cost per kilometre driven (e.g. fuel, vehicle depreciation)
  • Per-hour cost: Cost per hour worked, including driving, service time, and waiting
  • Per-job cost: Cost for each job performed (e.g. handling costs)
  • Per-run cost: Fixed cost if this driver is active at all (e.g. minimum wage guarantee, truck rental)

The optimiser will then find routes that minimise the total cost across all drivers.

When to use cost optimisation

  • When different drivers have different hourly rates
  • When you want to minimise the number of active drivers (set high per-run costs)
  • When distance costs (fuel) and time costs (wages) need to be balanced
  • When you want to favour certain drivers over others (set lower per-run costs for preferred drivers)

Note

Regardless of which objective you choose, time windows and driver shifts always apply as hard constraints on actual time. The objective only affects what the algorithm optimises for.


Optimisation Time

How long does an optimisation run for?

It Depends

Every optimisation is stopped by one of the following Stopping Criteria:

  1. The time_limit is reached
  2. The unimproved_time_limit is reached
  3. The iter_limit is reached
  4. The unimproved_iter_limit is reached

The first criterion met will stop the algorithm

Time-based stopping criteria

time_limit It's a time limit (in seconds) limiting how long the algorithm can optimise for.

unimproved_time_limit This is a very common way to determine when an algorithm should stop calculating. Concretely, we're talking about improvements in the Objective Function, which is a function that represents how good (or bad) each possible solution is.

It is based on the premise that algorithms make constant improvements when they first begin running, and those improvements become more infrequent as the algorithm gets closer to the optimal solution. In general this is true, but in cases where there are a lot of constraints (i.e. the Objective Function is less smooth), it's not uncommon to see no improvement for a long time, followed by a large improvement.

Note

You may see that the Routing API responds in a little longer than the time_limit.

This is because:

  1. time_limit restricts the amount of time it spends optimising only.
  2. The first thing the algorithm does is retrieve a Distance Matrix. This can take a few seconds for large optimisations. This time is not counted towards the time_limit.
  3. It can take time to send large requests and responses over the network, which is also not counted towards the time_limit

Iteration-based stopping criteria

The algorithm runs in iterations.

Each iteration, the algorithm tries several local-search operators to see if it can improve the solution.

In general:

  • Problems with fewer jobs iterate faster (e.g. 30 jobs might be 150 iterations per second)
  • Problems with more jobs iterate slower (e.g. 400 jobs might be 2 iterations per second)

Constraints impact iteration time too:

  • The solver will iterate faster on:
  • problems with "pruning constraints" (e.g. types, territories)
  • Problems with far fewer jobs than the vehicle capacity (shift length or size/capacity)
  • The solver will iterate slower on:
  • problems where vehicles' shifts are mostly full, or there are too many jobs to serve in the shifts
  • problems where vehicles' capcities are mostly full, or there are is too mauch demand for the vehicle capacity
  • problems with non-pruning constraints (soft constraints, sequence constraints, priority constraints)

iter_limit Sets a maximum number of iterations allowed.

unimproved_iter_limit Sets the maximum number of iterations the optimiser is allowed to continue without any improvement between iterations.

Guidelines for setting stopping criteria

Some Guidelines

In general we find that, for RoutingProblems with very basic constraints, we get pretty good solutions in the following timeframes:

nodes time (s)
10 1
30 5
50 10
80 20
100 30
150 45
200 60
300 120
1000 600

Note

n_locs in the following section refers to the number of jobs plus twice the number of drivers (representing vehicle start and end nodes).

Note

We recommend setting both the unimproved_time_limit and the unimproved_iter_limit because it's very difficult to know in advance if your combination of constraints, jobs and drivers will lead to a problem that iterates quickly or iterates slowly.

For problems that iterate slowly, the unimproved_time_limit ensures that they don't run for an unnecessarily long time

For problems that iterate quickly, the unimproved_iter_limit ensures that they end quickly after finding an optimal solution.

Good Solution Fast If you want good solutions (usually within 1%-2% of the best possible) quickly, we recommend to set:

  • unimproved_time_limit = n_locs / 20 and
  • unimproved_iter_limit = 25
{
   // e.g. for 20 Drivers and 180 Jobs (2 * 20 + 180 = 220 total locations)
   "unimproved_time_limit": 11,  // 220 / 20
   "unimproved_iter_limit": 25

}

Optimal Solutions (slower) If you want very close to optimal solutions (within 0% - 1% of best possible), we recommend to set:

  • unimproved_time_limit = n_locs / 7 and
  • unimproved_iter_limit = 100
{
   // e.g. for 20 Drivers and 180 Jobs (2 * 20 + 180 = 220 total locations)
   "unimproved_time_limit": 31,  // 220 / 7 (rounded up from ~31.4)
   "unimproved_iter_limit": 100

}

Driver Start & End Location

Driver start and end fields set where the driver begins and ends their route.

Driver Start

Sets the Location where the driver starts their route. This field is required.

Driver End

Can be used to set the driver to:

  • End at the Start
  • End at a specific defined location
  • End anywhere (i.e. at the last job)

This field is optional, and defaults to End at Start

In a multi-shift request these locations stay on the Driver, not on each Shift. What they mean depends on the mode:

  • reset: every Shift starts at start and respects end.
  • continue: only the first Shift starts at start. The Driver stays where they are between Shifts, and end applies to the final return after the last Job.

Multi-Shift

A normal request has one Shift per Driver: shift_start and shift_end on a single day.

When you need several days — or several Shifts in one day — use multi-shift. You still send the same /v2/vrp request. The difference is that every Driver replaces shift_start / shift_end with a multi_shift object.

There is no separate endpoint. If any Driver has multi_shift, every Driver must have it.

Planning Days

Times in a multi-shift request are a planning time: a day number plus a clock time, for example { "day": 1, "time": 8 } or { "day": 1, "time": "08:00" }.

Day 1 is the first day of this request, not a calendar date. Each Planning Day is exactly 24 hours. If you also set settings.traffic_date, that calendar date is Planning Day 1 and later days follow it for traffic.

A Shift can start on one day and end on the next. Work stops at shift_end. The Driver does not keep serving or driving after a Shift ends, even if the next Shift is the following morning.

Reset or Continue

Each Driver chooses a mode inside multi_shift.

reset (default) — each Shift is a new trip. The Driver starts every Shift at start and must respect end every Shift. Nothing is carried overnight: capacity is empty again at the start of the next Shift, and a pickup/delivery pair must finish in the same Shift. Use this for last-mile depot work: Monday to Friday, out in the morning, back in the evening.

continue — one journey across all of that Driver's Shifts. The Driver starts at start on the first Shift, stays where they are between Shifts (at the last Job, at the next Job if they already arrived, or partway along a drive), and only has to reach end at the end of the whole journey. Parcels stay on the vehicle. Use this for long-distance trips with overnight rest.

The same request can mix the two modes: one Driver on reset and another on continue.

How to put this in the request — including where the multi_shift object goes on Drivers and on Jobs — is in Multi-Shift.

Post and Poll Pattern (Asynchronous Optimization)

The API offers two ways to request route optimization:

Synchronous Requests

POST /v2/vrp - The request blocks and returns the complete solution immediately. Use this for small to medium problems (typically under 100 jobs).

Advantages: - Simple: one request, one response - No polling required

Disadvantages: - Request times out if optimization takes too long - Can't track progress - Not suitable for large problems

Asynchronous Requests (Post and Poll)

Follow a three-step pattern:

  1. POST to /v2/polling/vrp → Get unique ID and status/solution URLs (returns 202 Accepted)
  2. Poll GET /v2/polling/vrp/{uid}/status → Check progress and get recommended next check time
  3. Fetch GET /v2/polling/vrp/{uid} → Retrieve the final solution when solving: false

Advantages: - No timeout issues - optimization can run as long as needed - Real-time progress tracking with cost and estimated completion time - Suitable for large problems (100+ jobs) - Solutions persist for 24 hours

Disadvantages: - More complex - requires multiple requests and polling loop - Slightly higher latency (solution available after status polling completes)

When to Use Asynchronous: - Problems with 100+ jobs - Need real-time progress visibility - Long optimization times (>30 seconds) - Integration with background workers or notification systems

Auth

You need to include the header Authorization: Bearer <your_token> in every request you make to the Route Optimisation API

You should perform a login request at the beginning of each session, since tokens expire.

auth_req.sh
URL='https://api.route.optimiser.app/api/auth/token'
HEADERS='Content-Type: application/json'
BODY='{
        "email": "your_email_address",
        "password": "your_password"
}'

RESP=$(curl "$URL" -H "$HEADERS" --data "$BODY")

# jq is a command line utility for parsing JSON
# you can install it with `apt install jq` or equivalent.
# The idea is just to get the `access_token` from the JSON response.
TOKEN=$(echo $RESP | jq -r .access_token)
auth_req.py
import requests

url = 'https://api.route.optimiser.app/api/auth/token'
headers = {'Content-Type': 'application/json'}
body = {
    'email': 'your_email_address',
    'password': 'your_password',
}

r = requests.post(url, headers=headers, json=body)

token = r.json()['access_token']

Full documentation for this request is in the Tarot Routing Swagger.

If you don't have a Tarot Routing account already, send us an email and we'll get you started: info@tarotanalytics.com

Parallel Solving

The parallel setting controls whether the solver attempts to find solutions using multiple CPU cores simultaneously.

Note

Usually we think about parallel programs running faster. This is true, but it works a little differently here.

For the same stopping critera, parallel = "true" will likely spend about the same time optimising, BUT it will usually find a better solution in that time.

If you actually want results faster, you'll need to shorten your stopping criteria in addition to running a parallel optimisation.

The parallel setting can take one of three string values:

  • "false": Parallel solving is explicitly disabled. The solver will use a single thread.
  • "true": Parallel solving is explicitly enabled. The solver will attempt to use multiple threads.
  • "auto": The solver will automatically decide whether to use parallel solving based on the problem size (n_locs). If n_locs (number of jobs + twice the number of drivers) is 200 or greater, parallel solving will be enabled. Otherwise, it will remain disabled.

Warning

parallel = "true" is free for now to our early customers using the new optimiser. However, Tarot will restrict this setting to premium subscriptions in future.

We generally recommend using "auto" as it provides a good balance between performance and resource usage for varying problem sizes. For very small problems, the overhead of parallelization might negate any benefits.