001 package net.sf.cpsolver.exam.heuristics; 002 003 import java.util.Enumeration; 004 import java.util.HashSet; 005 import java.util.Iterator; 006 import java.util.Set; 007 import java.util.TreeSet; 008 import java.util.Vector; 009 010 import org.apache.log4j.Logger; 011 012 import net.sf.cpsolver.exam.model.Exam; 013 import net.sf.cpsolver.exam.model.ExamPeriodPlacement; 014 import net.sf.cpsolver.exam.model.ExamPlacement; 015 import net.sf.cpsolver.ifs.extension.ConflictStatistics; 016 import net.sf.cpsolver.ifs.extension.Extension; 017 import net.sf.cpsolver.ifs.heuristics.NeighbourSelection; 018 import net.sf.cpsolver.ifs.heuristics.ValueSelection; 019 import net.sf.cpsolver.ifs.model.Model; 020 import net.sf.cpsolver.ifs.model.Neighbour; 021 import net.sf.cpsolver.ifs.model.SimpleNeighbour; 022 import net.sf.cpsolver.ifs.model.Value; 023 import net.sf.cpsolver.ifs.model.Variable; 024 import net.sf.cpsolver.ifs.solution.Solution; 025 import net.sf.cpsolver.ifs.solver.Solver; 026 import net.sf.cpsolver.ifs.util.DataProperties; 027 import net.sf.cpsolver.ifs.util.ToolBox; 028 029 /** 030 * Tabu search algorithm. 031 * <br><br> 032 * If used as {@link NeighbourSelection}, the most improving (re)assignment of a value to a variable 033 * is returned (all variables and all their values are enumerated). If there are more than one of 034 * such assignments, one is selected randomly. A returned assignment can cause unassignment of 035 * other existing assignments. The search is stopped ({@link ExamTabuSearch#selectNeighbour(Solution)} 036 * returns null) after TabuSearch.MaxIdle idle (not improving) iterations. 037 * <br><br> 038 * If used as {@link ValueSelection}, the most improving (re)assignment of a value to a given variable 039 * is returned (all values of the given variable are enumerated). If there are more than one of 040 * such assignments, one is selected randomly. A returned assignment can cause unassignment of 041 * other existing assignments. 042 * <br><br> 043 * To avoid cycling, a tabu is maintainded during the search. It is the list of the last n 044 * selected values. A selection of a value that is present in the tabu list is only allowed when it improves the 045 * best ever found solution. 046 * <br><br> 047 * The minimum size of the tabu list is TabuSearch.MinSize, maximum size is TabuSearch.MaxSize (tabu 048 * list is not used when both sizes are zero). The current size of the tabu list starts at 049 * MinSize (and is reset to MinSize every time a new best solution is found), it is increased 050 * by one up to the MaxSize after TabuSearch.MaxIdle / (MaxSize - MinSize) non-improving 051 * iterations. 052 * <br><br> 053 * Conflict-based Statistics {@link ConflictStatistics} (CBS) can be used instead of (or together with) 054 * tabu list, when CBS is used as a solver extension. 055 * 056 * @version 057 * ExamTT 1.1 (Examination Timetabling)<br> 058 * Copyright (C) 2008 Tomáš Müller<br> 059 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 060 * Lazenska 391, 76314 Zlin, Czech Republic<br> 061 * <br> 062 * This library is free software; you can redistribute it and/or 063 * modify it under the terms of the GNU Lesser General Public 064 * License as published by the Free Software Foundation; either 065 * version 2.1 of the License, or (at your option) any later version. 066 * <br><br> 067 * This library is distributed in the hope that it will be useful, 068 * but 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. 071 * <br><br> 072 * You should have received a copy of the GNU Lesser General Public 073 * License along with this library; if not, write to the Free Software 074 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 075 */ 076 public class ExamTabuSearch implements NeighbourSelection, ValueSelection { 077 private static Logger sLog = Logger.getLogger(ExamTabuSearch.class); 078 private ConflictStatistics iStat = null; 079 080 private long iFirstIteration = -1; 081 private long iMaxIdleIterations = 10000; 082 083 private int iTabuSize = 20; 084 private int iTabuMinSize = 0; 085 private int iTabuMaxSize = 0; 086 private TabuList iTabu = null; 087 088 private double iConflictWeight = 1000000; 089 private double iValueWeight = 1; 090 091 /** 092 * <ul> 093 * <li>TabuSearch.MaxIdle ... maximum number of idle iterations (default is 10000) 094 * <li>TabuSearch.MinSize ... minimum size of the tabu list 095 * <li>TabuSearch.MaxSize ... maximum size of the tabu list 096 * <li>Value.ValueWeight ... weight of a value (i.e., {@link Value#toDouble()}) 097 * <li>Value.ConflictWeight ... weight of a conflicting value (see {@link Model#conflictValues(Value)}), 098 * it is also weighted by the past occurrences when conflict-based statistics is used 099 * </ul> 100 */ 101 public ExamTabuSearch(DataProperties properties) throws Exception { 102 iTabuMinSize = properties.getPropertyInt("TabuSearch.MinSize", iTabuMinSize); 103 iTabuMaxSize = properties.getPropertyInt("TabuSearch.MaxSize", iTabuMaxSize); 104 if (iTabuMaxSize > 0) iTabu = new TabuList(iTabuMinSize); 105 iMaxIdleIterations = properties.getPropertyLong("TabuSearch.MaxIdle", iMaxIdleIterations); 106 iConflictWeight = properties.getPropertyDouble("Value.ConflictWeight", iConflictWeight); 107 iValueWeight = properties.getPropertyDouble("Value.ValueWeight", iValueWeight); 108 } 109 110 /** Initialization */ 111 public void init(Solver solver) { 112 for (Enumeration i = solver.getExtensions().elements(); i.hasMoreElements();) { 113 Extension extension = (Extension)i.nextElement(); 114 if (extension instanceof ConflictStatistics) 115 iStat = (ConflictStatistics)extension; 116 } 117 } 118 119 /** 120 * Neighbor selection 121 */ 122 public Neighbour selectNeighbour(Solution solution) { 123 if (iFirstIteration<0) 124 iFirstIteration = solution.getIteration(); 125 long idle = solution.getIteration()-Math.max(iFirstIteration,solution.getBestIteration()); 126 if (idle>iMaxIdleIterations) { 127 sLog.debug(" [tabu] max idle iterations reached"); 128 iFirstIteration=-1; 129 if (iTabu!=null) iTabu.clear(); 130 return null; 131 } 132 if (iTabu!=null && iTabuMaxSize>iTabuMinSize) { 133 if (idle==0) { 134 iTabu.resize(iTabuMinSize); 135 } else if (idle%(iMaxIdleIterations/(iTabuMaxSize-iTabuMinSize))==0) { 136 iTabu.resize(Math.min(iTabuMaxSize,iTabu.size()+1)); 137 } 138 } 139 140 boolean acceptConflicts = solution.getModel().getBestUnassignedVariables()>0; 141 Model model = solution.getModel(); 142 double bestEval = 0.0; 143 Vector best = null; 144 for (Enumeration e=model.variables().elements();e.hasMoreElements();) { 145 Exam exam = (Exam)e.nextElement(); 146 ExamPlacement assigned = (ExamPlacement)exam.getAssignment(); 147 double assignedVal = (assigned==null?iConflictWeight:iValueWeight*assigned.toDouble()); 148 for (Enumeration f=exam.getPeriodPlacements().elements();f.hasMoreElements();) { 149 ExamPeriodPlacement period = (ExamPeriodPlacement)f.nextElement(); 150 Set rooms = exam.findBestAvailableRooms(period); 151 if (rooms==null) rooms = exam.findRoomsRandom(period, false); 152 if (rooms==null) continue; 153 ExamPlacement value = new ExamPlacement(exam, period, rooms); 154 if (value.equals(assigned)) continue; 155 double eval = iValueWeight*value.toDouble() - assignedVal; 156 if (acceptConflicts) { 157 Set conflicts = model.conflictValues(value); 158 for (Iterator i=conflicts.iterator();i.hasNext();) { 159 Value conflict = (Value)i.next(); 160 eval -= iValueWeight*conflict.toDouble(); 161 eval += iConflictWeight * (1.0+(iStat==null?0.0:iStat.countRemovals(solution.getIteration(), conflict, value))); 162 } 163 } else { 164 if (model.inConflict(value)) continue; 165 } 166 if (iTabu!=null && iTabu.contains(exam.getId()+":"+value.getPeriod().getIndex())) { 167 int un = model.nrUnassignedVariables()-(assigned==null?0:1); 168 if (un>model.getBestUnassignedVariables()) continue; 169 if (un==model.getBestUnassignedVariables() && model.getTotalValue()+eval>=solution.getBestValue()) continue; 170 } 171 if (best==null || bestEval>eval) { 172 if (best==null) 173 best = new Vector(); 174 else 175 best.clear(); 176 best.add(value); 177 bestEval = eval; 178 } else if (bestEval==eval) { 179 best.add(value); 180 } 181 } 182 } 183 184 if (best==null) { 185 sLog.debug(" [tabu] --none--"); 186 iFirstIteration=-1; 187 if (iTabu!=null) iTabu.clear(); 188 return null; 189 } 190 ExamPlacement bestVal = (ExamPlacement)ToolBox.random(best); 191 192 if (sLog.isDebugEnabled()) { 193 Set conflicts = model.conflictValues(bestVal); 194 double wconf = (iStat==null?0.0:iStat.countRemovals(solution.getIteration(), conflicts, bestVal)); 195 sLog.debug(" [tabu] "+bestVal+" ("+(bestVal.variable().getAssignment()==null?"":"was="+bestVal.variable().getAssignment()+", ")+"val="+bestEval+(conflicts.isEmpty()?"":", conf="+(wconf+conflicts.size())+"/"+conflicts)+")"); 196 } 197 198 if (iTabu!=null) 199 iTabu.add(bestVal.variable().getId()+":"+bestVal.getPeriod().getIndex()); 200 201 return new SimpleNeighbour(bestVal.variable(),bestVal); 202 } 203 204 /** 205 * Value selection 206 */ 207 public Value selectValue(Solution solution, Variable variable) { 208 if (iFirstIteration<0) 209 iFirstIteration = solution.getIteration(); 210 long idle = solution.getIteration()-Math.max(iFirstIteration,solution.getBestIteration()); 211 if (idle>iMaxIdleIterations) { 212 sLog.debug(" [tabu] max idle iterations reached"); 213 iFirstIteration=-1; 214 if (iTabu!=null) iTabu.clear(); 215 return null; 216 } 217 if (iTabu!=null && iTabuMaxSize>iTabuMinSize) { 218 if (idle==0) { 219 iTabu.resize(iTabuMinSize); 220 } else if (idle%(iMaxIdleIterations/(iTabuMaxSize-iTabuMinSize))==0) { 221 iTabu.resize(Math.min(iTabuMaxSize,iTabu.size()+1)); 222 } 223 } 224 225 Model model = solution.getModel(); 226 double bestEval = 0.0; 227 Vector best = null; 228 229 Exam exam = (Exam)variable; 230 ExamPlacement assigned = (ExamPlacement)variable.getAssignment(); 231 //double assignedVal = (assigned==null?-iConflictWeight:iValueWeight*assigned.toDouble()); 232 double assignedVal = (assigned==null?iConflictWeight:iValueWeight*assigned.toDouble()); 233 for (Enumeration f=exam.getPeriodPlacements().elements();f.hasMoreElements();) { 234 ExamPeriodPlacement period = (ExamPeriodPlacement)f.nextElement(); 235 Set rooms = exam.findBestAvailableRooms(period); 236 if (rooms==null) rooms = exam.findRoomsRandom(period, false); 237 if (rooms==null) { 238 sLog.info("Exam "+exam.getName()+" has no rooms for period "+period); 239 continue; 240 } 241 ExamPlacement value = new ExamPlacement(exam, period, rooms); 242 if (value.equals(assigned)) continue; 243 Set conflicts = model.conflictValues(value); 244 double eval = iValueWeight*value.toDouble() - assignedVal; 245 for (Iterator i=conflicts.iterator();i.hasNext();) { 246 Value conflict = (Value)i.next(); 247 eval -= iValueWeight*conflict.toDouble(); 248 eval += iConflictWeight * (1.0+(iStat==null?0.0:iStat.countRemovals(solution.getIteration(), conflict, value))); 249 } 250 if (iTabu!=null && iTabu.contains(exam.getId()+":"+value.getPeriod().getIndex())) { 251 int un = model.nrUnassignedVariables()-(assigned==null?0:1); 252 if (un>model.getBestUnassignedVariables()) continue; 253 if (un==model.getBestUnassignedVariables() && model.getTotalValue()+eval>=solution.getBestValue()) continue; 254 } 255 if (best==null || bestEval>eval) { 256 if (best==null) 257 best = new Vector(); 258 else 259 best.clear(); 260 best.add(value); 261 bestEval = eval; 262 } else if (bestEval==eval) { 263 best.add(value); 264 } 265 } 266 267 if (best==null) return null; 268 ExamPlacement bestVal = (ExamPlacement)ToolBox.random(best); 269 270 if (sLog.isDebugEnabled()) { 271 Set conflicts = model.conflictValues(bestVal); 272 double wconf = (iStat==null?0.0:iStat.countRemovals(solution.getIteration(), conflicts, bestVal)); 273 sLog.debug(" [tabu] "+bestVal+" ("+(bestVal.variable().getAssignment()==null?"":"was="+bestVal.variable().getAssignment()+", ")+"val="+bestEval+(conflicts.isEmpty()?"":", conf="+(wconf+conflicts.size())+"/"+conflicts)+")"); 274 } 275 276 if (iTabu!=null) iTabu.add(exam.getId()+":"+bestVal.getPeriod().getIndex()); 277 278 return bestVal; 279 } 280 281 282 /** Tabu-list */ 283 private static class TabuList { 284 private HashSet iList = new HashSet(); 285 private int iSize; 286 private long iIteration = 0; 287 288 public TabuList(int size) { 289 iSize = size; 290 } 291 292 public Object add(Object object) { 293 if (iSize==0) return object; 294 if (contains(object)) { 295 iList.remove(new TabuItem(object, 0)); 296 iList.add(new TabuItem(object, iIteration++)); 297 return null; 298 } else { 299 Object oldest = null; 300 if (iList.size()>=iSize) oldest = removeOldest(); 301 iList.add(new TabuItem(object, iIteration++)); 302 return oldest; 303 } 304 } 305 306 public void resize(int newSize) { 307 iSize = newSize; 308 while (iList.size()>newSize) removeOldest(); 309 } 310 311 public boolean contains(Object object) { 312 return iList.contains(new TabuItem(object,0)); 313 } 314 315 public void clear() { 316 iList.clear(); 317 } 318 319 public int size() { 320 return iSize; 321 } 322 323 public Object removeOldest() { 324 TabuItem oldest = null; 325 for (Iterator i=iList.iterator();i.hasNext();) { 326 TabuItem element = (TabuItem)i.next(); 327 if (oldest==null || oldest.getIteration()>element.getIteration()) 328 oldest = element; 329 } 330 if (oldest==null) return null; 331 iList.remove(oldest); 332 return oldest.getObject(); 333 } 334 335 public String toString() { 336 return new TreeSet(iList).toString(); 337 } 338 } 339 340 /** Tabu item (an item in {@link TabuList}) */ 341 private static class TabuItem implements Comparable { 342 private Object iObject; 343 private long iIteration; 344 public TabuItem(Object object, long iteration) { 345 iObject = object; iIteration = iteration; 346 } 347 public Object getObject() { 348 return iObject; 349 } 350 public long getIteration() { 351 return iIteration; 352 } 353 public boolean equals(Object object) { 354 if (object==null || !(object instanceof TabuItem)) return false; 355 return getObject().equals(((TabuItem)object).getObject()); 356 } 357 public int hashCode() { 358 return getObject().hashCode(); 359 } 360 public int compareTo(Object o) { 361 return Double.compare(getIteration(), ((TabuItem)o).getIteration()); 362 } 363 public String toString() { 364 return getObject().toString(); 365 } 366 } 367 }