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