using System.Diagnostics; using Core.Entities; using Google.OrTools.Sat; namespace Core.Calculation; /// /// Solves the team meeting scheduling problem using constraint programming. /// Assigns teams to time slots while minimizing student schedule conflicts. /// public class TeamScheduler { private readonly IList _studentObjects; private readonly IList _teamObjects; private readonly double _maxSolveTimeSeconds; private readonly int[] _students; private readonly int[] _teams; private readonly int[] _timeSlots; private readonly List<(int team1, int team2)> _scheduleSeparateTeams = []; /// /// Creates a new team scheduler instance. /// /// The teams to schedule (must not be null or empty) /// The number of available time slots (must be positive) /// All students participating in teams (must not be null or empty) /// Maximum solver time in seconds (default: 10.0) /// Thrown when teams or allStudents is null /// Thrown when collections are empty or numTimeSlots is invalid public TeamScheduler(IEnumerable teams, int numTimeSlots, IEnumerable allStudents, double maxSolveTimeSeconds = 10.0) { if (teams == null) throw new ArgumentNullException(nameof(teams)); if (allStudents == null) throw new ArgumentNullException(nameof(allStudents)); if (numTimeSlots <= 0) throw new ArgumentException("Number of time slots must be positive", nameof(numTimeSlots)); _teamObjects = teams.ToArray(); _studentObjects = allStudents.ToList(); _maxSolveTimeSeconds = maxSolveTimeSeconds; if (_teamObjects.Count == 0) throw new ArgumentException("Teams collection cannot be empty", nameof(teams)); if (_studentObjects.Count == 0) throw new ArgumentException("Students collection cannot be empty", nameof(allStudents)); _students = Enumerable.Range(0, _studentObjects.Count).ToArray(); _teams = Enumerable.Range(0, _teamObjects.Count).ToArray(); _timeSlots = Enumerable.Range(0, numTimeSlots).ToArray(); } /// /// Adds a constraint requiring two teams to be scheduled in different time slots. /// /// First team /// Second team /// Thrown when either team is not found in the scheduler public void ScheduleSeparate(Team team1, Team team2) { var one = _teamObjects.IndexOf(team1); var two = _teamObjects.IndexOf(team2); if (one == -1) throw new ArgumentException($"Team '{team1}' not found in scheduler", nameof(team1)); if (two == -1) throw new ArgumentException($"Team '{team2}' not found in scheduler", nameof(team2)); _scheduleSeparateTeams.Add((one, two)); } /// /// Solves the team scheduling problem using constraint programming. /// Minimizes the number of time slots where students have conflicting team meetings. /// /// A solution containing team assignments to time slots and conflict information public TeamSchedulerSolution Solve() { // Create constraint programming model var model = new CpModel(); // Build membership matrix: m[i,t] = 1 if student i is on team t, else 0 // Use team.Students to support PartialTeam objects with omitted students var m = new int[_students.Length,_teams.Length]; foreach (var i in _students) foreach (var t in _teams) m[i, t] = _teamObjects[t].Students.Any(s => s.Id == _studentObjects[i].Id) ? 1 : 0; // Decision variables: // x[t,s] = 1 if meeting of team t takes place at time slot s, else 0 var x = new IntVar[_teams.Length, _timeSlots.Length]; foreach (var t in _teams) foreach (var s in _timeSlots) x[t, s] = model.NewIntVar(0, 1,$"team time slots[{t},{s}]"); // y[i,s] = 1 if student i has at least one meeting at time slot s, else 0 var y = new IntVar[_students.Length, _timeSlots.Length]; foreach (var i in _students) foreach (var s in _timeSlots) y[i, s] = model.NewIntVar(0, 1, $"individual time slots[{i},{s}]"); // Constraint: each team meets exactly once foreach (var t in _teams) model.AddLinearConstraint(LinearExpr.Sum(_timeSlots.Select(s => x[t, s])), 1L, 1L); // Constraint: Link y[i,s] to whether student i has meetings at slot s // y[i,s] <= sum over all teams t of (m[i,t] * x[t,s]) // This forces y[i,s] to be 1 if student i has at least one team meeting at slot s foreach (var i in _students) foreach (var s in _timeSlots) model.Add( new BoundedLinearExpression( y[i, s], LinearExpr.Sum(_teams.Select(t => m[i, t] * x[t, s])), false)); // Objective: minimize the sum of y[i,s] values (minimize student conflicts) var indTimeSlotVars = LinearExpr.NewBuilder(); foreach (var i in _students) foreach (var s in _timeSlots) indTimeSlotVars.Add(y[i, s]); model.Minimize(indTimeSlotVars); // Constraint: teams marked as "separate" must be in different time slots foreach (var (team1, team2) in _scheduleSeparateTeams) foreach (var s in _timeSlots) model.Add(x[team1, s] != x[team2, s]); // Configure and run solver var solver = new CpSolver(); solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}"; var cpSolverStatus = solver.Solve(model); Debug.WriteLine($"Solver status: {cpSolverStatus}"); var timeSlotTeams = new Team[_timeSlots.Length][]; if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible)) return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString()); // Extract solution: which teams are assigned to each time slot foreach (var s in _timeSlots) { var teams = (from t in _teams where solver.Value(x[t, s]) > 0 select _teamObjects[t]).ToArray(); timeSlotTeams[s] = teams; } return new TeamSchedulerSolution(timeSlotTeams, _studentObjects.ToArray(), cpSolverStatus.ToString()); } }