# Complete GUROBIPY implementation

import gurobipy as gp
from gurobipy import GRB

def optimize_teacher_course_assignment():
    """Optimize teacher-course assignments to maximize expected grade improvement."""
    
    # 1. MODEL & DATA SETUP
    model = gp.Model("course_teach")
    
    # Data from the problem context
    courses = [101, 102, 103]
    teachers = [201, 202, 203]
    expected_improvements = {
        (101, 201): 0.8,
        (102, 202): 0.6,
        (103, 203): 0.7,
        (101, 202): 0.5,
        (102, 203): 0.4
    }
    
    # Validate data lengths
    assert len(expected_improvements) > 0, "Expected improvements data is empty"
    
    # 2. VARIABLES
    # Binary decision variables for teacher-course assignments
    x = model.addVars(expected_improvements.keys(), vtype=GRB.BINARY, name="x")
    
    # 3. OBJECTIVE FUNCTION
    # Maximize the total expected improvement in student grades
    model.setObjective(gp.quicksum(expected_improvements[i, j] * x[i, j] for i, j in expected_improvements), GRB.MAXIMIZE)
    
    # 4. CONSTRAINTS
    # Each course can have at most one teacher assigned
    model.addConstrs((gp.quicksum(x[i, j] for j in teachers if (i, j) in expected_improvements) <= 1 for i in courses), name="course_limit")
    
    # Each teacher can only be assigned to one course
    model.addConstrs((gp.quicksum(x[i, j] for i in courses if (i, j) in expected_improvements) <= 1 for j in teachers), name="teacher_limit")
    
    # 5. SOLVING & RESULTS
    model.optimize()
    
    if model.status == GRB.OPTIMAL:
        print(f"Optimal value: {model.objVal}")
        for i, j in expected_improvements:
            if x[i, j].x > 1e-6:
                print(f"Teacher {j} assigned to Course {i} with expected improvement {expected_improvements[i, 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_teacher_course_assignment()