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 Tomáš Müller<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 return new SctEnrollment(-1, this, lectures); 084 } 085 086 /** 087 * List of lectures of the given course that the student is instructing (if he/she is also an instructor, using {@link Student#getInstructor()}) 088 * @return list of lectures of the given course that the student is instructing 089 */ 090 public List<Lecture> getInstructingLectures() { 091 if (iInstructing == null && getStudent().getInstructor() != null) { 092 iInstructing = new ArrayList<Lecture>(); 093 for (Lecture lecture: getStudent().getInstructor().variables()) 094 if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId())) 095 iInstructing.add(lecture); 096 } 097 return iInstructing; 098 } 099 100 /** 101 * Is student also an instructor of the given course? 102 */ 103 public boolean isInstructing() { 104 return getStudent().getInstructor() != null && !getInstructingLectures().isEmpty(); 105 } 106 107 /** 108 * Conflict weight of a lecture pair 109 */ 110 public double getJenrConflictWeight(Lecture l1, Lecture l2) { 111 Placement p1 = getModel().getAssignment().getValue(l1); 112 Placement p2 = getModel().getAssignment().getValue(l2); 113 if (p1 == null || p2 == null) return 0.0; 114 if (getModel().getStudentConflictCriteria() == null) { 115 if (JenrlConstraint.isInConflict(p1, p2, getModel().getTimetableModel().getDistanceMetric(), getModel().getTimetableModel().getStudentWorkDayLimit())) 116 return getStudent().getJenrlWeight(l1, l2); 117 return 0.0; 118 } 119 double weight = 0.0; 120 for (StudentConflict sc: getModel().getStudentConflictCriteria()) 121 if (sc.isApplicable(getStudent(), p1.variable(), p2.variable()) && sc.inConflict(p1, p2)) 122 weight += sc.getWeight() * getStudent().getJenrlWeight(l1, l2); 123 return weight; 124 } 125 126 /** 127 * All scheduling subpart ids of a configuration. 128 */ 129 private List<Long> getSubpartIds(Configuration configuration) { 130 List<Long> subpartIds = new ArrayList<Long>(); 131 Queue<Lecture> queue = new LinkedList<Lecture>(); 132 for (Map.Entry<Long, Set<Lecture>> e: configuration.getTopLectures().entrySet()) { 133 subpartIds.add(e.getKey()); 134 queue.add(e.getValue().iterator().next()); 135 } 136 Lecture lecture = null; 137 while ((lecture = queue.poll()) != null) { 138 if (lecture.getChildren() != null) 139 for (Map.Entry<Long, List<Lecture>> e: lecture.getChildren().entrySet()) { 140 subpartIds.add(e.getKey()); 141 queue.add(e.getValue().iterator().next()); 142 } 143 } 144 return subpartIds; 145 } 146 147 /** 148 * Compute all possible enrollments 149 */ 150 private void computeEnrollments(Configuration configuration, Map<Long, Set<Lecture>> subparts, List<Long> subpartIds, Set<Lecture> enrollment, double conflictWeight) { 151 if (enrollment.size() == subpartIds.size()) { 152 iEnrollments.add(new SctEnrollment(iEnrollments.size(), this, enrollment, conflictWeight)); 153 iTotalEnrollmentWeight += conflictWeight; 154 } else { 155 Set<Lecture> lectures = subparts.get(subpartIds.get(enrollment.size())); 156 for (Lecture lecture: lectures) { 157 if (lecture.getParent() != null && !enrollment.contains(lecture.getParent())) continue; 158 if (!getStudent().canEnroll(lecture)) continue; 159 double delta = 0.0; 160 for (Lecture other: getStudent().getLectures()) 161 if (!configuration.getOfferingId().equals(other.getConfiguration().getOfferingId())) 162 delta += getJenrConflictWeight(lecture, other); 163 for (Lecture other: enrollment) 164 delta += getJenrConflictWeight(lecture, other); 165 enrollment.add(lecture); 166 computeEnrollments(configuration, subparts, subpartIds, enrollment, conflictWeight + delta); 167 enrollment.remove(lecture); 168 } 169 } 170 } 171 172 /** 173 * Compute all possible enrollments 174 */ 175 private void computeEnrollments() { 176 iEnrollments = new ArrayList<SctEnrollment>(); 177 if (isInstructing()) { 178 double conflictWeight = 0.0; 179 for (Lecture lecture: getInstructingLectures()) { 180 for (Lecture other: getStudent().getLectures()) 181 if (!getModel().getOfferingId().equals(other.getConfiguration().getOfferingId())) 182 conflictWeight += getJenrConflictWeight(lecture, other); 183 } 184 iEnrollments.add(new SctEnrollment(0, this, getInstructingLectures(), conflictWeight)); 185 return; 186 } 187 for (Configuration configuration: getModel().getConfigurations()) { 188 Map<Long, Set<Lecture>> subparts = getModel().getSubparts(configuration); 189 List<Long> subpartIds = getSubpartIds(configuration); 190 computeEnrollments(configuration, subparts, subpartIds, new HashSet<Lecture>(), 0.0); 191 } 192 Collections.sort(iEnrollments); 193 } 194 195 /** 196 * Return all possible enrollments of the given student into the given course 197 */ 198 public List<SctEnrollment> getEnrollments() { 199 if (iEnrollments == null) computeEnrollments(); 200 return iEnrollments; 201 } 202 203 public List<SctEnrollment> getEnrollments(Comparator<SctEnrollment> cmp) { 204 if (iEnrollments == null) computeEnrollments(); 205 if (cmp != null) 206 Collections.sort(iEnrollments, cmp); 207 return iEnrollments; 208 } 209 210 /** 211 * Number of all possible enrollments of the given student into the given course 212 */ 213 public int getNumberOfEnrollments() { 214 if (iEnrollments == null) computeEnrollments(); 215 return iEnrollments.size(); 216 } 217 218 /** 219 * Average conflict weight 220 */ 221 public double getAverageConflictWeight() { 222 if (iEnrollments == null) computeEnrollments(); 223 return iTotalEnrollmentWeight / iEnrollments.size(); 224 } 225 226 /** 227 * Offering weight using {@link Student#getOfferingWeight(Long)} 228 */ 229 public double getOfferingWeight() { 230 if (iOfferingWeight == null) 231 iOfferingWeight = getStudent().getOfferingWeight(getModel().getOfferingId()); 232 return iOfferingWeight; 233 } 234 235 /** 236 * Compare two students using their curriculum information 237 */ 238 public int compare(Student s1, Student s2) { 239 int cmp = (s1.getCurriculum() == null ? "" : s1.getCurriculum()).compareToIgnoreCase(s2.getCurriculum() == null ? "" : s2.getCurriculum()); 240 if (cmp != 0) return cmp; 241 cmp = (s1.getAcademicArea() == null ? "" : s1.getAcademicArea()).compareToIgnoreCase(s2.getAcademicArea() == null ? "" : s2.getAcademicArea()); 242 if (cmp != 0) return cmp; 243 cmp = (s1.getMajor() == null ? "" : s1.getMajor()).compareToIgnoreCase(s2.getMajor() == null ? "" : s2.getMajor()); 244 if (cmp != 0) return cmp; 245 cmp = (s1.getAcademicClassification() == null ? "" : s1.getAcademicClassification()).compareToIgnoreCase(s2.getAcademicClassification() == null ? "" : s2.getAcademicClassification()); 246 if (cmp != 0) return cmp; 247 return s1.getId().compareTo(s2.getId()); 248 } 249 250 /** 251 * Compare two student, using average conflict weight, number of possible enrollments and their curriculum information. 252 * Student that is harder to schedule goes first. 253 */ 254 @Override 255 public int compareTo(SctStudent s) { 256 int cmp = Double.compare(s.getAverageConflictWeight(), getAverageConflictWeight()); 257 if (cmp != 0) return cmp; 258 cmp = Double.compare(getNumberOfEnrollments(), s.getNumberOfEnrollments()); 259 if (cmp != 0) return cmp; 260 return compare(getStudent(), s.getStudent()); 261 } 262 263 @Override 264 public String toString() { 265 return getStudent() + "{enrls:" + getEnrollments().size() + (getEnrollments().isEmpty() ? "" : ", best:" + getEnrollments().get(0).getConflictWeight()) + ", avg:" + getAverageConflictWeight() + "}"; 266 } 267 268}