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