Warehouse 24 operates around the clock to fulfill online orders. Workers are scheduled in two consecutive 6-hour shifts and are paid $12 per hour. The possible shifts are as follows: midnight–6 A.M., 6 A.M.–noon, noon–6 P.M., and 6 P.M.–midnight. During each shift, the following numbers of orders arrive: midnight–6 A.M., 150; 6 A.M.–noon, 250; noon–6 P.M., 400; 6 P.M.–midnight, 300. Each worker can process up to 60 orders per shift. To account for customer dissatisfaction due to delayed orders, any order that remains unprocessed at the end of a shift incurs a penalty of $1. The warehouse ensures that all orders received by midnight are processed by the start of the next day, so the midnight–6 A.M. shift begins with no pending orders. How can Warehouse 24 minimize the total cost of labor and customer dissatisfaction penalties while meeting its order processing requirements?

| Shift              | Orders Received |  
|--------------------|-----------------|  
| Midnight–6 A.M.    | 150             |  
| 6 A.M.–Noon        | 250             |  
| Noon–6 P.M.        | 400             |  
| 6 P.M.–Midnight    | 300             |

Problem type: ILP (Small: 12 variables, 9 constraints, 20 nonzeros)

Mathematical model:
Set:
1. Shift
The set of shifts, \( S = \{1, 2, 3, 4\} \)  # 1: Midnight–6 A.M., 2: 6 A.M.–Noon, 3: Noon–6 P.M., 4: 6 P.M.–Midnight

Parameter:
1. OrdersReceived
[150, 250, 400, 300]  # number of orders received in each shift
2. OrdersPerWorkerPerShift
60  # number of orders each worker can process in a shift
3. WagePerHour
12  # wage per hour for each worker in dollars
4. ShiftDuration
6  # duration of each shift in hours
5. PenaltyPerUnprocessedOrder
1  # penalty for each unprocessed order in dollars

Decision variable:
1. WorkersScheduled
Integer variable, \( WorkersScheduled[s] \forall s \in S \), the number of workers scheduled for shift \( s \).
2. OrdersProcessed
Integer variable, \( OrdersProcessed[s] \forall s \in S \), the number of orders processed during shift \( s \).
3. UnprocessedOrders
Integer variable, \( UnprocessedOrders[s] \forall s \in S \), the number of unprocessed orders at the end of shift \( s \).

Objective:
1. Minimize the total cost of labor and penalties.
min: \sum_{s \in S} (WagePerHour * ShiftDuration * WorkersScheduled[s]) + \sum_{s \in S} (PenaltyPerUnprocessedOrder * (OrdersReceived[s]+UnprocessedOrders[s-1] - OrdersProcessed[s])) 
where \( UnprocessedOrders[1] = OrdersReceived[1] - OrdersProcessed[1] \) for the first shift, and \( UnprocessedOrders[s] = OrdersReceived[s] + UnprocessedOrders[s-1] - OrdersProcessed[s] \) for subsequent shifts.

Constraint:
1. Order processing constraint.
OrdersProcessed[s] \leq WorkersScheduled[s] * OrdersPerWorkerPerShift \forall s \in S
2. Unprocessed orders constraint.
UnprocessedOrders[s] = OrdersReceived[s] + UnprocessedOrders[s-1] - OrdersProcessed[s] \forall s \in S, s > 1
UnprocessedOrders[1] = OrdersReceived[1] - OrdersProcessed[1]  # for the first shift, no previous unprocessed orders
3. Non-negativity constraint.
WorkersScheduled[s] \geq 0 \forall s \in S
OrdersProcessed[s] \geq 0 \forall s \in S
UnprocessedOrders[s] \geq 0 \forall s \in S

Type:
Integer, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_warehouse_scheduling(
    orders_received={'midnight_6am': 150, '6am_noon': 250, 'noon_6pm': 400, '6pm_midnight': 300},
    hourly_wage=12,
    shift_hours=6,
    orders_per_worker=60,
    penalty_per_unprocessed_order=1
):
    """
    Models and solves the warehouse scheduling problem.
    """
    model = gp.Model("WarehouseScheduling")
    model.setParam('OutputFlag', 0)

    # --- Parameters ---
    shifts = ['midnight_6am', '6am_noon', 'noon_6pm', '6pm_midnight']
    shift_wage = hourly_wage * shift_hours

    # --- Decision Variables ---
    workers = model.addVars(shifts, name="Workers", vtype=GRB.INTEGER, lb=0)
    orders_processed = model.addVars(shifts, name="OrdersProcessed", vtype=GRB.CONTINUOUS, lb=0)
    unprocessed = model.addVars(shifts, name="UnprocessedOrders", vtype=GRB.CONTINUOUS, lb=0)

    # --- Objective Function ---
    total_cost = gp.LinExpr()
    for shift in shifts:
        total_cost += workers[shift] * shift_wage
        total_cost += unprocessed[shift] * penalty_per_unprocessed_order
    model.setObjective(total_cost, GRB.MINIMIZE)

    # --- Constraints ---
    for shift in shifts:
        model.addConstr(orders_processed[shift] <= workers[shift] * orders_per_worker,
                        name=f"ProcessingCapacity_{shift}")

    shift_order = ['midnight_6am', '6am_noon', 'noon_6pm', '6pm_midnight']
    for i, shift in enumerate(shift_order):
        if shift == 'midnight_6am':
            model.addConstr(orders_processed[shift] + unprocessed[shift] == orders_received[shift],
                           name=f"OrderBalance_{shift}")
        else:
            prev_shift = shift_order[i-1]
            model.addConstr(orders_processed[shift] + unprocessed[shift] ==
                           orders_received[shift] + unprocessed[prev_shift],
                           name=f"OrderBalance_{shift}")

    model.addConstr(unprocessed['6pm_midnight'] == 0, name="AllOrdersProcessedByMidnight")

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

Solution status: OPTIMAL
Optimal objective value: 1378