    001package org.cpsolver.studentsct.heuristics.selection;
    002
    003import java.text.DecimalFormat;
    004import java.util.ArrayList;
    005import java.util.Collection;
    006import java.util.Collections;
    007import java.util.Comparator;
    008import java.util.ConcurrentModificationException;
    009import java.util.HashMap;
    010import java.util.HashSet;
    011import java.util.Iterator;
    012import java.util.LinkedList;
    013import java.util.List;
    014import java.util.Map;
    015import java.util.Set;
    016
    017import org.apache.logging.log4j.Logger;
    018import org.cpsolver.ifs.assignment.Assignment;
    019import org.cpsolver.ifs.heuristics.NeighbourSelection;
    020import org.cpsolver.ifs.model.GlobalConstraint;
    021import org.cpsolver.ifs.model.InfoProvider;
    022import org.cpsolver.ifs.model.Neighbour;
    023import org.cpsolver.ifs.solution.Solution;
    024import org.cpsolver.ifs.solver.Solver;
    025import org.cpsolver.ifs.solver.SolverListener;
    026import org.cpsolver.ifs.util.DataProperties;
    027import org.cpsolver.ifs.util.JProf;
    028import org.cpsolver.ifs.util.Progress;
    029import org.cpsolver.studentsct.StudentSectioningModel;
    030import org.cpsolver.studentsct.constraint.LinkedSections;
    031import org.cpsolver.studentsct.extension.DistanceConflict;
    032import org.cpsolver.studentsct.extension.StudentQuality;
    033import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
    034import org.cpsolver.studentsct.filter.StudentFilter;
    035import org.cpsolver.studentsct.heuristics.studentord.StudentGroupsChoiceRealFirstOrder;
    036import org.cpsolver.studentsct.heuristics.studentord.StudentOrder;
    037import org.cpsolver.studentsct.model.CourseRequest;
    038import org.cpsolver.studentsct.model.Enrollment;
    039import org.cpsolver.studentsct.model.FreeTimeRequest;
    040import org.cpsolver.studentsct.model.Request;
    041import org.cpsolver.studentsct.model.Section;
    042import org.cpsolver.studentsct.model.Student;
    043import org.cpsolver.studentsct.model.Unavailability;
    044import org.cpsolver.studentsct.online.selection.OnlineSectioningCriterion.TimeToAvoid;
    045import org.cpsolver.studentsct.weights.StudentWeights;
    046
    047/**
    048 * Section all students using incremental branch &amp; bound (no unassignments). All
    049 * students are taken in a random order, for each student a branch &amp; bound
    050 * algorithm is used to find his/her best schedule on top of all other existing
    051 * student schedules (no enrollment of a different student is unassigned).
    052 *
    053 * <br>
    054 * <br>
    055 * Parameters: <br>
    056 * <table border='1' summary='Related Solver Parameters'>
    057 * <tr>
    058 * <th>Parameter</th>
    059 * <th>Type</th>
    060 * <th>Comment</th>
    061 * </tr>
    062 * <tr>
    063 * <td>Neighbour.BranchAndBoundTimeout</td>
    064 * <td>{@link Integer}</td>
    065 * <td>Timeout for each neighbour selection (in milliseconds).</td>
    066 * </tr>
    067 * <tr>
    068 * <td>Neighbour.BranchAndBoundMinimizePenalty</td>
    069 * <td>{@link Boolean}</td>
    070 * <td>If true, section penalties (instead of section values) are minimized:
    071 * overall penalty is minimized together with the maximization of the number of
    072 * assigned requests and minimization of distance conflicts -- this variant is
    073 * to better mimic the case when students can choose their sections (section
    074 * times).</td>
    075 * </tr>
    076 * </table>
    077 * <br>
    078 * <br>
    079 *
    080 * @version StudentSct 1.3 (Student Sectioning)<br>
    081 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
    082 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    083 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    084 * <br>
    085 *          This library is free software; you can redistribute it and/or modify
    086 *          it under the terms of the GNU Lesser General Public License as
    087 *          published by the Free Software Foundation; either version 3 of the
    088 *          License, or (at your option) any later version. <br>
    089 * <br>
    090 *          This library is distributed in the hope that it will be useful, but
    091 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    092 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    093 *          Lesser General Public License for more details. <br>
    094 * <br>
    095 *          You should have received a copy of the GNU Lesser General Public
    096 *          License along with this library; if not see
    097 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    098 */
    099
    100public class BranchBoundSelection implements NeighbourSelection<Request, Enrollment>, InfoProvider<Request, Enrollment>, SolverListener<Request, Enrollment> {
    101    private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(BranchBoundSelection.class);
    102    private static DecimalFormat sDF = new DecimalFormat("0.00");
    103    protected int iTimeout = 10000;
    104    protected DistanceConflict iDistanceConflict = null;
    105    protected TimeOverlapsCounter iTimeOverlaps = null;
    106    protected StudentQuality iStudentQuality = null;
    107    protected StudentSectioningModel iModel = null;
    108    public static boolean sDebug = false;
    109    protected LinkedList<Student> iStudents = null;
    110    protected boolean iMinimizePenalty = false;
    111    protected StudentOrder iOrder = new StudentGroupsChoiceRealFirstOrder();
    112    protected double iDistConfWeight = 1.0;
    113    protected boolean iBranchWhenSelectedHasNoConflict = false;
    114    protected boolean iTimesToAvoidHeuristics = true;
    115    protected StudentFilter iFilter = null;
    116
    117    protected long iNbrIterations = 0;
    118    protected long iTotalTime = 0;
    119    protected long iNbrTimeoutReached = 0;
    120    protected long iNbrNoSolution = 0;
    121
    122    /**
    123     * Constructor
    124     *
    125     * @param properties
    126     *            configuration
    127     */
    128    public BranchBoundSelection(DataProperties properties) {
    129        iTimeout = properties.getPropertyInt("Neighbour.BranchAndBoundTimeout", iTimeout);
    130        iMinimizePenalty = properties.getPropertyBoolean("Neighbour.BranchAndBoundMinimizePenalty", iMinimizePenalty);
    131        if (iMinimizePenalty)
    132            sLog.info("Overall penalty is going to be minimized (together with the maximization of the number of assigned requests and minimization of distance conflicts).");
    133        if (properties.getProperty("Neighbour.BranchAndBoundOrder") != null) {
    134            try {
    135                iOrder = (StudentOrder) Class.forName(properties.getProperty("Neighbour.BranchAndBoundOrder"))
    136                        .getConstructor(new Class[] { DataProperties.class }).newInstance(new Object[] { properties });
    137            } catch (Exception e) {
    138                sLog.error("Unable to set student order, reason:" + e.getMessage(), e);
    139            }
    140        }
    141        iDistConfWeight = properties.getPropertyDouble("DistanceConflict.Weight", iDistConfWeight);
    142        iBranchWhenSelectedHasNoConflict = properties.getPropertyBoolean("Students.BranchWhenSelectedHasNoConflict", iBranchWhenSelectedHasNoConflict);
    143        iTimesToAvoidHeuristics = properties.getPropertyBoolean("OnlineStudentSectioning.TimesToAvoidHeuristics", iTimesToAvoidHeuristics);
    144    }
    145
    146    /**
    147     * Initialize
    148     * @param solver current solver
    149     * @param name phase name
    150     */
    151    public void init(Solver<Request, Enrollment> solver, String name) {
    152        setModel((StudentSectioningModel) solver.currentSolution().getModel());
    153        Progress.getInstance(solver.currentSolution().getModel()).setPhase(name, iModel.getStudents().size());
    154        iNbrIterations = 0;
    155        iNbrTimeoutReached = 0;
    156        iNbrNoSolution = 0;
    157        iTotalTime = 0;
    158    }
    159
    160    public void setModel(StudentSectioningModel model) {
    161        iModel = model;
    162        List<Student> students = iOrder.order(iModel.getStudents());
    163        iStudents = new LinkedList<Student>(students);
    164        iTimeOverlaps = model.getTimeOverlaps();
    165        iDistanceConflict = model.getDistanceConflict();
    166        iStudentQuality = model.getStudentQuality();
    167    }
    168
    169    @Override
    170    public void init(Solver<Request, Enrollment> solver) {
    171        init(solver, "Branch&bound" + (iFilter == null ? "" : " (" + iFilter.getName().toLowerCase() + " students)") + "...");
    172    }
    173
    174    protected synchronized Student nextStudent() {
    175        while (true) {
    176            Student student = iStudents.poll();
    177            if (student == null) return null;
    178            if (iFilter == null || iFilter.accept(student))
    179                return student;
    180        }
    181    }
    182
    183    public synchronized void addStudent(Student student) {
    184        if (iStudents != null && !student.isDummy()) iStudents.addFirst(student);
    185    }
    186
    187    /**
    188     * Select neighbour. All students are taken, one by one in a random order.
    189     * For each student a branch &amp; bound search is employed.
    190     */
    191    @Override
    192    public Neighbour<Request, Enrollment> selectNeighbour(Solution<Request, Enrollment> solution) {
    193        Student student = null;
    194        while ((student = nextStudent()) != null) {
    195            Progress.getInstance(solution.getModel()).incProgress();
    196            for (int i = 0; i < 5; i++) {
    197                try {
    198                    Neighbour<Request, Enrollment> neighbour = getSelection(solution.getAssignment(), student).select();
    199                    if (neighbour != null) return neighbour;
    200                    break;
    201                } catch (ConcurrentModificationException e) {}
    202            }
    203        }
    204        return null;
    205    }
    206
    207    /**
    208     * Branch &amp; bound selection for a student
    209     * @param assignment current assignment
    210     * @param student selected student
    211     * @return selection
    212     */
    213    public Selection getSelection(Assignment<Request, Enrollment> assignment, Student student) {
    214        return new Selection(student, assignment);
    215    }
    216
    217    /**
    218     * Branch &amp; bound selection for a student
    219     */
    220    public class Selection {
    221        /** Student */
    222        protected Student iStudent;
    223        /** Start time */
    224        protected long iT0;
    225        /** End time */
    226        protected long iT1;
    227        /** Was timeout reached */
    228        protected boolean iTimeoutReached;
    229        /** Current assignment */
    230        protected Enrollment[] iAssignment;
    231        /** Best assignment */
    232        protected Enrollment[] iBestAssignment;
    233        /** Best value */
    234        protected double iBestValue;
    235        /** Value cache */
    236        protected HashMap<CourseRequest, List<Enrollment>> iValues;
    237        /** Current assignment */
    238        protected Assignment<Request, Enrollment> iCurrentAssignment;
    239        /** Times to avoid (used when comparing enrollments) */
    240        protected ArrayList<TimeToAvoid> iTimesToAvoid = null;
    241
    242        /**
    243         * Constructor
    244         *
    245         * @param student
    246         *            selected student
    247         * @param assignment current assignment
    248         */
    249        public Selection(Student student, Assignment<Request, Enrollment> assignment) {
    250            iStudent = student;
    251            iCurrentAssignment = assignment;
    252            if (iTimesToAvoidHeuristics) {
    253                iTimesToAvoid = new ArrayList<TimeToAvoid>();
    254                for (Request r : iStudent.getRequests()) {
    255                    if (r instanceof CourseRequest) {
    256                        List<Enrollment> enrollments = ((CourseRequest) r).getAvaiableEnrollmentsSkipSameTime(assignment);
    257                        if (enrollments.size() <= 5) {
    258                            int penalty = (7 - enrollments.size()) * (r.isAlternative() ? 1 : 7 - enrollments.size());
    259                            for (Enrollment enrollment : enrollments)
    260                                for (Section section : enrollment.getSections())
    261                                    if (section.getTime() != null)
    262                                        iTimesToAvoid.add(new TimeToAvoid(section.getTime(), penalty, r.getPriority()));
    263                        }
    264                    } else if (r instanceof FreeTimeRequest) {
    265                        iTimesToAvoid.add(new TimeToAvoid(((FreeTimeRequest) r).getTime(), 1, Integer.MAX_VALUE));
    266                    }
    267                }
    268                for (Unavailability unavailability: iStudent.getUnavailabilities())
    269                    if (unavailability.getTime() != null)
    270                        iTimesToAvoid.add(new TimeToAvoid(unavailability.getTime(), 1, Integer.MAX_VALUE));
    271            }
    272        }
    273
    274        /**
    275         * Execute branch &amp; bound, return the best found schedule for the
    276         * selected student.
    277         * @return best found schedule for the student
    278         */
    279        public BranchBoundNeighbour select() {
    280            iT0 = JProf.currentTimeMillis();
    281            iTimeoutReached = false;
    282            iAssignment = new Enrollment[iStudent.getRequests().size()];
    283            iBestAssignment = null;
    284            iBestValue = 0;
    285
    286            int i = 0;
    287            for (Request r: iStudent.getRequests())
    288                iAssignment[i++] = iCurrentAssignment.getValue(r);
    289            saveBest();
    290            for (int j = 0; j < iAssignment.length; j++)
    291                iAssignment[j] = null;
    292
    293
    294            iValues = new HashMap<CourseRequest, List<Enrollment>>();
    295            backTrack(0);
    296            iT1 = JProf.currentTimeMillis();
    297
    298            iNbrIterations ++;
    299            iTotalTime += (iT1 - iT0);
    300            if (iTimeoutReached) iNbrTimeoutReached ++;
    301            if (iBestAssignment == null) iNbrNoSolution ++;
    302
    303            if (iBestAssignment == null)
    304                return null;
    305            return new BranchBoundNeighbour(iStudent, iBestValue, iBestAssignment);
    306        }
    307
    308        /** Was timeout reached
    309         * @return true if the timeout was reached
    310         **/
    311        public boolean isTimeoutReached() {
    312            return iTimeoutReached;
    313        }
    314
    315        /** Time (in milliseconds) the branch &amp; bound did run
    316         * @return solver time
    317         **/
    318        public long getTime() {
    319            return iT1 - iT0;
    320        }
    321
    322        /** Best schedule
    323         * @return best schedule
    324         **/
    325        public Enrollment[] getBestAssignment() {
    326            return iBestAssignment;
    327        }
    328
    329        /** Value of the best schedule
    330         * @return value of the best schedule
    331         **/
    332        public double getBestValue() {
    333            return iBestValue;
    334        }
    335
    336        /** Number of requests assigned in the best schedule
    337         * @return number of assigned requests in the best schedule
    338         **/
    339        public int getBestNrAssigned() {
    340            int nrAssigned = 0;
    341            for (int i = 0; i < iBestAssignment.length; i++)
    342                if (iBestAssignment[i] != null)
    343                    nrAssigned += (iBestAssignment[i].isCourseRequest() ? 10 : 1);
    344            return nrAssigned;
    345        }
    346
    347        /** Bound for the number of assigned requests in the current schedule
    348         * @param idx index of the request that is being considered
    349         * @return bound for the given request
    350         **/
    351        public int getNrAssignedBound(int idx) {
    352            int bound = 0;
    353            int i = 0, alt = 0;
    354            for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) {
    355                Request r = e.next();
    356                boolean cr = r instanceof CourseRequest;
    357                if (i < idx) {
    358                    if (iAssignment[i] != null)
    359                        bound += (cr ? 10 : 1);
    360                    if (r.isAlternative()) {
    361                        if (iAssignment[i] != null || (cr && ((CourseRequest) r).isWaitlist()))
    362                            alt--;
    363                    } else {
    364                        if (cr && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null)
    365                            alt++;
    366                    }
    367                } else {
    368                    if (!r.isAlternative())
    369                        bound += (cr ? 10 : 1);
    370                    else if (alt > 0) {
    371                        bound += (cr ? 10 : 1);
    372                        alt--;
    373                    }
    374                }
    375            }
    376            return bound;
    377        }
    378
    379        /**
    380         * Distance conflicts of idx-th assignment of the current
    381         * schedule
    382         * @param idx index of the request
    383         * @return set of distance conflicts
    384         */
    385        public Set<DistanceConflict.Conflict> getDistanceConflicts(int idx) {
    386            if (iDistanceConflict == null || iAssignment[idx] == null)
    387                return null;
    388            Set<DistanceConflict.Conflict> dist = iDistanceConflict.conflicts(iAssignment[idx]);
    389            for (int x = 0; x < idx; x++)
    390                if (iAssignment[x] != null)
    391                    dist.addAll(iDistanceConflict.conflicts(iAssignment[x], iAssignment[idx]));
    392            return dist;
    393        }
    394
    395        /**
    396         * Time overlapping conflicts of idx-th assignment of the current
    397         * schedule
    398         * @param idx index of the request
    399         * @return set of time overlapping conflicts
    400         */
    401        public Set<TimeOverlapsCounter.Conflict> getTimeOverlappingConflicts(int idx) {
    402            if (iTimeOverlaps == null || iAssignment[idx] == null)
    403                return null;
    404            Set<TimeOverlapsCounter.Conflict> overlaps = new HashSet<TimeOverlapsCounter.Conflict>();
    405            for (int x = 0; x < idx; x++)
    406                if (iAssignment[x] != null)
    407                    overlaps.addAll(iTimeOverlaps.conflicts(iAssignment[x], iAssignment[idx]));
    408                else if (iStudent.getRequests().get(x) instanceof FreeTimeRequest)
    409                    overlaps.addAll(iTimeOverlaps.conflicts(((FreeTimeRequest)iStudent.getRequests().get(x)).createEnrollment(), iAssignment[idx]));
    410            overlaps.addAll(iTimeOverlaps.notAvailableTimeConflicts(iAssignment[idx]));
    411            return overlaps;
    412        }
    413
    414        public Set<StudentQuality.Conflict> getStudentQualityConflicts(int idx) {
    415            if (iStudentQuality == null || iAssignment[idx] == null)
    416                return null;
    417
    418            Set<StudentQuality.Conflict> conflicts = new HashSet<StudentQuality.Conflict>();
    419            for (StudentQuality.Type t: StudentQuality.Type.values()) {
    420                conflicts.addAll(iStudentQuality.conflicts(t, iAssignment[idx]));
    421                for (int x = 0; x < idx; x++)
    422                    if (iAssignment[x] != null)
    423                        conflicts.addAll(iStudentQuality.conflicts(t, iAssignment[x], iAssignment[idx]));
    424            }
    425            return conflicts;
    426        }
    427
    428        /**
    429         * Weight of an assignment. Unlike {@link StudentWeights#getWeight(Assignment, Enrollment, Set, Set)}, only count this side of distance conflicts and time overlaps.
    430         * @param enrollment an enrollment
    431         * @param distanceConflicts set of distance conflicts
    432         * @param timeOverlappingConflicts set of time overlapping conflicts
    433         * @return value of the assignment
    434         **/
    435        @Deprecated
    436        protected double getWeight(Enrollment enrollment, Set<DistanceConflict.Conflict> distanceConflicts, Set<TimeOverlapsCounter.Conflict> timeOverlappingConflicts) {
    437            double weight = - iModel.getStudentWeights().getWeight(iCurrentAssignment, enrollment);
    438            if (distanceConflicts != null)
    439                for (DistanceConflict.Conflict c: distanceConflicts) {
    440                    Enrollment other = (c.getE1().equals(enrollment) ? c.getE2() : c.getE1());
    441                    if (other.getRequest().getPriority() <= enrollment.getRequest().getPriority())
    442                        weight += iModel.getStudentWeights().getDistanceConflictWeight(iCurrentAssignment, c);
    443                }
    444            if (timeOverlappingConflicts != null)
    445                for (TimeOverlapsCounter.Conflict c: timeOverlappingConflicts) {
    446                    weight += iModel.getStudentWeights().getTimeOverlapConflictWeight(iCurrentAssignment, enrollment, c);
    447                }
    448            return enrollment.getRequest().getWeight() * weight;
    449        }
    450
    451        protected double getWeight(Enrollment enrollment, Set<StudentQuality.Conflict> conflicts) {
    452            double weight = - iModel.getStudentWeights().getWeight(iCurrentAssignment, enrollment);
    453            if (conflicts != null)
    454                for (StudentQuality.Conflict c: conflicts)
    455                    weight += iModel.getStudentWeights().getStudentQualityConflictWeight(iCurrentAssignment, enrollment, c);
    456            return enrollment.getRequest().getWeight() * weight;
    457        }
    458
    459        /** Return bound of a request
    460         * @param r a request
    461         * @return bound
    462         **/
    463        protected double getBound(Request r) {
    464            return r.getBound();
    465        }
    466
    467        /** Bound for the current schedule
    468         * @param idx index of the request
    469         * @return current bound
    470         **/
    471        public double getBound(int idx) {
    472            double bound = 0.0;
    473            int i = 0, alt = 0;
    474            for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) {
    475                Request r = e.next();
    476                if (i < idx) {
    477                    if (iAssignment[i] != null) {
    478                        if (iStudentQuality != null)
    479                            bound += getWeight(iAssignment[i], getStudentQualityConflicts(i));
    480                        else
    481                            bound += getWeight(iAssignment[i], getDistanceConflicts(i), getTimeOverlappingConflicts(i));
    482                    }
    483                    if (r.isAlternative()) {
    484                        if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist()))
    485                            alt--;
    486                    } else {
    487                        if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null)
    488                            alt++;
    489                    }
    490                } else {
    491                    if (!r.isAlternative())
    492                        bound += getBound(r);
    493                    else if (alt > 0) {
    494                        bound += getBound(r);
    495                        alt--;
    496                    }
    497                }
    498            }
    499            return bound;
    500        }
    501
    502        /** Value of the current schedule
    503         * @return value of the current schedule
    504         **/
    505        public double getValue() {
    506            double value = 0.0;
    507            for (int i = 0; i < iAssignment.length; i++)
    508                if (iAssignment[i] != null) {
    509                    if (iStudentQuality != null)
    510                        value += getWeight(iAssignment[i], getStudentQualityConflicts(i));
    511                    else
    512                        value += getWeight(iAssignment[i], getDistanceConflicts(i), getTimeOverlappingConflicts(i));
    513                }
    514            return value;
    515        }
    516
    517        /** Assignment penalty
    518         * @param i index of the request
    519         * @return assignment penalty
    520         **/
    521        protected double getAssignmentPenalty(int i) {
    522            return iAssignment[i].getPenalty() + iDistConfWeight * getDistanceConflicts(i).size();
    523        }
    524
    525        /** Penalty of the current schedule
    526         * @return penalty of the current schedule
    527         **/
    528        public double getPenalty() {
    529            double bestPenalty = 0;
    530            for (int i = 0; i < iAssignment.length; i++)
    531                if (iAssignment[i] != null)
    532                    bestPenalty += getAssignmentPenalty(i);
    533            return bestPenalty;
    534        }
    535
    536        /** Penalty bound of the current schedule
    537         * @param idx index of request
    538         * @return current penalty bound
    539         **/
    540        public double getPenaltyBound(int idx) {
    541            double bound = 0.0;
    542            int i = 0, alt = 0;
    543            for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) {
    544                Request r = e.next();
    545                if (i < idx) {
    546                    if (iAssignment[i] != null)
    547                        bound += getAssignmentPenalty(i);
    548                    if (r.isAlternative()) {
    549                        if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist()))
    550                            alt--;
    551                    } else {
    552                        if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null)
    553                            alt++;
    554                    }
    555                } else {
    556                    if (!r.isAlternative()) {
    557                        if (r instanceof CourseRequest)
    558                            bound += ((CourseRequest) r).getMinPenalty();
    559                    } else if (alt > 0) {
    560                        if (r instanceof CourseRequest)
    561                            bound += ((CourseRequest) r).getMinPenalty();
    562                        alt--;
    563                    }
    564                }
    565            }
    566            return bound;
    567        }
    568
    569        /** Save the current schedule as the best */
    570        public void saveBest() {
    571            if (iBestAssignment == null)
    572                iBestAssignment = new Enrollment[iAssignment.length];
    573            for (int i = 0; i < iAssignment.length; i++)
    574                iBestAssignment[i] = iAssignment[i];
    575            if (iMinimizePenalty)
    576                iBestValue = getPenalty();
    577            else
    578                iBestValue = getValue();
    579        }
    580
    581        /** True if the enrollment is conflicting
    582         * @param idx index of request
    583         * @param enrollment enrollment in question
    584         * @return true if there is a conflict with previous enrollments
    585         **/
    586        public boolean inConflict(final int idx, final Enrollment enrollment) {
    587            for (GlobalConstraint<Request, Enrollment> constraint : enrollment.variable().getModel().globalConstraints())
    588                if (constraint.inConflict(iCurrentAssignment, enrollment))
    589                    return true;
    590            for (LinkedSections linkedSections: iStudent.getLinkedSections()) {
    591                if (linkedSections.inConflict(enrollment, new LinkedSections.EnrollmentAssignment() {
    592                    @Override
    593                    public Enrollment getEnrollment(Request request, int index) {
    594                        return (index == idx ? enrollment : iAssignment[index]);
    595                    }
    596                }) != null) return true;
    597            }
    598            float credit = enrollment.getCredit();
    599            for (int i = 0; i < iAssignment.length; i++) {
    600                if (iAssignment[i] != null && i != idx) {
    601                    credit += iAssignment[i].getCredit();
    602                    if (credit > iStudent.getMaxCredit() || iAssignment[i].isOverlapping(enrollment))
    603                        return true;
    604                }
    605            }
    606            return false;
    607        }
    608
    609        /** First conflicting enrollment
    610         * @param idx index of request
    611         * @param enrollment enrollment in question
    612         * @return first conflicting enrollment
    613         **/
    614        public Enrollment firstConflict(int idx, Enrollment enrollment) {
    615            Set<Enrollment> conflicts = enrollment.variable().getModel().conflictValues(iCurrentAssignment, enrollment);
    616            if (conflicts.contains(enrollment))
    617                return enrollment;
    618            if (!conflicts.isEmpty()) {
    619                for (Enrollment conflict : conflicts) {
    620                    if (!conflict.getStudent().equals(iStudent))
    621                        return conflict;
    622                }
    623            }
    624            float credit = enrollment.getCredit();
    625            for (int i = 0; i < iAssignment.length; i++) {
    626                if (iAssignment[i] != null && i != idx) {
    627                    credit += iAssignment[i].getCredit();
    628                    if (credit > iStudent.getMaxCredit() || iAssignment[i].isOverlapping(enrollment))
    629                    return iAssignment[i];
    630                }
    631            }
    632            return null;
    633        }
    634
    635        /** True if the given request can be assigned
    636         * @param request given request
    637         * @param idx index of request
    638         * @return true if can be assigned
    639         **/
    640        public boolean canAssign(Request request, int idx) {
    641            if (iAssignment[idx] != null)
    642                return true;
    643            int alt = 0;
    644            int i = 0;
    645            float credit = 0;
    646            for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) {
    647                Request r = e.next();
    648                if (r.equals(request))
    649                    credit += r.getMinCredit();
    650                else if (iAssignment[i] != null)
    651                    credit += iAssignment[i].getCredit();
    652                if (r.equals(request))
    653                    continue;
    654                if (r.isAlternative()) {
    655                    if (iAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist()))
    656                        alt--;
    657                } else {
    658                    if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iAssignment[i] == null)
    659                        alt++;
    660                }
    661            }
    662            return (!request.isAlternative() || alt > 0) && (credit <= iStudent.getMaxCredit());
    663        }
    664
    665        /** Number of assigned requests in the current schedule
    666         * @return number of assigned requests
    667         **/
    668        public int getNrAssigned() {
    669            int assigned = 0;
    670            for (int i = 0; i < iAssignment.length; i++)
    671                if (iAssignment[i] != null)
    672                    assigned += (iAssignment[i].isCourseRequest() ? 10 : 1);
    673            return assigned;
    674        }
    675
    676        /** Returns true if the given request can be left unassigned
    677         * @param request given request
    678         * @return true if can be left unassigned
    679         **/
    680        protected boolean canLeaveUnassigned(Request request) {
    681            if (request instanceof CourseRequest && ((CourseRequest)request).getFixedValue() != null) return false;
    682            if (request.isMPP() && iModel.getKeepInitialAssignments()) return false;
    683            return true;
    684        }
    685
    686        /** Returns list of available enrollments for a course request
    687         * @param request given request
    688         * @return list of enrollments to consider
    689         **/
    690        protected List<Enrollment> values(final CourseRequest request) {
    691            List<Enrollment> values = request.getAvaiableEnrollments(iCurrentAssignment);
    692            Collections.sort(values, new Comparator<Enrollment>() {
    693
    694                private HashMap<Enrollment, Double> iValues = new HashMap<Enrollment, Double>();
    695
    696                private Double value(Enrollment e) {
    697                    Double value = iValues.get(e);
    698                    if (value == null) {
    699                        if (iModel.getStudentQuality() != null)
    700                            value = iModel.getStudentWeights().getWeight(iCurrentAssignment, e, iModel.getStudentQuality().conflicts(e));
    701                        else
    702                            value = iModel.getStudentWeights().getWeight(iCurrentAssignment, e,
    703                                    (iModel.getDistanceConflict() == null ? null : iModel.getDistanceConflict().conflicts(e)),
    704                                    (iModel.getTimeOverlaps() == null ? null : iModel.getTimeOverlaps().conflicts(e)));
    705                        iValues.put(e, value);
    706                    }
    707                    return value;
    708                }
    709
    710                @Override
    711                public int compare(Enrollment e1, Enrollment e2) {
    712                    if (e1.equals(e2)) return 0;
    713                    if (e1.equals(iCurrentAssignment.getValue(request))) return -1;
    714                    if (e2.equals(iCurrentAssignment.getValue(request))) return 1;
    715                    if (iTimesToAvoid != null) {
    716                        double o1 = 0.0, o2 = 0.0;
    717                        for (Section s : e1.getSections()) {
    718                            if (s.getTime() != null)
    719                                for (TimeToAvoid avoid : iTimesToAvoid) {
    720                                    if (avoid.priority() > e1.getRequest().getPriority())
    721                                        o1 += avoid.overlap(s.getTime());
    722                                }
    723                        }
    724                        for (Section s : e2.getSections()) {
    725                            if (s.getTime() != null)
    726                                for (TimeToAvoid avoid : iTimesToAvoid) {
    727                                    if (avoid.priority() > e2.getRequest().getPriority())
    728                                        o2 += avoid.overlap(s.getTime());
    729                                }
    730                        }
    731                        if (o1 < o2)
    732                            return -1;
    733                        if (o2 < o1)
    734                            return 1;
    735                    }
    736                    Double v1 = value(e1), v2 = value(e2);
    737                    return v1.equals(v2) ? e1.compareTo(iCurrentAssignment, e2) : v2.compareTo(v1);
    738                }
    739
    740            });
    741            return values;
    742        }
    743
    744        /** branch &amp; bound search
    745         * @param idx index of request
    746         **/
    747        public void backTrack(int idx) {
    748            if (sDebug)
    749                sLog.debug("backTrack(" + getNrAssigned() + "/" + getValue() + "," + idx + ")");
    750            if (iTimeout > 0 && (JProf.currentTimeMillis() - iT0) > iTimeout) {
    751                if (sDebug)
    752                    sLog.debug("  -- timeout reached");
    753                iTimeoutReached = true;
    754                return;
    755            }
    756            if (iMinimizePenalty) {
    757                if (getBestAssignment() != null
    758                        && (getNrAssignedBound(idx) < getBestNrAssigned() || (getNrAssignedBound(idx) == getBestNrAssigned() && getPenaltyBound(idx) >= getBestValue()))) {
    759                    if (sDebug)
    760                        sLog.debug("  -- branch number of assigned " + getNrAssignedBound(idx) + "<"
    761                                + getBestNrAssigned() + ", or penalty " + getPenaltyBound(idx) + ">=" + getBestValue());
    762                    return;
    763                }
    764                if (idx == iAssignment.length) {
    765                    if (getBestAssignment() == null
    766                            || (getNrAssigned() > getBestNrAssigned() || (getNrAssigned() == getBestNrAssigned() && getPenalty() < getBestValue()))) {
    767                        if (sDebug)
    768                            sLog.debug("  -- best solution found " + getNrAssigned() + "/" + getPenalty());
    769                        saveBest();
    770                    }
    771                    return;
    772                }
    773            } else {
    774                if (getBestAssignment() != null && getBound(idx) >= getBestValue()) {
    775                    if (sDebug)
    776                        sLog.debug("  -- branch " + getBound(idx) + " >= " + getBestValue());
    777                    return;
    778                }
    779                if (idx == iAssignment.length) {
    780                    if (getBestAssignment() == null || getValue() < getBestValue()) {
    781                        if (sDebug)
    782                            sLog.debug("  -- best solution found " + getNrAssigned() + "/" + getValue());
    783                        saveBest();
    784                    }
    785                    return;
    786                }
    787            }
    788
    789            Request request = iStudent.getRequests().get(idx);
    790            if (sDebug)
    791                sLog.debug("  -- request: " + request);
    792            if (!canAssign(request, idx)) {
    793                if (sDebug)
    794                    sLog.debug("    -- cannot assign");
    795                backTrack(idx + 1);
    796                return;
    797            }
    798            List<Enrollment> values = null;
    799            if (request instanceof CourseRequest) {
    800                CourseRequest courseRequest = (CourseRequest) request;
    801                if (courseRequest.getInitialAssignment() != null && iModel.isMPP()) {
    802                    Enrollment enrollment = courseRequest.getInitialAssignment();
    803                    if (!inConflict(idx, enrollment)) {
    804                        iAssignment[idx] = enrollment;
    805                        backTrack(idx + 1);
    806                        iAssignment[idx] = null;
    807                        return;
    808                    }
    809                }
    810                if (!courseRequest.getSelectedChoices().isEmpty()) {
    811                    if (sDebug)
    812                        sLog.debug("    -- selection among selected enrollments");
    813                    values = courseRequest.getSelectedEnrollments(iCurrentAssignment, true);
    814                    if (values != null && !values.isEmpty()) {
    815                        boolean hasNoConflictValue = false;
    816                        for (Enrollment enrollment : values) {
    817                            if (inConflict(idx, enrollment))
    818                                continue;
    819                            hasNoConflictValue = true;
    820                            if (sDebug)
    821                                sLog.debug("      -- nonconflicting enrollment found: " + enrollment);
    822                            iAssignment[idx] = enrollment;
    823                            backTrack(idx + 1);
    824                            iAssignment[idx] = null;
    825                        }
    826                        if (hasNoConflictValue && iBranchWhenSelectedHasNoConflict)
    827                            return;
    828                    }
    829                }
    830                values = iValues.get(courseRequest);
    831                if (values == null) {
    832                    values = values(courseRequest);
    833                    iValues.put(courseRequest, values);
    834                }
    835            } else {
    836                values = request.computeEnrollments(iCurrentAssignment);
    837            }
    838            if (sDebug) {
    839                sLog.debug("  -- nrValues: " + values.size());
    840                int vIdx = 1;
    841                for (Enrollment enrollment : values) {
    842                    if (sDebug)
    843                        sLog.debug("    -- [" + vIdx + "]: " + enrollment);
    844                }
    845            }
    846            boolean hasNoConflictValue = false;
    847            for (Enrollment enrollment : values) {
    848                if (sDebug)
    849                    sLog.debug("    -- enrollment: " + enrollment);
    850                if (sDebug) {
    851                    Enrollment conflict = firstConflict(idx, enrollment);
    852                    if (conflict != null) {
    853                        sLog.debug("        -- in conflict with: " + conflict);
    854                        continue;
    855                    }
    856                } else {
    857                    if (inConflict(idx, enrollment)) continue;
    858                }
    859                hasNoConflictValue = true;
    860                iAssignment[idx] = enrollment;
    861                backTrack(idx + 1);
    862                iAssignment[idx] = null;
    863            }
    864            if (canLeaveUnassigned(request) && (!hasNoConflictValue || request instanceof CourseRequest))
    865                backTrack(idx + 1);
    866        }
    867    }
    868
    869    /** Branch &amp; bound neighbour -- a schedule of a student */
    870    public static class BranchBoundNeighbour implements Neighbour<Request, Enrollment> {
    871        private double iValue;
    872        private Enrollment[] iAssignment;
    873        private Student iStudent;
    874
    875        /**
    876         * Constructor
    877         *
    878         * @param student selected student
    879         * @param value
    880         *            value of the schedule
    881         * @param assignment
    882         *            enrollments of student's requests
    883         */
    884        public BranchBoundNeighbour(Student student, double value, Enrollment[] assignment) {
    885            iValue = value;
    886            iAssignment = assignment;
    887            iStudent = student;
    888        }
    889
    890        @Override
    891        public double value(Assignment<Request, Enrollment> assignment) {
    892            return iValue;
    893        }
    894
    895        /** Assignment
    896         * @return an enrollment for each request of the student
    897         **/
    898        public Enrollment[] getAssignment() {
    899            return iAssignment;
    900        }
    901
    902        /** Student
    903         * @return selected student
    904         **/
    905        public Student getStudent() {
    906            return iStudent;
    907        }
    908
    909        /** Assign the schedule */
    910        @Override
    911        public void assign(Assignment<Request, Enrollment> assignment, long iteration) {
    912            for (int i = 0; i < iAssignment.length; i++)
    913                assignment.unassign(iteration, iStudent.getRequests().get(i), iAssignment[i]);
    914            for (int i = 0; i < iAssignment.length; i++)
    915                if (iAssignment[i] != null)
    916                    assignment.assign(iteration, iAssignment[i]);
    917        }
    918
    919        @Override
    920        public String toString() {
    921            StringBuffer sb = new StringBuffer("B&B{ " + iStudent + " " + sDF.format(-iValue * 100.0) + "%");
    922            int idx = 0;
    923            for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); idx++) {
    924                Request request = e.next();
    925                sb.append("\n  " + request);
    926                Enrollment enrollment = iAssignment[idx];
    927                if (enrollment == null)
    928                    sb.append("  -- not assigned");
    929                else
    930                    sb.append("  -- " + enrollment);
    931            }
    932            sb.append("\n}");
    933            return sb.toString();
    934        }
    935
    936        @Override
    937        public Map<Request, Enrollment> assignments() {
    938            Map<Request, Enrollment> ret = new HashMap<Request, Enrollment>();
    939            for (int i = 0; i < iAssignment.length; i++)
    940                ret.put(iStudent.getRequests().get(i), iAssignment[i]);
    941            return ret;
    942        }
    943
    944    }
    945
    946    @Override
    947    public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) {
    948        if (iNbrIterations > 0)
    949            info.put("Timing of " + getClass().getSimpleName(), sDF.format(((double)iTotalTime) / iNbrIterations) + " ms/it (" +
    950                    iNbrIterations + " iterations, " +
    951                    (iNbrNoSolution == 0 ? "" : sDF.format(100.0 * iNbrNoSolution / iNbrIterations) + "% no solution, ") +
    952                    sDF.format(100.0 * iNbrTimeoutReached / iNbrIterations) + "% time limit of " + sDF.format(iTimeout / 1000.0) + " seconds reached)");
    953    }
    954
    955    @Override
    956    public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) {
    957    }
    958
    959    /**
    960     * Only consider students meeting the given filter.
    961     */
    962    public StudentFilter getFilter() { return iFilter; }
    963
    964    /**
    965     * Only consider students meeting the given filter.
    966     */
    967    public BranchBoundSelection withFilter(StudentFilter filter) { iFilter = filter; return this; }
    968
    969    @Override
    970    public boolean variableSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable) {
    971        return false;
    972    }
    973    @Override
    974    public boolean valueSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable, Enrollment value) {
    975        return false;
    976    }
    977    @Override
    978    public boolean neighbourSelected(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) {
    979        return false;
    980    }
    981    @Override
    982    public void neighbourFailed(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) {
    983        if (neighbour instanceof BranchBoundNeighbour)
    984            addStudent(((BranchBoundNeighbour)neighbour).getStudent());
    985    }
    986}