001package org.cpsolver.studentsct.heuristics.selection; 002 003import java.text.DecimalFormat; 004import java.util.ArrayList; 005import java.util.Collection; 006import java.util.Collections; 007import java.util.Comparator; 008import java.util.ConcurrentModificationException; 009import java.util.HashMap; 010import java.util.HashSet; 011import java.util.Iterator; 012import java.util.LinkedList; 013import java.util.List; 014import java.util.Map; 015import java.util.Set; 016 017import org.apache.logging.log4j.Logger; 018import org.cpsolver.ifs.assignment.Assignment; 019import org.cpsolver.ifs.heuristics.NeighbourSelection; 020import org.cpsolver.ifs.model.GlobalConstraint; 021import org.cpsolver.ifs.model.InfoProvider; 022import org.cpsolver.ifs.model.Neighbour; 023import org.cpsolver.ifs.solution.Solution; 024import org.cpsolver.ifs.solver.Solver; 025import org.cpsolver.ifs.solver.SolverListener; 026import org.cpsolver.ifs.util.DataProperties; 027import org.cpsolver.ifs.util.JProf; 028import org.cpsolver.ifs.util.Progress; 029import org.cpsolver.studentsct.StudentSectioningModel; 030import org.cpsolver.studentsct.constraint.DependentCourses; 031import org.cpsolver.studentsct.constraint.LinkedSections; 032import org.cpsolver.studentsct.extension.DistanceConflict; 033import org.cpsolver.studentsct.extension.StudentQuality; 034import org.cpsolver.studentsct.extension.TimeOverlapsCounter; 035import org.cpsolver.studentsct.filter.StudentFilter; 036import org.cpsolver.studentsct.heuristics.studentord.StudentGroupsChoiceRealFirstOrder; 037import org.cpsolver.studentsct.heuristics.studentord.StudentOrder; 038import org.cpsolver.studentsct.model.CourseRequest; 039import org.cpsolver.studentsct.model.Enrollment; 040import org.cpsolver.studentsct.model.FreeTimeRequest; 041import org.cpsolver.studentsct.model.Request; 042import org.cpsolver.studentsct.model.Section; 043import org.cpsolver.studentsct.model.Student; 044import org.cpsolver.studentsct.model.Unavailability; 045import org.cpsolver.studentsct.online.selection.OnlineSectioningCriterion.TimeToAvoid; 046import org.cpsolver.studentsct.weights.StudentWeights; 047 048/** 049 * Section all students using incremental branch & bound (no unassignments). All 050 * students are taken in a random order, for each student a branch & bound 051 * algorithm is used to find his/her best schedule on top of all other existing 052 * student schedules (no enrollment of a different student is unassigned). 053 * 054 * <br> 055 * <br> 056 * Parameters: <br> 057 * <table border='1'><caption>Related Solver Parameters</caption> 058 * <tr> 059 * <th>Parameter</th> 060 * <th>Type</th> 061 * <th>Comment</th> 062 * </tr> 063 * <tr> 064 * <td>Neighbour.BranchAndBoundTimeout</td> 065 * <td>{@link Integer}</td> 066 * <td>Timeout for each neighbour selection (in milliseconds).</td> 067 * </tr> 068 * <tr> 069 * <td>Neighbour.BranchAndBoundMinimizePenalty</td> 070 * <td>{@link Boolean}</td> 071 * <td>If true, section penalties (instead of section values) are minimized: 072 * overall penalty is minimized together with the maximization of the number of 073 * assigned requests and minimization of distance conflicts -- this variant is 074 * to better mimic the case when students can choose their sections (section 075 * times).</td> 076 * </tr> 077 * </table> 078 * <br> 079 * <br> 080 * 081 * @author Tomáš Müller 082 * @version StudentSct 1.3 (Student Sectioning)<br> 083 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 084 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 085 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 086 * <br> 087 * This library is free software; you can redistribute it and/or modify 088 * it under the terms of the GNU Lesser General Public License as 089 * published by the Free Software Foundation; either version 3 of the 090 * License, or (at your option) any later version. <br> 091 * <br> 092 * This library is distributed in the hope that it will be useful, but 093 * WITHOUT ANY WARRANTY; without even the implied warranty of 094 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 095 * Lesser General Public License for more details. <br> 096 * <br> 097 * You should have received a copy of the GNU Lesser General Public 098 * License along with this library; if not see 099 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 100 */ 101 102public class BranchBoundSelection implements NeighbourSelection<Request, Enrollment>, InfoProvider<Request, Enrollment>, SolverListener<Request, Enrollment> { 103 private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(BranchBoundSelection.class); 104 private static DecimalFormat sDF = new DecimalFormat("0.00"); 105 protected int iTimeout = 10000; 106 protected DistanceConflict iDistanceConflict = null; 107 protected TimeOverlapsCounter iTimeOverlaps = null; 108 protected StudentQuality iStudentQuality = null; 109 protected StudentSectioningModel iModel = null; 110 public static boolean sDebug = false; 111 protected LinkedList<Student> iStudents = null; 112 protected boolean iMinimizePenalty = false; 113 protected StudentOrder iOrder = new StudentGroupsChoiceRealFirstOrder(); 114 protected double iDistConfWeight = 1.0; 115 protected boolean iBranchWhenSelectedHasNoConflict = false; 116 protected boolean iTimesToAvoidHeuristics = true; 117 protected StudentFilter iFilter = null; 118 119 protected long iNbrIterations = 0; 120 protected long iTotalTime = 0; 121 protected long iNbrTimeoutReached = 0; 122 protected long iNbrNoSolution = 0; 123 124 /** 125 * Constructor 126 * 127 * @param properties 128 * configuration 129 */ 130 public BranchBoundSelection(DataProperties properties) { 131 iTimeout = properties.getPropertyInt("Neighbour.BranchAndBoundTimeout", iTimeout); 132 iMinimizePenalty = properties.getPropertyBoolean("Neighbour.BranchAndBoundMinimizePenalty", iMinimizePenalty); 133 if (iMinimizePenalty) 134 sLog.info("Overall penalty is going to be minimized (together with the maximization of the number of assigned requests and minimization of distance conflicts)."); 135 if (properties.getProperty("Neighbour.BranchAndBoundOrder") != null) { 136 try { 137 iOrder = (StudentOrder) Class.forName(properties.getProperty("Neighbour.BranchAndBoundOrder")) 138 .getConstructor(new Class[] { DataProperties.class }).newInstance(new Object[] { properties }); 139 } catch (Exception e) { 140 sLog.error("Unable to set student order, reason:" + e.getMessage(), e); 141 } 142 } 143 iDistConfWeight = properties.getPropertyDouble("DistanceConflict.Weight", iDistConfWeight); 144 iBranchWhenSelectedHasNoConflict = properties.getPropertyBoolean("Students.BranchWhenSelectedHasNoConflict", iBranchWhenSelectedHasNoConflict); 145 iTimesToAvoidHeuristics = properties.getPropertyBoolean("OnlineStudentSectioning.TimesToAvoidHeuristics", iTimesToAvoidHeuristics); 146 } 147 148 /** 149 * Initialize 150 * @param solver current solver 151 * @param name phase name 152 */ 153 public void init(Solver<Request, Enrollment> solver, String name) { 154 setModel((StudentSectioningModel) solver.currentSolution().getModel()); 155 Progress.getInstance(solver.currentSolution().getModel()).setPhase(name, iModel.getStudents().size()); 156 iNbrIterations = 0; 157 iNbrTimeoutReached = 0; 158 iNbrNoSolution = 0; 159 iTotalTime = 0; 160 } 161 162 public void setModel(StudentSectioningModel model) { 163 iModel = model; 164 List<Student> students = iOrder.order(iModel.getStudents()); 165 iStudents = new LinkedList<Student>(students); 166 iTimeOverlaps = model.getTimeOverlaps(); 167 iDistanceConflict = model.getDistanceConflict(); 168 iStudentQuality = model.getStudentQuality(); 169 } 170 171 @Override 172 public void init(Solver<Request, Enrollment> solver) { 173 init(solver, "Branch&bound" + (iFilter == null ? "" : " (" + iFilter.getName().toLowerCase() + " students)") + "..."); 174 } 175 176 protected synchronized Student nextStudent() { 177 while (true) { 178 Student student = iStudents.poll(); 179 if (student == null) return null; 180 if (iFilter == null || iFilter.accept(student)) 181 return student; 182 } 183 } 184 185 public synchronized void addStudent(Student student) { 186 if (iStudents != null && !student.isDummy()) iStudents.addFirst(student); 187 } 188 189 /** 190 * Select neighbour. All students are taken, one by one in a random order. 191 * For each student a branch & bound search is employed. 192 */ 193 @Override 194 public Neighbour<Request, Enrollment> selectNeighbour(Solution<Request, Enrollment> solution) { 195 Student student = null; 196 while ((student = nextStudent()) != null) { 197 Progress.getInstance(solution.getModel()).incProgress(); 198 for (int i = 0; i < 5; i++) { 199 try { 200 Neighbour<Request, Enrollment> neighbour = getSelection(solution.getAssignment(), student).select(); 201 if (neighbour != null) return neighbour; 202 break; 203 } catch (ConcurrentModificationException e) {} 204 } 205 } 206 return null; 207 } 208 209 /** 210 * Branch & bound selection for a student 211 * @param assignment current assignment 212 * @param student selected student 213 * @return selection 214 */ 215 public Selection getSelection(Assignment<Request, Enrollment> assignment, Student student) { 216 return new Selection(student, assignment); 217 } 218 219 /** 220 * Branch & bound selection for a student 221 */ 222 public class Selection { 223 /** Student */ 224 protected Student iStudent; 225 /** Start time */ 226 protected long iT0; 227 /** End time */ 228 protected long iT1; 229 /** Was timeout reached */ 230 protected boolean iTimeoutReached; 231 /** Current assignment */ 232 protected Enrollment[] iAssignment; 233 /** Best assignment */ 234 protected Enrollment[] iBestAssignment; 235 /** Best value */ 236 protected double iBestValue; 237 /** Value cache */ 238 protected HashMap<CourseRequest, List<Enrollment>> iValues; 239 /** Current assignment */ 240 protected Assignment<Request, Enrollment> iCurrentAssignment; 241 /** Times to avoid (used when comparing enrollments) */ 242 protected ArrayList<TimeToAvoid> iTimesToAvoid = null; 243 244 /** 245 * Constructor 246 * 247 * @param student 248 * selected student 249 * @param assignment current assignment 250 */ 251 public Selection(Student student, Assignment<Request, Enrollment> assignment) { 252 iStudent = student; 253 iCurrentAssignment = assignment; 254 if (iTimesToAvoidHeuristics) { 255 iTimesToAvoid = new ArrayList<TimeToAvoid>(); 256 for (Request r : iStudent.getRequests()) { 257 if (r instanceof CourseRequest) { 258 List<Enrollment> enrollments = ((CourseRequest) r).getAvaiableEnrollmentsSkipSameTime(assignment); 259 if (enrollments.size() <= 5) { 260 int penalty = (7 - enrollments.size()) * (r.isAlternative() ? 1 : 7 - enrollments.size()); 261 for (Enrollment enrollment : enrollments) 262 for (Section section : enrollment.getSections()) 263 if (section.getTime() != null) 264 iTimesToAvoid.add(new TimeToAvoid(section.getTime(), penalty, r.getPriority())); 265 } 266 } else if (r instanceof FreeTimeRequest) { 267 iTimesToAvoid.add(new TimeToAvoid(((FreeTimeRequest) r).getTime(), 1, Integer.MAX_VALUE)); 268 } 269 } 270 for (Unavailability unavailability: iStudent.getUnavailabilities()) 271 if (unavailability.getTime() != null) 272 iTimesToAvoid.add(new TimeToAvoid(unavailability.getTime(), 1, Integer.MAX_VALUE)); 273 } 274 } 275 276 /** 277 * Execute branch & bound, return the best found schedule for the 278 * selected student. 279 * @return best found schedule for the student 280 */ 281 public BranchBoundNeighbour select() { 282 iT0 = JProf.currentTimeMillis(); 283 iTimeoutReached = false; 284 iAssignment = new Enrollment[iStudent.getRequests().size()]; 285 iBestAssignment = null; 286 iBestValue = 0; 287 288 int i = 0; 289 for (Request r: iStudent.getRequests()) 290 iAssignment[i++] = iCurrentAssignment.getValue(r); 291 saveBest(); 292 for (int j = 0; j < iAssignment.length; j++) 293 iAssignment[j] = null; 294 295 296 iValues = new HashMap<CourseRequest, List<Enrollment>>(); 297 backTrack(0); 298 iT1 = JProf.currentTimeMillis(); 299 300 iNbrIterations ++; 301 iTotalTime += (iT1 - iT0); 302 if (iTimeoutReached) iNbrTimeoutReached ++; 303 if (iBestAssignment == null) iNbrNoSolution ++; 304 305 if (iBestAssignment == null) 306 return null; 307 return new BranchBoundNeighbour(iStudent, iBestValue, iBestAssignment); 308 } 309 310 /** Was timeout reached 311 * @return true if the timeout was reached 312 **/ 313 public boolean isTimeoutReached() { 314 return iTimeoutReached; 315 } 316 317 /** Time (in milliseconds) the branch & bound did run 318 * @return solver time 319 **/ 320 public long getTime() { 321 return iT1 - iT0; 322 } 323 324 /** Best schedule 325 * @return best schedule 326 **/ 327 public Enrollment[] getBestAssignment() { 328 return iBestAssignment; 329 } 330 331 /** Value of the best schedule 332 * @return value of the best schedule 333 **/ 334 public double getBestValue() { 335 return iBestValue; 336 } 337 338 /** Number of requests assigned in the best schedule 339 * @return number of assigned requests in the best schedule 340 **/ 341 public int getBestNrAssigned() { 342 int nrAssigned = 0; 343 for (int i = 0; i < iBestAssignment.length; i++) 344 if (iBestAssignment[i] != null) 345 nrAssigned += (iBestAssignment[i].isCourseRequest() ? 10 : 1); 346 return nrAssigned; 347 } 348 349 /** Bound for the number of assigned requests in the current schedule 350 * @param idx index of the request that is being considered 351 * @return bound for the given request 352 **/ 353 public int getNrAssignedBound(int idx) { 354 int bound = 0; 355 int i = 0, alt = 0; 356 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) { 357 Request r = e.next(); 358 boolean cr = r instanceof CourseRequest; 359 if (i < idx) { 360 if (iAssignment[i] != null) 361 bound += (cr ? 10 : 1); 362 if (r.isAlternative()) { 363 if (iAssignment[i] != null || (cr && ((CourseRequest) r).isWaitlist())) 364 alt--; 365 } else { 366 if (cr && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null) 367 alt++; 368 } 369 } else { 370 if (!r.isAlternative()) 371 bound += (cr ? 10 : 1); 372 else if (alt > 0) { 373 bound += (cr ? 10 : 1); 374 alt--; 375 } 376 } 377 } 378 return bound; 379 } 380 381 /** 382 * Distance conflicts of idx-th assignment of the current 383 * schedule 384 * @param idx index of the request 385 * @return set of distance conflicts 386 */ 387 public Set<DistanceConflict.Conflict> getDistanceConflicts(int idx) { 388 if (iDistanceConflict == null || iAssignment[idx] == null) 389 return null; 390 Set<DistanceConflict.Conflict> dist = iDistanceConflict.conflicts(iAssignment[idx]); 391 for (int x = 0; x < idx; x++) 392 if (iAssignment[x] != null) 393 dist.addAll(iDistanceConflict.conflicts(iAssignment[x], iAssignment[idx])); 394 return dist; 395 } 396 397 /** 398 * Time overlapping conflicts of idx-th assignment of the current 399 * schedule 400 * @param idx index of the request 401 * @return set of time overlapping conflicts 402 */ 403 public Set<TimeOverlapsCounter.Conflict> getTimeOverlappingConflicts(int idx) { 404 if (iTimeOverlaps == null || iAssignment[idx] == null) 405 return null; 406 Set<TimeOverlapsCounter.Conflict> overlaps = new HashSet<TimeOverlapsCounter.Conflict>(); 407 for (int x = 0; x < idx; x++) 408 if (iAssignment[x] != null) 409 overlaps.addAll(iTimeOverlaps.conflicts(iAssignment[x], iAssignment[idx])); 410 else if (iStudent.getRequests().get(x) instanceof FreeTimeRequest) 411 overlaps.addAll(iTimeOverlaps.conflicts(((FreeTimeRequest)iStudent.getRequests().get(x)).createEnrollment(), iAssignment[idx])); 412 overlaps.addAll(iTimeOverlaps.notAvailableTimeConflicts(iAssignment[idx])); 413 return overlaps; 414 } 415 416 public Set<StudentQuality.Conflict> getStudentQualityConflicts(int idx) { 417 if (iStudentQuality == null || iAssignment[idx] == null) 418 return null; 419 420 Set<StudentQuality.Conflict> conflicts = new HashSet<StudentQuality.Conflict>(); 421 for (StudentQuality.Type t: StudentQuality.Type.values()) { 422 conflicts.addAll(iStudentQuality.conflicts(t, iAssignment[idx])); 423 for (int x = 0; x < idx; x++) 424 if (iAssignment[x] != null) 425 conflicts.addAll(iStudentQuality.conflicts(t, iAssignment[x], iAssignment[idx])); 426 } 427 return conflicts; 428 } 429 430 /** 431 * Weight of an assignment. Unlike {@link StudentWeights#getWeight(Assignment, Enrollment, Set, Set)}, only count this side of distance conflicts and time overlaps. 432 * @param enrollment an enrollment 433 * @param distanceConflicts set of distance conflicts 434 * @param timeOverlappingConflicts set of time overlapping conflicts 435 * @return value of the assignment 436 **/ 437 @Deprecated 438 protected double getWeight(Enrollment enrollment, Set<DistanceConflict.Conflict> distanceConflicts, Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts) { 439 double weight = - iModel.getStudentWeights().getWeight(iCurrentAssignment, enrollment); 440 if (distanceConflicts != null) 441 for (DistanceConflict.Conflict c: distanceConflicts) { 442 Enrollment other = (c.getE1().equals(enrollment) ? c.getE2() : c.getE1()); 443 if (other.getRequest().getPriority() <= enrollment.getRequest().getPriority()) 444 weight += iModel.getStudentWeights().getDistanceConflictWeight(iCurrentAssignment, c); 445 } 446 if (timeOverlappingConflicts != null) 447 for (TimeOverlapsCounter.Conflict c: timeOverlappingConflicts) { 448 weight += iModel.getStudentWeights().getTimeOverlapConflictWeight(iCurrentAssignment, enrollment, c); 449 } 450 return enrollment.getRequest().getWeight() * weight; 451 } 452 453 protected double getWeight(Enrollment enrollment, Set<StudentQuality.Conflict> conflicts) { 454 double weight = - iModel.getStudentWeights().getWeight(iCurrentAssignment, enrollment); 455 if (conflicts != null) 456 for (StudentQuality.Conflict c: conflicts) 457 weight += iModel.getStudentWeights().getStudentQualityConflictWeight(iCurrentAssignment, enrollment, c); 458 return enrollment.getRequest().getWeight() * weight; 459 } 460 461 /** Return bound of a request 462 * @param r a request 463 * @return bound 464 **/ 465 protected double getBound(Request r) { 466 return r.getBound(); 467 } 468 469 /** Bound for the current schedule 470 * @param idx index of the request 471 * @return current bound 472 **/ 473 public double getBound(int idx) { 474 double bound = 0.0; 475 int i = 0, alt = 0; 476 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) { 477 Request r = e.next(); 478 if (i < idx) { 479 if (iAssignment[i] != null) { 480 if (iStudentQuality != null) 481 bound += getWeight(iAssignment[i], getStudentQualityConflicts(i)); 482 else 483 bound += getWeight(iAssignment[i], getDistanceConflicts(i), getTimeOverlappingConflicts(i)); 484 } 485 if (r.isAlternative()) { 486 if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist())) 487 alt--; 488 } else { 489 if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null) 490 alt++; 491 } 492 } else { 493 if (!r.isAlternative()) 494 bound += getBound(r); 495 else if (alt > 0) { 496 bound += getBound(r); 497 alt--; 498 } 499 } 500 } 501 return bound; 502 } 503 504 /** Value of the current schedule 505 * @return value of the current schedule 506 **/ 507 public double getValue() { 508 double value = 0.0; 509 for (int i = 0; i < iAssignment.length; i++) 510 if (iAssignment[i] != null) { 511 if (iStudentQuality != null) 512 value += getWeight(iAssignment[i], getStudentQualityConflicts(i)); 513 else 514 value += getWeight(iAssignment[i], getDistanceConflicts(i), getTimeOverlappingConflicts(i)); 515 } 516 return value; 517 } 518 519 /** Assignment penalty 520 * @param i index of the request 521 * @return assignment penalty 522 **/ 523 protected double getAssignmentPenalty(int i) { 524 return iAssignment[i].getPenalty() + iDistConfWeight * getDistanceConflicts(i).size(); 525 } 526 527 /** Penalty of the current schedule 528 * @return penalty of the current schedule 529 **/ 530 public double getPenalty() { 531 double bestPenalty = 0; 532 for (int i = 0; i < iAssignment.length; i++) 533 if (iAssignment[i] != null) 534 bestPenalty += getAssignmentPenalty(i); 535 return bestPenalty; 536 } 537 538 /** Penalty bound of the current schedule 539 * @param idx index of request 540 * @return current penalty bound 541 **/ 542 public double getPenaltyBound(int idx) { 543 double bound = 0.0; 544 int i = 0, alt = 0; 545 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) { 546 Request r = e.next(); 547 if (i < idx) { 548 if (iAssignment[i] != null) 549 bound += getAssignmentPenalty(i); 550 if (r.isAlternative()) { 551 if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist())) 552 alt--; 553 } else { 554 if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null) 555 alt++; 556 } 557 } else { 558 if (!r.isAlternative()) { 559 if (r instanceof CourseRequest) 560 bound += ((CourseRequest) r).getMinPenalty(); 561 } else if (alt > 0) { 562 if (r instanceof CourseRequest) 563 bound += ((CourseRequest) r).getMinPenalty(); 564 alt--; 565 } 566 } 567 } 568 return bound; 569 } 570 571 /** Save the current schedule as the best */ 572 public void saveBest() { 573 if (iBestAssignment == null) 574 iBestAssignment = new Enrollment[iAssignment.length]; 575 for (int i = 0; i < iAssignment.length; i++) 576 iBestAssignment[i] = iAssignment[i]; 577 if (iMinimizePenalty) 578 iBestValue = getPenalty(); 579 else 580 iBestValue = getValue(); 581 } 582 583 /** True if the enrollment is conflicting 584 * @param idx index of request 585 * @param enrollment enrollment in question 586 * @return true if there is a conflict with previous enrollments 587 **/ 588 public boolean inConflict(final int idx, final Enrollment enrollment) { 589 for (GlobalConstraint<Request, Enrollment> constraint : enrollment.variable().getModel().globalConstraints()) 590 if (constraint instanceof DependentCourses) { 591 if (((DependentCourses)constraint).isPartialScheduleInConflict(iStudent, new LinkedSections.EnrollmentAssignment() { 592 @Override 593 public Enrollment getEnrollment(Request request, int index) { 594 return (index == idx ? enrollment : iAssignment[index]); 595 } 596 }, idx)) return true; 597 } else if (constraint.inConflict(iCurrentAssignment, enrollment)) 598 return true; 599 for (LinkedSections linkedSections: iStudent.getLinkedSections()) { 600 if (linkedSections.inConflict(enrollment, new LinkedSections.EnrollmentAssignment() { 601 @Override 602 public Enrollment getEnrollment(Request request, int index) { 603 return (index == idx ? enrollment : iAssignment[index]); 604 } 605 }) != null) return true; 606 } 607 float credit = enrollment.getCredit(); 608 for (int i = 0; i < iAssignment.length; i++) { 609 if (iAssignment[i] != null && i != idx) { 610 credit += iAssignment[i].getCredit(); 611 if (credit > iStudent.getMaxCredit() || iAssignment[i].isOverlapping(enrollment)) 612 return true; 613 } 614 } 615 return false; 616 } 617 618 /** First conflicting enrollment 619 * @param idx index of request 620 * @param enrollment enrollment in question 621 * @return first conflicting enrollment 622 **/ 623 public Enrollment firstConflict(int idx, Enrollment enrollment) { 624 Set<Enrollment> conflicts = enrollment.variable().getModel().conflictValues(iCurrentAssignment, enrollment); 625 if (conflicts.contains(enrollment)) 626 return enrollment; 627 if (!conflicts.isEmpty()) { 628 for (Enrollment conflict : conflicts) { 629 if (!conflict.getStudent().equals(iStudent)) 630 return conflict; 631 } 632 } 633 float credit = enrollment.getCredit(); 634 for (int i = 0; i < iAssignment.length; i++) { 635 if (iAssignment[i] != null && i != idx) { 636 credit += iAssignment[i].getCredit(); 637 if (credit > iStudent.getMaxCredit() || iAssignment[i].isOverlapping(enrollment)) 638 return iAssignment[i]; 639 } 640 } 641 return null; 642 } 643 644 /** True if the given request can be assigned 645 * @param request given request 646 * @param idx index of request 647 * @return true if can be assigned 648 **/ 649 public boolean canAssign(Request request, int idx) { 650 if (iAssignment[idx] != null) 651 return true; 652 int alt = 0; 653 int i = 0; 654 float credit = 0; 655 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) { 656 Request r = e.next(); 657 if (r.equals(request)) 658 credit += r.getMinCredit(); 659 else if (iAssignment[i] != null) 660 credit += iAssignment[i].getCredit(); 661 if (r.equals(request)) 662 continue; 663 if (r.isAlternative()) { 664 if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist())) 665 alt--; 666 } else { 667 if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null) 668 alt++; 669 } 670 } 671 return (!request.isAlternative() || alt > 0) && (credit <= iStudent.getMaxCredit()); 672 } 673 674 /** Number of assigned requests in the current schedule 675 * @return number of assigned requests 676 **/ 677 public int getNrAssigned() { 678 int assigned = 0; 679 for (int i = 0; i < iAssignment.length; i++) 680 if (iAssignment[i] != null) 681 assigned += (iAssignment[i].isCourseRequest() ? 10 : 1); 682 return assigned; 683 } 684 685 /** Returns true if the given request can be left unassigned 686 * @param request given request 687 * @return true if can be left unassigned 688 **/ 689 protected boolean canLeaveUnassigned(Request request) { 690 if (request instanceof CourseRequest && ((CourseRequest)request).getFixedValue() != null) return false; 691 if (request.isMPP() && iModel.getKeepInitialAssignments()) return false; 692 if (iModel.getDependentCoursesConstraint() != null && 693 !iModel.getDependentCoursesConstraint().canLeaveUnassigned(iStudent, new LinkedSections.EnrollmentAssignment() { 694 @Override 695 public Enrollment getEnrollment(Request r, int index) { 696 return iAssignment[index]; 697 } 698 }, request)) return false; 699 return true; 700 } 701 702 /** Returns list of available enrollments for a course request 703 * @param request given request 704 * @return list of enrollments to consider 705 **/ 706 protected List<Enrollment> values(final CourseRequest request) { 707 List<Enrollment> values = request.getAvaiableEnrollments(iCurrentAssignment); 708 Collections.sort(values, new Comparator<Enrollment>() { 709 710 private HashMap<Enrollment, Double> iValues = new HashMap<Enrollment, Double>(); 711 712 private Double value(Enrollment e) { 713 Double value = iValues.get(e); 714 if (value == null) { 715 if (iModel.getStudentQuality() != null) 716 value = iModel.getStudentWeights().getWeight(iCurrentAssignment, e, iModel.getStudentQuality().conflicts(e)); 717 else 718 value = iModel.getStudentWeights().getWeight(iCurrentAssignment, e, 719 (iModel.getDistanceConflict() == null ? null : iModel.getDistanceConflict().conflicts(e)), 720 (iModel.getTimeOverlaps() == null ? null : iModel.getTimeOverlaps().conflicts(e))); 721 iValues.put(e, value); 722 } 723 return value; 724 } 725 726 @Override 727 public int compare(Enrollment e1, Enrollment e2) { 728 if (e1.equals(e2)) return 0; 729 if (e1.equals(iCurrentAssignment.getValue(request))) return -1; 730 if (e2.equals(iCurrentAssignment.getValue(request))) return 1; 731 if (iTimesToAvoid != null) { 732 double o1 = 0.0, o2 = 0.0; 733 for (Section s : e1.getSections()) { 734 if (s.getTime() != null) 735 for (TimeToAvoid avoid : iTimesToAvoid) { 736 if (avoid.priority() > e1.getRequest().getPriority()) 737 o1 += avoid.overlap(s.getTime()); 738 } 739 } 740 for (Section s : e2.getSections()) { 741 if (s.getTime() != null) 742 for (TimeToAvoid avoid : iTimesToAvoid) { 743 if (avoid.priority() > e2.getRequest().getPriority()) 744 o2 += avoid.overlap(s.getTime()); 745 } 746 } 747 if (o1 < o2) 748 return -1; 749 if (o2 < o1) 750 return 1; 751 } 752 Double v1 = value(e1), v2 = value(e2); 753 return v1.equals(v2) ? e1.compareTo(iCurrentAssignment, e2) : v2.compareTo(v1); 754 } 755 756 }); 757 return values; 758 } 759 760 /** branch & bound search 761 * @param idx index of request 762 **/ 763 public void backTrack(int idx) { 764 if (sDebug) 765 sLog.debug("backTrack(" + getNrAssigned() + "/" + getValue() + "," + idx + ")"); 766 if (iTimeout > 0 && (JProf.currentTimeMillis() - iT0) > iTimeout) { 767 if (sDebug) 768 sLog.debug(" -- timeout reached"); 769 iTimeoutReached = true; 770 return; 771 } 772 if (iMinimizePenalty) { 773 if (getBestAssignment() != null 774 && (getNrAssignedBound(idx) < getBestNrAssigned() || (getNrAssignedBound(idx) == getBestNrAssigned() && getPenaltyBound(idx) >= getBestValue()))) { 775 if (sDebug) 776 sLog.debug(" -- branch number of assigned " + getNrAssignedBound(idx) + "<" 777 + getBestNrAssigned() + ", or penalty " + getPenaltyBound(idx) + ">=" + getBestValue()); 778 return; 779 } 780 if (idx == iAssignment.length) { 781 if (getBestAssignment() == null 782 || (getNrAssigned() > getBestNrAssigned() || (getNrAssigned() == getBestNrAssigned() && getPenalty() < getBestValue()))) { 783 if (sDebug) 784 sLog.debug(" -- best solution found " + getNrAssigned() + "/" + getPenalty()); 785 saveBest(); 786 } 787 return; 788 } 789 } else { 790 if (getBestAssignment() != null && getBound(idx) >= getBestValue()) { 791 if (sDebug) 792 sLog.debug(" -- branch " + getBound(idx) + " >= " + getBestValue()); 793 return; 794 } 795 if (idx == iAssignment.length) { 796 if (getBestAssignment() == null || getValue() < getBestValue()) { 797 if (sDebug) 798 sLog.debug(" -- best solution found " + getNrAssigned() + "/" + getValue()); 799 saveBest(); 800 } 801 return; 802 } 803 } 804 805 Request request = iStudent.getRequests().get(idx); 806 if (sDebug) 807 sLog.debug(" -- request: " + request); 808 if (!canAssign(request, idx)) { 809 if (sDebug) 810 sLog.debug(" -- cannot assign"); 811 backTrack(idx + 1); 812 return; 813 } 814 List<Enrollment> values = null; 815 if (request instanceof CourseRequest) { 816 CourseRequest courseRequest = (CourseRequest) request; 817 if (courseRequest.getInitialAssignment() != null && iModel.isMPP()) { 818 Enrollment enrollment = courseRequest.getInitialAssignment(); 819 if (!inConflict(idx, enrollment)) { 820 iAssignment[idx] = enrollment; 821 backTrack(idx + 1); 822 iAssignment[idx] = null; 823 return; 824 } 825 } 826 if (!courseRequest.getSelectedChoices().isEmpty()) { 827 if (sDebug) 828 sLog.debug(" -- selection among selected enrollments"); 829 values = courseRequest.getSelectedEnrollments(iCurrentAssignment, true); 830 if (values != null && !values.isEmpty()) { 831 boolean hasNoConflictValue = false; 832 for (Enrollment enrollment : values) { 833 if (inConflict(idx, enrollment)) 834 continue; 835 hasNoConflictValue = true; 836 if (sDebug) 837 sLog.debug(" -- nonconflicting enrollment found: " + enrollment); 838 iAssignment[idx] = enrollment; 839 backTrack(idx + 1); 840 iAssignment[idx] = null; 841 } 842 if (hasNoConflictValue && iBranchWhenSelectedHasNoConflict) 843 return; 844 } 845 } 846 values = iValues.get(courseRequest); 847 if (values == null) { 848 values = values(courseRequest); 849 iValues.put(courseRequest, values); 850 } 851 } else { 852 values = request.computeEnrollments(iCurrentAssignment); 853 } 854 if (sDebug) { 855 sLog.debug(" -- nrValues: " + values.size()); 856 int vIdx = 1; 857 for (Enrollment enrollment : values) { 858 if (sDebug) 859 sLog.debug(" -- [" + vIdx + "]: " + enrollment); 860 } 861 } 862 boolean hasNoConflictValue = false; 863 for (Enrollment enrollment : values) { 864 if (sDebug) 865 sLog.debug(" -- enrollment: " + enrollment); 866 if (sDebug) { 867 Enrollment conflict = firstConflict(idx, enrollment); 868 if (conflict != null) { 869 sLog.debug(" -- in conflict with: " + conflict); 870 continue; 871 } 872 } else { 873 if (inConflict(idx, enrollment)) continue; 874 } 875 hasNoConflictValue = true; 876 iAssignment[idx] = enrollment; 877 backTrack(idx + 1); 878 iAssignment[idx] = null; 879 } 880 if (canLeaveUnassigned(request) && (!hasNoConflictValue || request instanceof CourseRequest)) 881 backTrack(idx + 1); 882 } 883 } 884 885 /** Branch & bound neighbour -- a schedule of a student */ 886 public static class BranchBoundNeighbour implements Neighbour<Request, Enrollment> { 887 private double iValue; 888 private Enrollment[] iAssignment; 889 private Student iStudent; 890 891 /** 892 * Constructor 893 * 894 * @param student selected student 895 * @param value 896 * value of the schedule 897 * @param assignment 898 * enrollments of student's requests 899 */ 900 public BranchBoundNeighbour(Student student, double value, Enrollment[] assignment) { 901 iValue = value; 902 iAssignment = assignment; 903 iStudent = student; 904 } 905 906 @Override 907 public double value(Assignment<Request, Enrollment> assignment) { 908 return iValue; 909 } 910 911 /** Assignment 912 * @return an enrollment for each request of the student 913 **/ 914 public Enrollment[] getAssignment() { 915 return iAssignment; 916 } 917 918 /** Student 919 * @return selected student 920 **/ 921 public Student getStudent() { 922 return iStudent; 923 } 924 925 /** Assign the schedule */ 926 @Override 927 public void assign(Assignment<Request, Enrollment> assignment, long iteration) { 928 for (int i = 0; i < iAssignment.length; i++) 929 assignment.unassign(iteration, iStudent.getRequests().get(i), iAssignment[i]); 930 for (int i = 0; i < iAssignment.length; i++) 931 if (iAssignment[i] != null) 932 assignment.assign(iteration, iAssignment[i]); 933 } 934 935 @Override 936 public String toString() { 937 StringBuffer sb = new StringBuffer("B&B{ " + iStudent + " " + sDF.format(-iValue * 100.0) + "%"); 938 int idx = 0; 939 for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); idx++) { 940 Request request = e.next(); 941 sb.append("\n " + request); 942 Enrollment enrollment = iAssignment[idx]; 943 if (enrollment == null) 944 sb.append(" -- not assigned"); 945 else 946 sb.append(" -- " + enrollment); 947 } 948 sb.append("\n}"); 949 return sb.toString(); 950 } 951 952 @Override 953 public Map<Request, Enrollment> assignments() { 954 Map<Request, Enrollment> ret = new HashMap<Request, Enrollment>(); 955 for (int i = 0; i < iAssignment.length; i++) 956 ret.put(iStudent.getRequests().get(i), iAssignment[i]); 957 return ret; 958 } 959 960 } 961 962 @Override 963 public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) { 964 if (iNbrIterations > 0) 965 info.put("Timing of " + getClass().getSimpleName(), sDF.format(((double)iTotalTime) / iNbrIterations) + " ms/it (" + 966 iNbrIterations + " iterations, " + 967 (iNbrNoSolution == 0 ? "" : sDF.format(100.0 * iNbrNoSolution / iNbrIterations) + "% no solution, ") + 968 sDF.format(100.0 * iNbrTimeoutReached / iNbrIterations) + "% time limit of " + sDF.format(iTimeout / 1000.0) + " seconds reached)"); 969 } 970 971 @Override 972 public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) { 973 } 974 975 /** 976 * Only consider students meeting the given filter. 977 */ 978 public StudentFilter getFilter() { return iFilter; } 979 980 /** 981 * Only consider students meeting the given filter. 982 */ 983 public BranchBoundSelection withFilter(StudentFilter filter) { iFilter = filter; return this; } 984 985 @Override 986 public boolean variableSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable) { 987 return false; 988 } 989 @Override 990 public boolean valueSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable, Enrollment value) { 991 return false; 992 } 993 @Override 994 public boolean neighbourSelected(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) { 995 return false; 996 } 997 @Override 998 public void neighbourFailed(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) { 999 if (neighbour instanceof BranchBoundNeighbour) 1000 addStudent(((BranchBoundNeighbour)neighbour).getStudent()); 1001 } 1002}