001package org.cpsolver.studentsct.online.selection; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.Comparator; 006import java.util.HashMap; 007import java.util.Hashtable; 008import java.util.Iterator; 009import java.util.List; 010import java.util.Map; 011import java.util.Set; 012 013import org.cpsolver.ifs.assignment.Assignment; 014import org.cpsolver.ifs.model.GlobalConstraint; 015import org.cpsolver.ifs.util.DataProperties; 016import org.cpsolver.ifs.util.JProf; 017import org.cpsolver.studentsct.constraint.DependentCourses; 018import org.cpsolver.studentsct.constraint.LinkedSections; 019import org.cpsolver.studentsct.heuristics.selection.OnlineSelection; 020import org.cpsolver.studentsct.heuristics.selection.BranchBoundSelection.BranchBoundNeighbour; 021import org.cpsolver.studentsct.model.Config; 022import org.cpsolver.studentsct.model.CourseRequest; 023import org.cpsolver.studentsct.model.Enrollment; 024import org.cpsolver.studentsct.model.FreeTimeRequest; 025import org.cpsolver.studentsct.model.Request; 026import org.cpsolver.studentsct.model.Section; 027import org.cpsolver.studentsct.model.Student; 028import org.cpsolver.studentsct.model.Subpart; 029import org.cpsolver.studentsct.online.OnlineSectioningModel; 030 031/** 032 * Online student sectioning algorithm using multi-criteria selection. It is very 033 * similar to the {@link SuggestionSelection}, however, the {link {@link OnlineSelection} 034 * is used to compare two solutions and for branching. 035 * 036 * @author Tomáš Müller 037 * @version StudentSct 1.3 (Student Sectioning)<br> 038 * Copyright (C) 2014 Tomáš Müller<br> 039 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 040 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 041 * <br> 042 * This library is free software; you can redistribute it and/or modify 043 * it under the terms of the GNU Lesser General Public License as 044 * published by the Free Software Foundation; either version 3 of the 045 * License, or (at your option) any later version. <br> 046 * <br> 047 * This library is distributed in the hope that it will be useful, but 048 * WITHOUT ANY WARRANTY; without even the implied warranty of 049 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 050 * Lesser General Public License for more details. <br> 051 * <br> 052 * You should have received a copy of the GNU Lesser General Public 053 * License along with this library; if not see <a 054 * href='http://www.gnu.org/licenses'>http://www.gnu.org/licenses</a>. 055 * 056 */ 057public class MultiCriteriaBranchAndBoundSelection implements OnlineSectioningSelection { 058 protected int iTimeout = 1000; 059 protected OnlineSectioningModel iModel = null; 060 protected Assignment<Request, Enrollment> iAssignment = null; 061 protected SelectionCriterion iComparator = null; 062 private boolean iPriorityWeighting = true; 063 protected boolean iBranchWhenSelectedHasNoConflict = false; 064 private double iMaxOverExpected = -1.0; 065 066 /** Student */ 067 protected Student iStudent; 068 /** Start time */ 069 protected long iT0; 070 /** End time */ 071 protected long iT1; 072 /** Was timeout reached */ 073 protected boolean iTimeoutReached; 074 /** Current assignment */ 075 protected Enrollment[] iCurrentAssignment; 076 /** Best assignment */ 077 protected Enrollment[] iBestAssignment; 078 /** Value cache */ 079 protected HashMap<CourseRequest, List<Enrollment>> iValues; 080 081 private Set<FreeTimeRequest> iRequiredFreeTimes; 082 private Hashtable<CourseRequest, Set<Section>> iPreferredSections; 083 private Hashtable<CourseRequest, Config> iRequiredConfig = new Hashtable<CourseRequest, Config>(); 084 private Hashtable<CourseRequest, Hashtable<Subpart, Section>> iRequiredSection = new Hashtable<CourseRequest, Hashtable<Subpart, Section>>(); 085 private Set<CourseRequest> iRequiredUnassinged = null; 086 087 public MultiCriteriaBranchAndBoundSelection(DataProperties config) { 088 iTimeout = config.getPropertyInt("Neighbour.BranchAndBoundTimeout", iTimeout); 089 iPriorityWeighting = config.getPropertyBoolean("StudentWeights.PriorityWeighting", iPriorityWeighting); 090 iBranchWhenSelectedHasNoConflict = config.getPropertyBoolean("Students.BranchWhenSelectedHasNoConflict", iBranchWhenSelectedHasNoConflict); 091 } 092 093 @Override 094 public void setModel(OnlineSectioningModel model) { 095 iModel = model; 096 } 097 098 @Override 099 public void setPreferredSections(Hashtable<CourseRequest, Set<Section>> preferredSections) { 100 iPreferredSections = preferredSections; 101 } 102 103 public void setTimeout(int timeout) { 104 iTimeout = timeout; 105 } 106 107 @Override 108 public void setRequiredSections(Hashtable<CourseRequest, Set<Section>> requiredSections) { 109 if (requiredSections != null) { 110 for (Map.Entry<CourseRequest, Set<Section>> entry : requiredSections.entrySet()) { 111 Hashtable<Subpart, Section> subSection = new Hashtable<Subpart, Section>(); 112 iRequiredSection.put(entry.getKey(), subSection); 113 for (Section section : entry.getValue()) { 114 if (subSection.isEmpty()) 115 iRequiredConfig.put(entry.getKey(), section.getSubpart().getConfig()); 116 subSection.put(section.getSubpart(), section); 117 } 118 } 119 } 120 } 121 122 @Override 123 public void setRequiredFreeTimes(Set<FreeTimeRequest> requiredFreeTimes) { 124 iRequiredFreeTimes = requiredFreeTimes; 125 } 126 127 public BranchBoundNeighbour select(Assignment<Request, Enrollment> assignment, Student student, 128 SelectionCriterion comparator) { 129 iStudent = student; 130 iComparator = comparator; 131 iAssignment = assignment; 132 return select(); 133 } 134 135 @Override 136 public BranchBoundNeighbour select(Assignment<Request, Enrollment> assignment, Student student) { 137 SelectionCriterion comparator = null; 138 if (iPriorityWeighting) 139 comparator = new OnlineSectioningCriterion(student, iModel, assignment, iPreferredSections); 140 else 141 comparator = new EqualWeightCriterion(student, iModel, assignment, iPreferredSections); 142 return select(assignment, student, comparator); 143 } 144 145 /** 146 * Execute branch & bound, return the best found schedule for the selected 147 * student. 148 */ 149 public BranchBoundNeighbour select() { 150 iT0 = JProf.currentTimeMillis(); 151 iTimeoutReached = false; 152 iCurrentAssignment = new Enrollment[iStudent.getRequests().size()]; 153 iBestAssignment = null; 154 155 int i = 0; 156 for (Request r : iStudent.getRequests()) 157 iCurrentAssignment[i++] = iAssignment.getValue(r); 158 saveBest(); 159 for (int j = 0; j < iCurrentAssignment.length; j++) 160 iCurrentAssignment[j] = null; 161 162 iValues = new HashMap<CourseRequest, List<Enrollment>>(); 163 backTrack(0); 164 iT1 = JProf.currentTimeMillis(); 165 if (iBestAssignment == null) 166 return null; 167 168 return new BranchBoundNeighbour(iStudent, iComparator.getTotalWeight(iAssignment, iBestAssignment), 169 iBestAssignment); 170 } 171 172 /** Was timeout reached */ 173 public boolean isTimeoutReached() { 174 return iTimeoutReached; 175 } 176 177 /** Time (in milliseconds) the branch & bound did run */ 178 public long getTime() { 179 return iT1 - iT0; 180 } 181 182 /** Save the current schedule as the best */ 183 public void saveBest() { 184 if (iBestAssignment == null) 185 iBestAssignment = new Enrollment[iCurrentAssignment.length]; 186 for (int i = 0; i < iCurrentAssignment.length; i++) 187 iBestAssignment[i] = iCurrentAssignment[i]; 188 } 189 190 /** True if the enrollment is conflicting */ 191 public boolean inConflict(final int idx, final Enrollment enrollment) { 192 for (GlobalConstraint<Request, Enrollment> constraint : enrollment.variable().getModel().globalConstraints()) { 193 if (constraint instanceof DependentCourses) { 194 if (((DependentCourses)constraint).isPartialScheduleInConflict(iStudent, new LinkedSections.EnrollmentAssignment() { 195 @Override 196 public Enrollment getEnrollment(Request request, int index) { 197 return (index == idx ? enrollment : iCurrentAssignment[index]); 198 } 199 }, idx)) return true; 200 } else if (constraint.inConflict(iAssignment, enrollment)) 201 return true; 202 } 203 for (LinkedSections linkedSections : iStudent.getLinkedSections()) { 204 if (linkedSections.inConflict(enrollment, new LinkedSections.EnrollmentAssignment() { 205 @Override 206 public Enrollment getEnrollment(Request request, int index) { 207 return (index == idx ? enrollment : iCurrentAssignment[index]); 208 } 209 }) != null) 210 return true; 211 } 212 float credit = enrollment.getCredit(); 213 for (int i = 0; i < iCurrentAssignment.length; i++) { 214 if (iCurrentAssignment[i] != null && i != idx) { 215 credit += iCurrentAssignment[i].getCredit(); 216 if (credit > iStudent.getMaxCredit() || iCurrentAssignment[i].isOverlapping(enrollment)) 217 return true; 218 } 219 } 220 if (iMaxOverExpected >= 0.0) { 221 double penalty = 0.0; 222 for (int i = 0; i < idx; i++) { 223 if (iCurrentAssignment[i] != null && iCurrentAssignment[i].getAssignments() != null && iCurrentAssignment[i].isCourseRequest()) 224 for (Section section: iCurrentAssignment[i].getSections()) 225 penalty += iModel.getOverExpected(iAssignment, iCurrentAssignment, i, section, iCurrentAssignment[i].getRequest()); 226 } 227 if (enrollment.isCourseRequest()) 228 for (Section section: enrollment.getSections()) 229 penalty += iModel.getOverExpected(iAssignment, iCurrentAssignment, idx, section, enrollment.getRequest()); 230 if (penalty > iMaxOverExpected) return true; 231 } 232 return !isAllowed(idx, enrollment); 233 } 234 235 /** True if the given request can be assigned */ 236 public boolean canAssign(Request request, int idx) { 237 if (iCurrentAssignment[idx] != null) 238 return true; 239 int alt = 0; 240 int i = 0; 241 float credit = 0; 242 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) { 243 Request r = e.next(); 244 if (r.equals(request)) 245 credit += r.getMinCredit(); 246 else if (iCurrentAssignment[i] != null) 247 credit += iCurrentAssignment[i].getCredit(); 248 if (r.equals(request)) 249 continue; 250 if (r.isAlternative()) { 251 if (iCurrentAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist())) 252 alt--; 253 } else { 254 if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iCurrentAssignment[i] == null) 255 alt++; 256 } 257 } 258 return (!request.isAlternative() || alt > 0) && (credit <= request.getStudent().getMaxCredit()); 259 } 260 261 public boolean isAllowed(int idx, Enrollment enrollment) { 262 if (enrollment.isCourseRequest()) { 263 CourseRequest request = (CourseRequest) enrollment.getRequest(); 264 if (iRequiredUnassinged != null && iRequiredUnassinged.contains(request)) return false; 265 Config reqConfig = iRequiredConfig.get(request); 266 if (reqConfig != null) { 267 if (!reqConfig.equals(enrollment.getConfig())) 268 return false; 269 Hashtable<Subpart, Section> reqSections = iRequiredSection.get(request); 270 for (Section section : enrollment.getSections()) { 271 Section reqSection = reqSections.get(section.getSubpart()); 272 if (reqSection == null) 273 continue; 274 if (!section.equals(reqSection)) 275 return false; 276 } 277 } 278 } else if (iRequiredFreeTimes.contains(enrollment.getRequest())) { 279 if (enrollment.getAssignments() == null || enrollment.getAssignments().isEmpty()) 280 return false; 281 } 282 return true; 283 } 284 285 /** Returns true if the given request can be left unassigned */ 286 protected boolean canLeaveUnassigned(Request request) { 287 if (request instanceof CourseRequest) { 288 if (iRequiredConfig.get(request) != null) 289 return false; 290 } else if (iRequiredFreeTimes.contains(request)) 291 return false; 292 if (iModel.getDependentCoursesConstraint() != null && 293 !iModel.getDependentCoursesConstraint().canLeaveUnassigned(iStudent, new LinkedSections.EnrollmentAssignment() { 294 @Override 295 public Enrollment getEnrollment(Request r, int index) { 296 return iCurrentAssignment[index]; 297 } 298 }, request)) return false; 299 return true; 300 } 301 302 /** Returns list of available enrollments for a course request */ 303 protected List<Enrollment> values(final CourseRequest request) { 304 if (iRequiredUnassinged != null && iRequiredUnassinged.contains(request)) 305 return new ArrayList<Enrollment>(); 306 List<Enrollment> values = request.getAvaiableEnrollments(iAssignment, false); 307 Collections.sort(values, new Comparator<Enrollment>() { 308 @Override 309 public int compare(Enrollment o1, Enrollment o2) { 310 return iComparator.compare(iAssignment, o1, o2); 311 } 312 }); 313 return values; 314 } 315 316 /** branch & bound search */ 317 public void backTrack(int idx) { 318 if (iTimeout > 0 && (JProf.currentTimeMillis() - iT0) > iTimeout) { 319 iTimeoutReached = true; 320 return; 321 } 322 if (idx == iCurrentAssignment.length) { 323 if (iBestAssignment == null || iComparator.compare(iAssignment, iCurrentAssignment, iBestAssignment) < 0) 324 saveBest(); 325 return; 326 } else if (iBestAssignment != null 327 && !iComparator.canImprove(iAssignment, idx, iCurrentAssignment, iBestAssignment)) { 328 return; 329 } 330 331 Request request = iStudent.getRequests().get(idx); 332 if (!canAssign(request, idx)) { 333 backTrack(idx + 1); 334 return; 335 } 336 337 List<Enrollment> values = null; 338 if (request instanceof CourseRequest) { 339 CourseRequest courseRequest = (CourseRequest) request; 340 if (!courseRequest.getSelectedChoices().isEmpty()) { 341 values = courseRequest.getSelectedEnrollments(iAssignment, true); 342 if (values != null && !values.isEmpty()) { 343 boolean hasNoConflictValue = false; 344 for (Enrollment enrollment : values) { 345 if (inConflict(idx, enrollment)) 346 continue; 347 hasNoConflictValue = true; 348 iCurrentAssignment[idx] = enrollment; 349 backTrack(idx + 1); 350 iCurrentAssignment[idx] = null; 351 } 352 if (hasNoConflictValue && iBranchWhenSelectedHasNoConflict) 353 return; 354 } 355 } 356 values = iValues.get(courseRequest); 357 if (values == null) { 358 values = values(courseRequest); 359 iValues.put(courseRequest, values); 360 } 361 } else { 362 values = request.computeEnrollments(iAssignment); 363 } 364 365 boolean hasNoConflictValue = false; 366 for (Enrollment enrollment : values) { 367 if (inConflict(idx, enrollment)) 368 continue; 369 hasNoConflictValue = true; 370 iCurrentAssignment[idx] = enrollment; 371 backTrack(idx + 1); 372 iCurrentAssignment[idx] = null; 373 } 374 375 if (canLeaveUnassigned(request) || (!hasNoConflictValue && request instanceof CourseRequest)) 376 backTrack(idx + 1); 377 } 378 379 /** 380 * Enrollment comparator 381 */ 382 public interface SelectionComparator { 383 /** 384 * Compare two enrollments 385 * 386 * @param assignment 387 * current assignment 388 * @param e1 389 * first enrollment 390 * @param e2 391 * second enrollment 392 * @return -1 if the first enrollment is better than the second etc. 393 */ 394 public int compare(Assignment<Request, Enrollment> assignment, Enrollment e1, Enrollment e2); 395 } 396 397 /** 398 * Multi-criteria selection interface. 399 */ 400 public interface SelectionCriterion extends SelectionComparator { 401 /** 402 * Compare two solutions 403 * 404 * @param assignment 405 * current assignment 406 * @param current 407 * current solution 408 * @param best 409 * best known solution 410 * @return true if current solution is better than the best one 411 */ 412 public int compare(Assignment<Request, Enrollment> assignment, Enrollment[] current, Enrollment[] best); 413 414 /** 415 * Bound 416 * 417 * @param assignment 418 * current assignment 419 * @param idx 420 * current request index 421 * @param current 422 * current solution 423 * @param best 424 * best known solution 425 * @return if the current solution can be extended and possibly improve 426 * upon the best known solution 427 */ 428 public boolean canImprove(Assignment<Request, Enrollment> assignment, int idx, Enrollment[] current, 429 Enrollment[] best); 430 431 /** 432 * For backward compatibility, return a weighted sum 433 * 434 * @param assignment 435 * current assignment 436 * @param enrollments 437 * current solution 438 * @return solution weight (weighted sum) 439 */ 440 public double getTotalWeight(Assignment<Request, Enrollment> assignment, Enrollment[] enrollments); 441 } 442 443 @Override 444 public void setRequiredUnassinged(Set<CourseRequest> requiredUnassignedRequests) { 445 iRequiredUnassinged = requiredUnassignedRequests; 446 } 447 448 @Override 449 public void setMaxOverExpected(double maxOverExpected) { 450 iMaxOverExpected = maxOverExpected; 451 } 452}