# Complete Optimization Problem and Solution: architecture

## 1. Problem Context and Goals

### Context  
A construction company is tasked with optimizing the allocation of projects to architects to minimize the total length of bridges and mills built. The company must decide which bridges and mills to construct, represented by binary decision variables indicating whether a specific bridge or mill is built. Each bridge has a defined length in meters, and each mill has a defined length in feet, which directly contribute to the total length to be minimized.  

The company operates under specific business rules to ensure fair and manageable workloads for architects. Each architect must be assigned to at least one project, as defined by the minimum number of projects per architect, and no architect can be assigned more than three projects, as defined by the maximum number of projects per architect. These rules ensure a balanced distribution of work while preventing overburdening any single architect.  

The operational parameters, such as the lengths of bridges and mills, are sourced from the current database, which includes realistic data generated by experts in business, data, and optimization. The business configuration parameters, including the minimum and maximum number of projects per architect, are defined as scalar values and are critical for enforcing the constraints in the optimization problem.  

### Goals  
The primary goal of this optimization problem is to minimize the total length of bridges and mills built by architects. This is achieved by strategically selecting which bridges and mills to construct, ensuring that the sum of their lengths is as small as possible. Success is measured by the reduction in total length, which directly impacts resource efficiency and cost savings for the company.  

The optimization process ensures that all architects are assigned to at least one project and that no architect is assigned more than three projects. These constraints are critical for maintaining workload balance and operational feasibility.  

## 2. Constraints  

The optimization problem is subject to the following constraints:  
1. **Minimum Projects per Architect**: Each architect must be assigned to at least one project. This ensures that all architects are actively contributing to the construction efforts.  
2. **Maximum Projects per Architect**: No architect can be assigned more than three projects. This prevents overburdening any single architect and ensures manageable workloads.  

These constraints are enforced using the minimum and maximum number of projects per architect, which are defined as scalar parameters in the business configuration.  

## 3. Available Data  

### Database Schema  
```sql
-- Iteration 3 Database Schema
-- Objective: Schema changes include adding tables for decision variables and constraint bounds, updating the data dictionary, and refining business configuration logic to include scalar parameters for minimum and maximum projects per architect.

CREATE TABLE bridge_lengths (
  bridge_id INTEGER,
  length_meters INTEGER
);

CREATE TABLE mill_lengths (
  mill_id INTEGER,
  length_feet INTEGER
);

CREATE TABLE architect_project_assignments (
  architect_id INTEGER,
  bridge_assignment BOOLEAN,
  mill_assignment BOOLEAN
);

CREATE TABLE architect_constraints (
  architect_id INTEGER,
  min_projects INTEGER,
  max_projects INTEGER
);
```

### Data Dictionary  
- **bridge_lengths**: Stores the length of each bridge in meters, which is used to calculate the total length of bridges built.  
  - `bridge_id`: Unique identifier for each bridge.  
  - `length_meters`: Length of the bridge in meters, used as a coefficient in the optimization objective.  

- **mill_lengths**: Stores the length of each mill in feet, which is used to calculate the total length of mills built.  
  - `mill_id`: Unique identifier for each mill.  
  - `length_feet`: Length of the mill in feet, used as a coefficient in the optimization objective.  

- **architect_project_assignments**: Tracks the assignment of architects to projects, indicating whether a bridge or mill is built.  
  - `architect_id`: Unique identifier for each architect.  
  - `bridge_assignment`: Indicates whether the architect is assigned to build a bridge.  
  - `mill_assignment`: Indicates whether the architect is assigned to build a mill.  

- **architect_constraints**: Defines the minimum and maximum number of projects each architect can handle.  
  - `architect_id`: Unique identifier for each architect.  
  - `min_projects`: Minimum number of projects the architect must be assigned to.  
  - `max_projects`: Maximum number of projects the architect can be assigned to.  

### Current Stored Values  
```sql
-- Iteration 3 Realistic Data
-- Generated by triple expert (business + data + optimization)
-- Values were determined based on typical construction project lengths and architect workload capacities, ensuring realistic and meaningful data for optimization.

-- Realistic data for bridge_lengths
INSERT INTO bridge_lengths (bridge_id, length_meters) VALUES (1, 120);
INSERT INTO bridge_lengths (bridge_id, length_meters) VALUES (2, 250);
INSERT INTO bridge_lengths (bridge_id, length_meters) VALUES (3, 400);

-- Realistic data for mill_lengths
INSERT INTO mill_lengths (mill_id, length_feet) VALUES (1, 350);
INSERT INTO mill_lengths (mill_id, length_feet) VALUES (2, 450);
INSERT INTO mill_lengths (mill_id, length_feet) VALUES (3, 550);

-- Realistic data for architect_project_assignments
INSERT INTO architect_project_assignments (architect_id, bridge_assignment, mill_assignment) VALUES (1, True, False);
INSERT INTO architect_project_assignments (architect_id, bridge_assignment, mill_assignment) VALUES (2, False, True);
INSERT INTO architect_project_assignments (architect_id, bridge_assignment, mill_assignment) VALUES (3, True, True);

-- Realistic data for architect_constraints
INSERT INTO architect_constraints (architect_id, min_projects, max_projects) VALUES (1, 1, 3);
INSERT INTO architect_constraints (architect_id, min_projects, max_projects) VALUES (2, 1, 3);
INSERT INTO architect_constraints (architect_id, min_projects, max_projects) VALUES (3, 1, 3);
```

## 4. Mathematical Optimization Formulation

#### Decision Variables
- \( x_b \): Binary variable indicating whether bridge \( b \) is built (\( x_b = 1 \)) or not (\( x_b = 0 \)), for each bridge \( b \) in the `bridge_lengths` table.
- \( y_m \): Binary variable indicating whether mill \( m \) is built (\( y_m = 1 \)) or not (\( y_m = 0 \)), for each mill \( m \) in the `mill_lengths` table.
- \( z_{a,b} \): Binary variable indicating whether architect \( a \) is assigned to bridge \( b \) (\( z_{a,b} = 1 \)) or not (\( z_{a,b} = 0 \)), for each architect \( a \) and bridge \( b \).
- \( w_{a,m} \): Binary variable indicating whether architect \( a \) is assigned to mill \( m \) (\( w_{a,m} = 1 \)) or not (\( w_{a,m} = 0 \)), for each architect \( a \) and mill \( m \).

#### Objective Function
Minimize the total length of bridges and mills built:
\[
\text{Minimize } \sum_{b} \text{bridge_lengths.length_meters}_b \cdot x_b + \sum_{m} \text{mill_lengths.length_feet}_m \cdot y_m
\]

#### Constraints
1. **Minimum Projects per Architect**:
   \[
   \sum_{b} z_{a,b} + \sum_{m} w_{a,m} \geq \text{architect_constraints.min_projects}_a \quad \forall a
   \]
2. **Maximum Projects per Architect**:
   \[
   \sum_{b} z_{a,b} + \sum_{m} w_{a,m} \leq \text{architect_constraints.max_projects}_a \quad \forall a
   \]
3. **Bridge Assignment**:
   \[
   z_{a,b} \leq x_b \quad \forall a, b
   \]
4. **Mill Assignment**:
   \[
   w_{a,m} \leq y_m \quad \forall a, m
   \]
5. **Binary Constraints**:
   \[
   x_b, y_m, z_{a,b}, w_{a,m} \in \{0, 1\} \quad \forall a, b, m
   \]

#### Data Source Verification
- **Objective Function Coefficients**:
  - `bridge_lengths.length_meters` for each bridge \( b \).
  - `mill_lengths.length_feet` for each mill \( m \).
- **Constraint Coefficients**:
  - `architect_constraints.min_projects` for each architect \( a \).
  - `architect_constraints.max_projects` for each architect \( a \).

This formulation provides a complete, immediately solvable LINEAR mathematical model with all numerical coefficients derived from the provided data.

## 5. Gurobipy Implementation

```python
#!/usr/bin/env python3
"""
Gurobipy 12.0.2 Implementation for Construction Project Optimization
"""

import gurobipy as gp
from gurobipy import GRB

def optimize_construction_projects():
    """Optimize the allocation of projects to architects to minimize total length of bridges and mills built."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("construction_optimization")
    
    # Data from the database
    bridge_lengths = {1: 120, 2: 250, 3: 400}
    mill_lengths = {1: 350, 2: 450, 3: 550}
    architect_ids = [1, 2, 3]
    min_projects = {1: 1, 2: 1, 3: 1}
    max_projects = {1: 3, 2: 3, 3: 3}
    
    # Validate array lengths before loops
    assert len(bridge_lengths) == 3, "Bridge lengths array length mismatch"
    assert len(mill_lengths) == 3, "Mill lengths array length mismatch"
    assert len(architect_ids) == 3, "Architect IDs array length mismatch"
    assert len(min_projects) == 3, "Min projects array length mismatch"
    assert len(max_projects) == 3, "Max projects array length mismatch"
    
    # 2. VARIABLES
    # Decision variables for bridges and mills
    x = {b: model.addVar(vtype=GRB.BINARY, name=f"x_{b}") for b in bridge_lengths}
    y = {m: model.addVar(vtype=GRB.BINARY, name=f"y_{m}") for m in mill_lengths}
    
    # Assignment variables for architects
    z = {(a, b): model.addVar(vtype=GRB.BINARY, name=f"z_{a}_{b}") 
         for a in architect_ids for b in bridge_lengths}
    w = {(a, m): model.addVar(vtype=GRB.BINARY, name=f"w_{a}_{m}") 
         for a in architect_ids for m in mill_lengths}
    
    # 3. OBJECTIVE FUNCTION
    # Minimize the total length of bridges and mills built
    model.setObjective(
        gp.quicksum(bridge_lengths[b] * x[b] for b in bridge_lengths) +
        gp.quicksum(mill_lengths[m] * y[m] for m in mill_lengths),
        GRB.MINIMIZE
    )
    
    # 4. CONSTRAINTS
    
    # Minimum Projects per Architect
    for a in architect_ids:
        model.addConstr(
            gp.quicksum(z[a, b] for b in bridge_lengths) +
            gp.quicksum(w[a, m] for m in mill_lengths) >= min_projects[a],
            name=f"min_projects_{a}"
        )
    
    # Maximum Projects per Architect
    for a in architect_ids:
        model.addConstr(
            gp.quicksum(z[a, b] for b in bridge_lengths) +
            gp.quicksum(w[a, m] for m in mill_lengths) <= max_projects[a],
            name=f"max_projects_{a}"
        )
    
    # Bridge Assignment
    for a in architect_ids:
        for b in bridge_lengths:
            model.addConstr(z[a, b] <= x[b], name=f"bridge_assignment_{a}_{b}")
    
    # Mill Assignment
    for a in architect_ids:
        for m in mill_lengths:
            model.addConstr(w[a, m] <= y[m], name=f"mill_assignment_{a}_{m}")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for b in bridge_lengths:
            if x[b].x > 1e-6:
                print(f"Bridge {b} is built.")
        for m in mill_lengths:
            if y[m].x > 1e-6:
                print(f"Mill {m} is built.")
        for a in architect_ids:
            for b in bridge_lengths:
                if z[a, b].x > 1e-6:
                    print(f"Architect {a} is assigned to bridge {b}.")
            for m in mill_lengths:
                if w[a, m].x > 1e-6:
                    print(f"Architect {a} is assigned to mill {m}.")
    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_construction_projects()
```

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

## 6. DOCplex Implementation

```python
#!/usr/bin/env python3
"""
DOCplex implementation for the construction company optimization problem.
"""

from docplex.mp.model import Model

def optimize_architecture():
    """Optimize the allocation of projects to architects to minimize total length of bridges and mills built."""
    
    # 1. MODEL & DATA SETUP
    mdl = Model(name="architecture_optimization")
    
    # Data from the database
    bridge_lengths = {1: 120, 2: 250, 3: 400}
    mill_lengths = {1: 350, 2: 450, 3: 550}
    architect_constraints = {1: (1, 3), 2: (1, 3), 3: (1, 3)}
    
    # Extract unique IDs
    bridge_ids = list(bridge_lengths.keys())
    mill_ids = list(mill_lengths.keys())
    architect_ids = list(architect_constraints.keys())
    
    # CRITICAL: Validate array lengths to prevent IndexError
    assert len(bridge_ids) > 0, "No bridges found"
    assert len(mill_ids) > 0, "No mills found"
    assert len(architect_ids) > 0, "No architects found"
    
    # 2. VARIABLES
    # Decision variables
    x = {b: mdl.binary_var(name=f"x_{b}") for b in bridge_ids}  # Whether bridge b is built
    y = {m: mdl.binary_var(name=f"y_{m}") for m in mill_ids}    # Whether mill m is built
    z = {(a, b): mdl.binary_var(name=f"z_{a}_{b}") for a in architect_ids for b in bridge_ids}  # Architect a assigned to bridge b
    w = {(a, m): mdl.binary_var(name=f"w_{a}_{m}") for a in architect_ids for m in mill_ids}    # Architect a assigned to mill m
    
    # 3. OBJECTIVE FUNCTION
    # Minimize total length of bridges and mills built
    total_length = mdl.sum(bridge_lengths[b] * x[b] for b in bridge_ids) + mdl.sum(mill_lengths[m] * y[m] for m in mill_ids)
    mdl.minimize(total_length)
    
    # 4. CONSTRAINTS
    
    # Minimum Projects per Architect
    for a in architect_ids:
        min_projects = architect_constraints[a][0]
        mdl.add_constraint(mdl.sum(z[a, b] for b in bridge_ids) + mdl.sum(w[a, m] for m in mill_ids) >= min_projects, ctname=f"min_projects_{a}")
    
    # Maximum Projects per Architect
    for a in architect_ids:
        max_projects = architect_constraints[a][1]
        mdl.add_constraint(mdl.sum(z[a, b] for b in bridge_ids) + mdl.sum(w[a, m] for m in mill_ids) <= max_projects, ctname=f"max_projects_{a}")
    
    # Bridge Assignment
    for a in architect_ids:
        for b in bridge_ids:
            mdl.add_constraint(z[a, b] <= x[b], ctname=f"bridge_assignment_{a}_{b}")
    
    # Mill Assignment
    for a in architect_ids:
        for m in mill_ids:
            mdl.add_constraint(w[a, m] <= y[m], ctname=f"mill_assignment_{a}_{m}")
    
    # 5. SOLVING & RESULTS
    solution = mdl.solve()
    
    if solution:
        print(f"Optimal value: {solution.objective_value}")
        for b in bridge_ids:
            if solution.get_value(x[b]) > 0.5:
                print(f"Bridge {b} is built.")
        for m in mill_ids:
            if solution.get_value(y[m]) > 0.5:
                print(f"Mill {m} is built.")
        for a in architect_ids:
            for b in bridge_ids:
                if solution.get_value(z[a, b]) > 0.5:
                    print(f"Architect {a} is assigned to bridge {b}.")
            for m in mill_ids:
                if solution.get_value(w[a, m]) > 0.5:
                    print(f"Architect {a} is assigned to mill {m}.")
    else:
        print("No solution found")
        print(f"Status: {mdl.solve_details.status}")
    
    return mdl

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

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 120.0
**Execution Time**: 1.13 seconds
**Reliability**: medium
**Analysis**: DOCplex also achieved the optimal solution but took significantly longer (1.127 seconds) compared to Gurobipy. Its reliability is medium due to slower performance despite achieving the correct result.

## 7. Pyomo Implementation

```python
#!/usr/bin/env python3
"""
Pyomo 6.9.2 Implementation for Construction Company Optimization Problem
"""

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

def optimization_problem():
    """Optimize the allocation of projects to architects to minimize total length of bridges and mills built."""
    
    # 1. MODEL CREATION
    model = pyo.ConcreteModel()
    
    # 2. DATA SETUP
    # Bridge data
    bridge_ids = [1, 2, 3]
    bridge_lengths = {1: 120, 2: 250, 3: 400}  # in meters
    
    # Mill data
    mill_ids = [1, 2, 3]
    mill_lengths = {1: 350, 2: 450, 3: 550}  # in feet
    
    # Architect data
    architect_ids = [1, 2, 3]
    min_projects = {1: 1, 2: 1, 3: 1}
    max_projects = {1: 3, 2: 3, 3: 3}
    
    # CRITICAL: Validate array lengths before indexing
    assert len(bridge_ids) == len(bridge_lengths), "Bridge data length mismatch"
    assert len(mill_ids) == len(mill_lengths), "Mill data length mismatch"
    assert len(architect_ids) == len(min_projects) == len(max_projects), "Architect data length mismatch"
    
    # 3. SETS (Pyomo way to define indices)
    model.B = pyo.Set(initialize=bridge_ids)  # Set of bridges
    model.M = pyo.Set(initialize=mill_ids)    # Set of mills
    model.A = pyo.Set(initialize=architect_ids)  # Set of architects
    
    # 4. PARAMETERS (data containers)
    model.bridge_length = pyo.Param(model.B, initialize=bridge_lengths)
    model.mill_length = pyo.Param(model.M, initialize=mill_lengths)
    model.min_projects = pyo.Param(model.A, initialize=min_projects)
    model.max_projects = pyo.Param(model.A, initialize=max_projects)
    
    # 5. VARIABLES
    # Binary variables indicating whether a bridge or mill is built
    model.x = pyo.Var(model.B, within=pyo.Binary)  # x_b
    model.y = pyo.Var(model.M, within=pyo.Binary)  # y_m
    
    # Binary variables indicating architect assignments
    model.z = pyo.Var(model.A, model.B, within=pyo.Binary)  # z_{a,b}
    model.w = pyo.Var(model.A, model.M, within=pyo.Binary)  # w_{a,m}
    
    # 6. OBJECTIVE FUNCTION
    def obj_rule(model):
        return sum(model.bridge_length[b] * model.x[b] for b in model.B) + \
               sum(model.mill_length[m] * model.y[m] for m in model.M)
    model.objective = pyo.Objective(rule=obj_rule, sense=pyo.minimize)
    
    # 7. CONSTRAINTS
    
    # Minimum Projects per Architect
    def min_projects_rule(model, a):
        return sum(model.z[a, b] for b in model.B) + sum(model.w[a, m] for m in model.M) >= model.min_projects[a]
    model.min_projects_constraint = pyo.Constraint(model.A, rule=min_projects_rule)
    
    # Maximum Projects per Architect
    def max_projects_rule(model, a):
        return sum(model.z[a, b] for b in model.B) + sum(model.w[a, m] for m in model.M) <= model.max_projects[a]
    model.max_projects_constraint = pyo.Constraint(model.A, rule=max_projects_rule)
    
    # Bridge Assignment
    def bridge_assignment_rule(model, a, b):
        return model.z[a, b] <= model.x[b]
    model.bridge_assignment_constraint = pyo.Constraint(model.A, model.B, rule=bridge_assignment_rule)
    
    # Mill Assignment
    def mill_assignment_rule(model, a, m):
        return model.w[a, m] <= model.y[m]
    model.mill_assignment_constraint = pyo.Constraint(model.A, model.M, rule=mill_assignment_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("\nVariable values:")
        for b in model.B:
            if pyo.value(model.x[b]) > 0:
                print(f"Bridge {b} is built.")
        for m in model.M:
            if pyo.value(model.y[m]) > 0:
                print(f"Mill {m} is built.")
        for a in model.A:
            for b in model.B:
                if pyo.value(model.z[a, b]) > 0:
                    print(f"Architect {a} is assigned to bridge {b}.")
            for m in model.M:
                if pyo.value(model.w[a, m]) > 0:
                    print(f"Architect {a} is assigned to mill {m}.")
        
    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 problem
if __name__ == "__main__":
    optimization_problem()
```

### Execution Results
**Status**: OPTIMAL
**Optimal Value**: 120.0
**Execution Time**: 0.85 seconds
**Reliability**: medium
**Analysis**: Pyomo achieved the optimal solution with an execution time of 0.853 seconds, which is slower than Gurobipy but faster than DOCplex. Its reliability is medium due to its dependency on external solvers and slightly slower performance.

## 8. Cross-Solver Analysis and Final Recommendation

### Solver Results Comparison

| Solver | Status | Optimal Value | Execution Time | Decision Variables | Retry Attempt |
|--------|--------|---------------|----------------|-------------------|---------------|
| Gurobipy | OPTIMAL | 120.00 | 0.17s | N/A | N/A |
| Docplex | OPTIMAL | 120.00 | 1.13s | N/A | N/A |
| Pyomo | OPTIMAL | 120.00 | 0.85s | N/A | N/A |

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

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

### Business Interpretation
**Overall Strategy**: The optimal solution minimizes the total length of bridges and mills built, ensuring efficient resource allocation while meeting architect constraints.
**Objective Value Meaning**: The optimal value of 120.0 represents the minimized total length of bridges and mills built, ensuring cost-effective and efficient project execution.
**Resource Allocation Summary**: Resources should be allocated to build specific bridges and mills while ensuring architects are assigned to projects within their minimum and maximum project constraints.
**Implementation Recommendations**: 1. Build bridges and mills as per the optimal solution. 2. Assign architects to projects based on the optimal assignments. 3. Monitor project execution to ensure constraints are met.