# Complete Optimization Problem and Solution: gymnast

## 1. Problem Context and Goals

### Context  
The gymnastics competition organizer aims to strategically allocate gymnasts to various events to maximize the total points scored. Each gymnast's participation in an event is a binary decision, indicating whether they participate or not. The operational goal is to maximize the total points accumulated from all events, which include Floor Exercise, Pommel Horse, Rings, Vault, Parallel Bars, and Horizontal Bar. The points scored in each event are multiplied by the participation decision for each gymnast. The business configuration includes constraints such as the maximum number of events a single gymnast can participate in, and the maximum number of gymnasts allowed to participate in a single event. These constraints ensure that the competition remains fair and manageable, aligning with the linear optimization model requirements. The configuration parameters are set to realistic values based on typical competition structures and physical demands on gymnasts.

### Goals  
The primary goal is to maximize the total points scored by gymnasts across all events. This involves optimizing the allocation of gymnasts to events, ensuring that the total points from Floor Exercise, Pommel Horse, Rings, Vault, Parallel Bars, and Horizontal Bar are maximized. The success of this optimization is measured by the total points achieved, which directly correlates with the participation decisions and the points scored in each event. The objective is clearly defined in linear terms, focusing on maximizing the sum of points across all events.

## 2. Constraints    

The optimization problem is subject to specific constraints to ensure feasibility and fairness. Each gymnast can participate in a limited number of events, defined by the maximum events per gymnast constraint. Additionally, each event can accommodate a limited number of gymnasts, as specified by the maximum gymnasts per event constraint. These constraints are designed to maintain a balanced competition and are expressed in linear terms, aligning with the business requirements and ensuring that the optimization model remains linear.

## 3. Available Data  

### Database Schema  
```sql
-- Iteration 1 Database Schema
-- Objective: Schema changes include creating tables for decision variables and constraint bounds, and moving scalar parameters to configuration logic. Adjustments ensure alignment with OR expert's requirements and optimization model.

CREATE TABLE gymnast (
  Floor_Exercise_Points INTEGER,
  Pommel_Horse_Points INTEGER,
  Rings_Points INTEGER,
  Vault_Points INTEGER,
  Parallel_Bars_Points INTEGER,
  Horizontal_Bar_Points INTEGER
);

CREATE TABLE decision_variables (
  participation BOOLEAN
);

CREATE TABLE constraint_bounds (
  max_events_per_gymnast INTEGER,
  max_gymnasts_per_event INTEGER
);
```

### Data Dictionary  
The data dictionary provides a comprehensive mapping of tables and columns to their business purposes and roles in the optimization model:

- **Gymnast Table**: This table stores the performance data of gymnasts across different events. Each column represents the points scored by a gymnast in a specific event, serving as coefficients in the optimization objective.
  - **Floor Exercise Points**: Points scored in the Floor Exercise event.
  - **Pommel Horse Points**: Points scored in the Pommel Horse event.
  - **Rings Points**: Points scored in the Rings event.
  - **Vault Points**: Points scored in the Vault event.
  - **Parallel Bars Points**: Points scored in the Parallel Bars event.
  - **Horizontal Bar Points**: Points scored in the Horizontal Bar event.

- **Decision Variables Table**: This table captures the participation decisions for each gymnast, indicating whether they participate in an event.
  - **Participation**: A binary indicator of a gymnast's participation in an event.

- **Constraint Bounds Table**: This table defines the constraints for the optimization model, specifying the maximum number of events a gymnast can participate in and the maximum number of gymnasts per event.
  - **Max Events Per Gymnast**: The upper limit on the number of events a single gymnast can participate in.
  - **Max Gymnasts Per Event**: The upper limit on the number of gymnasts allowed in a single event.

### Current Stored Values  
```sql
-- Iteration 1 Realistic Data
-- Generated by triple expert (business + data + optimization)
-- Values were determined based on typical scoring ranges in gymnastics competitions and realistic participation constraints to ensure a balanced and competitive event setup.

-- Realistic data for gymnast
INSERT INTO gymnast (Floor_Exercise_Points, Pommel_Horse_Points, Rings_Points, Vault_Points, Parallel_Bars_Points, Horizontal_Bar_Points) VALUES (12, 10, 14, 15, 11, 9);
INSERT INTO gymnast (Floor_Exercise_Points, Pommel_Horse_Points, Rings_Points, Vault_Points, Parallel_Bars_Points, Horizontal_Bar_Points) VALUES (18, 16, 17, 19, 14, 13);
INSERT INTO gymnast (Floor_Exercise_Points, Pommel_Horse_Points, Rings_Points, Vault_Points, Parallel_Bars_Points, Horizontal_Bar_Points) VALUES (14, 12, 15, 17, 13, 11);

-- Realistic data for decision_variables
INSERT INTO decision_variables (participation) VALUES (True);
INSERT INTO decision_variables (participation) VALUES (False);
INSERT INTO decision_variables (participation) VALUES (True);

-- Realistic data for constraint_bounds
INSERT INTO constraint_bounds (max_events_per_gymnast, max_gymnasts_per_event) VALUES (3, 5);
```

## 4. Mathematical Optimization Formulation

#### Decision Variables
- Let \( x_{ij} \) be a binary decision variable where \( x_{ij} = 1 \) if gymnast \( i \) participates in event \( j \), and \( x_{ij} = 0 \) otherwise.
  - \( i \) represents the gymnast index.
  - \( j \) represents the event index, where \( j \in \{1, 2, 3, 4, 5, 6\} \) corresponding to Floor Exercise, Pommel Horse, Rings, Vault, Parallel Bars, and Horizontal Bar, respectively.

#### Objective Function
Maximize the total points scored by all gymnasts across all events:
\[
\text{Maximize } \sum_{i} \sum_{j} p_{ij} \cdot x_{ij}
\]
where \( p_{ij} \) is the points scored by gymnast \( i \) in event \( j \).

Data Source Verification:
- \( p_{ij} \) values are sourced from the `gymnast` table columns: `Floor_Exercise_Points`, `Pommel_Horse_Points`, `Rings_Points`, `Vault_Points`, `Parallel_Bars_Points`, `Horizontal_Bar_Points`.

#### Constraints
1. **Maximum Events Per Gymnast:**
   Each gymnast can participate in at most a specified number of events:
   \[
   \sum_{j} x_{ij} \leq \text{max\_events\_per\_gymnast} \quad \forall i
   \]
   Data Source Verification:
   - `max_events_per_gymnast` is sourced from the `constraint_bounds` table.

2. **Maximum Gymnasts Per Event:**
   Each event can have at most a specified number of gymnasts:
   \[
   \sum_{i} x_{ij} \leq \text{max\_gymnasts\_per\_event} \quad \forall j
   \]
   Data Source Verification:
   - `max_gymnasts_per_event` is sourced from the `constraint_bounds` table.

3. **Binary Decision Variables:**
   \[
   x_{ij} \in \{0, 1\} \quad \forall i, j
   \]

This formulation is a Mixed-Integer Programming (MIP) model due to the binary nature of the decision variables \( x_{ij} \). The model is linear, adhering to the constraints and objective function requirements, and is immediately solvable using standard optimization solvers.

## 5. Gurobipy Implementation

```python
# Complete GUROBIPY implementation

import gurobipy as gp
from gurobipy import GRB

def optimize_gymnast_allocation():
    """Optimize gymnast allocation to maximize total points scored in events."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("gymnast_allocation")
    
    # Data from the database
    points = [
        [12, 10, 14, 15, 11, 9],  # Gymnast 1
        [18, 16, 17, 19, 14, 13], # Gymnast 2
        [14, 12, 15, 17, 13, 11]  # Gymnast 3
    ]
    
    max_events_per_gymnast = 3
    max_gymnasts_per_event = 5
    
    n_gymnasts = len(points)
    n_events = len(points[0])
    
    # CRITICAL: Validate array lengths before loops
    assert all(len(row) == n_events for row in points), "Array length mismatch in points data"
    
    # 2. VARIABLES
    # Binary decision variables for participation
    x = model.addVars(n_gymnasts, n_events, vtype=GRB.BINARY, name="x")
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total points scored
    model.setObjective(
        gp.quicksum(points[i][j] * x[i, j] for i in range(n_gymnasts) for j in range(n_events)),
        GRB.MAXIMIZE
    )
    
    # 4. CONSTRAINTS - CORRECT SYNTAX PATTERNS
    
    # Maximum events per gymnast
    model.addConstrs(
        (gp.quicksum(x[i, j] for j in range(n_events)) <= max_events_per_gymnast for i in range(n_gymnasts)),
        name="max_events_per_gymnast"
    )
    
    # Maximum gymnasts per event
    model.addConstrs(
        (gp.quicksum(x[i, j] for i in range(n_gymnasts)) <= max_gymnasts_per_event for j in range(n_events)),
        name="max_gymnasts_per_event"
    )
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for i in range(n_gymnasts):
            for j in range(n_events):
                if x[i, j].x > 1e-6:
                    print(f"Gymnast {i+1} participates in event {j+1}")
    elif model.status == GRB.INFEASIBLE:
        print("Problem is infeasible")
    elif model.status == GRB.UNBOUNDED:
        print("Problem is unbounded")
    
    return model

# Run the optimization
optimize_gymnast_allocation()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 141.0
**Execution Time**: 0.96 seconds
**Reliability**: high
**Analysis**: Gurobipy found an optimal solution quickly with the shortest execution time, indicating efficient performance and reliability.

## 6. DOCplex Implementation

```python
# Complete DOCPLEX implementation

from docplex.mp.model import Model

def gymnastics_optimization():
    """Optimize gymnast event participation to maximize total points."""
    
    # 1. MODEL & DATA SETUP
    mdl = Model(name="gymnastics_optimization")
    
    # Data from the database
    points = [
        [12, 10, 14, 15, 11, 9],   # Gymnast 1
        [18, 16, 17, 19, 14, 13],  # Gymnast 2
        [14, 12, 15, 17, 13, 11]   # Gymnast 3
    ]
    
    max_events_per_gymnast = 3
    max_gymnasts_per_event = 5
    
    n_gymnasts = len(points)
    n_events = len(points[0])
    
    # CRITICAL: Validate array lengths to prevent IndexError
    assert all(len(p) == n_events for p in points), "Array length mismatch"
    safe_range_gymnasts = range(n_gymnasts)
    safe_range_events = range(n_events)
    
    # 2. VARIABLES
    x = {(i, j): mdl.binary_var(name=f"x_{i}_{j}") for i in safe_range_gymnasts for j in safe_range_events}
    
    # 3. OBJECTIVE FUNCTION
    objective = mdl.sum(points[i][j] * x[i, j] for i in safe_range_gymnasts for j in safe_range_events)
    mdl.maximize(objective)
    
    # 4. CONSTRAINTS
    
    # Maximum events per gymnast
    for i in safe_range_gymnasts:
        mdl.add_constraint(mdl.sum(x[i, j] for j in safe_range_events) <= max_events_per_gymnast, ctname=f"max_events_gymnast_{i}")
    
    # Maximum gymnasts per event
    for j in safe_range_events:
        mdl.add_constraint(mdl.sum(x[i, j] for i in safe_range_gymnasts) <= max_gymnasts_per_event, ctname=f"max_gymnasts_event_{j}")
    
    # 5. SOLVING & RESULTS
    solution = mdl.solve()
    
    if solution:
        print(f"Optimal value: {solution.objective_value}")
        for i in safe_range_gymnasts:
            for j in safe_range_events:
                value = solution.get_value(x[i, j])
                if value > 1e-6:
                    print(f"Gymnast {i} participates in event {j}")
    else:
        print("No solution found")
        print(f"Status: {mdl.solve_details.status}")
    
    return mdl

# Run the optimization
gymnastics_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 141.0
**Execution Time**: 4.04 seconds
**Reliability**: high
**Analysis**: DOCplex also found an optimal solution, albeit with a longer execution time compared to Gurobipy, but still reliable.

## 7. Pyomo Implementation

```python
# Complete PYOMO implementation

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

def gymnastics_optimization():
    """Optimize gymnast event participation to maximize total points."""
    
    # 1. MODEL CREATION
    model = pyo.ConcreteModel()
    
    # 2. DATA SETUP
    # Points scored by each gymnast in each event
    points = [
        [12, 10, 14, 15, 11, 9],  # Gymnast 1
        [18, 16, 17, 19, 14, 13], # Gymnast 2
        [14, 12, 15, 17, 13, 11]  # Gymnast 3
    ]
    
    # Constraints from the database
    max_events_per_gymnast = 3
    max_gymnasts_per_event = 5
    
    # Number of gymnasts and events
    n_gymnasts = len(points)
    n_events = len(points[0])
    
    # CRITICAL: Validate array lengths before indexing
    assert all(len(row) == n_events for row in points), "Array length mismatch in points data"
    
    # 3. SETS
    model.I = pyo.RangeSet(1, n_gymnasts)  # Gymnasts
    model.J = pyo.RangeSet(1, n_events)    # Events
    
    # 4. PARAMETERS
    model.points = pyo.Param(model.I, model.J, initialize={(i+1, j+1): points[i][j] for i in range(n_gymnasts) for j in range(n_events)})
    
    # 5. VARIABLES
    model.x = pyo.Var(model.I, model.J, within=pyo.Binary)
    
    # 6. OBJECTIVE FUNCTION
    def obj_rule(model):
        return sum(model.points[i, j] * model.x[i, j] for i in model.I for j in model.J)
    model.objective = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
    
    # 7. CONSTRAINTS
    
    # Maximum events per gymnast
    def max_events_rule(model, i):
        return sum(model.x[i, j] for j in model.J) <= max_events_per_gymnast
    model.max_events_constraint = pyo.Constraint(model.I, rule=max_events_rule)
    
    # Maximum gymnasts per event
    def max_gymnasts_rule(model, j):
        return sum(model.x[i, j] for i in model.I) <= max_gymnasts_per_event
    model.max_gymnasts_constraint = pyo.Constraint(model.J, rule=max_gymnasts_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("Optimal solution found!")
        print(f"Optimal value: {pyo.value(model.objective)}")
        
        # Extract variable values
        print("\nParticipation decisions:")
        for i in model.I:
            for j in model.J:
                x_val = pyo.value(model.x[i, j])
                if x_val > 0.5:  # Only print participation decisions
                    print(f"Gymnast {i} participates in event {j}")
        
    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
gymnastics_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 141.0
**Execution Time**: 3.51 seconds
**Reliability**: high
**Analysis**: Pyomo achieved the optimal solution with a slightly faster execution time than DOCplex, demonstrating reliability.

## 8. Cross-Solver Analysis and Final Recommendation

### Solver Results Comparison

| Solver | Status | Optimal Value | Execution Time | Decision Variables | Retry Attempt |
|--------|--------|---------------|----------------|-------------------|---------------|
| Gurobipy | OPTIMAL | 141.00 | 0.96s | N/A | N/A |
| Docplex | OPTIMAL | 141.00 | 4.04s | N/A | N/A |
| Pyomo | OPTIMAL | 141.00 | 3.51s | N/A | N/A |

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

### Final Recommendation
**Recommended Optimal Value**: 141.0
**Confidence Level**: HIGH
**Preferred Solver(s)**: gurobipy
**Reasoning**: Gurobipy is preferred due to its faster execution time and high reliability, making it suitable for time-sensitive applications.

### Business Interpretation
**Overall Strategy**: The optimal allocation of gymnasts to events maximizes the total points scored, ensuring the best performance outcome.
**Objective Value Meaning**: The optimal objective value of 141.0 represents the maximum achievable points by strategically assigning gymnasts to events.
**Resource Allocation Summary**: Allocate gymnasts to events based on their scoring potential while adhering to participation constraints.
**Implementation Recommendations**: Use Gurobipy for real-time decision-making in event planning, ensuring optimal gymnast assignments and maximizing performance.