Skip to content

Multi-Objective: Duration & Distance

Minimize travel time, travel distance, or any blend of the two. By default the solver minimizes total driving time; add a distance_weight to make distance count too — for fuel costs, vehicle wear, or emission targets.


How the Objective Works

The solver minimizes a generalized cost:

objective = duration_weight × total_driving_time + distance_weight × total_distance
Parameter Default Meaning
duration_weight 1.0 Weight of total travel duration
distance_weight 0.0 Weight of total travel distance

Both weights must be non-negative, and at least one must be positive. The ratio between them sets the duration/distance trade-off. Their absolute scale matters too, once other priced components are in play: soft-constraint penalties, fixed vehicle costs, and the waiting price are fixed prices, so doubling both weights makes travel twice as expensive relative to them and violations correspondingly cheaper.

The simplest mental model: keep duration_weight at 1.0 and tune distance_weight; set duration_weight=0 only for a pure-distance objective.

The solver is unit-agnostic — durations and distances are in whatever units your matrices use. With the default duration_weight=1.0, the objective is measured in your duration units, and distance_weight is the price of one distance unit expressed in them.


Quick Start: Pricing Distance In

Provide both matrices and set distance_weight. Below, four customers around a depot; the direct road between B and C is a motorway loop — 21 minutes, but 32 km — so the fastest tour is not the shortest one:

from tqrouting import TQRouter, VRPInstance, Customer, VehicleType, Location

router = TQRouter()

instance = VRPInstance(
    customers=[
        Customer(id=name, location=Location(matrix_id=i + 1), delivery_load=10)
        for i, name in enumerate("ABCD")
    ],
    fleet=[
        VehicleType(
            start_location=Location(matrix_id=0),
            end_location=Location(matrix_id=0),
            capacity_load=100,
            n_vehicles=1,
        ),
    ],
    duration_matrix=[          # seconds: depot, A, B, C, D
        [0, 900, 1350, 1050, 600],
        [900, 0, 210, 2520, 630],
        [1350, 210, 0, 1260, 630],
        [1050, 2520, 1260, 0, 1830],
        [600, 630, 630, 1830, 0],
    ],
    distance_matrix=[          # meters — B–C is the fast but long motorway leg
        [0, 8200, 9400, 9200, 4700],
        [8200, 0, 1800, 17500, 5200],
        [9400, 1800, 0, 31800, 4600],
        [9200, 17500, 31800, 0, 15500],
        [4700, 5200, 4600, 15500, 0],
    ],
)

for label, kwargs in [
    ("duration only (default)", {}),
    ("distance_weight=0.1", {"distance_weight": 0.1}),
    ("distance only", {"duration_weight": 0.0, "distance_weight": 1.0}),
]:
    solution = router.solve(instance, time_limit=2, **kwargs)
    tour = " → ".join(v.customer_id for v in solution.routes[0].visits)
    print(f"{label:24} {tour}   {solution.total_route_duration / 60:.1f} min, "
          f"{solution.total_driving_distance / 1000:.1f} km")

Output:

duration only (default)  D → A → B → C   62.5 min, 52.7 km
distance_weight=0.1      C → D → B → A   77.0 min, 39.3 km
distance only            D → B → A → C   83.5 min, 37.8 km

With distance_weight=0.1 (in these units: 1 km costs as much as 100 s of driving) the solver gives up 14.5 minutes to save 13.4 km; minimising distance alone saves another 1.5 km for 6.5 more minutes.


Choosing the Weight

distance_weight is an exchange rate: how many duration units one distance unit is worth. A practical way to derive it from your operating costs:

If one distance unit costs you C (fuel, wear) and one duration unit costs W (driver time), set distance_weight = C / W.

For example, with matrices in seconds and meters, a fleet cost of 0.50 €/km, and a driver cost of 36 €/h (= 0.01 €/s): one meter costs 0.0005 €, i.e. 0.0005 / 0.01 = 0.05 — driving 1 km is worth 50 seconds.

The same fleet economics, tabulated for second/meter matrices (what road-network providers like OSRM return):

distance_weight Interpretation
0.0 Distance is free — minimize pure travel time (default)
0.01 1 km ≙ 10 s — a mild nudge toward shorter routes
0.1 1 km ≙ 100 s — distance and time pull with comparable strength on typical road networks
1.0 1 m ≙ 1 s — distance dominates almost entirely

The right value depends on your units

A useful distance_weight reflects the relative scale of your two matrices. With meters and seconds, distance entries are orders of magnitude larger than duration entries — so 1.0 does not mean "equal importance", it makes distance overwhelm the objective. With kilometers instead of meters, every value in the table above shifts by ×1000. Derive the weight from your costs and units, start small, and calibrate on your own instances.


Pure Distance Minimization

Set duration_weight=0 to ignore time entirely — the classic distance-CVRP objective:

solution = router.solve(
    instance,
    duration_weight=0.0,
    distance_weight=1.0,
)

Time windows, shift ends, and duration limits are still enforced as constraints — the weights change only what is minimized, never what is feasible.


How It Composes with Other Costs

Every other objective component is priced in the same objective units — with the default duration_weight=1.0, the units of your duration matrix:

  • Fixed vehicle costs (fixed_vehicle_cost): a cost of 3600 weighs like 3600 duration units — one hour of driving, if durations are in seconds.
  • Soft-constraint penalties (penalty_* with soft constraints): the price per unit of violation.
  • Waiting time (penalty_waiting): the price per duration unit of unavoidable waiting.
  • Profits (prize-collecting): each prize offsets the cost of visiting its customer, in the same units.

Requirements & Pitfalls

A real trade-off needs two distinct matrices. If the instance has no distance_matrix, the solver reuses the duration matrix for distance — the blend then just rescales the duration objective, and the optimal routes don't change. The SDK emits a UserWarning in that case.

Road-network providers like OSRM give you both matrices from the same source:

from tqrouting.matrix import OSRMMatrixProvider

provider = OSRMMatrixProvider()
instance.compute_duration_matrix(provider)   # seconds on the road network
instance.compute_distance_matrix(provider)   # meters on the road network

See Distance & Duration Matrices for the full provider list.

Weights are validated. Negative, non-finite, or all-zero weights raise a ValueError.


Interactive Example

See the objectives notebook for a runnable duration/distance trade-off example.