001package org.cpsolver.studentsct.extension;
002
003import java.util.ArrayList;
004import java.util.BitSet;
005import java.util.Collection;
006import java.util.HashMap;
007import java.util.HashSet;
008import java.util.Iterator;
009import java.util.List;
010import java.util.Map;
011import java.util.Set;
012
013import org.apache.logging.log4j.Logger;
014import org.cpsolver.coursett.Constants;
015import org.cpsolver.coursett.model.Placement;
016import org.cpsolver.coursett.model.RoomLocation;
017import org.cpsolver.coursett.model.TimeLocation;
018import org.cpsolver.ifs.assignment.Assignment;
019import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
020import org.cpsolver.ifs.assignment.context.CanInheritContext;
021import org.cpsolver.ifs.assignment.context.ExtensionWithContext;
022import org.cpsolver.ifs.model.InfoProvider;
023import org.cpsolver.ifs.model.ModelListener;
024import org.cpsolver.ifs.solver.Solver;
025import org.cpsolver.ifs.util.DataProperties;
026import org.cpsolver.ifs.util.DistanceMetric;
027import org.cpsolver.studentsct.StudentSectioningModel;
028import org.cpsolver.studentsct.StudentSectioningModel.StudentSectioningModelContext;
029import org.cpsolver.studentsct.model.CourseRequest;
030import org.cpsolver.studentsct.model.Enrollment;
031import org.cpsolver.studentsct.model.FreeTimeRequest;
032import org.cpsolver.studentsct.model.Request;
033import org.cpsolver.studentsct.model.SctAssignment;
034import org.cpsolver.studentsct.model.Section;
035import org.cpsolver.studentsct.model.Student;
036import org.cpsolver.studentsct.model.Student.BackToBackPreference;
037import org.cpsolver.studentsct.model.Student.ModalityPreference;
038
039import org.cpsolver.studentsct.model.Unavailability;
040
041/**
042 * This extension computes student schedule quality using various matrices.
043 * It replaces {@link TimeOverlapsCounter} and {@link DistanceConflict} extensions.
044 * Besides of time and distance conflicts, it also counts cases when a student
045 * has a lunch break conflict, travel time during the day, it can prefer
046 * or discourage student class back-to-back and cases when a student has more than
047 * a given number of hours between the first and the last class on a day.
048 * See {@link StudentQuality.Type} for more details.
049 * 
050 * <br>
051 * <br>
052 * 
053 * @version StudentSct 1.3 (Student Sectioning)<br>
054 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
055 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
056 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
057 * <br>
058 *          This library is free software; you can redistribute it and/or modify
059 *          it under the terms of the GNU Lesser General Public License as
060 *          published by the Free Software Foundation; either version 3 of the
061 *          License, or (at your option) any later version. <br>
062 * <br>
063 *          This library is distributed in the hope that it will be useful, but
064 *          WITHOUT ANY WARRANTY; without even the implied warranty of
065 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
066 *          Lesser General Public License for more details. <br>
067 * <br>
068 *          You should have received a copy of the GNU Lesser General Public
069 *          License along with this library; if not see
070 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
071 */
072
073public class StudentQuality extends ExtensionWithContext<Request, Enrollment, StudentQuality.StudentQualityContext> implements ModelListener<Request, Enrollment>, CanInheritContext<Request, Enrollment, StudentQuality.StudentQualityContext>, InfoProvider<Request, Enrollment> {
074    private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(StudentQuality.class);
075    private Context iContext;
076    
077    /**
078     * Constructor
079     * @param solver student scheduling solver
080     * @param properties solver configuration
081     */
082    public StudentQuality(Solver<Request, Enrollment> solver, DataProperties properties) {
083        super(solver, properties);
084        if (solver != null) {
085            StudentSectioningModel model = (StudentSectioningModel) solver.currentSolution().getModel(); 
086            iContext = new Context(model.getDistanceMetric(), properties);
087            model.setStudentQuality(this, false);
088        } else {
089            iContext = new Context(null, properties);
090        }
091    }
092    
093    /**
094     * Constructor
095     * @param metrics distance metric
096     * @param properties solver configuration
097     */
098    public StudentQuality(DistanceMetric metrics, DataProperties properties) {
099        super(null, properties);
100        iContext = new Context(metrics, properties);
101    }
102    
103    /**
104     * Current distance metric
105     * @return distance metric
106     */
107    public DistanceMetric getDistanceMetric() {
108        return iContext.getDistanceMetric();
109    }
110    
111    /**
112     * Is debugging enabled
113     * @return true when StudentQuality.Debug is true
114     */
115    public boolean isDebug() {
116        return iContext.isDebug();
117    }
118    
119    /**
120     * Student quality context
121     */
122    public Context getStudentQualityContext() {
123        return iContext;
124    }
125    
126    /**
127     * Weighting types 
128     */
129    public static enum WeightType {
130        /** Penalty is incurred on the request with higher priority */
131        HIGHER,
132        /** Penalty is incurred on the request with lower priority */
133        LOWER,
134        /** Penalty is incurred on both requests */
135        BOTH,
136        /** Penalty is incurred on the course request (for conflicts between course request and a free time) */
137        REQUEST,
138        ;
139    }
140    
141    /**
142     * Measured student qualities
143     *
144     */
145    public static enum Type {
146        /** 
147         * Time conflicts between two classes that is allowed. Time conflicts are penalized as shared time
148         * between two course requests proportional to the time of each, capped at one half of the time.
149         * This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5.
150         */
151        CourseTimeOverlap(WeightType.BOTH, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){
152            @Override
153            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
154                return r1 instanceof CourseRequest && r2 instanceof CourseRequest;
155            }
156
157            @Override
158            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
159                if (a1.getTime() == null || a2.getTime() == null) return false;
160                if (((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false;
161                return a1.getTime().hasIntersection(a2.getTime());
162            }
163
164            @Override
165            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
166                if (!inConflict(cx, a1, a2)) return 0;
167                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
168            }
169            
170            @Override
171            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
172                return new Nothing();
173            }
174
175            @Override
176            public double getWeight(Context cx, Conflict c, Enrollment e) {
177                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / e.getNrSlots(), cx.getTimeOverlapMaxLimit());
178            }
179        }),
180        /** 
181         * Time conflict between class and a free time request. Free time conflicts are penalized as the time
182         * of a course request overlapping with a free time proportional to the time of the request, capped at one half
183         * of the time. This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5.
184         */
185        FreeTimeOverlap(WeightType.REQUEST, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){
186            @Override
187            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
188                return false;
189            }
190
191            @Override
192            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
193                if (a1.getTime() == null || a2.getTime() == null) return false;
194                return a1.getTime().hasIntersection(a2.getTime());
195            }
196
197            @Override
198            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
199                if (!inConflict(cx, a1, a2)) return 0;
200                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
201            }
202            
203            @Override
204            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
205                return (e.isCourseRequest() ? new FreeTimes(e.getStudent()) : new Nothing());
206            }
207            
208            @Override
209            public double getWeight(Context cx, Conflict c, Enrollment e) {
210                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit());
211            }
212        }),
213        /** 
214         * Student unavailability conflict. Time conflict between a class that the student is taking and a class that the student
215         * is teaching (if time conflicts are allowed). Unavailability conflicts are penalized as the time
216         * of a course request overlapping with an unavailability proportional to the time of the request, capped at one half
217         * of the time. This criterion is weighted by StudentWeights.TimeOverlapFactor, defaulting to 0.5.
218         */
219        Unavailability(WeightType.REQUEST, "StudentWeights.TimeOverlapFactor", 0.5000, new Quality(){
220            @Override
221            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
222                return false;
223            }
224
225            @Override
226            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
227                if (a1.getTime() == null || a2.getTime() == null) return false;
228                return a1.getTime().hasIntersection(a2.getTime());
229            }
230
231            @Override
232            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
233                if (!inConflict(cx, a1, a2)) return 0;
234                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
235            }
236
237            @Override
238            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
239                return (e.isCourseRequest() ? new Unavailabilities(e.getStudent()) : new Nothing());
240            }
241            
242            @Override
243            public double getWeight(Context cx, Conflict c, Enrollment e) {
244                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit());
245            }
246        }),
247        /**
248         * Distance conflict. When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to false,
249         * distance conflicts are only considered between back-to-back classes (break time of the first 
250         * class is shorter than the distance in minutes between the two classes). When 
251         * Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to true, the distance between the
252         * two classes is also considered.
253         * This criterion is weighted by StudentWeights.DistanceConflict, defaulting to 0.01.
254         */
255        Distance(WeightType.LOWER, "StudentWeights.DistanceConflict", 0.0100, new Quality(){
256            @Override
257            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
258                return r1 instanceof CourseRequest && r2 instanceof CourseRequest;
259            }
260
261            @Override
262            public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) {
263                Section s1 = (Section) sa1;
264                Section s2 = (Section) sa2;
265                if (s1.getPlacement() == null || s2.getPlacement() == null)
266                    return false;
267                TimeLocation t1 = s1.getTime();
268                TimeLocation t2 = s2.getTime();
269                if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
270                    return false;
271                int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
272                if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
273                    if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
274                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
275                        if (dist > t1.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()))
276                            return true;
277                    } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
278                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
279                        if (dist > t2.getBreakTime() + Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()))
280                            return true;
281                    }
282                } else {
283                    if (a1 + t1.getNrSlotsPerMeeting() == a2) {
284                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
285                        if (dist > t1.getBreakTime())
286                            return true;
287                    } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
288                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
289                        if (dist > t2.getBreakTime())
290                            return true;
291                    }
292                }
293                return false;
294            }
295
296            @Override
297            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
298                return inConflict(cx, a1, a2) ? 1 : 0;
299            }
300
301            @Override
302            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
303                return new Nothing();
304            }
305            
306            @Override
307            public double getWeight(Context cx, Conflict c, Enrollment e) {
308                return c.getPenalty();
309            }
310        }),
311        /**
312         * Short distance conflict. Similar to distance conflicts but for students that require short
313         * distances. When Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to false,
314         * distance conflicts are only considered between back-to-back classes (travel time between the
315         * two classes is more than zero minutes). When 
316         * Distances.ComputeDistanceConflictsBetweenNonBTBClasses is set to true, the distance between the
317         * two classes is also considered (break time is also ignored).
318         * This criterion is weighted by StudentWeights.ShortDistanceConflict, defaulting to 0.1.
319         */
320        ShortDistance(WeightType.LOWER, "StudentWeights.ShortDistanceConflict", 0.1000, new Quality(){
321            @Override
322            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
323                return student.isNeedShortDistances() && r1 instanceof CourseRequest && r2 instanceof CourseRequest;
324            }
325
326            @Override
327            public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) {
328                Section s1 = (Section) sa1;
329                Section s2 = (Section) sa2;
330                if (s1.getPlacement() == null || s2.getPlacement() == null)
331                    return false;
332                TimeLocation t1 = s1.getTime();
333                TimeLocation t2 = s2.getTime();
334                if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
335                    return false;
336                int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
337                if (cx.getDistanceMetric().doComputeDistanceConflictsBetweenNonBTBClasses()) {
338                    if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
339                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
340                        if (dist > Constants.SLOT_LENGTH_MIN * (a2 - a1 - t1.getLength()))
341                            return true;
342                    } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
343                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
344                        if (dist > Constants.SLOT_LENGTH_MIN * (a1 - a2 - t2.getLength()))
345                            return true;
346                    }
347                } else {
348                    if (a1 + t1.getNrSlotsPerMeeting() == a2) {
349                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
350                        if (dist > 0) return true;
351                    } else if (a2 + t2.getNrSlotsPerMeeting() == a1) {
352                        int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
353                        if (dist > 0) return true;
354                    }
355                }
356                return false;
357            }
358
359            @Override
360            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
361                return inConflict(cx, a1, a2) ? 1 : 0;
362            }
363
364            @Override
365            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
366                return new Nothing();
367            }
368            
369            @Override
370            public double getWeight(Context cx, Conflict c, Enrollment e) {
371                return c.getPenalty();
372            }
373        }),
374        /**
375         * Naive, yet effective approach for modeling student lunch breaks. It creates a conflict whenever there are
376         * two classes (of a student) overlapping with the lunch time which are one after the other with a break in
377         * between smaller than the requested lunch break. Lunch time is defined by StudentLunch.StartSlot and
378         * StudentLunch.EndStart properties (default is 11:00 am - 1:30 pm), with lunch break of at least
379         * StudentLunch.Length slots (default is 30 minutes). Such a conflict is weighted
380         * by StudentWeights.LunchBreakFactor, which defaults to 0.005.
381         */
382        LunchBreak(WeightType.BOTH, "StudentWeights.LunchBreakFactor", 0.0050, new Quality() {
383            @Override
384            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
385                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy();
386            }
387
388            @Override
389            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
390                if (a1.getTime() == null || a2.getTime() == null) return false;
391                if (((Section)a1).isToIgnoreStudentConflictsWith(a2.getId())) return false;
392                if (a1.getTime().hasIntersection(a2.getTime())) return false;
393                TimeLocation t1 = a1.getTime(), t2 = a2.getTime();
394                if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) return false;
395                int s1 = t1.getStartSlot(), s2 = t2.getStartSlot();
396                int e1 = t1.getStartSlot() + t1.getNrSlotsPerMeeting(), e2 = t2.getStartSlot() + t2.getNrSlotsPerMeeting();
397                if (e1 + cx.getLunchLength() > s2 && e2 + cx.getLunchLength() > s1 && e1 > cx.getLunchStart() && cx.getLunchEnd() > s1 && e2 > cx.getLunchStart() && cx.getLunchEnd() > s2)
398                    return true;
399                return false;
400            }
401
402            @Override
403            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
404                if (!inConflict(cx, a1, a2)) return 0;
405                return a1.getTime().nrSharedDays(a2.getTime());
406            }
407            
408            @Override
409            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
410                return new Nothing();
411            }
412
413            @Override
414            public double getWeight(Context cx, Conflict c, Enrollment e) {
415                return c.getPenalty();
416            }
417        }),
418        /**
419         * Naive, yet effective approach for modeling travel times. A conflict with the penalty
420         * equal to the distance in minutes occurs when two classes are less than TravelTime.MaxTravelGap
421         * time slots a part (defaults 1 hour), or when they are less then twice as long apart 
422         * and the travel time is longer than the break time of the first class.
423         * Such a conflict is weighted by StudentWeights.TravelTimeFactor, which defaults to 0.001.
424         */
425        TravelTime(WeightType.BOTH, "StudentWeights.TravelTimeFactor", 0.0010, new Quality() {
426            @Override
427            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
428                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy();
429            }
430
431            @Override
432            public boolean inConflict(Context cx, SctAssignment sa1, SctAssignment sa2) {
433                Section s1 = (Section) sa1;
434                Section s2 = (Section) sa2;
435                if (s1.getPlacement() == null || s2.getPlacement() == null)
436                    return false;
437                TimeLocation t1 = s1.getTime();
438                TimeLocation t2 = s2.getTime();
439                if (!t1.shareDays(t2) || !t1.shareWeeks(t2))
440                    return false;
441                int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
442                if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
443                    int gap = a2 - (a1 + t1.getNrSlotsPerMeeting());
444                    int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
445                    return (gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t1.getBreakTime());
446                } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
447                    int gap = a1 - (a2 + t2.getNrSlotsPerMeeting());
448                    int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
449                    return (gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t2.getBreakTime());
450                }
451                return false;
452            }
453
454            @Override
455            public int penalty(Context cx, Student s, SctAssignment sa1, SctAssignment sa2) {
456                Section s1 = (Section) sa1;
457                Section s2 = (Section) sa2;
458                if (s1.getPlacement() == null || s2.getPlacement() == null) return 0;
459                TimeLocation t1 = s1.getTime();
460                TimeLocation t2 = s2.getTime();
461                if (!t1.shareDays(t2) || !t1.shareWeeks(t2)) return 0;
462                int a1 = t1.getStartSlot(), a2 = t2.getStartSlot();
463                if (a1 + t1.getNrSlotsPerMeeting() <= a2) {
464                    int gap = a2 - (a1 + t1.getNrSlotsPerMeeting());
465                    int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
466                    if ((gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t1.getBreakTime()))
467                        return dist;
468                } else if (a2 + t2.getNrSlotsPerMeeting() <= a1) {
469                    int gap = a1 - (a2 + t2.getNrSlotsPerMeeting());
470                    int dist = cx.getDistanceInMinutes(s1.getPlacement(), s2.getPlacement());
471                    if ((gap < cx.getMaxTravelGap() && dist > 0) || (gap < 2 * cx.getMaxTravelGap() && dist > t2.getBreakTime()))
472                        return dist;
473                }
474                return 0;
475            }
476            
477            @Override
478            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
479                return new Nothing();
480            }
481
482            @Override
483            public double getWeight(Context cx, Conflict c, Enrollment e) {
484                return c.getPenalty();
485            }
486        }),
487        /**
488         * A back-to-back conflict is there every time when a student has two classes that are
489         * back-to-back or less than StudentWeights.BackToBackDistance time slots apart (defaults to 30 minutes).
490         * Such a conflict is weighted by StudentWeights.BackToBackFactor, which
491         * defaults to -0.0001 (these conflicts are preferred by default, trying to avoid schedule gaps).
492         * NEW: Consider student's back-to-back preference. That is, students with no preference are ignored, and
493         * students that discourage back-to-backs have a negative weight on the conflict.
494         */
495        BackToBack(WeightType.BOTH, "StudentWeights.BackToBackFactor", -0.0001, new Quality() {
496            @Override
497            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
498                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && 
499                        (student.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED || student.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED);
500            }
501
502            @Override
503            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
504                TimeLocation t1 = a1.getTime();
505                TimeLocation t2 = a2.getTime();
506                if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false;
507                if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) {
508                    int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting());
509                    return dist <= cx.getBackToBackDistance();
510                } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) {
511                    int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting());
512                    return dist <= cx.getBackToBackDistance();
513                }
514                return false;
515            }
516
517            @Override
518            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
519                if (!inConflict(cx, a1, a2)) return 0;
520                if (s.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED)
521                    return a1.getTime().nrSharedDays(a2.getTime());
522                else if (s.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED)
523                    return -a1.getTime().nrSharedDays(a2.getTime());
524                else
525                    return 0;
526            }
527            
528            @Override
529            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
530                return new Nothing();
531            }
532
533            @Override
534            public double getWeight(Context cx, Conflict c, Enrollment e) {
535                return c.getPenalty();
536            }
537        }),
538        /**
539         * A work-day conflict is there every time when a student has two classes that are too
540         * far apart. This means that the time between the start of the first class and the end
541         * of the last class is more than WorkDay.WorkDayLimit (defaults to 6 hours). A penalty
542         * of one is incurred for every hour started over this limit.
543         * Such a conflict is weighted by StudentWeights.WorkDayFactor, which defaults to 0.01.
544         */
545        WorkDay(WeightType.BOTH, "StudentWeights.WorkDayFactor", 0.0100, new Quality() {
546            @Override
547            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
548                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy();
549            }
550            
551            @Override
552            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
553                TimeLocation t1 = a1.getTime();
554                TimeLocation t2 = a2.getTime();
555                if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false;
556                int dist = Math.max(t1.getStartSlot() + t1.getLength(), t2.getStartSlot() + t2.getLength()) - Math.min(t1.getStartSlot(), t2.getStartSlot());
557                return dist > cx.getWorkDayLimit();
558            }
559
560            @Override
561            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
562                TimeLocation t1 = a1.getTime();
563                TimeLocation t2 = a2.getTime();
564                if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return 0;
565                int dist = Math.max(t1.getStartSlot() + t1.getLength(), t2.getStartSlot() + t2.getLength()) - Math.min(t1.getStartSlot(), t2.getStartSlot());
566                if (dist > cx.getWorkDayLimit())
567                    return a1.getTime().nrSharedDays(a2.getTime()) * (dist - cx.getWorkDayLimit());
568                else
569                    return 0;
570            }
571            
572            @Override
573            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
574                return new Nothing();
575            }
576
577            @Override
578            public double getWeight(Context cx, Conflict c, Enrollment e) {
579                return c.getPenalty() / 12.0;
580            }
581        }),
582        TooEarly(WeightType.REQUEST, "StudentWeights.TooEarlyFactor", 0.0500, new Quality(){
583            @Override
584            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
585                return false;
586            }
587
588            @Override
589            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
590                if (a1.getTime() == null || a2.getTime() == null) return false;
591                return a1.getTime().shareDays(a2.getTime()) && a1.getTime().shareHours(a2.getTime());
592            }
593
594            @Override
595            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
596                if (!inConflict(cx, a1, a2)) return 0;
597                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
598            }
599            
600            @Override
601            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
602                return (e.isCourseRequest() && !e.getStudent().isDummy() ? new SingleTimeIterable(0, cx.getEarlySlot()) : new Nothing());
603            }
604            
605            @Override
606            public double getWeight(Context cx, Conflict c, Enrollment e) {
607                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit());
608            }
609        }),
610        TooLate(WeightType.REQUEST, "StudentWeights.TooLateFactor", 0.0250, new Quality(){
611            @Override
612            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
613                return false;
614            }
615
616            @Override
617            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
618                if (a1.getTime() == null || a2.getTime() == null) return false;
619                return a1.getTime().shareDays(a2.getTime()) && a1.getTime().shareHours(a2.getTime());
620            }
621
622            @Override
623            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
624                if (!inConflict(cx, a1, a2)) return 0;
625                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
626            }
627            
628            @Override
629            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
630                return (e.isCourseRequest() && !e.getStudent().isDummy() ? new SingleTimeIterable(cx.getLateSlot(), 288) : new Nothing());
631            }
632            
633            @Override
634            public double getWeight(Context cx, Conflict c, Enrollment e) {
635                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit());
636            }
637        }),
638        /**
639         * There is a student modality preference conflict when a student that prefers online
640         * gets a non-online class ({@link Section#isOnline()} is false) or when a student that
641         * prefers non-online gets an online class (@{link Section#isOnline()} is true).
642         * Such a conflict is weighted by StudentWeights.ModalityFactor, which defaults to 0.05.
643         */
644        Modality(WeightType.REQUEST, "StudentWeights.ModalityFactor", 0.0500, new Quality(){
645            @Override
646            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
647                return false;
648            }
649
650            @Override
651            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
652                return a1.equals(a2);
653            }
654
655            @Override
656            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
657                return (inConflict(cx, a1, a2) ? 1 : 0);
658            }
659            
660            @Override
661            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
662                if (!e.isCourseRequest() || e.getStudent().isDummy()) return new Nothing();
663                if (e.getStudent().getModalityPreference() == ModalityPreference.ONLINE_PREFERRED)
664                    return new Online(e, false); // face-to-face sections are conflicting
665                else if (e.getStudent().getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED)
666                    return new Online(e, true); // online sections are conflicting
667                return new Nothing();
668            }
669            
670            @Override
671            public double getWeight(Context cx, Conflict c, Enrollment e) {
672                return ((double) c.getPenalty()) / ((double) e.getSections().size());
673            }
674        }),
675        /** 
676         * DRC: Time conflict between class and a free time request (for students with FT accommodation).
677         * Free time conflicts are penalized as the time of a course request overlapping with a free time
678         * proportional to the time of the request, capped at one half of the time.
679         * This criterion is weighted by Accommodations.FreeTimeOverlapFactor, defaulting to 0.5.
680         */
681        AccFreeTimeOverlap(WeightType.REQUEST, "Accommodations.FreeTimeOverlapFactor", 0.5000, new Quality(){
682            @Override
683            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
684                return false;
685            }
686
687            @Override
688            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
689                if (a1.getTime() == null || a2.getTime() == null) return false;
690                return a1.getTime().hasIntersection(a2.getTime());
691            }
692
693            @Override
694            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
695                if (!inConflict(cx, a1, a2)) return 0;
696                return a1.getTime().nrSharedDays(a2.getTime()) * a1.getTime().nrSharedHours(a2.getTime());
697            }
698            
699            @Override
700            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
701                if (!e.getStudent().hasAccommodation(cx.getFreeTimeAccommodation())) return new Nothing();
702                return (e.isCourseRequest() ? new FreeTimes(e.getStudent()) : new Nothing());
703            }
704            
705            @Override
706            public double getWeight(Context cx, Conflict c, Enrollment e) {
707                return Math.min(cx.getTimeOverlapMaxLimit() * c.getPenalty() / c.getE1().getNrSlots(), cx.getTimeOverlapMaxLimit());
708            }
709        }),
710        /**
711         * DRC: A back-to-back conflict (for students with BTB accommodation) is there every time when a student has two classes that are NOT
712         * back-to-back or less than Accommodations.BackToBackDistance time slots apart (defaults to 30 minutes).
713         * Such a conflict is weighted by Accommodations.BackToBackFactor, which defaults to 0.001
714         */
715        AccBackToBack(WeightType.BOTH, "Accommodations.BackToBackFactor", 0.001, new Quality() {
716            @Override
717            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
718                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && student.hasAccommodation(cx.getBackToBackAccommodation());
719            }
720
721            @Override
722            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
723                TimeLocation t1 = a1.getTime();
724                TimeLocation t2 = a2.getTime();
725                if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false;
726                if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) {
727                    int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting());
728                    return dist > cx.getBackToBackDistance();
729                } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) {
730                    int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting());
731                    return dist > cx.getBackToBackDistance();
732                }
733                return false;
734            }
735
736            @Override
737            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
738                if (!inConflict(cx, a1, a2)) return 0;
739                return a1.getTime().nrSharedDays(a2.getTime());
740            }
741            
742            @Override
743            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
744                return new Nothing();
745            }
746
747            @Override
748            public double getWeight(Context cx, Conflict c, Enrollment e) {
749                return c.getPenalty();
750            }
751        }),
752        /**
753         * DRC: A not back-to-back conflict (for students with BBC accommodation) is there every time when a student has two classes that are
754         * back-to-back or less than Accommodations.BackToBackDistance time slots apart (defaults to 30 minutes).
755         * Such a conflict is weighted by Accommodations.BreaksBetweenClassesFactor, which defaults to 0.001.
756         */
757        AccBreaksBetweenClasses(WeightType.BOTH, "Accommodations.BreaksBetweenClassesFactor", 0.001, new Quality() {
758            @Override
759            public boolean isApplicable(Context cx, Student student, Request r1, Request r2) {
760                return r1 instanceof CourseRequest && r2 instanceof CourseRequest && !student.isDummy() && student.hasAccommodation(cx.getBreakBetweenClassesAccommodation());
761            }
762
763            @Override
764            public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) {
765                TimeLocation t1 = a1.getTime();
766                TimeLocation t2 = a2.getTime();
767                if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return false;
768                if (t1.getStartSlot() + t1.getNrSlotsPerMeeting() <= t2.getStartSlot()) {
769                    int dist = t2.getStartSlot() - (t1.getStartSlot() + t1.getNrSlotsPerMeeting());
770                    return dist <= cx.getBackToBackDistance();
771                } else if (t2.getStartSlot() + t2.getNrSlotsPerMeeting() <= t1.getStartSlot()) {
772                    int dist = t1.getStartSlot() - (t2.getStartSlot() + t2.getNrSlotsPerMeeting());
773                    return dist <= cx.getBackToBackDistance();
774                }
775                return false;
776            }
777
778            @Override
779            public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) {
780                if (!inConflict(cx, a1, a2)) return 0;
781                return a1.getTime().nrSharedDays(a2.getTime());
782            }
783            
784            @Override
785            public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) {
786                return new Nothing();
787            }
788
789            @Override
790            public double getWeight(Context cx, Conflict c, Enrollment e) {
791                return c.getPenalty();
792            }
793        }),
794        ;
795        
796        private WeightType iType;
797        private Quality iQuality;
798        private String iWeightName;
799        private double iWeightDefault;
800        Type(WeightType type, String weightName, double weightDefault, Quality quality) {
801            iQuality = quality;
802            iType = type;
803            iWeightName = weightName;
804            iWeightDefault = weightDefault;
805        }
806        
807        
808        public boolean isApplicable(Context cx, Student student, Request r1, Request r2) { return iQuality.isApplicable(cx, student, r1, r2); }
809        public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2) { return iQuality.inConflict(cx, a1, a2); }
810        public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2) { return iQuality.penalty(cx, s, a1, a2); }
811        public Iterable<? extends SctAssignment> other(Context cx, Enrollment e) { return iQuality.other(cx, e); }
812        public double getWeight(Context cx, Conflict c, Enrollment e) { return iQuality.getWeight(cx, c, e); }
813        public String getName() { return name().replaceAll("(?<=[^A-Z0-9])([A-Z0-9])"," $1"); }
814        public String getAbbv() { return getName().replaceAll("[a-z ]",""); }
815        public WeightType getType() { return iType; }
816        public String getWeightName() { return iWeightName; }
817        public double getWeightDefault() { return iWeightDefault; }
818    }
819    
820    /**
821     * Schedule quality interface
822     */
823    public static interface Quality {
824        /**
825         * Check if the metric is applicable for the given student, between the given two requests
826         */
827        public boolean isApplicable(Context cx, Student student, Request r1, Request r2);
828        /**
829         * When applicable, is there a conflict between two sections
830         */
831        public boolean inConflict(Context cx, SctAssignment a1, SctAssignment a2);
832        /**
833         * When in conflict, what is the penalisation
834         */
835        public int penalty(Context cx, Student s, SctAssignment a1, SctAssignment a2);
836        /**
837         * Enumerate other section assignments applicable for the given enrollment (e.g., student unavailabilities)
838         */
839        public Iterable<? extends SctAssignment> other(Context cx, Enrollment e);
840        /**
841         * Base weight of the given conflict and enrollment. Typically based on the {@link Conflict#getPenalty()}, but 
842         * change to be between 0.0 and 1.0. For example, for time conflicts, a percentage of share is used. 
843         */
844        public double getWeight(Context cx, Conflict c, Enrollment e);
845    }
846    
847    /**
848     * Penalisation of the given type between two enrollments of a student.
849     */
850    public int penalty(Type type, Enrollment e1, Enrollment e2) {
851        if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) return 0;
852        int cnt = 0;
853        for (SctAssignment s1 : e1.getAssignments()) {
854            for (SctAssignment s2 : e2.getAssignments()) {
855                cnt += type.penalty(iContext, e1.getStudent(), s1, s2);
856            }
857        }
858        return cnt;
859    }
860    
861    /**
862     * Conflicss of the given type between two enrollments of a student.
863     */
864    public Set<Conflict> conflicts(Type type, Enrollment e1, Enrollment e2) {
865        Set<Conflict> ret = new HashSet<Conflict>();
866        if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) return ret;
867        for (SctAssignment s1 : e1.getAssignments()) {
868            for (SctAssignment s2 : e2.getAssignments()) {
869                int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
870                if (penalty != 0)
871                    ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e2, s2));
872            }
873        }
874        return ret;
875    }
876    
877    /**
878     * Conflicts of any type between two enrollments of a student.
879     */
880    public Set<Conflict> conflicts(Enrollment e1, Enrollment e2) {
881        Set<Conflict> ret = new HashSet<Conflict>();
882        for (Type type: iContext.getTypes()) {
883            if (!e1.getStudent().equals(e2.getStudent()) || !type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e2.getRequest())) continue;
884            for (SctAssignment s1 : e1.getAssignments()) {
885                for (SctAssignment s2 : e2.getAssignments()) {
886                    int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
887                    if (penalty != 0)
888                        ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e2, s2));
889                }
890            }
891        }
892        return ret;
893    }
894    
895    /**
896     * Conflicts of the given type between classes of a single enrollment (or with free times, unavailabilities, etc.)
897     */
898    public Set<Conflict> conflicts(Type type, Enrollment e1) {
899        Set<Conflict> ret = new HashSet<Conflict>();
900        boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest()); 
901        for (SctAssignment s1 : e1.getAssignments()) {
902            if (applicable) {
903                for (SctAssignment s2 : e1.getAssignments()) {
904                    if (s1.getId() < s2.getId()) {
905                        int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
906                        if (penalty != 0)
907                            ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e1, s2));
908                    }
909                }
910            }
911            for (SctAssignment s2: type.other(iContext, e1)) {
912                int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
913                if (penalty != 0)
914                    ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, s2));
915            }
916        }
917        return ret;
918    }
919    
920    /**
921     * Conflicts of any type between classes of a single enrollment (or with free times, unavailabilities, etc.)
922     */
923    public Set<Conflict> conflicts(Enrollment e1) {
924        Set<Conflict> ret = new HashSet<Conflict>();
925        for (Type type: iContext.getTypes()) {
926            boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest()); 
927            for (SctAssignment s1 : e1.getAssignments()) {
928                if (applicable) {
929                    for (SctAssignment s2 : e1.getAssignments()) {
930                        if (s1.getId() < s2.getId()) {
931                            int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
932                            if (penalty != 0)
933                                ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, e1, s2));
934                        }
935                    }
936                }
937                for (SctAssignment s2: type.other(iContext, e1)) {
938                    int penalty = type.penalty(iContext, e1.getStudent(), s1, s2);
939                    if (penalty != 0)
940                        ret.add(new Conflict(e1.getStudent(), type, penalty, e1, s1, s2));
941                }
942            }            
943        }
944        return ret;
945    }
946    
947    /**
948     * Penalty of given type between classes of a single enrollment (or with free times, unavailabilities, etc.)
949     */
950    public int penalty(Type type, Enrollment e1) {
951        int penalty = 0;
952        boolean applicable = type.isApplicable(iContext, e1.getStudent(), e1.getRequest(), e1.getRequest());
953        for (SctAssignment s1 : e1.getAssignments()) {
954            if (applicable) {
955                for (SctAssignment s2 : e1.getAssignments()) {
956                    if (s1.getId() < s2.getId()) {
957                        penalty += type.penalty(iContext, e1.getStudent(), s1, s2);
958                    }
959                }
960            }
961            for (SctAssignment s2: type.other(iContext, e1)) {
962                penalty += type.penalty(iContext, e1.getStudent(), s1, s2);
963            }
964        }
965        return penalty;
966    }
967    
968    /**
969     * Check whether the given type is applicable for the student and the two requests.
970     */
971    public boolean isApplicable(Type type, Student student, Request r1, Request r2) {
972        return type.isApplicable(iContext, student, r1, r2);
973    }
974  
975    /**
976     * Total penalisation of given type
977     */
978    public int getTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) {
979        return getContext(assignment).getTotalPenalty(type);
980    }
981    
982    /**
983     * Total penalisation of given types
984     */
985    public int getTotalPenalty(Assignment<Request, Enrollment> assignment, Type... types) {
986        int ret = 0;
987        for (Type type: types)
988            ret += getContext(assignment).getTotalPenalty(type);
989        return ret;
990    }
991    
992    /**
993     * Re-check total penalization for the given assignment 
994     */
995    public void checkTotalPenalty(Assignment<Request, Enrollment> assignment) {
996        for (Type type: iContext.getTypes())
997            checkTotalPenalty(type, assignment);
998    }
999    
1000    /**
1001     * Re-check total penalization for the given assignment and conflict type 
1002     */
1003    public void checkTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) {
1004        getContext(assignment).checkTotalPenalty(type, assignment);
1005    }
1006
1007    /**
1008     * All conflicts of the given type for the given assignment 
1009     */
1010    public Set<Conflict> getAllConflicts(Type type, Assignment<Request, Enrollment> assignment) {
1011        return getContext(assignment).getAllConflicts(type);
1012    }
1013    
1014    /**
1015     * All conflicts of the any type for the enrollment (including conflicts with other enrollments of the student)
1016     */
1017    public Set<Conflict> allConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
1018        Set<Conflict> conflicts = new HashSet<Conflict>();
1019        for (Type t: iContext.getTypes()) {
1020            conflicts.addAll(conflicts(t, enrollment));
1021            for (Request request : enrollment.getStudent().getRequests()) {
1022                if (request.equals(enrollment.getRequest()) || assignment.getValue(request) == null) continue;
1023                conflicts.addAll(conflicts(t, enrollment, assignment.getValue(request)));
1024            }
1025        }
1026        return conflicts;
1027    }
1028    
1029    @Override
1030    public void beforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1031        getContext(assignment).beforeAssigned(assignment, iteration, value);
1032    }
1033
1034    @Override
1035    public void afterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1036        getContext(assignment).afterAssigned(assignment, iteration, value);
1037    }
1038
1039    @Override
1040    public void afterUnassigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1041        getContext(assignment).afterUnassigned(assignment, iteration, value);
1042    }
1043    
1044    /** A representation of a time overlapping conflict */
1045    public class Conflict {
1046        private Type iType;
1047        private int iPenalty;
1048        private Student iStudent;
1049        private SctAssignment iA1, iA2;
1050        private Enrollment iE1, iE2;
1051        private int iHashCode;
1052
1053        /**
1054         * Constructor
1055         * 
1056         * @param student related student
1057         * @param type conflict type
1058         * @param penalty conflict penalization, e.g., the number of slots in common between the two conflicting sections
1059         * @param e1 first enrollment
1060         * @param a1 first conflicting section
1061         * @param e2 second enrollment
1062         * @param a2 second conflicting section
1063         */
1064        public Conflict(Student student, Type type, int penalty, Enrollment e1, SctAssignment a1, Enrollment e2, SctAssignment a2) {
1065            iStudent = student;
1066            if (a1.compareById(a2) < 0 ) {
1067                iA1 = a1;
1068                iA2 = a2;
1069                iE1 = e1;
1070                iE2 = e2;
1071            } else {
1072                iA1 = a2;
1073                iA2 = a1;
1074                iE1 = e2;
1075                iE2 = e1;
1076            }
1077            iHashCode = (iStudent.getId() + ":" + iA1.getId() + ":" + iA2.getId()).hashCode();
1078            iType = type;
1079            iPenalty = penalty;
1080        }
1081        
1082        public Conflict(Student student, Type type, int penalty, Enrollment e1, SctAssignment a1, SctAssignment a2) {
1083            this(student, type, penalty, e1, a1, a2 instanceof FreeTimeRequest ? ((FreeTimeRequest)a2).createEnrollment() : a2 instanceof Unavailability ? ((Unavailability)a2).createEnrollment() : e1, a2);
1084            
1085        }
1086
1087        /** Related student
1088         * @return student
1089         **/
1090        public Student getStudent() {
1091            return iStudent;
1092        }
1093
1094        /** First section
1095         * @return first section
1096         **/
1097        public SctAssignment getS1() {
1098            return iA1;
1099        }
1100
1101        /** Second section
1102         * @return second section
1103         **/
1104        public SctAssignment getS2() {
1105            return iA2;
1106        }
1107
1108        /** First request
1109         * @return first request
1110         **/
1111        public Request getR1() {
1112            return iE1.getRequest();
1113        }
1114        
1115        /** First request weight
1116         * @return first request weight
1117         **/
1118        public double getR1Weight() {
1119            return (iE1.getRequest() == null ? 0.0 : iE1.getRequest().getWeight());
1120        }
1121        
1122        /** Second request weight
1123         * @return second request weight
1124         **/
1125        public double getR2Weight() {
1126            return (iE2.getRequest() == null ? 0.0 : iE2.getRequest().getWeight());
1127        }
1128        
1129        /** Second request
1130         * @return second request
1131         **/
1132        public Request getR2() {
1133            return iE2.getRequest();
1134        }
1135        
1136        /** First enrollment
1137         * @return first enrollment
1138         **/
1139        public Enrollment getE1() {
1140            return iE1;
1141        }
1142
1143        /** Second enrollment
1144         * @return second enrollment
1145         **/
1146        public Enrollment getE2() {
1147            return iE2;
1148        }
1149        
1150        @Override
1151        public int hashCode() {
1152            return iHashCode;
1153        }
1154
1155        /** Conflict penalty, e.g., the number of overlapping slots against the number of slots of the smallest section
1156         * @return conflict penalty 
1157         **/
1158        public int getPenalty() {
1159            return iPenalty;
1160        }
1161        
1162        /** Other enrollment of the conflict */
1163        public Enrollment getOther(Enrollment enrollment) {
1164            return (getE1().getRequest().equals(enrollment.getRequest()) ? getE2() : getE1());
1165        }
1166        
1167        /** Weight of the conflict on the given enrollment */
1168        public double getWeight(Enrollment e) {
1169            return iType.getWeight(iContext, this, e);
1170        }
1171        
1172        /** Weight of the conflict on both enrollment (sum) */
1173        public double getWeight() {
1174            return (iType.getWeight(iContext, this, iE1) + iType.getWeight(iContext, this, iE2)) / 2.0;
1175        }
1176        
1177        /** Conflict type
1178         * @return conflict type;
1179         */
1180        public Type getType() {
1181            return iType;
1182        }
1183
1184        @Override
1185        public boolean equals(Object o) {
1186            if (o == null || !(o instanceof Conflict)) return false;
1187            Conflict c = (Conflict) o;
1188            return getType() == c.getType() && getStudent().equals(c.getStudent()) && getS1().equals(c.getS1()) && getS2().equals(c.getS2());
1189        }
1190
1191        @Override
1192        public String toString() {
1193            return getStudent() + ": (" + getType() + ", p:" + getPenalty() + ") " + getS1() + " -- " + getS2();
1194        }
1195    }
1196    
1197    /**
1198     * Context holding parameters and distance cache. See {@link Type} for the list of available parameters.
1199     */
1200    public static class Context {
1201        private List<Type> iTypes = null;
1202        private DistanceMetric iDistanceMetric = null;
1203        private boolean iDebug = false;
1204        protected double iTimeOverlapMaxLimit = 0.5000;
1205        private int iLunchStart, iLunchEnd, iLunchLength, iMaxTravelGap, iWorkDayLimit, iBackToBackDistance, iEarlySlot, iLateSlot, iAccBackToBackDistance;
1206        private String iFreeTimeAccommodation = "FT", iBackToBackAccommodation = "BTB", iBreakBetweenClassesAccommodation = "BBC";
1207        
1208        public Context(DistanceMetric dm, DataProperties config) {
1209            iDistanceMetric = (dm == null ? new DistanceMetric(config) : dm);
1210            iDebug = config.getPropertyBoolean("StudentQuality.Debug", false);
1211            iTimeOverlapMaxLimit = config.getPropertyDouble("StudentWeights.TimeOverlapMaxLimit", iTimeOverlapMaxLimit);
1212            iLunchStart = config.getPropertyInt("StudentLunch.StartSlot", (11 * 60) / 5);
1213            iLunchEnd = config.getPropertyInt("StudentLunch.EndStart", (13 * 60) / 5);
1214            iLunchLength = config.getPropertyInt("StudentLunch.Length", 30 / 5);
1215            iMaxTravelGap = config.getPropertyInt("TravelTime.MaxTravelGap", 12);
1216            iWorkDayLimit = config.getPropertyInt("WorkDay.WorkDayLimit", 6 * 12);
1217            iBackToBackDistance = config.getPropertyInt("StudentWeights.BackToBackDistance", 6);
1218            iAccBackToBackDistance = config.getPropertyInt("Accommodations.BackToBackDistance", 6);
1219            iEarlySlot = config.getPropertyInt("WorkDay.EarlySlot", 102);
1220            iLateSlot = config.getPropertyInt("WorkDay.LateSlot", 210);
1221            iFreeTimeAccommodation = config.getProperty("Accommodations.FreeTimeReference", iFreeTimeAccommodation);
1222            iBackToBackAccommodation = config.getProperty("Accommodations.BackToBackReference", iBackToBackAccommodation);
1223            iBreakBetweenClassesAccommodation = config.getProperty("Accommodations.BreakBetweenClassesReference", iBreakBetweenClassesAccommodation);
1224            iTypes = new ArrayList<Type>();
1225            for (Type t: Type.values())
1226                if (config.getPropertyDouble(t.getWeightName(), t.getWeightDefault()) != 0.0)
1227                    iTypes.add(t);
1228        }
1229        
1230        public DistanceMetric getDistanceMetric() {
1231            return iDistanceMetric;
1232        }
1233        
1234        public boolean isDebug() { return iDebug; }
1235        
1236        public double getTimeOverlapMaxLimit() { return iTimeOverlapMaxLimit; }
1237        public int getLunchStart() { return iLunchStart; }
1238        public int getLunchEnd() { return iLunchEnd; }
1239        public int getLunchLength() { return iLunchLength; }
1240        public int getMaxTravelGap() { return iMaxTravelGap; }
1241        public int getWorkDayLimit() { return iWorkDayLimit; }
1242        public int getBackToBackDistance() { return iBackToBackDistance; }
1243        public int getAccBackToBackDistance() { return iAccBackToBackDistance; }
1244        public int getEarlySlot() { return iEarlySlot; }
1245        public int getLateSlot() { return iLateSlot; }
1246        public String getFreeTimeAccommodation() { return iFreeTimeAccommodation; }
1247        public String getBackToBackAccommodation() { return iBackToBackAccommodation; }
1248        public String getBreakBetweenClassesAccommodation() { return iBreakBetweenClassesAccommodation; }
1249        public List<Type> getTypes() { return iTypes; }
1250            
1251        private Map<Long, Map<Long, Integer>> iDistanceCache = new HashMap<Long, Map<Long,Integer>>();
1252        protected synchronized int getDistanceInMinutes(RoomLocation r1, RoomLocation r2) {
1253            if (r1.getId().compareTo(r2.getId()) > 0) return getDistanceInMinutes(r2, r1);
1254            if (r1.getId().equals(r2.getId()) || r1.getIgnoreTooFar() || r2.getIgnoreTooFar())
1255                return 0;
1256            if (r1.getPosX() == null || r1.getPosY() == null || r2.getPosX() == null || r2.getPosY() == null)
1257                return iDistanceMetric.getMaxTravelDistanceInMinutes();
1258            Map<Long, Integer> other2distance = iDistanceCache.get(r1.getId());
1259            if (other2distance == null) {
1260                other2distance = new HashMap<Long, Integer>();
1261                iDistanceCache.put(r1.getId(), other2distance);
1262            }
1263            Integer distance = other2distance.get(r2.getId());
1264            if (distance == null) {
1265                distance = iDistanceMetric.getDistanceInMinutes(r1.getId(), r1.getPosX(), r1.getPosY(), r2.getId(), r2.getPosX(), r2.getPosY());
1266                other2distance.put(r2.getId(), distance);    
1267            }
1268            return distance;
1269        }
1270
1271        public int getDistanceInMinutes(Placement p1, Placement p2) {
1272            if (p1.isMultiRoom()) {
1273                if (p2.isMultiRoom()) {
1274                    int dist = 0;
1275                    for (RoomLocation r1 : p1.getRoomLocations()) {
1276                        for (RoomLocation r2 : p2.getRoomLocations()) {
1277                            dist = Math.max(dist, getDistanceInMinutes(r1, r2));
1278                        }
1279                    }
1280                    return dist;
1281                } else {
1282                    if (p2.getRoomLocation() == null)
1283                        return 0;
1284                    int dist = 0;
1285                    for (RoomLocation r1 : p1.getRoomLocations()) {
1286                        dist = Math.max(dist, getDistanceInMinutes(r1, p2.getRoomLocation()));
1287                    }
1288                    return dist;
1289                }
1290            } else if (p2.isMultiRoom()) {
1291                if (p1.getRoomLocation() == null)
1292                    return 0;
1293                int dist = 0;
1294                for (RoomLocation r2 : p2.getRoomLocations()) {
1295                    dist = Math.max(dist, getDistanceInMinutes(p1.getRoomLocation(), r2));
1296                }
1297                return dist;
1298            } else {
1299                if (p1.getRoomLocation() == null || p2.getRoomLocation() == null)
1300                    return 0;
1301                return getDistanceInMinutes(p1.getRoomLocation(), p2.getRoomLocation());
1302            }
1303        }        
1304    }
1305    
1306    /**
1307     * Assignment context
1308     */
1309    public class StudentQualityContext implements AssignmentConstraintContext<Request, Enrollment> {
1310        private int[] iTotalPenalty = null;
1311        private Set<Conflict>[] iAllConflicts = null;
1312        private Request iOldVariable = null;
1313        private Enrollment iUnassignedValue = null;
1314
1315        @SuppressWarnings("unchecked")
1316        public StudentQualityContext(Assignment<Request, Enrollment> assignment) {
1317            iTotalPenalty = new int[Type.values().length];
1318            for (Type t: iContext.getTypes())
1319                iTotalPenalty[t.ordinal()] = countTotalPenalty(t, assignment);
1320            if (iContext.isDebug()) {
1321                iAllConflicts = new Set[Type.values().length];
1322                for (Type t: iContext.getTypes())
1323                    iAllConflicts[t.ordinal()] = computeAllConflicts(t, assignment);
1324            }
1325            StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment);
1326            for (Type t: iContext.getTypes())
1327                for (Conflict c: computeAllConflicts(t, assignment)) cx.add(assignment, c);
1328        }
1329        
1330        @SuppressWarnings("unchecked")
1331        public StudentQualityContext(StudentQualityContext parent) {
1332            iTotalPenalty = new int[Type.values().length];
1333            for (Type t: iContext.getTypes())
1334                iTotalPenalty[t.ordinal()] = parent.iTotalPenalty[t.ordinal()];
1335            if (iContext.isDebug()) {
1336                iAllConflicts = new Set[Type.values().length];
1337                for (Type t: iContext.getTypes())
1338                    iAllConflicts[t.ordinal()] = new HashSet<Conflict>(parent.iAllConflicts[t.ordinal()]);
1339            }
1340        }
1341
1342        @Override
1343        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment value) {
1344            StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment);
1345            for (Type type: iContext.getTypes()) {
1346                iTotalPenalty[type.ordinal()] += allPenalty(type, assignment, value);
1347                for (Conflict c: allConflicts(type, assignment, value))
1348                    cx.add(assignment, c);
1349            }
1350            if (iContext.isDebug()) {
1351                sLog.debug("A:" + value.variable() + " := " + value);
1352                for (Type type: iContext.getTypes()) {
1353                    int inc = allPenalty(type, assignment, value);
1354                    if (inc != 0) {
1355                        sLog.debug("-- " + type + " +" + inc + " A: " + value.variable() + " := " + value);
1356                        for (Conflict c: allConflicts(type, assignment, value)) {
1357                            sLog.debug("  -- " + c);
1358                            iAllConflicts[type.ordinal()].add(c);
1359                            inc -= c.getPenalty();
1360                        }
1361                        if (inc != 0) {
1362                            sLog.error(type + ": Different penalty for the assigned value (difference: " + inc + ")!");
1363                        }
1364                    }
1365                }
1366            }
1367        }
1368
1369        /**
1370         * Called when a value is unassigned from a variable. Internal number of
1371         * time overlapping conflicts is updated, see
1372         * {@link TimeOverlapsCounter#getTotalNrConflicts(Assignment)}.
1373         */
1374        @Override
1375        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment value) {
1376            StudentSectioningModelContext cx = ((StudentSectioningModel)getModel()).getContext(assignment);
1377            for (Type type: iContext.getTypes()) {
1378                iTotalPenalty[type.ordinal()] -= allPenalty(type, assignment, value);
1379                for (Conflict c: allConflicts(type, assignment, value))
1380                    cx.remove(assignment, c);
1381            }
1382            if (iContext.isDebug()) {
1383                sLog.debug("U:" + value.variable() + " := " + value);
1384                for (Type type: iContext.getTypes()) {
1385                    int dec = allPenalty(type, assignment, value);
1386                    if (dec != 0) {
1387                        sLog.debug("--  " + type + " -" + dec + " U: " + value.variable() + " := " + value);
1388                        for (Conflict c: allConflicts(type, assignment, value)) {
1389                            sLog.debug("  -- " + c);
1390                            iAllConflicts[type.ordinal()].remove(c);
1391                            dec -= c.getPenalty();
1392                        }
1393                        if (dec != 0) {
1394                            sLog.error(type + ":Different penalty for the unassigned value (difference: " + dec + ")!");
1395                        }
1396                    }
1397                }
1398            }
1399        }
1400        
1401        /**
1402         * Called before a value is assigned to a variable.
1403         * @param assignment current assignment
1404         * @param iteration current iteration
1405         * @param value value to be assigned
1406         */
1407        public void beforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1408            if (value != null) {
1409                Enrollment old = assignment.getValue(value.variable());
1410                if (old != null) {
1411                    iUnassignedValue = old;
1412                    unassigned(assignment, old);
1413                }
1414                iOldVariable = value.variable();
1415            }
1416        }
1417
1418        /**
1419         * Called after a value is assigned to a variable.
1420         * @param assignment current assignment
1421         * @param iteration current iteration
1422         * @param value value that was assigned
1423         */
1424        public void afterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1425            iOldVariable = null;
1426            iUnassignedValue = null;
1427            if (value != null) {
1428                assigned(assignment, value);
1429            }
1430        }
1431
1432        /**
1433         * Called after a value is unassigned from a variable.
1434         * @param assignment current assignment
1435         * @param iteration current iteration
1436         * @param value value that was unassigned
1437         */
1438        public void afterUnassigned(Assignment<Request, Enrollment> assignment, long iteration, Enrollment value) {
1439            if (value != null && !value.equals(iUnassignedValue)) {
1440                unassigned(assignment, value);
1441            }
1442        }
1443        
1444        public Set<Conflict> getAllConflicts(Type type) {
1445            return iAllConflicts[type.ordinal()];
1446        }
1447        
1448        public int getTotalPenalty(Type type) {
1449            return iTotalPenalty[type.ordinal()];
1450        }
1451        
1452        public void checkTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) {
1453            int total = countTotalPenalty(type, assignment);
1454            if (total != iTotalPenalty[type.ordinal()]) {
1455                sLog.error(type + " penalty does not match for (actual: " + total + ", count: " + iTotalPenalty[type.ordinal()] + ")!");
1456                iTotalPenalty[type.ordinal()] = total;
1457                if (iContext.isDebug()) {
1458                    Set<Conflict> conflicts = computeAllConflicts(type, assignment);
1459                    for (Conflict c: conflicts) {
1460                        if (!iAllConflicts[type.ordinal()].contains(c))
1461                            sLog.debug("  +add+ " + c);
1462                    }
1463                    for (Conflict c: iAllConflicts[type.ordinal()]) {
1464                        if (!conflicts.contains(c))
1465                            sLog.debug("  -rem- " + c);
1466                    }
1467                    for (Conflict c: conflicts) {
1468                        for (Conflict d: iAllConflicts[type.ordinal()]) {
1469                            if (c.equals(d) && c.getPenalty() != d.getPenalty()) {
1470                                sLog.debug("  -dif- " + c + " (other: " + d.getPenalty() + ")");
1471                            }
1472                        }
1473                    }                
1474                    iAllConflicts[type.ordinal()] = conflicts;
1475                }
1476            }
1477        }
1478        
1479        public int countTotalPenalty(Type type, Assignment<Request, Enrollment> assignment) {
1480            int total = 0;
1481            for (Request r1 : getModel().variables()) {
1482                Enrollment e1 = assignment.getValue(r1);
1483                if (e1 == null || r1.equals(iOldVariable)) continue;
1484                for (Request r2 : r1.getStudent().getRequests()) {
1485                    Enrollment e2 = assignment.getValue(r2);
1486                    if (e2 != null && r1.getId() < r2.getId() && !r2.equals(iOldVariable)) {
1487                        if (type.isApplicable(iContext, r1.getStudent(), r1, r2))
1488                            total += penalty(type, e1, e2);
1489                    }
1490                }
1491                total += penalty(type, e1);
1492            }
1493            return total;
1494        }
1495
1496        public Set<Conflict> computeAllConflicts(Type type, Assignment<Request, Enrollment> assignment) {
1497            Set<Conflict> ret = new HashSet<Conflict>();
1498            for (Request r1 : getModel().variables()) {
1499                Enrollment e1 = assignment.getValue(r1);
1500                if (e1 == null || r1.equals(iOldVariable)) continue;
1501                for (Request r2 : r1.getStudent().getRequests()) {
1502                    Enrollment e2 = assignment.getValue(r2);
1503                    if (e2 != null && r1.getId() < r2.getId() && !r2.equals(iOldVariable)) {
1504                        if (type.isApplicable(iContext, r1.getStudent(), r1, r2))
1505                            ret.addAll(conflicts(type, e1, e2));
1506                    }                    
1507                }
1508                ret.addAll(conflicts(type, e1));
1509            }
1510            return ret;
1511        }
1512        
1513        public Set<Conflict> allConflicts(Type type, Assignment<Request, Enrollment> assignment, Student student) {
1514            Set<Conflict> ret = new HashSet<Conflict>();
1515            for (Request r1 : student.getRequests()) {
1516                Enrollment e1 = assignment.getValue(r1);
1517                if (e1 == null) continue;
1518                for (Request r2 : student.getRequests()) {
1519                    Enrollment e2 = assignment.getValue(r2);
1520                    if (e2 != null && r1.getId() < r2.getId()) {
1521                        if (type.isApplicable(iContext, r1.getStudent(), r1, r2))
1522                            ret.addAll(conflicts(type, e1, e2));
1523                    }
1524                }
1525                ret.addAll(conflicts(type, e1));
1526            }
1527            return ret;
1528        }
1529
1530        public Set<Conflict> allConflicts(Type type, Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
1531            Set<Conflict> ret = new HashSet<Conflict>();
1532            for (Request request : enrollment.getStudent().getRequests()) {
1533                if (request.equals(enrollment.getRequest())) continue;
1534                if (assignment.getValue(request) != null && !request.equals(iOldVariable)) {
1535                    ret.addAll(conflicts(type, enrollment, assignment.getValue(request)));
1536                }
1537            }
1538            ret.addAll(conflicts(type, enrollment));
1539            return ret;
1540        }
1541        
1542        public int allPenalty(Type type, Assignment<Request, Enrollment> assignment, Student student) {
1543            int penalty = 0;
1544            for (Request r1 : student.getRequests()) {
1545                Enrollment e1 = assignment.getValue(r1);
1546                if (e1 == null) continue;
1547                for (Request r2 : student.getRequests()) {
1548                    Enrollment e2 = assignment.getValue(r2);
1549                    if (e2 != null && r1.getId() < r2.getId()) {
1550                        if (type.isApplicable(iContext, r1.getStudent(), r1, r2))
1551                            penalty += penalty(type, e1, e2); 
1552                    }
1553                }
1554                penalty += penalty(type, e1);
1555            }
1556            return penalty;
1557        }
1558        
1559        public int allPenalty(Type type, Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
1560            int penalty = 0;
1561            for (Request request : enrollment.getStudent().getRequests()) {
1562                if (request.equals(enrollment.getRequest())) continue;
1563                if (assignment.getValue(request) != null && !request.equals(iOldVariable)) {
1564                    if (type.isApplicable(iContext, enrollment.getStudent(), enrollment.variable(), request))
1565                        penalty += penalty(type, enrollment, assignment.getValue(request));
1566                }
1567            }
1568            penalty += penalty(type, enrollment);
1569            return penalty;
1570        }
1571    }
1572
1573    @Override
1574    public StudentQualityContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
1575        return new StudentQualityContext(assignment);
1576    }
1577
1578    @Override
1579    public StudentQualityContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, StudentQualityContext parentContext) {
1580        return new StudentQualityContext(parentContext);
1581    }
1582    
1583    /** Empty iterator */
1584    public static class Nothing implements Iterable<SctAssignment> {
1585        @Override
1586        public Iterator<SctAssignment> iterator() {
1587            return new Iterator<SctAssignment>() {
1588                @Override
1589                public SctAssignment next() { return null; }
1590                @Override
1591                public boolean hasNext() { return false; }
1592                @Override
1593                public void remove() { throw new UnsupportedOperationException(); }
1594            };
1595        }
1596    }
1597    
1598    /** Unavailabilities of a student */
1599    public static class Unavailabilities implements Iterable<Unavailability> {
1600        private Student iStudent;
1601        public Unavailabilities(Student student) { iStudent = student; }
1602        @Override
1603        public Iterator<Unavailability> iterator() { return iStudent.getUnavailabilities().iterator(); }
1604    }
1605    
1606    private static class SingleTime implements SctAssignment {
1607        private TimeLocation iTime = null;
1608        
1609        public SingleTime(int start, int end) {
1610            iTime = new TimeLocation(0x7f, start, end-start, 0, 0.0, 0, null, null, new BitSet(), 0);
1611        }
1612
1613        @Override
1614        public TimeLocation getTime() { return iTime; }
1615        @Override
1616        public List<RoomLocation> getRooms() { return null; }
1617        @Override
1618        public int getNrRooms() { return 0; }
1619        @Override
1620        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {}
1621        @Override
1622        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {}
1623        @Override
1624        public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) { return null; }
1625        @Override
1626        public boolean isAllowOverlap() { return false; }
1627        @Override
1628        public long getId() { return -1;}
1629        @Override
1630        public int compareById(SctAssignment a) { return 0; }
1631
1632        @Override
1633        public boolean isOverlapping(SctAssignment assignment) {
1634            return assignment.getTime() != null && getTime().shareDays(assignment.getTime()) && getTime().shareHours(assignment.getTime());
1635        }
1636
1637        @Override
1638        public boolean isOverlapping(Set<? extends SctAssignment> assignments) {
1639            for (SctAssignment assignment : assignments) {
1640                if (isOverlapping(assignment)) return true;
1641            }
1642            return false;
1643        }
1644    }
1645    
1646    /** Early/late time */
1647    public static class SingleTimeIterable implements Iterable<SingleTime> {
1648        private SingleTime iTime = null;
1649        public SingleTimeIterable(int start, int end) {
1650            if (start < end)
1651                iTime = new SingleTime(start, end);
1652            
1653        }
1654        @Override
1655        public Iterator<SingleTime> iterator() {
1656            return new Iterator<SingleTime>() {
1657                @Override
1658                public SingleTime next() {
1659                    SingleTime ret = iTime; iTime = null; return ret;
1660                }
1661                @Override
1662                public boolean hasNext() { return iTime != null; }
1663                @Override
1664                public void remove() { throw new UnsupportedOperationException(); }
1665            };
1666        }
1667    }
1668    
1669    /** Free times of a student */
1670    public static class FreeTimes implements Iterable<FreeTimeRequest> {
1671        private Student iStudent;
1672        public FreeTimes(Student student) {
1673            iStudent = student;
1674        }
1675        
1676        @Override
1677        public Iterator<FreeTimeRequest> iterator() {
1678            return new Iterator<FreeTimeRequest>() {
1679                Iterator<Request> i = iStudent.getRequests().iterator();
1680                FreeTimeRequest next = null;
1681                boolean hasNext = nextFreeTime();
1682                
1683                private boolean nextFreeTime() {
1684                    while (i.hasNext()) {
1685                        Request r = i.next();
1686                        if (r instanceof FreeTimeRequest) {
1687                            next = (FreeTimeRequest)r;
1688                            return true;
1689                        }
1690                    }
1691                    return false;
1692                }
1693                
1694                @Override
1695                public FreeTimeRequest next() {
1696                    try {
1697                        return next;
1698                    } finally {
1699                        hasNext = nextFreeTime();
1700                    }
1701                }
1702                @Override
1703                public boolean hasNext() { return hasNext; }
1704                @Override
1705                public void remove() { throw new UnsupportedOperationException(); }
1706            };
1707        }
1708    }
1709    
1710    /** Online (or not-online) classes of an enrollment */
1711    public static class Online implements Iterable<Section> {
1712        private Enrollment iEnrollment;
1713        private boolean iOnline;
1714        public Online(Enrollment enrollment, boolean online) {
1715            iEnrollment = enrollment;
1716            iOnline = online;
1717        }
1718        
1719        protected boolean skip(Section section) {
1720            return iOnline != section.isOnline();
1721        }
1722        
1723        @Override
1724        public Iterator<Section> iterator() {
1725            return new Iterator<Section>() {
1726                Iterator<Section> i = iEnrollment.getSections().iterator();
1727                Section next = null;
1728                boolean hasNext = nextSection();
1729                
1730                private boolean nextSection() {
1731                    while (i.hasNext()) {
1732                        Section r = i.next();
1733                        if (!skip(r)) {
1734                            next = r;
1735                            return true;
1736                        }
1737                    }
1738                    return false;
1739                }
1740                
1741                @Override
1742                public Section next() {
1743                    try {
1744                        return next;
1745                    } finally {
1746                        hasNext = nextSection();
1747                    }
1748                }
1749                @Override
1750                public boolean hasNext() { return hasNext; }
1751                @Override
1752                public void remove() { throw new UnsupportedOperationException(); }
1753            };
1754        }
1755    }
1756
1757    @Override
1758    public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) {
1759        StudentQualityContext cx = getContext(assignment);
1760        if (iContext.isDebug())
1761            for (Type type: iContext.getTypes())
1762                info.put("[Schedule Quality] " + type.getName(), String.valueOf(cx.getTotalPenalty(type)));
1763    }
1764
1765    @Override
1766    public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) {
1767    }
1768    
1769    public String toString(Assignment<Request, Enrollment> assignment) {
1770        String ret = "";
1771        StudentQualityContext cx = getContext(assignment);
1772        for (Type type: iContext.getTypes()) {
1773            int p = cx.getTotalPenalty(type);
1774            if (p != 0) {
1775                ret += (ret.isEmpty() ? "" : ", ") + type.getAbbv() + ": " + p;
1776            }
1777        }
1778        return ret;
1779    }
1780    
1781    public boolean hasDistanceConflict(Student student, Section s1, Section s2) {
1782        if (student.isNeedShortDistances())
1783            return Type.ShortDistance.inConflict(iContext, s1, s2);
1784        else
1785            return Type.Distance.inConflict(iContext, s1, s2);
1786    }
1787}