001package org.cpsolver.coursett.model; 002 003import java.util.Collection; 004import java.util.List; 005import java.util.Locale; 006import java.util.Map; 007 008import org.cpsolver.coursett.model.InitialSectioning.Group; 009import org.cpsolver.coursett.sectioning.StudentSwapSectioning; 010import org.cpsolver.ifs.assignment.Assignment; 011import org.cpsolver.ifs.model.InfoProvider; 012import org.cpsolver.ifs.solution.Solution; 013import org.cpsolver.ifs.termination.TerminationCondition; 014import org.cpsolver.ifs.util.Progress; 015 016 017/** 018 * Default implementation of the student sectioning functions needed within the course timetabling solver 019 * consisting of {@link InitialSectioning} and {@link FinalSectioning}. 020 * <br> 021 * <br> 022 * Many course offerings consist of multiple classes, with students enrolled in 023 * the course divided among them. These classes are often linked by a set of 024 * constraints, namely: 025 * <ul> 026 * <li>Each class has a limit stating the maximum number of students who can be 027 * enrolled in it. 028 * <li>A student must be enrolled in exactly one class for each subpart of a 029 * course. 030 * <li>If two subparts of a course have a parent-child relationship, a student 031 * enrolled in the parent class must also be enrolled in one of the child 032 * classes. 033 * </ul> 034 * Moreover, some of the classes of an offering may be required or prohibited 035 * for certain students, based on reservations that can be set on an offering, a 036 * configuration, or a class. <br> 037 * While the data are loaded into the solver, an initial sectioning of students into 038 * classes is processed (see {@link InitialSectioning}). However, it 039 * is still possible to improve on the number of student conflicts in the 040 * solution. This can be accomplished by moving students between alternative 041 * classes of the same course during or after the search (see 042 * {@link FinalSectioning}). 043 * 044 * @version CourseTT 1.3 (University Course Timetabling)<br> 045 * Copyright (C) 2014 Tomáš Müller<br> 046 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 047 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 048 * <br> 049 * This library is free software; you can redistribute it and/or modify 050 * it under the terms of the GNU Lesser General Public License as 051 * published by the Free Software Foundation; either version 3 of the 052 * License, or (at your option) any later version. <br> 053 * <br> 054 * This library is distributed in the hope that it will be useful, but 055 * WITHOUT ANY WARRANTY; without even the implied warranty of 056 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 057 * Lesser General Public License for more details. <br> 058 * <br> 059 * You should have received a copy of the GNU Lesser General Public 060 * License along with this library; if not see 061 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 062 */ 063public class DefaultStudentSectioning implements StudentSectioning, InfoProvider<Lecture, Placement> { 064 protected TimetableModel iModel = null; 065 private Progress iProgress = null; 066 protected FinalSectioning iFinalSectioning = null; 067 protected static java.text.DecimalFormat sDF2 = new java.text.DecimalFormat("0.00", new java.text.DecimalFormatSymbols(Locale.US)); 068 069 /** 070 * Constructor 071 * @param model problem model 072 */ 073 public DefaultStudentSectioning(TimetableModel model) { 074 iModel = model; 075 iFinalSectioning = new FinalSectioning(model); 076 } 077 078 public Progress getProgress() { 079 if (iProgress == null) iProgress = Progress.getInstance(iModel); 080 return iProgress; 081 } 082 083 /** 084 * Enroll students into the given offering during the initial data load using {@link InitialSectioning}. 085 * @param offeringId instructional offering id 086 * @param courseName course name 087 * @param students list of students to be sectioned 088 * @param configurations list of configurations the students are to be sectioned into 089 */ 090 @Override 091 public void initialSectioning(Assignment<Lecture, Placement> assignment, Long offeringId, String courseName, Collection<Student> students, Collection<Configuration> configurations) { 092 if (students == null || students.isEmpty()) 093 return; 094 if (configurations == null || configurations.isEmpty()) 095 return; 096 if (configurations.size() == 1) { 097 Configuration cfg = configurations.iterator().next(); 098 for (Student st : students) { 099 st.addConfiguration(cfg); 100 } 101 for (Long subpartId: cfg.getTopSubpartIds()) { 102 initialSectioningLectures(assignment, offeringId, courseName, students, cfg.getTopLectures(subpartId)); 103 } 104 } else { 105 getProgress().trace("sectioning " + students.size() + " students of course " + courseName + " into " + configurations.size() + " configurations"); 106 Group[] studentsPerSection = studentsToConfigurations(offeringId, students, configurations); 107 for (int i = 0; i < configurations.size(); i++) { 108 Group group = studentsPerSection[i]; 109 getProgress().trace((i + 1) + ". configuration got " + group.getStudents().size() + " students (weighted=" + group.size() + ", cfgLimit=" + group.getConfiguration().getLimit() + ")"); 110 for (Student st : group.getStudents()) { 111 st.addConfiguration(group.getConfiguration()); 112 } 113 for (Long subpartId: group.getConfiguration().getTopSubpartIds()) { 114 initialSectioningLectures(assignment, offeringId, courseName, group.getStudents(), group.getConfiguration().getTopLectures(subpartId)); 115 } 116 } 117 } 118 } 119 120 /** 121 * Class label 122 * @param lecture a class 123 * @return class label including a link to be printed in the log 124 */ 125 protected String getClassLabel(Lecture lecture) { 126 return "<A href='classDetail.do?cid=" + lecture.getClassId() + "'>" + lecture.getName() + "</A>"; 127 } 128 129 /** 130 * Enroll students into the given classes during the initial data load using {@link InitialSectioning}. 131 * @param assignment current assignment 132 * @param offeringId instructional offering id 133 * @param courseName course name 134 * @param students list of students to be sectioned 135 * @param lectures list of lectures the students are to be sectioned into 136 */ 137 protected void initialSectioningLectures(Assignment<Lecture, Placement> assignment, Long offeringId, String courseName, Collection<Student> students, Collection<Lecture> lectures) { 138 if (lectures == null || lectures.isEmpty()) 139 return; 140 if (students == null || students.isEmpty()) 141 return; 142 for (Lecture lecture : lectures) { 143 if (lecture.classLimit(assignment) == 0 && !lecture.isCommitted()) 144 getProgress().warn("Class " + getClassLabel(lecture) + " has zero class limit."); 145 } 146 147 getProgress().trace("sectioning " + students.size() + " students of course " + courseName + " into " + lectures.size() + " sections"); 148 if (lectures.size() == 1) { 149 Lecture lect = lectures.iterator().next(); 150 for (Student st : students) { 151 if (!st.canEnroll(lect)) { 152 getProgress().info("Unable to enroll student " + st.getId() + " in class " + getClassLabel(lect)); 153 } 154 lect.addStudent(assignment, st); 155 st.addLecture(lect); 156 } 157 if (lect.hasAnyChildren()) { 158 for (Long subpartId: lect.getChildrenSubpartIds()) { 159 List<Lecture> children = lect.getChildren(subpartId); 160 initialSectioningLectures(assignment, offeringId, lect.getName(), students, children); 161 } 162 } 163 } else { 164 Group[] studentsPerSection = studentsToLectures(offeringId, students, lectures); 165 for (int i = 0; i < studentsPerSection.length; i++) { 166 Group group = studentsPerSection[i]; 167 Lecture lect = group.getLecture(); 168 if (group.getStudents().isEmpty()) { 169 getProgress().trace("Lecture " + getClassLabel(lect) + " got no students (cl=" + lect.classLimit(assignment) + ")"); 170 continue; 171 } 172 getProgress().trace("Lecture " + getClassLabel(lect) + " got " + group.getStudents().size() + " students (weighted=" + group.size() + ", classLimit=" + lect.classLimit(assignment) + ")"); 173 List<Student> studentsThisSection = group.getStudents(); 174 for (Student st : studentsThisSection) { 175 if (!st.canEnroll(lect)) { 176 getProgress().info("Unable to enroll student " + st.getId() + " in class " + getClassLabel(lect)); 177 } 178 lect.addStudent(assignment, st); 179 st.addLecture(lect); 180 } 181 if (lect.hasAnyChildren()) { 182 for (Long subpartId: lect.getChildrenSubpartIds()) { 183 List<Lecture> children = lect.getChildren(subpartId); 184 initialSectioningLectures(assignment, offeringId, lect.getName(), studentsThisSection, children); 185 } 186 } 187 } 188 } 189 } 190 191 /** 192 * Section students into configurations. This method calls the actual initial sectioning {@link InitialSectioning#getGroups()}. 193 * @param offeringId instructional offering id 194 * @param students list of students to be sectioned 195 * @param configurations list of configurations the students are to be sectioned into 196 * @return list of {@link Group} 197 */ 198 protected Group[] studentsToConfigurations(Long offeringId, Collection<Student> students, Collection<Configuration> configurations) { 199 InitialSectioning sect = new InitialSectioning(getProgress(), offeringId, configurations, students); 200 return sect.getGroups(); 201 } 202 203 /** 204 * Section students into lectures. This method calls the actual initial sectioning {@link InitialSectioning#getGroups()}. 205 * @param offeringId instructional offering id 206 * @param students list of students to be sectioned 207 * @param lectures list of lectures the students are to be sectioned into 208 * @return list of {@link Group} 209 */ 210 protected Group[] studentsToLectures(Long offeringId, Collection<Student> students, Collection<Lecture> lectures) { 211 InitialSectioning sect = new InitialSectioning(getProgress(), offeringId, lectures, students); 212 return sect.getGroups(); 213 } 214 215 /** 216 * Return true if final student sectioning is implemented. 217 */ 218 @Override 219 public boolean hasFinalSectioning() { 220 return true; 221 } 222 223 /** 224 * Run student final sectioning (switching students between sections of the same 225 * class in order to minimize overall number of student conflicts). 226 */ 227 @Override 228 public void switchStudents(Solution<Lecture, Placement> solution, TerminationCondition<Lecture, Placement> termination) { 229 iFinalSectioning.execute(solution, termination); 230 } 231 232 /** 233 * Perform sectioning on the given lecture 234 * @param lecture given lecture 235 * @param recursive recursively resection lectures affected by a student swap 236 * @param configAsWell resection students between configurations as well 237 **/ 238 @Override 239 public void resection(Assignment<Lecture, Placement> assignment, Lecture lecture, boolean recursive, boolean configAsWell) { 240 iFinalSectioning.resection(assignment, lecture, recursive, configAsWell); 241 } 242 243 @Override 244 public void getInfo(Assignment<Lecture, Placement> assignment, Map<String, String> info) { 245 if (!iModel.getStudentGroups().isEmpty()) 246 info.put("Student groups", sDF2.format(100.0 * StudentSwapSectioning.group(iModel) / iModel.getStudentGroups().size()) + "%"); 247 } 248 249 @Override 250 public void getInfo(Assignment<Lecture, Placement> assignment, Map<String, String> info, Collection<Lecture> variables) { 251 if (!iModel.getStudentGroups().isEmpty()) 252 info.put("Student groups", sDF2.format(StudentSwapSectioning.gp(iModel, variables)) + "%"); 253 } 254}