An assembly line needs to produce a product with 10 tasks. The cycle time is 66 time units. The tasks, their durations, and precedence relationships are given in the following table:

Task | Duration | Predecessors
-----|----------|-------------
1    | 36       | -
2    | 36       | 1
3    | 30       | -
4    | 30       | 3
5    | 24       | 1,4
6    | 30       | 5
7    | 24       | 2
8    | 12       | 6,7
9    | 54       | 8
10   | 12       | 9

The problem requires assigning these tasks to workstations while respecting the precedence relationships and cycle time constraint, with the objective of minimizing the number of workstations needed. Each task must be assigned to exactly one workstation, and the total duration of tasks assigned to any workstation cannot exceed the cycle time of 66 units.

Problem type: ILP (Medium: 77 variables, 33 constraints, 299 nonzeros)

Mathematical model:
Set:
1. Tasks
The set of all tasks, \( T = \{1, 2, ..., 10\} \)
2. Workstations
The set of potential workstations, \( W = \{1, 2, ..., 7\} \) (upper bound estimated)

Parameter:
1. TaskDuration
Duration of each task \( t \), \( d_t \): [36, 36, 30, 30, 24, 30, 24, 12, 54, 12]
2. CycleTime
Maximum allowed time per workstation, \( C = 66 \)
3. Precedence
Set of direct precedence pairs \( (i,j) \) where task \( i \) must precede task \( j \): 
{(1,2), (1,5), (2,7), (3,4), (4,5), (5,6), (6,8), (7,8), (8,9), (9,10)}
4. StationInterval
For each task \( t \), the range of possible workstations \( I_t \) it can be assigned to, calculated based on earliest and latest possible stations considering precedence and cycle time

Decision variable:
1. TaskAssignment
Binary variable \( x_{tw} \in \{0, 1\} \) for all \( t \in T, w \in W \), where \( x_{tw} = 1 \) if task \( t \) is assigned to workstation \( w \), and 0 otherwise
2. StationUsed
Binary variable \( y_w \in \{0, 1\} \) for all \( w \in W \), where \( y_w = 1 \) if workstation \( w \) is used, and 0 otherwise

Objective:
Minimize the number of workstations used
min: \(\sum_{w \in W} y_w\)

Constraint:
1. Each task must be assigned to exactly one workstation within its feasible interval
\(\sum_{w \in I_t} x_{tw} = 1, \forall t \in T\)
2. The total duration of tasks assigned to each workstation cannot exceed cycle time
\(\sum_{t \in T} d_t \cdot x_{tw} \leq C \cdot y_w, \forall w \in W\)
3. Workstations must be used in sequence (if station w+1 is used, station w must be used)
\(y_{w+1} \leq y_w, \forall w \in \{1,...,6\}\)
4. Precedence relationships must be maintained
\(\sum_{w \in W} w \cdot x_{iw} \leq \sum_{w \in W} w \cdot x_{jw}, \forall (i,j) \in Precedence\)

Type:
Binary, Linear, Linear
ILP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB


def solve_assembly_line_balancing(
    tasks=[1, 2, 3, 4, 5, 6, 7, 8, 9, 10],
    duration={
        1: 36, 2: 36, 3: 30, 4: 30, 5: 24,
        6: 30, 7: 24, 8: 12, 9: 54, 10: 12
    },
    precedence=[(1, 2), (1, 5), (2, 7), (3, 4), (4, 5), (5, 6), (6, 8),
                (7, 8), (8, 9), (9, 10)],
    cycle_time=66,
    max_stations=7
):
    """Solve the assembly line balancing problem using Gurobi."""
    # Create a new model
    model = gp.Model("AssemblyLineBalancing")

    workstations = range(1, max_stations + 1)

    # --- Decision Variables ---
    x = model.addVars(tasks, workstations, vtype=GRB.BINARY, name="x")
    y = model.addVars(workstations, vtype=GRB.BINARY, name="y")

    # --- Objective Function ---
    model.setObjective(y.sum(), GRB.MINIMIZE)

    # --- Constraints ---
    model.addConstrs((x.sum(t, '*') == 1 for t in tasks), name="TaskAssignment")
    model.addConstrs((gp.quicksum(duration[t] * x[t, w] for t in tasks) <= cycle_time * y[w]
                      for w in workstations), name="CycleTime")
    model.addConstrs((y[w + 1] <= y[w] for w in workstations if w < max_stations),
                     name="StationSequence")
    for (i, j) in precedence:
        model.addConstr(gp.quicksum(w * x[i, w] for w in workstations)
                        <= gp.quicksum(w * x[j, w] for w in workstations),
                        name=f"Precedence_{i}_{j}")

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


Solution status: OPTIMAL
Optimal objective value: 5