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