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