001package org.cpsolver.studentsct.reservation; 002 003import java.util.HashMap; 004import java.util.HashSet; 005import java.util.Map; 006import java.util.Set; 007 008import org.cpsolver.ifs.assignment.Assignment; 009import org.cpsolver.ifs.assignment.AssignmentComparable; 010import org.cpsolver.ifs.assignment.context.AbstractClassWithContext; 011import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext; 012import org.cpsolver.ifs.assignment.context.CanInheritContext; 013import org.cpsolver.ifs.model.Model; 014import org.cpsolver.studentsct.StudentSectioningModel; 015import org.cpsolver.studentsct.model.Config; 016import org.cpsolver.studentsct.model.Course; 017import org.cpsolver.studentsct.model.CourseRequest; 018import org.cpsolver.studentsct.model.Enrollment; 019import org.cpsolver.studentsct.model.Offering; 020import org.cpsolver.studentsct.model.Request; 021import org.cpsolver.studentsct.model.Section; 022import org.cpsolver.studentsct.model.Student; 023import org.cpsolver.studentsct.model.Subpart; 024 025 026 027/** 028 * Abstract reservation. This abstract class allow some section, courses, 029 * and other parts to be reserved to particular group of students. A reservation 030 * can be unlimited (any number of students of that particular group can attend 031 * a course, section, etc.) or with a limit (only given number of seats is 032 * reserved to the students of the particular group). 033 * 034 * <br> 035 * <br> 036 * 037 * @version StudentSct 1.3 (Student Sectioning)<br> 038 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 039 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 040 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 041 * <br> 042 * This library is free software; you can redistribute it and/or modify 043 * it under the terms of the GNU Lesser General Public License as 044 * published by the Free Software Foundation; either version 3 of the 045 * License, or (at your option) any later version. <br> 046 * <br> 047 * This library is distributed in the hope that it will be useful, but 048 * WITHOUT ANY WARRANTY; without even the implied warranty of 049 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 050 * Lesser General Public License for more details. <br> 051 * <br> 052 * You should have received a copy of the GNU Lesser General Public 053 * License along with this library; if not see 054 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 055 */ 056public abstract class Reservation extends AbstractClassWithContext<Request, Enrollment, Reservation.ReservationContext> 057 implements AssignmentComparable<Reservation, Request, Enrollment>, CanInheritContext<Request, Enrollment, Reservation.ReservationContext> { 058 /** Reservation unique id */ 059 private long iId = 0; 060 061 /** Is reservation expired? */ 062 private boolean iExpired; 063 064 /** Instructional offering on which the reservation is set, required */ 065 private Offering iOffering; 066 067 /** One or more configurations, if applicable */ 068 private Set<Config> iConfigs = new HashSet<Config>(); 069 070 /** One or more sections, if applicable */ 071 private Map<Subpart, Set<Section>> iSections = new HashMap<Subpart, Set<Section>>(); 072 073 /** Reservation priority */ 074 private int iPriority = 100; 075 076 /** Must this reservation be used */ 077 private boolean iMustBeUsed = false; 078 079 /** Can assign over class / configuration / course limit */ 080 private boolean iCanAssignOverLimit = false; 081 082 /** Does this reservation allow for overlaps */ 083 private boolean iAllowOverlap = false; 084 085 /** Does this reservation allow for disabled sections */ 086 private boolean iAllowDisabled = false; 087 088 /** No enrollment is matching this reservation when set to true */ 089 private boolean iNeverIncluded = false; 090 091 /** Can break linked-sections constraint */ 092 private boolean iBreakLinkedSections = false; 093 094 095 /** 096 * Constructor 097 * @param id reservation unique id 098 * @param offering instructional offering on which the reservation is set 099 * @param priority reservation priority 100 * @param mustBeUsed must this reservation be used 101 * @param canAssignOverLimit can assign over class / configuration / course limit 102 * @param allowOverlap does this reservation allow for overlaps 103 */ 104 public Reservation(long id, Offering offering, int priority, boolean mustBeUsed, boolean canAssignOverLimit, boolean allowOverlap) { 105 iId = id; 106 iOffering = offering; 107 iOffering.getReservations().add(this); 108 iOffering.clearReservationCache(); 109 iPriority = priority; 110 iMustBeUsed = mustBeUsed; 111 iCanAssignOverLimit = canAssignOverLimit; 112 iAllowOverlap = allowOverlap; 113 } 114 115 /** 116 * Reservation id 117 * @return reservation unique id 118 */ 119 public long getId() { return iId; } 120 121 /** 122 * Reservation limit 123 * @return reservation limit, -1 for unlimited 124 */ 125 public abstract double getReservationLimit(); 126 127 128 /** Reservation priority (e.g., individual reservations first) 129 * @return reservation priority 130 **/ 131 public int getPriority() { 132 return iPriority; 133 } 134 135 /** 136 * Set reservation priority (e.g., individual reservations first) 137 * @param priority reservation priority 138 */ 139 public void setPriority(int priority) { 140 iPriority = priority; 141 } 142 143 /** 144 * Returns true if the student is applicable for the reservation 145 * @param student a student 146 * @return true if student can use the reservation to get into the course / configuration / section 147 */ 148 public abstract boolean isApplicable(Student student); 149 150 /** 151 * Instructional offering on which the reservation is set. 152 * @return instructional offering 153 */ 154 public Offering getOffering() { return iOffering; } 155 156 /** 157 * One or more configurations on which the reservation is set (optional). 158 * @return instructional offering configurations 159 */ 160 public Set<Config> getConfigs() { return iConfigs; } 161 162 /** 163 * Add a configuration (of the offering {@link Reservation#getOffering()}) to this reservation 164 * @param config instructional offering configuration 165 */ 166 public void addConfig(Config config) { 167 iConfigs.add(config); 168 clearLimitCapCache(); 169 } 170 171 /** 172 * One or more sections on which the reservation is set (optional). 173 * @return class restrictions 174 */ 175 public Map<Subpart, Set<Section>> getSections() { return iSections; } 176 177 /** 178 * One or more sections on which the reservation is set (optional). 179 * @param subpart scheduling subpart 180 * @return class restrictions for the given scheduling subpart 181 */ 182 public Set<Section> getSections(Subpart subpart) { 183 return iSections.get(subpart); 184 } 185 186 /** 187 * Add a section (of the offering {@link Reservation#getOffering()}) to this reservation. 188 * This will also add all parent sections and the appropriate configuration to the offering. 189 * @param section a class restriction 190 */ 191 public void addSection(Section section, boolean inclusive) { 192 if (inclusive) { 193 addConfig(section.getSubpart().getConfig()); 194 while (section != null) { 195 Set<Section> sections = iSections.get(section.getSubpart()); 196 if (sections == null) { 197 sections = new HashSet<Section>(); 198 iSections.put(section.getSubpart(), sections); 199 } 200 sections.add(section); 201 section = section.getParent(); 202 } 203 } else { 204 Set<Section> sections = iSections.get(section.getSubpart()); 205 if (sections == null) { 206 sections = new HashSet<Section>(); 207 iSections.put(section.getSubpart(), sections); 208 } 209 sections.add(section); 210 } 211 clearLimitCapCache(); 212 } 213 214 public void addSection(Section section) { 215 addSection(section, true); 216 } 217 218 /** 219 * Return true if the given enrollment meets the reservation. 220 * @param enrollment given enrollment 221 * @return true if the given enrollment meets the reservation 222 */ 223 public boolean isIncluded(Enrollment enrollment) { 224 // Never included flag is set -- return false 225 if (neverIncluded()) return false; 226 227 // Free time request are never included 228 if (enrollment.getConfig() == null) return false; 229 230 // Check the offering 231 if (!iOffering.equals(enrollment.getConfig().getOffering())) return false; 232 233 if (areRestrictionsInclusive()) { 234 // If there are configurations, check the configuration 235 if (!iConfigs.isEmpty() && !iConfigs.contains(enrollment.getConfig())) return false; 236 237 // Check all the sections of the enrollment 238 for (Section section: enrollment.getSections()) { 239 Set<Section> sections = iSections.get(section.getSubpart()); 240 if (sections != null && !sections.contains(section)) 241 return false; 242 } 243 return true; 244 } else { 245 // no restrictions -> true 246 if (iConfigs.isEmpty() && iSections.isEmpty()) return true; 247 248 // configuration match -> true 249 if (iConfigs.contains(enrollment.getConfig())) return true; 250 251 // section match -> true 252 for (Section section: enrollment.getSections()) { 253 Set<Section> sections = iSections.get(section.getSubpart()); 254 if (sections != null && sections.contains(section)) 255 return true; 256 } 257 258 // no match -> false 259 return false; 260 } 261 } 262 263 /** 264 * True if the enrollment can be done using this reservation 265 * @param assignment current assignment 266 * @param enrollment given enrollment 267 * @return true if the given enrollment can be assigned 268 */ 269 public boolean canEnroll(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 270 // Check if student can use this reservation 271 if (!isApplicable(enrollment.getStudent())) return false; 272 273 // Check if the enrollment meets the reservation 274 if (!isIncluded(enrollment)) return false; 275 276 // Check the limit 277 return getLimit() < 0 || getContext(assignment).getUsedSpace() + enrollment.getRequest().getWeight() <= getLimit(); 278 } 279 280 /** 281 * True if can go over the course / config / section limit. Only to be used in the online sectioning. 282 * @return can assign over class / configuration / course limit 283 */ 284 public boolean canAssignOverLimit() { 285 return iCanAssignOverLimit; 286 } 287 288 /** 289 * True if the batch solver can assign the reservation over the course / config / section limit. 290 * @return {@link Reservation#canAssignOverLimit()} and {@link StudentSectioningModel#getReservationCanAssignOverTheLimit()} 291 */ 292 public boolean canBatchAssignOverLimit() { 293 return canAssignOverLimit() && (iOffering.getModel() == null || ((StudentSectioningModel)iOffering.getModel()).getReservationCanAssignOverTheLimit()); 294 } 295 296 /** 297 * Set to true if a student meeting this reservation can go over the course / config / section limit. 298 * @param canAssignOverLimit can assign over class / configuration / course limit 299 */ 300 public void setCanAssignOverLimit(boolean canAssignOverLimit) { 301 iCanAssignOverLimit = canAssignOverLimit; 302 } 303 304 /** 305 * If true, student must use the reservation (if applicable). Expired reservations do not need to be used. 306 * @return must this reservation be used 307 */ 308 public boolean mustBeUsed() { 309 return iMustBeUsed && !isExpired(); 310 } 311 312 /** 313 * If true, student must use the reservation (if applicable). Expiration date is ignored. 314 * @return must this reservation be used 315 */ 316 public boolean mustBeUsedIgnoreExpiration() { 317 return iMustBeUsed; 318 } 319 320 /** 321 * Set to true if the student must use the reservation (if applicable) 322 * @param mustBeUsed must this reservation be used 323 */ 324 public void setMustBeUsed(boolean mustBeUsed) { 325 iMustBeUsed = mustBeUsed; 326 } 327 328 /** 329 * Reservation restrictivity (estimated percentage of enrollments that include this reservation, 1.0 reservation on the whole offering) 330 * @return computed restrictivity 331 */ 332 public double getRestrictivity() { 333 if (iCachedRestrictivity == null) { 334 boolean inclusive = areRestrictionsInclusive(); 335 if (getConfigs().isEmpty()) return 1.0; 336 int nrChoices = 0, nrMatchingChoices = 0; 337 for (Config config: getOffering().getConfigs()) { 338 int x[] = nrChoices(config, 0, new HashSet<Section>(), getConfigs().contains(config), inclusive); 339 nrChoices += x[0]; 340 nrMatchingChoices += x[1]; 341 } 342 iCachedRestrictivity = ((double)nrMatchingChoices) / nrChoices; 343 } 344 return iCachedRestrictivity; 345 } 346 private Double iCachedRestrictivity = null; 347 348 349 /** Number of choices and number of chaing choices in the given sub enrollment */ 350 private int[] nrChoices(Config config, int idx, HashSet<Section> sections, boolean matching, boolean inclusive) { 351 if (config.getSubparts().size() == idx) { 352 return new int[]{1, matching ? 1 : 0}; 353 } else { 354 Subpart subpart = config.getSubparts().get(idx); 355 Set<Section> matchingSections = getSections(subpart); 356 int choicesThisSubpart = 0; 357 int matchingChoicesThisSubpart = 0; 358 for (Section section : subpart.getSections()) { 359 if (section.getParent() != null && !sections.contains(section.getParent())) 360 continue; 361 if (section.isOverlapping(sections)) 362 continue; 363 sections.add(section); 364 boolean m = (inclusive 365 ? matching && (matchingSections == null || matchingSections.contains(section)) 366 : matching || (matchingSections != null && matchingSections.contains(section)) 367 ); 368 int[] x = nrChoices(config, 1 + idx, sections, m, inclusive); 369 choicesThisSubpart += x[0]; 370 matchingChoicesThisSubpart += x[1]; 371 sections.remove(section); 372 } 373 return new int[] {choicesThisSubpart, matchingChoicesThisSubpart}; 374 } 375 } 376 377 /** 378 * Priority first, than restrictivity (more restrictive first), than availability (more available first), than id 379 */ 380 @Override 381 public int compareTo(Assignment<Request, Enrollment> assignment, Reservation r) { 382 if (mustBeUsed() != r.mustBeUsed()) { 383 return (mustBeUsed() ? -1 : 1); 384 } 385 if (getPriority() != r.getPriority()) { 386 return (getPriority() < r.getPriority() ? -1 : 1); 387 } 388 int cmp = Double.compare(getRestrictivity(), r.getRestrictivity()); 389 if (cmp != 0) return cmp; 390 cmp = - Double.compare(getContext(assignment).getReservedAvailableSpace(assignment, null), r.getContext(assignment).getReservedAvailableSpace(assignment, null)); 391 if (cmp != 0) return cmp; 392 return Long.valueOf(getId()).compareTo(r.getId()); 393 } 394 395 /** 396 * Priority first, than restrictivity (more restrictive first), than id 397 */ 398 @Override 399 public int compareTo(Reservation r) { 400 if (mustBeUsed() != r.mustBeUsed()) { 401 return (mustBeUsed() ? -1 : 1); 402 } 403 if (getPriority() != r.getPriority()) { 404 return (getPriority() < r.getPriority() ? -1 : 1); 405 } 406 int cmp = Double.compare(getRestrictivity(), r.getRestrictivity()); 407 if (cmp != 0) return cmp; 408 return Long.valueOf(getId()).compareTo(r.getId()); 409 } 410 411 /** 412 * Return minimum of two limits where -1 counts as unlimited (any limit is smaller) 413 */ 414 private static double min(double l1, double l2) { 415 return (l1 < 0 ? l2 : l2 < 0 ? l1 : Math.min(l1, l2)); 416 } 417 418 /** 419 * Add two limits where -1 counts as unlimited (unlimited plus anything is unlimited) 420 */ 421 private static double add(double l1, double l2) { 422 return (l1 < 0 ? -1 : l2 < 0 ? -1 : l1 + l2); 423 } 424 425 426 /** Limit cap cache */ 427 private Double iLimitCap = null; 428 429 /** 430 * Compute limit cap (maximum number of students that can get into the offering using this reservation) 431 * @return reservation limit cap 432 */ 433 public double getLimitCap() { 434 if (iLimitCap == null) iLimitCap = getLimitCapNoCache(); 435 return iLimitCap; 436 } 437 438 /** 439 * Check if restrictions are inclusive (that is for each section, the reservation also contains all its parents and the configuration) 440 */ 441 public boolean areRestrictionsInclusive() { 442 for (Map.Entry<Subpart, Set<Section>> entry: getSections().entrySet()) { 443 if (getConfigs().contains(entry.getKey().getConfig())) return true; 444 } 445 return false; 446 } 447 448 /** 449 * Compute limit cap (maximum number of students that can get into the offering using this reservation) 450 */ 451 private double getLimitCapNoCache() { 452 if (getConfigs().isEmpty()) return -1; // no config -> can be unlimited 453 454 if (canAssignOverLimit()) return -1; // can assign over limit -> no cap 455 456 double cap = 0; 457 if (areRestrictionsInclusive()) { 458 // for each config 459 for (Config config: getConfigs()) { 460 // config cap 461 double configCap = config.getLimit(); 462 463 for (Map.Entry<Subpart, Set<Section>> entry: getSections().entrySet()) { 464 if (!config.equals(entry.getKey().getConfig())) continue; 465 Set<Section> sections = entry.getValue(); 466 467 // subpart cap 468 double subpartCap = 0; 469 for (Section section: sections) 470 subpartCap = add(subpartCap, section.getLimit()); 471 472 // minimize 473 configCap = min(configCap, subpartCap); 474 } 475 476 // add config cap 477 cap = add(cap, configCap); 478 } 479 } else { 480 // for each config 481 for (Config config: getConfigs()) 482 cap = add(cap, config.getLimit()); 483 // for each subpart 484 for (Map.Entry<Subpart, Set<Section>> entry: getSections().entrySet()) { 485 Set<Section> sections = entry.getValue(); 486 // subpart cap 487 double subpartCap = 0; 488 for (Section section: sections) 489 subpartCap = add(subpartCap, section.getLimit()); 490 cap = add(cap, subpartCap); 491 } 492 } 493 494 return cap; 495 } 496 497 /** 498 * Clear limit cap cache 499 */ 500 private void clearLimitCapCache() { 501 iLimitCap = null; 502 } 503 504 /** 505 * Reservation limit capped the limit cap (see {@link Reservation#getLimitCap()}) 506 * @return reservation limit, -1 if unlimited 507 */ 508 public double getLimit() { 509 return min(getLimitCap(), getReservationLimit()); 510 } 511 512 /** 513 * True if holding this reservation allows a student to have attend overlapping class. 514 * @return does this reservation allow for overlaps 515 */ 516 public boolean isAllowOverlap() { 517 return iAllowOverlap; 518 } 519 520 /** 521 * Set to true if holding this reservation allows a student to have attend overlapping class. 522 * @param allowOverlap does this reservation allow for overlaps 523 */ 524 public void setAllowOverlap(boolean allowOverlap) { 525 iAllowOverlap = allowOverlap; 526 } 527 528 /** 529 * True if holding this reservation allows a student to attend a disabled class. 530 * @return does this reservation allow for disabled sections 531 */ 532 public boolean isAllowDisabled() { 533 return iAllowDisabled; 534 } 535 536 /** 537 * Set to true if holding this reservation allows a student to attend a disabled class 538 * @param allowDisabled does this reservation allow for disabled sections 539 */ 540 public void setAllowDisabled(boolean allowDisabled) { 541 iAllowDisabled = allowDisabled; 542 } 543 544 /** 545 * Set reservation expiration. If a reservation is expired, it works as ordinary reservation 546 * (especially the flags mutBeUsed and isAllowOverlap), except it does not block other students 547 * of getting into the offering / config / section. 548 * @param expired is this reservation expired 549 */ 550 public void setExpired(boolean expired) { 551 iExpired = expired; 552 } 553 554 /** 555 * True if the reservation is expired. If a reservation is expired, it works as ordinary reservation 556 * (especially the flags mutBeUsed and isAllowOverlap), except it does not block other students 557 * of getting into the offering / config / section. 558 * @return is this reservation expired 559 */ 560 public boolean isExpired() { 561 return iExpired; 562 } 563 564 /** 565 * No enrollment is matching this reservation when set to true 566 */ 567 public boolean neverIncluded() { return iNeverIncluded; } 568 569 /** 570 * No enrollment is matching this reservation when set to true 571 */ 572 public void setNeverIncluded(boolean neverIncluded) { iNeverIncluded = neverIncluded; } 573 574 /** 575 * Can break linked-section constraints when set to true 576 */ 577 public boolean canBreakLinkedSections() { return iBreakLinkedSections; } 578 579 /** 580 * Can break linked-section constraints when set to true 581 */ 582 public void setBreakLinkedSections(boolean breakLinkedSections) { iBreakLinkedSections = breakLinkedSections; } 583 584 585 @Override 586 public Model<Request, Enrollment> getModel() { 587 return getOffering().getModel(); 588 } 589 590 /** 591 * Available reserved space 592 * @param assignment current assignment 593 * @param excludeRequest excluding given request (if not null) 594 * @return available reserved space 595 **/ 596 public double getReservedAvailableSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 597 return getContext(assignment).getReservedAvailableSpace(assignment, excludeRequest); 598 } 599 600 /** Enrollments assigned using this reservation 601 * @param assignment current assignment 602 * @return assigned enrollments of this reservation 603 **/ 604 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { 605 return getContext(assignment).getEnrollments(); 606 } 607 608 @Override 609 public ReservationContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 610 return new ReservationContext(assignment); 611 } 612 613 614 @Override 615 public ReservationContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ReservationContext parentContext) { 616 return new ReservationContext(parentContext); 617 } 618 619 620 public class ReservationContext implements AssignmentConstraintContext<Request, Enrollment> { 621 /** Enrollments included in this reservation */ 622 private Set<Enrollment> iEnrollments = new HashSet<Enrollment>(); 623 624 /** Used part of the limit */ 625 private double iUsed = 0; 626 private boolean iReadOnly = false; 627 628 public ReservationContext(Assignment<Request, Enrollment> assignment) { 629 for (Course course: getOffering().getCourses()) 630 for (CourseRequest request: course.getRequests()) { 631 Enrollment enrollment = assignment.getValue(request); 632 if (enrollment != null && Reservation.this.equals(enrollment.getReservation())) 633 assigned(assignment, enrollment); 634 } 635 } 636 637 public ReservationContext(ReservationContext parent) { 638 iUsed = parent.iUsed; 639 iEnrollments = parent.iEnrollments; 640 iReadOnly = true; 641 } 642 643 /** Notify reservation about an unassignment */ 644 @Override 645 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 646 if (iReadOnly) { 647 iEnrollments = new HashSet<Enrollment>(iEnrollments); 648 iReadOnly = false; 649 } 650 if (iEnrollments.add(enrollment)) 651 iUsed += enrollment.getRequest().getWeight(); 652 } 653 654 /** Notify reservation about an assignment */ 655 @Override 656 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 657 if (iReadOnly) { 658 iEnrollments = new HashSet<Enrollment>(iEnrollments); 659 iReadOnly = false; 660 } 661 if (iEnrollments.remove(enrollment)) 662 iUsed -= enrollment.getRequest().getWeight(); 663 } 664 665 /** Enrollments assigned using this reservation 666 * @return assigned enrollments of this reservation 667 **/ 668 public Set<Enrollment> getEnrollments() { 669 return iEnrollments; 670 } 671 672 /** Used space 673 * @return spaced used of this reservation 674 **/ 675 public double getUsedSpace() { 676 return iUsed; 677 } 678 679 /** 680 * Available reserved space 681 * @param assignment current assignment 682 * @param excludeRequest excluding given request (if not null) 683 * @return available reserved space 684 **/ 685 public double getReservedAvailableSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 686 // Unlimited 687 if (getLimit() < 0) return Double.MAX_VALUE; 688 689 double reserved = getLimit() - getContext(assignment).getUsedSpace(); 690 if (excludeRequest != null && assignment.getValue(excludeRequest) != null && iEnrollments.contains(assignment.getValue(excludeRequest))) 691 reserved += excludeRequest.getWeight(); 692 693 return reserved; 694 } 695 } 696}