import numpy as np
from scipy.optimize import milp, LinearConstraint, Bounds
# Decision variables:
#
# x1A, x1B, x1C, x1D, x1E, x1F
# x2A, x2B, x2C, x2D, x2E, x2F
# x3A, x3B, x3C, x3D, x3E, x3F
# x4A, x4B, x4C, x4D, x4E, x4F
# z1, z2, z3, z4
#
# xij = 1 if area j is assigned to facility i
# zi = 1 if facility i is opened
c = np.array([
3*150, 4*100, 5*180, 4*150, 3*120, 4*100,
4*150, 3*100, 2*180, 5*150, 4*120, 3*100,
3*150, 2*100, 5*180, 4*150, 3*120, 5*100,
5*150, 3*100, 4*180, 2*150, 3*120, 4*100,
2000, 1800, 2200, 2500
])
# Demand assignment constraints:
# Each affected area must be assigned to exactly one facility.
A_demand = np.array([
[1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0], # Area A
[0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0], # Area B
[0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0], # Area C
[0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0], # Area D
[0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0], # Area E
[0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1], # Area F
])
A_demand = np.hstack([A_demand, np.zeros((6, 4))])
demand_lb = np.ones(6)
demand_ub = np.ones(6)
# Capacity constraints:
# demand_A*x1A + ... + demand_F*x1F <= capacity_1*z1
A_capacity = np.array([
[150, 100, 180, 150, 120, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -400, 0, 0, 0],
[0, 0, 0, 0, 0, 0, 150, 100, 180, 150, 120, 100, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -350, 0, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 100, 180, 150, 120, 100, 0, 0, 0, 0, 0, 0, 0, 0, -450, 0],
[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 100, 180, 150, 120, 100, 0, 0, 0, -500],
])
capacity_lb = np.array([-np.inf, -np.inf, -np.inf, -np.inf])
capacity_ub = np.array([0, 0, 0, 0])
# Combine constraints
A = np.vstack([A_demand, A_capacity])
lb = np.concatenate([demand_lb, capacity_lb])
ub = np.concatenate([demand_ub, capacity_ub])
constraints = LinearConstraint(A, lb, ub)
# Bounds:
# All xij and zi are binary, so bounds are 0 <= variable <= 1
bounds = Bounds(
lb=np.zeros(28),
ub=np.ones(28)
)
# Integrality:
# 1 means integer.
# Since all variables are bounded between 0 and 1, they are binary.
integrality = np.ones(28)
result = milp(
c=c,
constraints=constraints,
bounds=bounds,
integrality=integrality
)
print("Optimisation Success:", result.success)
print("Objective Function Value:", result.fun)
print("Decision Vars:", result.x)
# Display solution more clearly
x = result.x[:24].reshape(4, 6)
z = result.x[24:]
facility_names = ["F1", "F2", "F3", "F4"]
area_names = ["A", "B", "C", "D", "E", "F"]
demand = np.array([150, 100, 180, 150, 120, 100])
capacity = np.array([400, 350, 450, 500])
fac = [facility_names[i] for i in range(4) if round(z[i]) == 1]
print(f"\nOpened facilities: {fac}")
assignments = []
for j in range(6):
i = np.argmax(x[:, j])
assignments.append((area_names[j], facility_names[i]))
print(f"\nAssignments: {assignments}")
print("\nFacility usage:")
for i in range(4):
used = np.sum(demand * x[i, :])
print(f"{facility_names[i]}: {used:.0f} / {capacity[i]}")