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 _events; private readonly IList _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 _assignmentRequirements = new List(); private IList _droppedEvents = new List(); private IList _includedEvents = new List(); private IList _twoTeams = new List(); public EventAssignment(IList events, IList 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 events) { _droppedEvents = events; } public void IncludedEvents(IList events) { _includedEvents = events; } public void AllowTwoTeams(IList events) { _twoTeams = events; } public async Task 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 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(); 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(); 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(); 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 { 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(); 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(); 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 AddEventAssignmentThresholds(CpModel model, BoolVar[,] x) { var assignmentThresholdsList = new List(); // 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(); 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 { x[e, s] }; model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0); prohibitVar.Clear(); } } private static long[] GetEventPickCoefficients(IList events, List 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 _events; private readonly IList _students; private readonly BoolVar[,] _eventAssignment; public SolutionPrinter(IList events, IList 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) { } } } } } } }