001package org.cpsolver.studentsct.model; 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.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 * Representation of a group of students requesting the same course that 017 * should be scheduled in the same set of sections.<br> 018 * <br> 019 * 020 * @author Tomáš Müller 021 * @version StudentSct 1.3 (Student Sectioning)<br> 022 * Copyright (C) 2015 Tomáš Müller<br> 023 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 024 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 025 * <br> 026 * This library is free software; you can redistribute it and/or modify 027 * it under the terms of the GNU Lesser General Public License as 028 * published by the Free Software Foundation; either version 3 of the 029 * License, or (at your option) any later version. <br> 030 * <br> 031 * This library is distributed in the hope that it will be useful, but 032 * WITHOUT ANY WARRANTY; without even the implied warranty of 033 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 034 * Lesser General Public License for more details. <br> 035 * <br> 036 * You should have received a copy of the GNU Lesser General Public 037 * License along with this library; if not see 038 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 039 */ 040public class RequestGroup extends AbstractClassWithContext<Request, Enrollment, RequestGroup.RequestGroupContext> 041 implements CanInheritContext<Request, Enrollment, RequestGroup.RequestGroupContext>{ 042 private long iId = -1; 043 private String iName = null; 044 private Course iCourse; 045 private Set<CourseRequest> iRequests = new HashSet<CourseRequest>(); 046 private double iTotalWeight = 0.0; 047 048 /** 049 * Creates request group. Pair (id, course) must be unique. 050 * @param id identification of the group 051 * @param name group name 052 * @param course course for which the group is created (only course requests for this course can be of this group) 053 */ 054 public RequestGroup(long id, String name, Course course) { 055 iId = id; 056 iName = name; 057 iCourse = course; 058 iCourse.getRequestGroups().add(this); 059 } 060 061 /** 062 * Add course request to the group. It has to contain the course of this group {@link RequestGroup#getCourse()}. 063 * This is done automatically by {@link CourseRequest#addRequestGroup(RequestGroup)}. 064 * @param request course request to be added to this group 065 */ 066 public void addRequest(CourseRequest request) { 067 if (iRequests.add(request)) 068 iTotalWeight += request.getWeight(); 069 } 070 071 /** 072 * Remove course request from the group. This is done automatically by {@link CourseRequest#removeRequestGroup(RequestGroup)}. 073 * @param request course request to be removed from this group 074 */ 075 public void removeRequest(CourseRequest request) { 076 if (iRequests.remove(request)) 077 iTotalWeight -= request.getWeight(); 078 } 079 080 /** 081 * Return the set of course requests that are associated with this group. 082 * @return course requests of this group 083 */ 084 public Set<CourseRequest> getRequests() { 085 return iRequests; 086 } 087 088 /** 089 * Total weight (using {@link CourseRequest#getWeight()}) of the course requests of this group 090 * @return total weight of course requests in this group 091 */ 092 public double getTotalWeight() { 093 return iTotalWeight; 094 } 095 096 /** 097 * Request group id 098 * @return request group id 099 */ 100 public long getId() { 101 return iId; 102 } 103 104 /** 105 * Request group name 106 * @return request group name 107 */ 108 public String getName() { 109 return iName; 110 } 111 112 /** 113 * Course associated with this group. Only course requests for this course can be of this group. 114 * @return course of this request group 115 */ 116 public Course getCourse() { 117 return iCourse; 118 } 119 120 @Override 121 public boolean equals(Object o) { 122 if (o == null || !(o instanceof RequestGroup)) return false; 123 return getId() == ((RequestGroup)o).getId() && getCourse().getId() == ((RequestGroup)o).getCourse().getId(); 124 } 125 126 @Override 127 public int hashCode() { 128 return (int) (iId ^ (iCourse.getId() >>> 32)); 129 } 130 131 /** Called when an enrollment is assigned to a request of this request group */ 132 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 133 getContext(assignment).assigned(assignment, enrollment); 134 } 135 136 /** Called when an enrollment is unassigned from a request of this request group */ 137 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 138 getContext(assignment).unassigned(assignment, enrollment); 139 } 140 141 /** 142 * Enrollment weight -- weight of all requests which have an enrollment that 143 * is of this request group, excluding the given one. See 144 * {@link Request#getWeight()}. 145 * @param assignment current assignment 146 * @param excludeRequest course request to ignore, if any 147 * @return enrollment weight 148 */ 149 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 150 return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest); 151 } 152 153 /** 154 * Section weight -- weight of all requests which have an enrollment that 155 * is of this request group and that includes the given section, excluding the given one. See 156 * {@link Request#getWeight()}. 157 * @param assignment current assignment 158 * @param section section in question 159 * @param excludeRequest course request to ignore, if any 160 * @return enrollment weight 161 */ 162 public double getSectionWeight(Assignment<Request, Enrollment> assignment, Section section, Request excludeRequest) { 163 return getContext(assignment).getSectionWeight(assignment, section, excludeRequest); 164 } 165 166 /** 167 * Return how much is the given enrollment similar to other enrollments of this group. 168 * @param assignment current assignment 169 * @param enrollment enrollment in question 170 * @param bestRatio how much of the weight should be used on estimation of the enrollment potential 171 * (considering that students of this group that are not yet enrolled can take the same enrollment) 172 * @param fillRatio how much of the weight should be used in estimation how well are the sections of this enrollments going to be filled 173 * (bestRatio + fillRatio <= 1.0) 174 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 175 */ 176 public double getEnrollmentSpread(Assignment<Request, Enrollment> assignment, Enrollment enrollment, double bestRatio, double fillRatio) { 177 return getContext(assignment).getEnrollmentSpread(assignment, enrollment, bestRatio, fillRatio); 178 } 179 180 /** 181 * Return average section spread of this group. It reflects the probability of two students of this group 182 * being enrolled in the same section. 183 * @param assignment current assignment 184 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 185 */ 186 public double getAverageSpread(Assignment<Request, Enrollment> assignment) { 187 return getContext(assignment).getAverageSpread(); 188 } 189 190 /** 191 * Return section spread of this group. It reflects the probability of two students of this group 192 * being enrolled in this section. 193 * @param assignment current assignment 194 * @param section given section 195 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 196 */ 197 public double getSectionSpread(Assignment<Request, Enrollment> assignment, Section section) { 198 return getContext(assignment).getSectionSpread(section); 199 } 200 201 public class RequestGroupContext implements AssignmentConstraintContext<Request, Enrollment> { 202 private Set<Enrollment> iEnrollments = null; 203 private double iEnrollmentWeight = 0.0; 204 private Map<Long, Double> iSectionWeight = null; 205 private boolean iReadOnly = false; 206 207 public RequestGroupContext(Assignment<Request, Enrollment> assignment) { 208 iEnrollments = new HashSet<Enrollment>(); 209 iSectionWeight = new HashMap<Long, Double>(); 210 for (CourseRequest request: getCourse().getRequests()) { 211 if (request.getRequestGroups().contains(RequestGroup.this)) { 212 Enrollment enrollment = assignment.getValue(request); 213 if (enrollment != null && getCourse().equals(enrollment.getCourse())) 214 assigned(assignment, enrollment); 215 } 216 } 217 } 218 219 public RequestGroupContext(RequestGroupContext parent) { 220 iEnrollmentWeight = parent.iEnrollmentWeight; 221 iEnrollments = parent.iEnrollments; 222 iSectionWeight = parent.iSectionWeight; 223 iReadOnly = true; 224 } 225 226 /** Called when an enrollment is assigned to a request of this request group */ 227 @Override 228 public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 229 if (iReadOnly) { 230 iEnrollments = new HashSet<Enrollment>(iEnrollments); 231 iSectionWeight = new HashMap<Long, Double>(iSectionWeight); 232 iReadOnly = false; 233 } 234 if (iEnrollments.add(enrollment)) { 235 iEnrollmentWeight += enrollment.getRequest().getWeight(); 236 for (Section section: enrollment.getSections()) { 237 Double weight = iSectionWeight.get(section.getId()); 238 iSectionWeight.put(section.getId(), enrollment.getRequest().getWeight() + (weight == null ? 0.0 : weight.doubleValue())); 239 } 240 } 241 } 242 243 /** Called when an enrollment is unassigned from a request of this request group */ 244 @Override 245 public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) { 246 if (iReadOnly) { 247 iEnrollments = new HashSet<Enrollment>(iEnrollments); 248 iSectionWeight = new HashMap<Long, Double>(iSectionWeight); 249 iReadOnly = false; 250 } 251 if (iEnrollments.remove(enrollment)) { 252 iEnrollmentWeight -= enrollment.getRequest().getWeight(); 253 for (Section section: enrollment.getSections()) { 254 Double weight = iSectionWeight.get(section.getId()); 255 iSectionWeight.put(section.getId(), weight - enrollment.getRequest().getWeight()); 256 } 257 } 258 } 259 260 /** Set of assigned enrollments 261 * @return assigned enrollments of this request group 262 **/ 263 public Set<Enrollment> getEnrollments() { 264 return iEnrollments; 265 } 266 267 /** 268 * Enrollment weight -- weight of all requests which have an enrollment that 269 * is of this request group, excluding the given one. See 270 * {@link Request#getWeight()}. 271 * @param assignment current assignment 272 * @param excludeRequest course request to ignore, if any 273 * @return enrollment weight 274 */ 275 public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) { 276 double weight = iEnrollmentWeight; 277 if (excludeRequest != null) { 278 Enrollment enrollment = assignment.getValue(excludeRequest); 279 if (enrollment!= null && iEnrollments.contains(enrollment)) 280 weight -= excludeRequest.getWeight(); 281 } 282 return weight; 283 } 284 285 /** 286 * Section weight -- weight of all requests which have an enrollment that 287 * is of this request group and that includes the given section, excluding the given one. See 288 * {@link Request#getWeight()}. 289 * @param assignment current assignment 290 * @param section section in question 291 * @param excludeRequest course request to ignore, if any 292 * @return enrollment weight 293 */ 294 public double getSectionWeight(Assignment<Request, Enrollment> assignment, Section section, Request excludeRequest) { 295 Double weight = iSectionWeight.get(section.getId()); 296 if (excludeRequest != null && weight != null) { 297 Enrollment enrollment = assignment.getValue(excludeRequest); 298 if (enrollment!= null && iEnrollments.contains(enrollment) && enrollment.getSections().contains(section)) 299 weight -= excludeRequest.getWeight(); 300 } 301 return (weight == null ? 0.0 : weight.doubleValue()); 302 } 303 304 /** 305 * Return space available in the given section 306 * @param assignment current assignment 307 * @param section section to check 308 * @param enrollment enrollment in question 309 * @return check section reservations, if present; use unreserved space otherwise 310 */ 311 private double getAvailableSpace(Assignment<Request, Enrollment> assignment, Section section, Enrollment enrollment) { 312 Reservation reservation = enrollment.getReservation(); 313 Set<Section> sections = (reservation == null ? null : reservation.getSections(section.getSubpart())); 314 if (reservation != null && sections != null && sections.contains(section) && !reservation.isExpired()) { 315 double sectionAvailable = (section.getLimit() < 0 ? Double.MAX_VALUE : section.getLimit() - section.getEnrollmentWeight(assignment, enrollment.getRequest())); 316 double reservationAvailable = reservation.getReservedAvailableSpace(assignment, enrollment.getConfig(), enrollment.getRequest()); 317 return Math.min(sectionAvailable, reservationAvailable) + (reservation.mustBeUsed() ? 0.0 : section.getUnreservedSpace(assignment, enrollment.getRequest())); 318 } else { 319 return section.getUnreservedSpace(assignment, enrollment.getRequest()); 320 } 321 } 322 323 /** 324 * Return how much is the given enrollment (which is not part of the request group) creating an issue for this request group 325 * @param assignment current assignment 326 * @param enrollment enrollment in question 327 * @param bestRatio how much of the weight should be used on estimation of the enrollment potential 328 * (considering that students of this group that are not yet enrolled can take the same enrollment) 329 * @param fillRatio how much of the weight should be used in estimation how well are the sections of this enrollments going to be filled 330 * (bestRatio + fillRatio <= 1.0) 331 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 332 */ 333 public double getEnrollmentSpread(Assignment<Request, Enrollment> assignment, Enrollment enrollment, double bestRatio, double fillRatio) { 334 if (iTotalWeight <= 1.0) return 1.0; 335 336 // enrollment weight (excluding the given enrollment) 337 double totalEnrolled = getEnrollmentWeight(assignment, enrollment.getRequest()); 338 double totalRemaining = iTotalWeight - totalEnrolled; 339 340 // section weight (also excluding the given enrollment) 341 Enrollment e = assignment.getValue(enrollment.getRequest()); 342 double enrollmentPairs = 0.0, bestPairs = 0.0, fill = 0.0; 343 for (Section section: enrollment.getSections()) { 344 double potential = Math.max(Math.min(totalRemaining, getAvailableSpace(assignment, section, enrollment)), enrollment.getRequest().getWeight()); 345 Double enrolled = iSectionWeight.get(section.getId()); 346 if (enrolled != null) { 347 if (e != null && e.getSections().contains(section)) 348 enrolled -= enrollment.getRequest().getWeight(); 349 potential += enrolled; 350 enrollmentPairs += enrolled * (enrolled + 1.0); 351 } 352 bestPairs += potential * (potential - 1.0); 353 if (section.getLimit() > potential) { 354 fill += potential / section.getLimit(); 355 } else { 356 fill += 1.0; 357 } 358 } 359 double pEnrl = (totalEnrolled < 1.0 ? 0.0 : (enrollmentPairs / enrollment.getSections().size()) / (totalEnrolled * (totalEnrolled + 1.0))); 360 double pBest = (bestPairs / enrollment.getSections().size()) / (iTotalWeight * (iTotalWeight - 1.0)); 361 double pFill = fill / enrollment.getSections().size(); 362 363 return (1.0 - bestRatio - fillRatio) * pEnrl + bestRatio * pBest + fillRatio * pFill; 364 } 365 366 /** 367 * Return average section spread of this group. It reflects the probability of two students of this group 368 * being enrolled in the same section. 369 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 370 */ 371 public double getAverageSpread() { 372 // none or just one enrollment -> all the same 373 if (iEnrollmentWeight <= 1.0) return 1.0; 374 375 double weight = 0.0; 376 for (Config config: getCourse().getOffering().getConfigs()) { 377 double pairs = 0.0; 378 for (Subpart subpart: config.getSubparts()) 379 for (Section section: subpart.getSections()) { 380 Double enrollment = iSectionWeight.get(section.getId()); 381 if (enrollment != null && enrollment > 1.0) 382 pairs += enrollment * (enrollment - 1); 383 } 384 weight += (pairs / config.getSubparts().size()) / (iEnrollmentWeight * (iEnrollmentWeight - 1.0)); 385 } 386 return weight; 387 } 388 389 /** 390 * Return section spread of this group. It reflects the probability of two students of this group 391 * being enrolled in this section. 392 * @param section given section 393 * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all 394 */ 395 public double getSectionSpread(Section section) { 396 Double w = iSectionWeight.get(section.getId()); 397 if (w != null && w > 1.0) { 398 return (w * (w - 1.0)) / (iEnrollmentWeight * (iEnrollmentWeight - 1.0)); 399 } else { 400 return 0.0; 401 } 402 } 403 } 404 405 @Override 406 public RequestGroupContext createAssignmentContext(Assignment<Request, Enrollment> assignment) { 407 return new RequestGroupContext(assignment); 408 } 409 410 @Override 411 public RequestGroupContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, RequestGroupContext parentContext) { 412 return new RequestGroupContext(parentContext); 413 } 414 415 @Override 416 public Model<Request, Enrollment> getModel() { 417 return getCourse().getModel(); 418 } 419}