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