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