using System.Diagnostics; using Core.Entities; using Google.OrTools.Sat; using IntVar = Google.OrTools.Sat.IntVar; namespace Core.Calculation { public class EventAssigner { 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 event? private readonly int[] _eventPickCounts; private IList _preAssigned = new List(); private IList _prohibited = new List(); private IList _droppedEvents; private IList _includedEvents; public EventAssigner(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]; _preAssigned = new List(); _droppedEvents = new List(); _includedEvents = new List(); for (var i = 0; i < _events.Count; i++) { var e = _events[i]; _eventPickCounts[i] = _students.Count(s => s.RankedEventPicks.Contains(e)); } } public void AssignToEvent(EventAssignment preAssigned) { _preAssigned.Add(preAssigned); } public void ExcludeFromEvent(EventAssignment prohibited) { _prohibited.Add(prohibited); } public void RemoveEvent(IList events) { _droppedEvents = events; } public void IncludedEvents(IList events) { _includedEvents = events; } 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) { } } } } } public Team[] Solve() { // 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}]"); foreach (var preAssignment in _preAssigned) { var e = _events.IndexOf(preAssignment.Event); var s = _students.IndexOf(preAssignment.Student); model.AddAssumption(x[e, s]); } foreach (var prohibit in _prohibited) { var e = _events.IndexOf(prohibit.Event); var s = _students.IndexOf(prohibit.Student); var prohibitVar = new List { x[e, s] }; model.AddLinearConstraint(LinearExpr.Sum(prohibitVar), 0, 0); prohibitVar.Clear(); } // 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.0); 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.Format is EventFormat.Team && teamCount > 1) 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); model.AddLinearConstraint(LinearExpr.Sum(eventCapacity), lb, ub); Debug.WriteLine($"{evt.Name,30}\t{evt.Format,-10}\t{lb} - {ub}"); model.Minimize(LinearExpr.Sum(eventCapacity)); eventCapacity.Clear(); } // Limit the number of events a student is assigned foreach (var s in _allStudents) { var student = _students[s]; var studentCapacity = new List(); foreach (var e in _allEvents) { studentCapacity.Add(x[e, s]); } model.AddLinearConstraint(LinearExpr.Sum(studentCapacity), _parameters.AssignmentLowerBound, _parameters.AssignmentUpperBound); studentCapacity.Clear(); } var eventEffortCoefficients = GetEventEffortCoefficients(_events); 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); } // each student should be assigned at least one on site activity event 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(); } // students should have at maximum one individual event foreach (var s in _allStudents) { var individualEvent = new List(); foreach (var e in _allEvents) { if (_events[e].Format == EventFormat.Individual) individualEvent.Add(x[e, s]); } model.AddAtMostOne(individualEvent); individualEvent.Clear(); } // students should have at maximum regional events 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(); } var maximizePicks = LinearExpr.NewBuilder(); // optimize student selections foreach (var s in _allStudents) { var eventPickCoefficients = GetEventPickCoefficients(_students[s].RankedEventPicks, _events); foreach (var e in _allEvents) { maximizePicks.AddTerm(x[e, s], eventPickCoefficients[e]); } } model.Maximize(maximizePicks); var solver = new CpSolver(); var cpSolverStatus = solver.Solve(model); // print solver status Console.WriteLine($"Solver status: {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]); //Console.WriteLine($"{_events[evt].Name} : {_students[s].Name}"); } } if (students.Count > 0) eventAssignmentsList.Add(new Team(_events[e].Name, _events[e], students)); } } else { //Console.WriteLine("No solution found."); } return eventAssignmentsList.ToArray(); } private long[] GetEventPickCoefficients(IList rankedEvents, IEnumerable events) { var eventPickCount = rankedEvents.Count; return events.Select(e => { var eventPickIndex = rankedEvents.IndexOf(e); return eventPickIndex switch { 0 => eventPickCount + 1, 1 => eventPickCount + 0, > 1 => eventPickCount , _ => 0L }; }).ToArray(); } private long[] GetEventEffortCoefficients(IEnumerable events) { return events.Select(e => e.Name == "Tech Bowl" ? 0 : e.LevelOfEffort.HasValue ? e.LevelOfEffort.Value : 10L).ToArray(); } } }