void Optimize() { if (MyEngine.MyPriority == Priority.FirstAvailable) { MyResults = MyEngine.OptimizeGreedy(MyBuilder.IsRoomUnavailable, MyBuilder.IsInstructorUnavailable, MyBuilder.CurrentlyReleased, MyBuilder.LocallyTaughtCoursesPerDay); } else if (MyEngine.MyPriority == Priority.MaximizeInstructorLongestToTeach) { MyEngine.OptimizeLongestToTeach(MyBuilder.StartingResults, 0, MyBuilder.IsInstructorUnavailable, MyBuilder.IsRoomUnavailable, MyBuilder.CurrentlyReleased, MyBuilder.LocallyTaughtCoursesPerDay, 0, MyEngine.CourseCatalog); MyResults = MyEngine.CurrentBestAnswer; } else { MyEngine.OptimizeRecursion(MyBuilder.StartingResults, 0, MyBuilder.IsInstructorUnavailable, MyBuilder.IsRoomUnavailable, MyBuilder.CurrentlyReleased, MyBuilder.LocallyTaughtCoursesPerDay, 0); MyResults = MyEngine.CurrentBestAnswer; } MyResults.Inputs.AddRange(MyEngine.WillAlwaysFail); ThreadInProgress = false; return; }
/// <summary> /// Populates the properties of the optimizer object by pulling in data from the database context /// </summary> /// <param name="showSetup">Will write to console debug images to display the data that is pulled in</param> /// <param name="StartDate">The beginning of the optimization range</param> /// <param name="EndDate">The end of the optimizaiton range</param> internal OptimizerEngine Build(TimeSpan timeLimit) { if (ShowDebugMessages) { Console.WriteLine("Pulling in data from the database...\n"); } Engine.ShowDebugMessages = ShowDebugMessages; Engine.StartDate = StartDate; Engine.EndDate = EndDate; Engine.context = context; Engine.timeLimit = timeLimit; // Pull in instructors Engine.Instructors = context.User.Where(user => user.RoleID == 3).ToList(); // Course Catalog Engine.CourseCatalog = context.Course.ToList(); // Add to each course the instructors qualified to teach them context.InstructorStatus. Where(status => status.Deleted == false && status.Qualification == 1 && Engine.Instructors.Any(instr => instr.Username == status.InstructorID)).ToList(). ForEach(status => Engine.CourseCatalog. Where(course => course.ID == status.CourseID) .First().QualifiedInstructors.Add(status.InstructorID, DateTime.MinValue)); // Add to each instructor the amount of courses they can teach context.InstructorStatus. Where(status => status.Deleted == false && status.Qualification == 1 && Engine.Instructors.Any(y => y.Username == status.InstructorID)).ToList(). ForEach(status => Engine.Instructors.Where(x => x.Username == status.InstructorID).FirstOrDefault().QualificationCount++); // Add to each course the required resources it needs from a room context.CourseRequiredResources.ToList(). ForEach(required => Engine.CourseCatalog.Where(course => course.ID == required.CourseID). FirstOrDefault().RequiredResources[required.ResourceID] = required.Amount); // Rooms Engine.Rooms = context.Room.ToDictionary(room => room.ID, room => room); // Add to each room the resources it has context.RoomHasResources.ToList().ForEach(resources => Engine.Rooms. Where(room => room.Value.ID == resources.RoomID).FirstOrDefault().Value. Resources_dict[resources.ResourceID] = resources.Amount); // Get the optimizer input data Engine.Inputs = (from input in context.OptimizerInput join course in context.Course on input.CourseCode equals course.Code join location in context.Location on input.LocationID equals location.Code where input.Succeeded == false && input.Selected == true orderby course.Hours descending select new OptimizerInput() { Id = input.Id, CourseCode = input.CourseCode, LocationID = input.LocationID, NumTimesToRun = input.NumTimesToRun, StartTime = input.StartTime, CourseTitle = input.CourseTitle, Succeeded = input.Succeeded, Reason = "", LengthDays = Math.Max(course.Hours / 8, 1), CourseId = (int)course.ID, LocationIdLiteral = location.ID, RemainingRuns = input.NumTimesToRun, Selected = input.Selected }).ToList <OptimizerInput>(); OriginalInputCount = Engine.Inputs.Count; // Scheduled/Planned classes // Only grab classes within the optimzer time range and that aren't cancelled Engine.CurrentSchedule = context.ScheduledClass .Where(c => c.Cancelled == false && ( (DateTime.Compare(c.StartDate, Engine.StartDate) >= 0 && DateTime.Compare(c.StartDate, Engine.EndDate) <= 0) || (DateTime.Compare(c.EndDate, Engine.StartDate) >= 0 && DateTime.Compare(c.EndDate, Engine.EndDate) <= 0) || (DateTime.Compare(c.StartDate, Engine.StartDate) < 0 && DateTime.Compare(c.EndDate, Engine.EndDate) > 0) )).ToList(); // Instructor assignments Engine.InstructorAssignments = context.InstructorOfClass .Where(i => i.Cancelled == false && ( (DateTime.Compare(i.StartDate, Engine.StartDate) >= 0 && DateTime.Compare(i.StartDate, Engine.EndDate) <= 0) || (DateTime.Compare(i.EndDate, Engine.StartDate) >= 0 && DateTime.Compare(i.EndDate, Engine.EndDate) <= 0) || (DateTime.Compare(i.StartDate, Engine.StartDate) < 0 && DateTime.Compare(i.EndDate, Engine.EndDate) > 0) )).ToList(); // Remove instructor assignments if the instructor is not in the user table Engine.InstructorAssignments.RemoveAll(assignment => !Engine.Instructors.Any(instructor => instructor.Username == assignment.UserID)); // Remove all assignments if its specified course isn't in the catalog Engine.InstructorAssignments.RemoveAll(assignment => !Engine.CourseCatalog.Any(course => { if (Engine.CurrentSchedule.Any(schedule => schedule.ID == assignment.ClassID)) { return(Engine.CurrentSchedule.First(schedule => schedule.ID == assignment.ClassID).CourseID == course.ID); } else { return(false); } })); // Update instructor assignments by marking them if they are local assignments Engine.InstructorAssignments. ForEach(Assignment => Assignment.LocalAssignment = ( Engine.Instructors.Where(instructor => instructor.Username == Assignment.UserID).First().PointID == Engine.CurrentSchedule.Where(Class => Assignment.ClassID == Class.ID).First().LocationID)); // Exceptions Engine.Exceptions = context.Booking .Where(b => b.Status == 1 && b.Cancelled == false && ( (DateTime.Compare(b.StartDate, Engine.StartDate) >= 0 && DateTime.Compare(b.StartDate, Engine.EndDate) <= 0) || (DateTime.Compare(b.EndDate, Engine.StartDate) >= 0 && DateTime.Compare(b.EndDate, Engine.EndDate) <= 0) || (DateTime.Compare(b.StartDate, Engine.StartDate) < 0 && DateTime.Compare(b.EndDate, Engine.EndDate) > 0)) ).ToList(); // Locations Engine.Locations = context.Location.ToList(); // Populate the local rooms for each location context.Room.ToList().ForEach(room => Engine.Locations.Find(location => location.Code.Equals(room.Station)).LocalRooms.Add(room.ID)); // Populate the local instructs for each location Engine.Instructors.ForEach(instructor => Engine.Locations.First(location => location.ID == instructor.PointID).LocalInstructors.Add(instructor.Username)); if (ShowDebugMessages) { Console.WriteLine("Optimizer Inputs"); ConsoleTable.From <OptimizerInputPrintable>(Engine.Inputs.Select(x => new OptimizerInputPrintable(x))).Write(Format.MarkDown); Console.WriteLine(""); // Assign to each scheduled class its instructor, for printing purposes Engine.InstructorAssignments.ForEach(assignment => Engine.CurrentSchedule.First(classEvent => classEvent.ID == assignment.ClassID) .Instructor = Engine.Instructors.First(instructor => instructor.Username == assignment.UserID)); Engine.CurrentSchedule.ForEach(classEvent => classEvent.CourseCode = Engine.CourseCatalog.First(course => course.ID == classEvent.CourseID).Code); Engine.CurrentSchedule.ForEach(classEvent => classEvent.Location = Engine.Locations.First(loc => loc.ID == classEvent.LocationID).Code); if (Engine.CurrentSchedule.Count > 0) { Console.WriteLine("Current Schedule"); try { ConsoleTable.From <ScheduledClassPrintable>(Engine.CurrentSchedule.Select(c => new ScheduledClassPrintable(c))).Write(Format.MarkDown); } catch { } Console.WriteLine(""); } if (Engine.InstructorAssignments.Count > 0) { Console.WriteLine("Instructor Assignments"); try { ConsoleTable.From <InstructorOfClass>(Engine.InstructorAssignments).Write(Format.MarkDown); } catch { } Console.WriteLine(""); } if (Engine.Exceptions.Count > 0) { Console.WriteLine("Bookings"); try { ConsoleTable.From <BookingPrintable>(Engine.Exceptions.Select(e => new BookingPrintable(e))).Write(Format.MarkDown); } catch { } Console.WriteLine(""); } Console.WriteLine("Locations"); ConsoleTable.From(Engine.Locations).Write(Format.MarkDown); Console.WriteLine(""); Console.WriteLine("Locations' Local Rooms"); OptimizerUtilities.PrintRoomTable(Engine.Locations); Console.WriteLine(""); Console.WriteLine("Locations' Local Instructors"); OptimizerUtilities.PrintInstructorTable(Engine.Locations); Console.WriteLine(""); } // Store all the week days of the range into a list var WeekDaysInRange = new List <string>(); foreach (DateTime day in OptimizerUtilities.EachWeekDay(Engine.StartDate, Engine.EndDate)) { WeekDaysInRange.Add(day.ToString(TIME_FORMAT)); } if (ShowDebugMessages) { Console.WriteLine("Count of week days in range: " + WeekDaysInRange.Count()); } Engine.TotalWeekDays = WeekDaysInRange.Count; // Link each location to its locally taught courses per day foreach (var loc in Engine.Locations) { LocallyTaughtCoursesPerDay.Add(loc.ID, WeekDaysInRange.ToDictionary(x => x, x => new List <int>())); } // Fill the 2d dictionary IsRoomUnavailable // First pair: value is the room id to a second dictionary // Second pair: string of the date to the unavailability of the room context.Room.ToList().ForEach(room => IsRoomUnavailable.Add(room.ID, WeekDaysInRange.ToDictionary(x => x, x => false))); // Fill the 2d dictionary IsInstructorUnavailable // First pair: value is the instructor username to a second dictionary // Second pair: string of the date to the unavailability of the instructor Engine.Instructors.ForEach(instructor => IsInstructorUnavailable.Add(instructor.Username, WeekDaysInRange.ToDictionary(x => x, x => false))); // Fill the 2d dictionary CurrentlyReleased // First pair: value is the location id to a second dictionary // Second pair: string of the date to the unavailability of the instructor Engine.Locations.ForEach(loc => CurrentlyReleased.Add(loc.ID, WeekDaysInRange.ToDictionary(x => x, x => 0))); // Loop through the current schedule and populate the room availbility and the currently release dictionaries foreach (var clas in Engine.CurrentSchedule) { foreach (var date in OptimizerUtilities.EachWeekDay(OptimizerUtilities.Max(clas.StartDate, Engine.StartDate), OptimizerUtilities.Min(clas.EndDate, Engine.EndDate))) { IsRoomUnavailable[clas.RoomID][date.ToString(TIME_FORMAT)] = true; CurrentlyReleased[clas.LocationID][date.ToString(TIME_FORMAT)] += Engine.CourseCatalog.Find(course => course.ID == clas.CourseID).MaxSize; LocallyTaughtCoursesPerDay[clas.LocationID][date.ToString(TIME_FORMAT)].Add(clas.CourseID); } } if (ShowDebugMessages) { List <string> Headers; ConsoleTable table; foreach (var sublist in OptimizerUtilities.Split(WeekDaysInRange, 20)) { Headers = new List <string> { "RoomID" }; sublist.ForEach(day => Headers.Add(day.Substring(0, day.Length - 5))); table = new ConsoleTable(Headers.ToArray()); foreach (var RoomPair in IsRoomUnavailable) { var row = new List <string> { RoomPair.Key.ToString() }; RoomPair.Value.ToList().ForEach(pair => { if (sublist.Contains(pair.Key)) { row.Add((pair.Value ? "Yes" : "")); } }); table.AddRow(row.ToArray()); } Console.WriteLine("IsRoomUnavailable"); table.Write(Format.MarkDown); } foreach (var sublist in OptimizerUtilities.Split(WeekDaysInRange, 20)) { Headers = new List <string> { "Location" }; sublist.ForEach(day => Headers.Add(day.Substring(0, day.Length - 5))); table = new ConsoleTable(Headers.ToArray()); foreach (var LocPair in CurrentlyReleased) { var row = new List <string> { Engine.Locations.First(loc => loc.ID == LocPair.Key).Code }; LocPair.Value.ToList().ForEach(pair => { if (sublist.Contains(pair.Key)) { row.Add((pair.Value > 0 ? pair.Value.ToString() : "")); } }); table.AddRow(row.ToArray()); } Console.WriteLine("CurrentlyReleased"); table.Write(Format.MarkDown); } foreach (var sublist in OptimizerUtilities.Split(WeekDaysInRange, 20)) { Headers = new List <string> { "Location" }; sublist.ForEach(day => Headers.Add(day.Substring(0, day.Length - 5))); table = new ConsoleTable(Headers.ToArray()); foreach (var Location in LocallyTaughtCoursesPerDay) { var row = new List <string> { Engine.Locations.First(loc => loc.ID == Location.Key).Code }; Location.Value.Where(pair => sublist.Contains(pair.Key)).ToList().ForEach(list => { row.Add(string.Join(", ", list.Value)); }); table.AddRow(row.ToArray()); } Console.WriteLine("Locally taught courses at each location"); table.Write(Format.MarkDown); } } // Populate the unavailability area with the instructor assignments foreach (var assignment in Engine.InstructorAssignments) { // Mark each day of the relevant days of the assignment to the optimization range to true (unavailable) foreach (var date in OptimizerUtilities.EachWeekDay(OptimizerUtilities.Max(assignment.StartDate, Engine.StartDate), OptimizerUtilities.Min(assignment.EndDate, Engine.EndDate))) { IsInstructorUnavailable[assignment.UserID][date.ToString(TIME_FORMAT)] = true; } // Account for travel time by marking the day before and after for non local assignments unavailable for that instructor // Only if the day is adjacent to the assignment (i.e. not the friday before a monday of an assignment) if (!assignment.LocalAssignment) { // Check if the day before the start day for the assignment is in the index map // If its not, it is either an adjacent week day but outside the range of the optimizer // Or it is a weekend (the day before monday is sunday, will not be mapped in the dictionary var DayBeforeAssignment = OptimizerUtilities.Max(assignment.StartDate, Engine.StartDate).AddDays(-1).ToString(Engine.TIME_FORMAT); if (WeekDaysInRange.Contains(DayBeforeAssignment)) { IsInstructorUnavailable[assignment.UserID][DayBeforeAssignment] = true; } // Do same but for the day after var DayAfterAssignment = OptimizerUtilities.Min(assignment.EndDate, Engine.EndDate).ToString(Engine.TIME_FORMAT); if (WeekDaysInRange.Contains(DayAfterAssignment)) { IsInstructorUnavailable[assignment.UserID][DayAfterAssignment] = true; } } } // Add instructor exceptions to the unavailable dictionary foreach (var exception in Engine.Exceptions) { foreach (var date in OptimizerUtilities.EachWeekDay(OptimizerUtilities.Max(exception.StartDate, Engine.StartDate), OptimizerUtilities.Min(exception.EndDate, Engine.EndDate))) { IsInstructorUnavailable[exception.RequestForID][date.ToString(TIME_FORMAT)] = true; } } if (ShowDebugMessages) { List <string> Headers; ConsoleTable table; foreach (var sublist in OptimizerUtilities.Split(WeekDaysInRange, 20)) { Headers = new List <string> { "Username" }; sublist.ForEach(day => Headers.Add(day.Substring(0, day.Length - 5))); table = new ConsoleTable(Headers.ToArray()); foreach (var InstructorPair in IsInstructorUnavailable) { var row = new List <string> { InstructorPair.Key.ToString() }; InstructorPair.Value.ToList().ForEach(pair => { if (sublist.Contains(pair.Key)) { row.Add((pair.Value ? "Yes" : "")); } }); table.AddRow(row.ToArray()); } Console.WriteLine("IsInstructorUnavailable"); table.Write(Format.MarkDown); } } // Set the last day each instructor taught a course if the priority is set if (Engine.MyPriority == Priority.MaximizeInstructorLongestToTeach) { var CompleteSchedule = context.ScheduledClass.ToArray(); // Obtain every instructor assignment in the database var completeAssignments = context.InstructorOfClass.OrderBy(i => i.StartDate).ToArray(); Console.WriteLine("Calculating the last time each instructor taught a course..."); foreach (var a in completeAssignments) { if (CompleteSchedule.Any(s => s.ID == a.ClassID)) { // grab the class that corresponds to the assignment ScheduledClass correspondingClass = CompleteSchedule.First(s => s.ID == a.ClassID); if (Engine.CourseCatalog.Any(c => c.ID == correspondingClass.CourseID)) { // get the course info var CourseInfo = Engine.CourseCatalog.First(c => c.ID == CompleteSchedule.First(s => s.ID == a.ClassID).CourseID); if (CourseInfo.QualifiedInstructors.ContainsKey(a.UserID)) { // if this assignment ends after the last recorded time the instructor taught this course, update // the last time taugth the course to the end of this assignment if (DateTime.Compare(CourseInfo.QualifiedInstructors[a.UserID], a.EndDate) < 0) { CourseInfo.QualifiedInstructors[a.UserID] = a.EndDate; } } } } } // Limit Course catalog to courses just listed in the input Engine.CourseCatalog = Engine.CourseCatalog.Where(c => Engine.Inputs.Any(i => i.CourseId == c.ID)).ToList(); Console.WriteLine("Done."); if (ShowDebugMessages) { List <string> Headers; ConsoleTable table; Headers = new List <string> { "CourseCode" }; Headers.Add("Instructor FileNumber: Last Taught"); table = new ConsoleTable(Headers.ToArray()); foreach (var Course in Engine.CourseCatalog) { foreach (var sublist in OptimizerUtilities.Split(Course.QualifiedInstructors.ToList(), 8)) { var row = new List <string> { Course.Code.ToString() }; var rowString = ""; Course.QualifiedInstructors.ToList().ForEach(p => { if (sublist.Contains(p)) { if (p.Value == DateTime.MinValue) { rowString += $"{p.Key}: {"Never Taught"}; "; } else { rowString += $"{p.Key}: {p.Value.ToString("MM/dd/yyyy")}; "; } } } ); row.Add(rowString); table.AddRow(row.ToArray()); } } Console.WriteLine("Last Time A Course Was Taught By Who"); table.Write(Format.MarkDown); } } if (ShowDebugMessages) { Console.WriteLine("Data loading complete.\n"); } if (ShowDebugMessages) { Console.WriteLine("Checking if any inputs are impossible..."); } Engine.PrimeStartingResults(ref IsRoomUnavailable, ref IsInstructorUnavailable, ref CurrentlyReleased, ref LocallyTaughtCoursesPerDay); StartingResults = new OptimizerScheduleResults() { Results = new List <OptimizerResult>(), Inputs = Engine.Inputs, OptimizationScore = 0 }; Engine.InputCount = Engine.Inputs.Count; var Levels = 0; Engine.Inputs.ForEach(x => Levels += Math.Min(x.MaxPossibleIterations, x.NumTimesToRun)); Engine.NodesPerDepth = new int[Levels]; if (ShowDebugMessages) { Console.WriteLine("Done.\n"); } if (ShowDebugMessages) { Console.WriteLine("Optimizer Possible Inputs"); try { ConsoleTable.From <OptimizerInputPrintable>(Engine.Inputs.Select(x => new OptimizerInputPrintable(x))).Write(Format.MarkDown); } catch { } Console.WriteLine(""); } if (ShowDebugMessages) { Console.WriteLine("Predicting the best possible score for the engine..."); } // Setup the best answer as something guarenteed to always be the worst Engine.CurrentBestAnswer = new OptimizerScheduleResults { Results = new List <OptimizerResult>(), Inputs = new List <OptimizerInput>(), OptimizationScore = -1 }; if (Engine.MyPriority == Priority.MinimizeForeignInstructorCount || Engine.MyPriority == Priority.MinimizeInstructorTravelDistance) { Engine.CurrentBestAnswer.OptimizationScore = int.MaxValue; } switch (Engine.MyPriority) { case Priority.MaximizeInstructorLongestToTeach: Engine.BestPossibleScore = int.MaxValue; break; case (Priority.MaximizeSpecializedInstructors): Engine.BestPossibleScore = Engine.Instructors.MinBy(i => i.QualificationCount).First().QualificationCount *Engine.InputCount; if (Engine.BestPossibleScore == 0) { Engine.BestPossibleScore = Engine.InputCount; } break; case (Priority.MinimizeForeignInstructorCount): // Set the best answer to 0, meaning 0 instructors to travel Engine.BestPossibleScore = Levels; // Increment the best possible answer for every input that has no qualified local instructors foreach (var input in Engine.Inputs) { var CourseInfo = Engine.CourseCatalog.Where(course => course.ID == input.CourseId).First(); if (Engine.Locations.First(z => z.ID == input.LocationIdLiteral).LocalInstructors.Any(x => CourseInfo.QualifiedInstructors.Any(y => x == y.Key))) { Console.WriteLine($"Subtracting {input.MaxPossibleIterations} from the score because of input {input.Id}:"); Console.WriteLine($"Location {input.LocationID} does not have a local, qualified instructor to teach a course of type {input.CourseCode}."); Engine.BestPossibleScore -= input.MaxPossibleIterations; } } break; case (Priority.MinimizeInstructorTravelDistance): // Set the best answer to 0, meaning 0 instructors to travel Engine.BestPossibleScore = 0; break; default: Engine.BestPossibleScore = Engine.NodesPerDepth.Length; break; } if (ShowDebugMessages) { Console.WriteLine($"The best possible score is {Engine.BestPossibleScore}.\n"); } if (ShowDebugMessages) { Console.WriteLine($"If the best possible answer is not found in {timeLimit}, the current best answer will be returned.\n"); } return(Engine); }