## 4. Mathematical Optimization Formulation

#### Decision Variables
- Let \( x_{a,r} \) be a binary decision variable where:
  - \( x_{a,r} = 1 \) if actor \( a \) is assigned to role \( r \),
  - \( x_{a,r} = 0 \) otherwise.

#### Objective Function
Maximize the total audience engagement:
\[
\text{Maximize } \sum_{a \in A} \sum_{r \in R} (0.6 \times \text{age}_a + 0.4 \times \text{duration}_a) \times x_{a,r}
\]
Where:
- \( A \) is the set of actors,
- \( R \) is the set of roles,
- \( \text{age}_a \) is the age of actor \( a \) (from `actors.age`),
- \( \text{duration}_a \) is the performance duration of actor \( a \) (from `actors.performance_duration`).

#### Constraints
1. **Actor Assignment Limit**: Each actor can be assigned to at most one role.
\[
\sum_{r \in R} x_{a,r} \leq 1 \quad \forall a \in A
\]

2. **Role Fulfillment**: Each role must be filled by exactly one actor.
\[
\sum_{a \in A} x_{a,r} = 1 \quad \forall r \in R
\]

3. **Total Duration Limit**: The combined duration of all performances must not exceed 100 minutes.
\[
\sum_{a \in A} \sum_{r \in R} \text{duration}_a \times x_{a,r} \leq 100
\]

#### Data Source Verification
- **Coefficients for Objective Function**:
  - \( 0.6 \) and \( 0.4 \) are the weights for age and performance duration, respectively, as specified in the business problem.
  - \( \text{age}_a \) and \( \text{duration}_a \) are from `actors.age` and `actors.performance_duration`, respectively.

- **Coefficients for Constraints**:
  - The constant \( 1 \) in the actor assignment and role fulfillment constraints ensures that each actor is assigned to at most one role and each role is filled by exactly one actor.
  - The constant \( 100 \) in the total duration limit constraint is the maximum allowed total duration, as specified in the business problem.

This formulation provides a complete, immediately solvable LINEAR mathematical model for the given business problem.

## 5. Gurobipy Implementation

```python
#!/usr/bin/env python3
"""
Gurobipy Implementation for Musical Actor-Role Assignment Optimization
"""

import gurobipy as gp
from gurobipy import GRB

def optimize_actor_role_assignment():
    """Optimize actor-role assignments to maximize audience engagement."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("actor_role_assignment")
    
    # Data from the problem context
    actors = [
        {'actor_id': 1, 'age': 28, 'performance_duration': 15},
        {'actor_id': 2, 'age': 35, 'performance_duration': 20},
        {'actor_id': 3, 'age': 22, 'performance_duration': 10}
    ]
    
    roles = [
        {'role_id': 1, 'musical_id': 1, 'role_name': 'Lead'},
        {'role_id': 2, 'musical_id': 1, 'role_name': 'Supporting'},
        {'role_id': 3, 'musical_id': 2, 'role_name': 'Chorus'}
    ]
    
    # Weights for engagement metric
    age_weight = 0.6
    duration_weight = 0.4
    
    # Maximum total duration limit
    max_total_duration = 100
    
    # CRITICAL: Validate array lengths before loops
    assert len(actors) > 0, "No actors provided"
    assert len(roles) > 0, "No roles provided"
    
    # 2. VARIABLES
    # Binary decision variables: x[a][r] = 1 if actor a is assigned to role r
    x = model.addVars(
        [(a['actor_id'], r['role_id']) for a in actors for r in roles],
        vtype=GRB.BINARY,
        name="x"
    )
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total engagement: sum over all actor-role pairs of (0.6 * age + 0.4 * duration) * x[a][r]
    model.setObjective(
        gp.quicksum(
            (age_weight * a['age'] + duration_weight * a['performance_duration']) * x[a['actor_id'], r['role_id']]
            for a in actors for r in roles
        ),
        GRB.MAXIMIZE
    )
    
    # 4. CONSTRAINTS
    
    # Constraint 1: Each actor can be assigned to at most one role
    for a in actors:
        model.addConstr(
            gp.quicksum(x[a['actor_id'], r['role_id']] for r in roles) <= 1,
            name=f"actor_limit_{a['actor_id']}"
        )
    
    # Constraint 2: Each role must be filled by exactly one actor
    for r in roles:
        model.addConstr(
            gp.quicksum(x[a['actor_id'], r['role_id']] for a in actors) == 1,
            name=f"role_fulfillment_{r['role_id']}"
        )
    
    # Constraint 3: Total duration of all performances must not exceed 100 minutes
    model.addConstr(
        gp.quicksum(
            a['performance_duration'] * x[a['actor_id'], r['role_id']]
            for a in actors for r in roles
        ) <= max_total_duration,
        name="total_duration_limit"
    )
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for a in actors:
            for r in roles:
                if x[a['actor_id'], r['role_id']].x > 0.5:
                    print(f"Actor {a['actor_id']} assigned to Role {r['role_id']}")
    elif model.status == GRB.INFEASIBLE:
        print("Problem is infeasible")
    elif model.status == GRB.UNBOUNDED:
        print("Problem is unbounded")
    
    return model

# Execute the optimization
if __name__ == "__main__":
    optimize_actor_role_assignment()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 69.0
**Execution Time**: 0.16 seconds
**Reliability**: high
**Analysis**: Gurobipy found an optimal solution quickly with the lowest execution time (0.1648 seconds). Its reliability is high due to its consistent performance and accuracy in solving linear optimization problems.

## 6. DOCplex Implementation

```python
#!/usr/bin/env python3
"""
DOCPLEX implementation for the musical optimization problem
"""

from docplex.mp.model import Model

def musical_optimization():
    """Optimize actor assignments to maximize audience engagement"""
    
    # 1. MODEL & DATA SETUP
    mdl = Model(name="musical_optimization")
    
    # Data from the problem
    actors = [
        {"actor_id": 1, "age": 28, "performance_duration": 15},
        {"actor_id": 2, "age": 35, "performance_duration": 20},
        {"actor_id": 3, "age": 22, "performance_duration": 10}
    ]
    
    roles = [
        {"role_id": 1, "musical_id": 1, "role_name": "Lead"},
        {"role_id": 2, "musical_id": 1, "role_name": "Supporting"},
        {"role_id": 3, "musical_id": 2, "role_name": "Chorus"}
    ]
    
    # Weights and maximum duration
    age_weight = 0.6
    duration_weight = 0.4
    max_duration = 100
    
    # CRITICAL: Validate array lengths
    assert len(actors) > 0 and len(roles) > 0, "Empty actors or roles list"
    
    # 2. VARIABLES
    # Binary decision variables: x[a][r] = 1 if actor a is assigned to role r
    x = {(a["actor_id"], r["role_id"]): mdl.binary_var(name=f"x_{a['actor_id']}_{r['role_id']}")
         for a in actors for r in roles}
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total engagement: sum over all actors and roles of (0.6 * age + 0.4 * duration) * x[a][r]
    engagement = mdl.sum((age_weight * a["age"] + duration_weight * a["performance_duration"]) * x[(a["actor_id"], r["role_id"])]
                         for a in actors for r in roles)
    mdl.maximize(engagement)
    
    # 4. CONSTRAINTS
    
    # Actor Assignment Limit: Each actor can be assigned to at most one role
    for a in actors:
        mdl.add_constraint(mdl.sum(x[(a["actor_id"], r["role_id"])] for r in roles) <= 1,
                          ctname=f"actor_limit_{a['actor_id']}")
    
    # Role Fulfillment: Each role must be filled by exactly one actor
    for r in roles:
        mdl.add_constraint(mdl.sum(x[(a["actor_id"], r["role_id"])] for a in actors) == 1,
                          ctname=f"role_fulfillment_{r['role_id']}")
    
    # Total Duration Limit: Combined duration of all performances must not exceed max_duration
    total_duration = mdl.sum(a["performance_duration"] * x[(a["actor_id"], r["role_id"])]
                             for a in actors for r in roles)
    mdl.add_constraint(total_duration <= max_duration, ctname="total_duration_limit")
    
    # 5. SOLVING & RESULTS
    solution = mdl.solve()
    
    if solution:
        print(f"Optimal value: {solution.objective_value}")
        for a in actors:
            for r in roles:
                if solution.get_value(x[(a["actor_id"], r["role_id"])]) > 0.5:
                    print(f"Actor {a['actor_id']} assigned to Role {r['role_id']}")
    else:
        print("No solution found")
        print(f"Status: {mdl.solve_details.status}")
    
    return mdl

# Run the optimization
if __name__ == "__main__":
    musical_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 69.0
**Execution Time**: 1.19 seconds
**Reliability**: high
**Analysis**: DOCplex also found an optimal solution but took significantly longer (1.1879 seconds) compared to Gurobipy. Its reliability is high, but its slower performance may be a drawback for larger-scale problems.

## 7. Pyomo Implementation

```python
#!/usr/bin/env python3
"""
Pyomo 6.9.2 Implementation for Musical Actor-Role Assignment Problem
"""

import pyomo.environ as pyo
from pyomo.opt import SolverFactory

def optimize_actor_role_assignment():
    """Optimize actor-role assignments to maximize audience engagement."""
    
    # 1. MODEL CREATION
    model = pyo.ConcreteModel()
    
    # 2. DATA SETUP
    # Actors data: actor_id, age, performance_duration
    actors_data = {
        1: {'age': 28, 'performance_duration': 15},
        2: {'age': 35, 'performance_duration': 20},
        3: {'age': 22, 'performance_duration': 10}
    }
    
    # Roles data: role_id
    roles_data = {1, 2, 3}
    
    # Weights for engagement metric
    age_weight = 0.6
    duration_weight = 0.4
    
    # Maximum total duration limit
    max_total_duration = 100
    
    # 3. SETS
    model.A = pyo.Set(initialize=actors_data.keys())  # Set of actors
    model.R = pyo.Set(initialize=roles_data)         # Set of roles
    
    # 4. PARAMETERS
    model.age = pyo.Param(model.A, initialize={a: actors_data[a]['age'] for a in model.A})
    model.duration = pyo.Param(model.A, initialize={a: actors_data[a]['performance_duration'] for a in model.A})
    
    # 5. VARIABLES
    model.x = pyo.Var(model.A, model.R, within=pyo.Binary)  # Binary decision variable
    
    # 6. OBJECTIVE FUNCTION
    def obj_rule(model):
        return sum((age_weight * model.age[a] + duration_weight * model.duration[a]) * model.x[a, r] for a in model.A for r in model.R)
    model.objective = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
    
    # 7. CONSTRAINTS
    # Actor Assignment Limit: Each actor can be assigned to at most one role
    def actor_assignment_rule(model, a):
        return sum(model.x[a, r] for r in model.R) <= 1
    model.actor_assignment_constraint = pyo.Constraint(model.A, rule=actor_assignment_rule)
    
    # Role Fulfillment: Each role must be filled by exactly one actor
    def role_fulfillment_rule(model, r):
        return sum(model.x[a, r] for a in model.A) == 1
    model.role_fulfillment_constraint = pyo.Constraint(model.R, rule=role_fulfillment_rule)
    
    # Total Duration Limit: Combined duration must not exceed max_total_duration
    def total_duration_rule(model):
        return sum(model.duration[a] * model.x[a, r] for a in model.A for r in model.R) <= max_total_duration
    model.total_duration_constraint = pyo.Constraint(rule=total_duration_rule)
    
    # 8. SOLVING WITH GUROBI
    solver = SolverFactory('gurobi')
    
    # Optional: Set solver options
    solver.options['TimeLimit'] = 300  # 5 minutes
    solver.options['MIPGap'] = 0.01    # 1% gap
    
    # Solve the model
    results = solver.solve(model, tee=True)  # tee=True shows solver output
    
    # 9. RESULT PROCESSING
    # Check solver status
    if results.solver.termination_condition == pyo.TerminationCondition.optimal:
        print(f"Optimal value: {pyo.value(model.objective)}")
        
        # Extract variable values
        print("\nOptimal Assignments:")
        for a in model.A:
            for r in model.R:
                if pyo.value(model.x[a, r]) > 0.5:  # Only print assignments where x[a, r] = 1
                    print(f"Actor {a} assigned to Role {r}")
        
    elif results.solver.termination_condition == pyo.TerminationCondition.infeasible:
        print("Problem is infeasible")
    elif results.solver.termination_condition == pyo.TerminationCondition.unbounded:
        print("Problem is unbounded")
    else:
        print(f"Solver terminated with condition: {results.solver.termination_condition}")
    
    return model

# Execute the optimization
if __name__ == "__main__":
    optimize_actor_role_assignment()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 69.0
**Execution Time**: 0.88 seconds
**Reliability**: medium
**Analysis**: Pyomo found an optimal solution with an execution time of 0.8827 seconds, which is slower than Gurobipy but faster than DOCplex. Its reliability is medium due to its dependency on external solvers and potential variability in performance.

## 8. Cross-Solver Analysis and Final Recommendation

### Solver Results Comparison

| Solver | Status | Optimal Value | Execution Time | Decision Variables | Retry Attempt |
|--------|--------|---------------|----------------|-------------------|---------------|
| Gurobipy | OPTIMAL | 69.00 | 0.16s | N/A | N/A |
| Docplex | OPTIMAL | 69.00 | 1.19s | N/A | N/A |
| Pyomo | OPTIMAL | 69.00 | 0.88s | N/A | N/A |

### Solver Consistency Analysis
**Result**: All solvers produced consistent results ✓
**Consistent Solvers**: gurobipy, docplex, pyomo
**Majority Vote Optimal Value**: 69.0

### Final Recommendation
**Recommended Optimal Value**: 69.0
**Confidence Level**: HIGH
**Preferred Solver(s)**: gurobipy
**Reasoning**: Gurobipy is recommended due to its fast execution time, high reliability, and consistent performance. It is the most efficient solver for this problem.

### Business Interpretation
**Overall Strategy**: The optimal total audience engagement score is 69.0, achieved by assigning actors to roles in a way that maximizes the weighted sum of their age and performance duration while adhering to constraints.
**Objective Value Meaning**: The optimal objective value of 69.0 represents the maximum achievable audience engagement score, balancing actor age and performance duration.
**Resource Allocation Summary**: Actors should be assigned to roles such that each role is filled by exactly one actor, no actor is assigned to more than one role, and the total performance duration does not exceed 100 minutes.
**Implementation Recommendations**: 1. Use the optimal assignments from Gurobipy to allocate actors to roles. 2. Verify that the total performance duration does not exceed 100 minutes. 3. Monitor audience engagement metrics post-implementation to validate the results.