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