# Complete Optimization Problem and Solution: singer

## 1. Problem Context and Goals

### Context  
A music label is tasked with selecting a subset of singers to promote in order to maximize the total sales of their songs. The label must make these decisions while adhering to two key operational constraints: the total budget available for promotions and the maximum number of singers that can be effectively promoted. 

The decision to promote a singer is represented as a binary choice: either the singer is selected for promotion or they are not. The total sales of songs associated with the selected singers will be used to measure the success of the promotion strategy. The label has a fixed budget of 500,000 units, which limits the total net worth of the singers that can be promoted. Additionally, the label can promote no more than 3 singers at a time to ensure effective resource allocation and promotional impact.

### Goals  
The primary goal of this optimization problem is to maximize the total sales of songs by selecting the optimal set of singers for promotion. Success is measured by the cumulative sales of the songs associated with the selected singers. The label aims to achieve this goal while staying within the constraints of the available budget and the maximum number of singers that can be promoted.

## 2. Constraints  

1. **Budget Constraint**: The total net worth of the singers selected for promotion must not exceed the available budget of 500,000 units. This ensures that the label does not overspend on promotional activities.  
2. **Promotional Capacity Constraint**: The label can promote a maximum of 3 singers at a time. This ensures that the promotional efforts are focused and manageable, avoiding resource overextension.

## 3. Available Data  

### Database Schema  
```sql
-- Iteration 1 Database Schema
-- Objective: Schema changes include creating a new table for decision variables and updating the song table to include sales data. Configuration logic updates include adding scalar parameters for Total_Budget and Max_Singers.

CREATE TABLE singer_selection (
  singer_id INTEGER,
  is_selected BOOLEAN
);

CREATE TABLE song (
  song_id INTEGER,
  singer_id INTEGER,
  Sales INTEGER
);
```

### Data Dictionary  
- **singer_selection**:  
  - **Business Purpose**: Represents the selection of singers for promotion.  
  - **Optimization Role**: Decision variables indicating whether a singer is selected.  
  - **Columns**:  
    - **singer_id**: Unique identifier for each singer. Links to the singer being considered for selection.  
    - **is_selected**: Binary indicator of whether the singer is selected for promotion.  

- **song**:  
  - **Business Purpose**: Represents the songs and their sales data.  
  - **Optimization Role**: Provides the sales data used to measure the success of the promotion strategy.  
  - **Columns**:  
    - **song_id**: Unique identifier for each song.  
    - **singer_id**: Identifier for the singer of the song. Links the song to the singer.  
    - **Sales**: Sales of the song. Used as the objective coefficient in the optimization problem.  

### Current Stored Values  
```sql
-- Iteration 1 Realistic Data
-- Generated by triple expert (business + data + optimization)
-- Values were determined based on realistic business scenarios, considering typical budgets for music promotions, average song sales, and the number of singers a label can effectively promote.

-- Realistic data for singer_selection
INSERT INTO singer_selection (singer_id, is_selected) VALUES (1, False);
INSERT INTO singer_selection (singer_id, is_selected) VALUES (2, True);
INSERT INTO singer_selection (singer_id, is_selected) VALUES (3, False);

-- Realistic data for song
INSERT INTO song (song_id, singer_id, Sales) VALUES (1, 1, 1500);
INSERT INTO song (song_id, singer_id, Sales) VALUES (2, 2, 2500);
INSERT INTO song (song_id, singer_id, Sales) VALUES (3, 3, 1000);
```

## 4. Mathematical Optimization Formulation

#### Decision Variables
- Let \( x_i \) be a binary decision variable where:  
  \( x_i = 1 \) if singer \( i \) is selected for promotion,  
  \( x_i = 0 \) otherwise.  
  Here, \( i \in \{1, 2, 3\} \) represents the singers in the dataset.

#### Objective Function
Maximize the total sales of songs associated with the selected singers:  
\[
\text{Maximize } Z = 1500x_1 + 2500x_2 + 1000x_3
\]  
**Data Source Verification**:  
- Coefficients \( 1500, 2500, 1000 \) are from `song.Sales` for singers 1, 2, and 3, respectively.

#### Constraints
1. **Budget Constraint**: The total net worth of the selected singers must not exceed the budget of 500,000 units.  
   \[
   \text{Net Worth}_1 \cdot x_1 + \text{Net Worth}_2 \cdot x_2 + \text{Net Worth}_3 \cdot x_3 \leq 500,000
   \]  
   **Data Source Verification**:  
   - Net worth values for singers 1, 2, and 3 are not explicitly provided in the data. If available, replace \( \text{Net Worth}_i \) with the corresponding values from the dataset. If not, assume placeholder values (e.g., \( \text{Net Worth}_1 = 200,000 \), \( \text{Net Worth}_2 = 300,000 \), \( \text{Net Worth}_3 = 150,000 \)) for illustration.

2. **Promotional Capacity Constraint**: The label can promote a maximum of 3 singers:  
   \[
   x_1 + x_2 + x_3 \leq 3
   \]  
   **Data Source Verification**:  
   - The constraint is derived from the business requirement of promoting no more than 3 singers.

3. **Binary Decision Variables**:  
   \[
   x_i \in \{0, 1\} \quad \forall i \in \{1, 2, 3\}
   \]  
   **Data Source Verification**:  
   - This ensures that \( x_i \) is a binary variable, as defined in the decision variables section.

### Complete Linear Programming Model
\[
\text{Maximize } Z = 1500x_1 + 2500x_2 + 1000x_3
\]
Subject to:
\[
\text{Net Worth}_1 \cdot x_1 + \text{Net Worth}_2 \cdot x_2 + \text{Net Worth}_3 \cdot x_3 \leq 500,000
\]
\[
x_1 + x_2 + x_3 \leq 3
\]
\[
x_i \in \{0, 1\} \quad \forall i \in \{1, 2, 3\}
\]

## 5. Gurobipy Implementation

```python
#!/usr/bin/env python3
"""
Gurobipy 12.0.2 Implementation for Singer Promotion Optimization
"""

import gurobipy as gp
from gurobipy import GRB

def singer_promotion_optimization():
    """Optimize singer selection to maximize song sales within budget and promotional capacity constraints."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("singer_promotion")
    
    # Data from the problem
    singers = [1, 2, 3]
    sales = {1: 1500, 2: 2500, 3: 1000}
    net_worth = {1: 200000, 2: 300000, 3: 150000}  # Placeholder values for net worth
    total_budget = 500000
    max_singers = 3
    
    # CRITICAL: Validate array lengths before loops
    assert len(singers) == len(sales) == len(net_worth), "Array length mismatch"
    
    # 2. VARIABLES
    x = {i: model.addVar(vtype=GRB.BINARY, name=f"x_{i}") for i in singers}
    
    # 3. OBJECTIVE FUNCTION
    model.setObjective(gp.quicksum(sales[i] * x[i] for i in singers), GRB.MAXIMIZE)
    
    # 4. CONSTRAINTS
    # Budget Constraint
    model.addConstr(gp.quicksum(net_worth[i] * x[i] for i in singers) <= total_budget, name="budget_constraint")
    
    # Promotional Capacity Constraint
    model.addConstr(gp.quicksum(x[i] for i in singers) <= max_singers, name="promotional_capacity_constraint")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for i in singers:
            if x[i].x > 1e-6:
                print(f"Singer {i} is selected for promotion.")
    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__":
    singer_promotion_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 4000.0
**Execution Time**: 0.23 seconds
**Reliability**: high
**Analysis**: Gurobipy found the optimal solution quickly with the lowest execution time.

## 6. DOCplex Implementation

```python
# Complete DOCPLEX implementation

from docplex.mp.model import Model

def singer_promotion_optimization():
    """Optimization model for selecting singers to maximize song sales."""
    
    # 1. MODEL & DATA SETUP
    mdl = Model(name="singer_promotion")
    
    # Data from the problem
    singers = [1, 2, 3]
    sales = {1: 1500, 2: 2500, 3: 1000}
    net_worth = {1: 200000, 2: 300000, 3: 150000}  # Placeholder values
    total_budget = 500000
    max_singers = 3
    
    # CRITICAL: Validate array lengths to prevent IndexError
    assert len(singers) == len(sales) == len(net_worth), "Array length mismatch"
    safe_range = range(min(len(singers), len(sales), len(net_worth)))  # Safe indexing
    
    # 2. VARIABLES
    x = {i: mdl.binary_var(name=f"x_{i}") for i in safe_range}
    
    # 3. OBJECTIVE FUNCTION
    objective = mdl.sum(sales[i+1] * x[i] for i in safe_range)
    mdl.maximize(objective)
    
    # 4. CONSTRAINTS
    
    # Budget Constraint
    budget_constraint = mdl.sum(net_worth[i+1] * x[i] for i in safe_range)
    mdl.add_constraint(budget_constraint <= total_budget, ctname="budget_limit")
    
    # Promotional Capacity Constraint
    promotional_capacity = mdl.sum(x[i] for i in safe_range)
    mdl.add_constraint(promotional_capacity <= max_singers, ctname="promo_capacity")
    
    # 5. SOLVING & RESULTS
    solution = mdl.solve()
    
    if solution:
        print(f"Optimal value: {solution.objective_value}")
        for i in safe_range:
            value = solution.get_value(x[i])
            if value > 1e-6:
                print(f"Singer {singers[i]} selected: {value:.0f}")
    else:
        print("No solution found")
        print(f"Status: {mdl.solve_details.status}")
    
    return mdl

# Execute the optimization model
singer_promotion_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 4000.0
**Execution Time**: 1.15 seconds
**Reliability**: high
**Analysis**: DOCplex also found the optimal solution but took longer compared to Gurobipy.

## 7. Pyomo Implementation

```python
# Complete PYOMO implementation

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

def singer_optimization():
    """Optimization problem for selecting singers to maximize sales."""
    
    # 1. MODEL CREATION
    model = pyo.ConcreteModel()
    
    # 2. DATA SETUP
    singers = [1, 2, 3]
    sales = {1: 1500, 2: 2500, 3: 1000}
    net_worth = {1: 200000, 2: 300000, 3: 150000}  # Placeholder values
    total_budget = 500000
    max_singers = 3
    
    # CRITICAL: Validate array lengths before indexing
    assert len(singers) == len(sales) == len(net_worth), "Array length mismatch"
    
    # 3. SETS (Pyomo way to define indices)
    model.I = pyo.Set(initialize=singers)
    
    # 4. PARAMETERS (data containers)
    model.sales = pyo.Param(model.I, initialize=sales)
    model.net_worth = pyo.Param(model.I, initialize=net_worth)
    
    # 5. VARIABLES
    model.x = pyo.Var(model.I, within=pyo.Binary)
    
    # 6. OBJECTIVE FUNCTION
    def obj_rule(model):
        return sum(model.sales[i] * model.x[i] for i in model.I)
    model.objective = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
    
    # 7. CONSTRAINTS
    
    # Budget Constraint
    def budget_rule(model):
        return sum(model.net_worth[i] * model.x[i] for i in model.I) <= total_budget
    model.budget_constraint = pyo.Constraint(rule=budget_rule)
    
    # Promotional Capacity Constraint
    def capacity_rule(model):
        return sum(model.x[i] for i in model.I) <= max_singers
    model.capacity_constraint = pyo.Constraint(rule=capacity_rule)
    
    # 8. SOLVING WITH GUROBI (your available solver)
    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("Optimal solution found!")
        print(f"Optimal value: {pyo.value(model.objective)}")
        
        # Extract variable values
        print("\nSelected Singers:")
        for i in model.I:
            if pyo.value(model.x[i]) > 0.5:  # Only print selected singers
                print(f"Singer {i} is selected.")
        
    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

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

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 4000.0
**Execution Time**: 0.97 seconds
**Reliability**: high
**Analysis**: Pyomo found the optimal solution but had the longest execution time among the three solvers.

## 8. Cross-Solver Analysis and Final Recommendation

### Solver Results Comparison

| Solver | Status | Optimal Value | Execution Time | Decision Variables | Retry Attempt |
|--------|--------|---------------|----------------|-------------------|---------------|
| Gurobipy | OPTIMAL | 4000.00 | 0.23s | N/A | N/A |
| Docplex | OPTIMAL | 4000.00 | 1.15s | N/A | N/A |
| Pyomo | OPTIMAL | 4000.00 | 0.97s | N/A | N/A |

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

### Final Recommendation
**Recommended Optimal Value**: 4000.0
**Confidence Level**: HIGH
**Preferred Solver(s)**: gurobipy
**Reasoning**: Gurobipy is preferred due to its faster execution time while still providing the same optimal solution as the other solvers.

### Optimal Decision Variables
- **x_1** = 1.000
  - *Business Meaning*: Promote singer 1, who has a sales potential of 1500 units.
- **x_2** = 1.000
  - *Business Meaning*: Promote singer 2, who has a sales potential of 2500 units.
- **x_3** = 0.000
  - *Business Meaning*: Do not promote singer 3, who has a sales potential of 1000 units.

### Business Interpretation
**Overall Strategy**: The optimal solution suggests promoting singers 1 and 2, which maximizes total sales while staying within the budget and promotional capacity constraints.
**Objective Value Meaning**: The total sales from promoting singers 1 and 2 will be 4000 units, which is the maximum achievable under the given constraints.
**Resource Allocation Summary**: Allocate the budget to promote singers 1 and 2, ensuring the total net worth does not exceed 500,000 units.
**Implementation Recommendations**: Proceed with the promotion of singers 1 and 2. Monitor the budget and sales performance to ensure the expected outcomes are achieved.