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