001 package net.sf.cpsolver.studentsct.extension; 002 003 import java.text.DecimalFormat; 004 import java.util.Enumeration; 005 import java.util.HashSet; 006 import java.util.Iterator; 007 008 import org.apache.log4j.Logger; 009 010 import net.sf.cpsolver.coursett.model.Placement; 011 import net.sf.cpsolver.coursett.model.TimeLocation; 012 import net.sf.cpsolver.ifs.extension.Extension; 013 import net.sf.cpsolver.ifs.model.ModelListener; 014 import net.sf.cpsolver.ifs.model.Value; 015 import net.sf.cpsolver.ifs.model.Variable; 016 import net.sf.cpsolver.ifs.solver.Solver; 017 import net.sf.cpsolver.ifs.util.DataProperties; 018 import net.sf.cpsolver.studentsct.StudentSectioningModel; 019 import net.sf.cpsolver.studentsct.model.CourseRequest; 020 import net.sf.cpsolver.studentsct.model.Enrollment; 021 import net.sf.cpsolver.studentsct.model.Request; 022 import net.sf.cpsolver.studentsct.model.Section; 023 import net.sf.cpsolver.studentsct.model.Student; 024 025 /** 026 * This extension computes student distant conflicts. 027 * Two sections that are attended by the same student are considered in a 028 * distance conflict if they are back-to-back taught in locations 029 * that are two far away. The allowed distance is provided by method 030 * {@link DistanceConflict#getAllowedDistance(TimeLocation)}. 031 * 032 * @see TimeLocation 033 * @see Placement 034 * 035 * <br><br> 036 * 037 * @version 038 * StudentSct 1.1 (Student Sectioning)<br> 039 * Copyright (C) 2007 Tomáš Müller<br> 040 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 041 * Lazenska 391, 76314 Zlin, Czech Republic<br> 042 * <br> 043 * This library is free software; you can redistribute it and/or 044 * modify it under the terms of the GNU Lesser General Public 045 * License as published by the Free Software Foundation; either 046 * version 2.1 of the License, or (at your option) any later version. 047 * <br><br> 048 * This library is distributed in the hope that it will be useful, 049 * but WITHOUT ANY WARRANTY; without even the implied warranty of 050 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 051 * Lesser General Public License for more details. 052 * <br><br> 053 * You should have received a copy of the GNU Lesser General Public 054 * License along with this library; if not, write to the Free Software 055 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 056 */ 057 058 public class DistanceConflict extends Extension implements ModelListener { 059 private static Logger sLog = Logger.getLogger(DistanceConflict.class); 060 private static DecimalFormat sDF = new DecimalFormat("0.000"); 061 private double iTotalNrConflicts = 0; 062 private HashSet iAllConflicts = new HashSet(); 063 /** Debug flag */ 064 public static boolean sDebug = false; 065 private Variable iOldVariable = null; 066 067 /** 068 * Constructor. 069 * Beside of other thigs, this constructor also uses 070 * {@link StudentSectioningModel#setDistanceConflict(DistanceConflict)} to 071 * set the this instance to the model. 072 * @param solver constraint solver 073 * @param properties configuration 074 */ 075 public DistanceConflict(Solver solver, DataProperties properties) { 076 super(solver, properties); 077 if (solver!=null) 078 ((StudentSectioningModel)solver.currentSolution().getModel()).setDistanceConflict(this); 079 } 080 081 /** 082 * Initialize extension 083 */ 084 public boolean init(Solver solver) { 085 iTotalNrConflicts = countTotalNrConflicts(); 086 return true; 087 } 088 089 public String toString() { 090 return "DistanceConstraint"; 091 } 092 093 /** 094 * Return true if the given two sections are in distance conflict. 095 * This means that the sections are back-to-back and that they are 096 * placed in locations that are two far. 097 * @param s1 a section 098 * @param s2 a section 099 * @return true, if the given sections are in a distance conflict 100 */ 101 public boolean inConflict(Section s1, Section s2) { 102 if (s1.getPlacement()==null || s2.getPlacement()==null) return false; 103 TimeLocation t1 = s1.getTime(); 104 TimeLocation t2 = s2.getTime(); 105 if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) return false; 106 int a1 = t1.getStartSlot(), a2 = t2.getStartSlot(); 107 if (a1+t1.getNrSlotsPerMeeting()==a2) { 108 double dist = Placement.getDistance(s1.getPlacement(), s2.getPlacement()); 109 if (dist>getAllowedDistance(t1)) return true; 110 } else if (a2+t2.getNrSlotsPerMeeting()==a1) { 111 double dist = Placement.getDistance(s1.getPlacement(), s2.getPlacement()); 112 if (dist>getAllowedDistance(t2)) return true; 113 } 114 return false; 115 } 116 117 /** 118 * Return number of distance conflict of a (course) enrollment. 119 * It is the number of pairs of assignments of the enrollment 120 * that are in a distance conflict, weighted by the 121 * request's weight (see {@link Request#getWeight()}). 122 * @param e1 an enrollment 123 * @return number of distance conflicts 124 */ 125 public double nrConflicts(Enrollment e1) { 126 if (!e1.isCourseRequest()) return 0; 127 double cnt = 0; 128 for (Iterator i1=e1.getAssignments().iterator();i1.hasNext();) { 129 Section s1 = (Section)i1.next(); 130 for (Iterator i2=e1.getAssignments().iterator();i2.hasNext();) { 131 Section s2 = (Section)i2.next(); 132 if (s1.getId()<s2.getId() && inConflict(s1,s2)) cnt+=e1.getRequest().getWeight(); 133 } 134 } 135 return cnt; 136 } 137 138 /** 139 * Return number of distance conflicts that are between two enrollments. 140 * It is the number of pairs of assignments of these enrollments 141 * that are in a distance conflict, weighted by the average 142 * (see {@link DistanceConflict#avg(double, double)}) of the requests' 143 * weight (see {@link Request#getWeight()}). 144 * @param e1 an enrollment 145 * @param e2 an enrollment 146 * @return number of distance conflict between given enrollments 147 */ 148 public double nrConflicts(Enrollment e1, Enrollment e2) { 149 if (!e1.isCourseRequest() || !e2.isCourseRequest() || !e1.getStudent().equals(e2.getStudent())) return 0; 150 double cnt = 0; 151 for (Iterator i1=e1.getAssignments().iterator();i1.hasNext();) { 152 Section s1 = (Section)i1.next(); 153 for (Iterator i2=e2.getAssignments().iterator();i2.hasNext();) { 154 Section s2 = (Section)i2.next(); 155 if (inConflict(s1,s2)) cnt+=avg(e1.getRequest().getWeight(),e2.getRequest().getWeight()); 156 } 157 } 158 return cnt; 159 } 160 161 /** 162 * Return a set of distance conflicts ({@link Conflict} objects) of a (course) enrollment. 163 * @param e1 an enrollment 164 * @return list of distance conflicts that are between assignment of the given enrollment 165 */ 166 public HashSet conflicts(Enrollment e1) { 167 HashSet ret = new HashSet(); 168 if (!e1.isCourseRequest()) return ret; 169 int cnt = 0; 170 for (Iterator i1=e1.getAssignments().iterator();i1.hasNext();) { 171 Section s1 = (Section)i1.next(); 172 for (Iterator i2=e1.getAssignments().iterator();i2.hasNext();) { 173 Section s2 = (Section)i2.next(); 174 if (s1.getId()<s2.getId() && inConflict(s1,s2)) 175 ret.add(new Conflict(e1.getRequest().getWeight(),e1.getStudent(),s1,s2)); 176 } 177 } 178 return ret; 179 } 180 181 /** 182 * Return a set of distance conflicts ({@link Conflict} objects) between given (course) enrollments. 183 * @param e1 an enrollment 184 * @param e2 an enrollment 185 * @return list of distance conflicts that are between assignment of the given enrollments 186 */ 187 public HashSet conflicts(Enrollment e1, Enrollment e2) { 188 HashSet ret = new HashSet(); 189 if (!e1.isCourseRequest() || !e2.isCourseRequest() || !e1.getStudent().equals(e2.getStudent())) return ret; 190 int cnt = 0; 191 for (Iterator i1=e1.getAssignments().iterator();i1.hasNext();) { 192 Section s1 = (Section)i1.next(); 193 for (Iterator i2=e2.getAssignments().iterator();i2.hasNext();) { 194 Section s2 = (Section)i2.next(); 195 if (inConflict(s1,s2)) 196 ret.add(new Conflict(avg(e1.getRequest().getWeight(),e2.getRequest().getWeight()),e1.getStudent(),s1,s2)); 197 } 198 } 199 return ret; 200 } 201 202 /** 203 * Allowed distance for the course that follows the given time assignment. 204 * @param time a time assignment of the first of two sections that are back-to-back 205 * @return the maximal allowed distance 206 */ 207 public double getAllowedDistance(TimeLocation time) { 208 if (time.getBreakTime()>=15) return 100.0; 209 return 67.0; 210 } 211 212 /** 213 * Total sum of all conflict of the given enrollment and other enrollments that are assignmed to the same student. 214 */ 215 public double nrAllConflicts(Enrollment enrollment) { 216 if (!enrollment.isCourseRequest()) return 0; 217 double cnt = nrConflicts(enrollment); 218 for (Enumeration e=enrollment.getStudent().getRequests().elements(); e.hasMoreElements();) { 219 Request request = (Request)e.nextElement(); 220 if (request.equals(enrollment.getRequest()) || request.getAssignment()==null || request.equals(iOldVariable)) continue; 221 cnt += nrConflicts(enrollment, (Enrollment)request.getAssignment()); 222 } 223 return cnt; 224 } 225 226 /** 227 * The set of all conflicts ({@link Conflict} objects) of the given enrollment and 228 * other enrollments that are assignmed to the same student. 229 */ 230 public HashSet allConflicts(Enrollment enrollment) { 231 HashSet ret = new HashSet(); 232 if (!enrollment.isCourseRequest()) return ret; 233 for (Enumeration e=enrollment.getStudent().getRequests().elements(); e.hasMoreElements();) { 234 Request request = (Request)e.nextElement(); 235 if (request.equals(enrollment.getRequest()) || request.getAssignment()==null) continue; 236 ret.addAll(conflicts(enrollment, (Enrollment)request.getAssignment())); 237 } 238 return ret; 239 } 240 241 /** 242 * Called when a value is assigned to a variable. 243 * Internal number of distance conflicts is updated, see {@link DistanceConflict#getTotalNrConflicts()}. 244 */ 245 public void assigned(long iteration, Value value) { 246 double inc = nrAllConflicts((Enrollment)value); 247 iTotalNrConflicts += inc; 248 if (sDebug) { 249 sLog.debug("A:"+value); 250 HashSet allConfs = computeAllConflicts(); 251 double allConfWeight = 0.0; 252 for (Iterator i=allConfs.iterator();i.hasNext();) 253 allConfWeight += ((Conflict)i.next()).getWeight(); 254 if (Math.abs(iTotalNrConflicts-allConfWeight)>0.0001) { 255 sLog.error("Different number of conflicts "+sDF.format(iTotalNrConflicts)+"!="+sDF.format(allConfWeight)); 256 for (Iterator i=allConfs.iterator();i.hasNext();) { 257 Conflict c = (Conflict)i.next(); 258 if (!iAllConflicts.contains(c)) 259 sLog.debug(" +add+ "+c); 260 } 261 for (Iterator i=iAllConflicts.iterator();i.hasNext();) { 262 Conflict c = (Conflict)i.next(); 263 if (!allConfs.contains(c)) 264 sLog.debug(" -rem- "+c); 265 } 266 iTotalNrConflicts = allConfWeight; 267 } 268 iAllConflicts = allConfs; 269 if (inc!=0) { 270 sLog.debug("-- DC+"+sDF.format(inc)+" A: "+value); 271 HashSet confs = allConflicts((Enrollment)value); 272 for (Iterator i=confs.iterator();i.hasNext();) 273 sLog.debug(" -- "+i.next()); 274 } 275 } 276 } 277 278 /** 279 * Called when a value is unassigned from a variable. 280 * Internal number of distance conflicts is updated, see {@link DistanceConflict#getTotalNrConflicts()}. 281 */ 282 public void unassigned(long iteration, Value value) { 283 if (value.variable().equals(iOldVariable)) return; 284 double dec = nrAllConflicts((Enrollment)value); 285 iTotalNrConflicts -= dec; 286 if (sDebug) { 287 sLog.debug("U:"+value); 288 if (dec!=0) { 289 sLog.debug("-- DC-"+sDF.format(dec)+" U: "+value); 290 HashSet confs = allConflicts((Enrollment)value); 291 for (Iterator i=confs.iterator();i.hasNext();) 292 sLog.debug(" -- "+i.next()); 293 } 294 } 295 } 296 297 /** Actual number of all distance conflicts */ 298 public double getTotalNrConflicts() { 299 return iTotalNrConflicts; 300 } 301 302 /** 303 * Compute the actual number of all distance conflicts. 304 * Should be equal to {@link DistanceConflict#getTotalNrConflicts()}. 305 */ 306 public double countTotalNrConflicts() { 307 double total = 0; 308 for (Enumeration e=getModel().variables().elements();e.hasMoreElements();) { 309 Request r1 = (Request)e.nextElement(); 310 if (r1.getAssignment()==null || !(r1 instanceof CourseRequest)) continue; 311 total += nrConflicts((Enrollment)r1.getAssignment()); 312 for (Enumeration f=r1.getStudent().getRequests().elements();f.hasMoreElements();) { 313 Request r2 = (Request)f.nextElement(); 314 if (r2.getAssignment()==null || r1.getId()>=r2.getId() || !(r2 instanceof CourseRequest)) continue; 315 total += nrConflicts((Enrollment)r1.getAssignment(), (Enrollment)r2.getAssignment()); 316 } 317 } 318 return total; 319 } 320 321 /** 322 * Compute a set of all distance conflicts ({@link Conflict} objects). 323 */ 324 public HashSet computeAllConflicts() { 325 HashSet ret = new HashSet(); 326 for (Enumeration e=getModel().variables().elements();e.hasMoreElements();) { 327 Request r1 = (Request)e.nextElement(); 328 if (r1.getAssignment()==null || !(r1 instanceof CourseRequest)) continue; 329 ret.addAll(conflicts((Enrollment)r1.getAssignment())); 330 for (Enumeration f=r1.getStudent().getRequests().elements();f.hasMoreElements();) { 331 Request r2 = (Request)f.nextElement(); 332 if (r2.getAssignment()==null || r1.getId()>=r2.getId() || !(r2 instanceof CourseRequest)) continue; 333 ret.addAll(conflicts((Enrollment)r1.getAssignment(), (Enrollment)r2.getAssignment())); 334 } 335 } 336 return ret; 337 } 338 339 /** 340 * Quadratic average of two weights. 341 */ 342 public double avg(double w1, double w2) { 343 return Math.sqrt(w1*w2); 344 } 345 346 /** 347 * Called before a value is assigned to a variable. 348 */ 349 public void beforeAssigned(long iteration, Value value) { 350 if (value!=null) { 351 if (value.variable().getAssignment()!=null) 352 unassigned(iteration, value.variable().getAssignment()); 353 iOldVariable=value.variable(); 354 } 355 } 356 357 /** 358 * Called after a value is assigned to a variable. 359 */ 360 public void afterAssigned(long iteration, Value value) { 361 iOldVariable=null; 362 if (value!=null) assigned(iteration, value); 363 } 364 365 /** 366 * Called after a value is unassigned from a variable. 367 */ 368 public void afterUnassigned(long iteration, Value value) { 369 if (value!=null) 370 unassigned(iteration, value); 371 } 372 373 /** A representation of a distance conflict */ 374 public class Conflict { 375 private double iWeight; 376 private Student iStudent; 377 private Section iS1, iS2; 378 private int iHashCode; 379 380 /** 381 * Constructor 382 * @param weight conflict weight 383 * @param student related student 384 * @param s1 first conflicting section 385 * @param s2 second conflicting section 386 */ 387 public Conflict(double weight, Student student, Section s1, Section s2) { 388 iWeight = weight; 389 iStudent = student; 390 if (s1.getId()<s2.getId()) { 391 iS1 = s1; iS2 = s2; 392 } else { 393 iS1 = s2; iS2 = s1; 394 } 395 iHashCode = (iStudent.getId()+":"+iS1.getId()+":"+iS2.getId()).hashCode(); 396 } 397 398 /** Conflict weight */ 399 public double getWeight() { 400 return iWeight; 401 } 402 403 /** Related student */ 404 public Student getStudent() { 405 return iStudent; 406 } 407 408 /** First section */ 409 public Section getS1() { 410 return iS1; 411 } 412 413 /** Second section */ 414 public Section getS2() { 415 return iS2; 416 } 417 418 public int hashCode() { 419 return iHashCode; 420 } 421 422 /** The distance between conflicting sections*/ 423 public double getDistance() { 424 return Placement.getDistance(getS1().getPlacement(),getS2().getPlacement()); 425 } 426 427 public boolean equals(Object o) { 428 if (o==null || !(o instanceof Conflict)) 429 return false; 430 Conflict c = (Conflict)o; 431 return getStudent().getId()==c.getStudent().getId() && 432 getS1().getId()==c.getS1().getId() && 433 getS2().getId()==c.getS2().getId(); 434 } 435 436 public String toString() { 437 return getStudent()+": (w:"+sDF.format(getWeight())+",d:"+sDF.format(10.0*getDistance())+"m) "+getS1()+" -- "+getS2(); 438 } 439 } 440 }