# Complete GUROBIPY implementation

import gurobipy as gp
from gurobipy import GRB

def optimize_scientist_allocation():
    """Optimize the allocation of scientists to projects to maximize total hours worked."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("scientist_allocation")
    
    # Data from the database
    projects = [(1, 15.0), (2, 25.0), (3, 35.0)]
    assignments = [(101, 1), (101, 2), (102, 2), (102, 3), (103, 3)]
    constraint_bounds = {'scientist': 2, 'project': 1}
    
    # Extracting data
    project_ids = [p[0] for p in projects]
    hours = {p[0]: p[1] for p in projects}
    scientist_ids = list(set(a[0] for a in assignments))
    
    # Validate data lengths
    assert len(project_ids) == len(hours), "Mismatch in project data lengths"
    
    # 2. VARIABLES
    # Decision variables: x[i, j] = 1 if scientist i is assigned to project j
    x = model.addVars(scientist_ids, project_ids, vtype=GRB.BINARY, name="x")
    
    # 3. OBJECTIVE FUNCTION
    # Maximize total hours worked on projects
    model.setObjective(gp.quicksum(hours[j] * x[i, j] for i in scientist_ids for j in project_ids), GRB.MAXIMIZE)
    
    # 4. CONSTRAINTS
    
    # Minimum projects per scientist
    min_projects_per_scientist = constraint_bounds['scientist']
    model.addConstrs((gp.quicksum(x[i, j] for j in project_ids) >= min_projects_per_scientist for i in scientist_ids), 
                     name="min_projects_per_scientist")
    
    # Minimum scientists per project
    min_scientists_per_project = constraint_bounds['project']
    model.addConstrs((gp.quicksum(x[i, j] for i in scientist_ids) >= min_scientists_per_project for j in project_ids), 
                     name="min_scientists_per_project")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for i in scientist_ids:
            for j in project_ids:
                if x[i, j].x > 1e-6:
                    print(f"Scientist {i} assigned to Project {j}")
    elif model.status == GRB.INFEASIBLE:
        print("Problem is infeasible")
    elif model.status == GRB.UNBOUNDED:
        print("Problem is unbounded")
    
    return model

# Run the optimization
optimize_scientist_allocation()