A fresh food e-commerce platform has one distribution center in the city and needs to use a fleet of refrigerated trucks to deliver fresh products to 12 supermarkets. Each supermarket has a specified time window for receiving goods and a specified unloading time. The distribution center has 5 identical refrigerated trucks, each with a maximum load of 3 tons and a speed of 40 km/h. The coordinates of the distribution center are (0, 0), and the information of the supermarkets is as follows:

| SUPERMARKET ID | (x,y) | DEMAND(kg) | TIME WINDOW | UNLOADING TIME(min) |
|---------|-----------|------------|--------------|--------------|
| S1      | (-5, 4)   | 800        | [7:00-9:00]  | 20           |
| S2      | (3, 6)    | 600        | [8:00-10:00] | 15           |
| S3      | (-2, -3)  | 900        | [7:30-9:30]  | 25           |
| S4      | (4, -2)   | 700        | [8:30-10:30] | 20           |
| S5      | (-4, -5)  | 500        | [9:00-11:00] | 15           |
| S6      | (6, 3)    | 850        | [7:00-9:00]  | 25           |
| S7      | (-3, 5)   | 750        | [8:00-10:00] | 20           |
| S8      | (5, -4)   | 600        | [9:30-11:30] | 15           |
| S9      | (-6, -2)  | 450        | [8:00-10:00] | 15           |
| S10     | (2, 7)    | 800        | [7:30-9:30]  | 20           |
| S11     | (-1, -6)  | 550        | [9:00-11:00] | 15           |
| S12     | (4, 5)    | 700        | [8:30-10:30] | 20           |

The 5 refrigerated trucks leave the distribution center after 6:30 AM to deliver fresh products to the designated supermarkets and must return to the distribution center before 12:00 PM. What is the shortest total travel distance for the refrigerated trucks? (Distances are calculated as straight-line distances in kilometers.)

Problem type: MILP (Medium: 806 variables, 1690 constraints, 7728 nonzeros)

Mathematical model:
Set:
1. Node
The set of nodes, \( N = \{0, 1, \dots, SupermarketNum\} \)
# where node 0 is the starting point (Distribution Center) and nodes 1 to n are the supermarkets to be served.
2. Trucks
The set of trucks, \( K = \{1, 2, \dots, TruckNum\} \)

Parameter:
1. SupermarketNum
12
2. Coordinate
[(0, 0), (-5, 4), (3, 6), (-2, -3), (4, -2), (-4, -5), (6, 3), (-3, 5), (5, -4), (-6, -2), (2, 7), (-1, -6), (4, 5)]
3. Demand
[0, 800, 600, 900, 700, 500, 850, 750, 600, 450, 800, 550, 700]
4. Timewindow
[(6.5, 12), (7, 9), (8, 10), (7.5, 9.5), (8.5, 10.5), (9, 11), (7, 9), (8, 10), (9.5, 11.5), (8, 10), (7.5, 9.5), (9, 11), (8.5, 10.5)]
5. UnloadingTime
[0, 20, 15, 25, 20, 15, 25, 20, 15, 15, 20, 15, 20]
6. TruckNum
5
7. Speed
40
8. M
50000

Decision variable:
1. X
Binary variable, \( x_{ijk} \forall i \in N, j \in N, k \in K\), where \( x_{ijk} = 1 \) if truck \( k \) departs from node \( i \) and arrives at node \( j \), otherwise \( x_{ijk} = 0 \).
2. T^a
Continous variable, \( t^a_i \forall i \in N\), where \( t^a_i \) is the time at which the truck arrives at node \( i \).
3. T^d
Continous variable, \( t^d_i \forall i \in N\), where \( t^d_i \) is the time at which the truck departs from node \( i \).

Objective:
1. Minimize the total distance traveled by the trucks.
min: \sum_{k \in K}\sum_{i \in N} \sum_{j \in N} Distance[i][j] * x_{ijk}
# Where \( Distance[i][j] \) is the Euclidean distance between node \( i \) and node \( j \).

Constraint:
1. Each supermarket must be visited exactly once by one truck.
\sum_{k \in K} \sum_{i \in N} x_{ijk} = 1 \forall j \in N/{0}
2. Each supermarket must be exited exactly once by one truck.
\sum_{k \in K} \sum_{j \in N} x_{ijk} = 1 \forall i \in N/{0}
3. Each truck must leave the distribution center once.
\sum_{j \in N} x_{0jk} = 1 \forall k \in K
4. Each truck must return to the distribution center once.
\sum_{i \in N} x_{i0k} = 1 \forall k \in K
5. Eliminate sub-tours.
\sum_{i \in N} \sum_{j \in N} x_{ijk} \leq |S| - 1 \forall S \subseteq N, 1 < |S| < |N|, k \in K
6. The truck must arrive at each supermarket within its time window.
t^a_i \geq Timewindow[i][0] \forall i \in N/{0}
7. The truck must depart from each supermarket within its time window.
t^d_i \leq Timewindow[i][1] \forall i \in N/{0}
8. The truck's arrival time at each node must be more than or equal to the departure time from the previous node plus the travel time between the two nodes.
t^d_i + \frac{Distance[i][j]}{Speed} - M * (1 - x_{ijk}) \leq t^a_j \forall i, j \in N, k \in K
t^d_i + \frac{Distance[i][j]}{Speed} + M * (1 - x_{ijk}) \geq t^a_j \forall i, j \in N, k \in K

Type:
Mixed, Linear, Linear
MILP


Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB
import math


def solve_vrptw(
    coordinates=[(0, 0), (-5, 4), (3, 6), (-2, -3), (4, -2), (-4, -5), (6, 3),
                 (-3, 5), (5, -4), (-6, -2), (2, 7), (-1, -6), (4, 5)],
    time_windows=[(6.5, 12), (7, 9), (8, 10), (7.5, 9.5), (8.5, 10.5),
                  (9, 11), (7, 9), (8, 10), (9.5, 11.5), (8, 10), (7.5, 9.5),
                  (9, 11), (8.5, 10.5)],
    demands=[0, 800, 600, 900, 700, 500, 850, 750, 600, 450, 800, 550, 700],
    unloading_times=[0, 20, 15, 25, 20, 15, 25, 20, 15, 15, 20, 15, 20],
    truck_num=5,
    speed=40,
    M=50000):
    """
    Solves the Vehicle Routing Problem with Time Windows (VRPTW).
    """
    supermarket_num = len(coordinates) - 1
    nodes = range(supermarket_num + 1)  # 0 is DC, 1-12 are supermarkets
    trucks = range(truck_num)  # 0-4 are trucks

    # Calculate distance matrix
    def calculate_distance(coord1, coord2):
        return math.sqrt((coord1[0] - coord2[0])**2 +
                         (coord1[1] - coord2[1])**2)

    distance = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                distance[i, j] = calculate_distance(coordinates[i],
                                                    coordinates[j])

    # --- Model Initialization ---
    model = gp.Model("MultiVehicleTimeWindowedVRP")

    # --- Decision variables ---
    # x[i,j,k] = 1 if truck k travels from i to j
    x = {}
    for i in nodes:
        for j in nodes:
            if i != j:
                for k in trucks:
                    x[i, j, k] = model.addVar(vtype=GRB.BINARY,
                                              name=f'x_{i}_{j}_{k}')

    # t_a[i] = arrival time at node i
    t_a = model.addVars(nodes,
                        lb=[time_windows[i][0] for i in nodes],
                        ub=[time_windows[i][1] for i in nodes],
                        name='t_a')

    # t_d[i] = departure time from node i
    t_d = model.addVars(nodes,
                        lb=[time_windows[i][0] for i in nodes],
                        ub=[time_windows[i][1] for i in nodes],
                        name='t_d')

    # --- Objective function ---
    # Set objective: minimize total distance
    objective = gp.quicksum(distance[i, j] * x[i, j, k] for i in nodes
                            for j in nodes for k in trucks if i != j)
    model.setObjective(objective, GRB.MINIMIZE)

    # --- Constraints ---
    # Constraint 1: Each supermarket must be visited exactly once by one truck
    for j in nodes:
        if j != 0:  # Skip DC
            model.addConstr(
                gp.quicksum(x[i, j, k] for i in nodes for k in trucks
                            if i != j) == 1,
                name=f'visit_to_{j}')

    # Constraint 2: Each supermarket must be exited exactly once by one truck
    for i in nodes:
        if i != 0:  # Skip DC
            model.addConstr(
                gp.quicksum(x[i, j, k] for j in nodes for k in trucks
                            if i != j) == 1,
                name=f'leave_from_{i}')

    # Constraint 3: Each truck must leave the DC once
    for k in trucks:
        model.addConstr(gp.quicksum(x[0, j, k] for j in nodes if j != 0) == 1,
                        name=f'truck_{k}_leaves_dc')

    # Constraint 4: Each truck must return to the DC once
    for k in trucks:
        model.addConstr(gp.quicksum(x[i, 0, k] for i in nodes if i != 0) == 1,
                        name=f'truck_{k}_returns_dc')

    # Flow conservation for each truck at each supermarket
    for k in trucks:
        for h in nodes:
            if h != 0:  # Skip DC
                model.addConstr(
                    gp.quicksum(x[i, h, k] for i in nodes if i != h) ==
                    gp.quicksum(x[h, j, k] for j in nodes if j != h),
                    name=f'flow_cons_truck_{k}_node_{h}')

    # Time window constraints
    for i in nodes[1:]:  # Skip DC
        # Departure time must be after arrival time plus unloading time
        model.addConstr(t_d[i] >= t_a[i] + unloading_times[i] / 60,
                        name=f'depart_after_unload_{i}')

    # Time consistency constraints
    for i in nodes:
        for j in nodes:
            if i != j:
                for k in trucks:
                    # If truck k goes from i to j, arrival time at j must be after
                    # departure from i plus travel time
                    model.addConstr(
                        t_d[i] + distance[i, j] / speed - M *
                        (1 - x[i, j, k]) <= t_a[j],
                        name=f'time_cons_min_{i}_{j}_{k}')
                    model.addConstr(
                        t_d[i] + distance[i, j] / speed + M *
                        (1 - x[i, j, k]) >= t_a[j],
                        name=f'time_cons_max_{i}_{j}_{k}')

    def find_subtours(edges):
        # Create adjacency list
        adj = {i: [] for i in nodes}
        for i, j in edges:
            adj[i].append(j)

        # Find all subtours
        unvisited = set(nodes)
        subtours = []
        while unvisited:
            current = next(iter(unvisited))
            subtour = []
            while current in unvisited:
                unvisited.remove(current)
                subtour.append(current)
                next_nodes = adj.get(current, [])
                if not next_nodes:
                    break
                current = next_nodes[0]
            if len(subtour) < len(nodes):
                subtours.append(subtour)
        return subtours

    def subtour_cb(model, where):
        if where == GRB.Callback.MIPSOL:
            # Get values of binary variables
            x_vals = model.cbGetSolution(model._vars)
            edges = [(i, j, k) for i in nodes for j in nodes for k in trucks
                     if i != j and x_vals[i, j, k] > 0.5]

            # Find subtours for each truck
            for k in trucks:
                truck_edges = [(i, j) for i, j, k_idx in edges if k_idx == k]
                subtours = find_subtours(truck_edges)

                # Add subtour elimination constraints
                for S in subtours:
                    if len(S) < len(nodes) and 0 not in S:
                        model.cbLazy(
                            gp.quicksum(x[i, j, k] for i in S for j in S
                                        if i != j) <= len(S) - 1)

    # Enable lazy constraints
    model._vars = x
    model.Params.lazyConstraints = 1

    # Optimize model with callback
    model.optimize(subtour_cb)

    # --- Return Results ---
    if model.status == GRB.OPTIMAL:
        return {"status": "optimal", "obj": model.ObjVal}
    else:
        return {"status": f"{model.status}"}


if __name__ == "__main__":
    result = solve_vrptw()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 78.78396815014308