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