52bf303537
This commit introduces functionality in the MeetingSchedule feature to manage student exclusions during scheduling. The Index.razor component has been updated to include methods for saving and loading excluded students, as well as UI elements for toggling exclusions. Additionally, the TeamScheduler logic has been modified to account for excluded students when calculating overlaps and scheduling teams. These changes improve the flexibility and accuracy of the scheduling process, enhancing the overall user experience.
151 lines
6.1 KiB
C#
151 lines
6.1 KiB
C#
using System.Diagnostics;
|
|
using Core.Entities;
|
|
using Google.OrTools.Sat;
|
|
|
|
namespace Core.Calculation;
|
|
|
|
/// <summary>
|
|
/// Solves the team meeting scheduling problem using constraint programming.
|
|
/// Assigns teams to time slots while minimizing student schedule conflicts.
|
|
/// </summary>
|
|
public class TeamScheduler
|
|
{
|
|
private readonly IList<Student> _studentObjects;
|
|
private readonly IList<Team> _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 = [];
|
|
|
|
/// <summary>
|
|
/// Creates a new team scheduler instance.
|
|
/// </summary>
|
|
/// <param name="teams">The teams to schedule (must not be null or empty)</param>
|
|
/// <param name="numTimeSlots">The number of available time slots (must be positive)</param>
|
|
/// <param name="allStudents">All students participating in teams (must not be null or empty)</param>
|
|
/// <param name="maxSolveTimeSeconds">Maximum solver time in seconds (default: 10.0)</param>
|
|
/// <exception cref="ArgumentNullException">Thrown when teams or allStudents is null</exception>
|
|
/// <exception cref="ArgumentException">Thrown when collections are empty or numTimeSlots is invalid</exception>
|
|
public TeamScheduler(IEnumerable<Team> teams, int numTimeSlots, IEnumerable<Student> 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();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a constraint requiring two teams to be scheduled in different time slots.
|
|
/// </summary>
|
|
/// <param name="team1">First team</param>
|
|
/// <param name="team2">Second team</param>
|
|
/// <exception cref="ArgumentException">Thrown when either team is not found in the scheduler</exception>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Solves the team scheduling problem using constraint programming.
|
|
/// Minimizes the number of time slots where students have conflicting team meetings.
|
|
/// </summary>
|
|
/// <returns>A solution containing team assignments to time slots and conflict information</returns>
|
|
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());
|
|
}
|
|
} |