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 Tomas Muller<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 055 /** 056 * Constructor 057 * 058 * @param id 059 * instructional offering configuration unique id 060 * @param limit 061 * configuration limit (-1 for unlimited) 062 * @param name 063 * configuration name 064 * @param offering 065 * instructional offering to which this configuration belongs 066 */ 067 public Config(long id, int limit, String name, Offering offering) { 068 iId = id; 069 iLimit = limit; 070 iName = name; 071 iOffering = offering; 072 iOffering.getConfigs().add(this); 073 } 074 075 /** Configuration id 076 * @return instructional offering configuration unique id 077 **/ 078 public long getId() { 079 return iId; 080 } 081 082 /** 083 * Configuration limit. This is defines the maximal number of students that can be 084 * enrolled into this configuration at the same time. It is -1 in the case of an 085 * unlimited configuration 086 * @return configuration limit 087 */ 088 public int getLimit() { 089 return iLimit; 090 } 091 092 /** Set configuration limit 093 * @param limit configuration limit, -1 if unlimited 094 **/ 095 public void setLimit(int limit) { 096 iLimit = limit; 097 } 098 099 100 101 /** Configuration name 102 * @return configuration name 103 **/ 104 public String getName() { 105 return iName; 106 } 107 108 /** Instructional offering to which this configuration belongs. 109 * @return instructional offering 110 **/ 111 public Offering getOffering() { 112 return iOffering; 113 } 114 115 /** List of subparts 116 * @return scheduling subparts 117 **/ 118 public List<Subpart> getSubparts() { 119 return iSubparts; 120 } 121 122 @Override 123 public String toString() { 124 return getName(); 125 } 126 127 /** Average minimal penalty from {@link Subpart#getMinPenalty()} 128 * @return minimal penalty 129 **/ 130 public double getMinPenalty() { 131 double min = 0.0; 132 for (Subpart subpart : getSubparts()) { 133 min += subpart.getMinPenalty(); 134 } 135 return min / getSubparts().size(); 136 } 137 138 /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 139 * @return maximal penalty 140 **/ 141 public double getMaxPenalty() { 142 double max = 0.0; 143 for (Subpart subpart : getSubparts()) { 144 max += subpart.getMinPenalty(); 145 } 146 return max / getSubparts().size(); 147 } 148 149 /** 150 * Available space in the configuration that is not reserved by any config reservation 151 * @param assignment current assignment 152 * @param excludeRequest excluding given request (if not null) 153 * @return available space 154 **/ 155 public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 156 // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 157 // (in which case there is no unreserved space) 158 if (getLimit() < 0) { 159 // exclude reservations that are not directly set on this section 160 for (Reservation r: getConfigReservations()) { 161 // ignore expired reservations 162 if (r.isExpired()) continue; 163 // there is an unlimited reservation -> no unreserved space 164 if (r.getLimit() < 0) return 0.0; 165 } 166 return Double.MAX_VALUE; 167 } 168 169 double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 170 // exclude reservations that are not directly set on this section 171 for (Reservation r: getConfigReservations()) { 172 // ignore expired reservations 173 if (r.isExpired()) continue; 174 // unlimited reservation -> all the space is reserved 175 if (r.getLimit() < 0.0) return 0.0; 176 // compute space that can be potentially taken by this reservation 177 double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, excludeRequest); 178 // deduct the space from available space 179 available -= Math.max(0.0, reserved); 180 } 181 182 return available; 183 } 184 185 /** 186 * Total space in the configuration that cannot be reserved by any config reservation 187 * @return total unreserved space 188 **/ 189 public synchronized double getTotalUnreservedSpace() { 190 if (iTotalUnreservedSpace == null) 191 iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache(); 192 return iTotalUnreservedSpace; 193 } 194 private Double iTotalUnreservedSpace = null; 195 private double getTotalUnreservedSpaceNoCache() { 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() < 0) return 0.0; 205 } 206 return Double.MAX_VALUE; 207 } 208 209 // we need to check all reservations linked with this section 210 double available = getLimit(), reserved = 0, exclusive = 0; 211 Set<Config> configs = new HashSet<Config>(); 212 reservations: for (Reservation r: getConfigReservations()) { 213 // ignore expired reservations 214 if (r.isExpired()) continue; 215 // unlimited reservation -> no unreserved space 216 if (r.getLimit() < 0) return 0.0; 217 for (Config s: r.getConfigs()) { 218 if (s.equals(this)) continue; 219 if (s.getLimit() < 0) continue reservations; 220 if (configs.add(s)) 221 available += s.getLimit(); 222 } 223 reserved += r.getLimit(); 224 if (r.getConfigs().size() == 1) 225 exclusive += r.getLimit(); 226 } 227 228 return Math.min(available - reserved, getLimit() - exclusive); 229 } 230 231 /** 232 * Get reservations for this configuration 233 * @return related reservations 234 */ 235 public synchronized List<Reservation> getReservations() { 236 if (iReservations == null) { 237 iReservations = new ArrayList<Reservation>(); 238 for (Reservation r: getOffering().getReservations()) { 239 if (r.getConfigs().isEmpty() || r.getConfigs().contains(this)) 240 iReservations.add(r); 241 } 242 } 243 return iReservations; 244 } 245 List<Reservation> iReservations = null; 246 247 /** 248 * Get reservations that require this configuration 249 * @return related reservations 250 */ 251 public synchronized List<Reservation> getConfigReservations() { 252 if (iConfigReservations == null) { 253 iConfigReservations = new ArrayList<Reservation>(); 254 for (Reservation r: getOffering().getReservations()) { 255 if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this)) 256 iConfigReservations.add(r); 257 } 258 } 259 return iConfigReservations; 260 } 261 List<Reservation> iConfigReservations = null; 262 263 /** 264 * Clear reservation information that was cached on this configuration or below 265 */ 266 public synchronized void clearReservationCache() { 267 for (Subpart s: getSubparts()) 268 s.clearReservationCache(); 269 iReservations = null; 270 iConfigReservations = null; 271 iTotalUnreservedSpace = null; 272 } 273 274 @Override 275 public boolean equals(Object o) { 276 if (o == null || !(o instanceof Config)) return false; 277 return getId() == ((Config)o).getId(); 278 } 279 280 @Override 281 public int hashCode() { 282 return new Long(getId()).hashCode(); 283 } 284 285 @Override 286 public Model<Request, Enrollment> getModel() { 287 return getOffering().getModel(); 288 } 289 290 /** Set of assigned enrollments 291 * @param assignment current assignment 292 * @return enrollments in this configuration 293 **/ 294 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { 295 return getContext(assignment).getEnrollments(); 296 } 297 298 /** 299 * Enrollment weight -- weight of all requests which have an enrollment that 300 * contains this config, excluding the given one. See 301 * {@link Request#getWeight()}. 302 * @param assignment current assignment 303 * @param excludeRequest request to exclude, null if all requests are to be included 304 * @return enrollment weight 305 */ 306 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 307 return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 308 } 309 310 /** 311 * Maximal weight of a single enrollment in the config 312 * @param assignment current assignment 313 * @return maximal enrollment weight 314 */ 315 public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 316 return getContext(assignment).getMaxEnrollmentWeight(); 317 } 318 319 /** 320 * Minimal weight of a single enrollment in the config 321 * @param assignment current assignment 322 * @return minimal enrollment weight 323 */ 324 public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 325 return getContext(assignment).getMinEnrollmentWeight(); 326 } 327 328 @Override 329 public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 330 return new ConfigContext(assignment); 331 } 332 333 334 @Override 335 public ConfigContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ConfigContext parentContext) { 336 return new ConfigContext(parentContext); 337 } 338 339 public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> { 340 private double iEnrollmentWeight = 0.0; 341 private double iMaxEnrollmentWeight = 0.0; 342 private double iMinEnrollmentWeight = 0.0; 343 private Set<Enrollment> iEnrollments = null; 344 private boolean iReadOnly = false; 345 346 public ConfigContext(Assignment<Request, Enrollment> assignment) { 347 iEnrollments = new HashSet<Enrollment>(); 348 for (Course course: getOffering().getCourses()) { 349 for (CourseRequest request: course.getRequests()) { 350 Enrollment enrollment = assignment.getValue(request); 351 if (enrollment != null && Config.this.equals(enrollment.getConfig())) 352 assigned(assignment, enrollment); 353 } 354 } 355 } 356 357 public ConfigContext(ConfigContext parent) { 358 iEnrollmentWeight = parent.iEnrollmentWeight; 359 iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight; 360 iMinEnrollmentWeight = parent.iMinEnrollmentWeight; 361 iEnrollments = parent.iEnrollments; 362 iReadOnly = true; 363 } 364 365 /** Called when an enrollment with this config is assigned to a request */ 366 @Override 367 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 368 if (iReadOnly) { 369 iEnrollments = new HashSet<Enrollment>(iEnrollments); 370 iReadOnly = false; 371 } 372 if (iEnrollments.isEmpty()) { 373 iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight(); 374 } else { 375 iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight()); 376 iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight()); 377 } 378 if (iEnrollments.add(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 379 iEnrollmentWeight += enrollment.getRequest().getWeight(); 380 } 381 382 /** Called when an enrollment with this config is unassigned from a request */ 383 @Override 384 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 385 if (iReadOnly) { 386 iEnrollments = new HashSet<Enrollment>(iEnrollments); 387 iReadOnly = false; 388 } 389 if (iEnrollments.remove(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 390 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 391 if (iEnrollments.isEmpty()) { 392 iMinEnrollmentWeight = iMaxEnrollmentWeight = 0; 393 } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) { 394 if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) { 395 double newMinEnrollmentWeight = Double.MAX_VALUE; 396 for (Enrollment e : iEnrollments) { 397 if (e.getRequest().getWeight() == iMinEnrollmentWeight) { 398 newMinEnrollmentWeight = iMinEnrollmentWeight; 399 break; 400 } else { 401 newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight()); 402 } 403 } 404 iMinEnrollmentWeight = newMinEnrollmentWeight; 405 } 406 if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) { 407 double newMaxEnrollmentWeight = Double.MIN_VALUE; 408 for (Enrollment e : iEnrollments) { 409 if (e.getRequest().getWeight() == iMaxEnrollmentWeight) { 410 newMaxEnrollmentWeight = iMaxEnrollmentWeight; 411 break; 412 } else { 413 newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight()); 414 } 415 } 416 iMaxEnrollmentWeight = newMaxEnrollmentWeight; 417 } 418 } 419 } 420 421 /** 422 * Enrollment weight -- weight of all requests which have an enrollment that 423 * contains this config, excluding the given one. See 424 * {@link Request#getWeight()}. 425 * @param assignment current assignment 426 * @param excludeRequest request to exclude 427 * @return enrollment weight 428 */ 429 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 430 double weight = iEnrollmentWeight; 431 if (excludeRequest != null) { 432 Enrollment enrollment = assignment.getValue(excludeRequest); 433 if (enrollment != null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())) 434 weight -= excludeRequest.getWeight(); 435 } 436 return weight; 437 } 438 439 /** Set of assigned enrollments 440 * @return assigned enrollments (using this configuration) 441 **/ 442 public Set<Enrollment> getEnrollments() { 443 return iEnrollments; 444 } 445 446 /** 447 * Maximal weight of a single enrollment in the config 448 * @return maximal enrollment weight 449 */ 450 public double getMaxEnrollmentWeight() { 451 return iMaxEnrollmentWeight; 452 } 453 454 /** 455 * Minimal weight of a single enrollment in the config 456 * @return minimal enrollment weight 457 */ 458 public double getMinEnrollmentWeight() { 459 return iMinEnrollmentWeight; 460 } 461 } 462}