A distribution company is planning its logistics network for the upcoming year to serve 15 retail stores from 5 potential hub locations. The company aims to minimize its total annual operational cost, which consists of fixed daily costs for opening hubs and variable transportation costs for delivering goods.

The core decisions are which of the 5 potential hubs to open and which open hub should be assigned to serve each of the 15 retail stores.

Each potential hub has a specific daily operating cost, a maximum daily capacity, and an operational reliability score. The company has a strategic requirement that the average reliability of all opened hubs must be at least 90%. The transportation cost is fixed at $0.80 per unit per mile, and the planning horizon is a full year (365 days).

The plan must assign each store to exactly one hub, ensure that no hub's daily capacity is exceeded, meet the minimum average reliability standard, and minimize the total annual cost.

Hub Data:  

| Hub ID | Daily Setup Cost | Daily Capacity (units) | Reliability |
|:------:|:----------------:|:----------------------:|:-----------:|
| 1      | $100,000         | 1000                   | 0.85        |
| 2      | $80,000          | 800                    | 0.90        |
| 3      | $120,000         | 1500                   | 0.70        |
| 4      | $90,000          | 1200                   | 0.95        |
| 5      | $110,000         | 900                    | 0.80        |

Store Demand Data (units per day):  

| Store | Demand | | Store | Demand | | Store | Demand |
|:-----:|:------:| |:-----:|:------:| |:-----:|:------:|
| 1     | 151    | | 6     | 187    | | 11    | 121    |
| 2     | 188    | | 7     | 199    | | 12    | 173    |
| 3     | 174    | | 8     | 104    | | 13    | 100    |
| 4     | 161    | | 9     | 116    | | 14    | 187    |
| 5     | 111    | | 10    | 88     | | 15    | 199    |

Distance Matrix (Hub to Store, in miles):  

| Hub | S1 | S2 | S3 | S4 | S5 | S6 | S7 | S8 | S9 | S10 | S11 | S12 | S13 | S14 | S15 |
|:---:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:--:|:---:|:---:|:---:|:---:|:---:|:---:|
|  1  | 18 | 45 | 42 | 38 | 18 | 44 | 47 | 12 | 20 | 10  | 23  | 41  | 10  | 44  | 47  |
|  2  | 30 | 27 | 21 | 16 | 17 | 12 | 29 | 21 | 38 | 48  | 23  | 15  | 12  | 20  | 26  |
|  3  | 16 | 13 | 21 | 31 | 30 | 39 | 44 | 44 | 18 | 12  | 13  | 16  | 42  | 49  | 11  |
|  4  | 34 | 25 | 30 | 20 | 24 | 28 | 42 | 39 | 31 | 44  | 34  | 11  | 36  | 16  | 23  |
|  5  | 49 | 41 | 17 | 34 | 16 | 49 | 49 | 28 | 42 | 18  | 46  | 23  | 29  | 21  | 49  |

Problem type: ILP (Medium: 80 variables, 96 constraints, 309 nonzeros)

Mathematical model:
Set:
1. Hubs
   The set of potential hubs, \( H = \{1, 2, 3, 4, 5\} \)
2. Stores
   The set of retail stores, \( S = \{1, 2, ..., 15\} \)

Parameter:
1. FixedCost_h
   The fixed daily operating cost for opening hub \( h \): [100000, 80000, 120000, 90000, 110000]
2. Capacity_h
   The maximum daily handling capacity of hub \( h \): [1000, 800, 1500, 1200, 900]
3. Reliability_h
   The operational reliability score of hub \( h \): [0.85, 0.9, 0.7, 0.95, 0.8]
4. Demand_s
   The daily demand of store \( s \): [151, 188, 174, 161, 111, 187, 199, 104, 116, 88, 121, 173, 100, 187, 199]
5. Distance_hs
   The distance matrix between hub \( h \) and store \( s \) as specified in the table.
6. TransportCost
   The variable transportation cost per unit per mile: 0.8
7. MinAvgReliability
   The minimum required average reliability of opened hubs: 0.9
8. Days
   The number of days in the planning horizon: 365

Decision variable:
1. OpenHub_h
   Binary variable \( x_h \in \{0, 1\} \) for all \( h \in H \), where \( x_h = 1 \) if hub \( h \) is opened, and 0 otherwise.
2. AssignStore_hs
   Binary variable \( y_{hs} \in \{0, 1\} \) for all \( h \in H, s \in S \), where \( y_{hs} = 1 \) if store \( s \) is assigned to hub \( h \), and 0 otherwise.

Objective:
Minimize the total annual cost, which is the sum of daily fixed and transportation costs multiplied by the number of days.
min: \( \text{Days} \cdot \left( \sum_{h \in H} \text{FixedCost}_h \cdot x_h + \sum_{h \in H} \sum_{s \in S} \text{TransportCost} \cdot \text{Demand}_s \cdot \text{Distance}_{hs} \cdot y_{hs} \right) \)

Constraint:
1. Store Assignment
   Each store must be assigned to exactly one hub.
   \( \sum_{h \in H} y_{hs} = 1, \quad \forall s \in S \)
2. Hub Capacity
   The total demand of all stores assigned to a hub cannot exceed that hub's capacity. This constraint is only active if the hub is open.
   \( \sum_{s \in S} \text{Demand}_s \cdot y_{hs} \leq \text{Capacity}_h \cdot x_h, \quad \forall h \in H \)
3. Minimum Average Reliability
   The sum of reliability scores of all opened hubs must be greater than or equal to the minimum average reliability multiplied by the number of opened hubs.
   \( \sum_{h \in H} \text{Reliability}_h \cdot x_h \geq \text{MinAvgReliability} \cdot \left( \sum_{h \in H} x_h \right) \)
4. Logical Consistency
   A store can only be assigned to a hub if that hub is open.
   \( y_{hs} \leq x_h, \quad \forall h \in H, s \in S \)

Type:
Binary, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_hub_location(
    H=[1, 2, 3, 4, 5],
    S=list(range(1, 16)),
    SetupCost={1: 100, 2: 80, 3: 120, 4: 90, 5: 110},
    Capacity={1: 1000, 2: 800, 3: 1500, 4: 1200, 5: 900},
    Reliability={1: 0.85, 2: 0.9, 3: 0.7, 4: 0.95, 5: 0.8},
    TransportCost=0.8,
    MinAvgReliability=0.9,
    Days=365,
    Demand_data=None,
    Distance_data=None
):
    """
    Solve the hub location problem.
    """
    if Demand_data is None:
        Demand = {
            1: 151, 2: 188, 3: 174, 4: 161, 5: 111, 6: 187, 7: 199, 8: 104,
            9: 116, 10: 88, 11: 121, 12: 173, 13: 100, 14: 187, 15: 199
        }
    else:
        Demand = Demand_data

    if Distance_data is None:
        Distance = {
            (1, 1): 18, (1, 2): 45, (1, 3): 42, (1, 4): 38, (1, 5): 18, (1, 6): 44, (1, 7): 47, (1, 8): 12, (1, 9): 20, (1, 10): 10, (1, 11): 23, (1, 12): 41, (1, 13): 10, (1, 14): 44, (1, 15): 47,
            (2, 1): 30, (2, 2): 27, (2, 3): 21, (2, 4): 16, (2, 5): 17, (2, 6): 12, (2, 7): 29, (2, 8): 21, (2, 9): 38, (2, 10): 48, (2, 11): 23, (2, 12): 15, (2, 13): 12, (2, 14): 20, (2, 15): 26,
            (3, 1): 16, (3, 2): 13, (3, 3): 21, (3, 4): 31, (3, 5): 30, (3, 6): 39, (3, 7): 44, (3, 8): 44, (3, 9): 18, (3, 10): 12, (3, 11): 13, (3, 12): 16, (3, 13): 42, (3, 14): 49, (3, 15): 11,
            (4, 1): 34, (4, 2): 25, (4, 3): 30, (4, 4): 20, (4, 5): 24, (4, 6): 28, (4, 7): 42, (4, 8): 39, (4, 9): 31, (4, 10): 44, (4, 11): 34, (4, 12): 11, (4, 13): 36, (4, 14): 16, (4, 15): 23,
            (5, 1): 49, (5, 2): 41, (5, 3): 17, (5, 4): 34, (5, 5): 16, (5, 6): 49, (5, 7): 49, (5, 8): 28, (5, 9): 42, (5, 10): 18, (5, 11): 46, (5, 12): 23, (5, 13): 29, (5, 14): 21, (5, 15): 49
        }
    else:
        Distance = Distance_data

    model = gp.Model("HubLocationProblem")

    OpenHub = model.addVars(H, vtype=GRB.BINARY, name="OpenHub")
    AssignStore = model.addVars(H, S, vtype=GRB.BINARY, name="AssignStore")

    setup_cost = gp.quicksum(SetupCost[h] * 1000 * OpenHub[h] for h in H)
    transport_cost = gp.quicksum(
        TransportCost * Demand[s] * Distance[h, s] * AssignStore[h, s]
        for h in H for s in S
    )
    model.setObjective(Days * (setup_cost + transport_cost), GRB.MINIMIZE)

    model.addConstrs((gp.quicksum(AssignStore[h, s] for h in H) == 1 for s in S))
    model.addConstrs((gp.quicksum(Demand[s] * AssignStore[h, s] for s in S) <= Capacity[h] * OpenHub[h] for h in H))
    model.addConstr(gp.quicksum(Reliability[h] * OpenHub[h] for h in H) >= MinAvgReliability * gp.quicksum(OpenHub[h] for h in H))
    model.addConstrs((AssignStore[h, s] <= OpenHub[h] for h in H for s in S))

    model.optimize()

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


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

Solution status: OPTIMAL
Optimal objective value: 110628288