## 4. Mathematical Optimization Formulation

#### Decision Variables
- \( x_i \): Binary decision variable indicating whether to place a new restaurant in city \( i \).  
  \( x_i \in \{0, 1\} \) for all \( i \).  
  (Source: DECISION_VARIABLES.X_i)

#### Objective Function
Maximize the total customer satisfaction:  
\[
\text{Maximize } Z = \sum_{i} (\text{RESTAURANT.RATING}_i \times x_i)
\]  
(Source: RESTAURANT.RATING)

#### Constraints
1. **Total budget constraint**: The total number of new restaurants placed across all cities must not exceed 5.  
\[
\sum_{i} x_i \leq 5
\]  
(Source: Business configuration parameter)

2. **Regional distribution constraint**: Each geographical region must have at least 1 new restaurant placed.  
\[
\sum_{i \in \text{Region } r} x_i \geq 1 \quad \forall r \in \{\text{North, South, East, West}\}
\]  
(Source: RESTAURANT.REGION)

3. **City-level placement constraint**: No more than 2 new restaurants can be placed in any single city.  
\[
x_i \leq 2 \quad \forall i
\]  
(Source: Business configuration parameter)

#### Data Source Verification
- **RESTAURANT.RATING**: Used as coefficients in the objective function.  
- **RESTAURANT.REGION**: Used to group cities by region for the regional distribution constraint.  
- **DECISION_VARIABLES.X_i**: Represents the binary decision variables.  
- **Business configuration parameters**: Used for the total budget constraint (5) and city-level placement constraint (2).

#### Numerical Example
Using the provided data:  
- **RESTAURANT.RATING**: [4.5, 3.8, 4.2, 3.9, 4.1]  
- **RESTAURANT.REGION**: ['North', 'South', 'East', 'West', 'North']  

The objective function becomes:  
\[
\text{Maximize } Z = 4.5x_1 + 3.8x_2 + 4.2x_3 + 3.9x_4 + 4.1x_5
\]

The constraints are:  
1. \( x_1 + x_2 + x_3 + x_4 + x_5 \leq 5 \)  
2. \( x_1 + x_5 \geq 1 \) (North)  
   \( x_2 \geq 1 \) (South)  
   \( x_3 \geq 1 \) (East)  
   \( x_4 \geq 1 \) (West)  
3. \( x_i \leq 2 \) for all \( i \in \{1, 2, 3, 4, 5\} \)  

This is a complete, immediately solvable linear programming (LP) model.

## 5. Gurobipy Implementation

```python
#!/usr/bin/env python3
"""
Gurobipy 12.0.2 Implementation for Restaurant Placement Optimization
"""

import gurobipy as gp
from gurobipy import GRB

def restaurant_optimization():
    """Optimize restaurant placement to maximize customer satisfaction."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("restaurant_placement")
    
    # Data from the problem
    ratings = [4.5, 3.8, 4.2, 3.9, 4.1]
    regions = ['North', 'South', 'East', 'West', 'North']
    cities = range(len(ratings))
    unique_regions = list(set(regions))
    
    # CRITICAL: Validate array lengths before loops
    assert len(ratings) == len(regions), "Array length mismatch"
    
    # 2. VARIABLES
    # Binary decision variables for each city
    x = {i: model.addVar(vtype=GRB.BINARY, name=f"x_{i}") for i in cities}
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total customer satisfaction
    model.setObjective(gp.quicksum(ratings[i] * x[i] for i in cities), GRB.MAXIMIZE)
    
    # 4. CONSTRAINTS
    
    # Total budget constraint: sum of x_i <= 5
    model.addConstr(gp.quicksum(x[i] for i in cities) <= 5, name="total_budget")
    
    # Regional distribution constraint: at least 1 restaurant per region
    for r in unique_regions:
        model.addConstr(gp.quicksum(x[i] for i in cities if regions[i] == r) >= 1, name=f"region_{r}")
    
    # City-level placement constraint: no more than 2 restaurants per city
    for i in cities:
        model.addConstr(x[i] <= 2, name=f"city_limit_{i}")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for i in cities:
            if x[i].x > 0:
                print(f"City {i} selected: {x[i].x}")
    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__":
    restaurant_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 20.5
**Execution Time**: 0.21 seconds
**Reliability**: high
**Analysis**: Gurobipy achieved the optimal solution with the fastest execution time (0.209 seconds). Its reliability is high due to its consistent performance and robust optimization capabilities.

## 6. DOCplex Implementation

```python
#!/usr/bin/env python3
"""
DOCPLEX implementation for restaurant placement optimization
"""

from docplex.mp.model import Model

def restaurant_optimization():
    """Optimize restaurant placement to maximize customer satisfaction"""
    
    # 1. MODEL & DATA SETUP
    mdl = Model(name="restaurant_placement")
    
    # Data from the problem
    ratings = [4.5, 3.8, 4.2, 3.9, 4.1]
    regions = ['North', 'South', 'East', 'West', 'North']
    n_cities = len(ratings)
    
    # CRITICAL: Validate array lengths
    assert len(ratings) == len(regions) == n_cities, "Array length mismatch"
    safe_range = range(min(len(ratings), len(regions)))  # Safe indexing
    
    # 2. VARIABLES
    # Binary decision variables for each city
    x = {i: mdl.binary_var(name=f"x_{i}") for i in safe_range}
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total customer satisfaction
    objective = mdl.sum(ratings[i] * x[i] for i in safe_range)
    mdl.maximize(objective)
    
    # 4. CONSTRAINTS
    
    # Total budget constraint: No more than 5 new restaurants
    total_restaurants = mdl.sum(x[i] for i in safe_range)
    mdl.add_constraint(total_restaurants <= 5, ctname="total_budget")
    
    # Regional distribution constraint: At least 1 restaurant per region
    unique_regions = set(regions)
    for region in unique_regions:
        region_restaurants = mdl.sum(x[i] for i in safe_range if regions[i] == region)
        mdl.add_constraint(region_restaurants >= 1, ctname=f"region_{region}")
    
    # City-level placement constraint: No more than 2 restaurants per city
    for i in safe_range:
        mdl.add_constraint(x[i] <= 2, ctname=f"city_limit_{i}")
    
    # 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"x[{i}] = {value:.3f}")
    else:
        print("No solution found")
        print(f"Status: {mdl.solve_details.status}")
    
    return mdl

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

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 20.5
**Execution Time**: 1.26 seconds
**Reliability**: medium
**Analysis**: DOCplex also achieved the optimal solution but with a longer execution time (1.255 seconds). Its reliability is medium due to slower performance compared to Gurobipy.

## 7. Pyomo Implementation

```python
#!/usr/bin/env python3
"""
Pyomo 6.9.2 Implementation for Restaurant Placement Optimization
"""

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

def restaurant_optimization():
    """Optimize restaurant placement to maximize customer satisfaction"""
    
    # 1. MODEL CREATION
    model = pyo.ConcreteModel()
    
    # 2. DATA SETUP
    ratings = [4.5, 3.8, 4.2, 3.9, 4.1]
    regions = ['North', 'South', 'East', 'West', 'North']
    
    # CRITICAL: Validate array lengths before indexing
    assert len(ratings) == len(regions), "Array length mismatch"
    
    # 3. SETS (Pyomo way to define indices)
    n_cities = len(ratings)
    model.I = pyo.RangeSet(1, n_cities)  # 1-based indexing
    
    # 4. PARAMETERS (data containers)
    model.rating = pyo.Param(model.I, initialize={i+1: ratings[i] for i in range(n_cities)})
    model.region = pyo.Param(model.I, initialize={i+1: regions[i] for i in range(n_cities)})
    
    # 5. VARIABLES
    # Binary decision variables
    model.x = pyo.Var(model.I, within=pyo.Binary)
    
    # 6. OBJECTIVE FUNCTION
    def obj_rule(model):
        return sum(model.rating[i] * model.x[i] for i in model.I)
    model.objective = pyo.Objective(rule=obj_rule, sense=pyo.maximize)
    
    # 7. CONSTRAINTS
    
    # Total budget constraint
    def budget_rule(model):
        return sum(model.x[i] for i in model.I) <= 5
    model.budget_constraint = pyo.Constraint(rule=budget_rule)
    
    # Regional distribution constraint
    regions_list = list(set(regions))
    def region_rule(model, r):
        return sum(model.x[i] for i in model.I if model.region[i] == r) >= 1
    model.region_constraint = pyo.Constraint(regions_list, rule=region_rule)
    
    # City-level placement constraint
    def city_rule(model, i):
        return model.x[i] <= 2
    model.city_constraint = pyo.Constraint(model.I, rule=city_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("\nSelected restaurants:")
        for i in model.I:
            if pyo.value(model.x[i]) > 0.5:  # Only print selected restaurants
                print(f"City {i}: Rating = {model.rating[i]}, Region = {model.region[i]}")
        
    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__":
    restaurant_optimization()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 20.5
**Execution Time**: 1.13 seconds
**Reliability**: medium
**Analysis**: Pyomo achieved the optimal solution with an execution time of 1.128 seconds. Its reliability is medium due to slower performance compared to Gurobipy.

## 8. Cross-Solver Analysis and Final Recommendation

### Solver Results Comparison

| Solver | Status | Optimal Value | Execution Time | Decision Variables | Retry Attempt |
|--------|--------|---------------|----------------|-------------------|---------------|
| Gurobipy | OPTIMAL | 20.50 | 0.21s | N/A | N/A |
| Docplex | OPTIMAL | 20.50 | 1.26s | N/A | N/A |
| Pyomo | OPTIMAL | 20.50 | 1.13s | N/A | N/A |

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

### Final Recommendation
**Recommended Optimal Value**: 20.5
**Confidence Level**: HIGH
**Preferred Solver(s)**: gurobipy
**Reasoning**: Gurobipy is recommended due to its faster execution time and high reliability, ensuring efficient and accurate results.

### Business Interpretation
**Overall Strategy**: The optimal solution maximizes customer satisfaction by strategically placing restaurants within the budget and regional constraints.
**Objective Value Meaning**: The optimal objective value of 20.5 represents the maximum total customer satisfaction achievable under the given constraints.
**Resource Allocation Summary**: Resources should be allocated to place restaurants in cities with the highest ratings while ensuring regional distribution and budget adherence.
**Implementation Recommendations**: 1. Prioritize cities with higher ratings. 2. Ensure at least one restaurant is placed in each region. 3. Monitor the budget to avoid exceeding the limit of 5 restaurants.