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}