001package org.cpsolver.studentsct.model; 002 003import java.util.ArrayList; 004import java.util.HashSet; 005import java.util.List; 006import java.util.Set; 007 008import org.cpsolver.ifs.assignment.Assignment; 009import org.cpsolver.ifs.assignment.context.AbstractClassWithContext; 010import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext; 011import org.cpsolver.ifs.assignment.context.CanInheritContext; 012import org.cpsolver.ifs.model.Model; 013import org.cpsolver.studentsct.reservation.Reservation; 014 015 016 017 018/** 019 * Representation of a configuration of an offering. A configuration contains 020 * id, name, an offering and a list of subparts. <br> 021 * <br> 022 * Each instructional offering (see {@link Offering}) contains one or more 023 * configurations. Each configuration contain one or more subparts. Each student 024 * has to take a class of each subpart of one of the possible configurations. 025 * 026 * <br> 027 * <br> 028 * 029 * @author Tomáš Müller 030 * @version StudentSct 1.3 (Student Sectioning)<br> 031 * Copyright (C) 2007 - 2014 Tomáš Müller<br> 032 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 033 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 034 * <br> 035 * This library is free software; you can redistribute it and/or modify 036 * it under the terms of the GNU Lesser General Public License as 037 * published by the Free Software Foundation; either version 3 of the 038 * License, or (at your option) any later version. <br> 039 * <br> 040 * This library is distributed in the hope that it will be useful, but 041 * WITHOUT ANY WARRANTY; without even the implied warranty of 042 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 043 * Lesser General Public License for more details. <br> 044 * <br> 045 * You should have received a copy of the GNU Lesser General Public 046 * License along with this library; if not see 047 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 048 */ 049public class Config extends AbstractClassWithContext<Request, Enrollment, Config.ConfigContext> implements CanInheritContext<Request, Enrollment, Config.ConfigContext> { 050 private long iId = -1; 051 private String iName = null; 052 private Offering iOffering = null; 053 private int iLimit = -1; 054 private List<Subpart> iSubparts = new ArrayList<Subpart>(); 055 private Long iInstrMethodId; 056 private String iInstrMethodName; 057 private String iInstrMethodReference; 058 059 /** 060 * Constructor 061 * 062 * @param id 063 * instructional offering configuration unique id 064 * @param limit 065 * configuration limit (-1 for unlimited) 066 * @param name 067 * configuration name 068 * @param offering 069 * instructional offering to which this configuration belongs 070 */ 071 public Config(long id, int limit, String name, Offering offering) { 072 iId = id; 073 iLimit = limit; 074 iName = name; 075 iOffering = offering; 076 iOffering.getConfigs().add(this); 077 } 078 079 /** Configuration id 080 * @return instructional offering configuration unique id 081 **/ 082 public long getId() { 083 return iId; 084 } 085 086 /** 087 * Configuration limit. This is defines the maximal number of students that can be 088 * enrolled into this configuration at the same time. It is -1 in the case of an 089 * unlimited configuration 090 * @return configuration limit 091 */ 092 public int getLimit() { 093 return iLimit; 094 } 095 096 /** Set configuration limit 097 * @param limit configuration limit, -1 if unlimited 098 **/ 099 public void setLimit(int limit) { 100 iLimit = limit; 101 } 102 103 104 105 /** Configuration name 106 * @return configuration name 107 **/ 108 public String getName() { 109 return iName; 110 } 111 112 /** Instructional offering to which this configuration belongs. 113 * @return instructional offering 114 **/ 115 public Offering getOffering() { 116 return iOffering; 117 } 118 119 /** List of subparts 120 * @return scheduling subparts 121 **/ 122 public List<Subpart> getSubparts() { 123 return iSubparts; 124 } 125 126 /** 127 * Return instructional method id 128 * @return instructional method id 129 */ 130 public Long getInstructionalMethodId() { return iInstrMethodId; } 131 132 /** 133 * Set instructional method id 134 * @param instrMethodId instructional method id 135 */ 136 public void setInstructionalMethodId(Long instrMethodId) { iInstrMethodId = instrMethodId; } 137 138 /** 139 * Return instructional method name 140 * @return instructional method name 141 */ 142 public String getInstructionalMethodName() { return iInstrMethodName; } 143 144 /** 145 * Set instructional method name 146 * @param instrMethodName instructional method name 147 */ 148 public void setInstructionalMethodName(String instrMethodName) { iInstrMethodName = instrMethodName; } 149 150 /** 151 * Return instructional method reference 152 * @return instructional method reference 153 */ 154 public String getInstructionalMethodReference() { return iInstrMethodReference; } 155 156 /** 157 * Set instructional method reference 158 * @param instrMethodReference instructional method reference 159 */ 160 public void setInstructionalMethodReference(String instrMethodReference) { iInstrMethodReference = instrMethodReference; } 161 162 @Override 163 public String toString() { 164 return getName(); 165 } 166 167 /** Average minimal penalty from {@link Subpart#getMinPenalty()} 168 * @return minimal penalty 169 **/ 170 public double getMinPenalty() { 171 double min = 0.0; 172 for (Subpart subpart : getSubparts()) { 173 min += subpart.getMinPenalty(); 174 } 175 return min / getSubparts().size(); 176 } 177 178 /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 179 * @return maximal penalty 180 **/ 181 public double getMaxPenalty() { 182 double max = 0.0; 183 for (Subpart subpart : getSubparts()) { 184 max += subpart.getMinPenalty(); 185 } 186 return max / getSubparts().size(); 187 } 188 189 /** 190 * Available space in the configuration that is not reserved by any config reservation 191 * @param assignment current assignment 192 * @param excludeRequest excluding given request (if not null) 193 * @return available space 194 **/ 195 public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 196 // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 197 // (in which case there is no unreserved space) 198 if (getLimit() < 0) { 199 // exclude reservations that are not directly set on this section 200 for (Reservation r: getConfigReservations()) { 201 // ignore expired reservations 202 if (r.isExpired()) continue; 203 // there is an unlimited reservation -> no unreserved space 204 if (r.getLimit(this) < 0) return 0.0; 205 } 206 return Double.MAX_VALUE; 207 } 208 209 double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 210 // exclude reservations that are not directly set on this section 211 for (Reservation r: getConfigReservations()) { 212 // ignore expired reservations 213 if (r.isExpired()) continue; 214 // unlimited reservation -> all the space is reserved 215 if (r.getLimit(this) < 0.0) return 0.0; 216 // compute space that can be potentially taken by this reservation 217 double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, this, excludeRequest); 218 // deduct the space from available space 219 available -= Math.max(0.0, reserved); 220 } 221 222 return available; 223 } 224 225 /** 226 * Total space in the configuration that cannot be reserved by any config reservation 227 * @return total unreserved space 228 **/ 229 public synchronized double getTotalUnreservedSpace() { 230 if (iTotalUnreservedSpace == null) 231 iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache(); 232 return iTotalUnreservedSpace; 233 } 234 private Double iTotalUnreservedSpace = null; 235 private double getTotalUnreservedSpaceNoCache() { 236 // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 237 // (in which case there is no unreserved space) 238 if (getLimit() < 0) { 239 // exclude reservations that are not directly set on this section 240 for (Reservation r: getConfigReservations()) { 241 // ignore expired reservations 242 if (r.isExpired()) continue; 243 // there is an unlimited reservation -> no unreserved space 244 if (r.getLimit(this) < 0) return 0.0; 245 } 246 return Double.MAX_VALUE; 247 } 248 249 // we need to check all reservations linked with this section 250 double available = getLimit(), reserved = 0, exclusive = 0; 251 Set<Config> configs = new HashSet<Config>(); 252 reservations: for (Reservation r: getConfigReservations()) { 253 // ignore expired reservations 254 if (r.isExpired()) continue; 255 // unlimited reservation -> no unreserved space 256 if (r.getLimit(this) < 0) return 0.0; 257 for (Config s: r.getConfigs()) { 258 if (s.equals(this)) continue; 259 if (s.getLimit() < 0) continue reservations; 260 if (configs.add(s)) 261 available += s.getLimit(); 262 } 263 reserved += r.getLimit(this); 264 if (r.getConfigs().size() == 1) 265 exclusive += r.getLimit(this); 266 } 267 268 return Math.min(available - reserved, getLimit() - exclusive); 269 } 270 271 /** 272 * Get reservations for this configuration 273 * @return related reservations 274 */ 275 public synchronized List<Reservation> getReservations() { 276 if (iReservations == null) { 277 iReservations = new ArrayList<Reservation>(); 278 for (Reservation r: getOffering().getReservations()) { 279 if (r.getConfigs().isEmpty() || r.getConfigs().contains(this)) 280 iReservations.add(r); 281 } 282 } 283 return iReservations; 284 } 285 List<Reservation> iReservations = null; 286 287 /** 288 * Get reservations that require this configuration 289 * @return related reservations 290 */ 291 public synchronized List<Reservation> getConfigReservations() { 292 if (iConfigReservations == null) { 293 iConfigReservations = new ArrayList<Reservation>(); 294 for (Reservation r: getOffering().getReservations()) { 295 if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this)) 296 iConfigReservations.add(r); 297 } 298 } 299 return iConfigReservations; 300 } 301 List<Reservation> iConfigReservations = null; 302 303 /** 304 * Clear reservation information that was cached on this configuration or below 305 */ 306 public synchronized void clearReservationCache() { 307 for (Subpart s: getSubparts()) 308 s.clearReservationCache(); 309 iReservations = null; 310 iConfigReservations = null; 311 iTotalUnreservedSpace = null; 312 } 313 314 @Override 315 public boolean equals(Object o) { 316 if (o == null || !(o instanceof Config)) return false; 317 return getId() == ((Config)o).getId(); 318 } 319 320 @Override 321 public int hashCode() { 322 return Long.valueOf(getId()).hashCode(); 323 } 324 325 @Override 326 public Model<Request, Enrollment> getModel() { 327 return getOffering().getModel(); 328 } 329 330 /** Set of assigned enrollments 331 * @param assignment current assignment 332 * @return enrollments in this configuration 333 **/ 334 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { 335 return getContext(assignment).getEnrollments(); 336 } 337 338 /** 339 * Enrollment weight -- weight of all requests which have an enrollment that 340 * contains this config, excluding the given one. See 341 * {@link Request#getWeight()}. 342 * @param assignment current assignment 343 * @param excludeRequest request to exclude, null if all requests are to be included 344 * @return enrollment weight 345 */ 346 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 347 return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 348 } 349 350 /** 351 * Enrollment weight including over the limit enrollments. 352 * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true. 353 * {@link Request#getWeight()}. 354 * @param assignment current assignment 355 * @param excludeRequest request to exclude, null if all requests are to be included 356 * @return enrollment weight 357 */ 358 public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 359 return getContext(assignment).getEnrollmentTotalWeight(assignment, excludeRequest); 360 } 361 362 /** 363 * Maximal weight of a single enrollment in the config 364 * @param assignment current assignment 365 * @return maximal enrollment weight 366 */ 367 public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 368 return getContext(assignment).getMaxEnrollmentWeight(); 369 } 370 371 /** 372 * Minimal weight of a single enrollment in the config 373 * @param assignment current assignment 374 * @return minimal enrollment weight 375 */ 376 public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 377 return getContext(assignment).getMinEnrollmentWeight(); 378 } 379 380 @Override 381 public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 382 return new ConfigContext(assignment); 383 } 384 385 386 @Override 387 public ConfigContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ConfigContext parentContext) { 388 return new ConfigContext(parentContext); 389 } 390 391 public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> { 392 private double iEnrollmentWeight = 0.0; 393 private double iEnrollmentTotalWeight = 0.0; 394 private double iMaxEnrollmentWeight = 0.0; 395 private double iMinEnrollmentWeight = 0.0; 396 private Set<Enrollment> iEnrollments = null; 397 private boolean iReadOnly = false; 398 399 public ConfigContext(Assignment<Request, Enrollment> assignment) { 400 iEnrollments = new HashSet<Enrollment>(); 401 for (Course course: getOffering().getCourses()) { 402 for (CourseRequest request: course.getRequests()) { 403 Enrollment enrollment = assignment.getValue(request); 404 if (enrollment != null && Config.this.equals(enrollment.getConfig())) 405 assigned(assignment, enrollment); 406 } 407 } 408 } 409 410 public ConfigContext(ConfigContext parent) { 411 iEnrollmentWeight = parent.iEnrollmentWeight; 412 iEnrollmentTotalWeight = parent.iEnrollmentTotalWeight; 413 iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight; 414 iMinEnrollmentWeight = parent.iMinEnrollmentWeight; 415 iEnrollments = parent.iEnrollments; 416 iReadOnly = true; 417 } 418 419 /** Called when an enrollment with this config is assigned to a request */ 420 @Override 421 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 422 if (iReadOnly) { 423 iEnrollments = new HashSet<Enrollment>(iEnrollments); 424 iReadOnly = false; 425 } 426 if (iEnrollments.isEmpty()) { 427 iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight(); 428 } else { 429 iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight()); 430 iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight()); 431 } 432 if (iEnrollments.add(enrollment)) { 433 iEnrollmentTotalWeight += enrollment.getRequest().getWeight(); 434 if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()) 435 iEnrollmentWeight += enrollment.getRequest().getWeight(); 436 } 437 } 438 439 /** Called when an enrollment with this config is unassigned from a request */ 440 @Override 441 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 442 if (iReadOnly) { 443 iEnrollments = new HashSet<Enrollment>(iEnrollments); 444 iReadOnly = false; 445 } 446 if (iEnrollments.remove(enrollment)) { 447 iEnrollmentTotalWeight -= enrollment.getRequest().getWeight(); 448 if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()) 449 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 450 } 451 if (iEnrollments.isEmpty()) { 452 iMinEnrollmentWeight = iMaxEnrollmentWeight = 0; 453 } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) { 454 if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) { 455 double newMinEnrollmentWeight = Double.MAX_VALUE; 456 for (Enrollment e : iEnrollments) { 457 if (e.getRequest().getWeight() == iMinEnrollmentWeight) { 458 newMinEnrollmentWeight = iMinEnrollmentWeight; 459 break; 460 } else { 461 newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight()); 462 } 463 } 464 iMinEnrollmentWeight = newMinEnrollmentWeight; 465 } 466 if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) { 467 double newMaxEnrollmentWeight = Double.MIN_VALUE; 468 for (Enrollment e : iEnrollments) { 469 if (e.getRequest().getWeight() == iMaxEnrollmentWeight) { 470 newMaxEnrollmentWeight = iMaxEnrollmentWeight; 471 break; 472 } else { 473 newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight()); 474 } 475 } 476 iMaxEnrollmentWeight = newMaxEnrollmentWeight; 477 } 478 } 479 } 480 481 /** 482 * Enrollment weight -- weight of all requests which have an enrollment that 483 * contains this config, excluding the given one. See 484 * {@link Request#getWeight()}. 485 * @param assignment current assignment 486 * @param excludeRequest request to exclude 487 * @return enrollment weight 488 */ 489 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 490 double weight = iEnrollmentWeight; 491 if (excludeRequest != null) { 492 Enrollment enrollment = assignment.getValue(excludeRequest); 493 if (enrollment != null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 494 weight -= excludeRequest.getWeight(); 495 } 496 return weight; 497 } 498 499 /** 500 * Enrollment weight including over the limit enrollments. 501 * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true. 502 * @param assignment current assignment 503 * @param excludeRequest request to exclude 504 * @return enrollment weight 505 */ 506 public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 507 double weight = iEnrollmentTotalWeight; 508 if (excludeRequest != null) { 509 Enrollment enrollment = assignment.getValue(excludeRequest); 510 if (enrollment != null && iEnrollments.contains(enrollment)) 511 weight -= excludeRequest.getWeight(); 512 } 513 return weight; 514 } 515 516 /** Set of assigned enrollments 517 * @return assigned enrollments (using this configuration) 518 **/ 519 public Set<Enrollment> getEnrollments() { 520 return iEnrollments; 521 } 522 523 /** 524 * Maximal weight of a single enrollment in the config 525 * @return maximal enrollment weight 526 */ 527 public double getMaxEnrollmentWeight() { 528 return iMaxEnrollmentWeight; 529 } 530 531 /** 532 * Minimal weight of a single enrollment in the config 533 * @return minimal enrollment weight 534 */ 535 public double getMinEnrollmentWeight() { 536 return iMinEnrollmentWeight; 537 } 538 } 539 540 /** 541 * True if at least one subpart of this config has a credit value set 542 * @return true if a there is a subpart credit 543 */ 544 public boolean hasCreditValue() { 545 for (Subpart subpart: getSubparts()) 546 if (subpart.hasCreditValue()) return true; 547 return false; 548 } 549 550 /** 551 * Sum of subpart credit of this config 552 * return config credit value 553 */ 554 public Float getCreditValue() { 555 float credit = 0f; boolean hasCredit = false; 556 for (Subpart subpart: getSubparts()) 557 if (subpart.hasCreditValue()) { 558 hasCredit = true; 559 credit += subpart.getCreditValue(); 560 } 561 return (hasCredit ? Float.valueOf(credit) : null); 562 } 563 564 public int getNrOnline() { 565 int online = 0; 566 for (Subpart subpart: getSubparts()) 567 if (subpart.isOnline()) online ++; 568 return online; 569 } 570 571 public int getNrArrHours() { 572 int arrHrs = 0; 573 for (Subpart subpart: getSubparts()) 574 if (!subpart.hasTime()) arrHrs ++; 575 return arrHrs; 576 } 577 578 public int getNrPast() { 579 int past = 0; 580 for (Subpart subpart: getSubparts()) 581 if (subpart.isPast()) past ++; 582 return past; 583 } 584}