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.model.Model; 012import org.cpsolver.studentsct.reservation.Reservation; 013 014 015 016 017/** 018 * Representation of a configuration of an offering. A configuration contains 019 * id, name, an offering and a list of subparts. <br> 020 * <br> 021 * Each instructional offering (see {@link Offering}) contains one or more 022 * configurations. Each configuration contain one or more subparts. Each student 023 * has to take a class of each subpart of one of the possible configurations. 024 * 025 * <br> 026 * <br> 027 * 028 * @version StudentSct 1.3 (Student Sectioning)<br> 029 * Copyright (C) 2007 - 2014 Tomas Muller<br> 030 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 031 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 032 * <br> 033 * This library is free software; you can redistribute it and/or modify 034 * it under the terms of the GNU Lesser General Public License as 035 * published by the Free Software Foundation; either version 3 of the 036 * License, or (at your option) any later version. <br> 037 * <br> 038 * This library is distributed in the hope that it will be useful, but 039 * WITHOUT ANY WARRANTY; without even the implied warranty of 040 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 041 * Lesser General Public License for more details. <br> 042 * <br> 043 * You should have received a copy of the GNU Lesser General Public 044 * License along with this library; if not see 045 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 046 */ 047public class Config extends AbstractClassWithContext<Request, Enrollment, Config.ConfigContext> { 048 private long iId = -1; 049 private String iName = null; 050 private Offering iOffering = null; 051 private int iLimit = -1; 052 private List<Subpart> iSubparts = new ArrayList<Subpart>(); 053 054 /** 055 * Constructor 056 * 057 * @param id 058 * instructional offering configuration unique id 059 * @param limit 060 * configuration limit (-1 for unlimited) 061 * @param name 062 * configuration name 063 * @param offering 064 * instructional offering to which this configuration belongs 065 */ 066 public Config(long id, int limit, String name, Offering offering) { 067 iId = id; 068 iLimit = limit; 069 iName = name; 070 iOffering = offering; 071 iOffering.getConfigs().add(this); 072 } 073 074 /** Configuration id 075 * @return instructional offering configuration unique id 076 **/ 077 public long getId() { 078 return iId; 079 } 080 081 /** 082 * Configuration limit. This is defines the maximal number of students that can be 083 * enrolled into this configuration at the same time. It is -1 in the case of an 084 * unlimited configuration 085 * @return configuration limit 086 */ 087 public int getLimit() { 088 return iLimit; 089 } 090 091 /** Set configuration limit 092 * @param limit configuration limit, -1 if unlimited 093 **/ 094 public void setLimit(int limit) { 095 iLimit = limit; 096 } 097 098 099 100 /** Configuration name 101 * @return configuration name 102 **/ 103 public String getName() { 104 return iName; 105 } 106 107 /** Instructional offering to which this configuration belongs. 108 * @return instructional offering 109 **/ 110 public Offering getOffering() { 111 return iOffering; 112 } 113 114 /** List of subparts 115 * @return scheduling subparts 116 **/ 117 public List<Subpart> getSubparts() { 118 return iSubparts; 119 } 120 121 @Override 122 public String toString() { 123 return getName(); 124 } 125 126 /** Average minimal penalty from {@link Subpart#getMinPenalty()} 127 * @return minimal penalty 128 **/ 129 public double getMinPenalty() { 130 double min = 0.0; 131 for (Subpart subpart : getSubparts()) { 132 min += subpart.getMinPenalty(); 133 } 134 return min / getSubparts().size(); 135 } 136 137 /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 138 * @return maximal penalty 139 **/ 140 public double getMaxPenalty() { 141 double max = 0.0; 142 for (Subpart subpart : getSubparts()) { 143 max += subpart.getMinPenalty(); 144 } 145 return max / getSubparts().size(); 146 } 147 148 /** 149 * Available space in the configuration that is not reserved by any config reservation 150 * @param assignment current assignment 151 * @param excludeRequest excluding given request (if not null) 152 * @return available space 153 **/ 154 public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 155 // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 156 // (in which case there is no unreserved space) 157 if (getLimit() < 0) { 158 // exclude reservations that are not directly set on this section 159 for (Reservation r: getConfigReservations()) { 160 // ignore expired reservations 161 if (r.isExpired()) continue; 162 // there is an unlimited reservation -> no unreserved space 163 if (r.getLimit() < 0) return 0.0; 164 } 165 return Double.MAX_VALUE; 166 } 167 168 double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 169 // exclude reservations that are not directly set on this section 170 for (Reservation r: getConfigReservations()) { 171 // ignore expired reservations 172 if (r.isExpired()) continue; 173 // unlimited reservation -> all the space is reserved 174 if (r.getLimit() < 0.0) return 0.0; 175 // compute space that can be potentially taken by this reservation 176 double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, excludeRequest); 177 // deduct the space from available space 178 available -= Math.max(0.0, reserved); 179 } 180 181 return available; 182 } 183 184 /** 185 * Total space in the configuration that cannot be reserved by any config reservation 186 * @return total unreserved space 187 **/ 188 public synchronized double getTotalUnreservedSpace() { 189 if (iTotalUnreservedSpace == null) 190 iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache(); 191 return iTotalUnreservedSpace; 192 } 193 private Double iTotalUnreservedSpace = null; 194 private double getTotalUnreservedSpaceNoCache() { 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 // we need to check all reservations linked with this section 209 double available = getLimit(), reserved = 0, exclusive = 0; 210 Set<Config> configs = new HashSet<Config>(); 211 reservations: for (Reservation r: getConfigReservations()) { 212 // ignore expired reservations 213 if (r.isExpired()) continue; 214 // unlimited reservation -> no unreserved space 215 if (r.getLimit() < 0) return 0.0; 216 for (Config s: r.getConfigs()) { 217 if (s.equals(this)) continue; 218 if (s.getLimit() < 0) continue reservations; 219 if (configs.add(s)) 220 available += s.getLimit(); 221 } 222 reserved += r.getLimit(); 223 if (r.getConfigs().size() == 1) 224 exclusive += r.getLimit(); 225 } 226 227 return Math.min(available - reserved, getLimit() - exclusive); 228 } 229 230 /** 231 * Get reservations for this configuration 232 * @return related reservations 233 */ 234 public synchronized List<Reservation> getReservations() { 235 if (iReservations == null) { 236 iReservations = new ArrayList<Reservation>(); 237 for (Reservation r: getOffering().getReservations()) { 238 if (r.getConfigs().isEmpty() || r.getConfigs().contains(this)) 239 iReservations.add(r); 240 } 241 } 242 return iReservations; 243 } 244 List<Reservation> iReservations = null; 245 246 /** 247 * Get reservations that require this configuration 248 * @return related reservations 249 */ 250 public synchronized List<Reservation> getConfigReservations() { 251 if (iConfigReservations == null) { 252 iConfigReservations = new ArrayList<Reservation>(); 253 for (Reservation r: getOffering().getReservations()) { 254 if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this)) 255 iConfigReservations.add(r); 256 } 257 } 258 return iConfigReservations; 259 } 260 List<Reservation> iConfigReservations = null; 261 262 /** 263 * Clear reservation information that was cached on this configuration or below 264 */ 265 public synchronized void clearReservationCache() { 266 for (Subpart s: getSubparts()) 267 s.clearReservationCache(); 268 iReservations = null; 269 iConfigReservations = null; 270 iTotalUnreservedSpace = null; 271 } 272 273 @Override 274 public boolean equals(Object o) { 275 if (o == null || !(o instanceof Config)) return false; 276 return getId() == ((Config)o).getId(); 277 } 278 279 @Override 280 public int hashCode() { 281 return new Long(getId()).hashCode(); 282 } 283 284 @Override 285 public Model<Request, Enrollment> getModel() { 286 return getOffering().getModel(); 287 } 288 289 /** Set of assigned enrollments 290 * @param assignment current assignment 291 * @return enrollments in this configuration 292 **/ 293 public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { 294 return getContext(assignment).getEnrollments(); 295 } 296 297 /** 298 * Enrollment weight -- weight of all requests which have an enrollment that 299 * contains this config, excluding the given one. See 300 * {@link Request#getWeight()}. 301 * @param assignment current assignment 302 * @param excludeRequest request to exclude, null if all requests are to be included 303 * @return enrollment weight 304 */ 305 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 306 return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 307 } 308 309 /** 310 * Maximal weight of a single enrollment in the config 311 * @param assignment current assignment 312 * @return maximal enrollment weight 313 */ 314 public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 315 return getContext(assignment).getMaxEnrollmentWeight(); 316 } 317 318 /** 319 * Minimal weight of a single enrollment in the config 320 * @param assignment current assignment 321 * @return minimal enrollment weight 322 */ 323 public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) { 324 return getContext(assignment).getMinEnrollmentWeight(); 325 } 326 327 @Override 328 public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 329 return new ConfigContext(assignment); 330 } 331 332 public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> { 333 private double iEnrollmentWeight = 0.0; 334 private double iMaxEnrollmentWeight = 0.0; 335 private double iMinEnrollmentWeight = 0.0; 336 private Set<Enrollment> iEnrollments = new HashSet<Enrollment>(); 337 338 public ConfigContext(Assignment<Request, Enrollment> assignment) { 339 for (Course course: getOffering().getCourses()) { 340 for (CourseRequest request: course.getRequests()) { 341 Enrollment enrollment = assignment.getValue(request); 342 if (enrollment != null && Config.this.equals(enrollment.getConfig())) 343 assigned(assignment, enrollment); 344 } 345 } 346 } 347 348 /** Called when an enrollment with this config is assigned to a request */ 349 @Override 350 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 351 if (iEnrollments.isEmpty()) { 352 iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight(); 353 } else { 354 iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight()); 355 iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight()); 356 } 357 if (iEnrollments.add(enrollment)) 358 iEnrollmentWeight += enrollment.getRequest().getWeight(); 359 } 360 361 /** Called when an enrollment with this config is unassigned from a request */ 362 @Override 363 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 364 if (iEnrollments.remove(enrollment)) 365 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 366 if (iEnrollments.isEmpty()) { 367 iMinEnrollmentWeight = iMaxEnrollmentWeight = 0; 368 } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) { 369 if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) { 370 double newMinEnrollmentWeight = Double.MAX_VALUE; 371 for (Enrollment e : iEnrollments) { 372 if (e.getRequest().getWeight() == iMinEnrollmentWeight) { 373 newMinEnrollmentWeight = iMinEnrollmentWeight; 374 break; 375 } else { 376 newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight()); 377 } 378 } 379 iMinEnrollmentWeight = newMinEnrollmentWeight; 380 } 381 if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) { 382 double newMaxEnrollmentWeight = Double.MIN_VALUE; 383 for (Enrollment e : iEnrollments) { 384 if (e.getRequest().getWeight() == iMaxEnrollmentWeight) { 385 newMaxEnrollmentWeight = iMaxEnrollmentWeight; 386 break; 387 } else { 388 newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight()); 389 } 390 } 391 iMaxEnrollmentWeight = newMaxEnrollmentWeight; 392 } 393 } 394 } 395 396 /** 397 * Enrollment weight -- weight of all requests which have an enrollment that 398 * contains this config, excluding the given one. See 399 * {@link Request#getWeight()}. 400 * @param assignment current assignment 401 * @param excludeRequest request to exclude 402 * @return enrollment weight 403 */ 404 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 405 double weight = iEnrollmentWeight; 406 if (excludeRequest != null && assignment.getValue(excludeRequest) != null && iEnrollments.contains(assignment.getValue(excludeRequest))) 407 weight -= excludeRequest.getWeight(); 408 return weight; 409 } 410 411 /** Set of assigned enrollments 412 * @return assigned enrollments (using this configuration) 413 **/ 414 public Set<Enrollment> getEnrollments() { 415 return iEnrollments; 416 } 417 418 /** 419 * Maximal weight of a single enrollment in the config 420 * @return maximal enrollment weight 421 */ 422 public double getMaxEnrollmentWeight() { 423 return iMaxEnrollmentWeight; 424 } 425 426 /** 427 * Minimal weight of a single enrollment in the config 428 * @return minimal enrollment weight 429 */ 430 public double getMinEnrollmentWeight() { 431 return iMinEnrollmentWeight; 432 } 433 } 434}