001    package net.sf.cpsolver.studentsct.heuristics.selection;
002    
003    import java.util.Collection;
004    import java.util.Enumeration;
005    import java.util.Hashtable;
006    import java.util.Iterator;
007    import java.util.Set;
008    import java.util.Vector;
009    
010    import org.apache.log4j.Logger;
011    
012    import net.sf.cpsolver.ifs.extension.Extension;
013    import net.sf.cpsolver.ifs.heuristics.NeighbourSelection;
014    import net.sf.cpsolver.ifs.model.Neighbour;
015    import net.sf.cpsolver.ifs.solution.Solution;
016    import net.sf.cpsolver.ifs.solver.Solver;
017    import net.sf.cpsolver.ifs.util.DataProperties;
018    import net.sf.cpsolver.ifs.util.Progress;
019    import net.sf.cpsolver.studentsct.StudentSectioningModel;
020    import net.sf.cpsolver.studentsct.extension.DistanceConflict;
021    import net.sf.cpsolver.studentsct.heuristics.studentord.StudentChoiceRealFirstOrder;
022    import net.sf.cpsolver.studentsct.heuristics.studentord.StudentOrder;
023    import net.sf.cpsolver.studentsct.model.CourseRequest;
024    import net.sf.cpsolver.studentsct.model.Enrollment;
025    import net.sf.cpsolver.studentsct.model.Request;
026    import net.sf.cpsolver.studentsct.model.Student;
027    
028    /**
029     * Section all students using incremental branch & bound (no unassignments).
030     * All students are taken in a random order, for each student a branch & bound
031     * algorithm is used to find his/her best schedule on top of all other existing
032     * student schedules (no enrollment of a different student is unassigned).
033     *
034     * <br><br>
035     * Parameters:
036     * <br>
037     * <table border='1'><tr><th>Parameter</th><th>Type</th><th>Comment</th></tr>
038     * <tr><td>Neighbour.BranchAndBoundTimeout</td><td>{@link Integer}</td><td>Timeout for each neighbour selection (in milliseconds).</td></tr>
039     * <tr><td>Neighbour.BranchAndBoundMinimizePenalty</td><td>{@link Boolean}</td><td>If true, section penalties (instead of section values) are minimized: 
040     * overall penalty is minimized together with the maximization of the number of assigned requests and minimization of distance conflicts 
041     * -- this variant is to better mimic the case when students can choose their sections (section times).</td></tr>
042     * </table>
043     * <br><br>
044     * 
045     * @version
046     * StudentSct 1.1 (Student Sectioning)<br>
047     * Copyright (C) 2007 Tomáš Müller<br>
048     * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
049     * Lazenska 391, 76314 Zlin, Czech Republic<br>
050     * <br>
051     * This library is free software; you can redistribute it and/or
052     * modify it under the terms of the GNU Lesser General Public
053     * License as published by the Free Software Foundation; either
054     * version 2.1 of the License, or (at your option) any later version.
055     * <br><br>
056     * This library is distributed in the hope that it will be useful,
057     * but WITHOUT ANY WARRANTY; without even the implied warranty of
058     * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
059     * Lesser General Public License for more details.
060     * <br><br>
061     * You should have received a copy of the GNU Lesser General Public
062     * License along with this library; if not, write to the Free Software
063     * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
064    */
065    
066    public class BranchBoundSelection implements NeighbourSelection {
067        private static Logger sLog = Logger.getLogger(BranchBoundSelection.class); 
068        protected int iTimeout = 10000;
069        protected DistanceConflict iDistanceConflict = null;
070        public static boolean sDebug = false;
071        protected Enumeration iStudentsEnumeration = null;
072        protected boolean iMinimizePenalty = false;
073        protected StudentOrder iOrder = new StudentChoiceRealFirstOrder();
074        protected double iDistConfWeight = 1.0;
075        
076        /**
077         * Constructor
078         * @param properties configuration
079         */
080        public BranchBoundSelection(DataProperties properties) {
081            iTimeout = properties.getPropertyInt("Neighbour.BranchAndBoundTimeout", iTimeout);
082            iMinimizePenalty = properties.getPropertyBoolean("Neighbour.BranchAndBoundMinimizePenalty", iMinimizePenalty);
083            if (iMinimizePenalty) sLog.info("Overall penalty is going to be minimized (together with the maximization of the number of assigned requests and minimization of distance conflicts).");
084            if (properties.getProperty("Neighbour.BranchAndBoundOrder")!=null) {
085                try {
086                    iOrder = (StudentOrder)Class.forName(properties.getProperty("Neighbour.BranchAndBoundOrder")).
087                        getConstructor(new Class[] {DataProperties.class}).
088                        newInstance(new Object[] {properties});
089                } catch (Exception e) {
090                    sLog.error("Unable to set student order, reason:"+e.getMessage(),e);
091                }
092            }
093            iDistConfWeight = properties.getPropertyDouble("DistanceConflict.Weight", iDistConfWeight);
094        }
095        
096        /**
097         * Initialize
098         */
099        public void init(Solver solver, String name) {
100            Vector students = iOrder.order(((StudentSectioningModel)solver.currentSolution().getModel()).getStudents());
101            iStudentsEnumeration = students.elements();
102            if (iDistanceConflict==null)
103                for (Enumeration e=solver.getExtensions().elements();e.hasMoreElements();) {
104                    Extension ext = (Extension)e.nextElement();
105                    if (ext instanceof DistanceConflict)
106                        iDistanceConflict = (DistanceConflict)ext;
107                }
108            Progress.getInstance(solver.currentSolution().getModel()).setPhase(name, students.size());
109        }
110        
111        public void init(Solver solver) {
112            init(solver, "Branch&bound...");
113        }
114        
115        /** 
116         * Select neighbour. All students are taken, one by one in a random order. 
117         * For each student a branch & bound search is employed. 
118         */
119        public Neighbour selectNeighbour(Solution solution) {
120            while (iStudentsEnumeration.hasMoreElements()) {
121                Student student = (Student)iStudentsEnumeration.nextElement();
122                Progress.getInstance(solution.getModel()).incProgress();
123                Neighbour neighbour = getSelection(student).select();
124                if (neighbour!=null) return neighbour;
125            }
126            return null;
127        }
128        
129        /**
130         * Branch & bound selection for a student
131         */
132        public Selection getSelection(Student student) {
133            return new Selection(student);
134        }
135        
136        /**
137         * Branch & bound selection for a student
138         */
139        public class Selection {
140            /** Student */
141            protected Student iStudent;
142            /** Start time */
143            protected long iT0;
144            /** End time */
145            protected long iT1;
146            /** Was timeout reached */
147            protected boolean iTimeoutReached;
148            /** Current assignment */
149            protected Enrollment[] iAssignment;
150            /** Best assignment */
151            protected Enrollment[] iBestAssignment;
152            /** Best value */
153            protected double iBestValue;
154            /** Value cache */
155            protected Hashtable iValues;
156            
157            /**
158             * Constructor
159             * @param student selected student
160             */
161            public Selection(Student student) {
162                iStudent = student;
163            }
164            
165            /**
166             * Execute branch & bound, return the best found schedule for the selected student.
167             */
168            public BranchBoundNeighbour select() {
169                iT0 = System.currentTimeMillis();
170                iTimeoutReached = false;
171                iAssignment = new Enrollment[iStudent.getRequests().size()];
172                iBestAssignment = null;
173                iBestValue = 0;
174                iValues = new Hashtable();
175                backTrack(0);
176                iT1 = System.currentTimeMillis();
177                if (iBestAssignment==null) return null;
178                return new BranchBoundNeighbour(iBestValue, iBestAssignment);
179            }
180            
181            /** Was timeout reached */
182            public boolean isTimeoutReached() {
183                return iTimeoutReached;
184            }
185            
186            /** Time (in milliseconds) the branch & bound did run */
187            public long getTime() {
188                return iT1 - iT0;
189            }
190            
191            /** Best schedule */
192            public Enrollment[] getBestAssignment() {
193                return iBestAssignment;
194            }
195            
196            /** Value of the best schedule */
197            public double getBestValue() {
198                return iBestValue;
199            }
200            
201            /** Number of requests assigned in the best schedule */
202            public int getBestNrAssigned() {
203                int nrAssigned = 0;
204                for (int i=0;i<iBestAssignment.length;i++)
205                    if (iBestAssignment[i]!=null) nrAssigned++;
206                return nrAssigned;
207            }
208    
209            /** Bound for the number of assigned requests in the current schedule */ 
210            public int getNrAssignedBound(int idx) {
211                int bound = 0;
212                int i=0, alt=0;
213                for (Enumeration e=iStudent.getRequests().elements();e.hasMoreElements();i++) {
214                    Request r  = (Request)e.nextElement();
215                    if (i<idx) {
216                        if (iAssignment[i]!=null) 
217                            bound++;
218                        if (r.isAlternative()) {
219                            if (iAssignment[i]!=null || (r instanceof CourseRequest && ((CourseRequest)r).isWaitlist())) alt--;
220                        } else {
221                            if (r instanceof CourseRequest && !((CourseRequest)r).isWaitlist() && iAssignment[i]==null) alt++;
222                        }
223                    } else {
224                        if (!r.isAlternative())
225                            bound ++;
226                        else if (alt>0) {
227                            bound ++; alt--;
228                        }
229                    }
230                }
231                return bound;
232            }
233            
234            /** Number of distance conflicts of idx-th assignment of the current schedule */
235            public double getNrDistanceConflicts(int idx) {
236                if (iDistanceConflict==null || iAssignment[idx]==null) return 0;
237                double nrDist = iDistanceConflict.nrConflicts(iAssignment[idx]);
238                for (int x=0;x<idx;x++)
239                    if (iAssignment[x]!=null)
240                        nrDist+=iDistanceConflict.nrConflicts(iAssignment[x],iAssignment[idx]);
241                return nrDist;
242            }
243    
244            /** Bound for the current schedule */
245            public double getBound(int idx) {
246                double bound = 0.0;
247                int i=0, alt=0;
248                for (Enumeration e=iStudent.getRequests().elements();e.hasMoreElements();i++) {
249                    Request r  = (Request)e.nextElement();
250                    if (i<idx) {
251                        if (iAssignment[i]!=null) 
252                            bound += iAssignment[i].toDouble(getNrDistanceConflicts(i));
253                        if (r.isAlternative()) {
254                            if (iAssignment[i]!=null || (r instanceof CourseRequest && ((CourseRequest)r).isWaitlist())) alt--;
255                        } else {
256                            if (r instanceof CourseRequest && !((CourseRequest)r).isWaitlist() && iAssignment[i]==null) alt++;
257                        }
258                    } else {
259                        if (!r.isAlternative())
260                            bound += r.getBound();
261                        else if (alt>0) {
262                            bound += r.getBound(); alt--;
263                        }
264                    }
265                }
266                return bound;
267            }
268    
269            /** Value of the current schedule */
270            public double getValue() {
271                double value = 0.0;
272                for (int i=0;i<iAssignment.length;i++)
273                    if (iAssignment[i]!=null)
274                        value += iAssignment[i].toDouble(getNrDistanceConflicts(i));
275                return value;
276            }
277            
278            /** Assignment penalty */
279            protected double getAssignmentPenalty(int i) {
280                return iAssignment[i].getPenalty() + iDistConfWeight*getNrDistanceConflicts(i);
281            }
282            
283            /** Penalty of the current schedule */
284            public double getPenalty() {
285                double bestPenalty = 0;
286                for (int i=0;i<iAssignment.length;i++)
287                    if (iAssignment[i]!=null) bestPenalty += getAssignmentPenalty(i);
288                return bestPenalty;
289            }
290            
291            
292            /** Penalty bound of the current schedule */
293            public double getPenaltyBound(int idx) {
294                double bound = 0.0;
295                int i=0, alt=0;
296                for (Enumeration e=iStudent.getRequests().elements();e.hasMoreElements();i++) {
297                    Request r  = (Request)e.nextElement();
298                    if (i<idx) {
299                        if (iAssignment[i]!=null)
300                            bound += getAssignmentPenalty(i);
301                        if (r.isAlternative()) {
302                            if (iAssignment[i]!=null || (r instanceof CourseRequest && ((CourseRequest)r).isWaitlist())) alt--;
303                        } else {
304                            if (r instanceof CourseRequest && !((CourseRequest)r).isWaitlist() && iAssignment[i]==null) alt++;
305                        }
306                    } else {
307                        if (!r.isAlternative()) {
308                            if (r instanceof CourseRequest)
309                                bound += ((CourseRequest)r).getMinPenalty();
310                        } else if (alt>0) {
311                            if (r instanceof CourseRequest)
312                                bound += ((CourseRequest)r).getMinPenalty();
313                            alt--;
314                        }
315                    }
316                }
317                return bound;
318            }
319            
320            /** Save the current schedule as the best */ 
321            public void saveBest() {
322                if (iBestAssignment==null)
323                    iBestAssignment = new Enrollment[iAssignment.length];
324                for (int i=0;i<iAssignment.length;i++)
325                    iBestAssignment[i] = iAssignment[i];
326                if (iMinimizePenalty)
327                    iBestValue = getPenalty();
328                else
329                    iBestValue = getValue();
330            }
331            
332            /** First conflicting enrollment */
333            public Enrollment firstConflict(int idx, Enrollment enrollment) {
334                Set conflicts = enrollment.variable().getModel().conflictValues(enrollment);
335                if (conflicts.contains(enrollment)) return enrollment;
336                if (conflicts!=null && !conflicts.isEmpty()) {
337                    for (Iterator i=conflicts.iterator();i.hasNext();) {
338                        Enrollment conflict = (Enrollment)i.next();
339                        if (!conflict.getStudent().equals(iStudent)) return conflict;
340                    }
341                }
342                for (int i=0;i<iAssignment.length;i++) {
343                    if (iAssignment[i]==null) continue;
344                    if (!iAssignment[i].isConsistent(enrollment)) return iAssignment[i];
345                }
346                return null;
347            }
348            
349            /** True if the given request can be assigned */
350            public boolean canAssign(Request request, int idx) {
351                if (!request.isAlternative() || iAssignment[idx]!=null) return true;
352                int alt = 0;
353                int i = 0;
354                for (Enumeration e=iStudent.getRequests().elements();e.hasMoreElements();i++) {
355                    Request r  = (Request)e.nextElement();
356                    if (r.equals(request)) continue;
357                    if (r.isAlternative()) {
358                        if (iAssignment[i]!=null || (r instanceof CourseRequest && ((CourseRequest)r).isWaitlist())) alt--;
359                    } else {
360                        if (r instanceof CourseRequest && !((CourseRequest)r).isWaitlist() && iAssignment[i]==null) alt++;
361                    }
362                }
363                return (alt>0);
364            }
365            
366            /** Number of assigned requests in the current schedule */
367            public int getNrAssigned() {
368                int assigned = 0;
369                for (int i=0;i<iAssignment.length;i++)
370                    if (iAssignment[i]!=null) assigned++;            
371                return assigned;
372            }
373            
374            /** Returns true if the given request can be left unassigned */
375            protected boolean canLeaveUnassigned(Request request) {
376                    return true;
377            }
378            
379            /** branch & bound search */
380            public void backTrack(int idx) {
381                if (sDebug) sLog.debug("backTrack("+getNrAssigned()+"/"+getValue()+","+idx+")");
382                if (iTimeout>0 && (System.currentTimeMillis()-iT0)>iTimeout) {
383                    if (sDebug) sLog.debug("  -- timeout reached");
384                    iTimeoutReached=true; return;
385                }
386                if (iMinimizePenalty) {
387                    if (getBestAssignment()!=null && (getNrAssignedBound(idx)<getBestNrAssigned() || (getNrAssignedBound(idx)==getBestNrAssigned() && getPenaltyBound(idx)>=getBestValue()))) {
388                        if (sDebug) sLog.debug("  -- branch number of assigned "+getNrAssignedBound(idx)+"<"+getBestNrAssigned()+", or penalty "+getPenaltyBound(idx)+">="+getBestValue());
389                        return;
390                    }
391                    if (idx==iAssignment.length) {
392                        if (getBestAssignment()==null || (getNrAssigned()>getBestNrAssigned() || (getNrAssigned()==getBestNrAssigned() && getPenalty()<getBestValue()))) {
393                            if (sDebug) sLog.debug("  -- best solution found "+getNrAssigned()+"/"+getPenalty());
394                            saveBest();
395                        }
396                        return;
397                    }
398                } else {
399                    if (getBestAssignment()!=null && getBound(idx)>=getBestValue()) {
400                        if (sDebug) sLog.debug("  -- branch "+getBound(idx)+" >= "+getBestValue());
401                        return;
402                    }
403                    if (idx==iAssignment.length) {
404                        if (getBestAssignment()==null || getValue()<getBestValue()) {
405                            if (sDebug) sLog.debug("  -- best solution found "+getNrAssigned()+"/"+getValue());
406                            saveBest();
407                        }
408                        return;
409                    }
410                }
411                
412                Request request = (Request)iStudent.getRequests().elementAt(idx);
413                if (sDebug) sLog.debug("  -- request: "+request);
414                if (!canAssign(request, idx)) {
415                    if (sDebug) sLog.debug("    -- cannot assign");
416                    backTrack(idx+1);
417                    return;
418                }
419                Collection values = null;
420                if (request instanceof CourseRequest) {
421                    CourseRequest courseRequest = (CourseRequest)request;
422                    if (!courseRequest.getSelectedChoices().isEmpty()) {
423                        if (sDebug) sLog.debug("    -- selection among selected enrollments");
424                        values = courseRequest.getSelectedEnrollments(true);
425                        if (values!=null && !values.isEmpty()) { 
426                            boolean hasNoConflictValue = false;
427                            for (Iterator i=values.iterator();i.hasNext();) {
428                                Enrollment enrollment = (Enrollment)i.next();
429                                if (firstConflict(idx, enrollment)!=null) continue;
430                                hasNoConflictValue = true;
431                                if (sDebug) sLog.debug("      -- nonconflicting enrollment found: "+enrollment);
432                                iAssignment[idx] = enrollment;
433                                backTrack(idx+1);
434                                iAssignment[idx] = null;
435                            }
436                            if (hasNoConflictValue) return;
437                        }
438                    }
439                    values = (Collection)iValues.get(courseRequest);
440                    if (values==null) {
441                        values = courseRequest.getAvaiableEnrollments();
442                        iValues.put(courseRequest, values);
443                    }
444                } else {
445                    values = request.computeEnrollments();
446                }    
447                if (sDebug) {
448                    sLog.debug("  -- nrValues: "+values.size());
449                    int vIdx=1;
450                    for (Iterator i=values.iterator();i.hasNext();vIdx++) {
451                        Enrollment enrollment = (Enrollment)i.next();
452                        if (sDebug) sLog.debug("    -- ["+vIdx+"]: "+enrollment);
453                    }
454                }
455                boolean hasNoConflictValue = false;
456                for (Iterator i=values.iterator();i.hasNext();) {
457                    Enrollment enrollment = (Enrollment)i.next();
458                    if (sDebug) sLog.debug("    -- enrollment: "+enrollment);
459                    Enrollment conflict = firstConflict(idx, enrollment);
460                    if (conflict!=null) {
461                        if (sDebug) sLog.debug("        -- in conflict with: "+conflict);
462                        continue;
463                    }
464                    hasNoConflictValue = true;
465                    iAssignment[idx] = enrollment;
466                    backTrack(idx+1);
467                    iAssignment[idx] = null;
468                }
469                if (canLeaveUnassigned(request) && (!hasNoConflictValue || request instanceof CourseRequest)) backTrack(idx+1);
470            }
471        }    
472            
473        /** Branch & bound neighbour -- a schedule of a student */
474        public static class BranchBoundNeighbour extends Neighbour {
475            private double iValue;
476            private Enrollment[] iAssignment;
477            
478            /**
479             * Constructor
480             * @param value value of the schedule
481             * @param assignment enrollments of student's requests
482             */
483            public BranchBoundNeighbour(double value, Enrollment[] assignment) {
484                iValue = value;
485                iAssignment = assignment;
486            }
487            
488            public double value() {
489                return iValue;
490            }
491            
492            /** Assignment */
493            public Enrollment[] getAssignment() {
494                return iAssignment;
495            }
496            
497            /** Assign the schedule */
498            public void assign(long iteration) {
499                for (int i=0;i<iAssignment.length;i++)
500                    if (iAssignment[i]!=null)
501                        iAssignment[i].variable().assign(iteration, iAssignment[i]);
502            }
503            
504            public String toString() {
505                StringBuffer sb = new StringBuffer("B&B{");
506                Student student = null;
507                for (int i=0;i<iAssignment.length;i++) {
508                    if (iAssignment[i]!=null) {
509                        student = iAssignment[i].getRequest().getStudent();
510                        sb.append(" "+student);
511                        sb.append(" ("+iValue+")");
512                        break;
513                    }
514                }
515                if (student!=null) {
516                    int idx=0;
517                    for (Enumeration e=student.getRequests().elements();e.hasMoreElements();idx++) {
518                        Request request = (Request)e.nextElement();
519                        sb.append("\n"+request);
520                        Enrollment enrollment = iAssignment[idx];
521                        if (enrollment==null)
522                            sb.append("  -- not assigned");
523                        else
524                            sb.append("  -- "+enrollment);
525                    }
526                }
527                sb.append("\n}");
528                return sb.toString();
529            }
530            
531        }
532    }