001 package net.sf.cpsolver.studentsct; 002 003 import java.text.DecimalFormat; 004 import java.util.ArrayList; 005 import java.util.Collection; 006 import java.util.Comparator; 007 import java.util.HashSet; 008 import java.util.List; 009 import java.util.Map; 010 import java.util.Set; 011 import java.util.TreeSet; 012 013 import net.sf.cpsolver.ifs.model.Constraint; 014 import net.sf.cpsolver.ifs.model.ConstraintListener; 015 import net.sf.cpsolver.ifs.model.Model; 016 import net.sf.cpsolver.ifs.util.DataProperties; 017 import net.sf.cpsolver.studentsct.constraint.ConfigLimit; 018 import net.sf.cpsolver.studentsct.constraint.CourseLimit; 019 import net.sf.cpsolver.studentsct.constraint.LinkedSections; 020 import net.sf.cpsolver.studentsct.constraint.ReservationLimit; 021 import net.sf.cpsolver.studentsct.constraint.SectionLimit; 022 import net.sf.cpsolver.studentsct.constraint.StudentConflict; 023 import net.sf.cpsolver.studentsct.extension.DistanceConflict; 024 import net.sf.cpsolver.studentsct.extension.TimeOverlapsCounter; 025 import net.sf.cpsolver.studentsct.model.Config; 026 import net.sf.cpsolver.studentsct.model.Course; 027 import net.sf.cpsolver.studentsct.model.CourseRequest; 028 import net.sf.cpsolver.studentsct.model.Enrollment; 029 import net.sf.cpsolver.studentsct.model.Offering; 030 import net.sf.cpsolver.studentsct.model.Request; 031 import net.sf.cpsolver.studentsct.model.Section; 032 import net.sf.cpsolver.studentsct.model.Student; 033 import net.sf.cpsolver.studentsct.model.Subpart; 034 import net.sf.cpsolver.studentsct.reservation.Reservation; 035 import net.sf.cpsolver.studentsct.weights.PriorityStudentWeights; 036 import net.sf.cpsolver.studentsct.weights.StudentWeights; 037 038 import org.apache.log4j.Logger; 039 040 /** 041 * Student sectioning model. 042 * 043 * <br> 044 * <br> 045 * 046 * @version StudentSct 1.2 (Student Sectioning)<br> 047 * Copyright (C) 2007 - 2010 Tomas Muller<br> 048 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 049 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 050 * <br> 051 * This library is free software; you can redistribute it and/or modify 052 * it under the terms of the GNU Lesser General Public License as 053 * published by the Free Software Foundation; either version 3 of the 054 * License, or (at your option) any later version. <br> 055 * <br> 056 * This library is distributed in the hope that it will be useful, but 057 * WITHOUT ANY WARRANTY; without even the implied warranty of 058 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 059 * Lesser General Public License for more details. <br> 060 * <br> 061 * You should have received a copy of the GNU Lesser General Public 062 * License along with this library; if not see 063 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 064 */ 065 public class StudentSectioningModel extends Model<Request, Enrollment> { 066 private static Logger sLog = Logger.getLogger(StudentSectioningModel.class); 067 protected static DecimalFormat sDecimalFormat = new DecimalFormat("0.000"); 068 private List<Student> iStudents = new ArrayList<Student>(); 069 private List<Offering> iOfferings = new ArrayList<Offering>(); 070 private List<LinkedSections> iLinkedSections = new ArrayList<LinkedSections>(); 071 private Set<Student> iCompleteStudents = new java.util.HashSet<Student>(); 072 private double iTotalValue = 0.0; 073 private DataProperties iProperties; 074 private DistanceConflict iDistanceConflict = null; 075 private TimeOverlapsCounter iTimeOverlaps = null; 076 private int iNrDummyStudents = 0, iNrDummyRequests = 0, iNrAssignedDummyRequests = 0, iNrCompleteDummyStudents = 0; 077 private double iTotalDummyWeight = 0.0; 078 private double iTotalCRWeight = 0.0, iTotalDummyCRWeight = 0.0, iAssignedCRWeight = 0.0, iAssignedDummyCRWeight = 0.0; 079 private double iReservedSpace = 0.0, iTotalReservedSpace = 0.0; 080 private StudentWeights iStudentWeights = null; 081 private boolean iReservationCanAssignOverTheLimit; 082 protected double iProjectedStudentWeight = 0.0100; 083 084 085 /** 086 * Constructor 087 * 088 * @param properties 089 * configuration 090 */ 091 @SuppressWarnings("unchecked") 092 public StudentSectioningModel(DataProperties properties) { 093 super(); 094 iReservationCanAssignOverTheLimit = properties.getPropertyBoolean("Reservation.CanAssignOverTheLimit", false); 095 iAssignedVariables = new HashSet<Request>(); 096 iUnassignedVariables = new HashSet<Request>(); 097 iPerturbVariables = new HashSet<Request>(); 098 iStudentWeights = new PriorityStudentWeights(properties); 099 if (properties.getPropertyBoolean("Sectioning.SectionLimit", true)) { 100 SectionLimit sectionLimit = new SectionLimit(properties); 101 addGlobalConstraint(sectionLimit); 102 if (properties.getPropertyBoolean("Sectioning.SectionLimit.Debug", false)) { 103 sectionLimit.addConstraintListener(new ConstraintListener<Enrollment>() { 104 @Override 105 public void constraintBeforeAssigned(long iteration, Constraint<?, Enrollment> constraint, 106 Enrollment enrollment, Set<Enrollment> unassigned) { 107 if (enrollment.getStudent().isDummy()) 108 for (Enrollment conflict : unassigned) { 109 if (!conflict.getStudent().isDummy()) { 110 sLog.warn("Enrolment of a real student " + conflict.getStudent() + " is unassigned " 111 + "\n -- " + conflict + "\ndue to an enrollment of a dummy student " 112 + enrollment.getStudent() + " " + "\n -- " + enrollment); 113 } 114 } 115 } 116 117 @Override 118 public void constraintAfterAssigned(long iteration, Constraint<?, Enrollment> constraint, 119 Enrollment assigned, Set<Enrollment> unassigned) { 120 } 121 }); 122 } 123 } 124 if (properties.getPropertyBoolean("Sectioning.ConfigLimit", true)) { 125 ConfigLimit configLimit = new ConfigLimit(properties); 126 addGlobalConstraint(configLimit); 127 } 128 if (properties.getPropertyBoolean("Sectioning.CourseLimit", true)) { 129 CourseLimit courseLimit = new CourseLimit(properties); 130 addGlobalConstraint(courseLimit); 131 } 132 if (properties.getPropertyBoolean("Sectioning.ReservationLimit", true)) { 133 ReservationLimit reservationLimit = new ReservationLimit(properties); 134 addGlobalConstraint(reservationLimit); 135 } 136 try { 137 Class<StudentWeights> studentWeightsClass = (Class<StudentWeights>)Class.forName(properties.getProperty("StudentWeights.Class", PriorityStudentWeights.class.getName())); 138 iStudentWeights = studentWeightsClass.getConstructor(DataProperties.class).newInstance(properties); 139 } catch (Exception e) { 140 sLog.error("Unable to create custom student weighting model (" + e.getMessage() + "), using default.", e); 141 iStudentWeights = new PriorityStudentWeights(properties); 142 } 143 iProjectedStudentWeight = properties.getPropertyDouble("StudentWeights.ProjectedStudentWeight", iProjectedStudentWeight); 144 iProperties = properties; 145 } 146 147 /** 148 * Return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit 149 */ 150 public boolean getReservationCanAssignOverTheLimit() { 151 return iReservationCanAssignOverTheLimit; 152 } 153 154 /** 155 * Return student weighting model 156 */ 157 public StudentWeights getStudentWeights() { 158 return iStudentWeights; 159 } 160 161 /** 162 * Set student weighting model 163 */ 164 public void setStudentWeights(StudentWeights weights) { 165 iStudentWeights = weights; 166 } 167 168 /** 169 * Students 170 */ 171 public List<Student> getStudents() { 172 return iStudents; 173 } 174 175 /** 176 * Students with complete schedules (see {@link Student#isComplete()}) 177 */ 178 public Set<Student> getCompleteStudents() { 179 return iCompleteStudents; 180 } 181 182 /** 183 * Add a student into the model 184 */ 185 public void addStudent(Student student) { 186 iStudents.add(student); 187 if (student.isDummy()) 188 iNrDummyStudents++; 189 for (Request request : student.getRequests()) 190 addVariable(request); 191 if (getProperties().getPropertyBoolean("Sectioning.StudentConflict", true)) { 192 addConstraint(new StudentConflict(student)); 193 } 194 if (student.isComplete()) 195 iCompleteStudents.add(student); 196 } 197 198 @Override 199 public void addVariable(Request request) { 200 super.addVariable(request); 201 if (request instanceof CourseRequest) 202 iTotalCRWeight += request.getWeight(); 203 if (request.getStudent().isDummy()) { 204 iNrDummyRequests++; 205 iTotalDummyWeight += request.getWeight(); 206 if (request instanceof CourseRequest) 207 iTotalDummyCRWeight += request.getWeight(); 208 } 209 } 210 211 /** 212 * Recompute cached request weights 213 */ 214 public void requestWeightsChanged() { 215 iTotalCRWeight = 0.0; 216 iTotalDummyWeight = 0.0; iTotalDummyCRWeight = 0.0; 217 iAssignedCRWeight = 0.0; 218 iAssignedDummyCRWeight = 0.0; 219 iNrDummyRequests = 0; iNrAssignedDummyRequests = 0; 220 iTotalReservedSpace = 0.0; iReservedSpace = 0.0; 221 for (Request request: variables()) { 222 boolean cr = (request instanceof CourseRequest); 223 if (cr) 224 iTotalCRWeight += request.getWeight(); 225 if (request.getStudent().isDummy()) { 226 iTotalDummyWeight += request.getWeight(); 227 iNrDummyRequests ++; 228 if (cr) 229 iTotalDummyCRWeight += request.getWeight(); 230 } 231 if (request.getAssignment() != null) { 232 if (cr) 233 iAssignedCRWeight += request.getWeight(); 234 if (request.getAssignment().getReservation() != null) 235 iReservedSpace += request.getWeight(); 236 if (cr && ((CourseRequest)request).hasReservations()) 237 iTotalReservedSpace += request.getWeight(); 238 if (request.getStudent().isDummy()) { 239 iNrAssignedDummyRequests ++; 240 if (cr) 241 iAssignedDummyCRWeight += request.getWeight(); 242 } 243 } 244 } 245 } 246 247 /** 248 * Remove a student from the model 249 */ 250 public void removeStudent(Student student) { 251 iStudents.remove(student); 252 if (student.isDummy()) 253 iNrDummyStudents--; 254 if (student.isComplete()) 255 iCompleteStudents.remove(student); 256 StudentConflict conflict = null; 257 for (Request request : student.getRequests()) { 258 for (Constraint<Request, Enrollment> c : request.constraints()) { 259 if (c instanceof StudentConflict) { 260 conflict = (StudentConflict) c; 261 break; 262 } 263 } 264 if (conflict != null) 265 conflict.removeVariable(request); 266 removeVariable(request); 267 } 268 if (conflict != null) 269 removeConstraint(conflict); 270 } 271 272 @Override 273 public void removeVariable(Request request) { 274 super.removeVariable(request); 275 if (request instanceof CourseRequest) { 276 CourseRequest cr = (CourseRequest)request; 277 for (Course course: cr.getCourses()) 278 course.getRequests().remove(request); 279 } 280 if (request.getStudent().isDummy()) { 281 iNrDummyRequests--; 282 iTotalDummyWeight -= request.getWeight(); 283 if (request instanceof CourseRequest) 284 iTotalDummyCRWeight -= request.getWeight(); 285 } 286 if (request instanceof CourseRequest) 287 iTotalCRWeight -= request.getWeight(); 288 } 289 290 291 /** 292 * List of offerings 293 */ 294 public List<Offering> getOfferings() { 295 return iOfferings; 296 } 297 298 /** 299 * Add an offering into the model 300 */ 301 public void addOffering(Offering offering) { 302 iOfferings.add(offering); 303 } 304 305 /** 306 * Link sections using {@link LinkedSections} 307 */ 308 public void addLinkedSections(Section... sections) { 309 LinkedSections constraint = new LinkedSections(sections); 310 iLinkedSections.add(constraint); 311 constraint.createConstraints(); 312 } 313 314 /** 315 * Link sections using {@link LinkedSections} 316 */ 317 public void addLinkedSections(Collection<Section> sections) { 318 LinkedSections constraint = new LinkedSections(sections); 319 iLinkedSections.add(constraint); 320 constraint.createConstraints(); 321 } 322 323 /** 324 * List of linked sections 325 */ 326 public List<LinkedSections> getLinkedSections() { 327 return iLinkedSections; 328 } 329 330 /** 331 * Number of students with complete schedule 332 */ 333 public int nrComplete() { 334 return getCompleteStudents().size(); 335 } 336 337 /** 338 * Model info 339 */ 340 @Override 341 public Map<String, String> getInfo() { 342 Map<String, String> info = super.getInfo(); 343 if (!getStudents().isEmpty()) 344 info.put("Students with complete schedule", sDoubleFormat.format(100.0 * nrComplete() / getStudents().size()) 345 + "% (" + nrComplete() + "/" + getStudents().size() + ")"); 346 if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() != 0) 347 info.put("Student distance conflicts", String.valueOf(getDistanceConflict().getTotalNrConflicts())); 348 if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() != 0) 349 info.put("Time overlapping conflicts", String.valueOf(getTimeOverlaps().getTotalNrConflicts())); 350 int nrLastLikeStudents = getNrLastLikeStudents(false); 351 if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) { 352 int nrRealStudents = getStudents().size() - nrLastLikeStudents; 353 int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(false); 354 int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents; 355 if (nrLastLikeStudents > 0) 356 info.put("Projected students with complete schedule", sDecimalFormat.format(100.0 357 * nrLastLikeCompleteStudents / nrLastLikeStudents) 358 + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")"); 359 if (nrRealStudents > 0) 360 info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents 361 / nrRealStudents) 362 + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")"); 363 int nrLastLikeRequests = getNrLastLikeRequests(false); 364 int nrRealRequests = variables().size() - nrLastLikeRequests; 365 int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(false); 366 int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests; 367 if (nrLastLikeRequests > 0) 368 info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests / nrLastLikeRequests) 369 + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")"); 370 if (nrRealRequests > 0) 371 info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests) 372 + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")"); 373 if (iTotalCRWeight > 0.0) { 374 info.put("Assigned course requests", sDecimalFormat.format(100.0 * iAssignedCRWeight / iTotalCRWeight) + "% (" + (int)Math.round(iAssignedCRWeight) + "/" + (int)Math.round(iTotalCRWeight) + ")"); 375 if (iTotalDummyCRWeight != iTotalCRWeight) { 376 if (iTotalDummyCRWeight > 0.0) 377 info.put("Projected assigned course requests", sDecimalFormat.format(100.0 * iAssignedDummyCRWeight / iTotalDummyCRWeight) + "% (" + (int)Math.round(iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalDummyCRWeight) + ")"); 378 info.put("Real assigned course requests", sDecimalFormat.format(100.0 * (iAssignedCRWeight - iAssignedDummyCRWeight) / (iTotalCRWeight - iTotalDummyCRWeight)) + 379 "% (" + (int)Math.round(iAssignedCRWeight - iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalCRWeight - iTotalDummyCRWeight) + ")"); 380 } 381 } 382 if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() > 0) 383 info.put("Student distance conflicts", String.valueOf(getDistanceConflict().getTotalNrConflicts())); 384 if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() > 0) 385 info.put("Time overlapping conflicts", String.valueOf(getTimeOverlaps().getTotalNrConflicts())); 386 } 387 if (iTotalReservedSpace > 0.0) 388 info.put("Reservations", sDoubleFormat.format(100.0 * iReservedSpace / iTotalReservedSpace) + "% (" + Math.round(iReservedSpace) + "/" + Math.round(iTotalReservedSpace) + ")"); 389 390 return info; 391 } 392 393 /** 394 * Overall solution value 395 */ 396 public double getTotalValue(boolean precise) { 397 if (precise) { 398 double total = 0; 399 for (Request r: assignedVariables()) 400 total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment()); 401 if (iDistanceConflict != null) 402 for (DistanceConflict.Conflict c: iDistanceConflict.computeAllConflicts()) 403 total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c); 404 if (iTimeOverlaps != null) 405 for (TimeOverlapsCounter.Conflict c: iTimeOverlaps.computeAllConflicts()) { 406 total -= c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c); 407 total -= c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c); 408 } 409 return -total; 410 } 411 return iTotalValue; 412 } 413 414 /** 415 * Overall solution value 416 */ 417 @Override 418 public double getTotalValue() { 419 return iTotalValue; 420 } 421 422 423 /** 424 * Called after an enrollment was assigned to a request. The list of 425 * complete students and the overall solution value are updated. 426 */ 427 @Override 428 public void afterAssigned(long iteration, Enrollment enrollment) { 429 super.afterAssigned(iteration, enrollment); 430 Student student = enrollment.getStudent(); 431 if (student.isComplete()) 432 iCompleteStudents.add(student); 433 double value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(enrollment); 434 iTotalValue -= value; 435 enrollment.setExtra(value); 436 if (enrollment.isCourseRequest()) 437 iAssignedCRWeight += enrollment.getRequest().getWeight(); 438 if (enrollment.getReservation() != null) 439 iReservedSpace += enrollment.getRequest().getWeight(); 440 if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations()) 441 iTotalReservedSpace += enrollment.getRequest().getWeight(); 442 if (student.isDummy()) { 443 iNrAssignedDummyRequests++; 444 if (enrollment.isCourseRequest()) 445 iAssignedDummyCRWeight += enrollment.getRequest().getWeight(); 446 if (student.isComplete()) 447 iNrCompleteDummyStudents++; 448 } 449 } 450 451 /** 452 * Called before an enrollment was unassigned from a request. The list of 453 * complete students and the overall solution value are updated. 454 */ 455 @Override 456 public void afterUnassigned(long iteration, Enrollment enrollment) { 457 super.afterUnassigned(iteration, enrollment); 458 Student student = enrollment.getStudent(); 459 if (iCompleteStudents.contains(student) && !student.isComplete()) { 460 iCompleteStudents.remove(student); 461 if (student.isDummy()) 462 iNrCompleteDummyStudents--; 463 } 464 Double value = (Double)enrollment.getExtra(); 465 if (value == null) 466 value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(enrollment); 467 iTotalValue += value; 468 enrollment.setExtra(null); 469 if (enrollment.isCourseRequest()) 470 iAssignedCRWeight -= enrollment.getRequest().getWeight(); 471 if (enrollment.getReservation() != null) 472 iReservedSpace -= enrollment.getRequest().getWeight(); 473 if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations()) 474 iTotalReservedSpace -= enrollment.getRequest().getWeight(); 475 if (student.isDummy()) { 476 iNrAssignedDummyRequests--; 477 if (enrollment.isCourseRequest()) 478 iAssignedDummyCRWeight -= enrollment.getRequest().getWeight(); 479 } 480 } 481 482 /** 483 * Configuration 484 */ 485 public DataProperties getProperties() { 486 return iProperties; 487 } 488 489 /** 490 * Empty online student sectioning infos for all sections (see 491 * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}). 492 */ 493 public void clearOnlineSectioningInfos() { 494 for (Offering offering : iOfferings) { 495 for (Config config : offering.getConfigs()) { 496 for (Subpart subpart : config.getSubparts()) { 497 for (Section section : subpart.getSections()) { 498 section.setSpaceExpected(0); 499 section.setSpaceHeld(0); 500 } 501 } 502 } 503 } 504 } 505 506 /** 507 * Compute online student sectioning infos for all sections (see 508 * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}). 509 */ 510 public void computeOnlineSectioningInfos() { 511 clearOnlineSectioningInfos(); 512 for (Student student : getStudents()) { 513 if (!student.isDummy()) 514 continue; 515 for (Request request : student.getRequests()) { 516 if (!(request instanceof CourseRequest)) 517 continue; 518 CourseRequest courseRequest = (CourseRequest) request; 519 Enrollment enrollment = courseRequest.getAssignment(); 520 if (enrollment != null) { 521 for (Section section : enrollment.getSections()) { 522 section.setSpaceHeld(courseRequest.getWeight() + section.getSpaceHeld()); 523 } 524 } 525 List<Enrollment> feasibleEnrollments = new ArrayList<Enrollment>(); 526 int totalLimit = 0; 527 for (Enrollment enrl : courseRequest.values()) { 528 boolean overlaps = false; 529 for (Request otherRequest : student.getRequests()) { 530 if (otherRequest.equals(courseRequest) || !(otherRequest instanceof CourseRequest)) 531 continue; 532 Enrollment otherErollment = otherRequest.getAssignment(); 533 if (otherErollment == null) 534 continue; 535 if (enrl.isOverlapping(otherErollment)) { 536 overlaps = true; 537 break; 538 } 539 } 540 if (!overlaps) { 541 feasibleEnrollments.add(enrl); 542 if (totalLimit >= 0) { 543 int limit = enrl.getLimit(); 544 if (limit < 0) totalLimit = -1; 545 else totalLimit += limit; 546 } 547 } 548 } 549 double increment = courseRequest.getWeight() / (totalLimit > 0 ? totalLimit : feasibleEnrollments.size()); 550 for (Enrollment feasibleEnrollment : feasibleEnrollments) { 551 for (Section section : feasibleEnrollment.getSections()) { 552 if (totalLimit > 0) { 553 section.setSpaceExpected(section.getSpaceExpected() + increment * feasibleEnrollment.getLimit()); 554 } else { 555 section.setSpaceExpected(section.getSpaceExpected() + increment); 556 } 557 } 558 } 559 } 560 } 561 } 562 563 /** 564 * Sum of weights of all requests that are not assigned (see 565 * {@link Request#getWeight()}). 566 */ 567 public double getUnassignedRequestWeight() { 568 double weight = 0.0; 569 for (Request request : unassignedVariables()) { 570 weight += request.getWeight(); 571 } 572 return weight; 573 } 574 575 /** 576 * Sum of weights of all requests (see {@link Request#getWeight()}). 577 */ 578 public double getTotalRequestWeight() { 579 double weight = 0.0; 580 for (Request request : unassignedVariables()) { 581 weight += request.getWeight(); 582 } 583 return weight; 584 } 585 586 /** 587 * Set distance conflict extension 588 */ 589 public void setDistanceConflict(DistanceConflict dc) { 590 iDistanceConflict = dc; 591 } 592 593 /** 594 * Return distance conflict extension 595 */ 596 public DistanceConflict getDistanceConflict() { 597 return iDistanceConflict; 598 } 599 600 /** 601 * Set time overlaps extension 602 */ 603 public void setTimeOverlaps(TimeOverlapsCounter toc) { 604 iTimeOverlaps = toc; 605 } 606 607 /** 608 * Return time overlaps extension 609 */ 610 public TimeOverlapsCounter getTimeOverlaps() { 611 return iTimeOverlaps; 612 } 613 614 /** 615 * Average priority of unassigned requests (see 616 * {@link Request#getPriority()}) 617 */ 618 public double avgUnassignPriority() { 619 double totalPriority = 0.0; 620 for (Request request : unassignedVariables()) { 621 if (request.isAlternative()) 622 continue; 623 totalPriority += request.getPriority(); 624 } 625 return 1.0 + totalPriority / unassignedVariables().size(); 626 } 627 628 /** 629 * Average number of requests per student (see {@link Student#getRequests()} 630 * ) 631 */ 632 public double avgNrRequests() { 633 double totalRequests = 0.0; 634 int totalStudents = 0; 635 for (Student student : getStudents()) { 636 if (student.nrRequests() == 0) 637 continue; 638 totalRequests += student.nrRequests(); 639 totalStudents++; 640 } 641 return totalRequests / totalStudents; 642 } 643 644 /** Number of last like ({@link Student#isDummy()} equals true) students. */ 645 public int getNrLastLikeStudents(boolean precise) { 646 if (!precise) 647 return iNrDummyStudents; 648 int nrLastLikeStudents = 0; 649 for (Student student : getStudents()) { 650 if (student.isDummy()) 651 nrLastLikeStudents++; 652 } 653 return nrLastLikeStudents; 654 } 655 656 /** Number of real ({@link Student#isDummy()} equals false) students. */ 657 public int getNrRealStudents(boolean precise) { 658 if (!precise) 659 return getStudents().size() - iNrDummyStudents; 660 int nrRealStudents = 0; 661 for (Student student : getStudents()) { 662 if (!student.isDummy()) 663 nrRealStudents++; 664 } 665 return nrRealStudents; 666 } 667 668 /** 669 * Number of last like ({@link Student#isDummy()} equals true) students with 670 * a complete schedule ({@link Student#isComplete()} equals true). 671 */ 672 public int getNrCompleteLastLikeStudents(boolean precise) { 673 if (!precise) 674 return iNrCompleteDummyStudents; 675 int nrLastLikeStudents = 0; 676 for (Student student : getCompleteStudents()) { 677 if (student.isDummy()) 678 nrLastLikeStudents++; 679 } 680 return nrLastLikeStudents; 681 } 682 683 /** 684 * Number of real ({@link Student#isDummy()} equals false) students with a 685 * complete schedule ({@link Student#isComplete()} equals true). 686 */ 687 public int getNrCompleteRealStudents(boolean precise) { 688 if (!precise) 689 return getCompleteStudents().size() - iNrCompleteDummyStudents; 690 int nrRealStudents = 0; 691 for (Student student : getCompleteStudents()) { 692 if (!student.isDummy()) 693 nrRealStudents++; 694 } 695 return nrRealStudents; 696 } 697 698 /** 699 * Number of requests from projected ({@link Student#isDummy()} equals true) 700 * students. 701 */ 702 public int getNrLastLikeRequests(boolean precise) { 703 if (!precise) 704 return iNrDummyRequests; 705 int nrLastLikeRequests = 0; 706 for (Request request : variables()) { 707 if (request.getStudent().isDummy()) 708 nrLastLikeRequests++; 709 } 710 return nrLastLikeRequests; 711 } 712 713 /** 714 * Number of requests from real ({@link Student#isDummy()} equals false) 715 * students. 716 */ 717 public int getNrRealRequests(boolean precise) { 718 if (!precise) 719 return variables().size() - iNrDummyRequests; 720 int nrRealRequests = 0; 721 for (Request request : variables()) { 722 if (!request.getStudent().isDummy()) 723 nrRealRequests++; 724 } 725 return nrRealRequests; 726 } 727 728 /** 729 * Number of requests from projected ({@link Student#isDummy()} equals true) 730 * students that are assigned. 731 */ 732 public int getNrAssignedLastLikeRequests(boolean precise) { 733 if (!precise) 734 return iNrAssignedDummyRequests; 735 int nrLastLikeRequests = 0; 736 for (Request request : assignedVariables()) { 737 if (request.getStudent().isDummy()) 738 nrLastLikeRequests++; 739 } 740 return nrLastLikeRequests; 741 } 742 743 /** 744 * Number of requests from real ({@link Student#isDummy()} equals false) 745 * students that are assigned. 746 */ 747 public int getNrAssignedRealRequests(boolean precise) { 748 if (!precise) 749 return assignedVariables().size() - iNrAssignedDummyRequests; 750 int nrRealRequests = 0; 751 for (Request request : assignedVariables()) { 752 if (!request.getStudent().isDummy()) 753 nrRealRequests++; 754 } 755 return nrRealRequests; 756 } 757 758 /** 759 * Model extended info. Some more information (that is more expensive to 760 * compute) is added to an ordinary {@link Model#getInfo()}. 761 */ 762 @Override 763 public Map<String, String> getExtendedInfo() { 764 Map<String, String> info = getInfo(); 765 /* 766 int nrLastLikeStudents = getNrLastLikeStudents(true); 767 if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) { 768 int nrRealStudents = getStudents().size() - nrLastLikeStudents; 769 int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(true); 770 int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents; 771 info.put("Projected students with complete schedule", sDecimalFormat.format(100.0 772 * nrLastLikeCompleteStudents / nrLastLikeStudents) 773 + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")"); 774 info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents 775 / nrRealStudents) 776 + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")"); 777 int nrLastLikeRequests = getNrLastLikeRequests(true); 778 int nrRealRequests = variables().size() - nrLastLikeRequests; 779 int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(true); 780 int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests; 781 info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests 782 / nrLastLikeRequests) 783 + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")"); 784 info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests) 785 + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")"); 786 } 787 */ 788 // info.put("Average unassigned priority", sDecimalFormat.format(avgUnassignPriority())); 789 // info.put("Average number of requests", sDecimalFormat.format(avgNrRequests())); 790 791 /* 792 double total = 0; 793 for (Request r: variables()) 794 if (r.getAssignment() != null) 795 total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment()); 796 */ 797 double dc = 0; 798 if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() != 0) { 799 Set<DistanceConflict.Conflict> conf = getDistanceConflict().getAllConflicts(); 800 for (DistanceConflict.Conflict c: conf) 801 dc += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c); 802 if (!conf.isEmpty()) 803 info.put("Student distance conflicts", conf.size() + " (weighted: " + sDecimalFormat.format(dc) + ")"); 804 } 805 double toc = 0; 806 if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() != 0) { 807 Set<TimeOverlapsCounter.Conflict> conf = getTimeOverlaps().getAllConflicts(); 808 int share = 0; 809 for (TimeOverlapsCounter.Conflict c: conf) { 810 toc += c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c); 811 toc += c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c); 812 share += c.getShare(); 813 } 814 if (toc != 0.0) 815 info.put("Time overlapping conflicts", share + " (average: " + sDecimalFormat.format(5.0 * share / getStudents().size()) + " min, weighted: " + sDoubleFormat.format(toc) + ")"); 816 } 817 /* 818 info.put("Overall solution value", sDecimalFormat.format(total - dc - toc) + (dc == 0.0 && toc == 0.0 ? "" : 819 " (" + (dc != 0.0 ? "distance: " + sDecimalFormat.format(dc): "") + (dc != 0.0 && toc != 0.0 ? ", " : "") + 820 (toc != 0.0 ? "overlap: " + sDecimalFormat.format(toc) : "") + ")") 821 ); 822 */ 823 824 double disbWeight = 0; 825 int disbSections = 0; 826 int disb10Sections = 0; 827 int disb10Limit = getProperties().getPropertyInt("Info.ListDisbalancedSections", 0); 828 Set<String> disb10SectionList = (disb10Limit == 0 ? null : new TreeSet<String>()); 829 for (Offering offering: getOfferings()) { 830 for (Config config: offering.getConfigs()) { 831 double enrl = config.getEnrollmentWeight(null); 832 for (Subpart subpart: config.getSubparts()) { 833 if (subpart.getSections().size() <= 1) continue; 834 if (subpart.getLimit() > 0) { 835 // sections have limits -> desired size is section limit x (total enrollment / total limit) 836 double ratio = enrl / subpart.getLimit(); 837 for (Section section: subpart.getSections()) { 838 double desired = ratio * section.getLimit(); 839 disbWeight += Math.abs(section.getEnrollmentWeight(null) - desired); 840 disbSections ++; 841 if (Math.abs(desired - section.getEnrollmentWeight(null)) >= Math.max(1.0, 0.1 * section.getLimit())) { 842 disb10Sections++; 843 if (disb10SectionList != null) 844 disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName()); 845 } 846 } 847 } else { 848 // unlimited sections -> desired size is total enrollment / number of sections 849 for (Section section: subpart.getSections()) { 850 double desired = enrl / subpart.getSections().size(); 851 disbWeight += Math.abs(section.getEnrollmentWeight(null) - desired); 852 disbSections ++; 853 if (Math.abs(desired - section.getEnrollmentWeight(null)) >= Math.max(1.0, 0.1 * desired)) { 854 disb10Sections++; 855 if (disb10SectionList != null) 856 disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName()); 857 } 858 } 859 } 860 } 861 } 862 } 863 if (disbSections != 0) { 864 info.put("Average disbalance", sDecimalFormat.format(disbWeight / disbSections) + 865 " (" + sDecimalFormat.format(iAssignedCRWeight == 0 ? 0.0 : 100.0 * disbWeight / iAssignedCRWeight) + "%)"); 866 String list = ""; 867 if (disb10SectionList != null) { 868 int i = 0; 869 for (String section: disb10SectionList) { 870 if (i == disb10Limit) { 871 list += "<br>..."; 872 break; 873 } 874 list += "<br>" + section; 875 i++; 876 } 877 } 878 info.put("Sections disbalanced by 10% or more", disb10Sections + " (" + sDecimalFormat.format(disbSections == 0 ? 0.0 : 100.0 * disb10Sections / disbSections) + "%)" + list); 879 } 880 return info; 881 } 882 883 @Override 884 public void restoreBest() { 885 restoreBest(new Comparator<Request>() { 886 @Override 887 public int compare(Request r1, Request r2) { 888 Enrollment e1 = r1.getBestAssignment(); 889 Enrollment e2 = r2.getBestAssignment(); 890 // Reservations first 891 if (e1.getReservation() != null && e2.getReservation() == null) return -1; 892 if (e1.getReservation() == null && e2.getReservation() != null) return 1; 893 // Then assignment iteration (i.e., order in which assignments were made) 894 if (r1.getBestAssignmentIteration() != r2.getBestAssignmentIteration()) 895 return (r1.getBestAssignmentIteration() < r2.getBestAssignmentIteration() ? -1 : 1); 896 // Then student and priority 897 return r1.compareTo(r2); 898 } 899 }); 900 } 901 902 @Override 903 public String toString() { 904 return (getNrRealStudents(false) > 0 ? "RRq:" + getNrAssignedRealRequests(false) + "/" + getNrRealRequests(false) + ", " : "") 905 + (getNrLastLikeStudents(false) > 0 ? "DRq:" + getNrAssignedLastLikeRequests(false) + "/" + getNrLastLikeRequests(false) + ", " : "") 906 + (getNrRealStudents(false) > 0 ? "RS:" + getNrCompleteRealStudents(false) + "/" + getNrRealStudents(false) + ", " : "") 907 + (getNrLastLikeStudents(false) > 0 ? "DS:" + getNrCompleteLastLikeStudents(false) + "/" + getNrLastLikeStudents(false) + ", " : "") 908 + "V:" 909 + sDecimalFormat.format(-getTotalValue()) 910 + (getDistanceConflict() == null ? "" : ", DC:" + getDistanceConflict().getTotalNrConflicts()) 911 + (getTimeOverlaps() == null ? "" : ", TOC:" + getTimeOverlaps().getTotalNrConflicts()) 912 + ", %:" + sDecimalFormat.format(-100.0 * getTotalValue() / (getStudents().size() - iNrDummyStudents + 913 (iProjectedStudentWeight < 0.0 ? iNrDummyStudents * (iTotalDummyWeight / iNrDummyRequests) :iProjectedStudentWeight * iTotalDummyWeight))); 914 915 } 916 917 /** 918 * Quadratic average of two weights. 919 */ 920 public double avg(double w1, double w2) { 921 return Math.sqrt(w1 * w2); 922 } 923 924 public void add(DistanceConflict.Conflict c) { 925 iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c); 926 } 927 928 public void remove(DistanceConflict.Conflict c) { 929 iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c); 930 } 931 932 public void add(TimeOverlapsCounter.Conflict c) { 933 iTotalValue += c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c); 934 iTotalValue += c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c); 935 } 936 937 public void remove(TimeOverlapsCounter.Conflict c) { 938 iTotalValue -= c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c); 939 iTotalValue -= c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c); 940 } 941 }