001package org.cpsolver.studentsct.online.selection;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.Comparator;
006import java.util.HashMap;
007import java.util.Hashtable;
008import java.util.Iterator;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012
013import org.cpsolver.ifs.assignment.Assignment;
014import org.cpsolver.ifs.model.GlobalConstraint;
015import org.cpsolver.ifs.util.DataProperties;
016import org.cpsolver.ifs.util.JProf;
017import org.cpsolver.studentsct.constraint.DependentCourses;
018import org.cpsolver.studentsct.constraint.LinkedSections;
019import org.cpsolver.studentsct.heuristics.selection.OnlineSelection;
020import org.cpsolver.studentsct.heuristics.selection.BranchBoundSelection.BranchBoundNeighbour;
021import org.cpsolver.studentsct.model.Config;
022import org.cpsolver.studentsct.model.CourseRequest;
023import org.cpsolver.studentsct.model.Enrollment;
024import org.cpsolver.studentsct.model.FreeTimeRequest;
025import org.cpsolver.studentsct.model.Request;
026import org.cpsolver.studentsct.model.Section;
027import org.cpsolver.studentsct.model.Student;
028import org.cpsolver.studentsct.model.Subpart;
029import org.cpsolver.studentsct.online.OnlineSectioningModel;
030
031/**
032 * Online student sectioning algorithm using multi-criteria selection. It is very
033 * similar to the {@link SuggestionSelection}, however, the {link {@link OnlineSelection}
034 * is used to compare two solutions and for branching.
035 * 
036 * @author  Tomáš Müller
037 * @version StudentSct 1.3 (Student Sectioning)<br>
038 *          Copyright (C) 2014 Tomáš Müller<br>
039 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
040 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
041 * <br>
042 *          This library is free software; you can redistribute it and/or modify
043 *          it under the terms of the GNU Lesser General Public License as
044 *          published by the Free Software Foundation; either version 3 of the
045 *          License, or (at your option) any later version. <br>
046 * <br>
047 *          This library is distributed in the hope that it will be useful, but
048 *          WITHOUT ANY WARRANTY; without even the implied warranty of
049 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
050 *          Lesser General Public License for more details. <br>
051 * <br>
052 *          You should have received a copy of the GNU Lesser General Public
053 *          License along with this library; if not see <a
054 *          href='http://www.gnu.org/licenses'>http://www.gnu.org/licenses</a>.
055 * 
056 */
057public class MultiCriteriaBranchAndBoundSelection implements OnlineSectioningSelection {
058    protected int iTimeout = 1000;
059    protected OnlineSectioningModel iModel = null;
060    protected Assignment<Request, Enrollment> iAssignment = null;
061    protected SelectionCriterion iComparator = null;
062    private boolean iPriorityWeighting = true;
063    protected boolean iBranchWhenSelectedHasNoConflict = false;
064    private double iMaxOverExpected = -1.0;
065
066    /** Student */
067    protected Student iStudent;
068    /** Start time */
069    protected long iT0;
070    /** End time */
071    protected long iT1;
072    /** Was timeout reached */
073    protected boolean iTimeoutReached;
074    /** Current assignment */
075    protected Enrollment[] iCurrentAssignment;
076    /** Best assignment */
077    protected Enrollment[] iBestAssignment;
078    /** Value cache */
079    protected HashMap<CourseRequest, List<Enrollment>> iValues;
080
081    private Set<FreeTimeRequest> iRequiredFreeTimes;
082    private Hashtable<CourseRequest, Set<Section>> iPreferredSections;
083    private Hashtable<CourseRequest, Config> iRequiredConfig = new Hashtable<CourseRequest, Config>();
084    private Hashtable<CourseRequest, Hashtable<Subpart, Section>> iRequiredSection = new Hashtable<CourseRequest, Hashtable<Subpart, Section>>();
085    private Set<CourseRequest> iRequiredUnassinged = null;
086
087    public MultiCriteriaBranchAndBoundSelection(DataProperties config) {
088        iTimeout = config.getPropertyInt("Neighbour.BranchAndBoundTimeout", iTimeout);
089        iPriorityWeighting = config.getPropertyBoolean("StudentWeights.PriorityWeighting", iPriorityWeighting);
090        iBranchWhenSelectedHasNoConflict = config.getPropertyBoolean("Students.BranchWhenSelectedHasNoConflict", iBranchWhenSelectedHasNoConflict);
091    }
092
093    @Override
094    public void setModel(OnlineSectioningModel model) {
095        iModel = model;
096    }
097
098    @Override
099    public void setPreferredSections(Hashtable<CourseRequest, Set<Section>> preferredSections) {
100        iPreferredSections = preferredSections;
101    }
102
103    public void setTimeout(int timeout) {
104        iTimeout = timeout;
105    }
106
107    @Override
108    public void setRequiredSections(Hashtable<CourseRequest, Set<Section>> requiredSections) {
109        if (requiredSections != null) {
110            for (Map.Entry<CourseRequest, Set<Section>> entry : requiredSections.entrySet()) {
111                Hashtable<Subpart, Section> subSection = new Hashtable<Subpart, Section>();
112                iRequiredSection.put(entry.getKey(), subSection);
113                for (Section section : entry.getValue()) {
114                    if (subSection.isEmpty())
115                        iRequiredConfig.put(entry.getKey(), section.getSubpart().getConfig());
116                    subSection.put(section.getSubpart(), section);
117                }
118            }
119        }
120    }
121
122    @Override
123    public void setRequiredFreeTimes(Set<FreeTimeRequest> requiredFreeTimes) {
124        iRequiredFreeTimes = requiredFreeTimes;
125    }
126
127    public BranchBoundNeighbour select(Assignment<Request, Enrollment> assignment, Student student,
128            SelectionCriterion comparator) {
129        iStudent = student;
130        iComparator = comparator;
131        iAssignment = assignment;
132        return select();
133    }
134
135    @Override
136    public BranchBoundNeighbour select(Assignment<Request, Enrollment> assignment, Student student) {
137        SelectionCriterion comparator = null;
138        if (iPriorityWeighting)
139            comparator = new OnlineSectioningCriterion(student, iModel, assignment, iPreferredSections);
140        else
141            comparator = new EqualWeightCriterion(student, iModel, assignment, iPreferredSections);
142        return select(assignment, student, comparator);
143    }
144
145    /**
146     * Execute branch &amp; bound, return the best found schedule for the selected
147     * student.
148     */
149    public BranchBoundNeighbour select() {
150        iT0 = JProf.currentTimeMillis();
151        iTimeoutReached = false;
152        iCurrentAssignment = new Enrollment[iStudent.getRequests().size()];
153        iBestAssignment = null;
154
155        int i = 0;
156        for (Request r : iStudent.getRequests())
157            iCurrentAssignment[i++] = iAssignment.getValue(r);
158        saveBest();
159        for (int j = 0; j < iCurrentAssignment.length; j++)
160            iCurrentAssignment[j] = null;
161
162        iValues = new HashMap<CourseRequest, List<Enrollment>>();
163        backTrack(0);
164        iT1 = JProf.currentTimeMillis();
165        if (iBestAssignment == null)
166            return null;
167
168        return new BranchBoundNeighbour(iStudent, iComparator.getTotalWeight(iAssignment, iBestAssignment),
169                iBestAssignment);
170    }
171
172    /** Was timeout reached */
173    public boolean isTimeoutReached() {
174        return iTimeoutReached;
175    }
176
177    /** Time (in milliseconds) the branch &amp; bound did run */
178    public long getTime() {
179        return iT1 - iT0;
180    }
181
182    /** Save the current schedule as the best */
183    public void saveBest() {
184        if (iBestAssignment == null)
185            iBestAssignment = new Enrollment[iCurrentAssignment.length];
186        for (int i = 0; i < iCurrentAssignment.length; i++)
187            iBestAssignment[i] = iCurrentAssignment[i];
188    }
189
190    /** True if the enrollment is conflicting */
191    public boolean inConflict(final int idx, final Enrollment enrollment) {
192        for (GlobalConstraint<Request, Enrollment> constraint : enrollment.variable().getModel().globalConstraints()) {
193            if (constraint instanceof DependentCourses) {
194                if (((DependentCourses)constraint).isPartialScheduleInConflict(iStudent, new LinkedSections.EnrollmentAssignment() {
195                    @Override
196                    public Enrollment getEnrollment(Request request, int index) {
197                        return (index == idx ? enrollment : iCurrentAssignment[index]);
198                    }
199                }, idx)) return true;
200            } else if (constraint.inConflict(iAssignment, enrollment))
201                return true;
202        }
203        for (LinkedSections linkedSections : iStudent.getLinkedSections()) {
204            if (linkedSections.inConflict(enrollment, new LinkedSections.EnrollmentAssignment() {
205                @Override
206                public Enrollment getEnrollment(Request request, int index) {
207                    return (index == idx ? enrollment : iCurrentAssignment[index]);
208                }
209            }) != null)
210                return true;
211        }
212        float credit = enrollment.getCredit();
213        for (int i = 0; i < iCurrentAssignment.length; i++) {
214            if (iCurrentAssignment[i] != null && i != idx) {
215                credit += iCurrentAssignment[i].getCredit();
216                if (credit > iStudent.getMaxCredit() || iCurrentAssignment[i].isOverlapping(enrollment))
217                    return true;
218            }
219        }
220        if (iMaxOverExpected >= 0.0) {
221            double penalty = 0.0;
222            for (int i = 0; i < idx; i++) {
223                if (iCurrentAssignment[i] != null && iCurrentAssignment[i].getAssignments() != null && iCurrentAssignment[i].isCourseRequest())
224                    for (Section section: iCurrentAssignment[i].getSections())
225                        penalty += iModel.getOverExpected(iAssignment, iCurrentAssignment, i, section, iCurrentAssignment[i].getRequest());
226            }
227            if (enrollment.isCourseRequest())
228                for (Section section: enrollment.getSections())
229                    penalty += iModel.getOverExpected(iAssignment, iCurrentAssignment, idx, section, enrollment.getRequest());
230            if (penalty > iMaxOverExpected) return true;
231        }
232        return !isAllowed(idx, enrollment);
233    }
234
235    /** True if the given request can be assigned */
236    public boolean canAssign(Request request, int idx) {
237        if (iCurrentAssignment[idx] != null)
238            return true;
239        int alt = 0;
240        int i = 0;
241        float credit = 0;
242        for (Iterator<Request> e = iStudent.getRequests().iterator(); e.hasNext(); i++) {
243            Request r = e.next();
244            if (r.equals(request))
245                credit += r.getMinCredit();
246            else if (iCurrentAssignment[i] != null)
247                credit += iCurrentAssignment[i].getCredit();
248            if (r.equals(request))
249                continue;
250            if (r.isAlternative()) {
251                if (iCurrentAssignment[i] != null || (r instanceof CourseRequest && ((CourseRequest) r).isWaitlist()))
252                    alt--;
253            } else {
254                if (r instanceof CourseRequest && !((CourseRequest) r).isWaitlist() && iCurrentAssignment[i] == null)
255                    alt++;
256            }
257        }
258        return (!request.isAlternative() || alt > 0) && (credit <= request.getStudent().getMaxCredit());
259    }
260
261    public boolean isAllowed(int idx, Enrollment enrollment) {
262        if (enrollment.isCourseRequest()) {
263            CourseRequest request = (CourseRequest) enrollment.getRequest();
264            if (iRequiredUnassinged != null && iRequiredUnassinged.contains(request)) return false;
265            Config reqConfig = iRequiredConfig.get(request);
266            if (reqConfig != null) {
267                if (!reqConfig.equals(enrollment.getConfig()))
268                    return false;
269                Hashtable<Subpart, Section> reqSections = iRequiredSection.get(request);
270                for (Section section : enrollment.getSections()) {
271                    Section reqSection = reqSections.get(section.getSubpart());
272                    if (reqSection == null)
273                        continue;
274                    if (!section.equals(reqSection))
275                        return false;
276                }
277            }
278        } else if (iRequiredFreeTimes.contains(enrollment.getRequest())) {
279            if (enrollment.getAssignments() == null || enrollment.getAssignments().isEmpty())
280                return false;
281        }
282        return true;
283    }
284
285    /** Returns true if the given request can be left unassigned */
286    protected boolean canLeaveUnassigned(Request request) {
287        if (request instanceof CourseRequest) {
288            if (iRequiredConfig.get(request) != null)
289                return false;
290        } else if (iRequiredFreeTimes.contains(request))
291            return false;
292        if (iModel.getDependentCoursesConstraint() != null &&
293            !iModel.getDependentCoursesConstraint().canLeaveUnassigned(iStudent, new LinkedSections.EnrollmentAssignment() {
294                @Override
295                public Enrollment getEnrollment(Request r, int index) {
296                    return iCurrentAssignment[index];
297                }
298            }, request)) return false;
299        return true;
300    }
301
302    /** Returns list of available enrollments for a course request */
303    protected List<Enrollment> values(final CourseRequest request) {
304        if (iRequiredUnassinged != null && iRequiredUnassinged.contains(request))
305            return new ArrayList<Enrollment>();
306        List<Enrollment> values = request.getAvaiableEnrollments(iAssignment, false);
307        Collections.sort(values, new Comparator<Enrollment>() {
308            @Override
309            public int compare(Enrollment o1, Enrollment o2) {
310                return iComparator.compare(iAssignment, o1, o2);
311            }
312        });
313        return values;
314    }
315
316    /** branch &amp; bound search */
317    public void backTrack(int idx) {
318        if (iTimeout > 0 && (JProf.currentTimeMillis() - iT0) > iTimeout) {
319            iTimeoutReached = true;
320            return;
321        }
322        if (idx == iCurrentAssignment.length) {
323            if (iBestAssignment == null || iComparator.compare(iAssignment, iCurrentAssignment, iBestAssignment) < 0)
324                saveBest();
325            return;
326        } else if (iBestAssignment != null
327                && !iComparator.canImprove(iAssignment, idx, iCurrentAssignment, iBestAssignment)) {
328            return;
329        }
330
331        Request request = iStudent.getRequests().get(idx);
332        if (!canAssign(request, idx)) {
333            backTrack(idx + 1);
334            return;
335        }
336
337        List<Enrollment> values = null;
338        if (request instanceof CourseRequest) {
339            CourseRequest courseRequest = (CourseRequest) request;
340            if (!courseRequest.getSelectedChoices().isEmpty()) {
341                values = courseRequest.getSelectedEnrollments(iAssignment, true);
342                if (values != null && !values.isEmpty()) {
343                    boolean hasNoConflictValue = false;
344                    for (Enrollment enrollment : values) {
345                        if (inConflict(idx, enrollment))
346                            continue;
347                        hasNoConflictValue = true;
348                        iCurrentAssignment[idx] = enrollment;
349                        backTrack(idx + 1);
350                        iCurrentAssignment[idx] = null;
351                    }
352                    if (hasNoConflictValue && iBranchWhenSelectedHasNoConflict)
353                        return;
354                }
355            }
356            values = iValues.get(courseRequest);
357            if (values == null) {
358                values = values(courseRequest);
359                iValues.put(courseRequest, values);
360            }
361        } else {
362            values = request.computeEnrollments(iAssignment);
363        }
364
365        boolean hasNoConflictValue = false;
366        for (Enrollment enrollment : values) {
367            if (inConflict(idx, enrollment))
368                continue;
369            hasNoConflictValue = true;
370            iCurrentAssignment[idx] = enrollment;
371            backTrack(idx + 1);
372            iCurrentAssignment[idx] = null;
373        }
374
375        if (canLeaveUnassigned(request) || (!hasNoConflictValue && request instanceof CourseRequest))
376            backTrack(idx + 1);
377    }
378
379    /**
380     * Enrollment comparator
381     */
382    public interface SelectionComparator {
383        /**
384         * Compare two enrollments
385         * 
386         * @param assignment
387         *            current assignment
388         * @param e1
389         *            first enrollment
390         * @param e2
391         *            second enrollment
392         * @return -1 if the first enrollment is better than the second etc.
393         */
394        public int compare(Assignment<Request, Enrollment> assignment, Enrollment e1, Enrollment e2);
395    }
396
397    /**
398     * Multi-criteria selection interface.
399     */
400    public interface SelectionCriterion extends SelectionComparator {
401        /**
402         * Compare two solutions
403         * 
404         * @param assignment
405         *            current assignment
406         * @param current
407         *            current solution
408         * @param best
409         *            best known solution
410         * @return true if current solution is better than the best one
411         */
412        public int compare(Assignment<Request, Enrollment> assignment, Enrollment[] current, Enrollment[] best);
413
414        /**
415         * Bound
416         * 
417         * @param assignment
418         *            current assignment
419         * @param idx
420         *            current request index
421         * @param current
422         *            current solution
423         * @param best
424         *            best known solution
425         * @return if the current solution can be extended and possibly improve
426         *         upon the best known solution
427         */
428        public boolean canImprove(Assignment<Request, Enrollment> assignment, int idx, Enrollment[] current,
429                Enrollment[] best);
430
431        /**
432         * For backward compatibility, return a weighted sum
433         * 
434         * @param assignment
435         *            current assignment
436         * @param enrollments
437         *            current solution
438         * @return solution weight (weighted sum)
439         */
440        public double getTotalWeight(Assignment<Request, Enrollment> assignment, Enrollment[] enrollments);
441    }
442
443    @Override
444    public void setRequiredUnassinged(Set<CourseRequest> requiredUnassignedRequests) {
445        iRequiredUnassinged = requiredUnassignedRequests;
446    }
447
448    @Override
449    public void setMaxOverExpected(double maxOverExpected) {
450        iMaxOverExpected = maxOverExpected;
451    }
452}