001package org.cpsolver.exam.split; 002 003import java.util.ArrayList; 004import java.util.Collection; 005import java.util.HashMap; 006import java.util.Hashtable; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010import java.util.TreeSet; 011 012import org.cpsolver.exam.criteria.ExamCriterion; 013import org.cpsolver.exam.criteria.StudentBackToBackConflicts; 014import org.cpsolver.exam.criteria.StudentDirectConflicts; 015import org.cpsolver.exam.criteria.StudentMoreThan2ADayConflicts; 016import org.cpsolver.exam.model.Exam; 017import org.cpsolver.exam.model.ExamPeriod; 018import org.cpsolver.exam.model.ExamPlacement; 019import org.cpsolver.exam.model.ExamRoomPlacement; 020import org.cpsolver.exam.model.ExamStudent; 021import org.cpsolver.ifs.assignment.Assignment; 022import org.cpsolver.ifs.criteria.Criterion; 023import org.cpsolver.ifs.solver.Solver; 024import org.cpsolver.ifs.util.DataProperties; 025 026 027/** 028 * Experimental criterion that allows an exam to be split 029 * into two if it decreases the number of student conflicts. 030 * <br><br> 031 * An examination split is improving (and is considered) if the weighted 032 * number of student conflicts that will be removed by the split is bigger 033 * than the weight of the splitter criterion {@link ExamSplitter#getWeight()}. 034 * <br><br> 035 * To enable examination splitting, following parameters needs to be set: 036 * <ul> 037 * <li>HillClimber.AdditionalNeighbours=org.cpsolver.exam.split.ExamSplitMoves 038 * <li>GreatDeluge.AdditionalNeighbours=org.cpsolver.exam.split.ExamSplitMoves 039 * <li>Exams.AdditionalCriteria=org.cpsolver.exam.split.ExamSplitter 040 * <li>Exams.ExamSplitWeight=500 041 * </ul> 042 * The Exams.ExamSplitWeight represents the weight of a split. For instance, to 043 * allow only splits that decrease the number of student direct conflicts, 044 * half of the weight of a direct student conflict is a good value for this 045 * weight. 046 * 047 * <br> 048 * 049 * @author Tomáš Müller 050 * @version ExamTT 1.3 (Examination Timetabling)<br> 051 * Copyright (C) 2008 - 2014 Tomáš Müller<br> 052 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 053 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 054 * <br> 055 * This library is free software; you can redistribute it and/or modify 056 * it under the terms of the GNU Lesser General Public License as 057 * published by the Free Software Foundation; either version 3 of the 058 * License, or (at your option) any later version. <br> 059 * <br> 060 * This library is distributed in the hope that it will be useful, but 061 * WITHOUT ANY WARRANTY; without even the implied warranty of 062 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 063 * Lesser General Public License for more details. <br> 064 * <br> 065 * You should have received a copy of the GNU Lesser General Public 066 * License along with this library; if not see 067 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 068 */ 069public class ExamSplitter extends ExamCriterion { 070 private long iLastSplitId = 0; 071 private Map<Exam, List<Exam>> iChildren = new HashMap<Exam, List<Exam>>(); 072 private Map<Exam, Exam> iParent = new HashMap<Exam, Exam>(); 073 private Criterion<Exam, ExamPlacement> iStudentDirectConflicts, iStudentMoreThan2ADayConflicts, iStudentBackToBackConflicts; 074 private double iValue = 0.0; 075 076 private Map<Exam, List<ExamPlacement>> iBestSplit = null; 077 078 /** Examination splitter criterion. */ 079 public ExamSplitter() { 080 setValueUpdateType(ValueUpdateType.NoUpdate); 081 } 082 083 /** Initialization */ 084 @Override 085 public boolean init(Solver<Exam, ExamPlacement> solver) { 086 boolean ret = super.init(solver); 087 iStudentDirectConflicts = solver.currentSolution().getModel().getCriterion(StudentDirectConflicts.class); 088 iStudentMoreThan2ADayConflicts = solver.currentSolution().getModel().getCriterion(StudentMoreThan2ADayConflicts.class); 089 iStudentBackToBackConflicts = solver.currentSolution().getModel().getCriterion(StudentBackToBackConflicts.class); 090 return ret; 091 } 092 093 /** Returns Exams.ExamSplitWeight */ 094 @Override 095 public String getWeightName() { 096 return "Exams.ExamSplitWeight"; 097 } 098 099 /** Returns examSplitWeight */ 100 @Override 101 public String getXmlWeightName() { 102 return "examSplitWeight"; 103 } 104 105 /** Returns half of a student direct conflict weight */ 106 @Override 107 public double getWeightDefault(DataProperties config) { 108 return (iStudentDirectConflicts != null ? iStudentDirectConflicts.getWeight() / 2 : 500.0); 109 } 110 111 private boolean isDayBreakBackToBack() { 112 return ((StudentBackToBackConflicts)iStudentBackToBackConflicts).isDayBreakBackToBack(); 113 } 114 115 /** True, if an exam can be split 116 * @param exam given exam 117 * @return true if the given exam can be split 118 **/ 119 public boolean canSplit(Exam exam) { 120 if (iParent.containsKey(exam)) return false; // already split 121 return true; 122 } 123 124 /** 125 * Parent of an exam that has been split. 126 * @param exam an exam in question 127 * @return parent exam if the exam has been split, or the exam itself otherwise (each non-split exam is its own parent) 128 */ 129 public Exam parent(Exam exam) { 130 return (iParent.containsKey(exam) ? iParent.get(exam) : exam); 131 } 132 133 /** 134 * Children exams of an exam that has been split. These are all the exams that the parent exam has been split into. 135 * @param parent an exam in question 136 * @return all children exams, or null of the exam has not been split yet 137 */ 138 public List<Exam> children(Exam parent) { 139 return iChildren.get(parent); 140 } 141 142 /** 143 * Split an exam 144 * @param assignment current assignment 145 * @param parent an exam to be split 146 * @param iteration solver iteration 147 * @param placement placement of the new exam 148 * @return new exam assigned to the given placement with students moved into it; null if the given exam cannot be split 149 */ 150 public Exam split(Assignment<Exam, ExamPlacement> assignment, Exam parent, long iteration, ExamPlacement placement) { 151 if (!canSplit(parent)) return null; 152 153 // Create the child exam 154 Exam child = new Exam(--iLastSplitId, parent.getName(), parent.getLength(), parent.hasAltSeating(), parent.getMaxRooms(), parent.getMinSize(), parent.getPeriodPlacements(), parent.getRoomPlacements()); 155 child.setSizeOverride(parent.getSizeOverride()); 156 child.setPrintOffset(parent.getPrintOffset()); 157 child.setAveragePeriod(parent.getAveragePeriod()); 158 child.getOwners().addAll(parent.getOwners()); 159 160 // Update the parent and children structures 161 iParent.put(child, parent); 162 List<Exam> children = iChildren.get(parent); 163 if (children == null) { 164 children = new ArrayList<Exam>(); 165 iChildren.put(parent, children); 166 } 167 children.add(child); 168 iValue += 1.0; 169 170 // Add into model 171 parent.getModel().addVariable(child); 172 for (ExamRoomPlacement room : child.getRoomPlacements()) 173 room.getRoom().addVariable(child); 174 if (placement != null) assignment.assign(iteration, new ExamPlacement(child, placement.getPeriodPlacement(), placement.getRoomPlacements())); 175 176 177 // Shuffle students between parent exam and its children 178 shuffle(assignment, parent, iteration); 179 180 // Return the new exam 181 return child; 182 } 183 184 /** True, if the given exam can be merged (it has been split) 185 * @param exam given exam 186 * @return true if the given exam can be merged back 187 **/ 188 public boolean canMerge(Exam exam) { 189 if (!iParent.containsKey(exam)) return false; // not split 190 return true; 191 } 192 193 /** 194 * Merge an exam 195 * @param assignment current assignment 196 * @param child an exam to be merged 197 * @param iteration solver iteration 198 * @return parent exam of the exam that has been deleted; null if the given exam cannot be merged 199 */ 200 public Exam merge(Assignment<Exam, ExamPlacement> assignment, Exam child, long iteration) { 201 if (!canMerge(child)) return null; 202 203 // Update the parent and children structures 204 Exam parent = iParent.get(child); 205 iParent.remove(child); 206 List<Exam> children = iChildren.get(parent); 207 children.remove(child); 208 iValue -= 1.0; 209 210 // Unassign parent and the given exam 211 ExamPlacement parentPlacement = assignment.getValue(parent); 212 if (parentPlacement != null) assignment.unassign(iteration, parent); 213 if (assignment.getValue(child) != null) assignment.unassign(iteration, child); 214 215 // Move students back from the given exam 216 for (ExamStudent student: new ArrayList<ExamStudent>(child.getStudents())) { 217 student.removeVariable(child); 218 student.addVariable(parent); 219 } 220 221 // Remove the given exam from the model 222 for (ExamRoomPlacement room : child.getRoomPlacements()) 223 room.getRoom().removeVariable(child); 224 parent.getModel().removeVariable(child); 225 226 // Assign parent exam back 227 if (parentPlacement != null) assignment.assign(iteration, parentPlacement); 228 229 230 // Shuffle students between parent exam and its remaining children 231 shuffle(assignment, parent, iteration); 232 233 // Return parent exam 234 return parent; 235 } 236 237 /** 238 * Difference in the total weighted student conflicts (including {@link StudentDirectConflicts}, 239 * {@link StudentMoreThan2ADayConflicts}, and {@link StudentBackToBackConflicts}) if a student 240 * is moved from an exam with one placement into an exam with another placement. 241 * @param assignment current assignment 242 * @param student a student in question 243 * @param oldPlacement placement of the exam in which the student is now 244 * @param newPlacement placement of the exam into which the student would be moved 245 * @return difference in the student conflict weight 246 */ 247 public double delta(Assignment<Exam, ExamPlacement> assignment, ExamStudent student, ExamPlacement oldPlacement, ExamPlacement newPlacement) { 248 double delta = 0; 249 250 // Weights of removing student form the old placement 251 if (oldPlacement != null) { 252 Exam exam = oldPlacement.variable(); 253 ExamPeriod period = oldPlacement.getPeriod(); 254 Set<Exam> examsThisPeriod = student.getExams(assignment, period); 255 if (examsThisPeriod.size() > (examsThisPeriod.contains(exam) ? 1 : 0)) 256 delta -= iStudentDirectConflicts.getWeight(); // will remove a direct conflict 257 ExamPeriod prev = period.prev(); 258 if (prev != null && (prev.getDay() == period.getDay() || isDayBreakBackToBack())) { 259 Set<Exam> examsPrevPeriod = student.getExams(assignment, prev); 260 if (examsPrevPeriod.size() > (examsPrevPeriod.contains(exam) ? 1 : 0)) 261 delta -= iStudentBackToBackConflicts.getWeight(); // will remove a back-to-back conflict 262 } 263 ExamPeriod next = period.next(); 264 if (next != null && (next.getDay() == period.getDay() || isDayBreakBackToBack())) { 265 Set<Exam> examsNextPeriod = student.getExams(assignment, next); 266 if (examsNextPeriod.size() > (examsNextPeriod.contains(exam) ? 1 : 0)) 267 delta -= iStudentBackToBackConflicts.getWeight(); // will remove a back-to-back conflict 268 } 269 Set<Exam> examsInADay = student.getExamsADay(assignment, period); 270 if (examsInADay.size() > (examsInADay.contains(exam) ? 3 : 2)) 271 delta -= iStudentMoreThan2ADayConflicts.getWeight(); // will remove a more than 2 on a day conflict 272 } 273 274 // Weights of moving student into the new placement 275 if (newPlacement != null) { 276 Exam exam = newPlacement.variable(); 277 ExamPeriod period = newPlacement.getPeriod(); 278 Set<Exam> examsThisPeriod = student.getExams(assignment, period); 279 if (examsThisPeriod.size() > (examsThisPeriod.contains(exam) ? 1 : 0)) 280 delta += iStudentDirectConflicts.getWeight(); // will add a direct conflict 281 ExamPeriod prev = period.prev(); 282 if (prev != null && (prev.getDay() == period.getDay() || isDayBreakBackToBack())) { 283 Set<Exam> examsPrevPeriod = student.getExams(assignment, prev); 284 if (examsPrevPeriod.size() > (examsPrevPeriod.contains(exam) ? 1 : 0)) 285 delta += iStudentBackToBackConflicts.getWeight(); // will add a back-to-back conflict 286 } 287 ExamPeriod next = period.next(); 288 if (next != null && (next.getDay() == period.getDay() || isDayBreakBackToBack())) { 289 Set<Exam> examsNextPeriod = student.getExams(assignment, next); 290 if (examsNextPeriod.size() > (examsNextPeriod.contains(exam) ? 1 : 0)) 291 delta += iStudentBackToBackConflicts.getWeight(); // will add a back-to-back conflict 292 } 293 Set<Exam> examsInADay = student.getExamsADay(assignment, period); 294 if (examsInADay.size() > (examsInADay.contains(exam) ? 3 : 2)) 295 delta += iStudentMoreThan2ADayConflicts.getWeight(); // will add a more than 2 on a day conflict 296 } 297 298 return delta; 299 } 300 301 /** 302 * Shuffle students between the given exam and all the other exams in the split (if there are any). 303 * Only moves between exams that improve {@link ExamSplitter#delta(Assignment, ExamStudent, ExamPlacement, ExamPlacement)} are 304 * considered. 305 * @param assignment current assignment 306 * @param exam an exam in question 307 * @param iteration solver iteration 308 */ 309 public void shuffle(Assignment<Exam, ExamPlacement> assignment, Exam exam, long iteration) { 310 // Parent exam (its either the exam itself, or its parent if it has been already split) 311 Exam parent = (iParent.containsKey(exam) ? iParent.get(exam) : exam); 312 // Its children (if already split) 313 List<Exam> children = iChildren.get(parent); 314 315 if (children != null && !children.isEmpty()) { 316 // Unassign all involved exams 317 Map<Exam, ExamPlacement> assignments = new HashMap<Exam, ExamPlacement>(); 318 if (assignment.getValue(parent) != null) { 319 assignments.put(parent, assignment.getValue(parent)); 320 assignment.unassign(iteration, parent); 321 } 322 for (Exam child: children) { 323 if (assignment.getValue(child) != null) { 324 assignments.put(child, assignment.getValue(child)); 325 assignment.unassign(iteration, child); 326 } 327 } 328 329 // Move away from parent 330 for (ExamStudent student: new ArrayList<ExamStudent>(parent.getStudents())) { 331 Exam child = null; double delta = 0; 332 for (Exam x: children) { 333 double d = delta(assignment, student, assignments.get(parent), assignments.get(x)); 334 if (child == null || d < delta) { 335 delta = d; child = x; 336 } 337 } 338 if (child != null && delta < 0) { 339 student.removeVariable(parent); 340 student.addVariable(child); 341 } 342 } 343 344 // Move students away from a child 345 for (Exam child: children) { 346 for (ExamStudent student: new ArrayList<ExamStudent>(child.getStudents())) { 347 Exam other = parent; double delta = delta(assignment, student, assignments.get(child), assignments.get(parent)); 348 for (Exam x: children) { 349 if (x.equals(child)) continue; 350 double d = delta(assignment, student, assignments.get(child), assignments.get(x)); 351 if (d < delta) { 352 delta = d; other = x; 353 } 354 } 355 if (other != null && delta < 0) { 356 student.removeVariable(child); 357 student.addVariable(other); 358 } 359 } 360 } 361 362 // Assign everything back 363 ExamPlacement parentPlacement = assignments.get(parent); 364 if (parentPlacement != null) assignment.assign(iteration, parentPlacement); 365 for (Exam child: children) { 366 ExamPlacement placement = assignments.get(child); 367 if (placement != null) assignment.assign(iteration, placement); 368 } 369 } 370 } 371 372 @Override 373 public double getValue(Assignment<Exam, ExamPlacement> assignment) { 374 return iValue; 375 } 376 377 /** Not used */ 378 @Override 379 public double getValue(Assignment<Exam, ExamPlacement> assignment, ExamPlacement value, Set<ExamPlacement> conflicts) { 380 return 0.0; 381 } 382 383 /** Not used */ 384 @Override 385 public double[] getBounds(Assignment<Exam, ExamPlacement> assignment, Collection<Exam> exams) { 386 return new double[] { 0.0, 0.0 }; 387 } 388 389 @Override 390 public String toString(Assignment<Exam, ExamPlacement> assignment) { 391 return "XX:" + sDoubleFormat.format(getValue(assignment)); 392 } 393 394 /** Lists the split */ 395 @Override 396 public void getInfo(Assignment<Exam, ExamPlacement> assignment, Map<String, String> info) { 397 if (!iChildren.isEmpty()) { 398 int parents = 0; 399 String split = ""; 400 for (Exam parent: new TreeSet<Exam>(iChildren.keySet())) { 401 List<Exam> children = iChildren.get(parent); 402 if (children.isEmpty()) continue; 403 split += "\n "; 404 parents ++; 405 split += parent.getName() + ": " + parent.getStudents().size() + " (" + (assignment.getValue(parent) == null ? "N/A" : assignment.getValue(parent).getPeriod()) + ")"; 406 for (Exam child: children) 407 split += " + " + child.getStudents().size() + " (" + (assignment.getValue(child) == null ? "N/A" : assignment.getValue(child).getPeriod()) + ")"; 408 } 409 if (parents > 0) 410 info.put("Examination Splits", parents + split); 411 } 412 } 413 414 /** Best solution was saved, remember the current splits */ 415 @Override 416 public void bestSaved(Assignment<Exam, ExamPlacement> assignment) { 417 super.bestSaved(assignment); 418 419 if (iBestSplit == null) 420 iBestSplit = new Hashtable<Exam, List<ExamPlacement>>(); 421 else 422 iBestSplit.clear(); 423 424 for (Map.Entry<Exam, List<Exam>> entry: iChildren.entrySet()) { 425 Exam parent = entry.getKey(); 426 List<ExamPlacement> placements = new ArrayList<ExamPlacement>(); 427 for (Exam child: entry.getValue()) { 428 if (assignment.getValue(child) != null) 429 placements.add(assignment.getValue(child)); 430 } 431 if (!placements.isEmpty()) 432 iBestSplit.put(parent, placements); 433 } 434 } 435 436 /** Best solution was restored, change the splits back to what it was in the best solution */ 437 @Override 438 public void bestRestored(Assignment<Exam, ExamPlacement> assignment) { 439 super.bestRestored(assignment); 440 441 // Merge those that are not split 442 for (Exam parent: new ArrayList<Exam>(iChildren.keySet())) { 443 List<Exam> children = new ArrayList<Exam>(iChildren.get(parent)); 444 List<ExamPlacement> placements = iBestSplit.get(parent); 445 for (int i = (placements == null ? 0 : placements.size()); i < children.size(); i++) 446 merge(assignment, children.get(i), 0); 447 } 448 449 // Assign best placements to all children, create children if needed 450 iValue = 0; 451 for (Exam parent: iBestSplit.keySet()) { 452 List<ExamPlacement> placements = iBestSplit.get(parent); 453 for (int i = 0; i < placements.size(); i++) { 454 List<Exam> children = iChildren.get(parent); 455 if (children == null || children.size() <= i) { // create a child if needed 456 split(assignment, parent, 0, placements.get(i)); 457 } else { // otherwise, just assign the placement 458 assignment.assign(0l, new ExamPlacement(children.get(i), placements.get(i).getPeriodPlacement(), placements.get(i).getRoomPlacements())); 459 } 460 } 461 iValue += placements.size(); 462 } 463 } 464}