Skip to content

Time Windows

Time windows constrain when a customer can be served. The vehicle must arrive within the allowed window or wait until it opens.

How It Works

  • Each customer can have a time_window with start and end times
  • Each vehicle can have shift_start (earliest departure) and shift_end (latest return)
  • If a vehicle arrives at customer before the window opens, it waits until start
  • If a vehicle cannot arrive before end, the constraint is violated

Example

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

router = TQRouter()

instance = VRPInstance(
    customers=[
        Customer(
            location=Location(matrix_id=1),
            delivery_load=10,
            service_time="00:02:00",
            time_window=TimeWindow(start="08:00:00", end="13:00:00"),
        ),
        Customer(
            location=Location(matrix_id=2),
            delivery_load=8,
            service_time="00:01:00",
            time_window=TimeWindow(start="09:00:00", end="12:00:00"),
        ),
        Customer(
            location=Location(matrix_id=3),
            delivery_load=5,
            service_time="00:01:30",
            time_window=TimeWindow(start="07:30:00", end="11:00:00"),
        ),
    ],
    fleet=[
        VehicleType(
            start_location=Location(matrix_id=0),
            end_location=Location(matrix_id=0),
            shift_start="07:00:00",
            shift_end="18:00:00",
            capacity_load=30,
            n_vehicles=3,
        ),
    ],
    duration_matrix=[
        [0, 600, 900, 720],
        [600, 0, 480, 660],
        [900, 480, 0, 360],
        [720, 660, 360, 0],
    ],
)

solution = router.solve(instance)

for route in solution.routes:
    print(f"Route {route.id} (waiting: {route.route_waiting_time}s):")
    for visit in route.visits:
        print(f"  Customer {visit.customer_id}:")
        print(f"    Arrival: {visit.eta}")
        print(f"    Wait:    {visit.waiting_time}")
        print(f"    Service: {visit.start_service_time} → {visit.end_service_time}")

Using Time Strings

Instead of seconds, you can use human-readable HH:MM:SS strings — for every time field of the instance, not just some of them:

Customer(
    location=Location(matrix_id=1),
    service_time="00:15:00",                                  # 15 minutes
    time_window=TimeWindow(start="08:00:00", end="12:00:00"), # 8 AM – 12 PM
)

VehicleType(
    start_location=Location(matrix_id=0),
    shift_start="07:00:00",    # Vehicles leave at 7 AM
    shift_end="18:00:00",      # Must return by 6 PM
)

Understanding the Output

When time windows are present, the solution includes additional timing fields:

Field Description
solution.total_waiting_time Total waiting time across all routes
route.route_waiting_time Waiting time for a specific route
visit.eta When the vehicle arrives at the customer
visit.waiting_time Time the vehicle waits before service starts
visit.start_service_time When service begins (≥ time window start)
visit.end_service_time When service ends

Timing flow at each customer

  1. Vehicle arrives → eta
  2. If arrived early, wait → waiting_time = start_service_time - eta
  3. Service begins → start_service_time = max(eta, time_window.start)
  4. Service ends → end_service_time = start_service_time + service_time

Schedule Awareness

The solver also computes arrival windows and departure bounds for each route. These let you see how much flexibility exists in the schedule.

Departure Window

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

Any departure time between these bounds keeps the entire route feasible.

Arrival Windows per Customer

for visit in route.visits:
    print(f"Customer {visit.customer_id}:")
    print(f"  Arrival window: [{visit.earliest_arrival}, {visit.latest_arrival}]")
    print(f"  No-wait from:   {visit.no_wait_arrival}")
  • earliest_arrival — earliest feasible arrival (respects time windows and service times of all preceding stops)
  • latest_arrival — latest arrival that keeps downstream stops feasible
  • no_wait_arrival — earliest arrival that causes zero waiting time

Adjusting the Departure Time

By default, the solver picks the earliest departure that minimises total waiting time — it departs as early as possible without creating avoidable waiting. You can override this:

solution.set_departure_time(route_id=0, departure_time="07:30:00")

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

All ETAs, waiting times, and duration metrics are recalculated for the specified route.

See Reading Results → Schedule Awareness for the full reference.

Vehicle Shift Window

shift_start and shift_end define the vehicle's shift:

  • Vehicle cannot depart before shift_start
  • Vehicle must return to its end location by shift_end
VehicleType(
    start_location=Location(matrix_id=0),
    end_location=Location(matrix_id=0),
    shift_start="08:00:00",    # Shift starts at 8 AM
    shift_end="17:00:00",      # Must be back by 5 PM
    n_vehicles=5,
)

Note

The vehicle shift window is also covered in Route Duration & Distance Limits. Both pages describe the same constraint for convenience.

Tuning

If time-window constraints make it hard to find feasible solutions:

solution = router.solve(instance, penalty_time_window=100.0)

To account for idle waiting between stops, set penalty_waiting (off by default) so the solver prices time spent waiting for a window to open:

solution = router.solve(instance, penalty_waiting=5.0)

If a strict window or shift end should be a preference rather than a hard rule, mark it soft — violations are then priced instead of making the solution infeasible:

solution = router.solve(
    instance,
    soft_constraints=["time_window", "shift_end"],
    penalty_time_window=20.0,
    penalty_shift_end=15.0,
)

See Soft Constraints.


Interactive Example

See the constraints notebook for a runnable VRPTW example.