001package org.cpsolver.coursett.model;
002
003import java.util.ArrayList;
004import java.util.Collection;
005import java.util.HashSet;
006import java.util.HashMap;
007import java.util.Iterator;
008import java.util.List;
009
010import org.cpsolver.ifs.assignment.Assignment;
011import org.cpsolver.ifs.util.Progress;
012
013
014/**
015 * Student initial sectioning (before a solver is started). <br>
016 * <br>
017 * Many course offerings consist of multiple classes, with students enrolled in
018 * the course divided among them. These classes are often linked by a set of
019 * constraints, namely:
020 * <ul>
021 * <li>Each class has a limit stating the maximum number of students who can be
022 * enrolled in it.
023 * <li>A student must be enrolled in exactly one class for each subpart of a
024 * course.
025 * <li>If two subparts of a course have a parent-child relationship, a student
026 * enrolled in the parent class must also be enrolled in one of the child
027 * classes.
028 * </ul>
029 * Moreover, some of the classes of an offering may be required or prohibited
030 * for certain students, based on reservations that can be set on an offering, a
031 * configuration, or a class. <br>
032 * Before implementing the solver, an initial sectioning of students into
033 * classes is processed. This sectioning is based on Carter's homogeneous
034 * sectioning and is intended to minimize future student conflicts. However, it
035 * is still possible to improve on the number of student conflicts in the
036 * solution. This can be accomplished by moving students between alternative
037 * classes of the same course during or after the search (see
038 * {@link FinalSectioning}).
039 * 
040 * @author  Tomáš Müller
041 * @version CourseTT 1.3 (University Course Timetabling)<br>
042 *          Copyright (C) 2006 - 2014 Tomáš Müller<br>
043 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
044 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
045 * <br>
046 *          This library is free software; you can redistribute it and/or modify
047 *          it under the terms of the GNU Lesser General Public License as
048 *          published by the Free Software Foundation; either version 3 of the
049 *          License, or (at your option) any later version. <br>
050 * <br>
051 *          This library is distributed in the hope that it will be useful, but
052 *          WITHOUT ANY WARRANTY; without even the implied warranty of
053 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
054 *          Lesser General Public License for more details. <br>
055 * <br>
056 *          You should have received a copy of the GNU Lesser General Public
057 *          License along with this library; if not see
058 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
059 */
060public class InitialSectioning {
061    protected Collection<Student> iStudents = null;
062    protected Group[] iGroups = null;
063    protected Long iOfferingId = null;
064    protected Progress iProgress = null;
065    protected static double sNominalWeight = 0.00001;
066
067    public InitialSectioning(Progress progress, Long offeringId, Collection<?> lectureOrConfigurations, Collection<Student> students) {
068        iOfferingId = offeringId;
069        iStudents = new HashSet<Student>(students);
070        iProgress = progress;
071
072        iGroups = new Group[lectureOrConfigurations.size()];
073
074        int idx = 0;
075        double total = 0;
076        for (Iterator<?> i = lectureOrConfigurations.iterator(); i.hasNext(); idx++) {
077            Object lectureOrConfiguration = i.next();
078            if (lectureOrConfiguration instanceof Lecture) {
079                Lecture lecture = (Lecture) lectureOrConfiguration;
080                iGroups[idx] = new Group(lecture);
081            } else {
082                Configuration configuration = (Configuration) lectureOrConfiguration;
083                iGroups[idx] = new Group(configuration);
084            }
085            total += iGroups[idx].getMaxSize();
086        }
087
088        tweakSizes(total);
089
090        progress.trace("Initial sectioning:");
091        progress.trace("  going to section " + iStudents.size() + " into " + total + " seats");
092        for (idx = 0; idx < iGroups.length; idx++)
093            progress.trace("    " + (idx + 1) + ". group can accomodate <" + iGroups[idx].getMinSize() + ","
094                    + iGroups[idx].getMaxSize() + "> students");
095    }
096    
097    protected Progress getProgress() {
098        return iProgress;
099    }
100    
101    protected void tweakSizes(double total) {
102        if (total == 0) {
103            for (int idx = 0; idx < iGroups.length; idx++) {
104                iGroups[idx].setMaxSize(1);
105                total++;
106            }
107        }
108
109        double studentsWeight = 0;
110        for (Student s : iStudents) {
111            studentsWeight += s.getOfferingWeight(iOfferingId);
112        }
113
114        double factor = studentsWeight / total;
115        for (int idx = 0; idx < iGroups.length; idx++) {
116            iGroups[idx].setMaxSize(factor * iGroups[idx].getMaxSize());
117            iGroups[idx].setMinSize(Math.min(iGroups[idx].getMinSize(), 0.9 * iGroups[idx].getMaxSize()));
118        }
119    }
120
121    public void addStudent(Student student) {
122        iStudents.add(student);
123    }
124
125    private boolean moveAwayOneStudent(Group group) {
126        Group newGroup = null;
127        Student movingStudent = null;
128        double curDist = 0, newDist = 0;
129
130        for (Student student : group.getStudents()) {
131            if (group.isEnrolled(student))
132                continue;
133            double cd = group.getDistance(student);
134            for (int x = 0; x < iGroups.length; x++) {
135                if (group.equals(iGroups[x]))
136                    continue;
137                if (iGroups[x].size(student) > iGroups[x].getMaxSize())
138                    continue;
139                if (!iGroups[x].canEnroll(student))
140                    continue;
141                double nd = iGroups[x].getDistance(student);
142                if (newGroup == null || newDist - curDist > nd - cd) {
143                    newGroup = iGroups[x];
144                    movingStudent = student;
145                    curDist = cd;
146                    newDist = nd;
147                    if (newDist - curDist < 0.01)
148                        break;
149                }
150            }
151        }
152
153        if (newGroup != null) {
154            group.removeStudent(movingStudent);
155            newGroup.addStudent(movingStudent);
156            return true;
157        }
158
159        return false;
160    }
161
162    public boolean moveIntoOneStudent(Group group) {
163        Group oldGroup = null;
164        Student movingStudent = null;
165        double curDist = 0, newDist = 0;
166
167        for (int x = 0; x < iGroups.length; x++) {
168            if (group.equals(iGroups[x]))
169                continue;
170            if (iGroups[x].size() <= iGroups[x].getMinSize())
171                continue;
172            for (Student student : iGroups[x].getStudents()) {
173                if (!group.canEnroll(student))
174                    continue;
175                if (iGroups[x].isEnrolled(student))
176                    continue;
177                double cd = iGroups[x].getDistance(student);
178                double nd = group.getDistance(student);
179                if (oldGroup == null || newDist - curDist > nd - cd) {
180                    oldGroup = iGroups[x];
181                    movingStudent = student;
182                    curDist = cd;
183                    newDist = nd;
184                    if (newDist - curDist < 0.01)
185                        break;
186                }
187            }
188        }
189
190        if (oldGroup != null) {
191            oldGroup.removeStudent(movingStudent);
192            group.addStudent(movingStudent);
193            return true;
194        }
195
196        return false;
197    }
198
199    public Group[] getGroups() {
200        for (Iterator<Student> i = iStudents.iterator(); i.hasNext();) {
201            Student student = i.next();
202            Group group = null;
203            for (int idx = 0; idx < iGroups.length; idx++) {
204                if (iGroups[idx].isEnrolled(student)) {
205                    group = iGroups[idx];
206                    break;
207                }
208            }
209            if (group != null) {
210                group.addStudent(student);
211                i.remove();
212            }
213        }
214
215        for (Student student : iStudents) {
216            double studentWeight = student.getOfferingWeight(iOfferingId);
217
218            Group group = null;
219            double dist = 0.0; double size = 0.0;
220            for (int idx = 0; idx < iGroups.length; idx++) {
221                Group g = iGroups[idx];
222                if (!g.canEnroll(student))
223                    continue;
224                if (g.size(student) > g.getMaxSize())
225                    continue;
226                double d = g.getDistance(student);
227                if (group == null || d < dist || (d == dist || size < g.size())) {
228                    group = g;
229                    dist = d;
230                    size = g.size();
231                }
232            }
233
234            if (group != null) {
235                group.addStudent(student);
236                continue;
237            }
238
239            // disobey max size, but prefer sections with smallest excess
240            int excess = 0;
241            for (int idx = 0; idx < iGroups.length; idx++) {
242                Group g = iGroups[idx];
243                if (!g.canEnroll(student))
244                    continue;
245                double d = g.getDistance(student);
246                int ex = (int)Math.round(g.size() + studentWeight - g.getMaxSize());
247                if (group == null || ex < excess || (ex == excess && d < dist)) {
248                    group = g;
249                    dist = d;
250                    excess = ex;
251                }
252            }
253
254            if (group != null) {
255                group.addStudent(student);
256                continue;
257            }
258
259            // disobey max size & can enroll
260            for (int idx = 0; idx < iGroups.length; idx++) {
261                Group g = iGroups[idx];
262                double d = g.getDistance(student);
263                if (group == null || d < dist) {
264                    group = g;
265                    dist = d;
266                }
267            }
268
269            iProgress.debug("Unable to find a valid section for student "
270                    + student.getId()
271                    + ", enrolling to "
272                    + (group.getLecture() != null ? group.getLecture().getName() : group.getConfiguration()
273                            .getConfigId().toString()));
274
275            group.addStudent(student);
276        }
277
278        for (int idx = 0; idx < iGroups.length; idx++) {
279            Group group = iGroups[idx];
280
281            while (group.size(null) > group.getMaxSize()) {
282                if (!moveAwayOneStudent(group))
283                    break;
284            }
285
286            while (group.size(null) > group.getMaxSize()) {
287
288                Group newGroup = null;
289                Student movingStudent = null;
290
291                for (Student student : group.getStudents()) {
292                    if (group.isEnrolled(student))
293                        continue;
294                    for (int x = 0; x < iGroups.length; x++) {
295                        if (idx == x)
296                            continue;
297                        if (!iGroups[x].canEnroll(student))
298                            continue;
299                        while (iGroups[x].size(student) > iGroups[x].getMaxSize()) {
300                            if (!moveAwayOneStudent(iGroups[x]))
301                                break;
302                        }
303                        if (iGroups[x].size(student) > iGroups[x].getMaxSize())
304                            continue;
305                        newGroup = iGroups[x];
306                        movingStudent = student;
307                        break;
308                    }
309                    if (newGroup != null)
310                        break;
311                }
312
313                if (newGroup != null) {
314                    group.removeStudent(movingStudent);
315                    newGroup.addStudent(movingStudent);
316                } else {
317                    break;
318                }
319            }
320
321            while (group.size() < group.getMinSize()) {
322                if (!moveIntoOneStudent(group))
323                    break;
324            }
325        }
326
327        return iGroups;
328    }
329
330    public class Group {
331        private ArrayList<Student> iStudents = new ArrayList<Student>();
332        private Lecture iLecture = null;
333        private Configuration iConfiguration = null;
334        private Double iDist = null;
335        private double iMinSize = 0, iMaxSize = 0;
336        private HashMap<Student, Double> iDistCache = new HashMap<Student, Double>();
337        private double iSize = 0.0;
338
339        public Group(Lecture lecture) {
340            iLecture = lecture;
341            iMinSize = lecture.minClassLimit();
342            iMaxSize = lecture.maxAchievableClassLimit();
343        }
344
345        public Group(Configuration configuration) {
346            iConfiguration = configuration;
347            iMinSize = iMaxSize = iConfiguration.getLimit();
348        }
349
350        public Configuration getConfiguration() {
351            return iConfiguration;
352        }
353
354        public Lecture getLecture() {
355            return iLecture;
356        }
357
358        public double getMinSize() {
359            return iMinSize;
360        }
361
362        public double getMaxSize() {
363            return iMaxSize;
364        }
365
366        public void setMinSize(double minSize) {
367            iMinSize = minSize;
368        }
369
370        public void setMaxSize(double maxSize) {
371            iMaxSize = maxSize;
372        }
373        
374        public double getStudentWeight(Student student) {
375            double max = 0.0;
376            for (Student s: iStudents) {
377                double w = s.getOfferingWeight(iOfferingId);
378                if (w > max) max = w;
379            }
380            if (student != null) {
381                double w = student.getOfferingWeight(iOfferingId);
382                if (w > max) max = w;
383            }
384            return (student == null ? 0.0 : student.getOfferingWeight(iOfferingId)) + sNominalWeight - max;
385        }
386
387        public double getDistance() {
388            if (iDist == null) {
389                double dist = 0.0;
390                double prob = 10.0 / iStudents.size();
391                int cnt = 0;
392                for (Student s1 : iStudents) {
393                    if (Math.random() < prob) {
394                        for (Student s2 : iStudents) {
395                            if (s1.getId().compareTo(s2.getId()) <= 0)
396                                continue;
397                            if (Math.random() < prob) {
398                                dist += s1.getDistance(s2);
399                                cnt++;
400                            }
401                        }
402                    }
403                }
404                iDist = Double.valueOf(dist / cnt);
405            }
406            return iDist.doubleValue();
407        }
408
409        public double getDistance(Student student) {
410            if (iStudents.isEmpty()) return 0.0;
411            Double cachedDist = iDistCache.get(student);
412            if (cachedDist != null)
413                return cachedDist.doubleValue();
414            double dist = 0.0;
415            int cnt = 0;
416            for (Student s : iStudents) {
417                dist += s.getDistance(student);
418                cnt++;
419            }
420            iDistCache.put(student, dist / cnt);
421            return dist / cnt;
422        }
423
424        public void addStudent(Student student) {
425            iStudents.add(student);
426            iSize += student.getOfferingWeight(iOfferingId);
427            iDist = null;
428            iDistCache.clear();
429        }
430
431        public void removeStudent(Student student) {
432            iStudents.remove(student);
433            iSize -= student.getOfferingWeight(iOfferingId);
434            iDist = null;
435            iDistCache.clear();
436        }
437
438        public List<Student> getStudents() {
439            return iStudents;
440        }
441
442        public double size() {
443            return iSize;
444        }
445        
446        public double size(Student student) {
447            return iSize + getStudentWeight(student);
448        }
449
450        @Override
451        public String toString() {
452            return "{" + size() + "-" + getDistance() + "/" + getStudents() + "}";
453        }
454
455        public boolean canEnroll(Student student) {
456            if (getLecture() != null) {
457                return student.canEnroll(getLecture());
458            } else {
459                for (Long subpartId: getConfiguration().getTopSubpartIds()) {
460                    boolean canEnrollThisSubpart = false;
461                    for (Lecture lecture : getConfiguration().getTopLectures(subpartId)) {
462                        if (student.canEnroll(lecture)) {
463                            canEnrollThisSubpart = true;
464                            break;
465                        }
466                    }
467                    if (!canEnrollThisSubpart)
468                        return false;
469                }
470                return true;
471            }
472        }
473
474        public boolean isEnrolled(Student student) {
475            if (getLecture() != null) {
476                return student.getLectures().contains(getLecture());
477            } else {
478                for (Lecture lect : student.getLectures()) {
479                    if (lect.getConfiguration().equals(getConfiguration()))
480                        return true;
481                }
482                return false;
483            }
484
485        }
486    }
487
488    public static void initialSectioningCfg(Assignment<Lecture, Placement> assignment, Progress p, Long offeringId, String courseName, Collection<Student> students,
489            List<Configuration> configurations) {
490        if (students == null || students.isEmpty())
491            return;
492        if (configurations == null || configurations.isEmpty())
493            return;
494        if (configurations.size() == 1) {
495            Configuration cfg = configurations.get(0);
496            for (Student st : students) {
497                st.addConfiguration(cfg);
498            }
499            for (Long subpartId: cfg.getTopSubpartIds()) {
500                initialSectioning(assignment, p, offeringId, courseName, students, cfg.getTopLectures(subpartId));
501            }
502        } else {
503            p.trace("sectioning " + students.size() + " students of course " + courseName + " into "
504                    + configurations.size() + " configurations");
505            InitialSectioning sect = new InitialSectioning(p, offeringId, configurations, students);
506            Group[] studentsPerSection = sect.getGroups();
507            for (int i = 0; i < configurations.size(); i++) {
508                Group group = studentsPerSection[i];
509                p.trace((i + 1) + ". configuration got " + group.getStudents().size() + " students (weighted="
510                        + group.size() + ", cfgLimit=" + group.getConfiguration().getLimit() + ")");
511                for (Student st : group.getStudents()) {
512                    st.addConfiguration(group.getConfiguration());
513                }
514                for (Long subpartId: group.getConfiguration().getTopSubpartIds()) {
515                    initialSectioning(assignment, p, offeringId, courseName, group.getStudents(), group.getConfiguration().getTopLectures(subpartId));
516                }
517            }
518        }
519    }
520
521    private static String getClassLabel(Lecture lecture) {
522        return "<A href='classDetail.do?cid=" + lecture.getClassId() + "'>" + lecture.getName() + "</A>";
523    }
524
525    private static void initialSectioning(Assignment<Lecture, Placement> assignment, Progress p, Long offeringId, String parentName, Collection<Student> students,
526            Collection<Lecture> lectures) {
527        if (lectures == null || lectures.isEmpty())
528            return;
529        if (students == null || students.isEmpty())
530            return;
531        for (Lecture lecture : lectures) {
532            if (lecture.classLimit(assignment) == 0 && !lecture.isCommitted())
533                p.warn("Class " + getClassLabel(lecture) + " has zero class limit.");
534        }
535
536        p.trace("sectioning " + students.size() + " students of course " + parentName + " into " + lectures.size()
537                + " sections");
538        if (lectures.size() == 1) {
539            Lecture lect = lectures.iterator().next();
540            for (Student st : students) {
541                if (!st.canEnroll(lect)) {
542                    p.info("Unable to enroll student " + st.getId() + " in class " + getClassLabel(lect));
543                }
544                lect.addStudent(assignment, st);
545                st.addLecture(lect);
546            }
547            if (lect.hasAnyChildren()) {
548                for (Long subpartId: lect.getChildrenSubpartIds()) {
549                    List<Lecture> children = lect.getChildren(subpartId);
550                    initialSectioning(assignment, p, offeringId, lect.getName(), students, children);
551                }
552            }
553        } else {
554            InitialSectioning sect = new InitialSectioning(p, offeringId, lectures, students);
555            Group[] studentsPerSection = sect.getGroups();
556            for (int i = 0; i < studentsPerSection.length; i++) {
557                Group group = studentsPerSection[i];
558                Lecture lect = group.getLecture();
559                if (group.getStudents().isEmpty()) {
560                    p.trace("Lecture " + getClassLabel(lect) + " got no students (cl=" + lect.classLimit(assignment) + ")");
561                    continue;
562                }
563                p.trace("Lecture " + getClassLabel(lect) + " got " + group.getStudents().size()
564                        + " students (weighted=" + group.size() + ", classLimit=" + lect.classLimit(assignment) + ")");
565                List<Student> studentsThisSection = group.getStudents();
566                for (Student st : studentsThisSection) {
567                    if (!st.canEnroll(lect)) {
568                        p.info("Unable to enroll student " + st.getId() + " in class " + getClassLabel(lect));
569                    }
570                    lect.addStudent(assignment, st);
571                    st.addLecture(lect);
572                }
573                if (lect.hasAnyChildren()) {
574                    for (Long subpartId: lect.getChildrenSubpartIds()) {
575                        List<Lecture> children = lect.getChildren(subpartId);
576                        initialSectioning(assignment, p, offeringId, lect.getName(), studentsThisSection, children);
577                    }
578                }
579            }
580        }
581    }
582
583}