A retail chain wants to open warehouses in 3 potential locations to serve 5 customer zones. The distances (in km) between each potential warehouse location and customer zone are given in the table below. The goal is to select exactly 2 warehouse locations that minimize the total distance between each customer zone and its nearest warehouse. 

Customer Zones: {C1, C2, C3, C4, C5}
Potential Warehouse Locations: {W1, W2, W3}

Distance Matrix (km):
|       | C1 | C2 | C3 | C4 | C5 |
|-------|----|----|----|----|----|
|W1     | 10 | 15 | 12 | 8  | 20 |
|W2     | 8  | 10 | 15 | 12 | 18 |
|W3     | 12 | 8  | 10 | 15 | 12 |

Problem type: ILP (Small: 18 variables, 21 constraints, 48 nonzeros)

Mathematical model:
Set:
1. CustomerZones
The set of customer zones, \( C = \{C1, C2, C3, C4, C5\} \)
2. WarehouseLocations
The set of potential warehouse locations, \( W = \{W1, W2, W3\} \)

Parameter:
1. DistanceMatrix
The distance between warehouse location \( w \in W \) and customer zone \( c \in C \), denoted as \( d_{wc} \)
Values: 
d(W1,C1)=10, d(W1,C2)=15, ..., d(W3,C5)=12 (from the distance matrix above)
2. p
The number of warehouses to be opened, p=2

Decision variable:
1. WarehouseOpen
Binary variable \( y_w \in \{0, 1\} \) for all \( w \in W \), where \( y_w = 1 \) if warehouse \( w \) is opened, and \( y_w = 0 \) otherwise
2. Assignment
Binary variable \( x_{wc} \in \{0, 1\} \) for all \( w \in W, c \in C \), where \( x_{wc} = 1 \) if customer zone \( c \) is assigned to warehouse \( w \), and \( x_{wc} = 0 \) otherwise

Objective:
Minimize the total distance between each customer zone and its assigned warehouse
min: \( \sum_{w \in W} \sum_{c \in C} d_{wc} \cdot x_{wc} \)

Constraint:
1. Exactly p warehouses must be opened
\( \sum_{w \in W} y_w = p \)
2. Each customer zone must be assigned to exactly one warehouse
\( \sum_{w \in W} x_{wc} = 1, \forall c \in C \)
3. A customer zone can only be assigned to an opened warehouse
\( x_{wc} \leq y_w, \forall w \in W, \forall c \in C \)

Type:
Binary, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_warehouse_location(
    CustomerZones=['C1', 'C2', 'C3', 'C4', 'C5'],
    WarehouseLocations=['W1', 'W2', 'W3'],
    DistanceMatrix={
        'W1': {'C1': 10, 'C2': 15, 'C3': 12, 'C4': 8, 'C5': 20},
        'W2': {'C1': 8, 'C2': 10, 'C3': 15, 'C4': 12, 'C5': 18},
        'W3': {'C1': 12, 'C2': 8, 'C3': 10, 'C4': 15, 'C5': 12}
    },
    p=2
):
    """Solve the warehouse location selection problem using Gurobi."""
    # Create a new model
    model = gp.Model("WarehouseLocation")

    # Decision variables
    y = model.addVars(WarehouseLocations, vtype=GRB.BINARY, name="Open")
    x = model.addVars(WarehouseLocations, CustomerZones, vtype=GRB.BINARY, name="Assign")

    # Objective: Minimize total distance
    model.setObjective(
        gp.quicksum(DistanceMatrix[w][c] * x[w, c] for w in WarehouseLocations for c in CustomerZones),
        GRB.MINIMIZE
    )

    # Constraints
    model.addConstr(gp.quicksum(y[w] for w in WarehouseLocations) == p, "Open_p_warehouses")
    for c in CustomerZones:
        model.addConstr(gp.quicksum(x[w, c] for w in WarehouseLocations) == 1, f"Assign_one_warehouse_{c}")
    for w in WarehouseLocations:
        for c in CustomerZones:
            model.addConstr(x[w, c] <= y[w], f"Assign_only_if_open_{w}_{c}")

    # Optimize the model
    model.optimize()

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


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

Solution status: OPTIMAL
Optimal objective value: 48