SupplyChain Logistics Co. is responsible for distributing two types of products, Product A and Product B. The company needs to meet the market demand for these two products across four quarters of the year (Q1, Q2, Q3, Q4) while aiming to minimize the total annual operating costs, which include labor costs and inventory holding costs. The demand for Product A in the four quarters is 800, 600, 700, and 900 units, respectively; the demand for Product B in the four quarters is 500, 400, 600, and 300 units, respectively.Workers operate on a fixed rotation schedule: each worker works for three consecutive quarters and then takes one quarter off. To ensure that workers take their leave in the same quarter each year, workers are divided into four groups, taking leave in Q1, Q2, Q3, and Q4, respectively. The company must decide the number of workers in each group, and this number remains constant throughout the year. Each on-duty worker, in a working quarter, can only choose to handle one type of product: either a maximum of 60 units of Product A or a maximum of 40 units of Product B. They cannot split their capacity to handle both products within the same quarter. The wage for each worker per working quarter is $600, so the annual wage for each worker is $1800.At the end of each quarter, an inventory holding cost of $70 per unit is incurred for any remaining product (either A or B) in the warehouse. It is assumed that at the beginning of the first quarter of the first year, the initial inventory for both Product A and Product B is 0 units. To simplify the model, it is assumed that at the end of the fourth quarter each year, the inventory for both Product A and Product B must be 0 units.The company's optimization objective is: to determine the number of workers in each rotation group and how to assign on-duty workers to produce Product A and Product B each quarter, in order to meet all quarterly product demands and minimize the total annual operating costs (including the total wages for all registered workers and the total inventory holding costs incurred at the end of all quarters).

Problem type: ILP (Medium: 52 variables, 38 constraints, 108 nonzeros)

Mathematical model:
Set:
1. Product
The set of products, \( P = \{1, 2\} \)  # 1: A, 2: B
2. Quarter
The set of quarters, \( Q = \{1, 2, 3, 4\} \)
3. WorkerGroup
The set of worker groups, \( G = \{1, 2, 3, 4\} \) 

Parameter:
1. HandleProductPerWorkerPerQuarter
[60, 40]
2. WorkerCostPerYear
1800
3. InventoryHoldingCost
70
4. Demand
[[800, 600, 700, 900], [500, 400, 600, 300]]

Decision variable:
1. WorkersInGroup
Integer variable, \( WorkersInGroup[g] \forall g \in G \), the number of workers in group \( g \).
2. ProductProduced
Integer variable, \( ProductProduced[p][q] \forall p \in P, q \in Q \), the amount of product \( p \) produced in quarter \( q \).
3. ProductStorage
Integer variable, \( ProductStorage[p][q] \forall p \in P, q \in Q \), the amount of product \( p \) stored at the end of quarter \( q \).
4. WorkersOnDuty
Integer variable, \( WorkersOnDuty[g][p][q] \forall g \in G, p \in P, q \in Q \), the number of workers in group \( g \) on duty for product \( p \) in quarter \( q \).

Objective:
1. Minimize the total cost.
min: \sum_{g \in G} (WorkersInGroup[g] * WorkerCostPerYear) + \sum_{p \in P, q \in Q} (ProductStorage[p][q] * InventoryHoldingCost)

Constraint:
1. Demand constraint.
ProductProduced[p][1] - ProductStorage[p][1] \geq Demand[p][1] \forall p \in P
ProductProduced[p][q] + ProductStorage[p][q-1] - ProductStorage[p][q] \geq Demand[p][q] \forall p \in P, q \in Q/{1}
2. Production handling constraint.
\sum_{g \in G} WorkersOnDuty[g][p][q] * HandleProductPerWorkerPerQuarter[p] \geq ProductProduced[p][q] \forall p \in P, q \in Q
3. Worker group constraint.
\sum_{p \in P} WorkersOnDuty[g][p][q] \leq WorkersInGroup[g] \forall g \in G, q \in Q
4. Worker off-duty constraint.
WorkersOnDuty[g][p][g] = 0 \forall g \in G, p \in P
5. Inventory constraint.
ProductStorage[p][1] = 0 \forall p \in P
ProductStorage[p][4] = 0 \forall p \in P  # End of year inventory must be 0
4. Non-negativity constraint.
ProductProduced[p][q] \geq 0 \forall p \in P, q \in Q
ProductStorage[p][q] \geq 0 \forall p \in P, q \in Q

Type:
Integer, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_workforce_planning_optimization(
    HandleProductPerWorkerPerQuarter=[60, 40],
    WorkerCostPerYear=1800,
    InventoryHoldingCost=70,
    Demand=[[800, 600, 700, 900], [500, 400, 600, 300]]
):
    """
    Models and solves the supply chain logistics and workforce planning problem.
    """
    # Create a new model
    model = gp.Model("SupplyChain_Logistics_Workforce_Planning")

    # Sets
    P = range(len(HandleProductPerWorkerPerQuarter))  # Products
    Q = range(len(Demand[0]))  # Quarters
    G = range(len(Demand[0]))  # Worker Groups

    # Decision Variables
    WorkersInGroup = model.addVars(G, vtype=GRB.INTEGER, lb=0, name="WorkersInGroup")
    ProductProduced = model.addVars(P, Q, vtype=GRB.INTEGER, lb=0, name="ProductProduced")
    ProductStorage = model.addVars(P, Q, vtype=GRB.INTEGER, lb=0, name="ProductStorage")
    WorkersOnDuty = model.addVars(G, P, Q, vtype=GRB.INTEGER, lb=0, name="WorkersOnDuty")

    # Objective Function: Minimize total cost
    labor_cost = gp.quicksum(WorkersInGroup[g] * WorkerCostPerYear for g in G)
    inventory_cost = gp.quicksum(ProductStorage[p, q] * InventoryHoldingCost for p in P for q in Q)
    total_cost = labor_cost + inventory_cost

    model.setObjective(total_cost, GRB.MINIMIZE)

    # Constraints
    # 1. Demand constraints
    for p in P:
        # First quarter (no previous inventory)
        model.addConstr(
            ProductProduced[p, 0] - ProductStorage[p, 0] >= Demand[p][0],
            f"Demand_P{p}_Q1"
        )

        # Subsequent quarters
        for q in range(1, 4):
            model.addConstr(
                ProductProduced[p, q] + ProductStorage[p, q - 1] - ProductStorage[p, q] >= Demand[p][q],
                f"Demand_P{p}_Q{q + 1}"
            )

    # 2. Production handling constraints
    for p in P:
        for q in Q:
            model.addConstr(
                gp.quicksum(WorkersOnDuty[g, p, q] * HandleProductPerWorkerPerQuarter[p] for g in G) >= ProductProduced[p, q],
                f"Production_P{p}_Q{q + 1}"
            )

    # 3. Worker group constraints
    for g in G:
        for q in Q:
            # Workers in group g are off-duty in quarter (g+1)%4
            if q == g:
                # Workers in this group are on leave this quarter
                for p in P:
                    model.addConstr(WorkersOnDuty[g, p, q] == 0, f"OffDuty_G{g + 1}_Q{q + 1}")
            else:
                # Workers on duty can only handle one product type per quarter
                model.addConstr(
                    gp.quicksum(WorkersOnDuty[g, p, q] for p in P) <= WorkersInGroup[g],
                    f"WorkerCapacity_G{g + 1}_Q{q + 1}"
                )

    # 4. End of year inventory must be zero
    for p in P:
        model.addConstr(ProductStorage[p, 3] == 0, f"EndYearInventory_P{p}")

    # 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_workforce_planning_optimization()
    print(result)


Solution status: OPTIMAL
Optimal objective value: 59400