A community food bank plans to dispatch a refrigerated truck on a Friday afternoon to visit 4 partner supermarkets and collect same-day leftover fresh food donations. The truck has a maximum refrigerated capacity of 2 cubic meters and a payload limit of 500 kilograms. The truck will depart from the food bank at 13:00 and must return by 17:00, for a total operating time of 4 hours. The food bank's geographic coordinates are (0, 0), and the units for all location coordinates are kilometers (km). The truck's travel speed is a constant 40 km/h, and the distance between two points is calculated using straight-line Euclidean distance. Additionally, each stop at a supermarket for loading operations requires a fixed 20 minutes. The objective of the task is to plan an optimal route, deciding which supermarkets to visit and which items to collect, in order to maximize the total sum of priority weights of the collected donations. The detailed locations of each supermarket and the information on available donations are shown in the table below:

| Stop | Location Coordinates (km) | Donation Item | Volume (m³) | Weight (kg) | Priority Weight |
|:---:|:---:|:---|:---:|:---:|:---:|
| Supermarket S1 | (5, 10) | Milk | 0.8 | 150 | 80 |
| Supermarket S1 | (5, 10) | Cheese | 0.3 | 50 | 65 |
| Supermarket S2 |(15, 5) | Bread | 1.0 | 100 | 50 |
| Supermarket S2 |(15, 5) | Vegetables | 0.5 | 80 | 40 |
| Supermarket S3 |(20, 15) | Frozen Meat | 0.6 | 200 | 100 |
| Supermarket S3 |(20, 15) | Pasta | 0.4 | 60 | 45 |
| Supermarket S4 |(25, 20) | Fruit | 0.7 | 120 | 60 |
| Supermarket S4 |(25, 20) |Yogurt | 0.4 | 100 | 70 |
| Supermarket S4 |(25, 20) | Berries | 0.2 | 30 | 55 |
| Supermarket S5 |(30, 25) | Eggs | 0.3 | 40 | 75 |
| Supermarket S5 |(30, 25) | Butter | 0.2 | 50 | 50 |
| Supermarket S6 |(35, 30) | Sausages | 0.5 | 110 | 90 |
| Supermarket S6 |(35, 30) | Fish Fillets | 0.4 | 90 | 95 |
| Supermarket S7 |(40, 35) | Juice | 0.6 | 120 | 30 |
| Supermarket S7 |(40, 35) | Canned Soup | 0.8 | 150 | 25 |
| Supermarket S8 |(45, 40) | Salad Mix | 0.9 | 70 | 35 |

Problem type: NLP (Small: 114 variables, 118 constraints, 497 nonzeros)

Mathematical model:
Sets:
1. Nodes
    The set of all locations, \( N = \{0, 1, 2, 3, 4\} \), where node 0 is the Food Bank (Depot) and nodes \( S = \{1, 2, 3, 4\} \) represent the supermarkets.
2. Items
    The set of all available donation items, \( I = \{\text{Milk, Bread, Vegetables, Meat, Fruit, Yogurt}\} \).

Parameters:
1. Item Priority \(p_i\)
    The priority weight for each item \(i \in I\).
2. Item Volume \(v_i\)
    The volume (m³) for each item \(i \in I\).
3. Item Weight \(w_i\)
    The weight (kg) for each item \(i \in I\).
4. Item Location \(L_i\)
    A mapping indicating which supermarket node \(j \in S\) an item \(i \in I\) belongs to.
5. Node Coordinates \((cx_j, cy_j)\)
    The coordinates (km) for each node \(j \in N\).
6. Travel Time \(t_{jk}\)
    The pre-calculated travel time (in minutes) between node \(j\) and node \(k\).
    \(t_{jk} = \frac{\sqrt{(cx_j - cx_k)^2 + (cy_j - cy_k)^2}}{\text{Speed}} \times 60\)
7. Truck Volume Capacity \(V_{max}\)
    The maximum volume capacity of the truck (2 m³).
8. Truck Weight Capacity \(W_{max}\)
    The maximum weight capacity of the truck (500 kg).
9. Total Time Available \(T_{max}\)
    The maximum operational time available (4 hours = 240 minutes).
10. Loading Time \(T_{load}\)
    The fixed time required for loading at each supermarket stop (20 minutes).

Decision Variables
1. Collect Item \(c_i\)
    Binary variable, \(c_i \in \{0, 1\}\) for each item \(i \in I\). \(c_i = 1\) if item \(i\) is collected, 0 otherwise.
2. Visit Supermarket \(y_j\)
    Binary variable, \(y_j \in \{0, 1\}\) for each supermarket \(j \in S\). \(y_j = 1\) if supermarket \(j\) is visited, 0 otherwise.
3. Travel Arc \(z_{jk}\)
    Binary variable, \(z_{jk} \in \{0, 1\}\) for each pair of nodes \(j, k \in N\). \(z_{jk} = 1\) if the truck travels directly from node \(j\) to node \(k\), 0 otherwise.

Objective:
1. Maximize Total Priority
    Maximize the sum of the priority weights of all collected items.
    \(\text{max:} \sum_{i \in I} p_i \cdot c_i\)

Constraints:
1. Volume Capacity Constraint
    The total volume of collected items must not exceed the truck's volume capacity.
    \(\sum_{i \in I} v_i \cdot c_i \le V_{max}\)
2. Weight Capacity Constraint
    The total weight of collected items must not exceed the truck's weight capacity.
    \(\sum_{i \in I} w_i \cdot c_i \le W_{max}\)
3. Time Constraint
    The total travel time plus total loading time must not exceed the available operational time.
    \(\sum_{j \in N} \sum_{k \in N, j \ne k} t_{jk} \cdot z_{jk} + \sum_{j \in S} T_{load} \cdot y_j \le T_{max}\)
4. Item Collection Logic
    An item can only be collected if its corresponding supermarket is visited.
    \(c_i \le y_{L_i} \quad \forall i \in I\)
5. Routing Flow Conservation
    The truck must leave each supermarket it visits exactly once.
    \(\sum_{k \in N, k \ne j} z_{jk} = y_j \quad \forall j \in S\)
    The truck must arrive at each supermarket it visits exactly once.
    \(\sum_{j \in N, j \ne k} z_{jk} = y_k \quad \forall k \in S\)
6. Depot Departure and Return
    The truck must leave the depot once and return to the depot once.
    \(\sum_{k \in S} z_{0k} = 1\)
    \(\sum_{j \in S} z_{j0} = 1\)
7. Subtour Elimination
    Constraints to prevent disconnected loops (e.g., using the MTZ formulation, which requires additional variables \(u_j\) for tour position). A simplified representation is to state the requirement: Ensure a single continuous tour starting and ending at the depot.

Type:
# Pre-calculating travel time linearizes the nonlinear components, but the problem itself is inherently nonlinear.
Integer, Linear, Non-linear
NLP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB
from typing import List, Dict, Tuple
import math

def solve_food_bank_routing(
    supermarkets: Dict[str, Tuple[int, int]] = {
        'S1': (5, 10),
        'S2': (12, 8),
        'S3': (-10, 5),
        'S4': (0, -15),
        'S5': (-15, -10),
        'S6': (15, -5),
        'S7': (8, 18),
        'S8': (-5, -20)
    },
    items: Dict[str, Dict] = {
        # S1 Items
        'Milk':       {'supermarket': 'S1', 'volume': 0.8, 'weight': 150, 'priority': 80},
        'Cheese':     {'supermarket': 'S1', 'volume': 0.3, 'weight': 50,  'priority': 65},
        # S2 Items
        'Bread':      {'supermarket': 'S2', 'volume': 1.0, 'weight': 100, 'priority': 50},
        'Vegetables': {'supermarket': 'S2', 'volume': 0.5, 'weight': 80,  'priority': 40},
        # S3 Items
        'Frozen Meat':{'supermarket': 'S3', 'volume': 0.6, 'weight': 200, 'priority': 100},
        'Pasta':      {'supermarket': 'S3', 'volume': 0.4, 'weight': 60,  'priority': 45},
        # S4 Items
        'Fruit':      {'supermarket': 'S4', 'volume': 0.7, 'weight': 120, 'priority': 60},
        'Yogurt':     {'supermarket': 'S4', 'volume': 0.4, 'weight': 100, 'priority': 70},
        'Berries':    {'supermarket': 'S4', 'volume': 0.2, 'weight': 30,  'priority': 55},
        # S5 Items
        'Eggs':       {'supermarket': 'S5', 'volume': 0.3, 'weight': 40,  'priority': 75},
        'Butter':     {'supermarket': 'S5', 'volume': 0.2, 'weight': 50,  'priority': 50},
        # S6 Items
        'Sausages':   {'supermarket': 'S6', 'volume': 0.5, 'weight': 110, 'priority': 90},
        'Fish Fillets':{'supermarket': 'S6', 'volume': 0.4, 'weight': 90, 'priority': 95},
        # S7 Items
        'Juice':      {'supermarket': 'S7', 'volume': 0.6, 'weight': 120, 'priority': 30},
        'Canned Soup':{'supermarket': 'S7', 'volume': 0.8, 'weight': 150, 'priority': 25},
        # S8 Items
        'Salad Mix':  {'supermarket': 'S8', 'volume': 0.9, 'weight': 70,  'priority': 35},
    },
    depot_coords: Tuple[int, int] = (0, 0),
    truck_volume_capacity: float = 2.0,
    truck_weight_capacity: float = 500.0,
    max_time_mins: int = 240,
    truck_speed_kmh: int = 40,
    loading_time_mins: int = 20,
    M: int = 10000 # A large number for MTZ constraints
):
    """
    Solves the Food Bank Vehicle Routing Problem with Profits using Gurobi.

    Args:
        supermarkets (Dict[str, Tuple[int, int]]): A dictionary mapping supermarket IDs to their (x, y) coordinates.
        items (Dict[str, Dict]): A dictionary mapping item names to their properties.
        depot_coords (Tuple[int, int]): The (x, y) coordinates of the food bank depot.
        truck_volume_capacity (float): The maximum volume capacity of the truck (m³).
        truck_weight_capacity (float): The maximum weight capacity of the truck (kg).
        max_time_mins (int): The total available time for the route in minutes.
        truck_speed_kmh (int): The constant speed of the truck in km/h.
        loading_time_mins (int): The fixed time spent loading at each visited supermarket.
        M (int): A sufficiently large number for Big-M method constraints.
    """

    # --- 1. Data Pre-processing ---
    depot_id = 'Depot'
    supermarket_ids = list(supermarkets.keys())
    nodes = [depot_id] + supermarket_ids
    node_coords = {depot_id: depot_coords, **supermarkets}
    num_nodes = len(nodes)
    item_ids = list(items.keys())

    # Pre-calculate travel times between all nodes
    def euclidean_distance(p1, p2):
        return math.sqrt((p1[0] - p2[0])**2 + (p1[1] - p2[1])**2)

    travel_times = {}
    for j in nodes:
        for k in nodes:
            if j == k:
                continue
            dist = euclidean_distance(node_coords[j], node_coords[k])
            time_mins = (dist / truck_speed_kmh) * 60
            travel_times[j, k] = time_mins

    # --- 2. Gurobi Model Initialization ---
    model = gp.Model("FoodBankRouting")

    # --- 3. Define Decision Variables ---
    # c[i]: 1 if item i is collected, 0 otherwise
    c = model.addVars(item_ids, name="Collect", vtype=GRB.BINARY)
    # y[j]: 1 if supermarket j is visited, 0 otherwise
    y = model.addVars(supermarket_ids, name="Visit", vtype=GRB.BINARY)
    # z[j,k]: 1 if truck travels from node j to node k, 0 otherwise
    z = model.addVars(nodes, nodes, name="Arc", vtype=GRB.BINARY)
    # u[j]: Auxiliary variable for MTZ subtour elimination
    u = model.addVars(nodes, name="MTZ_u", vtype=GRB.CONTINUOUS)

    # --- 4. Set Objective Function ---
    total_priority = gp.quicksum(items[i]['priority'] * c[i] for i in item_ids)
    model.setObjective(total_priority, GRB.MAXIMIZE)

    # --- 5. Add Constraints ---
    # Constraint 1: Volume Capacity
    model.addConstr(gp.quicksum(items[i]['volume'] * c[i] for i in item_ids) <= truck_volume_capacity, "VolumeCapacity")

    # Constraint 2: Weight Capacity
    model.addConstr(gp.quicksum(items[i]['weight'] * c[i] for i in item_ids) <= truck_weight_capacity, "WeightCapacity")

    # Constraint 3: Time Constraint
    total_travel_time = gp.quicksum(travel_times[j, k] * z[j, k] for j in nodes for k in nodes if j != k)
    total_loading_time = gp.quicksum(loading_time_mins * y[j] for j in supermarket_ids)
    model.addConstr(total_travel_time + total_loading_time <= max_time_mins, "TimeLimit")

    # Constraint 4: Item Collection Logic
    for i in item_ids:
        supermarket_of_item = items[i]['supermarket']
        model.addConstr(c[i] <= y[supermarket_of_item], f"CollectLogic_{i}")

    # Constraint 5: Routing Flow Conservation
    for j in supermarket_ids:
        # If a supermarket is visited, truck must enter and leave it exactly once
        model.addConstr(gp.quicksum(z[k, j] for k in nodes if k != j) == y[j], f"Flow_In_{j}")
        model.addConstr(gp.quicksum(z[j, k] for k in nodes if k != j) == y[j], f"Flow_Out_{j}")

    # Constraint 6: Depot Departure and Return
    model.addConstr(gp.quicksum(z[depot_id, k] for k in supermarket_ids) == 1, "Depot_Leave")
    model.addConstr(gp.quicksum(z[j, depot_id] for j in supermarket_ids) == 1, "Depot_Return")
    
    # Disallow travel from a node to itself
    for j in nodes:
        model.addConstr(z[j,j] == 0, f"No_Self_Loop_{j}")

    # Constraint 7: Subtour Elimination (MTZ formulation)
    for j in supermarket_ids:
        model.addConstr(u[j] >= 2, f"MTZ_Lower_{j}")
        model.addConstr(u[j] <= num_nodes, f"MTZ_Upper_{j}")

    for j in supermarket_ids:
        for k in supermarket_ids:
            if j != k:
                model.addConstr(u[j] - u[k] + num_nodes * z[j, k] <= num_nodes - 1, f"MTZ_Main_{j}_{k}")

    # --- 6. Solve the Model ---
    model.optimize()

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


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


Solution status: OPTIMAL
Optimal objective value: 440