315 lines
8.5 KiB
C#
315 lines
8.5 KiB
C#
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<CompetitiveEvent> _events;
|
|
private readonly IList<Student> _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<EventAssignment> _preAssigned = new List<EventAssignment>();
|
|
private IList<EventAssignment> _prohibited = new List<EventAssignment>();
|
|
private IList<CompetitiveEvent> _droppedEvents;
|
|
private IList<CompetitiveEvent> _includedEvents;
|
|
|
|
public EventAssigner(IList<CompetitiveEvent> 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];
|
|
_preAssigned = new List<EventAssignment>();
|
|
_droppedEvents = new List<CompetitiveEvent>();
|
|
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<CompetitiveEvent> events)
|
|
{
|
|
_droppedEvents = events;
|
|
}
|
|
public void IncludedEvents(IList<CompetitiveEvent> events)
|
|
{
|
|
_includedEvents = events;
|
|
}
|
|
|
|
public class SolutionPrinter : CpSolverSolutionCallback
|
|
{
|
|
private readonly IList<CompetitiveEvent> _events;
|
|
private readonly IList<Student> _students;
|
|
private readonly BoolVar[,] _eventAssignment;
|
|
|
|
public SolutionPrinter(IList<CompetitiveEvent> 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)
|
|
{
|
|
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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<IntVar> { 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<IntVar>();
|
|
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<IntVar>();
|
|
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<ILiteral>();
|
|
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<ILiteral>();
|
|
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<ILiteral>();
|
|
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<Team>();
|
|
|
|
if (cpSolverStatus == CpSolverStatus.Optimal || cpSolverStatus == CpSolverStatus.Feasible)
|
|
{
|
|
foreach (var e in _allEvents)
|
|
{
|
|
var students = new List<Student>();
|
|
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<CompetitiveEvent> rankedEvents, IEnumerable<CompetitiveEvent> 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<CompetitiveEvent> events)
|
|
{
|
|
return
|
|
events.Select(e => e.Name == "Tech Bowl" ? 0 : e.LevelOfEffort.HasValue ? e.LevelOfEffort.Value : 10L).ToArray();
|
|
}
|
|
}
|
|
} |