Skip to content

Reading Results

solve() returns a VRPSolution object with typed access to routes, metrics, and feasibility status.

Solution Overview

solution = router.solve(instance)

print(f"Status: {solution.solution_status}")        # "Feasible" | "FeasibleWithSoftViolations" | "Infeasible"
print(f"Feasible: {solution.is_feasible}")          # True unless a hard constraint is violated
print(f"Soft violations: {solution.has_soft_violations}")
print(f"Total duration: {solution.total_route_duration}")
print(f"Total driving: {solution.total_driving_time}")
print(f"Total service: {solution.total_service_time}")
print(f"Total visits: {solution.total_n_visits}")
print(f"Total delivery load: {solution.total_delivery_load}")
print(f"Number of routes: {len(solution.routes)}")

VRPSolution Fields

Field Type Description
total_route_duration float Sum of all route durations (driving + service + waiting)
total_driving_time float Sum of driving time across all routes
total_driving_distance float or None Sum of driving distances (only when a separate distance matrix is provided)
total_service_time float Sum of service time across all routes
total_n_visits int Total customer visits across all routes
total_delivery_load float or list[float] Total delivery load across all routes (a [weight, volume] list on 2-D instances)
total_pickup_load float, list[float] or None Total pickup load (only for pickup-and-delivery problems)
solution_status str "Feasible", "Infeasible", or "FeasibleWithSoftViolations"
routes list[Route] List of routes (one per active vehicle)
constraint_violations list[ConstraintViolation] Reported violations; empty when fully feasible, non-empty for infeasible and soft-violating solutions
is_feasible bool True when no hard constraint is violated — i.e. status is "Feasible" or "FeasibleWithSoftViolations"
has_soft_violations bool True when the solution is feasible but violates a soft-configured constraint (status "FeasibleWithSoftViolations")
total_waiting_time float or None Total waiting time (only for time-window problems)
total_profit float or None Total profit collected (only for prize-collecting problems)
unvisited_customers list[int or str] or None IDs of customers left unvisited (only for prize-collecting problems)

Feasibility status

solution_status has three values:

Status is_feasible has_soft_violations Meaning
"Feasible" True False All constraints satisfied
"FeasibleWithSoftViolations" True True All hard constraints satisfied; one or more soft constraints violated (priced into the objective and listed in constraint_violations)
"Infeasible" False False At least one hard constraint is violated

Use is_feasible to accept a solution and has_soft_violations to detect priced soft breaches.


Routes

Each route represents one vehicle's journey:

for route in solution.routes:
    print(f"Route {route.id}:")
    print(f"  Vehicle type: {route.vehicle_type_id}")
    print(f"  Customers visited: {route.n_visits}")
    print(f"  Start time: {route.route_start_time}")
    print(f"  End time: {route.route_end_time}")
    print(f"  Duration: {route.route_duration}")
    print(f"  Driving time: {route.route_driving_time}")
    print(f"  Service time: {route.route_service_time}")
    print(f"  Delivery load: {route.route_delivery_load}")
    print(f"  Peak load: {route.route_peak_load}")

Route Fields

Field Type Description
id int Route index (0-based)
vehicle_type_id str or int Which vehicle type is assigned to this route
n_visits int Number of customers on this route (reload stops excluded)
route_start_time float or None When the vehicle departs the depot (default: earliest with minimum waiting)
route_end_time float or None When the vehicle returns to the depot
route_duration float Total duration (driving + service + waiting)
route_driving_time float Driving time only
route_distance float or None Total route distance (only when a separate distance matrix is provided)
route_service_time float Service time only
route_delivery_load float or list[float] Delivery load carried (a [weight, volume] list on 2-D instances)
route_pickup_load float, list[float] or None Pickup load carried (pickup-and-delivery problems only)
route_peak_load float, list[float] or None Maximum vehicle load at any point during the route (pickup-and-delivery problems only)
customer_sequence list[CustomerVisit \| ReloadStop] Ordered sequence of stops — includes ReloadStop entries on multi-trip routes
visits list[CustomerVisit] Property: customer visits only
reload_stops list[ReloadStop] Property: mid-route depot reloads only (see Multi-Trip Routes)
route_waiting_time float or None Waiting time (time-window problems only)
route_profit float or None Profit collected (prize-collecting only)
route_earliest_departure float or None Absolute earliest feasible depot departure — may involve extra waiting (schedule field)
route_latest_departure float or None Latest feasible depot departure (schedule field)

Customer Visits

route.visits lists the customer stops of a route. route.customer_sequence is the full stop timeline: on multi-trip routes it also contains ReloadStop entries between the visits.

Each visit has detailed timing information:

for route in solution.routes:
    for visit in route.visits:
        print(f"  Customer {visit.customer_id}:")
        print(f"    Arrival (ETA): {visit.eta}")
        print(f"    Service start: {visit.start_service_time}")
        print(f"    Service end:   {visit.end_service_time}")
        if visit.waiting_time is not None:
            print(f"    Waiting:       {visit.waiting_time}")

CustomerVisit Fields

Field Type Description
customer_id str or int Customer identifier (your custom ID or positional index)
eta float Estimated time of arrival
start_service_time float When service begins
end_service_time float When service ends
waiting_time float or None Time spent waiting before service (time-window problems only)
earliest_arrival float or None Earliest feasible arrival without violating any constraints (schedule field)
latest_arrival float or None Latest feasible arrival at this customer (schedule field)
no_wait_arrival float or None Earliest arrival with zero waiting time (schedule field)

Timing Relationship

eta ≤ start_service_time ≤ end_service_time

If the vehicle arrives early (before the time window opens), it waits: waiting_time = start_service_time - eta.


Schedule Awareness

When the problem includes time windows, vehicle shifts, or duration limits, the solver automatically computes arrival windows for each customer and departure bounds for each route. These fields help you understand how much scheduling flexibility exists.

How schedule fields are computed

TQRouter.solve() automatically calls solution.compute_schedule(instance) after solving, so schedule fields are immediately available. You only need to call compute_schedule() manually when loading a solution from a JSON file — see Loading a Solution from JSON below.

When are schedule fields populated?

Schedule fields (earliest_arrival, latest_arrival, no_wait_arrival, route_earliest_departure, route_latest_departure) are populated when the problem has at least one of:

  • Customer time windows
  • Vehicle shift_end
  • Vehicle max_duration

For pure capacity-only problems (CVRP) without any time constraints, these fields remain None.

Departure Window

Each route has a feasible departure range:

route = solution.routes[0]
print(f"Earliest departure: {route.route_earliest_departure}")
print(f"Latest departure:   {route.route_latest_departure}")

The vehicle can leave the depot at any time between route_earliest_departure and route_latest_departure while keeping the entire route feasible.

Open routes

For a vehicle type without end_location the route ends at its last customer, and all schedule fields are computed without a return leg — route_latest_departure, the arrival windows and the durations recomputed by set_departure_time() all stop at the last stop, exactly like the solver's own route_end_time.

The default route_start_time is the earliest departure that minimises total waiting time. This is not the same as route_earliest_departure — departing at route_earliest_departure is feasible but may cause the vehicle to arrive early at time-windowed customers and wait. The solver picks the earliest time at which no avoidable waiting occurs, which is always ≥ route_earliest_departure.

Arrival Windows

Each customer visit has an arrival window:

for visit in route.visits:
    print(f"Customer {visit.customer_id}:")
    print(f"  Can arrive:    [{visit.earliest_arrival}, {visit.latest_arrival}]")
    print(f"  No-wait from:  {visit.no_wait_arrival}")
    print(f"  Planned ETA:   {visit.eta}")
Field Meaning
earliest_arrival Earliest the vehicle can arrive at this customer without violating any constraints — accounts for travel time, service time, and time windows of all previous stops
latest_arrival Latest the vehicle can arrive and still serve all remaining customers on time. When max_duration is set, this value depends on the departure time
no_wait_arrival Earliest arrival that does not require waiting at this customer (max(earliest_arrival, time_window.start))

Understanding slack

The difference latest_arrival - eta tells you how much delay this customer can absorb before downstream stops are affected. This is useful for real-world dispatching where delays are inevitable.

Changing the Departure Time

Use set_departure_time() to recalculate all ETAs and timing metrics for a route based on a custom departure time:

solution = router.solve(instance)

route = solution.routes[0]
print(f"Default departure: {route.route_start_time}")   # earliest with min waiting
print(f"Default duration:  {route.route_duration}")

# Override the departure time and recalculate all ETAs
solution.set_departure_time(route_id=0, departure_time="09:00:00")

print(f"New departure: {route.route_start_time}")        # 32400.0 (09:00)
print(f"New duration:  {route.route_duration}")

for visit in route.visits:
    print(f"  {visit.customer_id}: ETA={visit.eta}, wait={visit.waiting_time}")

set_departure_time() updates the following fields on the route and its visits:

  • route.route_start_time, route.route_end_time, route.route_duration, route.route_waiting_time
  • visit.eta, visit.waiting_time, visit.start_service_time, visit.end_service_time
  • solution.total_route_duration, solution.total_waiting_time
  • visit.latest_arrival — only when max_duration is set on the vehicle type (see note below)

Departure bounds (route_earliest_departure, route_latest_departure), earliest_arrival, and no_wait_arrival are not affected — they are fixed properties of the route sequence and constraints, independent of the chosen departure time.

latest_arrival and max_duration

When a vehicle type has max_duration set, latest_arrival depends on the departure time: a later departure shifts the duration deadline forward, allowing later arrivals at each stop. Because of this, set_departure_time() automatically recomputes latest_arrival for every visit on the route to reflect the new duration deadline.

Without max_duration, latest_arrival is determined solely by time windows and depot end time, and does not change when the departure time changes.

Warning

Choosing a departure time outside [route_earliest_departure, route_latest_departure] will produce a schedule, but it may violate time window or duration constraints. The arrival windows indicate where feasible scheduling is possible.

Loading a Solution from JSON

When loading a solution from a file, schedule fields are not computed automatically. Call compute_schedule() manually:

from tqrouting import VRPSolution, VRPInstance

instance = VRPInstance.from_json("problem.json")
solution = VRPSolution.from_json("solution.json")

solution.compute_schedule(instance)

# Now schedule fields are available
print(solution.routes[0].route_earliest_departure)

# And set_departure_time works
solution.set_departure_time(route_id=0, departure_time="08:00:00")

A loaded solution keeps the time format of the file it came from, so saving it again writes the same representation. After compute_schedule(instance) it uses the instance's format, exactly like a solution returned by solve().

The file also carries the decimal the solution was solved with (see Matrix precision), so compute_schedule() rounds the duration matrix exactly as the solver did and the arrival windows agree with eta.


Handling Infeasible Solutions

When the solver cannot find a solution that satisfies all constraints, it returns the best solution found along with violation details:

solution = router.solve(instance)

if not solution.is_feasible:
    print("Solution has constraint violations:")
    for v in solution.constraint_violations:
        print(f"  {v.constraint} on route {v.route_id} (excess: {v.excess_value})")

ConstraintViolation Fields

Field Type Description
constraint str Name of the violated constraint
route_id int Which route has the violation
excess_value float By how much the constraint is exceeded
dimension int or None For "capacity" violations on 2-D instances: which dimension (0 = weight, 1 = volume)
customer_id int, str or None For "time_window" violations: the customer whose window was missed (mapped to your custom ID)

Possible constraint names:

Constraint Meaning
"capacity" Route load exceeds vehicle capacity (carries dimension on 2-D instances)
"time_window" Service starts after the customer's time window end (carries customer_id)
"shift_end" Vehicle returns after shift_end
"max_duration" Route duration exceeds max_duration
"max_distance" Route distance exceeds max_distance

A violation is reported for both hard (infeasible) and soft (FeasibleWithSoftViolations) constraint breaches — check solution_status / has_soft_violations to tell which.


Time Formatting

All time fields on the VRPSolution dataclass are always stored as seconds (float), regardless of how you specified times in the input. This makes it easy to do arithmetic directly on the solution:

solution = router.solve(instance)

solution.total_route_duration       # 5400.0 (seconds), not "01:30:00"
route = solution.routes[0]
route.route_driving_time            # 3600.0 (seconds), not "01:00:00"
route.visits[0].eta                 # 30600.0 (seconds), not "08:30:00"

String formatting happens only when you call to_dict() or to_json(). If your input used HH:MM:SS strings, the solution formats its times the same way (fractions of a second are rounded to the nearest second):

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

router = TQRouter()

instance = VRPInstance(
    customers=[
        Customer(
            location=Location(matrix_id=1),
            time_window=TimeWindow(start="08:00:00", end="17:00:00"),
            service_time="00:30:00",
        ),
    ],
    fleet=[VehicleType(start_location=Location(matrix_id=0), end_location=Location(matrix_id=0))],
    duration_matrix=[[0, 900], [900, 0]],   # 15 minutes each way
)

solution = router.solve(instance, time_limit=1)
result = solution.to_dict()
result["routes"][0]["route_start_time"]                          # "07:45:00" — leave so as to arrive as the window opens
result["routes"][0]["customer_sequence"][0]["eta"]               # "08:00:00"
result["routes"][0]["customer_sequence"][0]["end_service_time"]  # "08:30:00"
result["routes"][0]["route_end_time"]                            # "08:45:00"
solution.routes[0].customer_sequence[0].eta                      # 28800.0 — the dataclass keeps seconds

You can also override the format explicitly:

solution.to_dict(time_format="HH:MM:SS")  # Always HH:MM:SS
solution.to_dict(time_format="seconds")   # Raw seconds (float)

Serialization

Save and load solutions as JSON or Python dicts:

# Save to JSON
solution.to_json("result.json")

# Load from JSON
from tqrouting import VRPSolution
solution = VRPSolution.from_json("result.json")

# Or work with dicts directly
solution_dict = solution.to_dict()
solution = VRPSolution.from_dict(solution_dict)

to_dict() and to_json() both accept an optional time_format keyword argument to control how time values are serialized:

solution.to_dict(time_format="HH:MM:SS")  # Always HH:MM:SS
solution.to_dict(time_format="seconds")   # Raw seconds (float)

solution.to_json("result.json", time_format="HH:MM:SS")

If time_format is not specified, the format is automatically detected from the input instance: if you used HH:MM:SS strings in the input, the solution output uses the same format.

to_dict() produces a clean format: None fields are omitted, time values use the detected format, and all field names are lowercase.

Complete Example

Three orders, one truck type with room for two of them per trip:

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

router = TQRouter()

instance = VRPInstance(
    customers=[
        Customer(id="ORD-001", location=Location(matrix_id=1), delivery_load=10, service_time=60),
        Customer(id="ORD-002", location=Location(matrix_id=2), delivery_load=10, service_time=60),
        Customer(id="ORD-003", location=Location(matrix_id=3), delivery_load=8, service_time=60),
    ],
    fleet=[
        VehicleType(
            id="truck-A",
            start_location=Location(matrix_id=0),
            end_location=Location(matrix_id=0),
            capacity_load=20,
            n_vehicles=2,
        ),
    ],
    duration_matrix=[
        [0, 30, 45, 60],
        [30, 0, 40, 25],
        [45, 40, 0, 35],
        [60, 25, 35, 0],
    ],
)

solution = router.solve(instance, time_limit=1)
solution.solution_status          # "Feasible"
solution.is_feasible              # True
solution.total_route_duration     # 380.0
solution.total_driving_time       # 200.0
solution.total_service_time       # 180.0
solution.total_n_visits           # 3
solution.total_delivery_load      # 28.0

route = solution.routes[0]
route.id                          # 0
route.vehicle_type_id             # "truck-A"
route.n_visits                    # 1
route.route_start_time            # 0.0
route.route_end_time              # 120.0
route.route_duration              # 120.0

visit = route.visits[0]
visit.customer_id                 # "ORD-001"
visit.eta                         # 30.0
visit.start_service_time          # 30.0
visit.end_service_time            # 90.0

solution.to_dict():

{
  "total_route_duration": 380.0,
  "total_driving_time": 200.0,
  "total_service_time": 180.0,
  "total_n_visits": 3,
  "total_delivery_load": 28.0,
  "solution_status": "Feasible",
  "routes": [
    {
      "id": 0,
      "vehicle_type_id": "truck-A",
      "n_visits": 1,
      "route_start_time": 0.0,
      "route_end_time": 120.0,
      "route_duration": 120.0,
      "route_driving_time": 60.0,
      "route_service_time": 60.0,
      "route_delivery_load": 10.0,
      "customer_sequence": [
        {
          "customer_id": "ORD-001",
          "eta": 30.0,
          "start_service_time": 30.0,
          "end_service_time": 90.0
        }
      ]
    },
    {
      "id": 1,
      "vehicle_type_id": "truck-A",
      "n_visits": 2,
      "route_start_time": 0.0,
      "route_end_time": 260.0,
      "route_duration": 260.0,
      "route_driving_time": 140.0,
      "route_service_time": 120.0,
      "route_delivery_load": 18.0,
      "customer_sequence": [
        {
          "customer_id": "ORD-003",
          "eta": 60.0,
          "start_service_time": 60.0,
          "end_service_time": 120.0
        },
        {
          "customer_id": "ORD-002",
          "eta": 155.0,
          "start_service_time": 155.0,
          "end_service_time": 215.0
        }
      ]
    }
  ]
}

solution.to_dict(time_format="HH:MM:SS"):

{
  "total_route_duration": "00:06:20",
  "total_driving_time": "00:03:20",
  "total_service_time": "00:03:00",
  "total_n_visits": 3,
  "total_delivery_load": 28.0,
  "solution_status": "Feasible",
  "routes": [
    {
      "id": 0,
      "vehicle_type_id": "truck-A",
      "n_visits": 1,
      "route_start_time": "00:00:00",
      "route_end_time": "00:02:00",
      "route_duration": "00:02:00",
      "route_driving_time": "00:01:00",
      "route_service_time": "00:01:00",
      "route_delivery_load": 10.0,
      "customer_sequence": [
        {
          "customer_id": "ORD-001",
          "eta": "00:00:30",
          "start_service_time": "00:00:30",
          "end_service_time": "00:01:30"
        }
      ]
    },
    {
      "id": 1,
      "vehicle_type_id": "truck-A",
      "n_visits": 2,
      "route_start_time": "00:00:00",
      "route_end_time": "00:04:20",
      "route_duration": "00:04:20",
      "route_driving_time": "00:02:20",
      "route_service_time": "00:02:00",
      "route_delivery_load": 18.0,
      "customer_sequence": [
        {
          "customer_id": "ORD-003",
          "eta": "00:01:00",
          "start_service_time": "00:01:00",
          "end_service_time": "00:02:00"
        },
        {
          "customer_id": "ORD-002",
          "eta": "00:02:35",
          "start_service_time": "00:02:35",
          "end_service_time": "00:03:35"
        }
      ]
    }
  ]
}