001package org.cpsolver.coursett.sectioning; 002 003import java.util.ArrayList; 004import java.util.Arrays; 005import java.util.Collections; 006import java.util.Comparator; 007import java.util.HashMap; 008import java.util.HashSet; 009import java.util.LinkedList; 010import java.util.List; 011import java.util.Map; 012import java.util.Map.Entry; 013import java.util.Queue; 014import java.util.Set; 015import java.util.TreeSet; 016 017import org.cpsolver.coursett.constraint.JenrlConstraint; 018import org.cpsolver.coursett.criteria.StudentConflict; 019import org.cpsolver.coursett.model.Configuration; 020import org.cpsolver.coursett.model.Lecture; 021import org.cpsolver.coursett.model.Placement; 022import org.cpsolver.coursett.model.Student; 023import org.cpsolver.coursett.model.StudentGroup; 024import org.cpsolver.coursett.model.TimetableModel; 025import org.cpsolver.ifs.assignment.Assignment; 026import org.cpsolver.ifs.criteria.Criterion; 027import org.cpsolver.ifs.util.JProf; 028 029/** 030 * A model representing student enrollments of a course. Branch & bound algorithm 031 * is used to propose best possible enrollment of all students into the given course. 032 * 033 * 034 * @version CourseTT 1.3 (University Course Timetabling)<br> 035 * Copyright (C) 2017 Tomáš Müller<br> 036 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 037 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 038 * <br> 039 * This library is free software; you can redistribute it and/or modify 040 * it under the terms of the GNU Lesser General Public License as 041 * published by the Free Software Foundation; either version 3 of the 042 * License, or (at your option) any later version. <br> 043 * <br> 044 * This library is distributed in the hope that it will be useful, but 045 * WITHOUT ANY WARRANTY; without even the implied warranty of 046 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 047 * Lesser General Public License for more details. <br> 048 * <br> 049 * You should have received a copy of the GNU Lesser General Public 050 * License along with this library; if not see 051 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 052 */ 053public class SctModel { 054 public static double sEps = 0.0001; 055 private long iTimeOut = 1000; 056 private boolean iUseCriteria = true; 057 private TimetableModel iModel; 058 private Assignment<Lecture, Placement> iAssignment; 059 private List<StudentConflict> iStudentConflictCriteria = null; 060 private List<Configuration> iConfigurations = null; 061 private List<SctStudent> iStudents = null; 062 private Long iOfferingId = null; 063 private Map<Long, Double> iLimits = new HashMap<Long, Double>(); 064 private Map<Long, Map<Long, Set<Lecture>>> iSubparts = new HashMap<Long, Map<Long, Set<Lecture>>>(); 065 private boolean iTimeOutReached = false; 066 private boolean iGroupFirst = false; 067 068 /** 069 * Constructor 070 * @param model course timetabling model 071 * @param assignment current assignment 072 */ 073 public SctModel(TimetableModel model, Assignment<Lecture, Placement> assignment) { 074 iModel = model; 075 iAssignment = assignment; 076 iTimeOut = model.getProperties().getPropertyLong("SctSectioning.TimeOut", 1000); 077 iUseCriteria = model.getProperties().getPropertyBoolean("SctSectioning.UseCriteria", true); 078 iGroupFirst = model.getProperties().getPropertyBoolean("SctSectioning.GroupFirst", false); 079 } 080 081 /** 082 * List of student conflict criteria 083 */ 084 public List<StudentConflict> getStudentConflictCriteria() { 085 if (!iUseCriteria) return null; 086 if (iStudentConflictCriteria == null && iModel != null) { 087 iStudentConflictCriteria = new ArrayList<StudentConflict>(); 088 for (Criterion<Lecture, Placement> criterion: iModel.getCriteria()) 089 if (criterion instanceof StudentConflict) 090 iStudentConflictCriteria.add((StudentConflict)criterion); 091 } 092 return iStudentConflictCriteria; 093 } 094 095 /** 096 * All configuration of the selected offering 097 */ 098 public List<Configuration> getConfigurations() { return iConfigurations; } 099 100 /** 101 * Select an offering for the model 102 */ 103 public void setConfiguration(Configuration config) { 104 iConfigurations = new ArrayList<Configuration>(); 105 iConfigurations.add(config); 106 iOfferingId = config.getOfferingId(); 107 if (config.getAltConfigurations() != null) 108 for (Configuration alt: config.getAltConfigurations()) 109 if (!alt.equals(config)) iConfigurations.add(alt); 110 iStudents = new ArrayList<SctStudent>(); 111 Set<Long> studentIds = new HashSet<Long>(); 112 for (Configuration c: iConfigurations) 113 for (Lecture l: c.getTopLectures(c.getTopSubpartIds().iterator().next())) { 114 for (Student s: l.students()) { 115 if (studentIds.add(s.getId())) 116 iStudents.add(new SctStudent(this, s)); 117 } 118 } 119 for (Student student: getTimetableModel().getAllStudents()) 120 if (student.hasOffering(getOfferingId())) 121 if (studentIds.add(student.getId())) 122 iStudents.add(new SctStudent(this, student)); 123 Collections.sort(iStudents); 124 } 125 126 /** 127 * List of scheduling subparts and their classes of the given configuration 128 */ 129 public Map<Long, Set<Lecture>> getSubparts(Configuration configuration) { 130 Map<Long, Set<Lecture>> subparts = iSubparts.get(configuration.getConfigId()); 131 if (subparts == null) { 132 subparts = new HashMap<Long, Set<Lecture>>(); 133 Queue<Lecture> queue = new LinkedList<Lecture>(); 134 for (Map.Entry<Long, Set<Lecture>> e: configuration.getTopLectures().entrySet()) { 135 subparts.put(e.getKey(), e.getValue()); 136 queue.addAll(e.getValue()); 137 } 138 Lecture lecture = null; 139 while ((lecture = queue.poll()) != null) { 140 if (lecture.getChildren() != null) 141 for (Map.Entry<Long, List<Lecture>> e: lecture.getChildren().entrySet()) { 142 Set<Lecture> lectures = subparts.get(e.getKey()); 143 if (lectures == null) { 144 lectures = new HashSet<Lecture>(e.getValue()); 145 subparts.put(e.getKey(), lectures); 146 } else { 147 lectures.addAll(e.getValue()); 148 } 149 queue.addAll(e.getValue()); 150 } 151 } 152 iSubparts.put(configuration.getConfigId(), subparts); 153 } 154 return subparts; 155 } 156 157 /** 158 * Selected offering id 159 */ 160 public Long getOfferingId() { return iOfferingId; } 161 162 /** 163 * Course timetabling model 164 */ 165 public TimetableModel getTimetableModel() { return iModel; } 166 167 /** 168 * Current assignment 169 */ 170 public Assignment<Lecture, Placement> getAssignment() { return iAssignment; } 171 172 /** 173 * Enrollment of the given class 174 */ 175 private double getEnrollment(Lecture lecture, Map<Long, Double> limits) { 176 Double enrollment = limits.get(lecture.getClassId()); 177 return enrollment == null ? 0.0 : enrollment.doubleValue(); 178 } 179 180 /** 181 * Increment enrollment of the given class 182 */ 183 private void incEnrollment(Lecture lecture, Map<Long, Double> limits, double weight) { 184 Double enrollment = limits.get(lecture.getClassId()); 185 limits.put(lecture.getClassId(), (enrollment == null ? 0.0 : enrollment.doubleValue()) + weight); 186 } 187 188 /** 189 * Increment enrollment of all classes of the given classes 190 */ 191 private void incEnrollment(SctStudent student, SctEnrollment enrollment, Map<Long, Double> limits, Map<Long, Map<Long, Match>> matches) { 192 for (Lecture lecture: enrollment.getLectures()) 193 incEnrollment(lecture, limits, student.getOfferingWeight()); 194 for (StudentGroup group: student.getStudent().getGroups()) { 195 Map<Long, Match> match = matches.get(group.getId()); 196 if (match == null) { match = new HashMap<Long, Match>(); matches.put(group.getId(), match); } 197 for (Lecture lecture: enrollment.getLectures()) { 198 Match m = match.get(lecture.getSchedulingSubpartId()); 199 if (m == null) { m = new Match(group, lecture); match.put(lecture.getSchedulingSubpartId(), m); } 200 m.inc(lecture); 201 } 202 } 203 } 204 205 /** 206 * Decrement enrollment of all classes of the given classes 207 */ 208 private void decEnrollment(SctStudent student, SctEnrollment enrollment, Map<Long, Double> limits, Map<Long, Map<Long, Match>> matches) { 209 for (Lecture lecture: enrollment.getLectures()) 210 incEnrollment(lecture, limits, -student.getOfferingWeight()); 211 for (StudentGroup group: student.getStudent().getGroups()) { 212 Map<Long, Match> match = matches.get(group.getId()); 213 if (match == null) { match = new HashMap<Long, Match>(); matches.put(group.getId(), match); } 214 for (Lecture lecture: enrollment.getLectures()) { 215 Match m = match.get(lecture.getSchedulingSubpartId()); 216 if (m == null) { m = new Match(group, lecture); match.put(lecture.getSchedulingSubpartId(), m); } 217 m.dec(lecture); 218 } 219 } 220 } 221 222 /** 223 * Class limit 224 */ 225 private double getLimit(Lecture lecture) { 226 Double limit = iLimits.get(lecture.getClassId()); 227 if (limit == null) { 228 limit = Math.max(lecture.classLimit(getAssignment()), lecture.nrWeightedStudents()) - sEps; 229 iLimits.put(lecture.getClassId(), limit); 230 } 231 return limit; 232 } 233 234 /** 235 * Check if all classes of the given enrollment are available (the limit is not breached) 236 */ 237 private boolean isAvailable(SctStudent student, SctEnrollment enrollment, Map<Long, Double> limits) { 238 for (Lecture lecture: enrollment.getLectures()) 239 if (getEnrollment(lecture, limits) > getLimit(lecture)) return false; 240 return true; 241 } 242 243 /** 244 * Group weight of the given enrollments 245 */ 246 private double group(SctEnrollment[] enrollments) { 247 Map<Long, Map<Long, Match>> matches = new HashMap<Long, Map<Long, Match>>(); 248 for (SctEnrollment enrollment: enrollments) { 249 if (enrollment == null) continue; 250 for (StudentGroup group: enrollment.getStudent().getStudent().getGroups()) { 251 Map<Long, Match> match = matches.get(group.getId()); 252 if (match == null) { match = new HashMap<Long, Match>(); matches.put(group.getId(), match); } 253 for (Lecture lecture: enrollment.getLectures()) { 254 Match m = match.get(lecture.getSchedulingSubpartId()); 255 if (m == null) { m = new Match(group, lecture.getConfiguration()); match.put(lecture.getSchedulingSubpartId(), m); } 256 m.inc(lecture); 257 } 258 } 259 } 260 double ret = 0.0; 261 for (Map<Long, Match> match: matches.values()) { 262 for (Match m: match.values()) 263 ret += m.value(); 264 } 265 return ret; 266 } 267 268 /** 269 * Group weight of the given enrollments (up until the given index, computing bound for students above the index) 270 */ 271 protected double group(SctEnrollment[] enrollments, int index, Map<Long, Double> limits, Map<Long, Map<Long, Match>> matches) { 272 // Map<Long, Map<Long, Match>> matches = new HashMap<Long, Map<Long, Match>>(); 273 Map<Long, UnMatched> unmatched = new HashMap<Long, UnMatched>(); 274 for (int i = index; i < iStudents.size(); i++) { 275 SctStudent student = iStudents.get(i); 276 for (StudentGroup group: student.getStudent().getGroups()) { 277 UnMatched m = unmatched.get(group.getId()); 278 if (m == null) { m = new UnMatched(group); unmatched.put(group.getId(), m); } 279 m.incBound(student); 280 } 281 /* 282 if (i < index) { 283 SctEnrollment enrollment = enrollments[i]; 284 if (enrollment == null) continue; 285 for (StudentGroup group: student.getStudent().getGroups()) { 286 Map<Long, Match> match = matches.get(group.getId()); 287 if (match == null) { match = new HashMap<Long, Match>(); matches.put(group.getId(), match); } 288 for (Lecture lecture: enrollment.getLectures()) { 289 Match m = match.get(lecture.getSchedulingSubpartId()); 290 if (m == null) { m = new Match(group, lecture.getConfiguration()); match.put(lecture.getSchedulingSubpartId(), m); } 291 m.inc(lecture); 292 } 293 } 294 } else { 295 for (StudentGroup group: student.getStudent().getGroups()) { 296 Map<Long, Match> match = matches.get(group.getId()); 297 if (match == null) { 298 UnMatched m = unmatched.get(group.getId()); 299 if (m == null) { m = new UnMatched(group); unmatched.put(group.getId(), m); } 300 m.incBound(); 301 continue; 302 } 303 boolean increased = false; 304 for (Configuration configuration: iConfigurations) { 305 for (Long subpartId: getSubparts(configuration).keySet()) { 306 Match m = match.get(subpartId); 307 if (m != null && m.incBound()) increased = true; 308 } 309 if (increased) break; 310 } 311 if (!increased) { 312 UnMatched m = unmatched.get(group.getId()); 313 if (m == null) { m = new UnMatched(group); unmatched.put(group.getId(), m); } 314 m.incBound(); 315 } 316 } 317 }*/ 318 } 319 double ret = 0.0; 320 for (Map.Entry<Long, Map<Long, Match>> match: matches.entrySet()) { 321 for (Match m: match.getValue().values()) 322 ret += m.value(unmatched.remove(match.getKey()), limits); 323 } 324 for (UnMatched m: unmatched.values()) { 325 ret += m.value(); 326 } 327 return ret; 328 } 329 330 /** 331 * Compute best possible enrollment of students into the given offering 332 */ 333 public void computeSolution(SctSolution solution, int index, SctEnrollment[] enrollments, Map<Long, Double> limits, Map<Long, Map<Long, Match>> match, double totalConflicts, long t0) { 334 if (iTimeOutReached) return; 335 if (JProf.currentTimeMillis() - t0 > iTimeOut) { 336 iTimeOutReached = true; return; 337 } 338 if (index < iStudents.size()) { 339 if (!solution.checkBound(index, enrollments, totalConflicts, limits, match)) return; 340 SctStudent student = iStudents.get(index); 341 for (SctEnrollment enrollment: student.getEnrollments(new SctEnrollmentComparator(limits, match, index))) { 342 if (!isAvailable(student, enrollment, limits)) continue; 343 enrollments[index] = enrollment; 344 incEnrollment(student, enrollment, limits, match); 345 computeSolution(solution, index + 1, enrollments, limits, match, totalConflicts + enrollment.getConflictWeight(), t0); 346 decEnrollment(student, enrollment, limits, match); 347 } 348 } else { 349 if (solution.isBetter(enrollments, totalConflicts)) 350 solution.record(enrollments, totalConflicts); 351 } 352 } 353 354 /** 355 * Compute best possible enrollment of students into the given offering 356 */ 357 public SctSolution computeSolution() { 358 SctSolution solution = currentSolution(); 359 iTimeOutReached = false; 360 computeSolution(solution, 0, new SctEnrollment[iStudents.size()], new HashMap<Long, Double>(), new HashMap<Long, Map<Long, Match>>(), 0.0, JProf.currentTimeMillis()); 361 return solution; 362 } 363 364 /** 365 * Was time out reached while {@link SctModel#computeSolution()} was called? 366 */ 367 public boolean isTimeOutReached() { return iTimeOutReached; } 368 369 public SctSolution currentSolution() { 370 SctEnrollment[] enrollments = new SctEnrollment[iStudents.size()]; 371 for (int index = 0; index < iStudents.size(); index++) 372 enrollments[index] = iStudents.get(index).getCurrentEnrollment(true); 373 return new SctSolution(enrollments); 374 } 375 376 /** 377 * Decrement {@link JenrlConstraint} between the given two classes by the given student 378 */ 379 protected void decJenrl(Assignment<Lecture, Placement> assignment, Student student, Lecture l1, Lecture l2) { 380 if (l1.equals(l2)) return; 381 JenrlConstraint jenrl = l1.jenrlConstraint(l2); 382 if (jenrl != null) { 383 jenrl.decJenrl(assignment, student); 384 /* 385 if (jenrl.getNrStudents() == 0) { 386 jenrl.getContext(assignment).unassigned(assignment, null); 387 Object[] vars = jenrl.variables().toArray(); 388 for (int k = 0; k < vars.length; k++) 389 jenrl.removeVariable((Lecture) vars[k]); 390 iModel.removeConstraint(jenrl); 391 } 392 */ 393 } 394 } 395 396 /** 397 * Increment {@link JenrlConstraint} between the given two classes by the given student 398 */ 399 protected void incJenrl(Assignment<Lecture, Placement> assignment, Student student, Lecture l1, Lecture l2) { 400 if (l1.equals(l2)) return; 401 JenrlConstraint jenrl = l1.jenrlConstraint(l2); 402 if (jenrl == null) { 403 jenrl = new JenrlConstraint(); 404 jenrl.addVariable(l1); 405 jenrl.addVariable(l2); 406 iModel.addConstraint(jenrl); 407 } 408 jenrl.incJenrl(assignment, student); 409 } 410 411 /** 412 * Unassign all previous enrollments into the given offering 413 */ 414 public void unassign() { 415 for (SctStudent student: iStudents) { 416 Configuration configuration = null; 417 for (Lecture lecture: student.getCurrentEnrollment(false).getLectures()) { 418 if (configuration == null) configuration = lecture.getConfiguration(); 419 for (Lecture other: student.getStudent().getLectures()) 420 decJenrl(getAssignment(), student.getStudent(), lecture, other); 421 lecture.removeStudent(getAssignment(), student.getStudent()); 422 student.getStudent().removeLecture(lecture); 423 } 424 if (configuration != null) 425 student.getStudent().removeConfiguration(configuration); 426 } 427 } 428 429 /** 430 * Assign given solution 431 */ 432 public void assign(SctSolution solution) { 433 for (int index = 0; index < iStudents.size(); index++) { 434 SctStudent student = iStudents.get(index); 435 Configuration configuration = null; 436 SctEnrollment enrollment = solution.iEnrollments[index]; 437 if (enrollment == null) continue; 438 for (Lecture lecture: enrollment.getLectures()) { 439 if (configuration == null) configuration = lecture.getConfiguration(); 440 for (Lecture other: student.getStudent().getLectures()) 441 incJenrl(getAssignment(), student.getStudent(), lecture, other); 442 lecture.addStudent(getAssignment(), student.getStudent()); 443 student.getStudent().addLecture(lecture); 444 } 445 if (configuration != null) 446 student.getStudent().addConfiguration(configuration); 447 } 448 } 449 450 /** 451 * Enrollment solution. Represent enrollments of all students into the given course. 452 */ 453 class SctSolution { 454 private double iWeight = 0.0; 455 private double iGroup = 0.0; 456 private SctEnrollment[] iEnrollments = null; 457 458 /** 459 * Constructor (for empty solution) 460 */ 461 public SctSolution() {} 462 463 /** 464 * Constructor (for given solution) 465 * @param enrollments given solution 466 */ 467 public SctSolution(SctEnrollment[] enrollments) { 468 iEnrollments = enrollments; 469 iWeight = 0.0; 470 for (SctEnrollment enrollment: enrollments) 471 if (enrollment != null) iWeight += enrollment.getConflictWeight(); 472 iGroup = group(enrollments); 473 } 474 475 /** 476 * Compare two solutions 477 */ 478 public boolean isBetter(SctEnrollment[] solution, double weight) { 479 if (iGroupFirst) { 480 if (iEnrollments == null) return true; 481 double gr = group(solution); 482 return gr > iGroup || (gr == iGroup && weight < iWeight); 483 } else { 484 return iEnrollments == null || weight < iWeight || (weight == iWeight && group(solution) > iGroup); 485 } 486 } 487 488 /** 489 * Compare two solutions 490 */ 491 public boolean isBetter(SctSolution other) { 492 if (iGroupFirst) { 493 if (iEnrollments == null) return true; 494 return other.getGroup() < iGroup || (other.getGroup() == iGroup && iWeight < other.getWeight()); 495 } else { 496 return iEnrollments != null && (iWeight < other.getWeight() || (other.getWeight() == iWeight && other.getGroup() < iGroup)); 497 } 498 } 499 500 /** 501 * Record given solution 502 */ 503 public void record(SctEnrollment[] solution, double weight) { 504 iEnrollments = Arrays.copyOf(solution, solution.length); 505 iWeight = weight; 506 iGroup = group(solution); 507 } 508 509 /** 510 * Check bounds (false means no better solution exists by extending the given solution) 511 */ 512 public boolean checkBound(int index, SctEnrollment[] solution, double weight, Map<Long, Double> limits, Map<Long, Map<Long, Match>> match) { 513 if (iEnrollments == null) return true; 514 if (iGroupFirst) { 515 double gr = group(solution, index, limits, match); 516 if (gr == iGroup) { 517 double guess = weight; 518 for (int i = index; i < iStudents.size(); i++) { 519 SctStudent student = iStudents.get(i); 520 SctEnrollment enrollment = null; 521 for (SctEnrollment e: student.getEnrollments()) { 522 if (isAvailable(student, e, limits)) { enrollment = e; break; } 523 } 524 if (enrollment == null) return false; 525 guess += enrollment.getConflictWeight(); 526 if (guess >= iWeight) break; 527 } 528 return guess < iWeight; 529 } 530 return gr > iGroup; 531 } else { 532 double guess = weight; 533 for (int i = index; i < iStudents.size(); i++) { 534 SctStudent student = iStudents.get(i); 535 SctEnrollment enrollment = null; 536 for (SctEnrollment e: student.getEnrollments()) { 537 if (isAvailable(student, e, limits)) { enrollment = e; break; } 538 } 539 if (enrollment == null) return false; 540 guess += enrollment.getConflictWeight(); 541 if (guess > iWeight) break; 542 } 543 return (guess < iWeight || (guess == iWeight && group(solution, index, limits, match) > iGroup)); 544 } 545 } 546 547 /** 548 * Individual student enrollments 549 */ 550 public SctEnrollment[] getEnrollments() { return iEnrollments; } 551 /** 552 * Overall conflict weight 553 */ 554 public double getWeight() { return iWeight; } 555 /** 556 * Overall group weight 557 */ 558 public double getGroup() { return iGroup; } 559 /** 560 * False if empty 561 */ 562 public boolean isValid() { return iEnrollments != null; } 563 } 564 565 /** 566 * Matching students within a scheduling subpart (for student group weight computation) 567 */ 568 private class Match { 569 private int iTotal = 0; 570 private Map<Lecture, Integer> iMatch = new HashMap<Lecture, Integer>(); 571 private double iFraction = 1.0; 572 573 /** 574 * Constructor 575 * @param group student group 576 */ 577 Match(StudentGroup group, Lecture lecture) { 578 this(group, lecture.getConfiguration()); 579 for (Lecture l: lecture.sameSubpartLectures()) 580 iMatch.put(l, 0); 581 } 582 583 Match(StudentGroup group, Configuration config) { 584 iTotal = group.countStudents(getOfferingId()); 585 iFraction = 1.0 / getSubparts(config).size(); 586 } 587 588 /** 589 * Increment given lecture 590 */ 591 void inc(Lecture lecture) { 592 Integer val = iMatch.get(lecture); 593 iMatch.put(lecture, 1 + (val == null ? 0 : val.intValue())); 594 } 595 596 /** 597 * Decrement given lecture 598 */ 599 void dec(Lecture lecture) { 600 Integer val = iMatch.get(lecture); 601 iMatch.put(lecture, (val == null ? 0 : val.intValue()) - 1); 602 } 603 604 /** 605 * Returns counter for the given lecture 606 */ 607 int get(Lecture lecture) { 608 Integer val = iMatch.get(lecture); 609 return val == null ? 0 : val.intValue(); 610 } 611 612 /** 613 * Value (an overall probability of two students being in the same lecture) 614 */ 615 double value(UnMatched u, final Map<Long, Double> limits) { 616 if (iTotal <= 1) return iFraction; 617 if (u == null || u.getNotMatched() == 0) return value(); 618 double value = 0.0; 619 int unmatched = u.getNotMatched(); 620 double remains = u.getEnrollmentWeight(); 621 double avgWeight = remains / unmatched; 622 TreeSet<Map.Entry<Lecture, Integer>> entries = new TreeSet<Map.Entry<Lecture, Integer>>(new Comparator<Map.Entry<Lecture, Integer>>() { 623 @Override 624 public int compare(Entry<Lecture, Integer> e1, Entry<Lecture, Integer> e2) { 625 if (e1.getValue() > e2.getValue()) return -1; 626 if (e1.getValue() < e2.getValue()) return 1; 627 double r1 = getLimit(e1.getKey()) - getEnrollment(e1.getKey(), limits); 628 double r2 = getLimit(e2.getKey()) - getEnrollment(e2.getKey(), limits); 629 int cmp = Double.compare(r2, r1); 630 if (cmp != 0) return cmp; 631 return e1.getKey().compareTo(e2.getKey()); 632 } 633 }); 634 entries.addAll(iMatch.entrySet()); 635 for (Map.Entry<Lecture, Integer> entry: entries) { 636 Integer m = entry.getValue(); 637 if (unmatched > 0) { 638 double enroll = Math.min(remains, getLimit(entry.getKey()) - getEnrollment(entry.getKey(), limits)); 639 int inc = (int)Math.round(enroll / avgWeight); 640 if (inc > 0) { 641 m += inc; 642 unmatched -= inc; 643 remains -= enroll; 644 } 645 } 646 if (m > 1) 647 value += (m * (m - 1.0)) / (iTotal * (iTotal - 1.0)); 648 } 649 if (unmatched > 1) 650 value += (unmatched * (unmatched - 1.0)) / (iTotal * (iTotal - 1.0)); 651 return value * iFraction; 652 } 653 654 double value() { 655 if (iTotal <= 1) return iFraction; 656 double value = 0.0; 657 for (Integer m: iMatch.values()) 658 if (m > 1) { 659 value += (m * (m - 1.0)) / (iTotal * (iTotal - 1.0)); 660 } 661 return value * iFraction; 662 } 663 664 @Override 665 public String toString() { 666 return iTotal + "/" + iMatch + "[" + value() + "]"; 667 } 668 } 669 670 /** 671 * Matching students within a scheduling subpart (for student group weight computation) 672 */ 673 private class UnMatched { 674 private int iTotal = 0; 675 private int iNotMatched = 0; 676 private double iEnrollmentWeight = 0.0; 677 678 /** 679 * Constructor 680 * @param group student group 681 */ 682 UnMatched(StudentGroup group) { 683 iTotal = group.countStudents(getOfferingId()); 684 } 685 686 /** 687 * Increment bound (a student gets into the best possible class) 688 */ 689 void incBound(SctStudent student) { 690 iNotMatched ++; 691 iEnrollmentWeight += student.getStudent().getOfferingWeight(getOfferingId()); 692 } 693 694 /** 695 * Value (an overall probability of two students being in the same lecture) 696 */ 697 double value() { 698 if (iTotal <= 1) return 1.0; 699 if (iNotMatched > 1) 700 return (iNotMatched * (iNotMatched - 1.0)) / (iTotal * (iTotal - 1.0)); 701 return 0.0; 702 } 703 704 int getNotMatched() { return iNotMatched; } 705 706 public double getEnrollmentWeight() { return iEnrollmentWeight; } 707 708 @Override 709 public String toString() { 710 return iTotal + "/" + iNotMatched; 711 } 712 } 713 714 private class SctEnrollmentComparator implements Comparator<SctEnrollment> { 715 private Map<Long, Double> limits; 716 private Map<Long, Map<Long, Match>> matches; 717 private int index; 718 719 SctEnrollmentComparator(Map<Long, Double> limits, Map<Long, Map<Long, Match>> match, int index) { 720 this.limits = limits; this.matches = match; this.index = index; 721 } 722 723 public int compareByGroup(SctEnrollment e1, SctEnrollment e2) { 724 double m1 = 0, m2 = 0; 725 for (StudentGroup g: e1.getStudent().getStudent().getGroups()) { 726 int remaining = 0; 727 int total = g.countStudents(getOfferingId()); 728 double remainingWeight = 0.0; 729 for (int i = index; i < iStudents.size(); i++) { 730 SctStudent student = iStudents.get(i); 731 if (student.getStudent().hasGroup(g)) { 732 remaining++; 733 remainingWeight += student.getStudent().getOfferingWeight(getOfferingId()); 734 } 735 } 736 double avgWeight = remainingWeight / remaining; 737 Map<Long, Match> match = matches.get(g.getId()); 738 for (Lecture lecture: e1.getLectures()) { 739 Match m = (match == null ? null : match.get(lecture.getSchedulingSubpartId())); 740 double fraction = 1.0 / getSubparts(lecture.getConfiguration()).size(); 741 int a = (m == null ? 0 : m.get(lecture)); 742 double enroll = Math.min(remainingWeight, getLimit(lecture) - getEnrollment(lecture, limits)); 743 a += (int)Math.round(enroll / avgWeight); 744 m1 += fraction * (a * (a - 1)) / ((total * (total - 1))); 745 } 746 for (Lecture lecture: e2.getLectures()) { 747 Match m = (match == null ? null : match.get(lecture.getSchedulingSubpartId())); 748 double fraction = 1.0 / getSubparts(lecture.getConfiguration()).size(); 749 int a = (m == null ? 0 : m.get(lecture)); 750 double enroll = Math.min(remainingWeight, getLimit(lecture) - getEnrollment(lecture, limits)); 751 a += (int)Math.round(enroll / avgWeight); 752 m2 += fraction * (a * (a - 1)) / ((total * (total - 1))); 753 } 754 } 755 if (m1 != m2) return m1 > m2 ? -1 : 1; 756 return 0; 757 } 758 759 @Override 760 public int compare(SctEnrollment e1, SctEnrollment e2) { 761 if (iGroupFirst) { 762 int cmp = compareByGroup(e1, e2); 763 if (cmp != 0) return cmp; 764 cmp = Double.compare(e1.getConflictWeight(), e2.getConflictWeight()); 765 if (cmp != 0) return cmp; 766 } else { 767 int cmp = Double.compare(e1.getConflictWeight(), e2.getConflictWeight()); 768 if (cmp != 0) return cmp; 769 cmp = compareByGroup(e1, e2); 770 if (cmp != 0) return cmp; 771 } 772 return e1.getId().compareTo(e2.getId()); 773 } 774 775 } 776}