# Complete Optimization Problem and Solution: musical

## 1. Problem Context and Goals

### Context  
A theater company is planning multiple musical performances and aims to maximize audience engagement by optimally assigning actors to roles. The engagement metric is calculated as a weighted combination of each actor's age and their performance duration. The company has a set of actors, each with a specific age and performance duration, and a set of roles that need to be filled. The decision to assign an actor to a role is binary, meaning an actor is either assigned to a role or not. 

The company has established two weights to balance the importance of age and performance duration in the engagement metric. The weight for age is set to 0.6, emphasizing the value of experience, while the weight for performance duration is set to 0.4, ensuring a balanced consideration of performance length. Additionally, the company has set a maximum total duration limit of 100 minutes for all performances combined to ensure the event remains within a feasible timeframe.

The goal is to assign actors to roles in such a way that the total engagement is maximized, while ensuring that each actor is assigned to at most one role, each role is filled by exactly one actor, and the total duration of all performances does not exceed the allowed limit.

### Goals  
The primary optimization goal is to maximize the total audience engagement across all performances. This is achieved by assigning actors to roles in a way that maximizes the weighted sum of their ages and performance durations. Success is measured by the total engagement score, which is calculated using the predefined weights for age and performance duration. The solution must also respect the constraints on actor assignments and the total performance duration.

## 2. Constraints    

The optimization problem is subject to the following constraints:

1. **Actor Assignment Limit**: Each actor can be assigned to at most one role. This ensures that no actor is overcommitted and can focus on a single performance.
   
2. **Role Fulfillment**: Each role must be filled by exactly one actor. This ensures that all roles are covered and no role is left unassigned.

3. **Total Duration Limit**: The combined duration of all performances must not exceed the maximum allowed total duration of 100 minutes. This ensures that the event remains within the planned timeframe.

These constraints ensure that the assignment of actors to roles is both feasible and aligned with the company's operational requirements.

## 3. Available Data  

### Database Schema  
```sql
-- Iteration 1 Database Schema
-- Objective: Schema changes include creating tables for roles, actors, and assignments. Configuration logic updates include adding weights for engagement metric and maximum duration.

CREATE TABLE roles (
  role_id INTEGER,
  musical_id INTEGER,
  role_name STRING
);

CREATE TABLE actors (
  actor_id INTEGER,
  age INTEGER,
  performance_duration INTEGER
);

CREATE TABLE assignments (
  assignment_id INTEGER,
  actor_id INTEGER,
  role_id INTEGER,
  assignment_status BOOLEAN
);
```

### Data Dictionary  
- **Roles Table**: Contains information about the roles available in different musicals. Each role has a unique identifier, a musical identifier, and a role name.
  - **role_id**: Unique identifier for each role.
  - **musical_id**: Identifier for the musical the role belongs to.
  - **role_name**: Name of the role.

- **Actors Table**: Contains information about the actors, including their age and performance duration.
  - **actor_id**: Unique identifier for each actor.
  - **age**: Age of the actor, used in the engagement metric.
  - **performance_duration**: Duration of the actor's performance, used in the engagement metric.

- **Assignments Table**: Tracks the assignments of actors to roles. Each assignment has a unique identifier, an actor identifier, a role identifier, and a status indicating whether the actor is assigned to the role.
  - **assignment_id**: Unique identifier for each assignment.
  - **actor_id**: Identifier for the actor assigned.
  - **role_id**: Identifier for the role assigned.
  - **assignment_status**: Indicates if the actor is assigned to the role (True/False).

### Current Stored Values  
```sql
-- Iteration 1 Realistic Data
-- Generated by triple expert (business + data + optimization)
-- Values were determined based on realistic theater company scenarios, considering typical actor ages, performance durations, and role assignments. The weights and maximum duration were set to balance the engagement metric and ensure feasible solutions.

-- Realistic data for roles
INSERT INTO roles (role_id, musical_id, role_name) VALUES (1, 1, 'Lead');
INSERT INTO roles (role_id, musical_id, role_name) VALUES (2, 1, 'Supporting');
INSERT INTO roles (role_id, musical_id, role_name) VALUES (3, 2, 'Chorus');

-- Realistic data for actors
INSERT INTO actors (actor_id, age, performance_duration) VALUES (1, 28, 15);
INSERT INTO actors (actor_id, age, performance_duration) VALUES (2, 35, 20);
INSERT INTO actors (actor_id, age, performance_duration) VALUES (3, 22, 10);

-- Realistic data for assignments
INSERT INTO assignments (assignment_id, actor_id, role_id, assignment_status) VALUES (1, 1, 1, True);
INSERT INTO assignments (assignment_id, actor_id, role_id, assignment_status) VALUES (2, 2, 2, True);
INSERT INTO assignments (assignment_id, actor_id, role_id, assignment_status) VALUES (3, 3, 3, True);
```

## 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.