001package org.cpsolver.studentsct.model; 002 003import java.text.DecimalFormat; 004import java.util.HashSet; 005import java.util.Iterator; 006import java.util.Set; 007 008import org.cpsolver.ifs.assignment.Assignment; 009import org.cpsolver.ifs.model.Value; 010import org.cpsolver.ifs.util.ToolBox; 011import org.cpsolver.studentsct.StudentSectioningModel; 012import org.cpsolver.studentsct.extension.DistanceConflict; 013import org.cpsolver.studentsct.extension.StudentQuality; 014import org.cpsolver.studentsct.extension.TimeOverlapsCounter; 015import org.cpsolver.studentsct.reservation.Reservation; 016 017 018/** 019 * Representation of an enrollment of a student into a course. A student needs 020 * to be enrolled in a section of each subpart of a selected configuration. When 021 * parent-child relation is defined among sections, if a student is enrolled in 022 * a section that has a parent section defined, he/she has be enrolled in the 023 * parent section as well. Also, the selected sections cannot overlap in time. <br> 024 * <br> 025 * 026 * @version StudentSct 1.3 (Student Sectioning)<br> 027 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 028 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 029 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 030 * <br> 031 * This library is free software; you can redistribute it and/or modify 032 * it under the terms of the GNU Lesser General Public License as 033 * published by the Free Software Foundation; either version 3 of the 034 * License, or (at your option) any later version. <br> 035 * <br> 036 * This library is distributed in the hope that it will be useful, but 037 * WITHOUT ANY WARRANTY; without even the implied warranty of 038 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 039 * Lesser General Public License for more details. <br> 040 * <br> 041 * You should have received a copy of the GNU Lesser General Public 042 * License along with this library; if not see 043 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 044 */ 045 046public class Enrollment extends Value<Request, Enrollment> { 047 private static DecimalFormat sDF = new DecimalFormat("0.000"); 048 private Request iRequest = null; 049 private Config iConfig = null; 050 private Course iCourse = null; 051 private Set<? extends SctAssignment> iAssignments = null; 052 private Double iCachedPenalty = null; 053 private int iPriority = 0; 054 private boolean iNoReservationPenalty = false; 055 private Reservation iReservation = null; 056 private Long iTimeStamp = null; 057 private String iApproval = null; 058 059 /** 060 * Constructor 061 * 062 * @param request 063 * course / free time request 064 * @param priority 065 * zero for the course, one for the first alternative, two for the second alternative 066 * @param noReservationPenalty 067 * when true +1 is added to priority (prefer enrollments with reservations) 068 * @param course 069 * selected course 070 * @param config 071 * selected configuration 072 * @param assignments 073 * valid list of sections 074 * @param reservation used reservation 075 */ 076 public Enrollment(Request request, int priority, boolean noReservationPenalty, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) { 077 super(request); 078 iRequest = request; 079 iConfig = config; 080 iAssignments = assignments; 081 iPriority = priority; 082 iCourse = course; 083 iNoReservationPenalty = noReservationPenalty; 084 if (iConfig != null && iCourse == null) 085 for (Course c: ((CourseRequest)iRequest).getCourses()) { 086 if (c.getOffering().getConfigs().contains(iConfig)) { 087 iCourse = c; 088 break; 089 } 090 } 091 iReservation = reservation; 092 } 093 094 /** 095 * Constructor 096 * 097 * @param request 098 * course / free time request 099 * @param priority 100 * zero for the course, one for the first alternative, two for the second alternative 101 * @param course 102 * selected course 103 * @param config 104 * selected configuration 105 * @param assignments 106 * valid list of sections 107 * @param reservation used reservation 108 */ 109 public Enrollment(Request request, int priority, Course course, Config config, Set<? extends SctAssignment> assignments, Reservation reservation) { 110 this(request, priority, false, course, config, assignments, reservation); 111 } 112 113 /** 114 * Constructor 115 * 116 * @param request 117 * course / free time request 118 * @param priority 119 * zero for the course, one for the first alternative, two for the second alternative 120 * @param config 121 * selected configuration 122 * @param assignments 123 * valid list of sections 124 * @param assignment current assignment (to guess the reservation) 125 */ 126 public Enrollment(Request request, int priority, Config config, Set<? extends SctAssignment> assignments, Assignment<Request, Enrollment> assignment) { 127 this(request, priority, null, config, assignments, null); 128 if (assignments != null && assignment != null) 129 guessReservation(assignment, true); 130 } 131 132 /** 133 * Guess the reservation based on the enrollment 134 * @param assignment current assignment 135 * @param onlyAvailable use only reservation that have some space left in them 136 */ 137 public void guessReservation(Assignment<Request, Enrollment> assignment, boolean onlyAvailable) { 138 if (iCourse != null) { 139 Reservation best = null; 140 for (Reservation reservation: ((CourseRequest)iRequest).getReservations(iCourse)) { 141 if (reservation.isIncluded(this)) { 142 if (onlyAvailable && reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < iRequest.getWeight() && !reservation.canBatchAssignOverLimit()) 143 continue; 144 if (best == null || best.getPriority() > reservation.getPriority()) { 145 best = reservation; 146 } else if (best.getPriority() == reservation.getPriority() && 147 best.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest) < reservation.getContext(assignment).getReservedAvailableSpace(assignment, iConfig, iRequest)) { 148 best = reservation; 149 } 150 } 151 } 152 iReservation = best; 153 } 154 } 155 156 /** Student 157 * @return student 158 **/ 159 public Student getStudent() { 160 return iRequest.getStudent(); 161 } 162 163 /** Request 164 * @return request 165 **/ 166 public Request getRequest() { 167 return iRequest; 168 } 169 170 /** True if the request is course request 171 * @return true if the request if course request 172 **/ 173 public boolean isCourseRequest() { 174 return iConfig != null; 175 } 176 177 /** Offering of the course request 178 * @return offering of the course request 179 **/ 180 public Offering getOffering() { 181 return (iConfig == null ? null : iConfig.getOffering()); 182 } 183 184 /** Config of the course request 185 * @return config of the course request 186 **/ 187 public Config getConfig() { 188 return iConfig; 189 } 190 191 /** Course of the course request 192 * @return course of the course request 193 **/ 194 public Course getCourse() { 195 return iCourse; 196 } 197 198 /** List of assignments (selected sections) 199 * @return assignments (selected sections) 200 **/ 201 @SuppressWarnings("unchecked") 202 public Set<SctAssignment> getAssignments() { 203 return (Set<SctAssignment>) iAssignments; 204 } 205 206 /** List of sections (only for course request) 207 * @return selected sections 208 **/ 209 @SuppressWarnings("unchecked") 210 public Set<Section> getSections() { 211 if (isCourseRequest()) 212 return (Set<Section>) iAssignments; 213 return new HashSet<Section>(); 214 } 215 216 /** True when this enrollment is overlapping with the given enrollment 217 * @param enrl other enrollment 218 * @return true if there is an overlap 219 **/ 220 public boolean isOverlapping(Enrollment enrl) { 221 if (enrl == null || isAllowOverlap() || enrl.isAllowOverlap()) 222 return false; 223 for (SctAssignment a : getAssignments()) { 224 if (a.isOverlapping(enrl.getAssignments())) 225 return true; 226 } 227 return false; 228 } 229 230 /** Percent of sections that are wait-listed 231 * @return percent of sections that are wait-listed 232 **/ 233 public double percentWaitlisted() { 234 if (!isCourseRequest()) 235 return 0.0; 236 CourseRequest courseRequest = (CourseRequest) getRequest(); 237 int nrWaitlisted = 0; 238 for (Section section : getSections()) { 239 if (courseRequest.isWaitlisted(section)) 240 nrWaitlisted++; 241 } 242 return ((double) nrWaitlisted) / getAssignments().size(); 243 } 244 245 /** Percent of sections that are selected 246 * @return percent of sections that are selected 247 **/ 248 public double percentSelected() { 249 if (!isCourseRequest()) 250 return 0.0; 251 CourseRequest courseRequest = (CourseRequest) getRequest(); 252 int nrSelected = 0; 253 for (Section section : getSections()) { 254 if (courseRequest.isSelected(section)) 255 nrSelected++; 256 } 257 return ((double) nrSelected) / getAssignments().size(); 258 } 259 260 /** Percent of sections that are selected 261 * @return percent of sections that are selected 262 **/ 263 public double percentSelectedSameSection() { 264 if (!isCourseRequest() || getStudent().isDummy()) return (getRequest().hasSelection() ? 1.0 : 0.0); 265 CourseRequest courseRequest = (CourseRequest) getRequest(); 266 int nrSelected = 0; 267 Set<Long> nrMatching = new HashSet<Long>(); 268 sections: for (Section section : getSections()) { 269 for (Choice choice: courseRequest.getSelectedChoices()) { 270 if (choice.getSubpartId() != null) nrMatching.add(choice.getSubpartId()); 271 if (choice.sameSection(section)) { 272 nrSelected ++; continue sections; 273 } 274 } 275 } 276 return (nrMatching.isEmpty() ? 1.0 : ((double) nrSelected) / nrMatching.size()); 277 } 278 279 /** Percent of sections that have the same configuration 280 * @return percent of sections that are selected 281 **/ 282 public double percentSelectedSameConfig() { 283 if (!isCourseRequest() || getStudent().isDummy() || getConfig() == null) return (getRequest().hasSelection() ? 1.0 : 0.0); 284 CourseRequest courseRequest = (CourseRequest) getRequest(); 285 boolean hasConfigSelection = false; 286 for (Choice choice: courseRequest.getSelectedChoices()) { 287 if (choice.getConfigId() != null) { 288 hasConfigSelection = true; 289 if (choice.getConfigId().equals(getConfig().getId())) return 1.0; 290 } 291 } 292 return (hasConfigSelection ? 0.0 : 1.0); 293 } 294 295 /** Percent of sections that are initial 296 * @return percent of sections that of the initial enrollment 297 **/ 298 public double percentInitial() { 299 if (!isCourseRequest()) 300 return 0.0; 301 if (getRequest().getInitialAssignment() == null) 302 return 0.0; 303 Enrollment inital = getRequest().getInitialAssignment(); 304 int nrInitial = 0; 305 for (Section section : getSections()) { 306 if (inital.getAssignments().contains(section)) 307 nrInitial++; 308 } 309 return ((double) nrInitial) / getAssignments().size(); 310 } 311 312 /** Percent of sections that have same time as the initial assignment 313 * @return percent of sections that have same time as the initial assignment 314 **/ 315 public double percentSameTime() { 316 if (!isCourseRequest()) 317 return 0.0; 318 Enrollment ie = getRequest().getInitialAssignment(); 319 if (ie != null) { 320 int nrInitial = 0; 321 sections: for (Section section : getSections()) { 322 for (Section initial: ie.getSections()) { 323 if (section.sameInstructionalType(initial) && section.sameTime(initial)) { 324 nrInitial ++; 325 continue sections; 326 } 327 } 328 } 329 return ((double) nrInitial) / getAssignments().size(); 330 } 331 Set<Choice> selected = ((CourseRequest)getRequest()).getSelectedChoices(); 332 if (!selected.isEmpty()) { 333 int nrInitial = 0; 334 sections: for (Section section : getSections()) { 335 for (Choice choice: selected) { 336 if (choice.sameOffering(section) && choice.sameInstructionalType(section) && choice.sameTime(section)) { 337 nrInitial ++; 338 continue sections; 339 } 340 341 } 342 } 343 return ((double) nrInitial) / getAssignments().size(); 344 } 345 return 0.0; 346 } 347 348 /** True if all the sections are wait-listed 349 * @return all the sections are wait-listed 350 **/ 351 public boolean isWaitlisted() { 352 if (!isCourseRequest()) 353 return false; 354 CourseRequest courseRequest = (CourseRequest) getRequest(); 355 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 356 Section section = (Section) i.next(); 357 if (!courseRequest.isWaitlisted(section)) 358 return false; 359 } 360 return true; 361 } 362 363 /** True if all the sections are selected 364 * @return all the sections are selected 365 **/ 366 public boolean isSelected() { 367 if (!isCourseRequest()) 368 return false; 369 CourseRequest courseRequest = (CourseRequest) getRequest(); 370 for (Section section : getSections()) { 371 if (!courseRequest.isSelected(section)) 372 return false; 373 } 374 return true; 375 } 376 377 public boolean isRequired() { 378 if (!isCourseRequest()) 379 return false; 380 CourseRequest courseRequest = (CourseRequest) getRequest(); 381 for (Section section : getSections()) { 382 if (!courseRequest.isRequired(section)) 383 return false; 384 } 385 return true; 386 } 387 388 /** 389 * Enrollment penalty -- sum of section penalties (see 390 * {@link Section#getPenalty()}) 391 * @return online penalty 392 */ 393 public double getPenalty() { 394 if (iCachedPenalty == null) { 395 double penalty = 0.0; 396 if (isCourseRequest()) { 397 for (Section section : getSections()) { 398 penalty += section.getPenalty(); 399 } 400 } 401 iCachedPenalty = Double.valueOf(penalty / getAssignments().size()); 402 } 403 return iCachedPenalty.doubleValue(); 404 } 405 406 /** Enrollment value */ 407 @Override 408 public double toDouble(Assignment<Request, Enrollment> assignment) { 409 return toDouble(assignment, true); 410 } 411 412 /** Enrollment value 413 * @param assignment current assignment 414 * @param precise if false, distance conflicts and time overlaps are ignored (i.e., much faster, but less precise computation) 415 * @return enrollment penalty 416 **/ 417 public double toDouble(Assignment<Request, Enrollment> assignment, boolean precise) { 418 if (precise) { 419 StudentSectioningModel model = (StudentSectioningModel)variable().getModel(); 420 if (model.getStudentQuality() != null) 421 return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, studentQualityConflicts(assignment)); 422 else 423 return - getRequest().getWeight() * model.getStudentWeights().getWeight(assignment, this, distanceConflicts(assignment), timeOverlappingConflicts(assignment)); 424 } else { 425 Double value = (assignment == null ? null : variable().getContext(assignment).getLastWeight()); 426 if (value != null) return - value; 427 return - getRequest().getWeight() * ((StudentSectioningModel)variable().getModel()).getStudentWeights().getWeight(assignment, this); 428 } 429 } 430 431 /** Enrollment name */ 432 @Override 433 public String getName() { 434 if (getRequest() instanceof CourseRequest) { 435 Course course = null; 436 CourseRequest courseRequest = (CourseRequest) getRequest(); 437 for (Course c : courseRequest.getCourses()) { 438 if (c.getOffering().getConfigs().contains(getConfig())) { 439 course = c; 440 break; 441 } 442 } 443 String ret = (course == null ? getConfig() == null ? "" : getConfig().getName() : course.getName()); 444 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 445 Section assignment = (Section) i.next(); 446 ret += "\n " + assignment.getLongName(true) + (i.hasNext() ? "," : ""); 447 } 448 return ret; 449 } else if (getRequest() instanceof FreeTimeRequest) { 450 return "Free Time " + ((FreeTimeRequest) getRequest()).getTime().getLongName(true); 451 } else { 452 String ret = ""; 453 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 454 SctAssignment assignment = i.next(); 455 ret += assignment.toString() + (i.hasNext() ? "," : ""); 456 if (i.hasNext()) 457 ret += "\n "; 458 } 459 return ret; 460 } 461 } 462 463 public String toString(Assignment<Request, Enrollment> a) { 464 if (getAssignments().isEmpty()) return "not assigned"; 465 Set<DistanceConflict.Conflict> dc = distanceConflicts(a); 466 Set<TimeOverlapsCounter.Conflict> toc = timeOverlappingConflicts(a); 467 int share = 0; 468 if (toc != null) 469 for (TimeOverlapsCounter.Conflict c: toc) 470 share += c.getShare(); 471 String ret = toDouble(a) + "/" + sDF.format(getRequest().getBound()) 472 + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty())) 473 + (dc == null || dc.isEmpty() ? "" : "/dc:" + dc.size()) 474 + (share <= 0 ? "" : "/toc:" + share); 475 if (getRequest() instanceof CourseRequest) { 476 double sameGroup = 0.0; int groupCount = 0; 477 for (RequestGroup g: ((CourseRequest)getRequest()).getRequestGroups()) { 478 if (g.getCourse().equals(getCourse())) { 479 sameGroup += g.getEnrollmentSpread(a, this, 1.0, 0.0); 480 groupCount ++; 481 } 482 } 483 if (groupCount > 0) 484 ret += "/g:" + sDF.format(sameGroup / groupCount); 485 } 486 if (getRequest() instanceof CourseRequest) { 487 ret += " "; 488 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 489 SctAssignment assignment = i.next(); 490 ret += assignment + (i.hasNext() ? ", " : ""); 491 } 492 } 493 if (getReservation() != null) ret = "(r) " + ret; 494 return ret; 495 } 496 497 @Override 498 public String toString() { 499 if (getAssignments().isEmpty()) return "not assigned"; 500 String ret = sDF.format(getRequest().getBound()) + (getPenalty() == 0.0 ? "" : "/" + sDF.format(getPenalty())); 501 if (getRequest() instanceof CourseRequest) { 502 ret += " "; 503 for (Iterator<? extends SctAssignment> i = getAssignments().iterator(); i.hasNext();) { 504 SctAssignment assignment = i.next(); 505 ret += assignment + (i.hasNext() ? ", " : ""); 506 } 507 } 508 if (getReservation() != null) ret = "(r) " + ret; 509 if (getRequest().isMPP()) ret += " [i" + sDF.format(100.0 * percentInitial()) + "/t" + sDF.format(100.0 * percentSameTime()) + "]"; 510 return ret; 511 } 512 513 @Override 514 public boolean equals(Object o) { 515 if (o == null || !(o instanceof Enrollment)) 516 return false; 517 Enrollment e = (Enrollment) o; 518 if (!ToolBox.equals(getCourse(), e.getCourse())) 519 return false; 520 if (!ToolBox.equals(getConfig(), e.getConfig())) 521 return false; 522 if (!ToolBox.equals(getRequest(), e.getRequest())) 523 return false; 524 if (!ToolBox.equals(getAssignments(), e.getAssignments())) 525 return false; 526 if (!ToolBox.equals(getReservation(), e.getReservation())) 527 return false; 528 return true; 529 } 530 531 /** Distance conflicts, in which this enrollment is involved. 532 * @param assignment current assignment 533 * @return distance conflicts 534 **/ 535 public Set<DistanceConflict.Conflict> distanceConflicts(Assignment<Request, Enrollment> assignment) { 536 if (!isCourseRequest()) 537 return null; 538 if (getRequest().getModel() instanceof StudentSectioningModel) { 539 DistanceConflict dc = ((StudentSectioningModel) getRequest().getModel()).getDistanceConflict(); 540 if (dc == null) return null; 541 return dc.allConflicts(assignment, this); 542 } else 543 return null; 544 } 545 546 /** Time overlapping conflicts, in which this enrollment is involved. 547 * @param assignment current assignment 548 * @return time overlapping conflicts 549 **/ 550 public Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts(Assignment<Request, Enrollment> assignment) { 551 if (getRequest().getModel() instanceof StudentSectioningModel) { 552 TimeOverlapsCounter toc = ((StudentSectioningModel) getRequest().getModel()).getTimeOverlaps(); 553 if (toc == null) 554 return null; 555 return toc.allConflicts(assignment, this); 556 } else 557 return null; 558 } 559 560 public Set<StudentQuality.Conflict> studentQualityConflicts(Assignment<Request, Enrollment> assignment) { 561 if (!isCourseRequest()) 562 return null; 563 if (getRequest().getModel() instanceof StudentSectioningModel) { 564 StudentQuality sq = ((StudentSectioningModel) getRequest().getModel()).getStudentQuality(); 565 if (sq == null) return null; 566 return sq.allConflicts(assignment, this); 567 } else 568 return null; 569 } 570 571 /** 572 * Return enrollment priority 573 * @return zero for the course, one for the first alternative, two for the second alternative 574 */ 575 public int getPriority() { 576 return iPriority + (iNoReservationPenalty ? 1 : 0); 577 } 578 579 /** 580 * Return enrollment priority, ignoring priority bump provided by reservations 581 * @return zero for the course, one for the first alternative, two for the second alternative 582 */ 583 public int getTruePriority() { 584 return iPriority; 585 } 586 587 /** 588 * Return adjusted enrollment priority, including priority bump provided by reservations 589 * (but ensuring that getting the course without a reservation is still better than getting an alternative) 590 * @return zero for the course, two for the first alternative, four for the second alternative; plus one when the no reservation penalty applies 591 */ 592 public int getAdjustedPriority() { 593 return 2 * iPriority + (iNoReservationPenalty ? 1 : 0); 594 } 595 596 /** 597 * Return total number of slots of all sections in the enrollment. 598 * @return number of slots used 599 */ 600 public int getNrSlots() { 601 int ret = 0; 602 for (SctAssignment a: getAssignments()) { 603 if (a.getTime() != null) ret += a.getTime().getLength() * a.getTime().getNrMeetings(); 604 } 605 return ret; 606 } 607 608 /** 609 * Return reservation used for this enrollment 610 * @return used reservation 611 */ 612 public Reservation getReservation() { return iReservation; } 613 614 /** 615 * Set reservation for this enrollment 616 * @param reservation used reservation 617 */ 618 public void setReservation(Reservation reservation) { iReservation = reservation; } 619 620 /** 621 * Time stamp of the enrollment 622 * @return enrollment time stamp 623 */ 624 public Long getTimeStamp() { 625 return iTimeStamp; 626 } 627 628 /** 629 * Time stamp of the enrollment 630 * @param timeStamp enrollment time stamp 631 */ 632 public void setTimeStamp(Long timeStamp) { 633 iTimeStamp = timeStamp; 634 } 635 636 /** 637 * Approval of the enrollment (only used by the online student sectioning) 638 * @return consent approval 639 */ 640 public String getApproval() { 641 return iApproval; 642 } 643 644 /** 645 * Approval of the enrollment (only used by the online student sectioning) 646 * @param approval consent approval 647 */ 648 public void setApproval(String approval) { 649 iApproval = approval; 650 } 651 652 /** 653 * True if this enrollment can overlap with other enrollments of the student. 654 * @return can overlap with other enrollments of the student 655 */ 656 public boolean isAllowOverlap() { 657 return (getReservation() != null && getReservation().isAllowOverlap()); 658 } 659 660 /** 661 * Enrollment limit, i.e., the number of students that would be able to get into the offering using this enrollment (if all the sections are empty) 662 * @return enrollment limit 663 */ 664 public int getLimit() { 665 if (!isCourseRequest()) return -1; // free time requests have no limit 666 Integer limit = null; 667 for (Section section: getSections()) 668 if (section.getLimit() >= 0) { 669 if (limit == null) 670 limit = section.getLimit(); 671 else 672 limit = Math.min(limit, section.getLimit()); 673 } 674 return (limit == null ? -1 : limit); 675 } 676 677 /** 678 * Credit of this enrollment (using either {@link Course#getCreditValue()} or {@link Subpart#getCreditValue()} when course credit is not present) 679 * @return credit of this enrollment 680 */ 681 public float getCredit() { 682 if (getCourse() == null) return 0f; 683 if (getAssignments().isEmpty()) return 0f; 684 if (getCourse().hasCreditValue()) return getCourse().getCreditValue(); 685 float subpartCredit = 0f; 686 for (Subpart subpart: getConfig().getSubparts()) { 687 if (subpart.hasCreditValue()) subpartCredit += subpart.getCreditValue(); 688 } 689 return subpartCredit; 690 } 691 692}