001 package net.sf.cpsolver.exam.model; 002 003 import java.text.DecimalFormat; 004 import java.util.HashSet; 005 import java.util.Iterator; 006 import java.util.Set; 007 008 import net.sf.cpsolver.ifs.model.Value; 009 010 /** 011 * Representation of an exam placement (problem value), i.e., assignment of an 012 * exam to period and room(s). Each placement has defined a period and a set of 013 * rooms. The exam as well as the rooms have to be available during the given 014 * period (see {@link Exam#getPeriodPlacements()} and 015 * {@link Exam#getRoomPlacements()}). The total size of rooms have to be equal 016 * or greater than the number of students enrolled in the exam 017 * {@link Exam#getSize()}, using either {@link ExamRoom#getSize()} or 018 * {@link ExamRoom#getAltSize()}, depending on {@link Exam#hasAltSeating()}. 019 * Also, the number of rooms has to be smaller or equal to 020 * {@link Exam#getMaxRooms()}. If {@link Exam#getMaxRooms()} is zero, the exam 021 * is only to be assigned to period (the set of rooms is empty). <br> 022 * <br> 023 * The cost of an assignment consists of the following criteria: 024 * <ul> 025 * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()}, 026 * weighted by {@link ExamModel#getDirectConflictWeight()} 027 * <li>More than two exams a day student conflicts 028 * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by 029 * {@link ExamModel#getMoreThanTwoADayWeight()} 030 * <li>Back-to-back student conflicts 031 * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by 032 * {@link ExamModel#getBackToBackConflictWeight()} 033 * <li>Distance back-to-back student conflicts 034 * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by 035 * {@link ExamModel#getDistanceBackToBackConflictWeight()} 036 * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by 037 * {@link ExamModel#getPeriodWeight()} 038 * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()}, weighted by 039 * {@link ExamModel#getRoomSizeWeight()} 040 * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()}, weighted 041 * by {@link ExamModel#getRoomSplitWeight()} 042 * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by 043 * {@link ExamModel#getRoomWeight()} 044 * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()}, 045 * weighted by {@link ExamModel#getExamRotationWeight()} 046 * <li>Direct instructor conflicts 047 * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by 048 * {@link ExamModel#getInstructorDirectConflictWeight()} 049 * <li>More than two exams a day instructor conflicts 050 * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted by 051 * {@link ExamModel#getInstructorMoreThanTwoADayWeight()} 052 * <li>Back-to-back instructor conflicts 053 * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by 054 * {@link ExamModel#getInstructorBackToBackConflictWeight()} 055 * <li>Distance back-to-back instructor conflicts 056 * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()}, weighted 057 * by {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()} 058 * </ul> 059 * <br> 060 * <br> 061 * 062 * @version ExamTT 1.2 (Examination Timetabling)<br> 063 * Copyright (C) 2008 - 2010 Tomas Muller<br> 064 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 065 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 066 * <br> 067 * This library is free software; you can redistribute it and/or modify 068 * it under the terms of the GNU Lesser General Public License as 069 * published by the Free Software Foundation; either version 3 of the 070 * License, or (at your option) any later version. <br> 071 * <br> 072 * This library is distributed in the hope that it will be useful, but 073 * WITHOUT ANY WARRANTY; without even the implied warranty of 074 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 075 * Lesser General Public License for more details. <br> 076 * <br> 077 * You should have received a copy of the GNU Lesser General Public 078 * License along with this library; if not see 079 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 080 */ 081 public class ExamPlacement extends Value<Exam, ExamPlacement> { 082 private ExamPeriodPlacement iPeriodPlacement; 083 private Set<ExamRoomPlacement> iRoomPlacements; 084 private int iSize; 085 private int iRoomPenalty; 086 private double iRoomSplitDistance; 087 088 private int iHashCode; 089 090 /** 091 * Constructor 092 * 093 * @param exam 094 * an exam 095 * @param periodPlacement 096 * period placement 097 * @param roomPlacements 098 * a set of room placements {@link ExamRoomPlacement} 099 */ 100 public ExamPlacement(Exam exam, ExamPeriodPlacement periodPlacement, Set<ExamRoomPlacement> roomPlacements) { 101 super(exam); 102 iPeriodPlacement = periodPlacement; 103 if (roomPlacements == null) 104 iRoomPlacements = new HashSet<ExamRoomPlacement>(); 105 else 106 iRoomPlacements = roomPlacements; 107 iSize = 0; 108 iRoomPenalty = 0; 109 iRoomSplitDistance = 0.0; 110 for (ExamRoomPlacement r : iRoomPlacements) { 111 iSize += r.getSize(exam.hasAltSeating()); 112 iRoomPenalty += r.getPenalty(periodPlacement.getPeriod()); 113 if (iRoomPlacements.size() > 1) { 114 for (ExamRoomPlacement w : iRoomPlacements) { 115 if (r.getRoom().getId() < w.getRoom().getId()) 116 iRoomSplitDistance += r.getRoom().getDistanceInMeters(w.getRoom()); 117 } 118 } 119 } 120 if (iRoomPlacements.size() > 2) { 121 iRoomSplitDistance /= iRoomPlacements.size() * (iRoomPlacements.size() - 1) / 2; 122 } 123 iHashCode = getName().hashCode(); 124 } 125 126 /** 127 * Assigned period 128 */ 129 public ExamPeriod getPeriod() { 130 return iPeriodPlacement.getPeriod(); 131 } 132 133 /** 134 * Assigned period placement 135 */ 136 public ExamPeriodPlacement getPeriodPlacement() { 137 return iPeriodPlacement; 138 } 139 140 /** 141 * Assigned rooms (it is empty when {@link Exam#getMaxRooms()} is zero) 142 * 143 * @return list of {@link ExamRoomPlacement} 144 */ 145 public Set<ExamRoomPlacement> getRoomPlacements() { 146 return iRoomPlacements; 147 } 148 149 /** 150 * Overall size of assigned rooms 151 */ 152 public int getSize() { 153 return iSize; 154 } 155 156 /** 157 * Number of direct student conflicts, i.e., number of cases when this exam 158 * is attended by a student that attends some other exam at the same period 159 */ 160 public int getNrDirectConflicts() { 161 Exam exam = variable(); 162 // if (!exam.isAllowDirectConflicts()) return 0; 163 int penalty = 0; 164 for (ExamStudent s : exam.getStudents()) { 165 Set<Exam> exams = s.getExams(getPeriod()); 166 int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1); 167 if (nrExams > 1) 168 penalty++; 169 else if (!s.isAvailable(getPeriod())) 170 penalty++; 171 } 172 return penalty; 173 } 174 175 /** 176 * Number of direct student conflicts caused by the fact that a student is 177 * not available 178 */ 179 public int getNrNotAvailableConflicts() { 180 Exam exam = variable(); 181 int penalty = 0; 182 for (ExamStudent s : exam.getStudents()) { 183 if (!s.isAvailable(getPeriod())) 184 penalty++; 185 } 186 return penalty; 187 } 188 189 /** 190 * Number of back-to-back student conflicts, i.e., number of cases when this 191 * exam is attended by a student that attends some other exam at the 192 * previous {@link ExamPeriod#prev()} or following {@link ExamPeriod#next()} 193 * period. If {@link ExamModel#isDayBreakBackToBack()} is false, 194 * back-to-back conflicts are only considered between consecutive periods 195 * that are of the same day. 196 */ 197 public int getNrBackToBackConflicts() { 198 Exam exam = variable(); 199 ExamModel model = (ExamModel) exam.getModel(); 200 int penalty = 0; 201 for (ExamStudent s : exam.getStudents()) { 202 if (getPeriod().prev() != null) { 203 if (model.isDayBreakBackToBack() || getPeriod().prev().getDay() == getPeriod().getDay()) { 204 Set<Exam> exams = s.getExams(getPeriod().prev()); 205 int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0); 206 penalty += nrExams; 207 } 208 } 209 if (getPeriod().next() != null) { 210 if (model.isDayBreakBackToBack() || getPeriod().next().getDay() == getPeriod().getDay()) { 211 Set<Exam> exams = s.getExams(getPeriod().next()); 212 int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0); 213 penalty += nrExams; 214 } 215 } 216 } 217 return penalty; 218 } 219 220 /** 221 * Distance between two placements, i.e., maximal distance between a room of 222 * this placement and a room of the given placement. Method 223 * {@link ExamRoom#getDistanceInMeters(ExamRoom)} is used to get a distance between 224 * two rooms. 225 */ 226 public double getDistanceInMeters(ExamPlacement other) { 227 if (getRoomPlacements().isEmpty() || other.getRoomPlacements().isEmpty()) 228 return 0; 229 double maxDistance = 0; 230 for (ExamRoomPlacement r1 : getRoomPlacements()) { 231 for (ExamRoomPlacement r2 : other.getRoomPlacements()) { 232 maxDistance = Math.max(maxDistance, r1.getDistanceInMeters(r2)); 233 } 234 } 235 return maxDistance; 236 } 237 238 /** 239 * Number of back-to-back distance student conflicts, i.e., number of cases 240 * when this exam is attended by a student that attends some other exam at 241 * the previous {@link ExamPeriod#prev()} or following 242 * {@link ExamPeriod#next()} period and the distance 243 * {@link ExamPlacement#getDistanceInMeters(ExamPlacement)} between these two exams 244 * is greater than {@link ExamModel#getBackToBackDistance()}. Distance 245 * back-to-back conflicts are only considered between consecutive periods 246 * that are of the same day. 247 */ 248 public int getNrDistanceBackToBackConflicts() { 249 Exam exam = variable(); 250 ExamModel model = (ExamModel) exam.getModel(); 251 double btbDist = model.getBackToBackDistance(); 252 if (btbDist < 0) 253 return 0; 254 int penalty = 0; 255 for (ExamStudent s : exam.getStudents()) { 256 if (getPeriod().prev() != null) { 257 if (getPeriod().prev().getDay() == getPeriod().getDay()) { 258 for (Exam x : s.getExams(getPeriod().prev())) { 259 if (x.equals(exam)) 260 continue; 261 if (getDistanceInMeters(x.getAssignment()) > btbDist) 262 penalty++; 263 } 264 } 265 } 266 if (getPeriod().next() != null) { 267 if (getPeriod().next().getDay() == getPeriod().getDay()) { 268 for (Exam x : s.getExams(getPeriod().next())) { 269 if (x.equals(exam)) 270 continue; 271 if (getDistanceInMeters(x.getAssignment()) > btbDist) 272 penalty++; 273 } 274 } 275 } 276 } 277 return penalty; 278 } 279 280 /** 281 * Number of more than two exams a day student conflicts, i.e., when this 282 * exam is attended by a student that attends two or more other exams at the 283 * same day. 284 */ 285 public int getNrMoreThanTwoADayConflicts() { 286 Exam exam = variable(); 287 int penalty = 0; 288 for (ExamStudent s : exam.getStudents()) { 289 Set<Exam> exams = s.getExamsADay(getPeriod()); 290 int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1); 291 if (nrExams > 2) 292 penalty++; 293 } 294 return penalty; 295 } 296 297 /** 298 * Number of direct instructor conflicts, i.e., number of cases when this 299 * exam is attended by an instructor that attends some other exam at the 300 * same period 301 */ 302 public int getNrInstructorDirectConflicts() { 303 Exam exam = variable(); 304 // if (!exam.isAllowDirectConflicts()) return 0; 305 int penalty = 0; 306 for (ExamInstructor s : exam.getInstructors()) { 307 Set<Exam> exams = s.getExams(getPeriod()); 308 int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1); 309 if (nrExams > 1) 310 penalty++; 311 else if (!s.isAvailable(getPeriod())) 312 penalty++; 313 } 314 return penalty; 315 } 316 317 /** 318 * Number of direct instructor conflicts caused by the fact that a student 319 * is not available 320 */ 321 public int getNrInstructorNotAvailableConflicts() { 322 Exam exam = variable(); 323 int penalty = 0; 324 for (ExamInstructor s : exam.getInstructors()) { 325 if (!s.isAvailable(getPeriod())) 326 penalty++; 327 } 328 return penalty; 329 } 330 331 /** 332 * Number of back-to-back instructor conflicts, i.e., number of cases when 333 * this exam is attended by an instructor that attends some other exam at 334 * the previous {@link ExamPeriod#prev()} or following 335 * {@link ExamPeriod#next()} period. If 336 * {@link ExamModel#isDayBreakBackToBack()} is false, back-to-back conflicts 337 * are only considered between consecutive periods that are of the same day. 338 */ 339 public int getNrInstructorBackToBackConflicts() { 340 Exam exam = variable(); 341 ExamModel model = (ExamModel) exam.getModel(); 342 int penalty = 0; 343 for (ExamInstructor s : exam.getInstructors()) { 344 if (getPeriod().prev() != null) { 345 if (model.isDayBreakBackToBack() || getPeriod().prev().getDay() == getPeriod().getDay()) { 346 Set<Exam> exams = s.getExams(getPeriod().prev()); 347 int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0); 348 penalty += nrExams; 349 } 350 } 351 if (getPeriod().next() != null) { 352 if (model.isDayBreakBackToBack() || getPeriod().next().getDay() == getPeriod().getDay()) { 353 Set<Exam> exams = s.getExams(getPeriod().next()); 354 int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0); 355 penalty += nrExams; 356 } 357 } 358 } 359 return penalty; 360 } 361 362 /** 363 * Number of back-to-back distance instructor conflicts, i.e., number of 364 * cases when this exam is attended by an instructor that attends some other 365 * exam at the previous {@link ExamPeriod#prev()} or following 366 * {@link ExamPeriod#next()} period and the distance 367 * {@link ExamPlacement#getDistanceInMeters(ExamPlacement)} between these two exams 368 * is greater than {@link ExamModel#getBackToBackDistance()}. Distance 369 * back-to-back conflicts are only considered between consecutive periods 370 * that are of the same day. 371 */ 372 public int getNrInstructorDistanceBackToBackConflicts() { 373 Exam exam = variable(); 374 ExamModel model = (ExamModel) exam.getModel(); 375 double btbDist = model.getBackToBackDistance(); 376 if (btbDist < 0) 377 return 0; 378 int penalty = 0; 379 for (ExamInstructor s : exam.getInstructors()) { 380 if (getPeriod().prev() != null) { 381 if (getPeriod().prev().getDay() == getPeriod().getDay()) { 382 for (Exam x : s.getExams(getPeriod().prev())) { 383 if (x.equals(exam)) 384 continue; 385 if (getDistanceInMeters(x.getAssignment()) > btbDist) 386 penalty++; 387 } 388 } 389 } 390 if (getPeriod().next() != null) { 391 if (getPeriod().next().getDay() == getPeriod().getDay()) { 392 for (Exam x : s.getExams(getPeriod().next())) { 393 if (x.equals(exam)) 394 continue; 395 if (getDistanceInMeters(x.getAssignment()) > btbDist) 396 penalty++; 397 } 398 } 399 } 400 } 401 return penalty; 402 } 403 404 /** 405 * Number of more than two exams a day instructor conflicts, i.e., when this 406 * exam is attended by an instructor student that attends two or more other 407 * exams at the same day. 408 */ 409 public int getNrInstructorMoreThanTwoADayConflicts() { 410 Exam exam = variable(); 411 int penalty = 0; 412 for (ExamInstructor s : exam.getInstructors()) { 413 Set<Exam> exams = s.getExamsADay(getPeriod()); 414 int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1); 415 if (nrExams > 2) 416 penalty++; 417 } 418 return penalty; 419 } 420 421 /** 422 * Cost for using room(s) that are too big 423 * 424 * @return difference between total room size (computed using either 425 * {@link ExamRoom#getSize()} or {@link ExamRoom#getAltSize()} based 426 * on {@link Exam#hasAltSeating()}) and the number of students 427 * {@link Exam#getSize()} 428 */ 429 public int getRoomSizePenalty() { 430 Exam exam = variable(); 431 int diff = getSize() - exam.getSize(); 432 return (diff < 0 ? 0 : diff); 433 } 434 435 /** 436 * Cost for using more than one room (nrSplits^2). 437 * 438 * @return penalty (1 for 2 rooms, 4 for 3 rooms, 9 for 4 rooms, etc.) 439 */ 440 public int getRoomSplitPenalty() { 441 return (iRoomPlacements.size() <= 1 ? 0 : (iRoomPlacements.size() - 1) * (iRoomPlacements.size() - 1)); 442 } 443 444 /** 445 * Cost for using a period, i.e., {@link ExamPeriodPlacement#getPenalty()} 446 */ 447 public int getPeriodPenalty() { 448 return iPeriodPlacement.getPenalty(); 449 } 450 451 /** 452 * Rotation penalty (an exam that has been in later period last times tries 453 * to be in an earlier period) 454 */ 455 public int getRotationPenalty() { 456 Exam exam = variable(); 457 if (exam.getAveragePeriod() < 0) 458 return 0; 459 return (1 + getPeriod().getIndex()) * (1 + exam.getAveragePeriod()); 460 } 461 462 /** 463 * Front load penalty (large exam is discouraged to be placed on or after a 464 * certain period) 465 * 466 * @return zero if not large exam or if before 467 * {@link ExamModel#getLargePeriod()}, one otherwise 468 */ 469 public int getLargePenalty() { 470 Exam exam = variable(); 471 ExamModel model = (ExamModel) exam.getModel(); 472 if (model.getLargeSize() < 0 || exam.getSize() < model.getLargeSize()) 473 return 0; 474 int periodIdx = (int) Math.round(model.getPeriods().size() * model.getLargePeriod()); 475 return (getPeriod().getIndex() < periodIdx ? 0 : 1); 476 } 477 478 /** 479 * Room penalty (penalty for using given rooms), i.e., sum of 480 * {@link ExamRoomPlacement#getPenalty(ExamPeriod)} of assigned rooms 481 */ 482 public int getRoomPenalty() { 483 return iRoomPenalty; 484 } 485 486 /** 487 * Perturbation penalty, i.e., penalty for using a different assignment than 488 * initial. Only applicable when {@link ExamModel#isMPP()} is true (minimal 489 * perturbation problem). 490 * 491 * @return |period index - initial period index | * exam size 492 */ 493 public int getPerturbationPenalty() { 494 Exam exam = variable(); 495 if (!((ExamModel) exam.getModel()).isMPP()) 496 return 0; 497 ExamPlacement initial = exam.getInitialAssignment(); 498 if (initial == null) 499 return 0; 500 return Math.abs(initial.getPeriod().getIndex() - getPeriod().getIndex()) * (1 + exam.getSize()); 501 } 502 503 /** 504 * Room perturbation penalty, i.e., number of assigned rooms different from 505 * initial. Only applicable when {@link ExamModel#isMPP()} is true (minimal 506 * perturbation problem). 507 * 508 * @return |period index - initial period index | * exam size 509 */ 510 public int getRoomPerturbationPenalty() { 511 Exam exam = variable(); 512 if (!((ExamModel) exam.getModel()).isMPP()) 513 return 0; 514 ExamPlacement initial = exam.getInitialAssignment(); 515 if (initial == null) 516 return 0; 517 int penalty = 0; 518 for (ExamRoomPlacement rp : getRoomPlacements()) { 519 if (!initial.getRoomPlacements().contains(rp)) 520 penalty++; 521 } 522 return penalty; 523 } 524 525 /** 526 * Room split distance penalty, i.e., average distance between two rooms of 527 * this placement 528 */ 529 public double getRoomSplitDistancePenalty() { 530 return iRoomSplitDistance; 531 } 532 533 /** 534 * Distribution penalty, i.e., sum weights of violated distribution 535 * constraints 536 */ 537 public double getDistributionPenalty() { 538 int penalty = 0; 539 for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) { 540 if (dc.isHard()) 541 continue; 542 boolean sat = dc.isSatisfied(this); 543 if (sat != dc.isSatisfied()) 544 penalty += (sat ? -dc.getWeight() : dc.getWeight()); 545 } 546 return penalty; 547 } 548 549 /** 550 * Room related distribution penalty, i.e., sum weights of violated 551 * distribution constraints 552 */ 553 public double getRoomDistributionPenalty() { 554 int penalty = 0; 555 for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) { 556 if (dc.isHard() || !dc.isRoomRelated()) 557 continue; 558 boolean sat = dc.isSatisfied(this); 559 if (sat != dc.isSatisfied()) 560 penalty += (sat ? -dc.getWeight() : dc.getWeight()); 561 } 562 return penalty; 563 } 564 565 /** 566 * Period related distribution penalty, i.e., sum weights of violated 567 * distribution constraints 568 */ 569 public double getPeriodDistributionPenalty() { 570 int penalty = 0; 571 for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) { 572 if (dc.isHard() || !dc.isPeriodRelated()) 573 continue; 574 boolean sat = dc.isSatisfied(this); 575 if (sat != dc.isSatisfied()) 576 penalty += (sat ? -dc.getWeight() : dc.getWeight()); 577 } 578 return penalty; 579 } 580 581 /** 582 * Overall cost of using this placement. The cost of an assignment consists 583 * of the following criteria: 584 * <ul> 585 * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()} 586 * , weighted by {@link ExamModel#getDirectConflictWeight()} 587 * <li>More than two exams a day student conflicts 588 * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by 589 * {@link ExamModel#getMoreThanTwoADayWeight()} 590 * <li>Back-to-back student conflicts 591 * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by 592 * {@link ExamModel#getBackToBackConflictWeight()} 593 * <li>Distance back-to-back student conflicts 594 * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by 595 * {@link ExamModel#getDistanceBackToBackConflictWeight()} 596 * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by 597 * {@link ExamModel#getPeriodWeight()} 598 * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()}, 599 * weighted by {@link ExamModel#getRoomSizeWeight()} 600 * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()}, 601 * weighted by {@link ExamModel#getRoomSplitWeight()} 602 * <li>Room split distance penalty 603 * {@link ExamPlacement#getRoomSplitDistancePenalty()}, weighted by 604 * {@link ExamModel#getRoomSplitDistanceWeight()} 605 * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by 606 * {@link ExamModel#getRoomWeight()} 607 * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()}, 608 * weighted by {@link ExamModel#getExamRotationWeight()} 609 * <li>Direct instructor conflicts 610 * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by 611 * {@link ExamModel#getInstructorDirectConflictWeight()} 612 * <li>More than two exams a day instructor conflicts 613 * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted 614 * by {@link ExamModel#getInstructorMoreThanTwoADayWeight()} 615 * <li>Back-to-back instructor conflicts 616 * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by 617 * {@link ExamModel#getInstructorBackToBackConflictWeight()} 618 * <li>Distance back-to-back instructor conflicts 619 * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()}, 620 * weighted by 621 * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()} 622 * <li>Front load penalty {@link ExamPlacement#getLargePenalty()}, weighted 623 * by {@link ExamModel#getLargeWeight()} 624 * </ul> 625 */ 626 @Override 627 public double toDouble() { 628 Exam exam = variable(); 629 ExamModel model = (ExamModel) exam.getModel(); 630 return model.getDirectConflictWeight() * getNrDirectConflicts() + model.getMoreThanTwoADayWeight() 631 * getNrMoreThanTwoADayConflicts() + model.getBackToBackConflictWeight() * getNrBackToBackConflicts() 632 + model.getDistanceBackToBackConflictWeight() * getNrDistanceBackToBackConflicts() 633 + model.getPeriodWeight() * getPeriodPenalty() + model.getPeriodSizeWeight() * getPeriodPenalty() 634 * (exam.getSize() + 1) + model.getPeriodIndexWeight() * getPeriod().getIndex() 635 + model.getRoomSizeWeight() * getRoomSizePenalty() + model.getRoomSplitWeight() * getRoomSplitPenalty() 636 + model.getExamRotationWeight() * getRotationPenalty() + model.getRoomWeight() * getRoomPenalty() 637 + model.getInstructorDirectConflictWeight() * getNrInstructorDirectConflicts() 638 + model.getInstructorMoreThanTwoADayWeight() * getNrInstructorMoreThanTwoADayConflicts() 639 + model.getInstructorBackToBackConflictWeight() * getNrInstructorBackToBackConflicts() 640 + model.getInstructorDistanceBackToBackConflictWeight() * getNrInstructorDistanceBackToBackConflicts() 641 + model.getRoomSplitDistanceWeight() * getRoomSplitDistancePenalty() + model.getPerturbationWeight() 642 * getPerturbationPenalty() + model.getRoomPerturbationWeight() * getRoomPerturbationPenalty() 643 + model.getDistributionWeight() * getDistributionPenalty() + model.getLargeWeight() * getLargePenalty(); 644 } 645 646 /** 647 * Overall cost of using this period. The time cost of an assignment 648 * consists of the following criteria: 649 * <ul> 650 * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()} 651 * , weighted by {@link ExamModel#getDirectConflictWeight()} 652 * <li>More than two exams a day student conflicts 653 * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by 654 * {@link ExamModel#getMoreThanTwoADayWeight()} 655 * <li>Back-to-back student conflicts 656 * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by 657 * {@link ExamModel#getBackToBackConflictWeight()} 658 * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by 659 * {@link ExamModel#getPeriodWeight()} 660 * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()}, 661 * weighted by {@link ExamModel#getExamRotationWeight()} 662 * <li>Direct instructor conflicts 663 * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by 664 * {@link ExamModel#getInstructorDirectConflictWeight()} 665 * <li>More than two exams a day instructor conflicts 666 * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted 667 * by {@link ExamModel#getInstructorMoreThanTwoADayWeight()} 668 * <li>Back-to-back instructor conflicts 669 * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by 670 * {@link ExamModel#getInstructorBackToBackConflictWeight()} 671 * <li>Distance back-to-back instructor conflicts 672 * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()}, 673 * weighted by 674 * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()} 675 * <li>Front load penalty {@link ExamPlacement#getLargePenalty()}, weighted 676 * by {@link ExamModel#getLargeWeight()} 677 * </ul> 678 */ 679 public double getTimeCost() { 680 Exam exam = variable(); 681 ExamModel model = (ExamModel) exam.getModel(); 682 return model.getDirectConflictWeight() * getNrDirectConflicts() + model.getBackToBackConflictWeight() 683 * getNrBackToBackConflicts() + model.getMoreThanTwoADayWeight() * getNrMoreThanTwoADayConflicts() 684 + model.getPeriodWeight() * getPeriodPenalty() + model.getPeriodSizeWeight() * getPeriodPenalty() 685 * (exam.getSize() + 1) + model.getPeriodIndexWeight() * getPeriod().getIndex() 686 + model.getExamRotationWeight() * getRotationPenalty() + model.getInstructorDirectConflictWeight() 687 * getNrInstructorDirectConflicts() + model.getInstructorMoreThanTwoADayWeight() 688 * getNrInstructorMoreThanTwoADayConflicts() + model.getInstructorBackToBackConflictWeight() 689 * getNrInstructorBackToBackConflicts() + model.getPerturbationWeight() * getPerturbationPenalty() 690 + model.getDistributionWeight() * getPeriodDistributionPenalty() + model.getLargeWeight() 691 * getLargePenalty(); 692 } 693 694 /** 695 * Overall cost of using this set or rooms. The room cost of an assignment 696 * consists of the following criteria: 697 * <ul> 698 * <li>Distance back-to-back student conflicts 699 * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by 700 * {@link ExamModel#getDistanceBackToBackConflictWeight()} 701 * <li>Distance back-to-back instructor conflicts 702 * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()}, 703 * weighted by 704 * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()} 705 * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()}, 706 * weighted by {@link ExamModel#getRoomSizeWeight()} 707 * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()}, 708 * weighted by {@link ExamModel#getRoomSplitWeight()} 709 * <li>Room split distance penalty 710 * {@link ExamPlacement#getRoomSplitDistancePenalty()}, weighted by 711 * {@link ExamModel#getRoomSplitDistanceWeight()} 712 * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by 713 * {@link ExamModel#getRoomWeight()} 714 * </ul> 715 */ 716 public double getRoomCost() { 717 Exam exam = variable(); 718 ExamModel model = (ExamModel) exam.getModel(); 719 return model.getDistanceBackToBackConflictWeight() * getNrDistanceBackToBackConflicts() 720 + model.getRoomSizeWeight() * getRoomSizePenalty() + model.getRoomSplitWeight() * getRoomSplitPenalty() 721 + model.getRoomWeight() * getRoomPenalty() + model.getInstructorDistanceBackToBackConflictWeight() 722 * getNrInstructorDistanceBackToBackConflicts() + model.getRoomSplitDistanceWeight() 723 * getRoomSizePenalty() + model.getDistributionWeight() * getRoomDistributionPenalty() 724 + model.getRoomPerturbationWeight() * getRoomPerturbationPenalty(); 725 } 726 727 /** 728 * Room names separated with the given delimiter 729 */ 730 public String getRoomName(String delim) { 731 String roomName = ""; 732 for (Iterator<ExamRoomPlacement> i = getRoomPlacements().iterator(); i.hasNext();) { 733 ExamRoomPlacement r = i.next(); 734 roomName += r.getRoom().getName(); 735 if (i.hasNext()) 736 roomName += delim; 737 } 738 return roomName; 739 } 740 741 /** 742 * Assignment name (period / room(s)) 743 */ 744 @Override 745 public String getName() { 746 return getPeriod() + "/" + getRoomName(","); 747 } 748 749 /** 750 * String representation -- returns a list of assignment costs 751 */ 752 @Override 753 public String toString() { 754 DecimalFormat df = new DecimalFormat("0.00"); 755 Exam exam = variable(); 756 ExamModel model = (ExamModel) exam.getModel(); 757 return variable().getName() + " = " + getName() + " (" + df.format(toDouble()) + "/" + "DC:" 758 + getNrDirectConflicts() + "," + "M2D:" + getNrMoreThanTwoADayConflicts() + "," + "BTB:" 759 + getNrBackToBackConflicts() + "," 760 + (model.getBackToBackDistance() < 0 ? "" : "dBTB:" + getNrDistanceBackToBackConflicts() + ",") + "PP:" 761 + getPeriodPenalty() + "," + "@P:" + getRotationPenalty() + "," + "RSz:" + getRoomSizePenalty() + "," 762 + "RSp:" + getRoomSplitPenalty() + "," + "RD:" + df.format(getRoomSplitDistancePenalty()) + "," + "RP:" 763 + getRoomPenalty() 764 + (model.isMPP() ? ",IP:" + getPerturbationPenalty() + ",IRP:" + getRoomPerturbationPenalty() : "") 765 + ")"; 766 } 767 768 /** 769 * Compare two assignments for equality 770 */ 771 @Override 772 public boolean equals(Object o) { 773 if (o == null || !(o instanceof ExamPlacement)) 774 return false; 775 ExamPlacement p = (ExamPlacement) o; 776 return p.variable().equals(variable()) && p.getPeriod().equals(getPeriod()) 777 && p.getRoomPlacements().equals(getRoomPlacements()); 778 } 779 780 /** 781 * Hash code 782 */ 783 @Override 784 public int hashCode() { 785 return iHashCode; 786 } 787 788 /** 789 * True if given room is between {@link ExamPlacement#getRoomPlacements()} 790 */ 791 public boolean contains(ExamRoom room) { 792 return getRoomPlacements().contains(new ExamRoomPlacement(room)); 793 } 794 }