001package org.cpsolver.exam.split; 002 003import java.util.HashSet; 004import java.util.List; 005import java.util.Map; 006import java.util.Set; 007 008 009import org.apache.logging.log4j.Logger; 010import org.cpsolver.exam.criteria.RoomPenalty; 011import org.cpsolver.exam.criteria.RoomSizePenalty; 012import org.cpsolver.exam.model.Exam; 013import org.cpsolver.exam.model.ExamPeriodPlacement; 014import org.cpsolver.exam.model.ExamPlacement; 015import org.cpsolver.exam.model.ExamRoomPlacement; 016import org.cpsolver.exam.model.ExamStudent; 017import org.cpsolver.ifs.assignment.Assignment; 018import org.cpsolver.ifs.heuristics.NeighbourSelection; 019import org.cpsolver.ifs.model.Neighbour; 020import org.cpsolver.ifs.solution.Solution; 021import org.cpsolver.ifs.solver.Solver; 022import org.cpsolver.ifs.util.DataProperties; 023import org.cpsolver.ifs.util.ToolBox; 024 025/** 026 * Experimental neighbor selection that allows an exam to be split 027 * into two if it decreases the number of student conflicts. 028 * <br><br> 029 * An examination split is improving (and is considered) if the weighted 030 * number of student conflicts that will be removed by the split is bigger 031 * than the weight of the splitter criterion {@link ExamSplitter#getWeight()}. 032 * 033 * <br> 034 * 035 * @version ExamTT 1.3 (Examination Timetabling)<br> 036 * Copyright (C) 2008 - 2014 Tomáš Müller<br> 037 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 038 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 039 * <br> 040 * This library is free software; you can redistribute it and/or modify 041 * it under the terms of the GNU Lesser General Public License as 042 * published by the Free Software Foundation; either version 3 of the 043 * License, or (at your option) any later version. <br> 044 * <br> 045 * This library is distributed in the hope that it will be useful, but 046 * WITHOUT ANY WARRANTY; without even the implied warranty of 047 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 048 * Lesser General Public License for more details. <br> 049 * <br> 050 * You should have received a copy of the GNU Lesser General Public 051 * License along with this library; if not see 052 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 053 */ 054public class ExamSplitMoves implements NeighbourSelection<Exam, ExamPlacement> { 055 private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(ExamSplitMoves.class); 056 private ExamSplitter iSplitter = null; 057 058 /** Constructor 059 * @param properties solver configuration 060 **/ 061 public ExamSplitMoves(DataProperties properties) {} 062 063 /** Initialization */ 064 @Override 065 public void init(Solver<Exam, ExamPlacement> solver) { 066 iSplitter = (ExamSplitter)solver.currentSolution().getModel().getCriterion(ExamSplitter.class); 067 if (iSplitter == null) throw new RuntimeException("ExamSplitter criterion needs to be used as well."); 068 } 069 070 /** 071 * Find best available rooms for a new exam (that is to be split from the given one), 072 * if is is assigned into the given examination period. 073 * 074 * @param assignment current assignment 075 * @param exam an exam to be split 076 * @param period a period to be assigned to the new exam 077 * @param examSize size of the new exam (i.e., the number of students that will be moved from the given exam to the new one) 078 * @return best room placement for the given exam and period 079 */ 080 public Set<ExamRoomPlacement> findBestAvailableRooms(Assignment<Exam, ExamPlacement> assignment, Exam exam, ExamPeriodPlacement period, int examSize) { 081 if (exam.getMaxRooms() == 0) return new HashSet<ExamRoomPlacement>(); 082 double sw = exam.getModel().getCriterion(RoomSizePenalty.class).getWeight(); 083 double pw = exam.getModel().getCriterion(RoomPenalty.class).getWeight(); 084 loop: for (int nrRooms = 1; nrRooms <= exam.getMaxRooms(); nrRooms++) { 085 HashSet<ExamRoomPlacement> rooms = new HashSet<ExamRoomPlacement>(); 086 int size = 0; 087 while (rooms.size() < nrRooms && size < examSize) { 088 int minSize = (examSize - size) / (nrRooms - rooms.size()); 089 ExamRoomPlacement best = null; 090 double bestWeight = 0; 091 int bestSize = 0; 092 for (ExamRoomPlacement room : exam.getRoomPlacements()) { 093 if (!room.isAvailable(period.getPeriod())) 094 continue; 095 if (!room.getRoom().getPlacements(assignment, period.getPeriod()).isEmpty()) 096 continue; 097 if (rooms.contains(room)) 098 continue; 099 int s = room.getSize(exam.hasAltSeating()); 100 if (s < minSize) 101 break; 102 int p = room.getPenalty(period.getPeriod()); 103 double w = pw * p + sw * (s - minSize); 104 double d = 0; 105 if (!rooms.isEmpty()) { 106 for (ExamRoomPlacement r : rooms) { 107 d += r.getDistanceInMeters(room); 108 } 109 w += d / rooms.size(); 110 } 111 if (best == null || bestWeight > w) { 112 best = room; 113 bestSize = s; 114 bestWeight = w; 115 } 116 } 117 if (best == null) 118 continue loop; 119 rooms.add(best); 120 size += bestSize; 121 } 122 if (size >= examSize) 123 return rooms; 124 } 125 return null; 126 } 127 128 /** 129 * Find a best split for the given exam. Only improving neighbors are considered. 130 * @param assignment current assignment 131 * @param exam an exam to be split 132 * @return best neighbor that will do the split 133 */ 134 public ExamSplitNeighbour bestSplit(Assignment<Exam, ExamPlacement> assignment, Exam exam) { 135 ExamSplitNeighbour split = null; 136 ExamPlacement placement = assignment.getValue(exam); 137 int px = ToolBox.random(exam.getPeriodPlacements().size()); 138 for (int p = 0; p < exam.getPeriodPlacements().size(); p++) { // Iterate over possible periods 139 ExamPeriodPlacement period = exam.getPeriodPlacements().get((p + px) % exam.getPeriodPlacements().size()); 140 if (placement != null && placement.getPeriod().equals(period.getPeriod())) continue; 141 // Try to create a neighbor 142 ExamSplitNeighbour s = new ExamSplitNeighbour(assignment, exam, new ExamPlacement(exam, period, null)); 143 if (split == null || s.value(assignment) < split.value(assignment)) { 144 // If improving, try to find available rooms 145 Set<ExamRoomPlacement> rooms = findBestAvailableRooms(assignment, exam, period, s.nrStudents()); 146 if (rooms != null) { 147 // Remember as best split 148 s.placement().getRoomPlacements().addAll(rooms); 149 split = s; 150 } 151 } 152 } 153 return split; 154 } 155 156 /** 157 * Select a split (split an exam into two), a merge (merge two split exams back together) or 158 * shuffle operation (move students between two exams that has been split before). 159 */ 160 @Override 161 public Neighbour<Exam, ExamPlacement> selectNeighbour(Solution<Exam, ExamPlacement> solution) { 162 // Randomly select an exam 163 Exam exam = ToolBox.random(solution.getAssignment().assignedVariables()); 164 Assignment<Exam, ExamPlacement> assignment = solution.getAssignment(); 165 166 // Parent exam (its either the exam itself, or its parent if it has been already split) 167 Exam parent = iSplitter.parent(exam); 168 // Its children (if already split) 169 List<Exam> children = iSplitter.children(parent); 170 171 // Already split -> try shuffle 172 if (children != null && !children.isEmpty()) { 173 ExamShuffleNeighbour shuffle = new ExamShuffleNeighbour(assignment, exam); 174 if (shuffle.value(assignment) < 0.0) return shuffle; 175 } 176 177 // Can split -> try a split 178 if (iSplitter.canSplit(exam)) { 179 ExamSplitNeighbour split = bestSplit(solution.getAssignment(), exam); 180 if (split != null && split.value(assignment) < 0.0) return split; 181 } 182 183 // Can merge -> try to merge 184 if (iSplitter.canMerge(exam)) { 185 ExamMergeNeighbour merge = new ExamMergeNeighbour(assignment, exam); 186 if (merge.value(assignment) < 0.0) return merge; 187 } 188 189 return null; 190 } 191 192 /** 193 * Split an exam into two 194 */ 195 protected class ExamSplitNeighbour implements Neighbour<Exam, ExamPlacement> { 196 private Exam iExam; 197 private ExamPlacement iPlacement; 198 private double iValue = 0.0; 199 private int iNrStudents = 0; 200 201 /** 202 * Split an exam into two, assign the new exam into the given placement. 203 * @param assignment current assignment 204 * @param exam an exam to be split 205 * @param placement a placement to be assigned to the new exam 206 */ 207 public ExamSplitNeighbour(Assignment<Exam, ExamPlacement> assignment, Exam exam, ExamPlacement placement) { 208 iExam = exam; 209 iPlacement = placement; 210 211 // Parent exam (its either the exam itself, or its parent if it has been already split) 212 Exam parent = iSplitter.parent(exam); 213 // Its children (if already split) 214 List<Exam> children = iSplitter.children(parent); 215 216 // Compute improvement 217 // Consider moving all students of the parent exam to the new placement 218 for (ExamStudent student: parent.getStudents()) { 219 double delta = iSplitter.delta(assignment, student, assignment.getValue(parent), placement); 220 if (delta < 0.0) { 221 iValue += delta; 222 iNrStudents ++; 223 } 224 } 225 // If there already are other children, consider moving students of these children to the 226 // new placement as well 227 if (children != null) 228 for (Exam child: children) 229 for (ExamStudent student: child.getStudents()) { 230 double delta = iSplitter.delta(assignment, student, assignment.getValue(child), placement); 231 if (delta < 0.0) { 232 iValue += delta; 233 iNrStudents ++; 234 } 235 } 236 237 // Increase the weight by the splitter criterion weight 238 iValue += iSplitter.getWeight(); 239 } 240 241 /** 242 * Perform the split. 243 */ 244 @Override 245 public void assign(Assignment<Exam, ExamPlacement> assignment, long iteration) { 246 sLog.info("Splitting " + iExam.getName() + " (" + assignment.getValue(iExam).getName() + ", " + iPlacement.getName() + ", value: " + iValue + ")"); 247 iSplitter.split(assignment, iExam, iteration, iPlacement); 248 } 249 250 /** 251 * Value of the split. This is the weight of the splitter criterion minus the weighted sum of 252 * all student conflicts that will be removed by the split. 253 */ 254 @Override 255 public double value(Assignment<Exam, ExamPlacement> assignment) { 256 return iValue; 257 } 258 259 /** 260 * Number of students that will be moved into the new exam. 261 * @return number of students 262 */ 263 public int nrStudents() { 264 return iNrStudents; 265 } 266 267 /** 268 * Exam to be split. 269 * @return exam to be split 270 */ 271 public Exam exam() { 272 return iExam; 273 } 274 275 /** 276 * Placement of the new exam. 277 * @return placement of the new exam 278 */ 279 public ExamPlacement placement() { 280 return iPlacement; 281 } 282 283 @Override 284 public Map<Exam, ExamPlacement> assignments() { 285 throw new UnsupportedOperationException(); 286 } 287 } 288 289 /** 290 * Merge two exams that have been split before back into one. This moves 291 * the students from the child exam back to its parent and removes the 292 * child exam from the problem. 293 */ 294 protected class ExamMergeNeighbour implements Neighbour<Exam, ExamPlacement> { 295 private Exam iExam; 296 private double iValue = 0.0; 297 298 /** 299 * Child exam to be removed. 300 * @param assignment current assignment 301 * @param exam child exam to be merged back 302 */ 303 public ExamMergeNeighbour(Assignment<Exam, ExamPlacement> assignment, Exam exam) { 304 iExam = exam; 305 306 // Parent exam (its either the exam itself, or its parent if it has been already split) 307 Exam parent = iSplitter.parent(exam); 308 // Its children (if already split) 309 List<Exam> children = iSplitter.children(parent); 310 311 // Compute improvement 312 for (ExamStudent student: exam.getStudents()) { 313 // Try to move each student either back to the parent exam or to any of the other 314 // children exams, if there are any 315 double delta = iSplitter.delta(assignment, student, assignment.getValue(exam), assignment.getValue(parent)); 316 for (Exam child: children) { 317 if (child.equals(exam)) continue; 318 double d = iSplitter.delta(assignment, student, assignment.getValue(exam), assignment.getValue(child)); 319 if (d < delta) delta = d; 320 } 321 iValue += delta; 322 } 323 // Decrease the weight by the splitter criterion weight 324 iValue -= iSplitter.getWeight(); 325 } 326 327 /** 328 * Perform the merge. 329 */ 330 @Override 331 public void assign(Assignment<Exam, ExamPlacement> assignment, long iteration) { 332 sLog.info("Mergning " + iExam.getName() + " (" + assignment.getValue(iExam).getName() + ", value: " + iValue + ")"); 333 iSplitter.merge(assignment, iExam, iteration); 334 } 335 336 /** 337 * Value of the merge. This is the weighted sum of all student conflicts that will be added by the merge, 338 * minus the weight of the splitter criterion. 339 */ 340 @Override 341 public double value(Assignment<Exam, ExamPlacement> assignment) { 342 return iValue; 343 } 344 345 /** 346 * Number of students that will be moved back to the parent exam or to some other child (if there are any). 347 * @return number of students 348 */ 349 public int nrStudents() { 350 return iExam.getStudents().size(); 351 } 352 353 /** 354 * Exam to be merged. 355 * @return exam to be merged 356 */ 357 public Exam exam() { 358 return iExam; 359 } 360 361 @Override 362 public Map<Exam, ExamPlacement> assignments() { 363 throw new UnsupportedOperationException(); 364 } 365 } 366 367 /** 368 * Shuffle students between the parent exam and all of its children. Only swaps 369 * that are decreasing the weighted sum of student conflicts are considered. 370 */ 371 protected class ExamShuffleNeighbour implements Neighbour<Exam, ExamPlacement> { 372 private Exam iExam; 373 private double iValue = 0.0; 374 375 /** 376 * Exam to be shuffled. 377 * @param assignment current exam 378 * @param exam child exam to be shuffled 379 */ 380 public ExamShuffleNeighbour(Assignment<Exam, ExamPlacement> assignment, Exam exam) { 381 iExam = exam; 382 383 // Parent exam (its either the exam itself, or its parent if it has been already split) 384 Exam parent = iSplitter.parent(exam); 385 // Its children (if already split) 386 List<Exam> children = iSplitter.children(parent); 387 388 // Compute improvement 389 // Try moving students away from parent 390 for (ExamStudent student: parent.getStudents()) { 391 Double delta = null; 392 for (Exam x: children) { 393 double d = iSplitter.delta(assignment, student, assignment.getValue(parent), assignment.getValue(x)); 394 if (delta == null || d < delta) delta = d; 395 } 396 if (delta != null && delta < 0) iValue += delta; 397 } 398 399 // Try moving students away from any child 400 for (Exam child: children) { 401 for (ExamStudent student: child.getStudents()) { 402 double delta = iSplitter.delta(assignment, student, assignment.getValue(child), assignment.getValue(parent)); 403 for (Exam x: children) { 404 if (x.equals(child)) continue; 405 double d = iSplitter.delta(assignment, student, assignment.getValue(child), assignment.getValue(x)); 406 if (d < delta) delta = d; 407 } 408 if (delta < 0) iValue += delta; 409 } 410 } 411 } 412 413 /** 414 * Perform the shuffle. 415 */ 416 @Override 417 public void assign(Assignment<Exam, ExamPlacement> assignment, long iteration) { 418 sLog.info("Shuffling " + iExam.getName() + " (" + assignment.getValue(iExam).getName() + ", value: " + iValue + ")"); 419 iSplitter.shuffle(assignment, iExam, iteration); 420 } 421 422 /** 423 * Value of the shuffle. This is the weighted sum of all student conflicts that will be removed by the shuffle. 424 */ 425 @Override 426 public double value(Assignment<Exam, ExamPlacement> assignment) { 427 return iValue; 428 } 429 430 /** 431 * Exam to be shuffled. 432 * @return exam to be shuffled 433 */ 434 public Exam exam() { 435 return iExam; 436 } 437 438 @Override 439 public Map<Exam, ExamPlacement> assignments() { 440 throw new UnsupportedOperationException(); 441 } 442 } 443}