    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}