001package org.cpsolver.exam.heuristics; 002 003import java.util.ArrayList; 004import java.util.Collection; 005import java.util.HashSet; 006import java.util.HashMap; 007import java.util.List; 008import java.util.Map; 009import java.util.Set; 010import java.util.TreeSet; 011 012import org.cpsolver.exam.model.Exam; 013import org.cpsolver.exam.model.ExamInstructor; 014import org.cpsolver.exam.model.ExamModel; 015import org.cpsolver.exam.model.ExamPeriodPlacement; 016import org.cpsolver.exam.model.ExamPlacement; 017import org.cpsolver.exam.model.ExamRoomPlacement; 018import org.cpsolver.exam.model.ExamStudent; 019import org.cpsolver.ifs.assignment.Assignment; 020import org.cpsolver.ifs.heuristics.NeighbourSelection; 021import org.cpsolver.ifs.model.Neighbour; 022import org.cpsolver.ifs.solution.Solution; 023import org.cpsolver.ifs.solver.Solver; 024import org.cpsolver.ifs.util.DataProperties; 025import org.cpsolver.ifs.util.JProf; 026import org.cpsolver.ifs.util.Progress; 027 028 029/** 030 * Examination timetabling construction heuristics based on graph vertex coloring. 031 * This approach is trying to find a (direct) conflict-free examination schedule using 032 * a depth-first search, assigning periods to exams in a way that there is not student or 033 * instructor direct conflict. 034 * <br> 035 * <br> 036 * This heuristics works in following modes (defined by Exam.ColoringConstructionMode). 037 * <ul> 038 * <li>Greedy .. all exams are greedily assigned with periods (and best available rooms); 039 * Exams with smallest number of available periods (or highest number of connected exams 040 * if multiple) are assigned first. 041 * <li>ColorOnly .. all exams are assigned with periods using a depth-first search (ignoring 042 * all other constraints), this coloring is then extended to exam placements as much as possible 043 * <li>Irredundant .. other constraints (distributions, rooms) are considered, however, to 044 * speedup the search redundant colorings are avoided -- this may not find a complete solution, 045 * especially when some periods are not swap-able 046 * <li>Full .. all constraints are considered, a complete solution is guaranteed to be found, if 047 * it exists (and enough time is given) 048 * </ul> 049 * <br> 050 * Time to run can be limited using Exam.ColoringConstructionTimeLimit parameter (double precision, 051 * limit is in seconds, defaults to 5 minutes) 052 * <br> 053 * <br> 054 * 055 * @author Tomáš Müller 056 * @version ExamTT 1.3 (Examination Timetabling)<br> 057 * Copyright (C) 2008 - 2014 Tomáš Müller<br> 058 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 059 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 060 * <br> 061 * This library is free software; you can redistribute it and/or modify 062 * it under the terms of the GNU Lesser General Public License as 063 * published by the Free Software Foundation; either version 3 of the 064 * License, or (at your option) any later version. <br> 065 * <br> 066 * This library is distributed in the hope that it will be useful, but 067 * WITHOUT ANY WARRANTY; without even the implied warranty of 068 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 069 * Lesser General Public License for more details. <br> 070 * <br> 071 * You should have received a copy of the GNU Lesser General Public 072 * License along with this library; if not see 073 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 074 */ 075public class ExamColoringConstruction implements NeighbourSelection<Exam, ExamPlacement> { 076 private Progress iProgress; 077 private double iT0; 078 private double iTimeLimit = 300.0; 079 private boolean iTimeLimitReached = false; 080 private Mode iMode = Mode.Full; 081 private Solver<Exam, ExamPlacement> iSolver; 082 083 private static enum Mode { 084 Greedy(true, false, true), 085 ColorOnly(false, false, false), 086 Irredundant(false, false, false), 087 Full(false, true, true); 088 boolean iGreedy, iRedundant, iConstraintCheck; 089 private Mode(boolean gr, boolean r, boolean ch) { iGreedy = gr; iRedundant = r; iConstraintCheck = ch; } 090 public boolean isGreedy() { return iGreedy; } 091 public boolean isRedundant() { return iRedundant; } 092 public boolean isConstraintCheck() { return iConstraintCheck; } 093 } 094 095 public ExamColoringConstruction(DataProperties config) { 096 iTimeLimit = config.getPropertyDouble("Exam.ColoringConstructionTimeLimit", iTimeLimit); 097 iMode = Mode.valueOf(config.getProperty("Exam.ColoringConstructionMode", iMode.name())); 098 } 099 100 private boolean backTrack(Solution<Exam, ExamPlacement> solution, int index, HashSet<Integer> colorsUsedSoFar, Collection<Vertex> vertices) { 101 Assignment<Exam, ExamPlacement> assignment = solution.getAssignment(); 102 if (iTimeLimitReached || iSolver.isStop()) return false; 103 if (JProf.currentTimeSec() - iT0 > iTimeLimit) { 104 iTimeLimitReached = true; 105 return false; 106 } 107 if (index == vertices.size()) return true; 108 if (iProgress.getProgress() < index) { 109 iProgress.setProgress(index); 110 if (iMode.isConstraintCheck()) 111 solution.saveBest(); 112 } 113 Vertex vertex = null; 114 for (Vertex v: vertices) { 115 if (v.color() >= 0) continue; 116 if (vertex == null || v.compareTo(vertex) < 0) { 117 vertex = v; 118 } 119 } 120 if (colorsUsedSoFar != null) { 121 for (int color: new TreeSet<Integer>(colorsUsedSoFar)) 122 if (vertex.colorize(assignment, color) && backTrack(solution, 1 + index, colorsUsedSoFar, vertices)) return true; 123 for (int color: vertex.domain()) { 124 if (colorsUsedSoFar.contains(color)) continue; 125 if (vertex.colorize(assignment, color)) { 126 colorsUsedSoFar.add(color); 127 if (backTrack(solution, 1 + index, colorsUsedSoFar, vertices)) return true; 128 colorsUsedSoFar.remove(color); 129 } 130 break; 131 } 132 } else { 133 for (int color: vertex.domain()) 134 if (vertex.colorize(assignment, color) && backTrack(solution, 1 + index, colorsUsedSoFar, vertices)) return true; 135 } 136 vertex.colorize(assignment, -1); 137 return false; 138 } 139 140 @Override 141 public void init(Solver<Exam, ExamPlacement> solver) { 142 iProgress = Progress.getInstance(solver.currentSolution().getModel()); 143 iSolver = solver; 144 } 145 146 public Set<ExamRoomPlacement> findRooms(Assignment<Exam, ExamPlacement> assignment, Exam exam, ExamPeriodPlacement period) { 147 Set<ExamRoomPlacement> rooms = exam.findBestAvailableRooms(assignment, period); 148 if (rooms != null) return rooms; 149 150 rooms = new HashSet<ExamRoomPlacement>(); 151 int size = 0; 152 while (size < exam.getSize()) { 153 ExamRoomPlacement bestRoom = null; int bestSize = 0; 154 for (ExamRoomPlacement r: exam.getRoomPlacements()) { 155 if (!r.isAvailable(period.getPeriod())) continue; 156 if (rooms.contains(r)) continue; 157 if (!r.getRoom().getPlacements(assignment, period.getPeriod()).isEmpty()) continue; 158 int s = r.getSize(exam.hasAltSeating()); 159 if (bestRoom == null || s > bestSize) { 160 bestRoom = r; 161 bestSize = s; 162 } 163 } 164 if (bestRoom == null) return rooms; 165 rooms.add(bestRoom); size += bestSize; 166 } 167 168 return rooms; 169 } 170 171 @Override 172 public Neighbour<Exam, ExamPlacement> selectNeighbour(final Solution<Exam, ExamPlacement> solution) { 173 ExamModel model = (ExamModel)solution.getModel(); 174 // if (!model.assignedVariables().isEmpty()) return null; 175 final HashMap<Exam, Vertex> vertices = new HashMap<Exam, Vertex>(); 176 for (Exam x: model.variables()) { 177 vertices.put(x, new Vertex(x)); 178 } 179 for (ExamStudent s: model.getStudents()) { 180 for (Exam x: s.variables()) { 181 for (Exam y: s.variables()) { 182 if (!x.equals(y)) { 183 vertices.get(x).neighbors().add(vertices.get(y)); 184 vertices.get(y).neighbors().add(vertices.get(x)); 185 } 186 } 187 } 188 } 189 for (ExamInstructor i: model.getInstructors()) { 190 for (Exam x: i.variables()) { 191 for (Exam y: i.variables()) { 192 if (!x.equals(y)) { 193 vertices.get(x).neighbors().add(vertices.get(y)); 194 vertices.get(y).neighbors().add(vertices.get(x)); 195 } 196 } 197 } 198 } 199 iProgress.setPhase("Graph coloring-based construction", vertices.size()); 200 iProgress.info("Looking for a conflict-free assignment using " + model.getPeriods().size() + " periods."); 201 iT0 = JProf.currentTimeSec(); iTimeLimitReached = false; 202 if (iMode.isGreedy()) { 203 iProgress.info("Using greedy heuristics only (no backtracking)..."); 204 } else if (backTrack(solution, 0, (iMode.isRedundant() ? null : new HashSet<Integer>()), vertices.values())) { 205 iProgress.info("Success!"); 206 } else if (iTimeLimitReached) { 207 iProgress.info("There was no conflict-free schedule found during the given time."); 208 } else if (iSolver.isStop()) { 209 iProgress.info("Solver was stopped."); 210 } else { 211 if (iMode.isRedundant()) 212 iProgress.info("There is no conflict-free schedule!"); 213 else 214 iProgress.info("Conflict-free schedule not found."); 215 } 216 if (iMode.isConstraintCheck()) 217 solution.restoreBest(); 218 HashSet<Vertex> remaning = new HashSet<Vertex>(); 219 for (Vertex v: vertices.values()) 220 if (v.color() < 0) remaning.add(v); 221 remaining: while (!remaning.isEmpty()) { 222 Vertex vertex = null; 223 for (Vertex v: remaning) 224 if (vertex == null || v.compareTo(vertex) < 0) 225 vertex = v; 226 remaning.remove(vertex); 227 for (int color: vertex.domain()) 228 if (vertex.colorize(solution.getAssignment(), color)) continue remaining; 229 } 230 if (!iMode.isConstraintCheck()) { 231 return new Neighbour<Exam, ExamPlacement>() { 232 @Override 233 public void assign(Assignment<Exam, ExamPlacement> assignment, long iteration) { 234 iProgress.info("Using graph coloring solution ..."); 235 for (Vertex vertex: new TreeSet<Vertex>(vertices.values())) { 236 ExamPeriodPlacement period = vertex.period(); 237 if (period == null || !vertex.exam().checkDistributionConstraints(assignment, period)) continue; 238 Set<ExamRoomPlacement> rooms = findRooms(assignment, vertex.exam(), period); 239 if (rooms == null) continue; 240 assignment.assign(iteration, new ExamPlacement(vertex.exam(), period, rooms)); 241 } 242 HashSet<Vertex> unassigned = new HashSet<Vertex>(); 243 for (Vertex vertex: vertices.values()) { 244 if (assignment.getValue(vertex.exam()) == null) { 245 unassigned.add(vertex); 246 vertex.colorize(assignment, -1); 247 } 248 } 249 solution.saveBest(); 250 iProgress.info("Extending ..."); 251 unassigned: while (!unassigned.isEmpty()) { 252 Vertex vertex = null; 253 for (Vertex v: unassigned) 254 if (vertex == null || v.compareTo(vertex) < 0) 255 vertex = v; 256 unassigned.remove(vertex); 257 for (int color: vertex.domain()) { 258 if (!vertex.colorize(assignment, color)) continue; 259 ExamPeriodPlacement period = vertex.period(color); 260 if (period == null || !vertex.exam().checkDistributionConstraints(assignment, period)) continue; 261 Set<ExamRoomPlacement> rooms = findRooms(assignment, vertex.exam(), period); 262 if (rooms == null) continue; 263 assignment.assign(iteration, new ExamPlacement(vertex.exam(), period, rooms)); 264 continue unassigned; 265 } 266 vertex.colorize(assignment, -1); 267 } 268 solution.saveBest(); 269 iProgress.info("Construction done."); 270 } 271 @Override 272 public double value(Assignment<Exam, ExamPlacement> assignment) { 273 return 0; 274 } 275 @Override 276 public Map<Exam, ExamPlacement> assignments() { 277 throw new UnsupportedOperationException(); 278 } 279 }; 280 } 281 return null; 282 } 283 284 /** Internal graph representation -- needed for domain caching */ 285 private class Vertex implements Comparable<Vertex> { 286 private Exam iExam; 287 private List<Vertex> iNeighbors = new ArrayList<Vertex>(); 288 private int iColor = -1; 289 private HashMap<Integer, ExamPeriodPlacement> iDomain = new HashMap<Integer, ExamPeriodPlacement>(); 290 private HashMap<Integer, Vertex> iTaken = new HashMap<Integer, Vertex>(); 291 292 public Vertex(Exam exam) { 293 iExam = exam; 294 for (ExamPeriodPlacement period: exam.getPeriodPlacements()) 295 iDomain.put(period.getIndex(), period); 296 } 297 298 public List<Vertex> neighbors() { return iNeighbors; } 299 300 public Set<Integer> domain() { return iDomain.keySet(); } 301 302 public ExamPeriodPlacement period() { return (iColor < 0 ? null : iDomain.get(iColor)); } 303 304 public ExamPeriodPlacement period(int color) { return (color < 0 ? null : iDomain.get(color)); } 305 306 private boolean neighborColored(Vertex v, int color) { 307 if (!iDomain.containsKey(color)) return true; 308 if (iTaken.get(color) == null) 309 iTaken.put(color, v); 310 return iTaken.size() < iDomain.size(); 311 } 312 313 private void neighborUncolored(Vertex v, int color) { 314 if (!iDomain.containsKey(color)) return; 315 if (v.equals(iTaken.get(color))) { 316 for (Vertex w: neighbors()) { 317 if (w.equals(v)) continue; 318 if (w.color() == color) { 319 iTaken.put(color, w); 320 return; 321 } 322 } 323 iTaken.remove(color); 324 } 325 } 326 327 public int color() { return iColor; } 328 329 public boolean colorize(Assignment<Exam, ExamPlacement> assignment, int color) { 330 if (iColor == color) return true; 331 ExamPlacement placement = null; 332 if (color >= 0) { 333 if (iTaken.get(color) != null || !iDomain.containsKey(color)) 334 return false; 335 if (iMode.isConstraintCheck()) { 336 ExamPeriodPlacement period = iDomain.get(color); 337 if (!iExam.checkDistributionConstraints(assignment, period)) return false; 338 Set<ExamRoomPlacement> rooms = findRooms(assignment, iExam, period); 339 if (rooms == null) return false; 340 placement = new ExamPlacement(iExam, period, rooms); 341 } 342 } 343 if (iColor >= 0) { 344 for (Vertex v: neighbors()) 345 v.neighborUncolored(this, iColor); 346 } 347 boolean success = true; 348 if (color >= 0) { 349 for (Vertex v: neighbors()) { 350 if (!v.neighborColored(this, color)) { 351 success = false; break; 352 } 353 } 354 } 355 if (success) { 356 iColor = color; 357 if (iMode.isConstraintCheck()) { 358 if (placement != null) 359 assignment.assign(0l, placement); 360 else 361 assignment.unassign(0l, iExam); 362 } 363 } else { // undo 364 for (Vertex v: neighbors()) { 365 v.neighborUncolored(this, color); 366 if (iColor >= 0) 367 v.neighborColored(this, iColor); 368 } 369 } 370 return success; 371 } 372 373 public int degree() { 374 return iNeighbors.size(); 375 } 376 377 public int available() { 378 return iDomain.size() - iTaken.size(); 379 } 380 381 public int degreeNotColored() { 382 int ret = 0; 383 for (Vertex v: neighbors()) 384 if (v.color() < 0) ret ++; 385 return ret; 386 } 387 388 public Exam exam() { return iExam; } 389 390 @Override 391 public int compareTo(Vertex v) { 392 if (available() < v.available()) return -1; 393 if (v.available() < available()) return 1; 394 if (degree() > v.degree()) return -1; 395 if (v.degree() > degree()) return 1; 396 if (degreeNotColored() > v.degreeNotColored()) return -1; 397 if (v.degreeNotColored() > degreeNotColored()) return 1; 398 return Double.compare(exam().getId(), v.exam().getId()); 399 } 400 } 401}