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