To strengthen grassroots medical services, the Health Bureau of a city has equipped a mobile medical vehicle to regularly provide basic medical check-ups and medicines to various communities in the suburbs. To improve service efficiency, it is necessary to optimize the route of the mobile medical vehicle. The starting point of the medical vehicle is the Central City Hospital, located at coordinates (0, 0). The information for the 10 communities to be served is as follows:

|  COMMUNITY ID |        (x, y)       | TIME WINDOW |
|---------------|---------------------|-------------|
| C1            | (-5.87, -3.3)       | [8, 12]     |
| C2            | (-4.1, 4.6)         | [9, 13]     |
| C3            | (-2.27, -7.45)      | [10, 15]    |
| C4            | (4.36, 3.38)        | [11, 15]    |
| C5            | (9.05, 5.02)        | [9, 16]     |
| C6            | (-7.46, 6.97)       | [9, 17]     |
| C7            | (7.75, 7.38)        | [10, 18]    |
| C8            | (0.06, -4.25)       | [13, 19]    |
| C9            | (-6.13, -9.78)      | [8, 16]     |
| C10           | (3.25, 9.86)        | [8, 16]     |

Each community has a time window during which the medical vehicle must arrive. The medical vehicle departs from the Central City Hospital at 8:00 AM and travels at a speed of 15 km/h. After visiting all communities, the vehicle must return to the Central City Hospital. What is the shortest travel distance? (Distances are calculated as straight-line distances in kilometers.)

Problem type: MILP (Medium: 132 variables, 272 constraints, 920 nonzeros)

Mathematical model:
Set:
1. Node
The set of nodes, \( N = \{0, 1, \dots, CommunityNum\} \)
# where node 0 is the starting point (Central City Hospital) and nodes 1 to n are the communities to be served.

Parameter:
1. CommunityNum
10
2. Coordinate
[(0, 0), (-5.87, -3.3), (-4.1, 4.6), (-2.27, -7.45), (4.36, 3.38), (9.05, 5.02), (-7.46, 6.97), (7.75, 7.38), (0.06, -4.25), (-6.13, -9.78), (3.25, 9.86)]
3. Timewindow
[(8, 16), (8, 12), (9, 13), (10, 15), (11, 15), (9, 16), (9, 17), (10, 18), (13, 19), (8, 16), (8, 16)]
4. StartTime
8
5. Speed
15
6. M
10000

Decision variable:
1. X
Binary variable, \( x_{ij} \forall i \in N, j \in N\), where \( x_{ij} = 1 \) if the vehicle departs from node \( i \) and arrives at node \( j \), otherwise \( x_{ij} = 0 \).
2. T^a
Continous variable, \( t^a_i \forall i \in N\), where \( t^a_i \) is the time at which the vehicle 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 vehicle departs from node \( i \).

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

Constraint:
1. Each community must be visited exactly once.
\sum_{i \in N} x_{ij} = 1 \forall j \in N
2. Each community must be exited exactly once.
\sum_{j \in N} x_{ij} = 1 \forall i \in N
3. Eliminate sub-tours.
\sum_{i \in N} \sum_{j \in N} x_{ij} \leq |S| - 1 \forall S \subseteq N, 1 < |S| < |N|
4. The vehicle must arrive at each community within its time window.
t^a_i \geq Timewindow[i][0] \forall i \in N
5. The vehicle must depart from each community within its time window.
t^d_i \leq Timewindow[i][1] \forall i \in N
6. The vehicle's arriveal time at each node must be 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_{ij}) \leq t^a_j \forall i, j \in N
t^d_i + \frac{Distance[i][j]}{Speed} + M * (1 - x_{ij}) \geq t^a_j \forall i, j \in N

Type:
Mixed, Linear, Linear
MILP

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


def solve_time_windowed_tsp(
    coordinates=[(0, 0), (-5.87, -3.3), (-4.1, 4.6), (-2.27, -7.45),
                 (4.36, 3.38), (9.05, 5.02), (-7.46, 6.97), (7.75, 7.38),
                 (0.06, -4.25), (-6.13, -9.78), (3.25, 9.86)],
    time_windows=[(8, 16), (8, 12), (9, 13), (10, 15), (11, 15), (9, 16),
                  (9, 17), (10, 18), (13, 19), (8, 16), (8, 16)],
    speed=15,
    M=10000,
    start_time=8):
    """
    Solves the Traveling Salesperson Problem with Time Windows (TSPTW).
    """
    community_num = len(coordinates) - 1
    nodes = range(community_num + 1)  # 0 is hospital, 1-10 are communities

    # 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("TimeWindowedTSP")

    # --- Decision variables ---
    # x[i,j] = 1 if path goes from i to j
    x = model.addVars(nodes, nodes, vtype=GRB.BINARY, name='x')
    for i in nodes:
        x[i, i].ub = 0

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

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

    # Hospital departure time is fixed
    t_d[0].lb = start_time
    t_d[0].ub = start_time

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

    # --- Constraints ---
    # Constraint 1: Each node must be visited exactly once
    model.addConstrs((gp.quicksum(x[i, j] for i in nodes if i != j) == 1
                      for j in nodes if j != 0),
                     name='visit_to')

    # Constraint 2: Must leave each node exactly once
    model.addConstrs((gp.quicksum(x[i, j] for j in nodes if i != j) == 1
                      for i in nodes if i != 0),
                     name='leave_from')

    # Hospital constraints: must leave once and return once
    model.addConstr(gp.quicksum(x[0, j] for j in nodes if j != 0) == 1,
                    name='leave_hospital')
    model.addConstr(gp.quicksum(x[i, 0] for i in nodes if i != 0) == 1,
                    name='return_hospital')

    # Time window constraints
    for i in nodes[1:]:  # Skip hospital
        # Departure time must be after arrival time
        model.addConstr(t_d[i] >= t_a[i], name=f'depart_after_arrival_{i}')

    # Time consistency constraints
    for i in nodes:
        for j in nodes:
            if i != j:
                # If we go 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]) <=
                    t_a[j], f'time_consistency_min_{i}_{j}')
                model.addConstr(
                    t_d[i] + distance[i, j] / speed + M * (1 - x[i, j]) >=
                    t_a[j], f'time_consistency_max_{i}_{j}')

    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) for i, j in x_vals.keys() if x_vals[i, j] > 0.5]

            # Find subtours
            subtours = find_subtours(edges)

            # Add subtour elimination constraints
            for S in subtours:
                if len(S) < len(nodes):
                    model.cbLazy(
                        gp.quicksum(x[i, j] 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_time_windowed_tsp()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 70.65521686057