Refactor/cleanup the EventAssignemnt
This commit is contained in:
+286
-233
@@ -1,150 +1,209 @@
|
|||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.Security.Cryptography.X509Certificates;
|
|
||||||
using Core.Entities;
|
using Core.Entities;
|
||||||
using Google.OrTools.Sat;
|
using Google.OrTools.Sat;
|
||||||
using Microsoft.EntityFrameworkCore.Metadata;
|
|
||||||
using IntVar = Google.OrTools.Sat.IntVar;
|
|
||||||
|
|
||||||
namespace Core.Calculation
|
namespace Core.Calculation
|
||||||
{
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Solves the event assignment problem using constraint programming.
|
||||||
|
/// Assigns students to events based on rankings, capacity constraints, and team requirements.
|
||||||
|
/// </summary>
|
||||||
public class EventAssignment
|
public class EventAssignment
|
||||||
{
|
{
|
||||||
private readonly IList<EventDefinition> _events;
|
// Constants for magic numbers
|
||||||
|
private const double TEAM_DIVISION_MULTIPLIER = 1.25;
|
||||||
|
private const int MIN_REGIONAL_EVENTS = 1;
|
||||||
|
private const int MAX_REGIONAL_EVENTS = 2;
|
||||||
|
|
||||||
|
private readonly IList<EventDefinition> _events;
|
||||||
private readonly IList<Student> _students;
|
private readonly IList<Student> _students;
|
||||||
private readonly AssignmentParameters _parameters;
|
private readonly AssignmentParameters _parameters;
|
||||||
private readonly int[] _allEvents;
|
private readonly double _maxSolveTimeSeconds;
|
||||||
|
private readonly int[] _allEvents;
|
||||||
private readonly int[] _allStudents;
|
private readonly int[] _allStudents;
|
||||||
// how many students have picked each eventDefinition?
|
// how many students have picked each event?
|
||||||
private readonly int[] _eventPickCounts;
|
private readonly int[] _eventPickCounts;
|
||||||
private IList<AssignmentRequirement> _assignmentRequirements = new List<AssignmentRequirement>();
|
private IList<AssignmentRequirement> _assignmentRequirements = [];
|
||||||
private IList<EventDefinition> _droppedEvents = new List<EventDefinition>();
|
private IList<EventDefinition> _droppedEvents = [];
|
||||||
private IList<EventDefinition> _includedEvents = new List<EventDefinition>();
|
private IList<EventDefinition> _includedEvents = [];
|
||||||
private IList<EventDefinition> _twoTeams = new List<EventDefinition>();
|
private IList<EventDefinition> _twoTeams = [];
|
||||||
|
|
||||||
public EventAssignment(IList<EventDefinition> events, IList<Student> students, AssignmentParameters parameters)
|
/// <summary>
|
||||||
|
/// Creates a new event assignment optimizer.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">The events to assign students to (must not be null or empty)</param>
|
||||||
|
/// <param name="students">The students to assign to events (must not be null or empty)</param>
|
||||||
|
/// <param name="parameters">Assignment parameters and constraints</param>
|
||||||
|
/// <param name="maxSolveTimeSeconds">Maximum solver time in seconds (default: 60.0)</param>
|
||||||
|
/// <exception cref="ArgumentNullException">Thrown when events, students, or parameters is null</exception>
|
||||||
|
/// <exception cref="ArgumentException">Thrown when collections are empty</exception>
|
||||||
|
public EventAssignment(IList<EventDefinition> events, IList<Student> students,
|
||||||
|
AssignmentParameters parameters, double maxSolveTimeSeconds = 60.0)
|
||||||
{
|
{
|
||||||
_events = events;
|
if (events == null)
|
||||||
|
throw new ArgumentNullException(nameof(events));
|
||||||
|
if (students == null)
|
||||||
|
throw new ArgumentNullException(nameof(students));
|
||||||
|
if (parameters == null)
|
||||||
|
throw new ArgumentNullException(nameof(parameters));
|
||||||
|
|
||||||
|
if (events.Count == 0)
|
||||||
|
throw new ArgumentException("Events collection cannot be empty", nameof(events));
|
||||||
|
if (students.Count == 0)
|
||||||
|
throw new ArgumentException("Students collection cannot be empty", nameof(students));
|
||||||
|
|
||||||
|
_events = events;
|
||||||
_students = students;
|
_students = students;
|
||||||
_parameters = parameters;
|
_parameters = parameters;
|
||||||
_allEvents = Enumerable.Range(0, _events.Count).ToArray();
|
_maxSolveTimeSeconds = maxSolveTimeSeconds;
|
||||||
|
_allEvents = Enumerable.Range(0, _events.Count).ToArray();
|
||||||
_allStudents = Enumerable.Range(0, _students.Count).ToArray();
|
_allStudents = Enumerable.Range(0, _students.Count).ToArray();
|
||||||
_eventPickCounts = new int[_allEvents.Length];
|
_eventPickCounts = new int[_allEvents.Length];
|
||||||
|
|
||||||
for (var i = 0; i < _events.Count; i++)
|
for (var i = 0; i < _events.Count; i++)
|
||||||
{
|
{
|
||||||
var e = _events[i];
|
var e = _events[i];
|
||||||
|
// Performance: Use Any() instead of Count() > 0
|
||||||
_eventPickCounts[i] = _students.Count(s => s.EventRankings.Count(er => er.EventDefinition == e) > 0);
|
_eventPickCounts[i] = _students.Count(s => s.EventRankings.Any(er => er.EventDefinition == e));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds a requirement to include or exclude a specific student-event pairing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="assignmentRequirement">The assignment requirement to add</param>
|
||||||
public void AddAssignmentRequirement(AssignmentRequirement assignmentRequirement)
|
public void AddAssignmentRequirement(AssignmentRequirement assignmentRequirement)
|
||||||
{
|
{
|
||||||
_assignmentRequirements.Add(assignmentRequirement);
|
_assignmentRequirements.Add(assignmentRequirement);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void RemoveEvents(IList<EventDefinition> events)
|
/// <summary>
|
||||||
|
/// Marks events to be excluded from the assignment solution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">Events to drop from consideration</param>
|
||||||
|
public void RemoveEvents(IList<EventDefinition> events)
|
||||||
{
|
{
|
||||||
_droppedEvents = events;
|
_droppedEvents = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void IncludedEvents(IList<EventDefinition> events)
|
/// <summary>
|
||||||
|
/// Forces specific events to be included in the solution.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">Events that must be included</param>
|
||||||
|
public void SetIncludedEvents(IList<EventDefinition> events)
|
||||||
{
|
{
|
||||||
_includedEvents = events;
|
_includedEvents = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Allows specific events to have two teams instead of one.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="events">Events that can have two teams</param>
|
||||||
public void AllowTwoTeams(IList<EventDefinition> events)
|
public void AllowTwoTeams(IList<EventDefinition> events)
|
||||||
{
|
{
|
||||||
_twoTeams = events;
|
_twoTeams = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Solves the event assignment problem using constraint programming.
|
||||||
|
/// Assigns students to events based on rankings, capacity constraints, and requirements.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>A solution containing team assignments and status</returns>
|
||||||
public async Task<EventAssignmentSolution> Solve()
|
public async Task<EventAssignmentSolution> Solve()
|
||||||
{
|
{
|
||||||
Debug.WriteLine(_parameters);
|
try
|
||||||
|
{
|
||||||
|
Debug.WriteLine(_parameters);
|
||||||
|
|
||||||
// Model.
|
// Create constraint programming model
|
||||||
var model = new CpModel();
|
var model = new CpModel();
|
||||||
|
|
||||||
// Variables.
|
// Decision variables: x[e,s] = 1 if student s is assigned to event e
|
||||||
var x = new BoolVar[_allEvents.Length, _allStudents.Length];
|
var x = new BoolVar[_allEvents.Length, _allStudents.Length];
|
||||||
foreach (var e in _allEvents)
|
foreach (var e in _allEvents)
|
||||||
foreach (var s in _allStudents)
|
foreach (var s in _allStudents)
|
||||||
x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");
|
x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");
|
||||||
|
|
||||||
AddAssignmentRequirements(model, x);
|
// Add all constraints
|
||||||
|
AddAssignmentRequirements(model, x);
|
||||||
|
var assignmentThresholdsList = AddEventAssignmentThresholds(model, x);
|
||||||
|
LimitStudentAssignment(model, x);
|
||||||
|
SetLevelOfEffort(model, x);
|
||||||
|
|
||||||
var assignmentThresholdsList = AddEventAssignmentThresholds(model, x);
|
if (_parameters.RequireOnSite)
|
||||||
|
RequireOnSiteActivity(model, x);
|
||||||
|
|
||||||
LimitStudentAssignment(model, x);
|
if (_parameters.RequireRegional)
|
||||||
|
RequireRegionalEvent(model, x);
|
||||||
|
|
||||||
// set the range for level of effort
|
AtMostOneIndividualEvent(model, x);
|
||||||
SetLevelOfEffort(model, x);
|
IndividualEventsMustBeRanked(model, x);
|
||||||
|
OptimizeStudentEventRankings(model, x);
|
||||||
// each student should be assigned at least one on site activity
|
|
||||||
if (_parameters.RequireOnSite)
|
|
||||||
RequireOnSiteActivity(model, x);
|
|
||||||
|
|
||||||
// students should have at least one regional event
|
|
||||||
if (_parameters.RequireRegional)
|
|
||||||
RequireRegionalEvent(model, x);
|
|
||||||
|
|
||||||
// students should have at maximum one individual event
|
// Configure and run solver
|
||||||
AtMostOneIndividualEvent(model, x);
|
Debug.WriteLine("Starting optimization");
|
||||||
|
var solver = new CpSolver();
|
||||||
|
solver.StringParameters = $"max_time_in_seconds:{_maxSolveTimeSeconds}";
|
||||||
|
var cpSolverStatus = await Task.Run(() => solver.Solve(model));
|
||||||
|
|
||||||
//EventHasInterestedStudent(model, x);
|
Debug.WriteLine($"Solver status: {cpSolverStatus}");
|
||||||
|
|
||||||
IndividualEventsMustBeRanked(model, x);
|
if (cpSolverStatus == CpSolverStatus.Infeasible)
|
||||||
|
{
|
||||||
|
Debug.WriteLine("Problem is infeasible - constraints cannot be satisfied");
|
||||||
|
}
|
||||||
|
|
||||||
OptimizeStudentEventRankings(model, x);
|
var eventAssignmentsList = GetEventAssignments(x, solver, cpSolverStatus);
|
||||||
|
|
||||||
|
var eventAssignmentSolution =
|
||||||
|
new EventAssignmentSolution
|
||||||
|
(
|
||||||
|
eventAssignmentsList.ToArray(),
|
||||||
|
cpSolverStatus.ToString(),
|
||||||
|
assignmentThresholdsList
|
||||||
|
);
|
||||||
|
|
||||||
Debug.WriteLine("Starting optimization");
|
return eventAssignmentSolution;
|
||||||
var solver = new CpSolver();
|
}
|
||||||
var cpSolverStatus = await Task.Run(() => solver.Solve(model));
|
catch (Exception ex)
|
||||||
|
{
|
||||||
// print solver status
|
Debug.WriteLine($"Error solving event assignment: {ex}");
|
||||||
Debug.WriteLine($"Solver status: {cpSolverStatus}");
|
throw;
|
||||||
|
}
|
||||||
var eventAssignmentsList = GetEventAssignments(x, solver, cpSolverStatus);
|
|
||||||
|
|
||||||
var eventAssignmentSolution =
|
|
||||||
new EventAssignmentSolution
|
|
||||||
(
|
|
||||||
eventAssignmentsList.ToArray(),
|
|
||||||
cpSolverStatus.ToString(),
|
|
||||||
assignmentThresholdsList
|
|
||||||
);
|
|
||||||
|
|
||||||
return eventAssignmentSolution;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Take the solution and map it back to the entities
|
/// <summary>
|
||||||
|
/// Extracts the solution from the solver results.
|
||||||
|
/// </summary>
|
||||||
private List<Team> GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus)
|
private List<Team> GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus)
|
||||||
{
|
{
|
||||||
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
|
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
|
||||||
return [];
|
return [];
|
||||||
|
|
||||||
var eventAssignments =
|
var eventAssignments =
|
||||||
from e in _allEvents
|
from e in _allEvents
|
||||||
let students =
|
let students =
|
||||||
from s in _allStudents
|
from s in _allStudents
|
||||||
where solver.BooleanValue(x[e, s])
|
where solver.BooleanValue(x[e, s])
|
||||||
select _students[s]
|
select _students[s]
|
||||||
where students.Any()
|
where students.Any()
|
||||||
select new Team
|
select new Team
|
||||||
{
|
{
|
||||||
Identifier = _events[e].Name,
|
Identifier = _events[e].Name,
|
||||||
Event = _events[e],
|
Event = _events[e],
|
||||||
Students = students.ToList()
|
Students = students.ToList()
|
||||||
};
|
};
|
||||||
|
|
||||||
return eventAssignments.ToList();
|
return eventAssignments.ToList();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximize student event rankings
|
/// <summary>
|
||||||
|
/// Objective function: Maximize student event rankings (students get higher-ranked events).
|
||||||
|
/// </summary>
|
||||||
private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x)
|
private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
var maximizePicks = LinearExpr.NewBuilder();
|
var maximizePicks = LinearExpr.NewBuilder();
|
||||||
|
|
||||||
foreach (var s in _allStudents)
|
foreach (var s in _allStudents)
|
||||||
{
|
{
|
||||||
var eventPickCoefficients = GetEventPickCoefficients(_events, _students[s].EventRankings);
|
var eventPickCoefficients = GetEventPickCoefficients(_events, _students[s].EventRankings);
|
||||||
@@ -157,58 +216,64 @@ namespace Core.Calculation
|
|||||||
model.Maximize(maximizePicks);
|
model.Maximize(maximizePicks);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint: Each student must have 1-2 regional events.
|
||||||
|
/// </summary>
|
||||||
private void RequireRegionalEvent(CpModel model, BoolVar[,] x)
|
private void RequireRegionalEvent(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
foreach (var s in _allStudents)
|
AddStudentConstraint(model, x,
|
||||||
{
|
evt => evt.RegionalEvent,
|
||||||
var regionalEvent = new List<ILiteral>();
|
(m, list) => m.AddLinearConstraint(LinearExpr.Sum(list), MIN_REGIONAL_EVENTS, MAX_REGIONAL_EVENTS));
|
||||||
foreach (var e in _allEvents)
|
|
||||||
{
|
|
||||||
if (_events[e].RegionalEvent)
|
|
||||||
regionalEvent.Add(x[e, s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
// between 1 and 2 regional events
|
|
||||||
model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2);
|
|
||||||
regionalEvent.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint: Each student can have at most one individual event.
|
||||||
|
/// </summary>
|
||||||
private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x)
|
private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
foreach (var s in _allStudents)
|
AddStudentConstraint(model, x,
|
||||||
{
|
evt => evt.EventFormat == EventFormat.Individual,
|
||||||
var individualEvent = new List<ILiteral>();
|
(m, list) => m.AddAtMostOne(list));
|
||||||
foreach (var e in _allEvents)
|
|
||||||
{
|
|
||||||
if (_events[e].EventFormat == EventFormat.Individual)
|
|
||||||
individualEvent.Add(x[e, s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
model.AddAtMostOne(individualEvent);
|
|
||||||
individualEvent.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint: Each student must have at least one on-site activity.
|
||||||
|
/// </summary>
|
||||||
private void RequireOnSiteActivity(CpModel model, BoolVar[,] x)
|
private void RequireOnSiteActivity(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
|
AddStudentConstraint(model, x,
|
||||||
|
evt => evt.OnSiteActivity,
|
||||||
|
(m, list) => m.AddAtLeastOne(list));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Helper method to add constraints for all students based on event filters.
|
||||||
|
/// Reduces code duplication and improves performance by reusing the list buffer.
|
||||||
|
/// </summary>
|
||||||
|
private void AddStudentConstraint(CpModel model, BoolVar[,] x,
|
||||||
|
Func<EventDefinition, bool> eventFilter, Action<CpModel, List<ILiteral>> constraintAction)
|
||||||
|
{
|
||||||
|
var buffer = new List<ILiteral>();
|
||||||
foreach (var s in _allStudents)
|
foreach (var s in _allStudents)
|
||||||
{
|
{
|
||||||
var onSiteActivity = new List<ILiteral>();
|
buffer.Clear();
|
||||||
foreach (var e in _allEvents)
|
foreach (var e in _allEvents)
|
||||||
{
|
{
|
||||||
if (_events[e].OnSiteActivity)
|
if (eventFilter(_events[e]))
|
||||||
onSiteActivity.Add(x[e, s]);
|
buffer.Add(x[e, s]);
|
||||||
}
|
}
|
||||||
|
if (buffer.Count > 0)
|
||||||
//if (_parameters.RequireOnSite)
|
constraintAction(model, buffer);
|
||||||
model.AddAtLeastOne(onSiteActivity);
|
|
||||||
onSiteActivity.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint: Individual events can only be assigned if the student ranked them.
|
||||||
|
/// </summary>
|
||||||
private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x)
|
private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
|
var prohibitVar = new List<IntVar>(1);
|
||||||
|
|
||||||
foreach (var s in _allStudents)
|
foreach (var s in _allStudents)
|
||||||
{
|
{
|
||||||
var student = _students[s];
|
var student = _students[s];
|
||||||
@@ -216,15 +281,20 @@ namespace Core.Calculation
|
|||||||
{
|
{
|
||||||
var evt = _events[e];
|
var evt = _events[e];
|
||||||
|
|
||||||
var prohibitVar = new List<IntVar> { x[e, s] };
|
if (evt.EventFormat == EventFormat.Individual
|
||||||
if (evt.EventFormat == EventFormat.Individual
|
|
||||||
&& student.EventRankings.Find(er => er.EventDefinition == evt) == null)
|
&& student.EventRankings.Find(er => er.EventDefinition == evt) == null)
|
||||||
|
{
|
||||||
|
prohibitVar.Clear();
|
||||||
|
prohibitVar.Add(x[e, s]);
|
||||||
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
|
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
|
||||||
prohibitVar.Clear();
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Constraint: Each student's total level of effort must be within bounds.
|
||||||
|
/// </summary>
|
||||||
private void SetLevelOfEffort(CpModel model, BoolVar[,] x)
|
private void SetLevelOfEffort(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
long[] eventEffortCoefficients = _events.Select(
|
long[] eventEffortCoefficients = _events.Select(
|
||||||
@@ -240,12 +310,7 @@ namespace Core.Calculation
|
|||||||
effortVar[e] = x[e, s];
|
effortVar[e] = x[e, s];
|
||||||
}
|
}
|
||||||
var student = _students[s];
|
var student = _students[s];
|
||||||
var experienceOffset = 0;
|
var experienceOffset = student.TsaYear == 1 ? 1 : 0;
|
||||||
switch (student.TsaYear)
|
|
||||||
{
|
|
||||||
case 1: experienceOffset = 1; break;
|
|
||||||
default: break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var ub = _parameters.EffortUpperBound - experienceOffset;
|
var ub = _parameters.EffortUpperBound - experienceOffset;
|
||||||
var lb = _parameters.EffortLowerBound;
|
var lb = _parameters.EffortLowerBound;
|
||||||
@@ -257,12 +322,16 @@ namespace Core.Calculation
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Limit the number of events a student is assigned
|
/// <summary>
|
||||||
|
/// Constraint: Limit the number of events a student is assigned.
|
||||||
|
/// </summary>
|
||||||
private void LimitStudentAssignment(CpModel model, BoolVar[,] x)
|
private void LimitStudentAssignment(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
|
var studentCapacity = new List<IntVar>();
|
||||||
|
|
||||||
foreach (var s in _allStudents)
|
foreach (var s in _allStudents)
|
||||||
{
|
{
|
||||||
var studentCapacity = new List<IntVar>();
|
studentCapacity.Clear();
|
||||||
foreach (var e in _allEvents)
|
foreach (var e in _allEvents)
|
||||||
{
|
{
|
||||||
studentCapacity.Add(x[e, s]);
|
studentCapacity.Add(x[e, s]);
|
||||||
@@ -270,162 +339,146 @@ namespace Core.Calculation
|
|||||||
|
|
||||||
model.AddLinearConstraint(
|
model.AddLinearConstraint(
|
||||||
LinearExpr.Sum(studentCapacity), _parameters.EventsLowerBound, _parameters.EventsUpperBound);
|
LinearExpr.Sum(studentCapacity), _parameters.EventsLowerBound, _parameters.EventsUpperBound);
|
||||||
studentCapacity.Clear();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EventHasInterestedStudent(CpModel model, BoolVar[,] x)
|
|
||||||
{
|
|
||||||
foreach (var e in _allEvents)
|
|
||||||
{
|
|
||||||
var evt = _events[e];
|
|
||||||
var studentInterest = new List<IntVar>();
|
|
||||||
|
|
||||||
foreach (var s in _allStudents)
|
|
||||||
{
|
|
||||||
var student = _students[s];
|
|
||||||
if (student.EventRankings.Find(er => er.EventDefinition == evt) != null)
|
|
||||||
studentInterest.Add(x[e, s]);
|
|
||||||
}
|
|
||||||
model.AddLinearConstraint(
|
|
||||||
LinearExpr.Sum(studentInterest), 1, 10);
|
|
||||||
studentInterest.Clear();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds event capacity constraints and calculates assignment thresholds.
|
||||||
|
/// </summary>
|
||||||
private List<EventAssignmentThresholds> AddEventAssignmentThresholds(CpModel model, BoolVar[,] x)
|
private List<EventAssignmentThresholds> AddEventAssignmentThresholds(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
var assignmentThresholdsList = new List<EventAssignmentThresholds>();
|
var assignmentThresholdsList = new List<EventAssignmentThresholds>();
|
||||||
// Limit the capacity of each event
|
|
||||||
foreach (var e in _allEvents)
|
foreach (var e in _allEvents)
|
||||||
{
|
{
|
||||||
var evt = _events[e];
|
var evt = _events[e];
|
||||||
var eventPickCounts = _eventPickCounts[e];
|
var teamCount = CalculateTeamCount(evt, e);
|
||||||
|
var (lb, ub) = CalculateEventBounds(evt, teamCount);
|
||||||
|
|
||||||
var evtMinTeamSize = evt.MinTeamSize;
|
AddEventConstraint(model, x, e, lb, ub);
|
||||||
var evtMaxTeamSize = evt.MaxTeamSize;
|
assignmentThresholdsList.Add(CreateThreshold(evt, teamCount, e));
|
||||||
|
|
||||||
var teamDivs = eventPickCounts / (evtMinTeamSize * 1.25);
|
|
||||||
if (_includedEvents.Contains(evt))
|
|
||||||
teamDivs = 1;
|
|
||||||
|
|
||||||
//var teamsCount = (int)Math.Ceiling(teamDivs);
|
|
||||||
var teamCount = (int)Math.Round(teamDivs);
|
|
||||||
if (teamCount > evt.ChapterEligibilityCountState)
|
|
||||||
teamCount = evt.ChapterEligibilityCountState;
|
|
||||||
|
|
||||||
// limit to one team for group events
|
|
||||||
if (_parameters.LimitTeamsToOne
|
|
||||||
&& evt.EventFormat is EventFormat.Team
|
|
||||||
&& teamCount > 1
|
|
||||||
&& !_twoTeams.Contains(evt)
|
|
||||||
)
|
|
||||||
teamCount = 1;
|
|
||||||
|
|
||||||
if (_twoTeams.Contains(evt))
|
|
||||||
teamCount = 2;
|
|
||||||
|
|
||||||
if (evt.Name == "Tech Bowl")
|
|
||||||
teamCount = 1;
|
|
||||||
|
|
||||||
var eventCapacity = new List<IntVar>();
|
|
||||||
foreach (var s in _allStudents)
|
|
||||||
{
|
|
||||||
eventCapacity.Add(x[e, s]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_droppedEvents != null && _droppedEvents.Contains(evt))
|
|
||||||
teamCount = 0;
|
|
||||||
|
|
||||||
if (evt.EventFormat == EventFormat.Individual)
|
|
||||||
evtMinTeamSize = 0;
|
|
||||||
|
|
||||||
var lb = evtMinTeamSize * teamCount;
|
|
||||||
var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit * teamCount);
|
|
||||||
|
|
||||||
assignmentThresholdsList.Add(
|
|
||||||
new EventAssignmentThresholds
|
|
||||||
{
|
|
||||||
Event = evt,
|
|
||||||
TeamCount = teamCount,
|
|
||||||
LowerBound = evtMinTeamSize,
|
|
||||||
UpperBound = evtMaxTeamSize,
|
|
||||||
StudentRankingCount = eventPickCounts
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub);
|
|
||||||
|
|
||||||
Debug.WriteLine($"{evt.Name,30}\t{evt.EventFormat,-10}\t{lb} - {ub}");
|
Debug.WriteLine($"{evt.Name,30}\t{evt.EventFormat,-10}\t{lb} - {ub}");
|
||||||
|
|
||||||
model.Minimize(LinearExpr.Sum(eventCapacity));
|
|
||||||
eventCapacity.Clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return assignmentThresholdsList;
|
return assignmentThresholdsList;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates how many teams should be formed for an event.
|
||||||
|
/// </summary>
|
||||||
|
private int CalculateTeamCount(EventDefinition evt, int eventIndex)
|
||||||
|
{
|
||||||
|
var eventPickCounts = _eventPickCounts[eventIndex];
|
||||||
|
var teamDivs = eventPickCounts / (evt.MinTeamSize * TEAM_DIVISION_MULTIPLIER);
|
||||||
|
|
||||||
|
if (_includedEvents.Contains(evt))
|
||||||
|
return 1;
|
||||||
|
|
||||||
|
var teamCount = (int)Math.Round(teamDivs);
|
||||||
|
if (teamCount > evt.ChapterEligibilityCountState)
|
||||||
|
teamCount = evt.ChapterEligibilityCountState;
|
||||||
|
|
||||||
|
// Limit to one team for group events
|
||||||
|
if (_parameters.LimitTeamsToOne
|
||||||
|
&& evt.EventFormat is EventFormat.Team
|
||||||
|
&& teamCount > 1
|
||||||
|
&& !_twoTeams.Contains(evt))
|
||||||
|
teamCount = 1;
|
||||||
|
|
||||||
|
if (_twoTeams.Contains(evt))
|
||||||
|
teamCount = 2;
|
||||||
|
|
||||||
|
if (_droppedEvents.Contains(evt))
|
||||||
|
teamCount = 0;
|
||||||
|
|
||||||
|
return teamCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates the lower and upper bounds for event capacity.
|
||||||
|
/// </summary>
|
||||||
|
private (int lb, int ub) CalculateEventBounds(EventDefinition evt, int teamCount)
|
||||||
|
{
|
||||||
|
var evtMinTeamSize = evt.MinTeamSize;
|
||||||
|
var evtMaxTeamSize = evt.MaxTeamSize;
|
||||||
|
|
||||||
|
if (evt.EventFormat == EventFormat.Individual)
|
||||||
|
evtMinTeamSize = 0;
|
||||||
|
|
||||||
|
var lb = evtMinTeamSize * teamCount;
|
||||||
|
var ub = Math.Min(evtMaxTeamSize * teamCount, _parameters.TeamSizeLimit * teamCount);
|
||||||
|
|
||||||
|
return (lb, ub);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds capacity constraint for a specific event.
|
||||||
|
/// </summary>
|
||||||
|
private void AddEventConstraint(CpModel model, BoolVar[,] x, int eventIndex, int lb, int ub)
|
||||||
|
{
|
||||||
|
var eventCapacity = new List<IntVar>();
|
||||||
|
foreach (var s in _allStudents)
|
||||||
|
{
|
||||||
|
eventCapacity.Add(x[eventIndex, s]);
|
||||||
|
}
|
||||||
|
|
||||||
|
model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub);
|
||||||
|
model.Minimize(LinearExpr.Sum(eventCapacity));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Creates a threshold object for tracking event assignment metadata.
|
||||||
|
/// </summary>
|
||||||
|
private EventAssignmentThresholds CreateThreshold(EventDefinition evt, int teamCount, int eventIndex)
|
||||||
|
{
|
||||||
|
return new EventAssignmentThresholds
|
||||||
|
{
|
||||||
|
Event = evt,
|
||||||
|
TeamCount = teamCount,
|
||||||
|
LowerBound = evt.MinTeamSize,
|
||||||
|
UpperBound = evt.MaxTeamSize,
|
||||||
|
StudentRankingCount = _eventPickCounts[eventIndex]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds user-specified assignment requirements (include/exclude student-event pairs).
|
||||||
|
/// </summary>
|
||||||
private void AddAssignmentRequirements(CpModel model, BoolVar[,] x)
|
private void AddAssignmentRequirements(CpModel model, BoolVar[,] x)
|
||||||
{
|
{
|
||||||
foreach (var includedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Include))
|
foreach (var includedAssignment in _assignmentRequirements.Where(a => a.Requirement == Requirement.Include))
|
||||||
{
|
{
|
||||||
var e = _events.IndexOf(includedAssignment.EventDefinition);
|
var e = _events.IndexOf(includedAssignment.EventDefinition);
|
||||||
var s = _students.IndexOf(includedAssignment.Student);
|
var s = _students.IndexOf(includedAssignment.Student);
|
||||||
model.AddAssumption(x[e, s]);
|
model.AddAssumption(x[e, s]);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var excludedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Exclude))
|
var prohibitVar = new List<IntVar>(1);
|
||||||
|
foreach (var excludedAssignment in _assignmentRequirements.Where(a => a.Requirement == Requirement.Exclude))
|
||||||
{
|
{
|
||||||
var e = _events.IndexOf(excludedAssignment.EventDefinition);
|
var e = _events.IndexOf(excludedAssignment.EventDefinition);
|
||||||
var s = _students.IndexOf(excludedAssignment.Student);
|
var s = _students.IndexOf(excludedAssignment.Student);
|
||||||
|
|
||||||
var prohibitVar = new List<IntVar> { x[e, s] };
|
|
||||||
|
|
||||||
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
|
|
||||||
prohibitVar.Clear();
|
prohibitVar.Clear();
|
||||||
|
prohibitVar.Add(x[e, s]);
|
||||||
|
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Calculates coefficients for optimizing student preferences.
|
||||||
|
/// Higher-ranked events get higher coefficients.
|
||||||
|
/// </summary>
|
||||||
private static long[] GetEventPickCoefficients(IList<EventDefinition> events, List<StudentEventRanking> eventRankings)
|
private static long[] GetEventPickCoefficients(IList<EventDefinition> events, List<StudentEventRanking> eventRankings)
|
||||||
{
|
{
|
||||||
return
|
return events.Select(e =>
|
||||||
events.Select(e =>
|
|
||||||
{
|
{
|
||||||
var eventRank = eventRankings.FirstOrDefault(er => er.EventDefinition == e);
|
var eventRank = eventRankings.FirstOrDefault(er => er.EventDefinition == e);
|
||||||
return
|
return eventRank == null
|
||||||
eventRank == null
|
? 0L
|
||||||
? 0L
|
: StudentEventRanking.MaxRank - eventRank.Rank; // Inverse ranking
|
||||||
// TODO: MaxRank can be calculated
|
|
||||||
: StudentEventRanking.MaxRank - eventRank.Rank; // inverse
|
|
||||||
}).ToArray();
|
}).ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public class SolutionPrinter : CpSolverSolutionCallback
|
|
||||||
{
|
|
||||||
private readonly IList<EventDefinition> _events;
|
|
||||||
private readonly IList<Student> _students;
|
|
||||||
private readonly BoolVar[,] _eventAssignment;
|
|
||||||
|
|
||||||
public SolutionPrinter(IList<EventDefinition> events, IList<Student> students, BoolVar[,] eventAssignment)
|
|
||||||
{
|
|
||||||
_events = events;
|
|
||||||
_students = students;
|
|
||||||
_eventAssignment = eventAssignment;
|
|
||||||
}
|
|
||||||
public override void OnSolutionCallback()
|
|
||||||
{
|
|
||||||
Console.WriteLine($"Solution ");
|
|
||||||
foreach (var evt in Enumerable.Range(0, _events.Count))
|
|
||||||
{
|
|
||||||
foreach (var student in Enumerable.Range(0, _students.Count))
|
|
||||||
{
|
|
||||||
if (Value(_eventAssignment[evt, student]) == 1L)
|
|
||||||
{
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user