001package org.cpsolver.studentsct.extension; 002 003import java.util.ArrayList; 004import java.util.BitSet; 005import java.util.Collection; 006import java.util.HashMap; 007import java.util.HashSet; 008import java.util.Iterator; 009import java.util.List; 010import java.util.Map; 011import java.util.Set; 012 013import org.apache.logging.log4j.Logger; 014import org.cpsolver.coursett.Constants; 015import org.cpsolver.coursett.model.Placement; 016import org.cpsolver.coursett.model.RoomLocation; 017import org.cpsolver.coursett.model.TimeLocation; 018import org.cpsolver.ifs.assignment.Assignment; 019import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext; 020import org.cpsolver.ifs.assignment.context.CanInheritContext; 021import org.cpsolver.ifs.assignment.context.ExtensionWithContext; 022import org.cpsolver.ifs.model.InfoProvider; 023import org.cpsolver.ifs.model.ModelListener; 024import org.cpsolver.ifs.solver.Solver; 025import org.cpsolver.ifs.util.DataProperties; 026import org.cpsolver.ifs.util.DistanceMetric; 027import org.cpsolver.studentsct.StudentSectioningModel; 028import org.cpsolver.studentsct.StudentSectioningModel.StudentSectioningModelContext; 029import org.cpsolver.studentsct.model.CourseRequest; 030import org.cpsolver.studentsct.model.Enrollment; 031import org.cpsolver.studentsct.model.FreeTimeRequest; 032import org.cpsolver.studentsct.model.Request; 033import org.cpsolver.studentsct.model.SctAssignment; 034import org.cpsolver.studentsct.model.Section; 035import org.cpsolver.studentsct.model.Student; 036import org.cpsolver.studentsct.model.Student.BackToBackPreference; 037import org.cpsolver.studentsct.model.Student.ModalityPreference; 038 039import org.cpsolver.studentsct.model.Unavailability; 040 041/** 042 * This extension computes student schedule quality using various matrices. 043 * It replaces {@link TimeOverlapsCounter} and {@link DistanceConflict} extensions. 044 * Besides of time and distance conflicts, it also counts cases when a student 045 * has a lunch break conflict, travel time during the day, it can prefer 046 * or discourage student class back-to-back and cases when a student has more than 047 * a given number of hours between the first and the last class on a day. 048 * See {@link StudentQuality.Type} for more details. 049 * 050 * <br> 051 * <br> 052 * 053 * @version StudentSct 1.3 (Student Sectioning)<br> 054 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 055 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 056 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 057 * <br> 058 * This library is free software; you can redistribute it and/or modify 059 * it under the terms of the GNU Lesser General Public License as 060 * published by the Free Software Foundation; either version 3 of the 061 * License, or (at your option) any later version. <br> 062 * <br> 063 * This library is distributed in the hope that it will be useful, but 064 * WITHOUT ANY WARRANTY; without even the implied warranty of 065 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 066 * Lesser General Public License for more details. <br> 067 * <br> 068 * You should have received a copy of the GNU Lesser General Public 069 * License along with this library; if not see 070 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 071 */ 072 073public class StudentQuality extends ExtensionWithContext<Request, Enrollment, StudentQuality.StudentQualityContext> implements ModelListener<Request, Enrollment>, CanInheritContext<Request, Enrollment, StudentQuality.StudentQualityContext>, InfoProvider<Request, Enrollment> { 074 private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(StudentQuality.class); 075 private Context iContext; 076 077 /** 078 * Constructor 079 * @param solver student scheduling solver 080 * @param properties solver configuration 081 */ 082 public StudentQuality(Solver<Request, Enrollment> solver, DataProperties properties) { 083 super(solver, properties); 084 if (solver != null) { 085 StudentSectioningModel model = (StudentSectioningModel) solver.currentSolution().getModel(); 086 iContext = new Context(model.getDistanceMetric(), properties); 087 model.setStudentQuality(this, false); 088 } else { 089 iContext = new Context(null, properties); 090 } 091 } 092 093 /** 094 * Constructor 095 * @param metrics distance metric 096 * @param properties solver configuration 097 */ 098 public StudentQuality(DistanceMetric metrics, DataProperties properties) { 099 super(null, properties); 100 iContext = new Context(metrics, properties); 101 } 102 103 /** 104 * Current distance metric 105 * @return distance metric 106 */ 107 public DistanceMetric getDistanceMetric() { 108 return iContext.getDistanceMetric(); 109 } 110 111 /** 112 * Is debugging enabled 113 * @return true when StudentQuality.Debug is true 114 */ 115 public boolean isDebug() { 116 return iContext.isDebug(); 117 } 118 119 /** 120 * Student quality context 121 */ 122 public Context getStudentQualityContext() { 123 return iContext; 124 } 125 126 /** 127 * Weighting types 128 */ 129 public static enum WeightType { 130 /** Penalty is incurred on the request with higher priority */ 131 HIGHER, 132 /** Penalty is incurred on the request with lower priority */ 133 LOWER, 134 /** Penalty is incurred on both requests */ 135 BOTH, 136 /** Penalty is incurred on the course request (for conflicts between course request and a free time) */ 137 REQUEST, 138 ; 139 } 140 141 /** 142 * Measured student qualities 143 * 144 */ 145 public static enum Type { 146 /** 147 * Time conflicts between two classes that is allowed. Time conflicts are penalized as shared time 148 * between two course requests proportional to the time of each, capped at one half of the time. 149 * This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5. 150 */ 151 CourseTimeOverlap(WeightType.BOTH, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){ 152 @Override 153 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 154 return r1 instanceof CourseRequest && r2 instanceof CourseRequest; 155 } 156 157 @Override 158 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 159 if (a1.getTime() == null || a2.getTime() == null) return false; 160 if (((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false; 161 return a1.getTime().hasIntersection(a2.getTime()); 162 } 163 164 @Override 165 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 166 if (!inConflict(cx, a1, a2)) return 0; 167 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 168 } 169 170 @Override 171 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 172 return new Nothing(); 173 } 174 175 @Override 176 public double getWeight(Context cx, Conflict c, Enrollment e) { 177 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / e.getNrSlots(), cx.getTimeOverlapMaxLimit()); 178 } 179 }), 180 /** 181 * Time conflict between class and a free time request. Free time conflicts are penalized as the time 182 * of a course request overlapping with a free time proportional to the time of the request, capped at one half 183 * of the time. This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5. 184 */ 185 FreeTimeOverlap(WeightType.REQUEST, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){ 186 @Override 187 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 188 return false; 189 } 190 191 @Override 192 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 193 if (a1.getTime() == null || a2.getTime() == null) return false; 194 return a1.getTime().hasIntersection(a2.getTime()); 195 } 196 197 @Override 198 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 199 if (!inConflict(cx, a1, a2)) return 0; 200 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 201 } 202 203 @Override 204 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 205 return (e.isCourseRequest() ? new FreeTimes(e.getStudent()) : new Nothing()); 206 } 207 208 @Override 209 public double getWeight(Context cx, Conflict c, Enrollment e) { 210 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit()); 211 } 212 }), 213 /** 214 * Student unavailability conflict. Time conflict between a class that the student is taking and a class that the student 215 * is teaching (if time conflicts are allowed). Unavailability conflicts are penalized as the time 216 * of a course request overlapping with an unavailability proportional to the time of the request, capped at one half 217 * of the time. This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5. 218 */ 219 Unavailability(WeightType.REQUEST, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){ 220 @Override 221 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 222 return false; 223 } 224 225 @Override 226 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 227 if (a1.getTime() == null || a2.getTime() == null) return false; 228 return a1.getTime().hasIntersection(a2.getTime()); 229 } 230 231 @Override 232 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 233 if (!inConflict(cx, a1, a2)) return 0; 234 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 235 } 236 237 @Override 238 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 239 return (e.isCourseRequest() ? new Unavailabilities(e.getStudent()) : new Nothing()); 240 } 241 242 @Override 243 public double getWeight(Context cx, Conflict c, Enrollment e) { 244 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit()); 245 } 246 }), 247 /** 248 * Distance conflict. When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to false, 249 * distance conflicts are only considered between back-to-back classes (break time of the first 250 * class is shorter than the distance in minutes between the two classes). When 251 * Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to true, the distance between the 252 * two classes is also considered. 253 * This criterion is weighted by StudentWeights.DistanceConflict, defaulting to 0.01. 254 */ 255 Distance(WeightType.LOWER, "StudentWeights.DistanceConflict", 0.0100, new Quality(){ 256 @Override 257 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 258 return r1 instanceof CourseRequest && r2 instanceof CourseRequest; 259 } 260 261 @Override 262 public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) { 263 Section s1 = (Section) sa1; 264 Section s2 = (Section) sa2; 265 if (s1.getPlacement() == null || s2.getPlacement() == null) 266 return false; 267 TimeLocation t1 = s1.getTime(); 268 TimeLocation t2 = s2.getTime(); 269 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) 270 return false; 271 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 272 if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) { 273 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 274 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 275 if (dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength())) 276 return true; 277 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 278 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 279 if (dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength())) 280 return true; 281 } 282 } else { 283 if (a1 + t1.getNrSlotsPerMeeting() == a2) { 284 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 285 if (dist > t1.getBreakTime()) 286 return true; 287 } else if (a2 + t2.getNrSlotsPerMeeting() == a1) { 288 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 289 if (dist > t2.getBreakTime()) 290 return true; 291 } 292 } 293 return false; 294 } 295 296 @Override 297 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 298 return inConflict(cx, a1, a2) ? 1 : 0; 299 } 300 301 @Override 302 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 303 return new Nothing(); 304 } 305 306 @Override 307 public double getWeight(Context cx, Conflict c, Enrollment e) { 308 return c.getPenalty(); 309 } 310 }), 311 /** 312 * Short distance conflict. Similar to distance conflicts but for students that require short 313 * distances. When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to false, 314 * distance conflicts are only considered between back-to-back classes (travel time between the 315 * two classes is more than zero minutes). When 316 * Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to true, the distance between the 317 * two classes is also considered (break time is also ignored). 318 * This criterion is weighted by StudentWeights.ShortDistanceConflict, defaulting to 0.1. 319 */ 320 ShortDistance(WeightType.LOWER, "StudentWeights.ShortDistanceConflict", 0.1000, new Quality(){ 321 @Override 322 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 323 return student.isNeedShortDistances() && r1 instanceof CourseRequest && r2 instanceof CourseRequest; 324 } 325 326 @Override 327 public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) { 328 Section s1 = (Section) sa1; 329 Section s2 = (Section) sa2; 330 if (s1.getPlacement() == null || s2.getPlacement() == null) 331 return false; 332 TimeLocation t1 = s1.getTime(); 333 TimeLocation t2 = s2.getTime(); 334 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) 335 return false; 336 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 337 if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) { 338 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 339 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 340 if (dist > Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength())) 341 return true; 342 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 343 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 344 if (dist > Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength())) 345 return true; 346 } 347 } else { 348 if (a1 + t1.getNrSlotsPerMeeting() == a2) { 349 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 350 if (dist > 0) return true; 351 } else if (a2 + t2.getNrSlotsPerMeeting() == a1) { 352 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 353 if (dist > 0) return true; 354 } 355 } 356 return false; 357 } 358 359 @Override 360 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 361 return inConflict(cx, a1, a2) ? 1 : 0; 362 } 363 364 @Override 365 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 366 return new Nothing(); 367 } 368 369 @Override 370 public double getWeight(Context cx, Conflict c, Enrollment e) { 371 return c.getPenalty(); 372 } 373 }), 374 /** 375 * Naive, yet effective approach for modeling student lunch breaks. It creates a conflict whenever there are 376 * two classes (of a student) overlapping with the lunch time which are one after the other with a break in 377 * between smaller than the requested lunch break. Lunch time is defined by StudentLunch.StartSlot and 378 * StudentLunch.EndStart properties (default is 11:00 am - 1:30 pm), with lunch break of at least 379 * StudentLunch.Length slots (default is 30 minutes). Such a conflict is weighted 380 * by StudentWeights.LunchBreakFactor, which defaults to 0.005. 381 */ 382 LunchBreak(WeightType.BOTH, "StudentWeights.LunchBreakFactor", 0.0050, new Quality() { 383 @Override 384 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 385 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy(); 386 } 387 388 @Override 389 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 390 if (a1.getTime() == null || a2.getTime() == null) return false; 391 if (((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false; 392 if (a1.getTime().hasIntersection(a2.getTime())) return false; 393 TimeLocation t1 = a1.getTime(), t2 = a2.getTime(); 394 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 395 int s1 = t1.getStartSlot(), s2 = t2.getStartSlot(); 396 int e1 = t1.getStartSlot() + t1.getNrSlotsPerMeeting(), e2 = t2.getStartSlot() + t2.getNrSlotsPerMeeting(); 397 if (e1 + cx.getLunchLength() > s2 && e2 + cx.getLunchLength() > s1 && e1 > cx.getLunchStart() && cx.getLunchEnd() > s1 && e2 > cx.getLunchStart() && cx.getLunchEnd() > s2) 398 return true; 399 return false; 400 } 401 402 @Override 403 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 404 if (!inConflict(cx, a1, a2)) return 0; 405 return a1.getTime().nrSharedDays(a2.getTime()); 406 } 407 408 @Override 409 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 410 return new Nothing(); 411 } 412 413 @Override 414 public double getWeight(Context cx, Conflict c, Enrollment e) { 415 return c.getPenalty(); 416 } 417 }), 418 /** 419 * Naive, yet effective approach for modeling travel times. A conflict with the penalty 420 * equal to the distance in minutes occurs when two classes are less than TravelTime.MaxTravelGap 421 * time slots a part (defaults 1 hour), or when they are less then twice as long apart 422 * and the travel time is longer than the break time of the first class. 423 * Such a conflict is weighted by StudentWeights.TravelTimeFactor, which defaults to 0.001. 424 */ 425 TravelTime(WeightType.BOTH, "StudentWeights.TravelTimeFactor", 0.0010, new Quality() { 426 @Override 427 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 428 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy(); 429 } 430 431 @Override 432 public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) { 433 Section s1 = (Section) sa1; 434 Section s2 = (Section) sa2; 435 if (s1.getPlacement() == null || s2.getPlacement() == null) 436 return false; 437 TimeLocation t1 = s1.getTime(); 438 TimeLocation t2 = s2.getTime(); 439 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) 440 return false; 441 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 442 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 443 int gap = a2 - (a1 + t1.getNrSlotsPerMeeting()); 444 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 445 return (gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t1.getBreakTime()); 446 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 447 int gap = a1 - (a2 + t2.getNrSlotsPerMeeting()); 448 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 449 return (gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t2.getBreakTime()); 450 } 451 return false; 452 } 453 454 @Override 455 public int penalty(Context cx, Student s, SctAssignment sa1, SctAssignment sa2) { 456 Section s1 = (Section) sa1; 457 Section s2 = (Section) sa2; 458 if (s1.getPlacement() == null || s2.getPlacement() == null) return 0; 459 TimeLocation t1 = s1.getTime(); 460 TimeLocation t2 = s2.getTime(); 461 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) return 0; 462 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 463 if (a1 + t1.getNrSlotsPerMeeting() <= a2) { 464 int gap = a2 - (a1 + t1.getNrSlotsPerMeeting()); 465 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 466 if ((gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t1.getBreakTime())) 467 return dist; 468 } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) { 469 int gap = a1 - (a2 + t2.getNrSlotsPerMeeting()); 470 int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement()); 471 if ((gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t2.getBreakTime())) 472 return dist; 473 } 474 return 0; 475 } 476 477 @Override 478 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 479 return new Nothing(); 480 } 481 482 @Override 483 public double getWeight(Context cx, Conflict c, Enrollment e) { 484 return c.getPenalty(); 485 } 486 }), 487 /** 488 * A back-to-back conflict is there every time when a student has two classes that are 489 * back-to-back or less than StudentWeights.BackToBackDistance time slots apart (defaults to 30 minutes). 490 * Such a conflict is weighted by StudentWeights.BackToBackFactor, which 491 * defaults to -0.0001 (these conflicts are preferred by default, trying to avoid schedule gaps). 492 * NEW: Consider student's back-to-back preference. That is, students with no preference are ignored, and 493 * students that discourage back-to-backs have a negative weight on the conflict. 494 */ 495 BackToBack(WeightType.BOTH, "StudentWeights.BackToBackFactor", -0.0001, new Quality() { 496 @Override 497 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 498 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && 499 (student.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED || student.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED); 500 } 501 502 @Override 503 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 504 TimeLocation t1 = a1.getTime(); 505 TimeLocation t2 = a2.getTime(); 506 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 507 if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) { 508 int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting()); 509 return dist <= cx.getBackToBackDistance(); 510 } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) { 511 int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting()); 512 return dist <= cx.getBackToBackDistance(); 513 } 514 return false; 515 } 516 517 @Override 518 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 519 if (!inConflict(cx, a1, a2)) return 0; 520 if (s.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED) 521 return a1.getTime().nrSharedDays(a2.getTime()); 522 else if (s.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED) 523 return -a1.getTime().nrSharedDays(a2.getTime()); 524 else 525 return 0; 526 } 527 528 @Override 529 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 530 return new Nothing(); 531 } 532 533 @Override 534 public double getWeight(Context cx, Conflict c, Enrollment e) { 535 return c.getPenalty(); 536 } 537 }), 538 /** 539 * A work-day conflict is there every time when a student has two classes that are too 540 * far apart. This means that the time between the start of the first class and the end 541 * of the last class is more than WorkDay.WorkDayLimit (defaults to 6 hours). A penalty 542 * of one is incurred for every hour started over this limit. 543 * Such a conflict is weighted by StudentWeights.WorkDayFactor, which defaults to 0.01. 544 */ 545 WorkDay(WeightType.BOTH, "StudentWeights.WorkDayFactor", 0.0100, new Quality() { 546 @Override 547 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 548 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy(); 549 } 550 551 @Override 552 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 553 TimeLocation t1 = a1.getTime(); 554 TimeLocation t2 = a2.getTime(); 555 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 556 int dist = Math.max(t1.getStartSlot() + t1.getLength(), t2.getStartSlot() + t2.getLength()) - Math.min(t1.getStartSlot(), t2.getStartSlot()); 557 return dist > cx.getWorkDayLimit(); 558 } 559 560 @Override 561 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 562 TimeLocation t1 = a1.getTime(); 563 TimeLocation t2 = a2.getTime(); 564 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return 0; 565 int dist = Math.max(t1.getStartSlot() + t1.getLength(), t2.getStartSlot() + t2.getLength()) - Math.min(t1.getStartSlot(), t2.getStartSlot()); 566 if (dist > cx.getWorkDayLimit()) 567 return a1.getTime().nrSharedDays(a2.getTime()) * (dist - cx.getWorkDayLimit()); 568 else 569 return 0; 570 } 571 572 @Override 573 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 574 return new Nothing(); 575 } 576 577 @Override 578 public double getWeight(Context cx, Conflict c, Enrollment e) { 579 return c.getPenalty() / 12.0; 580 } 581 }), 582 TooEarly(WeightType.REQUEST, "StudentWeights.TooEarlyFactor", 0.0500, new Quality(){ 583 @Override 584 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 585 return false; 586 } 587 588 @Override 589 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 590 if (a1.getTime() == null || a2.getTime() == null) return false; 591 return a1.getTime().shareDays(a2.getTime()) && a1.getTime().shareHours(a2.getTime()); 592 } 593 594 @Override 595 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 596 if (!inConflict(cx, a1, a2)) return 0; 597 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 598 } 599 600 @Override 601 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 602 return (e.isCourseRequest() && !e.getStudent().isDummy() ? new SingleTimeIterable(0, cx.getEarlySlot()) : new Nothing()); 603 } 604 605 @Override 606 public double getWeight(Context cx, Conflict c, Enrollment e) { 607 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit()); 608 } 609 }), 610 TooLate(WeightType.REQUEST, "StudentWeights.TooLateFactor", 0.0250, new Quality(){ 611 @Override 612 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 613 return false; 614 } 615 616 @Override 617 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 618 if (a1.getTime() == null || a2.getTime() == null) return false; 619 return a1.getTime().shareDays(a2.getTime()) && a1.getTime().shareHours(a2.getTime()); 620 } 621 622 @Override 623 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 624 if (!inConflict(cx, a1, a2)) return 0; 625 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 626 } 627 628 @Override 629 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 630 return (e.isCourseRequest() && !e.getStudent().isDummy() ? new SingleTimeIterable(cx.getLateSlot(), 288) : new Nothing()); 631 } 632 633 @Override 634 public double getWeight(Context cx, Conflict c, Enrollment e) { 635 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit()); 636 } 637 }), 638 /** 639 * There is a student modality preference conflict when a student that prefers online 640 * gets a non-online class ({@link Section#isOnline()} is false) or when a student that 641 * prefers non-online gets an online class (@{link Section#isOnline()} is true). 642 * Such a conflict is weighted by StudentWeights.ModalityFactor, which defaults to 0.05. 643 */ 644 Modality(WeightType.REQUEST, "StudentWeights.ModalityFactor", 0.0500, new Quality(){ 645 @Override 646 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 647 return false; 648 } 649 650 @Override 651 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 652 return a1.equals(a2); 653 } 654 655 @Override 656 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 657 return (inConflict(cx, a1, a2) ? 1 : 0); 658 } 659 660 @Override 661 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 662 if (!e.isCourseRequest() || e.getStudent().isDummy()) return new Nothing(); 663 if (e.getStudent().getModalityPreference() == ModalityPreference.ONLINE_PREFERRED) 664 return new Online(e, false); // face-to-face sections are conflicting 665 else if (e.getStudent().getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED) 666 return new Online(e, true); // online sections are conflicting 667 return new Nothing(); 668 } 669 670 @Override 671 public double getWeight(Context cx, Conflict c, Enrollment e) { 672 return ((double) c.getPenalty()) / ((double) e.getSections().size()); 673 } 674 }), 675 /** 676 * DRC: Time conflict between class and a free time request (for students with FT accommodation). 677 * Free time conflicts are penalized as the time of a course request overlapping with a free time 678 * proportional to the time of the request, capped at one half of the time. 679 * This criterion is weighted by Accommodations.FreeTimeOverlapFactor, defaulting to 0.5. 680 */ 681 AccFreeTimeOverlap(WeightType.REQUEST, "Accommodations.FreeTimeOverlapFactor", 0.5000, new Quality(){ 682 @Override 683 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 684 return false; 685 } 686 687 @Override 688 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 689 if (a1.getTime() == null || a2.getTime() == null) return false; 690 return a1.getTime().hasIntersection(a2.getTime()); 691 } 692 693 @Override 694 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 695 if (!inConflict(cx, a1, a2)) return 0; 696 return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime()); 697 } 698 699 @Override 700 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 701 if (!e.getStudent().hasAccommodation(cx.getFreeTimeAccommodation())) return new Nothing(); 702 return (e.isCourseRequest() ? new FreeTimes(e.getStudent()) : new Nothing()); 703 } 704 705 @Override 706 public double getWeight(Context cx, Conflict c, Enrollment e) { 707 return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit()); 708 } 709 }), 710 /** 711 * DRC: A back-to-back conflict (for students with BTB accommodation) is there every time when a student has two classes that are NOT 712 * back-to-back or less than Accommodations.BackToBackDistance time slots apart (defaults to 30 minutes). 713 * Such a conflict is weighted by Accommodations.BackToBackFactor, which defaults to 0.001 714 */ 715 AccBackToBack(WeightType.BOTH, "Accommodations.BackToBackFactor", 0.001, new Quality() { 716 @Override 717 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 718 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && student.hasAccommodation(cx.getBackToBackAccommodation()); 719 } 720 721 @Override 722 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 723 TimeLocation t1 = a1.getTime(); 724 TimeLocation t2 = a2.getTime(); 725 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 726 if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) { 727 int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting()); 728 return dist > cx.getBackToBackDistance(); 729 } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) { 730 int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting()); 731 return dist > cx.getBackToBackDistance(); 732 } 733 return false; 734 } 735 736 @Override 737 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 738 if (!inConflict(cx, a1, a2)) return 0; 739 return a1.getTime().nrSharedDays(a2.getTime()); 740 } 741 742 @Override 743 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 744 return new Nothing(); 745 } 746 747 @Override 748 public double getWeight(Context cx, Conflict c, Enrollment e) { 749 return c.getPenalty(); 750 } 751 }), 752 /** 753 * DRC: A not back-to-back conflict (for students with BBC accommodation) is there every time when a student has two classes that are 754 * back-to-back or less than Accommodations.BackToBackDistance time slots apart (defaults to 30 minutes). 755 * Such a conflict is weighted by Accommodations.BreaksBetweenClassesFactor, which defaults to 0.001. 756 */ 757 AccBreaksBetweenClasses(WeightType.BOTH, "Accommodations.BreaksBetweenClassesFactor", 0.001, new Quality() { 758 @Override 759 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { 760 return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && student.hasAccommodation(cx.getBreakBetweenClassesAccommodation()); 761 } 762 763 @Override 764 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { 765 TimeLocation t1 = a1.getTime(); 766 TimeLocation t2 = a2.getTime(); 767 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 768 if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) { 769 int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting()); 770 return dist <= cx.getBackToBackDistance(); 771 } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) { 772 int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting()); 773 return dist <= cx.getBackToBackDistance(); 774 } 775 return false; 776 } 777 778 @Override 779 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { 780 if (!inConflict(cx, a1, a2)) return 0; 781 return a1.getTime().nrSharedDays(a2.getTime()); 782 } 783 784 @Override 785 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { 786 return new Nothing(); 787 } 788 789 @Override 790 public double getWeight(Context cx, Conflict c, Enrollment e) { 791 return c.getPenalty(); 792 } 793 }), 794 ; 795 796 private WeightType iType; 797 private Quality iQuality; 798 private String iWeightName; 799 private double iWeightDefault; 800 Type(WeightType type, String weightName, double weightDefault, Quality quality) { 801 iQuality = quality; 802 iType = type; 803 iWeightName = weightName; 804 iWeightDefault = weightDefault; 805 } 806 807 808 public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { return iQuality.isApplicable(cx, student, r1, r2); } 809 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { return iQuality.inConflict(cx, a1, a2); } 810 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { return iQuality.penalty(cx, s, a1, a2); } 811 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { return iQuality.other(cx, e); } 812 public double getWeight(Context cx, Conflict c, Enrollment e) { return iQuality.getWeight(cx, c, e); } 813 public String getName() { return name().replaceAll("(?<=[^A-Z0-9])([A-Z0-9])"," $1"); } 814 public String getAbbv() { return getName().replaceAll("[a-z ]",""); } 815 public WeightType getType() { return iType; } 816 public String getWeightName() { return iWeightName; } 817 public double getWeightDefault() { return iWeightDefault; } 818 } 819 820 /** 821 * Schedule quality interface 822 */ 823 public static interface Quality { 824 /** 825 * Check if the metric is applicable for the given student, between the given two requests 826 */ 827 public boolean isApplicable(Context cx, Student student, Request r1, Request r2); 828 /** 829 * When applicable, is there a conflict between two sections 830 */ 831 public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2); 832 /** 833 * When in conflict, what is the penalisation 834 */ 835 public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2); 836 /** 837 * Enumerate other section assignments applicable for the given enrollment (e.g., student unavailabilities) 838 */ 839 public Iterable<? extends SctAssignment> other(Context cx, Enrollment e); 840 /** 841 * Base weight of the given conflict and enrollment. Typically based on the {@link Conflict#getPenalty()}, but 842 * change to be between 0.0 and 1.0. For example, for time conflicts, a percentage of share is used. 843 */ 844 public double getWeight(Context cx, Conflict c, Enrollment e); 845 } 846 847 /** 848 * Penalisation of the given type between two enrollments of a student. 849 */ 850 public int penalty(Type type, Enrollment e1, Enrollment e2) { 851 if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) return 0; 852 int cnt = 0; 853 for (SctAssignment s1 : e1.getAssignments()) { 854 for (SctAssignment s2 : e2.getAssignments()) { 855 cnt += type.penalty(iContext, e1.getStudent(), s1, s2); 856 } 857 } 858 return cnt; 859 } 860 861 /** 862 * Conflicss of the given type between two enrollments of a student. 863 */ 864 public Set<Conflict> conflicts(Type type, Enrollment e1, Enrollment e2) { 865 Set<Conflict> ret = new HashSet<Conflict>(); 866 if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) return ret; 867 for (SctAssignment s1 : e1.getAssignments()) { 868 for (SctAssignment s2 : e2.getAssignments()) { 869 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 870 if (penalty != 0) 871 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e2, s2)); 872 } 873 } 874 return ret; 875 } 876 877 /** 878 * Conflicts of any type between two enrollments of a student. 879 */ 880 public Set<Conflict> conflicts(Enrollment e1, Enrollment e2) { 881 Set<Conflict> ret = new HashSet<Conflict>(); 882 for (Type type: iContext.getTypes()) { 883 if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) continue; 884 for (SctAssignment s1 : e1.getAssignments()) { 885 for (SctAssignment s2 : e2.getAssignments()) { 886 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 887 if (penalty != 0) 888 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e2, s2)); 889 } 890 } 891 } 892 return ret; 893 } 894 895 /** 896 * Conflicts of the given type between classes of a single enrollment (or with free times, unavailabilities, etc.) 897 */ 898 public Set<Conflict> conflicts(Type type, Enrollment e1) { 899 Set<Conflict> ret = new HashSet<Conflict>(); 900 boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest()); 901 for (SctAssignment s1 : e1.getAssignments()) { 902 if (applicable) { 903 for (SctAssignment s2 : e1.getAssignments()) { 904 if (s1.getId() < s2.getId()) { 905 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 906 if (penalty != 0) 907 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e1, s2)); 908 } 909 } 910 } 911 for (SctAssignment s2: type.other(iContext, e1)) { 912 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 913 if (penalty != 0) 914 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, s2)); 915 } 916 } 917 return ret; 918 } 919 920 /** 921 * Conflicts of any type between classes of a single enrollment (or with free times, unavailabilities, etc.) 922 */ 923 public Set<Conflict> conflicts(Enrollment e1) { 924 Set<Conflict> ret = new HashSet<Conflict>(); 925 for (Type type: iContext.getTypes()) { 926 boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest()); 927 for (SctAssignment s1 : e1.getAssignments()) { 928 if (applicable) { 929 for (SctAssignment s2 : e1.getAssignments()) { 930 if (s1.getId() < s2.getId()) { 931 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 932 if (penalty != 0) 933 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e1, s2)); 934 } 935 } 936 } 937 for (SctAssignment s2: type.other(iContext, e1)) { 938 int penalty = type.penalty(iContext, e1.getStudent(), s1, s2); 939 if (penalty != 0) 940 ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, s2)); 941 } 942 } 943 } 944 return ret; 945 } 946 947 /** 948 * Penalty of given type between classes of a single enrollment (or with free times, unavailabilities, etc.) 949 */ 950 public int penalty(Type type, Enrollment e1) { 951 int penalty = 0; 952 boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest()); 953 for (SctAssignment s1 : e1.getAssignments()) { 954 if (applicable) { 955 for (SctAssignment s2 : e1.getAssignments()) { 956 if (s1.getId() < s2.getId()) { 957 penalty += type.penalty(iContext, e1.getStudent(), s1, s2); 958 } 959 } 960 } 961 for (SctAssignment s2: type.other(iContext, e1)) { 962 penalty += type.penalty(iContext, e1.getStudent(), s1, s2); 963 } 964 } 965 return penalty; 966 } 967 968 /** 969 * Check whether the given type is applicable for the student and the two requests. 970 */ 971 public boolean isApplicable(Type type, Student student, Request r1, Request r2) { 972 return type.isApplicable(iContext, student, r1, r2); 973 } 974 975 /** 976 * Total penalisation of given type 977 */ 978 public int getTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) { 979 return getContext(assignment).getTotalPenalty(type); 980 } 981 982 /** 983 * Total penalisation of given types 984 */ 985 public int getTotalPenalty(Assignment<Request, Enrollment> assignment, Type... types) { 986 int ret = 0; 987 for (Type type: types) 988 ret += getContext(assignment).getTotalPenalty(type); 989 return ret; 990 } 991 992 /** 993 * Re-check total penalization for the given assignment 994 */ 995 public void checkTotalPenalty(Assignment<Request, Enrollment> assignment) { 996 for (Type type: iContext.getTypes()) 997 checkTotalPenalty(type, assignment); 998 } 999 1000 /** 1001 * Re-check total penalization for the given assignment and conflict type 1002 */ 1003 public void checkTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) { 1004 getContext(assignment).checkTotalPenalty(type, assignment); 1005 } 1006 1007 /** 1008 * All conflicts of the given type for the given assignment 1009 */ 1010 public Set<Conflict> getAllConflicts(Type type, Assignment<Request, Enrollment> assignment) { 1011 return getContext(assignment).getAllConflicts(type); 1012 } 1013 1014 /** 1015 * All conflicts of the any type for the enrollment (including conflicts with other enrollments of the student) 1016 */ 1017 public Set<Conflict> allConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 1018 Set<Conflict> conflicts = new HashSet<Conflict>(); 1019 for (Type t: iContext.getTypes()) { 1020 conflicts.addAll(conflicts(t, enrollment)); 1021 for (Request request : enrollment.getStudent().getRequests()) { 1022 if (request.equals(enrollment.getRequest()) || assignment.getValue(request) == null) continue; 1023 conflicts.addAll(conflicts(t, enrollment, assignment.getValue(request))); 1024 } 1025 } 1026 return conflicts; 1027 } 1028 1029 @Override 1030 public void beforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1031 getContext(assignment).beforeAssigned(assignment, iteration, value); 1032 } 1033 1034 @Override 1035 public void afterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1036 getContext(assignment).afterAssigned(assignment, iteration, value); 1037 } 1038 1039 @Override 1040 public void afterUnassigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1041 getContext(assignment).afterUnassigned(assignment, iteration, value); 1042 } 1043 1044 /** A representation of a time overlapping conflict */ 1045 public class Conflict { 1046 private Type iType; 1047 private int iPenalty; 1048 private Student iStudent; 1049 private SctAssignment iA1, iA2; 1050 private Enrollment iE1, iE2; 1051 private int iHashCode; 1052 1053 /** 1054 * Constructor 1055 * 1056 * @param student related student 1057 * @param type conflict type 1058 * @param penalty conflict penalization, e.g., the number of slots in common between the two conflicting sections 1059 * @param e1 first enrollment 1060 * @param a1 first conflicting section 1061 * @param e2 second enrollment 1062 * @param a2 second conflicting section 1063 */ 1064 public Conflict(Student student, Type type, int penalty, Enrollment e1, SctAssignment a1, Enrollment e2, SctAssignment a2) { 1065 iStudent = student; 1066 if (a1.compareById(a2) < 0 ) { 1067 iA1 = a1; 1068 iA2 = a2; 1069 iE1 = e1; 1070 iE2 = e2; 1071 } else { 1072 iA1 = a2; 1073 iA2 = a1; 1074 iE1 = e2; 1075 iE2 = e1; 1076 } 1077 iHashCode = (iStudent.getId() + ":" + iA1.getId() + ":" + iA2.getId()).hashCode(); 1078 iType = type; 1079 iPenalty = penalty; 1080 } 1081 1082 public Conflict(Student student, Type type, int penalty, Enrollment e1, SctAssignment a1, SctAssignment a2) { 1083 this(student, type, penalty, e1, a1, a2 instanceof FreeTimeRequest ? ((FreeTimeRequest)a2).createEnrollment() : a2 instanceof Unavailability ? ((Unavailability)a2).createEnrollment() : e1, a2); 1084 1085 } 1086 1087 /** Related student 1088 * @return student 1089 **/ 1090 public Student getStudent() { 1091 return iStudent; 1092 } 1093 1094 /** First section 1095 * @return first section 1096 **/ 1097 public SctAssignment getS1() { 1098 return iA1; 1099 } 1100 1101 /** Second section 1102 * @return second section 1103 **/ 1104 public SctAssignment getS2() { 1105 return iA2; 1106 } 1107 1108 /** First request 1109 * @return first request 1110 **/ 1111 public Request getR1() { 1112 return iE1.getRequest(); 1113 } 1114 1115 /** First request weight 1116 * @return first request weight 1117 **/ 1118 public double getR1Weight() { 1119 return (iE1.getRequest() == null ? 0.0 : iE1.getRequest().getWeight()); 1120 } 1121 1122 /** Second request weight 1123 * @return second request weight 1124 **/ 1125 public double getR2Weight() { 1126 return (iE2.getRequest() == null ? 0.0 : iE2.getRequest().getWeight()); 1127 } 1128 1129 /** Second request 1130 * @return second request 1131 **/ 1132 public Request getR2() { 1133 return iE2.getRequest(); 1134 } 1135 1136 /** First enrollment 1137 * @return first enrollment 1138 **/ 1139 public Enrollment getE1() { 1140 return iE1; 1141 } 1142 1143 /** Second enrollment 1144 * @return second enrollment 1145 **/ 1146 public Enrollment getE2() { 1147 return iE2; 1148 } 1149 1150 @Override 1151 public int hashCode() { 1152 return iHashCode; 1153 } 1154 1155 /** Conflict penalty, e.g., the number of overlapping slots against the number of slots of the smallest section 1156 * @return conflict penalty 1157 **/ 1158 public int getPenalty() { 1159 return iPenalty; 1160 } 1161 1162 /** Other enrollment of the conflict */ 1163 public Enrollment getOther(Enrollment enrollment) { 1164 return (getE1().getRequest().equals(enrollment.getRequest()) ? getE2() : getE1()); 1165 } 1166 1167 /** Weight of the conflict on the given enrollment */ 1168 public double getWeight(Enrollment e) { 1169 return iType.getWeight(iContext, this, e); 1170 } 1171 1172 /** Weight of the conflict on both enrollment (sum) */ 1173 public double getWeight() { 1174 return (iType.getWeight(iContext, this, iE1) + iType.getWeight(iContext, this, iE2)) / 2.0; 1175 } 1176 1177 /** Conflict type 1178 * @return conflict type; 1179 */ 1180 public Type getType() { 1181 return iType; 1182 } 1183 1184 @Override 1185 public boolean equals(Object o) { 1186 if (o == null || !(o instanceof Conflict)) return false; 1187 Conflict c = (Conflict) o; 1188 return getType() == c.getType() && getStudent().equals(c.getStudent()) && getS1().equals(c.getS1()) && getS2().equals(c.getS2()); 1189 } 1190 1191 @Override 1192 public String toString() { 1193 return getStudent() + ": (" + getType() + ", p:" + getPenalty() + ") " + getS1() + " -- " + getS2(); 1194 } 1195 } 1196 1197 /** 1198 * Context holding parameters and distance cache. See {@link Type} for the list of available parameters. 1199 */ 1200 public static class Context { 1201 private List<Type> iTypes = null; 1202 private DistanceMetric iDistanceMetric = null; 1203 private boolean iDebug = false; 1204 protected double iTimeOverlapMaxLimit = 0.5000; 1205 private int iLunchStart, iLunchEnd, iLunchLength, iMaxTravelGap, iWorkDayLimit, iBackToBackDistance, iEarlySlot, iLateSlot, iAccBackToBackDistance; 1206 private String iFreeTimeAccommodation = "FT", iBackToBackAccommodation = "BTB", iBreakBetweenClassesAccommodation = "BBC"; 1207 1208 public Context(DistanceMetric dm, DataProperties config) { 1209 iDistanceMetric = (dm == null ? new DistanceMetric(config) : dm); 1210 iDebug = config.getPropertyBoolean("StudentQuality.Debug", false); 1211 iTimeOverlapMaxLimit = config.getPropertyDouble("StudentWeights.TimeOverlapMaxLimit", iTimeOverlapMaxLimit); 1212 iLunchStart = config.getPropertyInt("StudentLunch.StartSlot", (11 * 60) / 5); 1213 iLunchEnd = config.getPropertyInt("StudentLunch.EndStart", (13 * 60) / 5); 1214 iLunchLength = config.getPropertyInt("StudentLunch.Length", 30 / 5); 1215 iMaxTravelGap = config.getPropertyInt("TravelTime.MaxTravelGap", 12); 1216 iWorkDayLimit = config.getPropertyInt("WorkDay.WorkDayLimit", 6 * 12); 1217 iBackToBackDistance = config.getPropertyInt("StudentWeights.BackToBackDistance", 6); 1218 iAccBackToBackDistance = config.getPropertyInt("Accommodations.BackToBackDistance", 6); 1219 iEarlySlot = config.getPropertyInt("WorkDay.EarlySlot", 102); 1220 iLateSlot = config.getPropertyInt("WorkDay.LateSlot", 210); 1221 iFreeTimeAccommodation = config.getProperty("Accommodations.FreeTimeReference", iFreeTimeAccommodation); 1222 iBackToBackAccommodation = config.getProperty("Accommodations.BackToBackReference", iBackToBackAccommodation); 1223 iBreakBetweenClassesAccommodation = config.getProperty("Accommodations.BreakBetweenClassesReference", iBreakBetweenClassesAccommodation); 1224 iTypes = new ArrayList<Type>(); 1225 for (Type t: Type.values()) 1226 if (config.getPropertyDouble(t.getWeightName(), t.getWeightDefault()) != 0.0) 1227 iTypes.add(t); 1228 } 1229 1230 public DistanceMetric getDistanceMetric() { 1231 return iDistanceMetric; 1232 } 1233 1234 public boolean isDebug() { return iDebug; } 1235 1236 public double getTimeOverlapMaxLimit() { return iTimeOverlapMaxLimit; } 1237 public int getLunchStart() { return iLunchStart; } 1238 public int getLunchEnd() { return iLunchEnd; } 1239 public int getLunchLength() { return iLunchLength; } 1240 public int getMaxTravelGap() { return iMaxTravelGap; } 1241 public int getWorkDayLimit() { return iWorkDayLimit; } 1242 public int getBackToBackDistance() { return iBackToBackDistance; } 1243 public int getAccBackToBackDistance() { return iAccBackToBackDistance; } 1244 public int getEarlySlot() { return iEarlySlot; } 1245 public int getLateSlot() { return iLateSlot; } 1246 public String getFreeTimeAccommodation() { return iFreeTimeAccommodation; } 1247 public String getBackToBackAccommodation() { return iBackToBackAccommodation; } 1248 public String getBreakBetweenClassesAccommodation() { return iBreakBetweenClassesAccommodation; } 1249 public List<Type> getTypes() { return iTypes; } 1250 1251 private Map<Long, Map<Long, Integer>> iDistanceCache = new HashMap<Long, Map<Long,Integer>>(); 1252 protected synchronized int getDistanceInMinutes(RoomLocation r1, RoomLocation r2) { 1253 if (r1.getId().compareTo(r2.getId()) > 0) return getDistanceInMinutes(r2, r1); 1254 if (r1.getId().equals(r2.getId()) || r1.getIgnoreTooFar() || r2.getIgnoreTooFar()) 1255 return 0; 1256 if (r1.getPosX() == null || r1.getPosY() == null || r2.getPosX() == null || r2.getPosY() == null) 1257 return iDistanceMetric.getMaxTravelDistanceInMinutes(); 1258 Map<Long, Integer> other2distance = iDistanceCache.get(r1.getId()); 1259 if (other2distance == null) { 1260 other2distance = new HashMap<Long, Integer>(); 1261 iDistanceCache.put(r1.getId(), other2distance); 1262 } 1263 Integer distance = other2distance.get(r2.getId()); 1264 if (distance == null) { 1265 distance = iDistanceMetric.getDistanceInMinutes(r1.getId(), r1.getPosX(), r1.getPosY(), r2.getId(), r2.getPosX(), r2.getPosY()); 1266 other2distance.put(r2.getId(), distance); 1267 } 1268 return distance; 1269 } 1270 1271 public int getDistanceInMinutes(Placement p1, Placement p2) { 1272 if (p1.isMultiRoom()) { 1273 if (p2.isMultiRoom()) { 1274 int dist = 0; 1275 for (RoomLocation r1 : p1.getRoomLocations()) { 1276 for (RoomLocation r2 : p2.getRoomLocations()) { 1277 dist = Math.max(dist, getDistanceInMinutes(r1, r2)); 1278 } 1279 } 1280 return dist; 1281 } else { 1282 if (p2.getRoomLocation() == null) 1283 return 0; 1284 int dist = 0; 1285 for (RoomLocation r1 : p1.getRoomLocations()) { 1286 dist = Math.max(dist, getDistanceInMinutes(r1, p2.getRoomLocation())); 1287 } 1288 return dist; 1289 } 1290 } else if (p2.isMultiRoom()) { 1291 if (p1.getRoomLocation() == null) 1292 return 0; 1293 int dist = 0; 1294 for (RoomLocation r2 : p2.getRoomLocations()) { 1295 dist = Math.max(dist, getDistanceInMinutes(p1.getRoomLocation(), r2)); 1296 } 1297 return dist; 1298 } else { 1299 if (p1.getRoomLocation() == null || p2.getRoomLocation() == null) 1300 return 0; 1301 return getDistanceInMinutes(p1.getRoomLocation(), p2.getRoomLocation()); 1302 } 1303 } 1304 } 1305 1306 /** 1307 * Assignment context 1308 */ 1309 public class StudentQualityContext implements AssignmentConstraintContext<Request, Enrollment> { 1310 private int[] iTotalPenalty = null; 1311 private Set<Conflict>[] iAllConflicts = null; 1312 private Request iOldVariable = null; 1313 private Enrollment iUnassignedValue = null; 1314 1315 @SuppressWarnings("unchecked") 1316 public StudentQualityContext(Assignment<Request, Enrollment> assignment) { 1317 iTotalPenalty = new int[Type.values().length]; 1318 for (Type t: iContext.getTypes()) 1319 iTotalPenalty[t.ordinal()] = countTotalPenalty(t, assignment); 1320 if (iContext.isDebug()) { 1321 iAllConflicts = new Set[Type.values().length]; 1322 for (Type t: iContext.getTypes()) 1323 iAllConflicts[t.ordinal()] = computeAllConflicts(t, assignment); 1324 } 1325 StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment); 1326 for (Type t: iContext.getTypes()) 1327 for (Conflict c: computeAllConflicts(t, assignment)) cx.add(assignment, c); 1328 } 1329 1330 @SuppressWarnings("unchecked") 1331 public StudentQualityContext(StudentQualityContext parent) { 1332 iTotalPenalty = new int[Type.values().length]; 1333 for (Type t: iContext.getTypes()) 1334 iTotalPenalty[t.ordinal()] = parent.iTotalPenalty[t.ordinal()]; 1335 if (iContext.isDebug()) { 1336 iAllConflicts = new Set[Type.values().length]; 1337 for (Type t: iContext.getTypes()) 1338 iAllConflicts[t.ordinal()] = new HashSet<Conflict>(parent.iAllConflicts[t.ordinal()]); 1339 } 1340 } 1341 1342 @Override 1343 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment value) { 1344 StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment); 1345 for (Type type: iContext.getTypes()) { 1346 iTotalPenalty[type.ordinal()] += allPenalty(type, assignment, value); 1347 for (Conflict c: allConflicts(type, assignment, value)) 1348 cx.add(assignment, c); 1349 } 1350 if (iContext.isDebug()) { 1351 sLog.debug("A:" + value.variable() + " := " + value); 1352 for (Type type: iContext.getTypes()) { 1353 int inc = allPenalty(type, assignment, value); 1354 if (inc != 0) { 1355 sLog.debug("-- " + type + " +" + inc + " A: " + value.variable() + " := " + value); 1356 for (Conflict c: allConflicts(type, assignment, value)) { 1357 sLog.debug(" -- " + c); 1358 iAllConflicts[type.ordinal()].add(c); 1359 inc -= c.getPenalty(); 1360 } 1361 if (inc != 0) { 1362 sLog.error(type + ": Different penalty for the assigned value (difference: " + inc + ")!"); 1363 } 1364 } 1365 } 1366 } 1367 } 1368 1369 /** 1370 * Called when a value is unassigned from a variable. Internal number of 1371 * time overlapping conflicts is updated, see 1372 * {@link TimeOverlapsCounter#getTotalNrConflicts(Assignment)}. 1373 */ 1374 @Override 1375 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment value) { 1376 StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment); 1377 for (Type type: iContext.getTypes()) { 1378 iTotalPenalty[type.ordinal()] -= allPenalty(type, assignment, value); 1379 for (Conflict c: allConflicts(type, assignment, value)) 1380 cx.remove(assignment, c); 1381 } 1382 if (iContext.isDebug()) { 1383 sLog.debug("U:" + value.variable() + " := " + value); 1384 for (Type type: iContext.getTypes()) { 1385 int dec = allPenalty(type, assignment, value); 1386 if (dec != 0) { 1387 sLog.debug("-- " + type + " -" + dec + " U: " + value.variable() + " := " + value); 1388 for (Conflict c: allConflicts(type, assignment, value)) { 1389 sLog.debug(" -- " + c); 1390 iAllConflicts[type.ordinal()].remove(c); 1391 dec -= c.getPenalty(); 1392 } 1393 if (dec != 0) { 1394 sLog.error(type + ":Different penalty for the unassigned value (difference: " + dec + ")!"); 1395 } 1396 } 1397 } 1398 } 1399 } 1400 1401 /** 1402 * Called before a value is assigned to a variable. 1403 * @param assignment current assignment 1404 * @param iteration current iteration 1405 * @param value value to be assigned 1406 */ 1407 public void beforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1408 if (value != null) { 1409 Enrollment old = assignment.getValue(value.variable()); 1410 if (old != null) { 1411 iUnassignedValue = old; 1412 unassigned(assignment, old); 1413 } 1414 iOldVariable = value.variable(); 1415 } 1416 } 1417 1418 /** 1419 * Called after a value is assigned to a variable. 1420 * @param assignment current assignment 1421 * @param iteration current iteration 1422 * @param value value that was assigned 1423 */ 1424 public void afterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1425 iOldVariable = null; 1426 iUnassignedValue = null; 1427 if (value != null) { 1428 assigned(assignment, value); 1429 } 1430 } 1431 1432 /** 1433 * Called after a value is unassigned from a variable. 1434 * @param assignment current assignment 1435 * @param iteration current iteration 1436 * @param value value that was unassigned 1437 */ 1438 public void afterUnassigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) { 1439 if (value != null && !value.equals(iUnassignedValue)) { 1440 unassigned(assignment, value); 1441 } 1442 } 1443 1444 public Set<Conflict> getAllConflicts(Type type) { 1445 return iAllConflicts[type.ordinal()]; 1446 } 1447 1448 public int getTotalPenalty(Type type) { 1449 return iTotalPenalty[type.ordinal()]; 1450 } 1451 1452 public void checkTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) { 1453 int total = countTotalPenalty(type, assignment); 1454 if (total != iTotalPenalty[type.ordinal()]) { 1455 sLog.error(type + " penalty does not match for (actual: " + total + ", count: " + iTotalPenalty[type.ordinal()] + ")!"); 1456 iTotalPenalty[type.ordinal()] = total; 1457 if (iContext.isDebug()) { 1458 Set<Conflict> conflicts = computeAllConflicts(type, assignment); 1459 for (Conflict c: conflicts) { 1460 if (!iAllConflicts[type.ordinal()].contains(c)) 1461 sLog.debug(" +add+ " + c); 1462 } 1463 for (Conflict c: iAllConflicts[type.ordinal()]) { 1464 if (!conflicts.contains(c)) 1465 sLog.debug(" -rem- " + c); 1466 } 1467 for (Conflict c: conflicts) { 1468 for (Conflict d: iAllConflicts[type.ordinal()]) { 1469 if (c.equals(d) && c.getPenalty() != d.getPenalty()) { 1470 sLog.debug(" -dif- " + c + " (other: " + d.getPenalty() + ")"); 1471 } 1472 } 1473 } 1474 iAllConflicts[type.ordinal()] = conflicts; 1475 } 1476 } 1477 } 1478 1479 public int countTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) { 1480 int total = 0; 1481 for (Request r1 : getModel().variables()) { 1482 Enrollment e1 = assignment.getValue(r1); 1483 if (e1 == null || r1.equals(iOldVariable)) continue; 1484 for (Request r2 : r1.getStudent().getRequests()) { 1485 Enrollment e2 = assignment.getValue(r2); 1486 if (e2 != null && r1.getId() < r2.getId() && !r2.equals(iOldVariable)) { 1487 if (type.isApplicable(iContext, r1.getStudent(), r1, r2)) 1488 total += penalty(type, e1, e2); 1489 } 1490 } 1491 total += penalty(type, e1); 1492 } 1493 return total; 1494 } 1495 1496 public Set<Conflict> computeAllConflicts(Type type, Assignment<Request, Enrollment> assignment) { 1497 Set<Conflict> ret = new HashSet<Conflict>(); 1498 for (Request r1 : getModel().variables()) { 1499 Enrollment e1 = assignment.getValue(r1); 1500 if (e1 == null || r1.equals(iOldVariable)) continue; 1501 for (Request r2 : r1.getStudent().getRequests()) { 1502 Enrollment e2 = assignment.getValue(r2); 1503 if (e2 != null && r1.getId() < r2.getId() && !r2.equals(iOldVariable)) { 1504 if (type.isApplicable(iContext, r1.getStudent(), r1, r2)) 1505 ret.addAll(conflicts(type, e1, e2)); 1506 } 1507 } 1508 ret.addAll(conflicts(type, e1)); 1509 } 1510 return ret; 1511 } 1512 1513 public Set<Conflict> allConflicts(Type type, Assignment<Request, Enrollment> assignment, Student student) { 1514 Set<Conflict> ret = new HashSet<Conflict>(); 1515 for (Request r1 : student.getRequests()) { 1516 Enrollment e1 = assignment.getValue(r1); 1517 if (e1 == null) continue; 1518 for (Request r2 : student.getRequests()) { 1519 Enrollment e2 = assignment.getValue(r2); 1520 if (e2 != null && r1.getId() < r2.getId()) { 1521 if (type.isApplicable(iContext, r1.getStudent(), r1, r2)) 1522 ret.addAll(conflicts(type, e1, e2)); 1523 } 1524 } 1525 ret.addAll(conflicts(type, e1)); 1526 } 1527 return ret; 1528 } 1529 1530 public Set<Conflict> allConflicts(Type type, Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 1531 Set<Conflict> ret = new HashSet<Conflict>(); 1532 for (Request request : enrollment.getStudent().getRequests()) { 1533 if (request.equals(enrollment.getRequest())) continue; 1534 if (assignment.getValue(request) != null && !request.equals(iOldVariable)) { 1535 ret.addAll(conflicts(type, enrollment, assignment.getValue(request))); 1536 } 1537 } 1538 ret.addAll(conflicts(type, enrollment)); 1539 return ret; 1540 } 1541 1542 public int allPenalty(Type type, Assignment<Request, Enrollment> assignment, Student student) { 1543 int penalty = 0; 1544 for (Request r1 : student.getRequests()) { 1545 Enrollment e1 = assignment.getValue(r1); 1546 if (e1 == null) continue; 1547 for (Request r2 : student.getRequests()) { 1548 Enrollment e2 = assignment.getValue(r2); 1549 if (e2 != null && r1.getId() < r2.getId()) { 1550 if (type.isApplicable(iContext, r1.getStudent(), r1, r2)) 1551 penalty += penalty(type, e1, e2); 1552 } 1553 } 1554 penalty += penalty(type, e1); 1555 } 1556 return penalty; 1557 } 1558 1559 public int allPenalty(Type type, Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 1560 int penalty = 0; 1561 for (Request request : enrollment.getStudent().getRequests()) { 1562 if (request.equals(enrollment.getRequest())) continue; 1563 if (assignment.getValue(request) != null && !request.equals(iOldVariable)) { 1564 if (type.isApplicable(iContext, enrollment.getStudent(), enrollment.variable(), request)) 1565 penalty += penalty(type, enrollment, assignment.getValue(request)); 1566 } 1567 } 1568 penalty += penalty(type, enrollment); 1569 return penalty; 1570 } 1571 } 1572 1573 @Override 1574 public StudentQualityContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 1575 return new StudentQualityContext(assignment); 1576 } 1577 1578 @Override 1579 public StudentQualityContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, StudentQualityContext parentContext) { 1580 return new StudentQualityContext(parentContext); 1581 } 1582 1583 /** Empty iterator */ 1584 public static class Nothing implements Iterable<SctAssignment> { 1585 @Override 1586 public Iterator<SctAssignment> iterator() { 1587 return new Iterator<SctAssignment>() { 1588 @Override 1589 public SctAssignment next() { return null; } 1590 @Override 1591 public boolean hasNext() { return false; } 1592 @Override 1593 public void remove() { throw new UnsupportedOperationException(); } 1594 }; 1595 } 1596 } 1597 1598 /** Unavailabilities of a student */ 1599 public static class Unavailabilities implements Iterable<Unavailability> { 1600 private Student iStudent; 1601 public Unavailabilities(Student student) { iStudent = student; } 1602 @Override 1603 public Iterator<Unavailability> iterator() { return iStudent.getUnavailabilities().iterator(); } 1604 } 1605 1606 private static class SingleTime implements SctAssignment { 1607 private TimeLocation iTime = null; 1608 1609 public SingleTime(int start, int end) { 1610 iTime = new TimeLocation(0x7f, start, end-start, 0, 0.0, 0, null, null, new BitSet(), 0); 1611 } 1612 1613 @Override 1614 public TimeLocation getTime() { return iTime; } 1615 @Override 1616 public List<RoomLocation> getRooms() { return null; } 1617 @Override 1618 public int getNrRooms() { return 0; } 1619 @Override 1620 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {} 1621 @Override 1622 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {} 1623 @Override 1624 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { return null; } 1625 @Override 1626 public boolean isAllowOverlap() { return false; } 1627 @Override 1628 public long getId() { return -1;} 1629 @Override 1630 public int compareById(SctAssignment a) { return 0; } 1631 1632 @Override 1633 public boolean isOverlapping(SctAssignment assignment) { 1634 return assignment.getTime() != null && getTime().shareDays(assignment.getTime()) && getTime().shareHours(assignment.getTime()); 1635 } 1636 1637 @Override 1638 public boolean isOverlapping(Set<? extends SctAssignment> assignments) { 1639 for (SctAssignment assignment : assignments) { 1640 if (isOverlapping(assignment)) return true; 1641 } 1642 return false; 1643 } 1644 } 1645 1646 /** Early/late time */ 1647 public static class SingleTimeIterable implements Iterable<SingleTime> { 1648 private SingleTime iTime = null; 1649 public SingleTimeIterable(int start, int end) { 1650 if (start < end) 1651 iTime = new SingleTime(start, end); 1652 1653 } 1654 @Override 1655 public Iterator<SingleTime> iterator() { 1656 return new Iterator<SingleTime>() { 1657 @Override 1658 public SingleTime next() { 1659 SingleTime ret = iTime; iTime = null; return ret; 1660 } 1661 @Override 1662 public boolean hasNext() { return iTime != null; } 1663 @Override 1664 public void remove() { throw new UnsupportedOperationException(); } 1665 }; 1666 } 1667 } 1668 1669 /** Free times of a student */ 1670 public static class FreeTimes implements Iterable<FreeTimeRequest> { 1671 private Student iStudent; 1672 public FreeTimes(Student student) { 1673 iStudent = student; 1674 } 1675 1676 @Override 1677 public Iterator<FreeTimeRequest> iterator() { 1678 return new Iterator<FreeTimeRequest>() { 1679 Iterator<Request> i = iStudent.getRequests().iterator(); 1680 FreeTimeRequest next = null; 1681 boolean hasNext = nextFreeTime(); 1682 1683 private boolean nextFreeTime() { 1684 while (i.hasNext()) { 1685 Request r = i.next(); 1686 if (r instanceof FreeTimeRequest) { 1687 next = (FreeTimeRequest)r; 1688 return true; 1689 } 1690 } 1691 return false; 1692 } 1693 1694 @Override 1695 public FreeTimeRequest next() { 1696 try { 1697 return next; 1698 } finally { 1699 hasNext = nextFreeTime(); 1700 } 1701 } 1702 @Override 1703 public boolean hasNext() { return hasNext; } 1704 @Override 1705 public void remove() { throw new UnsupportedOperationException(); } 1706 }; 1707 } 1708 } 1709 1710 /** Online (or not-online) classes of an enrollment */ 1711 public static class Online implements Iterable<Section> { 1712 private Enrollment iEnrollment; 1713 private boolean iOnline; 1714 public Online(Enrollment enrollment, boolean online) { 1715 iEnrollment = enrollment; 1716 iOnline = online; 1717 } 1718 1719 protected boolean skip(Section section) { 1720 return iOnline != section.isOnline(); 1721 } 1722 1723 @Override 1724 public Iterator<Section> iterator() { 1725 return new Iterator<Section>() { 1726 Iterator<Section> i = iEnrollment.getSections().iterator(); 1727 Section next = null; 1728 boolean hasNext = nextSection(); 1729 1730 private boolean nextSection() { 1731 while (i.hasNext()) { 1732 Section r = i.next(); 1733 if (!skip(r)) { 1734 next = r; 1735 return true; 1736 } 1737 } 1738 return false; 1739 } 1740 1741 @Override 1742 public Section next() { 1743 try { 1744 return next; 1745 } finally { 1746 hasNext = nextSection(); 1747 } 1748 } 1749 @Override 1750 public boolean hasNext() { return hasNext; } 1751 @Override 1752 public void remove() { throw new UnsupportedOperationException(); } 1753 }; 1754 } 1755 } 1756 1757 @Override 1758 public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) { 1759 StudentQualityContext cx = getContext(assignment); 1760 if (iContext.isDebug()) 1761 for (Type type: iContext.getTypes()) 1762 info.put("[Schedule Quality] " + type.getName(), String.valueOf(cx.getTotalPenalty(type))); 1763 } 1764 1765 @Override 1766 public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) { 1767 } 1768 1769 public String toString(Assignment<Request, Enrollment> assignment) { 1770 String ret = ""; 1771 StudentQualityContext cx = getContext(assignment); 1772 for (Type type: iContext.getTypes()) { 1773 int p = cx.getTotalPenalty(type); 1774 if (p != 0) { 1775 ret += (ret.isEmpty() ? "" : ", ") + type.getAbbv() + ": " + p; 1776 } 1777 } 1778 return ret; 1779 } 1780 1781 public boolean hasDistanceConflict(Student student, Section s1, Section s2) { 1782 if (student.isNeedShortDistances()) 1783 return Type.ShortDistance.inConflict(iContext, s1, s2); 1784 else 1785 return Type.Distance.inConflict(iContext, s1, s2); 1786 } 1787}