Skip to content

Solver Configuration

Basic Usage

from tqrouting import TQRouter

router = TQRouter()
solution = router.solve(instance)

Create TQRouter once and call solve() as many times as needed — it's designed for reuse.


Solver Parameters

Pass solver settings as keyword arguments to solve():

solution = router.solve(
    instance,
    time_limit=30,
    nb_threads=4,
)

Available Parameters

Parameter Type Default Description
evolutionary_search bool False Use the evolutionary search instead of the default adaptive search — see Search Algorithm
decomposition bool True Split large instances into subproblems — see Decomposition
time_limit float 30 Solver runtime in seconds (minimum 1)
nb_threads int 1 Number of solver threads (parallel search) — see Multi-Threading
decimal int 1 Decimal places the duration matrix is rounded to before searching, in the unit of the matrix. -1 = no rounding. Anything other than 0 or 1 warns — see Matrix precision
penalty_capacity_load float 10.0 Initial penalty weight for weight-capacity violations
penalty_capacity_volume float 10.0 Initial penalty weight for volume-capacity violations (independent of the weight dimension)
penalty_time_window float 10.0 Initial penalty weight for time window violations
penalty_duration float 10.0 Initial penalty weight for route duration violations
penalty_distance float 10.0 Initial penalty weight for route distance violations
penalty_waiting float 0 Penalty weight for unavoidable waiting time (0 = off)
penalty_shift_end float 10.0 Initial penalty weight for shift-end overtime (returning after shift_end)
profit_weight float 1.0 Converts profit units into objective units (prize-collecting) — see Prize-Collecting
duration_weight float 1.0 Weight of total travel duration in the objective — see Objective Weights
distance_weight float 0.0 Weight of total travel distance in the objective — see Objective Weights
soft_constraints list[str] [] Constraint types to treat as soft instead of hard — see Soft Constraints
validate bool True Whether to validate input/output data

Search Algorithm

The solver offers two search algorithms:

Algorithm How to select When to use
Adaptive search default A strong all-round choice, and the better option on large instances.
Evolutionary search evolutionary_search=True A population-based alternative. Worth trying on smaller instances where a different search behaviour may pay off.
solution = router.solve(instance, evolutionary_search=True)

Both solve the same problem definition and produce the same output format — only the search behaviour differs. Leave the default unless you have a reason to switch.


Decomposition

For large instances the solver can split the problem into geographic subproblems, solve them, and merge the partial solutions. This keeps runtimes practical on very large fleets and customer sets.

Decomposition is on by default and is applied only once the instance is large enough:

Configuration Decomposition starts at
Default (adaptive search) 3000 customers
evolutionary_search=True 600 customers

Below the threshold the instance is solved as a whole, even with decomposition=True. To disable it completely — so the instance is always solved as a whole, no matter how many customers it has:

solution = router.solve(instance, decomposition=False)

Time Limit

Controls how long the solver runs. Longer time = potentially better solution.

# Quick solve (good for testing)
solution = router.solve(instance, time_limit=5)

# Longer solve (better results for large instances)
solution = router.solve(instance, time_limit=120)

Multi-Threading

The solver runs on a single thread by default. Set nb_threads to use more of the machine:

import os

solution = router.solve(instance, nb_threads=os.cpu_count())

The threads search cooperatively, sharing their best solution, and the search throughput grows almost linearly with the thread count up to the number of physical cores: eight threads do close to eight times the search work of one within the same time_limit. Use as many threads as the machine offers when solution quality matters — the same time_limit then yields a better solution, or the same quality is reached in a fraction of the time.


Penalty & Profit Weights

Penalty weights control how aggressively the solver avoids constraint violations during search, and profit_weight converts prize-collecting profit values into objective units (see Prize-Collecting).

All constraint penalty weights default to 10.0 and are adapted automatically during the search, so they rarely need tuning. profit_weight defaults to 1.0 — right whenever profits are already expressed in the objective's units.

penalty_waiting is a fixed price per unit of unavoidable waiting, added straight to the objective (like profit or fixed vehicle costs). It defaults to 0 and is never auto-adapted.

Override the initial values only if you need fine-grained control:

solution = router.solve(
    instance,
    penalty_capacity_load=100.0,  # Increase to push harder on weight feasibility
    penalty_capacity_volume=50.0, # Volume dimension is tuned independently
    penalty_time_window=50.0,     # Increase if time windows are frequently violated
    penalty_duration=30.0,        # Increase for stricter duration limits
    penalty_distance=30.0,        # Increase for stricter distance limits
    penalty_waiting=5.0,          # Objective price per unit of unavoidable waiting (0 = free)
    penalty_shift_end=50.0,       # Discourage returning after shift_end
    profit_weight=10.0,            # Higher → prioritize profit more aggressively
)

Set penalty_waiting > 0 to make the solver trade route length against time spent idle between stops.


Objective Weights

By default the solver minimizes total driving time. Set distance_weight to price distance into the objective, or duration_weight=0 for pure distance minimization:

solution = router.solve(
    instance,
    distance_weight=0.1,    # with second/meter matrices: 1 km ≙ 100 s of driving
)

The objective becomes duration_weight × driving time + distance_weight × distance, in the units of your matrices. With the default duration_weight=1.0, distance_weight is an exchange rate: the price of one distance unit, expressed in duration units. A real trade-off requires a distance_matrix distinct from the duration_matrix.

See Multi-Objective (Duration & Distance) for choosing the weight, pure-distance objectives, and how the blend composes with fixed vehicle costs and penalties.


Soft Constraints

By default every constraint is hard: a solution that violates it is infeasible. You can instead mark chosen constraints as soft — their violations are priced into the objective (via the matching penalty_* weight) rather than making the solution infeasible. This is useful when a problem would otherwise have no feasible solution, or when a limit is a preference rather than a hard rule.

Pass the constraint names to soft_constraints:

solution = router.solve(
    instance,
    soft_constraints=["time_window", "shift_end"],
    penalty_time_window=20.0,   # cost per unit of time-window lateness
    penalty_shift_end=15.0,     # cost per unit of shift overtime
)

Allowed names:

Name Constraint made soft
"capacity_load" Weight capacity
"capacity_volume" Volume capacity (independent of the weight dimension)
"time_window" Customer time windows
"max_duration" Route duration limit
"max_distance" Route distance limit
"shift_end" Vehicle shift-end overtime

An unknown name raises ValueError. For a soft constraint the penalty_* weight is a fixed cost per unit of violation (it is not auto-adapted). When a solution stays within all hard constraints but breaks a soft one, its solution_status is "FeasibleWithSoftViolations" — see Reading Results.


Skipping Validation

By default, TQrouting validates input data before solving and output data after. To skip validation (for performance or if you trust your data):

solution = router.solve(instance, validate=False)

Standalone Validation

You can validate a problem definition without solving it using validate_input(). It accepts the dict format — the same format produced by VRPInstance.to_dict():

from tqrouting import validate_input, VRPInstance, ValidationError

instance = VRPInstance(...)
try:
    validate_input(instance.to_dict())
except ValidationError as e:
    print(f"Invalid input: {e}")

The solver also calls validate_input automatically before solving (unless validate=False).

Validation checks include:

  • At least one matrix (distance or duration) is present
  • Fleet has at least one vehicle type
  • Matrices are square, both the same size, and exactly max(matrix_id) + 1 rows (no unreferenced trailing rows); customer matrix_ids are unique
  • Time windows are valid (start ≤ end, not negative)
  • Route limits and shift bounds (max_duration, max_distance, shift_start, shift_end) are non-negative, and every time field parses as a number or an HH:MM:SS string, with one time format used throughout the instance (all strings or all numbers)
  • No single customer exceeds maximum vehicle capacity (per dimension, on 2-D instances)
  • When both quantities appear (in demands or in capacities), every vehicle type declares the capacity of each quantity its customers carry
  • Custom ids are all-or-none and unique within customers and within vehicle types
  • profit is set on every customer or on none
  • Coordinate consistency: x/y and lat/lon come in pairs, and x/y are set on every location or on none