001package org.cpsolver.coursett.sectioning; 002 003import java.util.ArrayList; 004import java.util.Collections; 005import java.util.Comparator; 006import java.util.HashSet; 007import java.util.LinkedList; 008import java.util.List; 009import java.util.Map; 010import java.util.Queue; 011import java.util.Set; 012 013import org.cpsolver.coursett.constraint.JenrlConstraint; 014import org.cpsolver.coursett.criteria.StudentConflict; 015import org.cpsolver.coursett.model.Configuration; 016import org.cpsolver.coursett.model.Lecture; 017import org.cpsolver.coursett.model.Placement; 018import org.cpsolver.coursett.model.Student; 019 020/** 021 * A class wrapping a student, including an ordered set of possible enrollment into a given 022 * course. 023 * 024 * @version CourseTT 1.3 (University Course Timetabling)<br> 025 * Copyright (C) 2017 Tomas Muller<br> 026 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 027 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 028 * <br> 029 * This library is free software; you can redistribute it and/or modify 030 * it under the terms of the GNU Lesser General Public License as 031 * published by the Free Software Foundation; either version 3 of the 032 * License, or (at your option) any later version. <br> 033 * <br> 034 * This library is distributed in the hope that it will be useful, but 035 * WITHOUT ANY WARRANTY; without even the implied warranty of 036 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 037 * Lesser General Public License for more details. <br> 038 * <br> 039 * You should have received a copy of the GNU Lesser General Public 040 * License along with this library; if not see 041 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 042 */ 043public class SctStudent implements Comparable<SctStudent> { 044 private SctModel iModel; 045 private Student iStudent; 046 private List<SctEnrollment> iEnrollments = null; 047 private double iTotalEnrollmentWeight = 0.0; 048 private Double iOfferingWeight = null; 049 private List<Lecture> iInstructing = null; 050 051 /** 052 * Constructor. 053 * @param model student sectioning model 054 * @param student student to represent 055 */ 056 public SctStudent(SctModel model, Student student) { 057 iStudent = student; 058 iModel = model; 059 } 060 061 /** 062 * Student sectioning model 063 */ 064 public SctModel getModel() { return iModel; } 065 066 /** 067 * Student for which the possible enrollments are being computed 068 */ 069 public Student getStudent() { return iStudent; } 070 071 /** 072 * Current enrollment of this student 073 * @param checkInstructor if the student is also instructor and he/she is instructing this class, return classes he/she is instructing instead 074 * @return current enrollment of this student into the given course 075 */ 076 public SctEnrollment getCurrentEnrollment(boolean checkInstructor) { 077 if (checkInstructor && isInstructing()) 078 return new SctEnrollment(-1, this, getInstructingLectures()); 079 List<Lecture> lectures = new ArrayList<Lecture>(); 080 for (Lecture lecture: getStudent().getLectures()) 081 if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId())) 082 lectures.add(lecture); 083 if (!lectures.isEmpty()) 084 return new SctEnrollment(-1, this, lectures); 085 return null; 086 } 087 088 /** 089 * List of lectures of the given course that the student is instructing (if he/she is also an instructor, using {@link Student#getInstructor()}) 090 * @return list of lectures of the given course that the student is instructing 091 */ 092 public List<Lecture> getInstructingLectures() { 093 if (iInstructing == null && getStudent().getInstructor() != null) { 094 iInstructing = new ArrayList<Lecture>(); 095 for (Lecture lecture: getStudent().getInstructor().variables()) 096 if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId())) 097 iInstructing.add(lecture); 098 } 099 return iInstructing; 100 } 101 102 /** 103 * Is student also an instructor of the given course? 104 */ 105 public boolean isInstructing() { 106 return getStudent().getInstructor() != null && !getInstructingLectures().isEmpty(); 107 } 108 109 /** 110 * Conflict weight of a lecture pair 111 */ 112 public double getJenrConflictWeight(Lecture l1, Lecture l2) { 113 Placement p1 = getModel().getAssignment().getValue(l1); 114 Placement p2 = getModel().getAssignment().getValue(l2); 115 if (p1 == null || p2 == null) return 0.0; 116 if (getModel().getStudentConflictCriteria() == null) { 117 if (JenrlConstraint.isInConflict(p1, p2, getModel().getTimetableModel().getDistanceMetric())) 118 return getStudent().getJenrlWeight(l1, l2); 119 return 0.0; 120 } 121 double weight = 0.0; 122 for (StudentConflict sc: getModel().getStudentConflictCriteria()) 123 if (sc.isApplicable(getStudent(), p1.variable(), p2.variable()) && sc.inConflict(p1, p2)) 124 weight += sc.getWeight() * getStudent().getJenrlWeight(l1, l2); 125 return weight; 126 } 127 128 /** 129 * All scheduling subpart ids of a configuration. 130 */ 131 private List<Long> getSubpartIds(Configuration configuration) { 132 List<Long> subpartIds = new ArrayList<Long>(); 133 Queue<Lecture> queue = new LinkedList<Lecture>(); 134 for (Map.Entry<Long, Set<Lecture>> e: configuration.getTopLectures().entrySet()) { 135 subpartIds.add(e.getKey()); 136 queue.add(e.getValue().iterator().next()); 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 subpartIds.add(e.getKey()); 143 queue.add(e.getValue().iterator().next()); 144 } 145 } 146 return subpartIds; 147 } 148 149 /** 150 * Compute all possible enrollments 151 */ 152 private void computeEnrollments(Configuration configuration, Map<Long, Set<Lecture>> subparts, List<Long> subpartIds, Set<Lecture> enrollment, double conflictWeight) { 153 if (enrollment.size() == subpartIds.size()) { 154 iEnrollments.add(new SctEnrollment(iEnrollments.size(), this, enrollment, conflictWeight)); 155 iTotalEnrollmentWeight += conflictWeight; 156 } else { 157 Set<Lecture> lectures = subparts.get(subpartIds.get(enrollment.size())); 158 for (Lecture lecture: lectures) { 159 if (lecture.getParent() != null && !enrollment.contains(lecture.getParent())) continue; 160 if (!getStudent().canEnroll(lecture)) continue; 161 double delta = 0.0; 162 for (Lecture other: getStudent().getLectures()) 163 if (!configuration.getOfferingId().equals(other.getConfiguration().getOfferingId())) 164 delta += getJenrConflictWeight(lecture, other); 165 for (Lecture other: enrollment) 166 delta += getJenrConflictWeight(lecture, other); 167 enrollment.add(lecture); 168 computeEnrollments(configuration, subparts, subpartIds, enrollment, conflictWeight + delta); 169 enrollment.remove(lecture); 170 } 171 } 172 } 173 174 /** 175 * Compute all possible enrollments 176 */ 177 private void computeEnrollments() { 178 iEnrollments = new ArrayList<SctEnrollment>(); 179 if (isInstructing()) { 180 double conflictWeight = 0.0; 181 for (Lecture lecture: getInstructingLectures()) { 182 for (Lecture other: getStudent().getLectures()) 183 if (!getModel().getOfferingId().equals(other.getConfiguration().getOfferingId())) 184 conflictWeight += getJenrConflictWeight(lecture, other); 185 } 186 iEnrollments.add(new SctEnrollment(0, this, getInstructingLectures(), conflictWeight)); 187 return; 188 } 189 for (Configuration configuration: getModel().getConfigurations()) { 190 Map<Long, Set<Lecture>> subparts = getModel().getSubparts(configuration); 191 List<Long> subpartIds = getSubpartIds(configuration); 192 computeEnrollments(configuration, subparts, subpartIds, new HashSet<Lecture>(), 0.0); 193 } 194 Collections.sort(iEnrollments); 195 } 196 197 /** 198 * Return all possible enrollments of the given student into the given course 199 */ 200 public List<SctEnrollment> getEnrollments() { 201 if (iEnrollments == null) computeEnrollments(); 202 return iEnrollments; 203 } 204 205 public List<SctEnrollment> getEnrollments(Comparator<SctEnrollment> cmp) { 206 if (iEnrollments == null) computeEnrollments(); 207 if (cmp != null) 208 Collections.sort(iEnrollments, cmp); 209 return iEnrollments; 210 } 211 212 /** 213 * Number of all possible enrollments of the given student into the given course 214 */ 215 public int getNumberOfEnrollments() { 216 if (iEnrollments == null) computeEnrollments(); 217 return iEnrollments.size(); 218 } 219 220 /** 221 * Average conflict weight 222 */ 223 public double getAverageConflictWeight() { 224 if (iEnrollments == null) computeEnrollments(); 225 return iTotalEnrollmentWeight / iEnrollments.size(); 226 } 227 228 /** 229 * Offering weight using {@link Student#getOfferingWeight(Long)} 230 */ 231 public double getOfferingWeight() { 232 if (iOfferingWeight == null) 233 iOfferingWeight = getStudent().getOfferingWeight(getModel().getOfferingId()); 234 return iOfferingWeight; 235 } 236 237 /** 238 * Compare two students using their curriculum information 239 */ 240 public int compare(Student s1, Student s2) { 241 int cmp = (s1.getCurriculum() == null ? "" : s1.getCurriculum()).compareToIgnoreCase(s2.getCurriculum() == null ? "" : s2.getCurriculum()); 242 if (cmp != 0) return cmp; 243 cmp = (s1.getAcademicArea() == null ? "" : s1.getAcademicArea()).compareToIgnoreCase(s2.getAcademicArea() == null ? "" : s2.getAcademicArea()); 244 if (cmp != 0) return cmp; 245 cmp = (s1.getMajor() == null ? "" : s1.getMajor()).compareToIgnoreCase(s2.getMajor() == null ? "" : s2.getMajor()); 246 if (cmp != 0) return cmp; 247 cmp = (s1.getAcademicClassification() == null ? "" : s1.getAcademicClassification()).compareToIgnoreCase(s2.getAcademicClassification() == null ? "" : s2.getAcademicClassification()); 248 if (cmp != 0) return cmp; 249 return s1.getId().compareTo(s2.getId()); 250 } 251 252 /** 253 * Compare two student, using average conflict weight, number of possible enrollments and their curriculum information. 254 * Student that is harder to schedule goes first. 255 */ 256 @Override 257 public int compareTo(SctStudent s) { 258 int cmp = Double.compare(s.getAverageConflictWeight(), getAverageConflictWeight()); 259 if (cmp != 0) return cmp; 260 cmp = Double.compare(getNumberOfEnrollments(), s.getNumberOfEnrollments()); 261 if (cmp != 0) return cmp; 262 return compare(getStudent(), s.getStudent()); 263 } 264 265 @Override 266 public String toString() { 267 return getStudent() + "{enrls:" + getEnrollments().size() + (getEnrollments().isEmpty() ? "" : ", best:" + getEnrollments().get(0).getConflictWeight()) + ", avg:" + getAverageConflictWeight() + "}"; 268 } 269 270}