Files
chapter-organizer/Core/Calculation/EventAssignment.cs
T
poprhythm 6407dfca71 Remove Team.Number
Add Team.Identifier
2025-10-03 12:47:05 -04:00

431 lines
15 KiB
C#

using System.Diagnostics;
using System.Security.Cryptography.X509Certificates;
using Core.Entities;
using Google.OrTools.Sat;
using Microsoft.EntityFrameworkCore.Metadata;
using IntVar = Google.OrTools.Sat.IntVar;
namespace Core.Calculation
{
public class EventAssignment
{
private readonly IList<EventDefinition> _events;
private readonly IList<Student> _students;
private readonly AssignmentParameters _parameters;
private readonly int[] _allEvents;
private readonly int[] _allStudents;
// how many students have picked each eventDefinition?
private readonly int[] _eventPickCounts;
private IList<AssignmentRequirement> _assignmentRequirements = new List<AssignmentRequirement>();
private IList<EventDefinition> _droppedEvents = new List<EventDefinition>();
private IList<EventDefinition> _includedEvents = new List<EventDefinition>();
private IList<EventDefinition> _twoTeams = new List<EventDefinition>();
public EventAssignment(IList<EventDefinition> events, IList<Student> students, AssignmentParameters parameters)
{
_events = events;
_students = students;
_parameters = parameters;
_allEvents = Enumerable.Range(0, _events.Count).ToArray();
_allStudents = Enumerable.Range(0, _students.Count).ToArray();
_eventPickCounts = new int[_allEvents.Length];
for (var i = 0; i < _events.Count; i++)
{
var e = _events[i];
_eventPickCounts[i] = _students.Count(s => s.EventRankings.Count(er => er.EventDefinition == e) > 0);
}
}
public void AddAssignmentRequirement(AssignmentRequirement assignmentRequirement)
{
_assignmentRequirements.Add(assignmentRequirement);
}
public void RemoveEvents(IList<EventDefinition> events)
{
_droppedEvents = events;
}
public void IncludedEvents(IList<EventDefinition> events)
{
_includedEvents = events;
}
public void AllowTwoTeams(IList<EventDefinition> events)
{
_twoTeams = events;
}
public async Task<EventAssignmentSolution> Solve()
{
Debug.WriteLine(_parameters);
// Model.
var model = new CpModel();
// Variables.
var x = new BoolVar[_allEvents.Length, _allStudents.Length];
foreach (var e in _allEvents)
foreach (var s in _allStudents)
x[e, s] = model.NewBoolVar($"eventAssignments[{e},{s}]");
AddAssignmentRequirements(model, x);
var assignmentThresholdsList = AddEventAssignmentThresholds(model, x);
LimitStudentAssignment(model, x);
// set the range for level of effort
SetLevelOfEffort(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
AtMostOneIndividualEvent(model, x);
//EventHasInterestedStudent(model, x);
IndividualEventsMustBeRanked(model, x);
OptimizeStudentEventRankings(model, x);
Debug.WriteLine("Starting optimization");
var solver = new CpSolver();
var cpSolverStatus = await Task.Run(() => solver.Solve(model));
// print solver status
Debug.WriteLine($"Solver status: {cpSolverStatus}");
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
private List<Team> GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus)
{
if (cpSolverStatus is not (CpSolverStatus.Optimal or CpSolverStatus.Feasible))
return [];
var eventAssignments =
from e in _allEvents
let students =
from s in _allStudents
where solver.BooleanValue(x[e, s])
select _students[s]
where students.Any()
select new Team
{
Identifier = _events[e].Name,
Event = _events[e],
Students = students.ToList()
};
return eventAssignments.ToList();
}
// Maximize student event rankings
private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x)
{
var maximizePicks = LinearExpr.NewBuilder();
foreach (var s in _allStudents)
{
var eventPickCoefficients = GetEventPickCoefficients(_events, _students[s].EventRankings);
foreach (var e in _allEvents)
{
maximizePicks.AddTerm(x[e, s], eventPickCoefficients[e]);
}
}
model.Maximize(maximizePicks);
}
private void RequireRegionalEvent(CpModel model, BoolVar[,] x)
{
foreach (var s in _allStudents)
{
var regionalEvent = new List<ILiteral>();
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();
}
}
private void AtMostOneIndividualEvent(CpModel model, BoolVar[,] x)
{
foreach (var s in _allStudents)
{
var individualEvent = new List<ILiteral>();
foreach (var e in _allEvents)
{
if (_events[e].EventFormat == EventFormat.Individual)
individualEvent.Add(x[e, s]);
}
model.AddAtMostOne(individualEvent);
individualEvent.Clear();
}
}
private void RequireOnSiteActivity(CpModel model, BoolVar[,] x)
{
foreach (var s in _allStudents)
{
var onSiteActivity = new List<ILiteral>();
foreach (var e in _allEvents)
{
if (_events[e].OnSiteActivity)
onSiteActivity.Add(x[e, s]);
}
//if (_parameters.RequireOnSite)
model.AddAtLeastOne(onSiteActivity);
onSiteActivity.Clear();
}
}
private void IndividualEventsMustBeRanked(CpModel model, BoolVar[,] x)
{
foreach (var s in _allStudents)
{
var student = _students[s];
foreach (var e in _allEvents)
{
var evt = _events[e];
var prohibitVar = new List<IntVar> { x[e, s] };
if (evt.EventFormat == EventFormat.Individual
&& student.EventRankings.Find(er => er.EventDefinition == evt) == null)
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
prohibitVar.Clear();
}
}
}
private void SetLevelOfEffort(CpModel model, BoolVar[,] x)
{
long[] eventEffortCoefficients = _events.Select(
e => e.LevelOfEffort ?? 1L
).ToArray();
foreach (var s in _allStudents)
{
var effortVar = new BoolVar[_allEvents.Length];
foreach (var e in _allEvents)
{
effortVar[e] = x[e, s];
}
var student = _students[s];
var experienceOffset = 0;
switch (student.TsaYear)
{
case 1: experienceOffset = 1; break;
default: break;
}
var ub = _parameters.EffortUpperBound - experienceOffset;
var lb = _parameters.EffortLowerBound;
if (ub <= lb)
lb = ub - 1;
model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) >= lb);
model.Add(LinearExpr.WeightedSum(effortVar, eventEffortCoefficients) <= ub);
}
}
// Limit the number of events a student is assigned
private void LimitStudentAssignment(CpModel model, BoolVar[,] x)
{
foreach (var s in _allStudents)
{
var studentCapacity = new List<IntVar>();
foreach (var e in _allEvents)
{
studentCapacity.Add(x[e, s]);
}
model.AddLinearConstraint(
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();
}
}
private List<EventAssignmentThresholds> AddEventAssignmentThresholds(CpModel model, BoolVar[,] x)
{
var assignmentThresholdsList = new List<EventAssignmentThresholds>();
// Limit the capacity of each event
foreach (var e in _allEvents)
{
var evt = _events[e];
var eventPickCounts = _eventPickCounts[e];
var evtMinTeamSize = evt.MinTeamSize;
var evtMaxTeamSize = evt.MaxTeamSize;
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}");
model.Minimize(LinearExpr.Sum(eventCapacity));
eventCapacity.Clear();
}
return assignmentThresholdsList;
}
private void AddAssignmentRequirements(CpModel model, BoolVar[,] x)
{
foreach (var includedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Include))
{
var e = _events.IndexOf(includedAssignment.EventDefinition);
var s = _students.IndexOf(includedAssignment.Student);
model.AddAssumption(x[e, s]);
}
foreach (var excludedAssignment in _assignmentRequirements.Where(e => e.Requirement == Requirement.Exclude))
{
var e = _events.IndexOf(excludedAssignment.EventDefinition);
var s = _students.IndexOf(excludedAssignment.Student);
var prohibitVar = new List<IntVar> { x[e, s] };
model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0);
prohibitVar.Clear();
}
}
private static long[] GetEventPickCoefficients(IList<EventDefinition> events, List<StudentEventRanking> eventRankings)
{
return
events.Select(e =>
{
var eventRank = eventRankings.FirstOrDefault(er => er.EventDefinition == e);
return
eventRank == null
? 0L
// TODO: MaxRank can be calculated
: StudentEventRanking.MaxRank - eventRank.Rank; // inverse
}).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)
{
}
}
}
}
}
}
}