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.HashSet; 008import java.util.Hashtable; 009import java.util.Iterator; 010import java.util.List; 011import java.util.Map; 012import java.util.Set; 013import java.util.TreeSet; 014 015import org.cpsolver.coursett.Constants; 016import org.cpsolver.coursett.model.TimeLocation; 017import org.cpsolver.ifs.assignment.Assignment; 018import org.cpsolver.ifs.util.DataProperties; 019import org.cpsolver.ifs.util.ToolBox; 020import org.cpsolver.studentsct.model.Config; 021import org.cpsolver.studentsct.model.Course; 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.online.OnlineSectioningModel; 029import org.cpsolver.studentsct.online.expectations.OverExpectedCriterion; 030import org.cpsolver.studentsct.online.selection.MultiCriteriaBranchAndBoundSelection.SelectionComparator; 031 032/** 033 * Computation of suggestions using a limited depth branch and bound. 034 * For a given schedule and a selected section, compute 035 * possible changes that will be displayed to the student as suggestions. The number 036 * of suggestions is limited by Suggestions.MaxSuggestions parameter (defaults to 20). 037 * Time is limited by Suggestions.Timeout (defaults to 5000 ms), search depth is limited 038 * by Suggestions.MaxDepth parameter (default to 4). 039 * 040 * @author Tomáš Müller 041 * @version StudentSct 1.3 (Student Sectioning)<br> 042 * Copyright (C) 2014 Tomáš Müller<br> 043 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 044 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 045 * <br> 046 * This library is free software; you can redistribute it and/or modify 047 * it under the terms of the GNU Lesser General Public License as 048 * published by the Free Software Foundation; either version 3 of the 049 * License, or (at your option) any later version. <br> 050 * <br> 051 * This library is distributed in the hope that it will be useful, but 052 * WITHOUT ANY WARRANTY; without even the implied warranty of 053 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 054 * Lesser General Public License for more details. <br> 055 * <br> 056 * You should have received a copy of the GNU Lesser General Public 057 * License along with this library; if not see <a 058 * href='http://www.gnu.org/licenses'>http://www.gnu.org/licenses</a>. 059 * 060 */ 061public class SuggestionsBranchAndBound { 062 private Hashtable<CourseRequest, Set<Section>> iRequiredSections = null; 063 private Set<FreeTimeRequest> iRequiredFreeTimes = null; 064 private Hashtable<CourseRequest, Set<Section>> iPreferredSections = null; 065 private Request iSelectedRequest = null; 066 private Section iSelectedSection = null; 067 private Student iStudent = null; 068 private TreeSet<Suggestion> iSuggestions = new TreeSet<Suggestion>(); 069 private int iMaxDepth = 4; 070 private long iTimeout = 5000; 071 private int iMaxSuggestions = 20; 072 private long iT0, iT1; 073 private boolean iTimeoutReached = false; 074 private int iNrSolutionsSeen = 0; 075 private OnlineSectioningModel iModel; 076 private Assignment<Request, Enrollment> iAssignment; 077 private Hashtable<Request, List<Enrollment>> iValues = new Hashtable<Request, List<Enrollment>>(); 078 private long iLastSuggestionId = 0; 079 private SuggestionFilter iFilter = null; 080 protected SelectionComparator iComparator = null; 081 protected int iMatched = 0; 082 protected double iMaxSectionsWithPenalty = 0; 083 084 /** 085 * Constructor 086 * @param properties configuration 087 * @param student given student 088 * @param assignment current assignment 089 * @param requiredSections required sections 090 * @param requiredFreeTimes required free times (free time requests that must be assigned) 091 * @param preferredSections preferred sections 092 * @param selectedRequest selected request 093 * @param selectedSection selected section 094 * @param filter section filter 095 * @param maxSectionsWithPenalty maximal number of sections that have a positive over-expectation penalty {@link OverExpectedCriterion#getOverExpected(Assignment, Section, Request)} 096 */ 097 public SuggestionsBranchAndBound(DataProperties properties, Student student, 098 Assignment<Request, Enrollment> assignment, Hashtable<CourseRequest, Set<Section>> requiredSections, 099 Set<FreeTimeRequest> requiredFreeTimes, Hashtable<CourseRequest, Set<Section>> preferredSections, 100 Request selectedRequest, Section selectedSection, SuggestionFilter filter, double maxSectionsWithPenalty) { 101 iRequiredSections = requiredSections; 102 iRequiredFreeTimes = requiredFreeTimes; 103 iPreferredSections = preferredSections; 104 iSelectedRequest = selectedRequest; 105 iSelectedSection = selectedSection; 106 iStudent = student; 107 iModel = (OnlineSectioningModel) selectedRequest.getModel(); 108 iAssignment = assignment; 109 iMaxDepth = properties.getPropertyInt("Suggestions.MaxDepth", iMaxDepth); 110 iTimeout = properties.getPropertyLong("Suggestions.Timeout", iTimeout); 111 iMaxSuggestions = properties.getPropertyInt("Suggestions.MaxSuggestions", iMaxSuggestions); 112 iMaxSectionsWithPenalty = maxSectionsWithPenalty; 113 iFilter = filter; 114 iComparator = new SelectionComparator() { 115 private HashMap<Enrollment, Double> iValues = new HashMap<Enrollment, Double>(); 116 117 private Double value(Enrollment e) { 118 Double value = iValues.get(e); 119 if (value == null) { 120 if (iModel.getStudentQuality() != null) 121 value = iModel.getStudentWeights().getWeight(iAssignment, e, iModel.getStudentQuality().conflicts(e)); 122 else 123 value = iModel.getStudentWeights().getWeight(iAssignment, e, 124 (iModel.getDistanceConflict() == null ? null : iModel.getDistanceConflict().conflicts(e)), 125 (iModel.getTimeOverlaps() == null ? null : iModel.getTimeOverlaps().conflicts(e))); 126 iValues.put(e, value); 127 } 128 return value; 129 } 130 131 @Override 132 public int compare(Assignment<Request, Enrollment> a, Enrollment e1, Enrollment e2) { 133 return value(e2).compareTo(value(e1)); 134 } 135 }; 136 } 137 138 /** 139 * Return search time 140 * @return search time 141 */ 142 public long getTime() { 143 return iT1 - iT0; 144 } 145 146 /** 147 * Was time limit reached 148 * @return true if reached 149 */ 150 public boolean isTimeoutReached() { 151 return iTimeoutReached; 152 } 153 154 /** 155 * Number of possible suggestions visited 156 * @return a number of solutions seen 157 */ 158 public int getNrSolutionsSeen() { 159 return iNrSolutionsSeen; 160 } 161 162 /** 163 * Perform the search 164 * @return an ordered set of possible suggestions 165 */ 166 public TreeSet<Suggestion> computeSuggestions() { 167 iT0 = System.currentTimeMillis(); 168 iTimeoutReached = false; 169 iNrSolutionsSeen = 0; 170 iSuggestions.clear(); 171 172 ArrayList<Request> requests2resolve = new ArrayList<Request>(); 173 requests2resolve.add(iSelectedRequest); 174 TreeSet<Request> altRequests2resolve = new TreeSet<Request>(); 175 176 for (Map.Entry<CourseRequest, Set<Section>> entry : iPreferredSections.entrySet()) { 177 CourseRequest request = entry.getKey(); 178 Set<Section> sections = entry.getValue(); 179 if (!sections.isEmpty() && sections.size() == sections.iterator().next().getSubpart().getConfig().getSubparts().size()) 180 iAssignment.assign(0, request.createEnrollment(iAssignment, sections)); 181 else if (!request.equals(iSelectedRequest)) { 182 if (sections.isEmpty()) 183 altRequests2resolve.add(request); 184 else 185 requests2resolve.add(request); 186 } 187 } 188 189 for (Request request : iStudent.getRequests()) { 190 if (iAssignment.getValue(request) == null && request instanceof FreeTimeRequest) { 191 FreeTimeRequest ft = (FreeTimeRequest) request; 192 Enrollment enrollment = ft.createEnrollment(); 193 if (iModel.conflictValues(iAssignment, enrollment).isEmpty()) 194 iAssignment.assign(0, enrollment); 195 } 196 } 197 198 for (Request request : iStudent.getRequests()) { 199 request.setInitialAssignment(iAssignment.getValue(request)); 200 } 201 202 backtrack(requests2resolve, altRequests2resolve, 0, iMaxDepth, false); 203 204 iT1 = System.currentTimeMillis(); 205 return iSuggestions; 206 } 207 208 /** 209 * Main branch and bound rutine 210 * @param requests2resolve remaining requests to assign 211 * @param altRequests2resolve alternative requests to assign 212 * @param idx current depth 213 * @param depth remaining depth 214 * @param alt can leave a request unassigned 215 */ 216 protected void backtrack(ArrayList<Request> requests2resolve, TreeSet<Request> altRequests2resolve, int idx, 217 int depth, boolean alt) { 218 if (!iTimeoutReached && iTimeout > 0 && System.currentTimeMillis() - iT0 > iTimeout) 219 iTimeoutReached = true; 220 int nrUnassigned = requests2resolve.size() - idx; 221 if (nrUnassigned == 0) { 222 List<FreeTimeRequest> okFreeTimes = new ArrayList<FreeTimeRequest>(); 223 double sectionsWithPenalty = 0; 224 for (Request r : iStudent.getRequests()) { 225 Enrollment e = iAssignment.getValue(r); 226 if (iMaxSectionsWithPenalty >= 0 && e != null && r instanceof CourseRequest) { 227 for (Section s : e.getSections()) 228 sectionsWithPenalty += iModel.getOverExpected(iAssignment, s, r); 229 } 230 if (e == null && r instanceof FreeTimeRequest) { 231 FreeTimeRequest ft = (FreeTimeRequest) r; 232 Enrollment enrollment = ft.createEnrollment(); 233 if (iModel.conflictValues(iAssignment, enrollment).isEmpty()) { 234 iAssignment.assign(0, enrollment); 235 okFreeTimes.add(ft); 236 } 237 } 238 if (e != null && e.isCourseRequest() && e.getSections().isEmpty()) { 239 Double minPenalty = null; 240 for (Enrollment other : values(e.getRequest())) { 241 if (!isAllowed(other)) continue; 242 if (e.equals(other)) continue; 243 double penalty = 0.0; 244 for (Section s: other.getSections()) 245 penalty += iModel.getOverExpected(iAssignment, s, other.getRequest()); 246 if (minPenalty == null || minPenalty > penalty) minPenalty = penalty; 247 if (minPenalty == 0.0) break; 248 } 249 if (minPenalty != null) sectionsWithPenalty += minPenalty; 250 } 251 } 252 if (iMaxSectionsWithPenalty >= 0 && sectionsWithPenalty > iMaxSectionsWithPenalty) 253 return; 254 Suggestion s = new Suggestion(requests2resolve); 255 if (iSuggestions.size() >= iMaxSuggestions && iSuggestions.last().compareTo(s) <= 0) 256 return; 257 if (iMatched != 1) { 258 for (Iterator<Suggestion> i = iSuggestions.iterator(); i.hasNext();) { 259 Suggestion x = i.next(); 260 if (x.sameSelectedSection()) { 261 if (x.compareTo(s) <= 0) return; 262 i.remove(); 263 } 264 } 265 } 266 s.init(); 267 iSuggestions.add(s); 268 if (iSuggestions.size() > iMaxSuggestions) 269 iSuggestions.remove(iSuggestions.last()); 270 for (FreeTimeRequest ft : okFreeTimes) 271 iAssignment.unassign(0, ft); 272 return; 273 } 274 if (!canContinue(requests2resolve, idx, depth)) 275 return; 276 Request request = requests2resolve.get(idx); 277 for (Enrollment enrollment : values(request)) { 278 if (!canContinueEvaluation()) 279 break; 280 if (!isAllowed(enrollment)) 281 continue; 282 if (enrollment.equals(iAssignment.getValue(request))) 283 continue; 284 if (enrollment.getAssignments().isEmpty() && alt) 285 continue; 286 Set<Enrollment> conflicts = iModel.conflictValues(iAssignment, enrollment); 287 if (!checkBound(requests2resolve, idx, depth, enrollment, conflicts)) 288 continue; 289 Enrollment current = iAssignment.getValue(request); 290 ArrayList<Request> newVariables2resolve = new ArrayList<Request>(requests2resolve); 291 for (Iterator<Enrollment> i = conflicts.iterator(); i.hasNext();) { 292 Enrollment conflict = i.next(); 293 iAssignment.unassign(0, conflict.variable()); 294 if (!newVariables2resolve.contains(conflict.variable())) 295 newVariables2resolve.add(conflict.variable()); 296 } 297 if (current != null) 298 iAssignment.unassign(0, current.variable()); 299 iAssignment.assign(0, enrollment); 300 if (enrollment.getAssignments().isEmpty()) { 301 if (altRequests2resolve != null && !altRequests2resolve.isEmpty()) { 302 Suggestion lastBefore = (iSuggestions.isEmpty() ? null : iSuggestions.last()); 303 int sizeBefore = iSuggestions.size(); 304 for (Request r : altRequests2resolve) { 305 newVariables2resolve.add(r); 306 backtrack(newVariables2resolve, null, idx + 1, depth, true); 307 newVariables2resolve.remove(r); 308 } 309 Suggestion lastAfter = (iSuggestions.isEmpty() ? null : iSuggestions.last()); 310 int sizeAfter = iSuggestions.size(); 311 // did not succeeded with an alternative -> try without it 312 if (sizeBefore == sizeAfter && (sizeAfter < iMaxSuggestions || sizeAfter == 0 || lastAfter.compareTo(lastBefore) == 0)) 313 backtrack(newVariables2resolve, altRequests2resolve, idx + 1, depth - 1, alt); 314 } else { 315 backtrack(newVariables2resolve, altRequests2resolve, idx + 1, depth - 1, alt); 316 } 317 } else { 318 backtrack(newVariables2resolve, altRequests2resolve, idx + 1, depth - 1, alt); 319 } 320 if (current == null) 321 iAssignment.unassign(0, request); 322 else 323 iAssignment.assign(0, current); 324 for (Iterator<Enrollment> i = conflicts.iterator(); i.hasNext();) { 325 Enrollment conflict = i.next(); 326 iAssignment.assign(0, conflict); 327 } 328 } 329 } 330 331 /** 332 * Domain of a request 333 * @param request given request 334 * @return possible enrollments (meeting the filter etc) 335 */ 336 protected List<Enrollment> values(final Request request) { 337 if (!request.equals(iSelectedRequest) && !request.getStudent().canAssign(iAssignment, request)) { 338 if (canLeaveUnassigned(request)) { 339 List<Enrollment> values = new ArrayList<Enrollment>(); 340 Config config = null; 341 if (request instanceof CourseRequest) 342 config = ((CourseRequest) request).getCourses().get(0).getOffering().getConfigs().get(0); 343 values.add(new Enrollment(request, 0, config, new HashSet<Section>(), iAssignment)); 344 return values; 345 } 346 return new ArrayList<Enrollment>(); 347 } 348 List<Enrollment> values = iValues.get(request); 349 if (values != null) 350 return values; 351 if (request instanceof CourseRequest) { 352 CourseRequest cr = (CourseRequest) request; 353 values = (cr.equals(iSelectedRequest) ? cr.getAvaiableEnrollments(iAssignment) : cr.getAvaiableEnrollmentsSkipSameTime(iAssignment)); 354 if (cr.equals(iSelectedRequest)) { 355 Collections.sort(values, new Comparator<Enrollment>() { 356 @Override 357 public int compare(Enrollment e1, Enrollment e2) { 358 int s1 = 0; 359 for (Section s: e1.getSections()) 360 if (((CourseRequest)iSelectedRequest).isSelected(s)) s1++; 361 int s2 = 0; 362 for (Section s: e2.getSections()) 363 if (((CourseRequest)iSelectedRequest).isSelected(s)) s2++; 364 if (s1 != s2) return (s1 > s2 ? -1 : 1); 365 366 if (e1.getRequest().getInitialAssignment() != null) { 367 Enrollment original = e1.getRequest().getInitialAssignment(); 368 int x1 = 0; 369 if (original.getCourse().equals(e1.getCourse())) x1 += 100; 370 if (original.getConfig().equals(e1.getConfig())) { 371 x1 += 10; 372 for (Section section: original.getSections()) { 373 for (Section s: e1.getSections()) { 374 if (s.getSubpart().getId() == section.getSubpart().getId()) { 375 if (ToolBox.equals(section.getTime(), s.getTime()) && ToolBox.equals(section.getRooms(), s.getRooms())) 376 x1 ++; 377 break; 378 } 379 } 380 } 381 } 382 int x2 = 0; 383 if (original.getCourse().equals(e2.getCourse())) x2 += 100; 384 if (original.getConfig().equals(e2.getConfig())) { 385 x2 += 10; 386 for (Section section: original.getSections()) { 387 for (Section s: e2.getSections()) { 388 if (s.getSubpart().getId() == section.getSubpart().getId()) { 389 if (ToolBox.equals(section.getTime(), s.getTime()) && ToolBox.equals(section.getRooms(), s.getRooms())) 390 x2 ++; 391 break; 392 } 393 } 394 } 395 } 396 if (x1 != x2) { 397 return (x1 > x2 ? -1 : 1); 398 } 399 } 400 401 return iComparator.compare(iAssignment, e1, e2); 402 } 403 }); 404 } else { 405 Collections.sort(values, new Comparator<Enrollment>() { 406 @Override 407 public int compare(Enrollment e1, Enrollment e2) { 408 return iComparator.compare(iAssignment, e1, e2); 409 } 410 }); 411 } 412 } else { 413 values = new ArrayList<Enrollment>(); 414 values.add(((FreeTimeRequest) request).createEnrollment()); 415 } 416 if (canLeaveUnassigned(request)) { 417 Config config = null; 418 if (request instanceof CourseRequest) 419 config = ((CourseRequest) request).getCourses().get(0).getOffering().getConfigs().get(0); 420 values.add(new Enrollment(request, 0, config, new HashSet<Section>(), iAssignment)); 421 } 422 iValues.put(request, values); 423 if (request.equals(iSelectedRequest) && iFilter != null && request instanceof CourseRequest) { 424 for (Iterator<Enrollment> i = values.iterator(); i.hasNext();) { 425 Enrollment enrollment = i.next(); 426 if (enrollment.getAssignments() != null && !enrollment.getAssignments().isEmpty()) { 427 boolean match = false; 428 for (Iterator<Section> j = enrollment.getSections().iterator(); j.hasNext();) { 429 Section section = j.next(); 430 if (iSelectedSection != null) { 431 if (section.getSubpart().getId() == iSelectedSection.getSubpart().getId()) { 432 if (iFilter.match(enrollment.getCourse(), section)) { 433 match = true; 434 break; 435 } 436 } 437 if (section.getSubpart().getConfig().getId() != iSelectedSection.getSubpart().getConfig().getId() && 438 section.getSubpart().getInstructionalType().equals(iSelectedSection.getSubpart().getInstructionalType())) { 439 if (iFilter.match(enrollment.getCourse(), section)) { 440 match = true; 441 break; 442 } 443 } 444 } else { 445 if (iFilter.match(enrollment.getCourse(), section)) { 446 match = true; 447 break; 448 } 449 } 450 } 451 if (!match) 452 i.remove(); 453 } 454 } 455 } 456 if (request.equals(iSelectedRequest)) 457 iMatched = values.size(); 458 return values; 459 } 460 461 /** 462 * Termination criterion 463 * @param requests2resolve request to resolve 464 * @param idx current depth 465 * @param depth remaining depth 466 * @return true if the search can continue 467 */ 468 protected boolean canContinue(ArrayList<Request> requests2resolve, int idx, int depth) { 469 if (depth <= 0) 470 return false; 471 if (iTimeoutReached) 472 return false; 473 return true; 474 } 475 476 /** 477 * Can continue evaluation of possible enrollments 478 * @return false if the timeout was reached 479 */ 480 protected boolean canContinueEvaluation() { 481 return !iTimeoutReached; 482 } 483 484 /** 485 * Check bound 486 * @param requests2resolve requests to resolve 487 * @param idx current depth 488 * @param depth remaining depth 489 * @param value enrollment in question 490 * @param conflicts conflicting enrollments 491 * @return false if the branch can be cut 492 */ 493 protected boolean checkBound(ArrayList<Request> requests2resolve, int idx, int depth, Enrollment value, 494 Set<Enrollment> conflicts) { 495 if (iMaxSectionsWithPenalty < 0.0 && idx > 0 && !conflicts.isEmpty()) return false; 496 int nrUnassigned = requests2resolve.size() - idx; 497 if ((nrUnassigned + conflicts.size() > depth)) { 498 return false; 499 } 500 for (Enrollment conflict : conflicts) { 501 int confIdx = requests2resolve.indexOf(conflict.variable()); 502 if (confIdx >= 0 && confIdx <= idx) 503 return false; 504 } 505 if (iMaxSectionsWithPenalty >= 0) { 506 double sectionsWithPenalty = 0; 507 for (Request r : iStudent.getRequests()) { 508 Enrollment e = iAssignment.getValue(r); 509 if (r.equals(value.variable())) { 510 e = value; 511 } else if (conflicts.contains(e)) { 512 e = null; 513 } 514 if (e != null && e.isCourseRequest()) { 515 sectionsWithPenalty += iModel.getOverExpected(iAssignment, e, value, conflicts); 516 } 517 } 518 if (sectionsWithPenalty > iMaxSectionsWithPenalty) 519 return false; 520 } 521 return true; 522 } 523 524 /** 525 * Check required sections 526 * @param enrollment given enrollment 527 * @return true if the given enrollment allowed 528 */ 529 public boolean isAllowed(Enrollment enrollment) { 530 if (iRequiredSections != null && enrollment.getRequest() instanceof CourseRequest) { 531 // Obey required sections 532 Set<Section> required = iRequiredSections.get(enrollment.getRequest()); 533 if (required != null && !required.isEmpty()) { 534 if (enrollment.getAssignments() == null) 535 return false; 536 for (Section r : required) 537 if (!enrollment.getAssignments().contains(r)) 538 return false; 539 } 540 } 541 if (enrollment.getRequest().equals(iSelectedRequest)) { 542 // Selected request must be assigned 543 if (enrollment.getAssignments() == null || enrollment.getAssignments().isEmpty()) 544 return false; 545 // Selected section must be assigned differently 546 if (iSelectedSection != null && enrollment.getAssignments().contains(iSelectedSection)) 547 return false; 548 } 549 return true; 550 } 551 552 /** 553 * Can this request be left unassigned 554 * @param request given request 555 * @return true if can be left unassigned (there is no requirement) 556 */ 557 public boolean canLeaveUnassigned(Request request) { 558 if (request instanceof CourseRequest) { 559 if (iRequiredSections != null) { 560 // Request with required section must be assigned 561 Set<Section> required = iRequiredSections.get(request); 562 if (required != null && !required.isEmpty()) 563 return false; 564 } 565 } else { 566 // Free time is required 567 if (iRequiredFreeTimes.contains(request)) 568 return false; 569 } 570 // Selected request must be assigned 571 if (request.equals(iSelectedRequest)) 572 return false; 573 return true; 574 } 575 576 /** 577 * Compare two suggestions 578 * @param assignment current assignment 579 * @param s1 first suggestion 580 * @param s2 second suggestion 581 * @return true if s1 is better than s2 582 */ 583 protected int compare(Assignment<Request, Enrollment> assignment, Suggestion s1, Suggestion s2) { 584 return Double.compare(s1.getValue(), s2.getValue()); 585 } 586 587 /** 588 * Suggestion 589 */ 590 public class Suggestion implements Comparable<Suggestion> { 591 private double iValue = 0.0; 592 private int iNrUnassigned = 0; 593 private int iUnassignedPriority = 0; 594 private int iNrChanges = 0; 595 596 private long iId = iLastSuggestionId++; 597 private Enrollment[] iEnrollments; 598 private Section iSelectedEnrollment = null; 599 private boolean iSelectedEnrollmentChangeTime = false; 600 private TreeSet<Section> iSelectedSections = new TreeSet<Section>(new EnrollmentSectionComparator()); 601 private int iSelectedChoice = 0; 602 603 /** 604 * Create suggestion 605 * @param resolvedRequests assigned requests 606 */ 607 public Suggestion(ArrayList<Request> resolvedRequests) { 608 for (Request request : resolvedRequests) { 609 Enrollment enrollment = iAssignment.getValue(request); 610 if (enrollment.getAssignments().isEmpty()) { 611 iNrUnassigned++; 612 iUnassignedPriority += request.getPriority(); 613 } 614 iValue += (enrollment.getAssignments() == null || enrollment.getAssignments().isEmpty() ? 0.0 : enrollment.toDouble(iAssignment, false)); 615 if (request.getInitialAssignment() != null && enrollment.isCourseRequest()) { 616 Enrollment original = request.getInitialAssignment(); 617 for (Iterator<Section> i = enrollment.getSections().iterator(); i.hasNext();) { 618 Section section = i.next(); 619 Section originalSection = null; 620 for (Iterator<Section> j = original.getSections().iterator(); j.hasNext();) { 621 Section x = j.next(); 622 if (x.getSubpart().getId() == section.getSubpart().getId()) { 623 originalSection = x; 624 break; 625 } 626 } 627 if (originalSection == null || !ToolBox.equals(section.getTime(), originalSection.getTime()) 628 || !ToolBox.equals(section.getRooms(), originalSection.getRooms())) 629 iNrChanges++; 630 } 631 if (!enrollment.getCourse().equals(request.getInitialAssignment().getCourse())) 632 iNrChanges += 100 * (1 + enrollment.getTruePriority()); 633 if (!enrollment.getConfig().equals(request.getInitialAssignment().getConfig())) 634 iNrChanges += 10; 635 } 636 } 637 if (iSelectedRequest != null && iSelectedSection != null) { 638 Enrollment enrollment = iAssignment.getValue(iSelectedRequest); 639 if (enrollment.getAssignments() != null && !enrollment.getAssignments().isEmpty()) { 640 for (Iterator<Section> i = enrollment.getSections().iterator(); i.hasNext();) { 641 Section section = i.next(); 642 if (section.getSubpart().getId() == iSelectedSection.getSubpart().getId()) { 643 iSelectedEnrollment = section; 644 break; 645 } 646 if (section.getSubpart().getConfig().getId() != iSelectedSection.getSubpart().getConfig() 647 .getId() 648 && section.getSubpart().getInstructionalType() 649 .equals(iSelectedSection.getSubpart().getInstructionalType())) { 650 iSelectedEnrollment = section; 651 break; 652 } 653 } 654 } 655 } 656 if (iSelectedEnrollment != null) 657 iSelectedEnrollmentChangeTime = !ToolBox.equals(iSelectedEnrollment.getTime(), 658 iSelectedSection.getTime()); 659 if (iSelectedRequest != null) { 660 Enrollment enrollment = iAssignment.getValue(iSelectedRequest); 661 if (enrollment.isCourseRequest() && enrollment.getAssignments() != null 662 && !enrollment.getAssignments().isEmpty()) { 663 iSelectedSections.addAll(enrollment.getSections()); 664 iSelectedChoice = ((CourseRequest)iSelectedRequest).getCourses().indexOf(enrollment.getCourse()); 665 } 666 } 667 } 668 669 /** initialization */ 670 public void init() { 671 iEnrollments = new Enrollment[iStudent.getRequests().size()]; 672 for (int i = 0; i < iStudent.getRequests().size(); i++) { 673 Request r = iStudent.getRequests().get(i); 674 iEnrollments[i] = iAssignment.getValue(r); 675 if (iEnrollments[i] == null) { 676 Config c = null; 677 if (r instanceof CourseRequest) 678 c = ((CourseRequest) r).getCourses().get(0).getOffering().getConfigs().get(0); 679 iEnrollments[i] = new Enrollment(r, 0, c, null, iAssignment); 680 } 681 } 682 } 683 684 /** 685 * Current assignment for the student 686 * @return schedule 687 */ 688 public Enrollment[] getEnrollments() { 689 return iEnrollments; 690 } 691 692 /** 693 * Current value 694 * @return assignment value 695 */ 696 public double getValue() { 697 return iValue; 698 } 699 700 /** 701 * Number of unassigned requests 702 * @return number of unassigned requests 703 */ 704 public int getNrUnassigned() { 705 return iNrUnassigned; 706 } 707 708 /** 709 * Average unassigned priority 710 * @return average priority of unassinged requests 711 */ 712 public double getAverageUnassignedPriority() { 713 return ((double) iUnassignedPriority) / iNrUnassigned; 714 } 715 716 /** 717 * Number of changes in this schedule (comparing to the original one) 718 * @return number of changes 719 */ 720 public int getNrChanges() { 721 return iNrChanges; 722 } 723 724 /** 725 * Is the same section selected (as in the current assignment) 726 * @return true the same section is selected 727 */ 728 public boolean sameSelectedSection() { 729 if (iSelectedRequest != null && iSelectedEnrollment != null) { 730 Enrollment enrollment = iAssignment.getValue(iSelectedRequest); 731 if (enrollment != null && enrollment.getAssignments().contains(iSelectedEnrollment)) 732 return true; 733 if (iSelectedEnrollmentChangeTime && iSelectedSection.getSubpart().getSections().size() > iMaxSuggestions) { 734 Section selectedEnrollment = null; 735 for (Iterator<Section> i = enrollment.getSections().iterator(); i.hasNext();) { 736 Section section = i.next(); 737 if (section.getSubpart().getId() == iSelectedSection.getSubpart().getId()) { 738 selectedEnrollment = section; 739 break; 740 } 741 if (section.getSubpart().getConfig().getId() != iSelectedSection.getSubpart().getConfig().getId() && 742 section.getSubpart().getInstructionalType().equals(iSelectedSection.getSubpart().getInstructionalType())) { 743 selectedEnrollment = section; 744 break; 745 } 746 } 747 if (selectedEnrollment != null && ToolBox.equals(selectedEnrollment.getTime(), iSelectedEnrollment.getTime())) 748 return true; 749 } 750 } 751 return false; 752 } 753 754 @Override 755 public int compareTo(Suggestion suggestion) { 756 int cmp = Double.compare(getNrUnassigned(), suggestion.getNrUnassigned()); 757 if (cmp != 0) 758 return cmp; 759 if (getNrUnassigned() > 0) { 760 cmp = Double.compare(suggestion.getAverageUnassignedPriority(), getAverageUnassignedPriority()); 761 if (cmp != 0) 762 return cmp; 763 } 764 765 if (iSelectedRequest != null && iSelectedRequest instanceof CourseRequest) { 766 int s1 = 0; 767 for (Section s: iSelectedSections) 768 if (((CourseRequest)iSelectedRequest).isSelected(s)) s1++; 769 int s2 = 0; 770 for (Section s: suggestion.iSelectedSections) 771 if (((CourseRequest)iSelectedRequest).isSelected(s)) s2++; 772 if (s1 != s2) { 773 return (s1 > s2 ? -1 : 1); 774 } 775 } 776 777 cmp = Double.compare(getNrChanges(), suggestion.getNrChanges()); 778 if (cmp != 0) 779 return cmp; 780 781 cmp = Double.compare(iSelectedChoice, suggestion.iSelectedChoice); 782 if (cmp != 0) 783 return cmp; 784 785 Iterator<Section> i1 = iSelectedSections.iterator(); 786 Iterator<Section> i2 = suggestion.iSelectedSections.iterator(); 787 SectionAssignmentComparator c = new SectionAssignmentComparator(); 788 while (i1.hasNext() && i2.hasNext()) { 789 cmp = c.compare(i1.next(), i2.next()); 790 if (cmp != 0) 791 return cmp; 792 } 793 794 cmp = compare(iAssignment, this, suggestion); 795 if (cmp != 0) 796 return cmp; 797 798 return Double.compare(iId, suggestion.iId); 799 } 800 } 801 802 /** 803 * Enrollment comparator (used to sort enrollments in a domain). 804 * Selected sections go first. 805 */ 806 public class EnrollmentSectionComparator implements Comparator<Section> { 807 /** 808 * Is section s1 parent of section s2 (or a parent of a parent...) 809 * @param s1 a section 810 * @param s2 a section 811 * @return if there is a parent-child relation between the two sections 812 */ 813 public boolean isParent(Section s1, Section s2) { 814 Section p1 = s1.getParent(); 815 if (p1 == null) 816 return false; 817 if (p1.equals(s2)) 818 return true; 819 return isParent(p1, s2); 820 } 821 822 @Override 823 public int compare(Section a, Section b) { 824 if (iSelectedSection != null && iSelectedSection.getSubpart().getId() == a.getSubpart().getId()) 825 return -1; 826 if (iSelectedSection != null && iSelectedSection.getSubpart().getId() == b.getSubpart().getId()) 827 return 1; 828 829 if (isParent(a, b)) 830 return 1; 831 if (isParent(b, a)) 832 return -1; 833 834 int cmp = a.getSubpart().getInstructionalType().compareToIgnoreCase(b.getSubpart().getInstructionalType()); 835 if (cmp != 0) 836 return cmp; 837 838 return Double.compare(a.getId(), b.getId()); 839 } 840 } 841 842 /** 843 * Section comparator. Order section by time first, then by room. 844 */ 845 public class SectionAssignmentComparator implements Comparator<Section> { 846 @Override 847 public int compare(Section a, Section b) { 848 TimeLocation t1 = (a == null ? null : a.getTime()); 849 TimeLocation t2 = (b == null ? null : b.getTime()); 850 if (t1 != null && t2 != null) { 851 for (int i = 0; i < Constants.DAY_CODES.length; i++) { 852 if ((t1.getDayCode() & Constants.DAY_CODES[i]) != 0) { 853 if ((t2.getDayCode() & Constants.DAY_CODES[i]) == 0) 854 return -1; 855 } else if ((t2.getDayCode() & Constants.DAY_CODES[i]) != 0) { 856 return 1; 857 } 858 } 859 int cmp = Double.compare(t1.getStartSlot(), t2.getStartSlot()); 860 if (cmp != 0) 861 return cmp; 862 } 863 String r1 = (a == null || a.getRooms() == null ? null : a.getRooms().toString()); 864 String r2 = (b == null || b.getRooms() == null ? null : b.getRooms().toString()); 865 if (r1 != null && r2 != null) { 866 int cmp = r1.compareToIgnoreCase(r2); 867 if (cmp != 0) 868 return cmp; 869 } 870 871 return 0; 872 } 873 } 874 875 /** 876 * Number of possible enrollments of the selected request 877 * @return number of possible enrollment of the selected request (meeting the given filter etc.) 878 */ 879 public int getNrMatched() { 880 return iMatched; 881 } 882 883 /** 884 * Suggestion filter. 885 */ 886 public static interface SuggestionFilter { 887 /** 888 * Match the given section 889 * @param course given course 890 * @param section given section 891 * @return true if matching the filter (can be used) 892 */ 893 public boolean match(Course course, Section section); 894 } 895 896}