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 var eventEffortCoefficients = GetEventEffortCoefficients(_events); SetLevelOfEffort(model, x, eventEffortCoefficients); // each student should be assigned at least one on site activity eventDefinition if (_parameters.RequireOnSite) RequireOnSiteActivity(model, x); // students should have at maximum one individual eventDefinition LimitIndividualEvent(model, x); // students should have at least one regional event if (_parameters.RequireRegional) RequireRegionalEvent(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; } private List GetEventAssignments(BoolVar[,] x, CpSolver solver, CpSolverStatus cpSolverStatus) { var eventAssignmentsList = new List(); if (cpSolverStatus == CpSolverStatus.Optimal || cpSolverStatus == CpSolverStatus.Feasible) { foreach (var e in _allEvents) { var students = new List(); foreach (var s in _allStudents) { if (solver.BooleanValue(x[e, s])) { students.Add(_students[s]); } } if (students.Count > 0) eventAssignmentsList.Add(new Team { TeamId = _events[e].Name, Event = _events[e], Students = students}); } } return eventAssignmentsList; } private void OptimizeStudentEventRankings(CpModel model, BoolVar[,] x) { var maximizePicks = LinearExpr.NewBuilder(); // optimize student event rankings 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]); } //if (_parameters.RequireRegional) model.AddLinearConstraint(LinearExpr.Sum(regionalEvent), 1, 2); regionalEvent.Clear(); } } private void LimitIndividualEvent(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 SetLevelOfEffort(CpModel model, BoolVar[,] x, long[] eventEffortCoefficients) { 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); } } private void LimitStudentAssignment(CpModel model, BoolVar[,] x) { // Limit the number of events a student is assigned 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 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.MaxTeamCountState) teamCount = evt.MaxTeamCountState; // limit to one team for group events if (_parameters.LimitTeamsToOne && evt.EventFormat is EventFormat.Team && teamCount > 1 && !_twoTeams.Contains(evt) ) teamCount = 1; 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; 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 : StudentEventRanking.MaxRank - eventRank.Rank; // inverse }).ToArray(); } private long[] GetEventEffortCoefficients(IEnumerable events) { return events.Select( e => //e.Name == "Tech Bowl" // ? 0 // Tech bowl gets requires no effort? //: e.LevelOfEffort ?? 10L ).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) { } } } } } } }