    001package org.cpsolver.studentsct;
    002
    003import java.text.DecimalFormat;
    004import java.util.ArrayList;
    005import java.util.Collection;
    006import java.util.Comparator;
    007import java.util.HashSet;
    008import java.util.List;
    009import java.util.Map;
    010import java.util.Set;
    011import java.util.TreeSet;
    012
    013import org.apache.logging.log4j.Logger;
    014import org.cpsolver.coursett.Constants;
    015import org.cpsolver.ifs.assignment.Assignment;
    016import org.cpsolver.ifs.assignment.InheritedAssignment;
    017import org.cpsolver.ifs.assignment.OptimisticInheritedAssignment;
    018import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
    019import org.cpsolver.ifs.assignment.context.CanInheritContext;
    020import org.cpsolver.ifs.assignment.context.ModelWithContext;
    021import org.cpsolver.ifs.model.Constraint;
    022import org.cpsolver.ifs.model.ConstraintListener;
    023import org.cpsolver.ifs.model.GlobalConstraint;
    024import org.cpsolver.ifs.model.InfoProvider;
    025import org.cpsolver.ifs.model.Model;
    026import org.cpsolver.ifs.solution.Solution;
    027import org.cpsolver.ifs.util.DataProperties;
    028import org.cpsolver.ifs.util.DistanceMetric;
    029import org.cpsolver.studentsct.constraint.CancelledSections;
    030import org.cpsolver.studentsct.constraint.ConfigLimit;
    031import org.cpsolver.studentsct.constraint.CourseLimit;
    032import org.cpsolver.studentsct.constraint.DependentCourses;
    033import org.cpsolver.studentsct.constraint.DisabledSections;
    034import org.cpsolver.studentsct.constraint.FixInitialAssignments;
    035import org.cpsolver.studentsct.constraint.FreeTimeConflicts;
    036import org.cpsolver.studentsct.constraint.HardDistanceConflicts;
    037import org.cpsolver.studentsct.constraint.LinkedSections;
    038import org.cpsolver.studentsct.constraint.RequiredReservation;
    039import org.cpsolver.studentsct.constraint.RequiredRestrictions;
    040import org.cpsolver.studentsct.constraint.RequiredSections;
    041import org.cpsolver.studentsct.constraint.ReservationLimit;
    042import org.cpsolver.studentsct.constraint.SectionLimit;
    043import org.cpsolver.studentsct.constraint.StudentConflict;
    044import org.cpsolver.studentsct.constraint.StudentNotAvailable;
    045import org.cpsolver.studentsct.extension.DistanceConflict;
    046import org.cpsolver.studentsct.extension.StudentQuality;
    047import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
    048import org.cpsolver.studentsct.model.Config;
    049import org.cpsolver.studentsct.model.Course;
    050import org.cpsolver.studentsct.model.CourseRequest;
    051import org.cpsolver.studentsct.model.Enrollment;
    052import org.cpsolver.studentsct.model.Offering;
    053import org.cpsolver.studentsct.model.Request;
    054import org.cpsolver.studentsct.model.RequestGroup;
    055import org.cpsolver.studentsct.model.Section;
    056import org.cpsolver.studentsct.model.Student;
    057import org.cpsolver.studentsct.model.Subpart;
    058import org.cpsolver.studentsct.model.Unavailability;
    059import org.cpsolver.studentsct.model.Request.RequestPriority;
    060import org.cpsolver.studentsct.model.Student.BackToBackPreference;
    061import org.cpsolver.studentsct.model.Student.ModalityPreference;
    062import org.cpsolver.studentsct.model.Student.StudentPriority;
    063import org.cpsolver.studentsct.reservation.Reservation;
    064import org.cpsolver.studentsct.weights.PriorityStudentWeights;
    065import org.cpsolver.studentsct.weights.StudentWeights;
    066
    067/**
    068 * Student sectioning model.
    069 *
    070 * <br>
    071 * <br>
    072 *
    073 * @author  Tomáš Müller
    074 * @version StudentSct 1.3 (Student Sectioning)<br>
    075 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
    076 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    077 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    078 * <br>
    079 *          This library is free software; you can redistribute it and/or modify
    080 *          it under the terms of the GNU Lesser General Public License as
    081 *          published by the Free Software Foundation; either version 3 of the
    082 *          License, or (at your option) any later version. <br>
    083 * <br>
    084 *          This library is distributed in the hope that it will be useful, but
    085 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    086 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    087 *          Lesser General Public License for more details. <br>
    088 * <br>
    089 *          You should have received a copy of the GNU Lesser General Public
    090 *          License along with this library; if not see
    091 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    092 */
    093public class StudentSectioningModel extends ModelWithContext<Request, Enrollment, StudentSectioningModel.StudentSectioningModelContext> implements CanInheritContext<Request, Enrollment, StudentSectioningModel.StudentSectioningModelContext> {
    094    private static Logger sLog = org.apache.logging.log4j.LogManager.getLogger(StudentSectioningModel.class);
    095    protected static DecimalFormat sDecimalFormat = new DecimalFormat("0.00");
    096    private List<Student> iStudents = new ArrayList<Student>();
    097    private List<Offering> iOfferings = new ArrayList<Offering>();
    098    private List<LinkedSections> iLinkedSections = new ArrayList<LinkedSections>();
    099    private DataProperties iProperties;
    100    private DistanceConflict iDistanceConflict = null;
    101    private TimeOverlapsCounter iTimeOverlaps = null;
    102    private StudentQuality iStudentQuality = null;
    103    private int iNrDummyStudents = 0, iNrDummyRequests = 0;
    104    private int[] iNrPriorityStudents = null;
    105    private double iTotalDummyWeight = 0.0;
    106    private double iTotalCRWeight = 0.0, iTotalDummyCRWeight = 0.0;
    107    private double[] iTotalPriorityCRWeight = null;
    108    private double[] iTotalCriticalCRWeight;
    109    private double[][] iTotalPriorityCriticalCRWeight;
    110    private double iTotalMPPCRWeight = 0.0;
    111    private double iTotalSelCRWeight = 0.0;
    112    private double iBestAssignedCourseRequestWeight = 0.0;
    113    private StudentWeights iStudentWeights = null;
    114    private boolean iReservationCanAssignOverTheLimit;
    115    private boolean iMPP;
    116    private boolean iKeepInitials;
    117    protected double iProjectedStudentWeight = 0.0100;
    118    private int iMaxDomainSize = -1;
    119    private int iDayOfWeekOffset = 0;
    120    private DependentCourses iDependentCourses = null;
    121
    122    /**
    123     * Constructor
    124     *
    125     * @param properties
    126     *            configuration
    127     */
    128    @SuppressWarnings("unchecked")
    129    public StudentSectioningModel(DataProperties properties) {
    130        super();
    131        iTotalCriticalCRWeight = new double[RequestPriority.values().length];
    132        iTotalPriorityCriticalCRWeight = new double[RequestPriority.values().length][StudentPriority.values().length];
    133        for (int i = 0; i < RequestPriority.values().length; i++) {
    134            iTotalCriticalCRWeight[i] = 0.0;
    135            for (int j = 0; j < StudentPriority.values().length; j++) {
    136                iTotalPriorityCriticalCRWeight[i][j] = 0.0;
    137            }
    138        }
    139        iNrPriorityStudents = new int[StudentPriority.values().length];
    140        iTotalPriorityCRWeight = new double[StudentPriority.values().length];
    141        for (int i = 0; i < StudentPriority.values().length; i++) {
    142            iNrPriorityStudents[i] = 0;
    143            iTotalPriorityCRWeight[i] = 0.0;
    144        }
    145        iReservationCanAssignOverTheLimit =  properties.getPropertyBoolean("Reservation.CanAssignOverTheLimit", false);
    146        iMPP = properties.getPropertyBoolean("General.MPP", false);
    147        iKeepInitials = properties.getPropertyBoolean("Sectioning.KeepInitialAssignments", false);
    148        iStudentWeights = new PriorityStudentWeights(properties);
    149        iMaxDomainSize = properties.getPropertyInt("Sectioning.MaxDomainSize", iMaxDomainSize);
    150        iDayOfWeekOffset = properties.getPropertyInt("DatePattern.DayOfWeekOffset", 0);
    151        if (properties.getPropertyBoolean("Sectioning.SectionLimit", true)) {
    152            SectionLimit sectionLimit = new SectionLimit(properties);
    153            addGlobalConstraint(sectionLimit);
    154            if (properties.getPropertyBoolean("Sectioning.SectionLimit.Debug", false)) {
    155                sectionLimit.addConstraintListener(new ConstraintListener<Request, Enrollment>() {
    156                    @Override
    157                    public void constraintBeforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Constraint<Request, Enrollment> constraint, Enrollment enrollment, Set<Enrollment> unassigned) {
    158                        if (enrollment.getStudent().isDummy())
    159                            for (Enrollment conflict : unassigned) {
    160                                if (!conflict.getStudent().isDummy()) {
    161                                    sLog.warn("Enrolment of a real student " + conflict.getStudent() + " is unassigned "
    162                                            + "\n  -- " + conflict + "\ndue to an enrollment of a dummy student "
    163                                            + enrollment.getStudent() + " " + "\n  -- " + enrollment);
    164                                }
    165                            }
    166                    }
    167
    168                    @Override
    169                    public void constraintAfterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Constraint<Request, Enrollment> constraint, Enrollment assigned, Set<Enrollment> unassigned) {
    170                    }
    171                });
    172            }
    173        }
    174        if (properties.getPropertyBoolean("Sectioning.ConfigLimit", true)) {
    175            ConfigLimit configLimit = new ConfigLimit(properties);
    176            addGlobalConstraint(configLimit);
    177        }
    178        if (properties.getPropertyBoolean("Sectioning.CourseLimit", true)) {
    179            CourseLimit courseLimit = new CourseLimit(properties);
    180            addGlobalConstraint(courseLimit);
    181        }
    182        if (properties.getPropertyBoolean("Sectioning.ReservationLimit", true)) {
    183            ReservationLimit reservationLimit = new ReservationLimit(properties);
    184            addGlobalConstraint(reservationLimit);
    185        }
    186        if (properties.getPropertyBoolean("Sectioning.RequiredReservations", true)) {
    187            RequiredReservation requiredReservation = new RequiredReservation();
    188            addGlobalConstraint(requiredReservation);
    189        }
    190        if (properties.getPropertyBoolean("Sectioning.CancelledSections", true)) {
    191            CancelledSections cancelledSections = new CancelledSections();
    192            addGlobalConstraint(cancelledSections);
    193        }
    194        if (properties.getPropertyBoolean("Sectioning.StudentNotAvailable", true)) {
    195            StudentNotAvailable studentNotAvailable = new StudentNotAvailable();
    196            addGlobalConstraint(studentNotAvailable);
    197        }
    198        if (properties.getPropertyBoolean("Sectioning.DisabledSections", true)) {
    199            DisabledSections disabledSections = new DisabledSections();
    200            addGlobalConstraint(disabledSections);
    201        }
    202        if (properties.getPropertyBoolean("Sectioning.RequiredSections", true)) {
    203            RequiredSections requiredSections = new RequiredSections();
    204            addGlobalConstraint(requiredSections);
    205        }
    206        if (properties.getPropertyBoolean("Sectioning.RequiredRestrictions", true)) {
    207            RequiredRestrictions requiredRestrictions = new RequiredRestrictions();
    208            addGlobalConstraint(requiredRestrictions);
    209        }
    210        if (properties.getPropertyBoolean("Sectioning.HardDistanceConflict", false)) {
    211            HardDistanceConflicts hardDistanceConflicts = new HardDistanceConflicts();
    212            addGlobalConstraint(hardDistanceConflicts);
    213        }
    214        if (properties.getPropertyBoolean("Sectioning.FreeTimeConflict", false)) {
    215            FreeTimeConflicts freeTimeConflicts = new FreeTimeConflicts();
    216            addGlobalConstraint(freeTimeConflicts);
    217        }
    218        if (iMPP && iKeepInitials) {
    219            addGlobalConstraint(new FixInitialAssignments());
    220        }
    221        try {
    222            Class<StudentWeights> studentWeightsClass = (Class<StudentWeights>)Class.forName(properties.getProperty("StudentWeights.Class", PriorityStudentWeights.class.getName()));
    223            iStudentWeights = studentWeightsClass.getConstructor(DataProperties.class).newInstance(properties);
    224        } catch (Exception e) {
    225            sLog.error("Unable to create custom student weighting model (" + e.getMessage() + "), using default.", e);
    226            iStudentWeights = new PriorityStudentWeights(properties);
    227        }
    228        iProjectedStudentWeight = properties.getPropertyDouble("StudentWeights.ProjectedStudentWeight", iProjectedStudentWeight);
    229        iProperties = properties;
    230    }
    231
    232    /**
    233     * Return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit
    234     * @return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit
    235     */
    236    public boolean getReservationCanAssignOverTheLimit() {
    237        return iReservationCanAssignOverTheLimit;
    238    }
    239
    240    /**
    241     * Return true if the problem is minimal perturbation problem
    242     * @return true if MPP is enabled
    243     */
    244    public boolean isMPP() {
    245        return iMPP;
    246    }
    247
    248    /**
    249     * Return true if the inital assignments are to be kept unchanged
    250     * @return true if the initial assignments are to be kept at all cost
    251     */
    252    public boolean getKeepInitialAssignments() {
    253        return iKeepInitials;
    254    }
    255
    256    /**
    257     * Return student weighting model
    258     * @return student weighting model
    259     */
    260    public StudentWeights getStudentWeights() {
    261        return iStudentWeights;
    262    }
    263
    264    /**
    265     * Set student weighting model
    266     * @param weights student weighting model
    267     */
    268    public void setStudentWeights(StudentWeights weights) {
    269        iStudentWeights = weights;
    270    }
    271
    272    /**
    273     * Students
    274     * @return all students in the problem
    275     */
    276    public List<Student> getStudents() {
    277        return iStudents;
    278    }
    279
    280    /**
    281     * Add a student into the model
    282     * @param student a student to be added into the problem
    283     */
    284    public void addStudent(Student student) {
    285        iStudents.add(student);
    286        if (student.isDummy())
    287            iNrDummyStudents++;
    288        iNrPriorityStudents[student.getPriority().ordinal()]++;
    289        for (Request request : student.getRequests())
    290            addVariable(request);
    291        if (getProperties().getPropertyBoolean("Sectioning.StudentConflict", true)) {
    292            addConstraint(new StudentConflict(student));
    293        }
    294    }
    295
    296    public int getNbrStudents(StudentPriority priority) {
    297        return iNrPriorityStudents[priority.ordinal()];
    298    }
    299
    300    @Override
    301    public void addVariable(Request request) {
    302        super.addVariable(request);
    303        if (request instanceof CourseRequest && !request.isAlternative())
    304            iTotalCRWeight += request.getWeight();
    305        if (request instanceof CourseRequest && request.getRequestPriority() != RequestPriority.Normal && !request.getStudent().isDummy() && !request.isAlternative())
    306            iTotalCriticalCRWeight[request.getRequestPriority().ordinal()] += request.getWeight();
    307        if (request instanceof CourseRequest && request.getRequestPriority() != RequestPriority.Normal && !request.isAlternative())
    308            iTotalPriorityCriticalCRWeight[request.getRequestPriority().ordinal()][request.getStudent().getPriority().ordinal()] += request.getWeight();
    309        if (request.getStudent().isDummy()) {
    310            iNrDummyRequests++;
    311            iTotalDummyWeight += request.getWeight();
    312            if (request instanceof CourseRequest && !request.isAlternative())
    313                iTotalDummyCRWeight += request.getWeight();
    314        }
    315        if (request instanceof CourseRequest && !request.isAlternative())
    316            iTotalPriorityCRWeight[request.getStudent().getPriority().ordinal()] += request.getWeight();
    317        if (request.isMPP())
    318            iTotalMPPCRWeight += request.getWeight();
    319        if (request.hasSelection())
    320            iTotalSelCRWeight += request.getWeight();
    321    }
    322
    323    public void setCourseRequestPriority(CourseRequest request, RequestPriority priority) {
    324        if (request.getRequestPriority() != RequestPriority.Normal && !request.getStudent().isDummy() && !request.isAlternative())
    325            iTotalCriticalCRWeight[request.getRequestPriority().ordinal()] -= request.getWeight();
    326        if (request.getRequestPriority() != RequestPriority.Normal && !request.isAlternative())
    327            iTotalPriorityCriticalCRWeight[request.getRequestPriority().ordinal()][request.getStudent().getPriority().ordinal()] -= request.getWeight();
    328        request.setRequestPriority(priority);
    329        if (request.getRequestPriority() != RequestPriority.Normal && !request.getStudent().isDummy() && !request.isAlternative())
    330            iTotalCriticalCRWeight[request.getRequestPriority().ordinal()] += request.getWeight();
    331        if (request.getRequestPriority() != RequestPriority.Normal && !request.isAlternative())
    332            iTotalPriorityCriticalCRWeight[request.getRequestPriority().ordinal()][request.getStudent().getPriority().ordinal()] += request.getWeight();
    333    }
    334
    335    /**
    336     * Recompute cached request weights
    337     * @param assignment current assignment
    338     */
    339    public void requestWeightsChanged(Assignment<Request, Enrollment> assignment) {
    340        getContext(assignment).requestWeightsChanged(assignment);
    341    }
    342
    343    /**
    344     * Remove a student from the model
    345     * @param student a student to be removed from the problem
    346     */
    347    public void removeStudent(Student student) {
    348        iStudents.remove(student);
    349        if (student.isDummy())
    350            iNrDummyStudents--;
    351        iNrPriorityStudents[student.getPriority().ordinal()]--;
    352        StudentConflict conflict = null;
    353        for (Request request : student.getRequests()) {
    354            for (Constraint<Request, Enrollment> c : request.constraints()) {
    355                if (c instanceof StudentConflict) {
    356                    conflict = (StudentConflict) c;
    357                    break;
    358                }
    359            }
    360            if (conflict != null)
    361                conflict.removeVariable(request);
    362            removeVariable(request);
    363        }
    364        if (conflict != null)
    365            removeConstraint(conflict);
    366    }
    367
    368    @Override
    369    public void removeVariable(Request request) {
    370        super.removeVariable(request);
    371        if (request instanceof CourseRequest) {
    372            CourseRequest cr = (CourseRequest)request;
    373            for (Course course: cr.getCourses())
    374                course.getRequests().remove(request);
    375        }
    376        if (request.getStudent().isDummy()) {
    377            iNrDummyRequests--;
    378            iTotalDummyWeight -= request.getWeight();
    379            if (request instanceof CourseRequest && !request.isAlternative())
    380                iTotalDummyCRWeight -= request.getWeight();
    381        }
    382        if (request instanceof CourseRequest && !request.isAlternative())
    383            iTotalPriorityCRWeight[request.getStudent().getPriority().ordinal()] -= request.getWeight();
    384        if (request.isMPP())
    385            iTotalMPPCRWeight -= request.getWeight();
    386        if (request.hasSelection())
    387            iTotalSelCRWeight -= request.getWeight();
    388        if (request instanceof CourseRequest && !request.isAlternative())
    389            iTotalCRWeight -= request.getWeight();
    390        if (request instanceof CourseRequest && request.getRequestPriority() != RequestPriority.Normal && !request.getStudent().isDummy() && !request.isAlternative())
    391            iTotalCriticalCRWeight[request.getRequestPriority().ordinal()] -= request.getWeight();
    392        if (request instanceof CourseRequest && request.getRequestPriority() != RequestPriority.Normal && !request.isAlternative())
    393            iTotalPriorityCriticalCRWeight[request.getRequestPriority().ordinal()][request.getStudent().getPriority().ordinal()] -= request.getWeight();
    394    }
    395
    396
    397    /**
    398     * List of offerings
    399     * @return all instructional offerings of the problem
    400     */
    401    public List<Offering> getOfferings() {
    402        return iOfferings;
    403    }
    404
    405    /**
    406     * Add an offering into the model
    407     * @param offering an instructional offering to be added into the problem
    408     */
    409    public void addOffering(Offering offering) {
    410        iOfferings.add(offering);
    411        offering.setModel(this);
    412    }
    413
    414    /**
    415     * Link sections using {@link LinkedSections}
    416     * @param mustBeUsed if true,  a pair of linked sections must be used when a student requests both courses
    417     * @param sections a linked section constraint to be added into the problem
    418     */
    419    public void addLinkedSections(boolean mustBeUsed, Section... sections) {
    420        LinkedSections constraint = new LinkedSections(sections);
    421        constraint.setMustBeUsed(mustBeUsed);
    422        iLinkedSections.add(constraint);
    423        constraint.createConstraints();
    424    }
    425
    426    /**
    427     * Link sections using {@link LinkedSections}
    428     * @param sections a linked section constraint to be added into the problem
    429     */
    430    @Deprecated
    431    public void addLinkedSections(Section... sections) {
    432        addLinkedSections(false, sections);
    433    }
    434
    435    /**
    436     * Link sections using {@link LinkedSections}
    437     * @param mustBeUsed if true,  a pair of linked sections must be used when a student requests both courses
    438     * @param sections a linked section constraint to be added into the problem
    439     */
    440    public void addLinkedSections(boolean mustBeUsed, Collection<Section> sections) {
    441        LinkedSections constraint = new LinkedSections(sections);
    442        constraint.setMustBeUsed(mustBeUsed);
    443        iLinkedSections.add(constraint);
    444        constraint.createConstraints();
    445    }
    446
    447    /**
    448     * Link sections using {@link LinkedSections}
    449     * @param sections a linked section constraint to be added into the problem
    450     */
    451    @Deprecated
    452    public void addLinkedSections(Collection<Section> sections) {
    453        addLinkedSections(false, sections);
    454    }
    455
    456    /**
    457     * List of linked sections
    458     * @return all linked section constraints of the problem
    459     */
    460    public List<LinkedSections> getLinkedSections() {
    461        return iLinkedSections;
    462    }
    463
    464    /**
    465     * Model info
    466     */
    467    @Override
    468    public Map<String, String> getInfo(Assignment<Request, Enrollment> assignment) {
    469        Map<String, String> info = super.getInfo(assignment);
    470        StudentSectioningModelContext context = getContext(assignment);
    471        if (!getStudents().isEmpty())
    472            info.put("Students with complete schedule", sDoubleFormat.format(100.0 * context.nrComplete() / getStudents().size()) + "% (" + context.nrComplete() + "/" + getStudents().size() + ")");
    473        String priorityComplete = "";
    474        for (StudentPriority sp: StudentPriority.values()) {
    475            if (sp != StudentPriority.Dummy && iNrPriorityStudents[sp.ordinal()] > 0)
    476                priorityComplete += (priorityComplete.isEmpty() ? "" : "\n") +
    477                    sp.name() + ": " + sDoubleFormat.format(100.0 * context.iNrCompletePriorityStudents[sp.ordinal()] / iNrPriorityStudents[sp.ordinal()]) + "% (" + context.iNrCompletePriorityStudents[sp.ordinal()] + "/" + iNrPriorityStudents[sp.ordinal()] + ")";
    478        }
    479        if (!priorityComplete.isEmpty())
    480            info.put("Students with complete schedule (priority students)", priorityComplete);
    481        if (getStudentQuality() != null) {
    482            int confs = getStudentQuality().getTotalPenalty(StudentQuality.Type.Distance, assignment);
    483            int shortConfs = getStudentQuality().getTotalPenalty(StudentQuality.Type.ShortDistance, assignment);
    484            int unavConfs = getStudentQuality().getTotalPenalty(StudentQuality.Type.UnavailabilityDistance, assignment);
    485            if (confs > 0 || shortConfs > 0) {
    486                info.put("Student distance conflicts", confs + (shortConfs == 0 ? "" : " (" + getDistanceMetric().getShortDistanceAccommodationReference() + ": " + shortConfs + ")"));
    487            }
    488            if (unavConfs > 0) {
    489                info.put("Unavailabilities: Distance conflicts", String.valueOf(unavConfs));
    490            }
    491        } else if (getDistanceConflict() != null) {
    492            int confs = getDistanceConflict().getTotalNrConflicts(assignment);
    493            if (confs > 0) {
    494                int shortConfs = getDistanceConflict().getTotalNrShortConflicts(assignment);
    495                info.put("Student distance conflicts", confs + (shortConfs == 0 ? "" : " (" + getDistanceConflict().getDistanceMetric().getShortDistanceAccommodationReference() + ": " + shortConfs + ")"));
    496            }
    497        }
    498        if (getStudentQuality() != null) {
    499            int shareCR = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.CourseTimeOverlap, assignment);
    500            int shareFT = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.FreeTimeOverlap, assignment);
    501            int shareUN = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.Unavailability, assignment);
    502            if (shareCR + shareFT + shareUN > 0)
    503                info.put("Time overlapping conflicts", sDoubleFormat.format((5.0 * (shareCR + shareFT + shareUN)) / iStudents.size()) + " mins per student\n" +
    504                        "(" + sDoubleFormat.format(5.0 * shareCR / iStudents.size()) + " between courses, " + sDoubleFormat.format(5.0 * shareFT / iStudents.size()) + " free time" +
    505                        (shareUN == 0 ? "" : ", " + sDoubleFormat.format(5.0 * shareUN / iStudents.size()) + " teaching assignments & unavailabilities") + "; " + sDoubleFormat.format((shareCR + shareFT + shareUN) / 12.0) + " hours total)");
    506        } else if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts(assignment) != 0) {
    507            info.put("Time overlapping conflicts", sDoubleFormat.format(5.0 * getTimeOverlaps().getTotalNrConflicts(assignment) / iStudents.size()) + " mins per student (" + sDoubleFormat.format(getTimeOverlaps().getTotalNrConflicts(assignment) / 12.0) + " hours total)");
    508        }
    509        if (getStudentQuality() != null) {
    510            int confLunch = getStudentQuality().getTotalPenalty(StudentQuality.Type.LunchBreak, assignment);
    511            if (confLunch > 0)
    512                info.put("Schedule Quality: Lunch conflicts", sDoubleFormat.format(20.0 * confLunch / getNrRealStudents(false)) + "% (" + confLunch + ")");
    513            int confTravel = getStudentQuality().getTotalPenalty(StudentQuality.Type.TravelTime, assignment);
    514            if (confTravel > 0)
    515                info.put("Schedule Quality: Travel time", sDoubleFormat.format(((double)confTravel) / getNrRealStudents(false)) + " mins per student (" + sDecimalFormat.format(confTravel / 60.0) + " hours total)");
    516            int confBtB = getStudentQuality().getTotalPenalty(StudentQuality.Type.BackToBack, assignment);
    517            if (confBtB != 0)
    518                info.put("Schedule Quality: Back-to-back classes", sDoubleFormat.format(((double)confBtB) / getNrRealStudents(false)) + " per student (" + confBtB + ")");
    519            int confMod = getStudentQuality().getTotalPenalty(StudentQuality.Type.Modality, assignment);
    520            if (confMod > 0)
    521                info.put("Schedule Quality: Online class preference", sDoubleFormat.format(((double)confMod) / getNrRealStudents(false)) + " per student (" + confMod + ")");
    522            int confWorkDay = getStudentQuality().getTotalPenalty(StudentQuality.Type.WorkDay, assignment);
    523            if (confWorkDay > 0)
    524                info.put("Schedule Quality: Work day", sDoubleFormat.format(5.0 * confWorkDay / getNrRealStudents(false)) + " mins over " +
    525                        new DecimalFormat("0.#").format(getProperties().getPropertyInt("WorkDay.WorkDayLimit", 6*12) / 12.0) + " hours a day per student\n(from start to end, " + sDoubleFormat.format(confWorkDay / 12.0) + " hours total)");
    526            int early = getStudentQuality().getTotalPenalty(StudentQuality.Type.TooEarly, assignment);
    527            if (early > 0) {
    528                int min = getProperties().getPropertyInt("WorkDay.EarlySlot", 102) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
    529                int h = min / 60;
    530                int m = min % 60;
    531                String time = (getProperties().getPropertyBoolean("General.UseAmPm", true) ? (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h >= 12 ? "p" : "a") : h + ":" + (m < 10 ? "0" : "") + m);
    532                info.put("Schedule Quality: Early classes", sDoubleFormat.format(5.0 * early / iStudents.size()) + " mins before " + time + " per student (" + sDoubleFormat.format(early / 12.0) + " hours total)");
    533            }
    534            int late = getStudentQuality().getTotalPenalty(StudentQuality.Type.TooLate, assignment);
    535            if (late > 0) {
    536                int min = getProperties().getPropertyInt("WorkDay.LateSlot", 210) * Constants.SLOT_LENGTH_MIN + Constants.FIRST_SLOT_TIME_MIN;
    537                int h = min / 60;
    538                int m = min % 60;
    539                String time = (getProperties().getPropertyBoolean("General.UseAmPm", true) ? (h > 12 ? h - 12 : h) + ":" + (m < 10 ? "0" : "") + m + (h >= 12 ? "p" : "a") : h + ":" + (m < 10 ? "0" : "") + m);
    540                info.put("Schedule Quality: Late classes", sDoubleFormat.format(5.0 * late / iStudents.size()) + " mins after " + time + " per student (" + sDoubleFormat.format(late / 12.0) + " hours total)");
    541            }
    542            int accFT = getStudentQuality().getTotalPenalty(StudentQuality.Type.AccFreeTimeOverlap, assignment);
    543            if (accFT > 0) {
    544                info.put("Accommodations: Free time conflicts", sDoubleFormat.format(5.0 * accFT / getStudentsWithAccommodation(getStudentQuality().getStudentQualityContext().getFreeTimeAccommodation())) + " mins per student, " + sDoubleFormat.format(accFT / 12.0) + " hours total");
    545            }
    546            int accBtB = getStudentQuality().getTotalPenalty(StudentQuality.Type.AccBackToBack, assignment);
    547            if (accBtB > 0) {
    548                info.put("Accommodations: Back-to-back classes", sDoubleFormat.format(((double)accBtB) / getStudentsWithAccommodation(getStudentQuality().getStudentQualityContext().getBackToBackAccommodation())) + " non-BTB classes per student, " + accBtB + " total");
    549            }
    550            int accBbc = getStudentQuality().getTotalPenalty(StudentQuality.Type.AccBreaksBetweenClasses, assignment);
    551            if (accBbc > 0) {
    552                info.put("Accommodations: Break between classes", sDoubleFormat.format(((double)accBbc) / getStudentsWithAccommodation(getStudentQuality().getStudentQualityContext().getBreakBetweenClassesAccommodation())) + " BTB classes per student, " + accBbc + " total");
    553            }
    554            int shortConfs = getStudentQuality().getTotalPenalty(StudentQuality.Type.ShortDistance, assignment);
    555            if (shortConfs > 0) {
    556                info.put("Accommodations: Distance conflicts", sDoubleFormat.format(((double)shortConfs) / getStudentsWithAccommodation(getStudentQuality().getDistanceMetric().getShortDistanceAccommodationReference())) + " short distance conflicts per student, " + shortConfs + " total");
    557            }
    558        }
    559        int nrLastLikeStudents = getNrLastLikeStudents(false);
    560        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
    561            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
    562            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(assignment, false);
    563            int nrRealCompleteStudents = context.nrComplete() - nrLastLikeCompleteStudents;
    564            if (nrLastLikeStudents > 0)
    565                info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
    566                        * nrLastLikeCompleteStudents / nrLastLikeStudents)
    567                        + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
    568            if (nrRealStudents > 0)
    569                info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
    570                        / nrRealStudents)
    571                        + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
    572            int nrLastLikeRequests = getNrLastLikeRequests(false);
    573            int nrRealRequests = variables().size() - nrLastLikeRequests;
    574            int nrLastLikeAssignedRequests = context.getNrAssignedLastLikeRequests();
    575            int nrRealAssignedRequests = assignment.nrAssignedVariables() - nrLastLikeAssignedRequests;
    576            if (nrLastLikeRequests > 0)
    577                info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests / nrLastLikeRequests)
    578                        + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
    579            if (nrRealRequests > 0)
    580                info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
    581                        + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
    582        }
    583        context.getInfo(assignment, info);
    584
    585        double groupSpread = 0.0; double groupCount = 0;
    586        for (Offering offering: iOfferings) {
    587            for (Course course: offering.getCourses()) {
    588                for (RequestGroup group: course.getRequestGroups()) {
    589                    groupSpread += group.getAverageSpread(assignment) * group.getEnrollmentWeight(assignment, null);
    590                    groupCount += group.getEnrollmentWeight(assignment, null);
    591                }
    592            }
    593        }
    594        if (groupCount > 0)
    595            info.put("Same group", sDecimalFormat.format(100.0 * groupSpread / groupCount) + "%");
    596
    597        return info;
    598    }
    599
    600    /**
    601     * Overall solution value
    602     * @param assignment current assignment
    603     * @param precise true if should be computed
    604     * @return solution value
    605     */
    606    public double getTotalValue(Assignment<Request, Enrollment> assignment, boolean precise) {
    607        if (precise) {
    608            double total = 0;
    609            for (Request r: assignment.assignedVariables())
    610                total += r.getWeight() * iStudentWeights.getWeight(assignment, assignment.getValue(r));
    611            if (iDistanceConflict != null)
    612                for (DistanceConflict.Conflict c: iDistanceConflict.computeAllConflicts(assignment))
    613                    total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
    614            if (iTimeOverlaps != null)
    615                for (TimeOverlapsCounter.Conflict c: iTimeOverlaps.getContext(assignment).computeAllConflicts(assignment)) {
    616                    if (c.getR1() != null) total -= c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
    617                    if (c.getR2() != null) total -= c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
    618                }
    619            if (iStudentQuality != null)
    620                for (StudentQuality.Type t: StudentQuality.Type.values()) {
    621                    for (StudentQuality.Conflict c: iStudentQuality.getContext(assignment).computeAllConflicts(t, assignment)) {
    622                        switch (c.getType().getType()) {
    623                            case REQUEST:
    624                                if (c.getR1() instanceof CourseRequest)
    625                                    total -= c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    626                                else
    627                                    total -= c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    628                                break;
    629                            case BOTH:
    630                                total -= c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    631                                total -= c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    632                                break;
    633                            case LOWER:
    634                                total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    635                                break;
    636                            case HIGHER:
    637                                total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    638                                break;
    639                        }
    640                    }
    641                }
    642            return -total;
    643        }
    644        return getContext(assignment).getTotalValue();
    645    }
    646
    647    /**
    648     * Overall solution value
    649     */
    650    @Override
    651    public double getTotalValue(Assignment<Request, Enrollment> assignment) {
    652        return getContext(assignment).getTotalValue();
    653    }
    654
    655    /**
    656     * Configuration
    657     * @return solver configuration
    658     */
    659    public DataProperties getProperties() {
    660        return iProperties;
    661    }
    662
    663    /**
    664     * Empty online student sectioning infos for all sections (see
    665     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
    666     */
    667    public void clearOnlineSectioningInfos() {
    668        for (Offering offering : iOfferings) {
    669            for (Config config : offering.getConfigs()) {
    670                for (Subpart subpart : config.getSubparts()) {
    671                    for (Section section : subpart.getSections()) {
    672                        section.setSpaceExpected(0);
    673                        section.setSpaceHeld(0);
    674                    }
    675                }
    676            }
    677        }
    678    }
    679
    680    /**
    681     * Compute online student sectioning infos for all sections (see
    682     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
    683     * @param assignment current assignment
    684     */
    685    public void computeOnlineSectioningInfos(Assignment<Request, Enrollment> assignment) {
    686        clearOnlineSectioningInfos();
    687        for (Student student : getStudents()) {
    688            if (!student.isDummy())
    689                continue;
    690            for (Request request : student.getRequests()) {
    691                if (!(request instanceof CourseRequest))
    692                    continue;
    693                CourseRequest courseRequest = (CourseRequest) request;
    694                Enrollment enrollment = assignment.getValue(courseRequest);
    695                if (enrollment != null) {
    696                    for (Section section : enrollment.getSections()) {
    697                        section.setSpaceHeld(courseRequest.getWeight() + section.getSpaceHeld());
    698                    }
    699                }
    700                List<Enrollment> feasibleEnrollments = new ArrayList<Enrollment>();
    701                int totalLimit = 0;
    702                for (Enrollment enrl : courseRequest.values(assignment)) {
    703                    boolean overlaps = false;
    704                    for (Request otherRequest : student.getRequests()) {
    705                        if (otherRequest.equals(courseRequest) || !(otherRequest instanceof CourseRequest))
    706                            continue;
    707                        Enrollment otherErollment = assignment.getValue(otherRequest);
    708                        if (otherErollment == null)
    709                            continue;
    710                        if (enrl.isOverlapping(otherErollment)) {
    711                            overlaps = true;
    712                            break;
    713                        }
    714                    }
    715                    if (!overlaps) {
    716                        feasibleEnrollments.add(enrl);
    717                        if (totalLimit >= 0) {
    718                            int limit = enrl.getLimit();
    719                            if (limit < 0) totalLimit = -1;
    720                            else totalLimit += limit;
    721                        }
    722                    }
    723                }
    724                double increment = courseRequest.getWeight() / (totalLimit > 0 ? totalLimit : feasibleEnrollments.size());
    725                for (Enrollment feasibleEnrollment : feasibleEnrollments) {
    726                    for (Section section : feasibleEnrollment.getSections()) {
    727                        if (totalLimit > 0) {
    728                            section.setSpaceExpected(section.getSpaceExpected() + increment * feasibleEnrollment.getLimit());
    729                        } else {
    730                            section.setSpaceExpected(section.getSpaceExpected() + increment);
    731                        }
    732                    }
    733                }
    734            }
    735        }
    736    }
    737
    738    /**
    739     * Sum of weights of all requests that are not assigned (see
    740     * {@link Request#getWeight()}).
    741     * @param assignment current assignment
    742     * @return unassigned request weight
    743     */
    744    public double getUnassignedRequestWeight(Assignment<Request, Enrollment> assignment) {
    745        double weight = 0.0;
    746        for (Request request : assignment.unassignedVariables(this)) {
    747            weight += request.getWeight();
    748        }
    749        return weight;
    750    }
    751
    752    /**
    753     * Sum of weights of all requests (see {@link Request#getWeight()}).
    754     * @return total request weight
    755     */
    756    public double getTotalRequestWeight() {
    757        double weight = 0.0;
    758        for (Request request : variables()) {
    759            weight += request.getWeight();
    760        }
    761        return weight;
    762    }
    763
    764    /**
    765     * Set distance conflict extension
    766     * @param dc distance conflicts extension
    767     */
    768    public void setDistanceConflict(DistanceConflict dc) {
    769        iDistanceConflict = dc;
    770    }
    771
    772    /**
    773     * Return distance conflict extension
    774     * @return distance conflicts extension
    775     */
    776    public DistanceConflict getDistanceConflict() {
    777        return iDistanceConflict;
    778    }
    779
    780    /**
    781     * Set time overlaps extension
    782     * @param toc time overlapping conflicts extension
    783     */
    784    public void setTimeOverlaps(TimeOverlapsCounter toc) {
    785        iTimeOverlaps = toc;
    786    }
    787
    788    /**
    789     * Return time overlaps extension
    790     * @return time overlapping conflicts extension
    791     */
    792    public TimeOverlapsCounter getTimeOverlaps() {
    793        return iTimeOverlaps;
    794    }
    795
    796    public StudentQuality getStudentQuality() { return iStudentQuality; }
    797    public void setStudentQuality(StudentQuality q, boolean register) {
    798        if (iStudentQuality != null)
    799            getInfoProviders().remove(iStudentQuality);
    800        iStudentQuality = q;
    801        if (iStudentQuality != null)
    802            getInfoProviders().add(iStudentQuality);
    803        if (register) {
    804            iStudentQuality.setAssignmentContextReference(createReference(iStudentQuality));
    805            iStudentQuality.register(this);
    806        }
    807    }
    808
    809    public void setStudentQuality(StudentQuality q) {
    810        setStudentQuality(q, true);
    811    }
    812
    813    /**
    814     * Average priority of unassigned requests (see
    815     * {@link Request#getPriority()})
    816     * @param assignment current assignment
    817     * @return average priority of unassigned requests
    818     */
    819    public double avgUnassignPriority(Assignment<Request, Enrollment> assignment) {
    820        double totalPriority = 0.0;
    821        for (Request request : assignment.unassignedVariables(this)) {
    822            if (request.isAlternative())
    823                continue;
    824            totalPriority += request.getPriority();
    825        }
    826        return 1.0 + totalPriority / assignment.nrUnassignedVariables(this);
    827    }
    828
    829    /**
    830     * Average number of requests per student (see {@link Student#getRequests()}
    831     * )
    832     * @return average number of requests per student
    833     */
    834    public double avgNrRequests() {
    835        double totalRequests = 0.0;
    836        int totalStudents = 0;
    837        for (Student student : getStudents()) {
    838            if (student.nrRequests() == 0)
    839                continue;
    840            totalRequests += student.nrRequests();
    841            totalStudents++;
    842        }
    843        return totalRequests / totalStudents;
    844    }
    845
    846    /** Number of last like ({@link Student#isDummy()} equals true) students.
    847     * @param precise true if to be computed
    848     * @return number of last like (projected) students
    849     **/
    850    public int getNrLastLikeStudents(boolean precise) {
    851        if (!precise)
    852            return iNrDummyStudents;
    853        int nrLastLikeStudents = 0;
    854        for (Student student : getStudents()) {
    855            if (student.isDummy())
    856                nrLastLikeStudents++;
    857        }
    858        return nrLastLikeStudents;
    859    }
    860
    861    /** Number of real ({@link Student#isDummy()} equals false) students.
    862     * @param precise true if to be computed
    863     * @return number of real students
    864     **/
    865    public int getNrRealStudents(boolean precise) {
    866        if (!precise)
    867            return getStudents().size() - iNrDummyStudents;
    868        int nrRealStudents = 0;
    869        for (Student student : getStudents()) {
    870            if (!student.isDummy())
    871                nrRealStudents++;
    872        }
    873        return nrRealStudents;
    874    }
    875
    876    /**
    877     * Count students with given accommodation
    878     */
    879    public int getStudentsWithAccommodation(String acc) {
    880        int nrAccStudents = 0;
    881        for (Student student : getStudents()) {
    882            if (student.hasAccommodation(acc))
    883                nrAccStudents++;
    884        }
    885        return nrAccStudents;
    886    }
    887
    888    /**
    889     * Number of last like ({@link Student#isDummy()} equals true) students with
    890     * a complete schedule ({@link Student#isComplete(Assignment)} equals true).
    891     * @param assignment current assignment
    892     * @param precise true if to be computed
    893     * @return number of last like (projected) students with a complete schedule
    894     */
    895    public int getNrCompleteLastLikeStudents(Assignment<Request, Enrollment> assignment, boolean precise) {
    896        if (!precise)
    897            return getContext(assignment).getNrCompleteLastLikeStudents();
    898        int nrLastLikeStudents = 0;
    899        for (Student student : getStudents()) {
    900            if (student.isComplete(assignment) && student.isDummy())
    901                nrLastLikeStudents++;
    902        }
    903        return nrLastLikeStudents;
    904    }
    905
    906    /**
    907     * Number of real ({@link Student#isDummy()} equals false) students with a
    908     * complete schedule ({@link Student#isComplete(Assignment)} equals true).
    909     * @param assignment current assignment
    910     * @param precise true if to be computed
    911     * @return number of real students with a complete schedule
    912     */
    913    public int getNrCompleteRealStudents(Assignment<Request, Enrollment> assignment, boolean precise) {
    914        if (!precise)
    915            return getContext(assignment).nrComplete() - getContext(assignment).getNrCompleteLastLikeStudents();
    916        int nrRealStudents = 0;
    917        for (Student student : getStudents()) {
    918            if (student.isComplete(assignment) && !student.isDummy())
    919                nrRealStudents++;
    920        }
    921        return nrRealStudents;
    922    }
    923
    924    /**
    925     * Number of requests from projected ({@link Student#isDummy()} equals true)
    926     * students.
    927     * @param precise true if to be computed
    928     * @return number of requests from projected students
    929     */
    930    public int getNrLastLikeRequests(boolean precise) {
    931        if (!precise)
    932            return iNrDummyRequests;
    933        int nrLastLikeRequests = 0;
    934        for (Request request : variables()) {
    935            if (request.getStudent().isDummy())
    936                nrLastLikeRequests++;
    937        }
    938        return nrLastLikeRequests;
    939    }
    940
    941    /**
    942     * Number of requests from real ({@link Student#isDummy()} equals false)
    943     * students.
    944     * @param precise true if to be computed
    945     * @return number of requests from real students
    946     */
    947    public int getNrRealRequests(boolean precise) {
    948        if (!precise)
    949            return variables().size() - iNrDummyRequests;
    950        int nrRealRequests = 0;
    951        for (Request request : variables()) {
    952            if (!request.getStudent().isDummy())
    953                nrRealRequests++;
    954        }
    955        return nrRealRequests;
    956    }
    957
    958    /**
    959     * Number of requests from projected ({@link Student#isDummy()} equals true)
    960     * students that are assigned.
    961     * @param assignment current assignment
    962     * @param precise true if to be computed
    963     * @return number of requests from projected students that are assigned
    964     */
    965    public int getNrAssignedLastLikeRequests(Assignment<Request, Enrollment> assignment, boolean precise) {
    966        if (!precise)
    967            return getContext(assignment).getNrAssignedLastLikeRequests();
    968        int nrLastLikeRequests = 0;
    969        for (Request request : assignment.assignedVariables()) {
    970            if (request.getStudent().isDummy())
    971                nrLastLikeRequests++;
    972        }
    973        return nrLastLikeRequests;
    974    }
    975
    976    /**
    977     * Number of requests from real ({@link Student#isDummy()} equals false)
    978     * students that are assigned.
    979     * @param assignment current assignment
    980     * @param precise true if to be computed
    981     * @return number of requests from real students that are assigned
    982     */
    983    public int getNrAssignedRealRequests(Assignment<Request, Enrollment> assignment, boolean precise) {
    984        if (!precise)
    985            return assignment.nrAssignedVariables() - getContext(assignment).getNrAssignedLastLikeRequests();
    986        int nrRealRequests = 0;
    987        for (Request request : assignment.assignedVariables()) {
    988            if (!request.getStudent().isDummy())
    989                nrRealRequests++;
    990        }
    991        return nrRealRequests;
    992    }
    993
    994    /**
    995     * Model extended info. Some more information (that is more expensive to
    996     * compute) is added to an ordinary {@link Model#getInfo(Assignment)}.
    997     */
    998    @Override
    999    public Map<String, String> getExtendedInfo(Assignment<Request, Enrollment> assignment) {
    1000        Map<String, String> info = getInfo(assignment);
    1001        /*
    1002        int nrLastLikeStudents = getNrLastLikeStudents(true);
    1003        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
    1004            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
    1005            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(true);
    1006            int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents;
    1007            info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
    1008                    * nrLastLikeCompleteStudents / nrLastLikeStudents)
    1009                    + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
    1010            info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
    1011                    / nrRealStudents)
    1012                    + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
    1013            int nrLastLikeRequests = getNrLastLikeRequests(true);
    1014            int nrRealRequests = variables().size() - nrLastLikeRequests;
    1015            int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(true);
    1016            int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests;
    1017            info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests
    1018                    / nrLastLikeRequests)
    1019                    + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
    1020            info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
    1021                    + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
    1022        }
    1023        */
    1024        // info.put("Average unassigned priority", sDecimalFormat.format(avgUnassignPriority()));
    1025        // info.put("Average number of requests", sDecimalFormat.format(avgNrRequests()));
    1026
    1027        /*
    1028        double total = 0;
    1029        for (Request r: variables())
    1030            if (r.getAssignment() != null)
    1031                total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment());
    1032        */
    1033        /*
    1034        double dc = 0;
    1035        if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts(assignment) != 0) {
    1036            Set<DistanceConflict.Conflict> conf = getDistanceConflict().getAllConflicts(assignment);
    1037            int sdc = 0;
    1038            for (DistanceConflict.Conflict c: conf) {
    1039                dc += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
    1040                if (c.getStudent().isNeedShortDistances()) sdc ++;
    1041            }
    1042            if (!conf.isEmpty())
    1043                info.put("Student distance conflicts", conf.size() + (sdc > 0 ? " (" + getDistanceConflict().getDistanceMetric().getShortDistanceAccommodationReference() + ": " + sdc + ", weighted: " : " (weighted: ") + sDecimalFormat.format(dc) + ")");
    1044        }
    1045        */
    1046        if (getStudentQuality() == null && getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts(assignment) != 0) {
    1047            Set<TimeOverlapsCounter.Conflict> conf = getTimeOverlaps().getContext(assignment).computeAllConflicts(assignment);
    1048            int share = 0, crShare = 0;
    1049            for (TimeOverlapsCounter.Conflict c: conf) {
    1050                share += c.getShare();
    1051                if (c.getR1() instanceof CourseRequest && c.getR2() instanceof CourseRequest)
    1052                    crShare += c.getShare();
    1053            }
    1054            if (share > 0)
    1055                info.put("Time overlapping conflicts", sDoubleFormat.format(5.0 * share / iStudents.size()) + " mins per student\n(" + sDoubleFormat.format(5.0 * crShare / iStudents.size()) + " between courses; " + sDoubleFormat.format(getTimeOverlaps().getTotalNrConflicts(assignment) / 12.0) + " hours total)");
    1056        }
    1057        if (getStudentQuality() != null) {
    1058            int confBtB = getStudentQuality().getTotalPenalty(StudentQuality.Type.BackToBack, assignment);
    1059            if (confBtB != 0) {
    1060                int prefBtb = 0, discBtb = 0;
    1061                int prefStd = 0, discStd = 0;
    1062                int prefPairs = 0, discPairs = 0;
    1063                for (Student s: getStudents()) {
    1064                    if (s.isDummy() || s.getBackToBackPreference() == BackToBackPreference.NO_PREFERENCE) continue;
    1065                    int[] classesPerDay = new int[] {0, 0, 0, 0, 0, 0, 0};
    1066                    for (Request r: s.getRequests()) {
    1067                        Enrollment e = r.getAssignment(assignment);
    1068                        if (e == null || !e.isCourseRequest()) continue;
    1069                        for (Section x: e.getSections()) {
    1070                            if (x.getTime() != null)
    1071                                for (int i = 0; i < Constants.DAY_CODES.length; i++)
    1072                                    if ((x.getTime().getDayCode() & Constants.DAY_CODES[i]) != 0)
    1073                                        classesPerDay[i] ++;
    1074                        }
    1075                    }
    1076                    int max = 0;
    1077                    for (int c: classesPerDay)
    1078                        if (c > 1) max += c - 1;
    1079                    int btb = getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.BackToBack, assignment, s);
    1080                    if (s.getBackToBackPreference() == BackToBackPreference.BTB_PREFERRED) {
    1081                        prefStd ++;
    1082                        prefBtb += btb;
    1083                        prefPairs += Math.max(btb, max);
    1084                    } else if (s.getBackToBackPreference() == BackToBackPreference.BTB_DISCOURAGED) {
    1085                        discStd ++;
    1086                        discBtb -= btb;
    1087                        discPairs += Math.max(btb, max);
    1088                    }
    1089                }
    1090                if (prefStd > 0)
    1091                    info.put("Schedule Quality: Back-to-back preferred", sDoubleFormat.format((100.0 * prefBtb) / prefPairs) + "% back-to-backs on average (" + prefBtb + "/" + prefPairs + " BTBs for " + prefStd + " students)");
    1092                if (discStd > 0)
    1093                    info.put("Schedule Quality: Back-to-back discouraged", sDoubleFormat.format(100.0 - (100.0 * discBtb) / discPairs) + "% non back-to-backs on average (" + discBtb + "/" + discPairs + " BTBs for " + discStd + " students)");
    1094            }
    1095            int confMod = getStudentQuality().getTotalPenalty(StudentQuality.Type.Modality, assignment);
    1096            if (confMod > 0) {
    1097                int prefOnl = 0, discOnl = 0;
    1098                int prefStd = 0, discStd = 0;
    1099                int prefCls = 0, discCls = 0;
    1100                for (Student s: getStudents()) {
    1101                    if (s.isDummy()) continue;
    1102                    if (s.isDummy() || s.getModalityPreference() == ModalityPreference.NO_PREFERENCE || s.getModalityPreference() == ModalityPreference.ONLINE_REQUIRED) continue;
    1103                    int classes = 0;
    1104                    for (Request r: s.getRequests()) {
    1105                        Enrollment e = r.getAssignment(assignment);
    1106                        if (e == null || !e.isCourseRequest()) continue;
    1107                        classes += e.getSections().size();
    1108                    }
    1109                    if (s.getModalityPreference() == ModalityPreference.ONLINE_PREFERRED) {
    1110                        prefStd ++;
    1111                        prefOnl += getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.Modality, assignment, s);
    1112                        prefCls += classes;
    1113                    } else if (s.getModalityPreference() == ModalityPreference.ONILNE_DISCOURAGED) {
    1114                        discStd ++;
    1115                        discOnl += getStudentQuality().getContext(assignment).allPenalty(StudentQuality.Type.Modality, assignment, s);
    1116                        discCls += classes;
    1117                    }
    1118                }
    1119                if (prefStd > 0)
    1120                    info.put("Schedule Quality: Online preferred", sDoubleFormat.format(100.0 - (100.0 * prefOnl) / prefCls) + "% online classes on average (" + prefOnl + "/" + prefCls + " classes for " + prefStd + " students)");
    1121                if (discStd > 0)
    1122                    info.put("Schedule Quality: Online discouraged", sDoubleFormat.format(100.0 - (100.0 * discOnl) / discCls) + "% face-to-face classes on average (" + discOnl + "/" + discCls + " classes for " + discStd + " students)");
    1123            }
    1124        }
    1125        /*
    1126        info.put("Overall solution value", sDecimalFormat.format(total - dc - toc) + (dc == 0.0 && toc == 0.0 ? "" :
    1127            " (" + (dc != 0.0 ? "distance: " + sDecimalFormat.format(dc): "") + (dc != 0.0 && toc != 0.0 ? ", " : "") +
    1128            (toc != 0.0 ? "overlap: " + sDecimalFormat.format(toc) : "") + ")")
    1129            );
    1130        */
    1131
    1132        double disbWeight = 0;
    1133        int disbSections = 0;
    1134        int disb10Sections = 0;
    1135        int disb10Limit = getProperties().getPropertyInt("Info.ListDisbalancedSections", 0);
    1136        Set<String> disb10SectionList = (disb10Limit == 0 ? null : new TreeSet<String>());
    1137        for (Offering offering: getOfferings()) {
    1138            if (offering.isDummy()) continue;
    1139            for (Config config: offering.getConfigs()) {
    1140                double enrl = config.getEnrollmentTotalWeight(assignment, null);
    1141                for (Subpart subpart: config.getSubparts()) {
    1142                    if (subpart.getSections().size() <= 1) continue;
    1143                    if (subpart.getLimit() > 0) {
    1144                        // sections have limits -> desired size is section limit x (total enrollment / total limit)
    1145                        double ratio = enrl / subpart.getLimit();
    1146                        for (Section section: subpart.getSections()) {
    1147                            double desired = ratio * section.getLimit();
    1148                            disbWeight += Math.abs(section.getEnrollmentTotalWeight(assignment, null) - desired);
    1149                            disbSections ++;
    1150                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, 0.1 * section.getLimit())) {
    1151                                disb10Sections++;
    1152                                if (disb10SectionList != null)
    1153                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName());
    1154                            }
    1155                        }
    1156                    } else {
    1157                        // unlimited sections -> desired size is total enrollment / number of sections
    1158                        for (Section section: subpart.getSections()) {
    1159                            double desired = enrl / subpart.getSections().size();
    1160                            disbWeight += Math.abs(section.getEnrollmentTotalWeight(assignment, null) - desired);
    1161                            disbSections ++;
    1162                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, 0.1 * desired)) {
    1163                                disb10Sections++;
    1164                                if (disb10SectionList != null)
    1165                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName());
    1166                            }
    1167                        }
    1168                    }
    1169                }
    1170            }
    1171        }
    1172        if (disbSections != 0) {
    1173            double assignedCRWeight = getContext(assignment).getAssignedCourseRequestWeight();
    1174            info.put("Average disbalance", sDecimalFormat.format(assignedCRWeight == 0 ? 0.0 : 100.0 * disbWeight / assignedCRWeight) + "% (" + sDecimalFormat.format(disbWeight / disbSections) + ")");
    1175            String list = "";
    1176            if (disb10SectionList != null) {
    1177                int i = 0;
    1178                for (String section: disb10SectionList) {
    1179                    if (i == disb10Limit) {
    1180                        list += "\n...";
    1181                        break;
    1182                    }
    1183                    list += "\n" + section;
    1184                    i++;
    1185                }
    1186            }
    1187            info.put("Sections disbalanced by 10% or more", sDecimalFormat.format(disbSections == 0 ? 0.0 : 100.0 * disb10Sections / disbSections) + "% (" + disb10Sections + ")" + (list.isEmpty() ? "" : "\n" + list));
    1188        }
    1189
    1190        int assCR = 0, priCR = 0;
    1191        for (Request r: variables()) {
    1192            if (r instanceof CourseRequest && !r.getStudent().isDummy()) {
    1193                CourseRequest cr = (CourseRequest)r;
    1194                Enrollment e = assignment.getValue(cr);
    1195                if (e != null) {
    1196                    assCR ++;
    1197                    if (!cr.isAlternative() && cr.getCourses().get(0).equals(e.getCourse())) priCR ++;
    1198                }
    1199            }
    1200        }
    1201        if (assCR > 0)
    1202            info.put("Assigned priority course requests", sDoubleFormat.format(100.0 * priCR / assCR) + "% (" + priCR + "/" + assCR + ")");
    1203        int[] missing = new int[] {0, 0, 0, 0, 0};
    1204        int incomplete = 0;
    1205        for (Student student: getStudents()) {
    1206            if (student.isDummy()) continue;
    1207            int nrRequests = 0;
    1208            int nrAssignedRequests = 0;
    1209            for (Request r : student.getRequests()) {
    1210                if (!(r instanceof CourseRequest)) continue; // ignore free times
    1211                if (!r.isAlternative()) nrRequests++;
    1212                if (r.isAssigned(assignment)) nrAssignedRequests++;
    1213            }
    1214            if (nrAssignedRequests < nrRequests) {
    1215                missing[Math.min(nrRequests - nrAssignedRequests, missing.length) - 1] ++;
    1216                incomplete ++;
    1217            }
    1218        }
    1219
    1220        for (int i = 0; i < missing.length; i++)
    1221            if (missing[i] > 0)
    1222                info.put("Students missing " + (i == 0 ? "1 course" : i + 1 == missing.length ? (i + 1) + " or more courses" : (i + 1) + " courses"), sDecimalFormat.format(100.0 * missing[i] / incomplete) + "% (" + missing[i] + ")");
    1223
    1224        info.put("Overall solution value", sDoubleFormat.format(getTotalValue(assignment)));// + " [precise: " + sDoubleFormat.format(getTotalValue(assignment, true)) + "]");
    1225
    1226        int nrStudentsBelowMinCredit = 0, nrStudents = 0;
    1227        for (Student student: getStudents()) {
    1228            if (student.isDummy()) continue;
    1229            if (student.hasMinCredit()) {
    1230                nrStudents++;
    1231                float credit = student.getAssignedCredit(assignment);
    1232                if (credit < student.getMinCredit() && !student.isComplete(assignment))
    1233                    nrStudentsBelowMinCredit ++;
    1234            }
    1235        }
    1236        if (nrStudentsBelowMinCredit > 0)
    1237            info.put("Students below min credit", sDoubleFormat.format(100.0 * nrStudentsBelowMinCredit / nrStudents) + "% (" + nrStudentsBelowMinCredit + "/" + nrStudents + ")");
    1238
    1239        int[] notAssignedPriority = new int[] {0, 0, 0, 0, 0, 0, 0};
    1240        int[] assignedChoice = new int[] {0, 0, 0, 0, 0};
    1241        int notAssignedTotal = 0, assignedChoiceTotal = 0;
    1242        int avgPriority = 0, avgChoice = 0;
    1243        for (Student student: getStudents()) {
    1244            if (student.isDummy()) continue;
    1245            for (Request r : student.getRequests()) {
    1246                if (!(r instanceof CourseRequest)) continue; // ignore free times
    1247                Enrollment e = r.getAssignment(assignment);
    1248                if (e == null) {
    1249                    if (!r.isAlternative()) {
    1250                        notAssignedPriority[Math.min(r.getPriority(), notAssignedPriority.length - 1)] ++;
    1251                        notAssignedTotal ++;
    1252                        avgPriority += r.getPriority();
    1253                    }
    1254                } else {
    1255                    assignedChoice[Math.min(e.getTruePriority(), assignedChoice.length - 1)] ++;
    1256                    assignedChoiceTotal ++;
    1257                    avgChoice += e.getTruePriority();
    1258                }
    1259            }
    1260        }
    1261        for (int i = 0; i < notAssignedPriority.length; i++)
    1262            if (notAssignedPriority[i] > 0)
    1263                info.put("Priority: Not-assigned priority " + (i + 1 == notAssignedPriority.length ? (i + 1) + "+" : (i + 1)) + " course requests", sDecimalFormat.format(100.0 * notAssignedPriority[i] / notAssignedTotal) + "% (" + notAssignedPriority[i] + ")");
    1264        if (notAssignedTotal > 0)
    1265            info.put("Priority: Average not-assigned priority", sDecimalFormat.format(1.0 + ((double)avgPriority) / notAssignedTotal));
    1266        for (int i = 0; i < assignedChoice.length; i++)
    1267            if (assignedChoice[i] > 0)
    1268                info.put("Choice: assigned " + (i == 0 ? "1st": i == 1 ? "2nd" : i == 2 ? "3rd" : i + 1 == assignedChoice.length ? (i + 1) + "th+" : (i + 1) + "th") + " course choice", sDecimalFormat.format(100.0 * assignedChoice[i] / assignedChoiceTotal) + "% (" + assignedChoice[i] + ")");
    1269        if (assignedChoiceTotal > 0)
    1270            info.put("Choice: Average assigned choice", sDecimalFormat.format(1.0 + ((double)avgChoice) / assignedChoiceTotal));
    1271
    1272        int nbrSections = 0, nbrFullSections = 0, nbrSections98 = 0, nbrSections95 = 0, nbrSections90 = 0, nbrSectionsDis = 0;
    1273        int enrlSections = 0, enrlFullSections = 0, enrlSections98 = 0, enrlSections95 = 0, enrlSections90 = 0, enrlSectionsDis = 0;
    1274        int nbrOfferings = 0, nbrFullOfferings = 0, nbrOfferings98 = 0, nbrOfferings95 = 0, nbrOfferings90 = 0;
    1275        int enrlOfferings = 0, enrlOfferingsFull = 0, enrlOfferings98 = 0, enrlOfferings95 = 0, enrlOfferings90 = 0;
    1276        for (Offering offering: getOfferings()) {
    1277            if (offering.isDummy()) continue;
    1278            int offeringLimit = 0, offeringEnrollment = 0;
    1279            for (Config config: offering.getConfigs()) {
    1280                int configLimit = config.getLimit();
    1281                for (Subpart subpart: config.getSubparts()) {
    1282                    int subpartLimit = 0;
    1283                    for (Section section: subpart.getSections()) {
    1284                        if (section.isCancelled()) continue;
    1285                        int enrl = section.getEnrollments(assignment).size();
    1286                        if (section.getLimit() < 0 || subpartLimit < 0)
    1287                            subpartLimit = -1;
    1288                        else
    1289                            subpartLimit += (section.isEnabled() ? section.getLimit() : enrl);
    1290                        nbrSections ++;
    1291                        enrlSections += enrl;
    1292                        if (section.getLimit() >= 0 && section.getLimit() <= enrl) {
    1293                            nbrFullSections ++;
    1294                            enrlFullSections += enrl;
    1295                        }
    1296                        if (!section.isEnabled() && (enrl > 0 || section.getLimit() >= 0)) {
    1297                            nbrSectionsDis ++;
    1298                            enrlSectionsDis += enrl;
    1299                        }
    1300                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.02 * section.getLimit())) {
    1301                            nbrSections98 ++;
    1302                            enrlSections98 += enrl;
    1303                        }
    1304                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.05 * section.getLimit())) {
    1305                            nbrSections95 ++;
    1306                            enrlSections95 += enrl;
    1307                        }
    1308                        if (section.getLimit() >= 0 && (section.getLimit() - enrl) <= Math.round(0.10 * section.getLimit())) {
    1309                            nbrSections90 ++;
    1310                            enrlSections90 += enrl;
    1311                        }
    1312                    }
    1313                    if (configLimit < 0 || subpartLimit < 0)
    1314                        configLimit = -1;
    1315                    else
    1316                        configLimit = Math.min(configLimit, subpartLimit);
    1317                }
    1318                if (offeringLimit < 0 || configLimit < 0)
    1319                    offeringLimit = -1;
    1320                else
    1321                    offeringLimit += configLimit;
    1322                offeringEnrollment += config.getEnrollments(assignment).size();
    1323            }
    1324            nbrOfferings ++;
    1325            enrlOfferings += offeringEnrollment;
    1326
    1327            if (offeringLimit >=0 && offeringEnrollment >= offeringLimit) {
    1328                nbrFullOfferings ++;
    1329                enrlOfferingsFull += offeringEnrollment;
    1330            }
    1331            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.02 * offeringLimit)) {
    1332                nbrOfferings98++;
    1333                enrlOfferings98 += offeringEnrollment;
    1334            }
    1335            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.05 * offeringLimit)) {
    1336                nbrOfferings95++;
    1337                enrlOfferings95 += offeringEnrollment;
    1338            }
    1339            if (offeringLimit >= 0 && (offeringLimit - offeringEnrollment) <= Math.round(0.10 * offeringLimit)) {
    1340                nbrOfferings90++;
    1341                enrlOfferings90 += offeringEnrollment;
    1342            }
    1343        }
    1344        if (enrlOfferings90 > 0 && enrlOfferings > 0)
    1345            info.put("Full Offerings", (nbrFullOfferings > 0 ? nbrFullOfferings + " with no space (" + sDecimalFormat.format(100.0 * nbrFullOfferings / nbrOfferings) + "% of all offerings, " +
    1346                    sDecimalFormat.format(100.0 * enrlOfferingsFull / enrlOfferings) + "% assignments)\n" : "")+
    1347                    (nbrOfferings98 > nbrFullOfferings ? nbrOfferings98 + " with &leq; 2% available (" + sDecimalFormat.format(100.0 * nbrOfferings98 / nbrOfferings) + "% of all offerings, " +
    1348                    sDecimalFormat.format(100.0 * enrlOfferings98 / enrlOfferings) + "% assignments)\n" : "")+
    1349                    (nbrOfferings95 > nbrOfferings98 ? nbrOfferings95 + " with &leq; 5% available (" + sDecimalFormat.format(100.0 * nbrOfferings95 / nbrOfferings) + "% of all offerings, " +
    1350                    sDecimalFormat.format(100.0 * enrlOfferings95 / enrlOfferings) + "% assignments)\n" : "")+
    1351                    (nbrOfferings90 > nbrOfferings95 ? nbrOfferings90 + " with &leq; 10% available (" + sDecimalFormat.format(100.0 * nbrOfferings90 / nbrOfferings) + "% of all offerings, " +
    1352                    sDecimalFormat.format(100.0 * enrlOfferings90 / enrlOfferings) + "% assignments)" : ""));
    1353        if ((enrlSections90 > 0 || nbrSectionsDis > 0) && enrlSections > 0)
    1354            info.put("Full Sections", (nbrFullSections > 0 ? nbrFullSections + " with no space (" + sDecimalFormat.format(100.0 * nbrFullSections / nbrSections) + "% of all sections, "+
    1355                    sDecimalFormat.format(100.0 * enrlFullSections / enrlSections) + "% assignments)\n" : "") +
    1356                    (nbrSectionsDis > 0 ? nbrSectionsDis + " disabled (" + sDecimalFormat.format(100.0 * nbrSectionsDis / nbrSections) + "% of all sections, "+
    1357                    sDecimalFormat.format(100.0 * enrlSectionsDis / enrlSections) + "% assignments)\n" : "") +
    1358                    (enrlSections98 > nbrFullSections ? nbrSections98 + " with &leq; 2% available (" + sDecimalFormat.format(100.0 * nbrSections98 / nbrSections) + "% of all sections, " +
    1359                    sDecimalFormat.format(100.0 * enrlSections98 / enrlSections) + "% assignments)\n" : "") +
    1360                    (nbrSections95 > enrlSections98 ? nbrSections95 + " with &leq; 5% available (" + sDecimalFormat.format(100.0 * nbrSections95 / nbrSections) + "% of all sections, " +
    1361                    sDecimalFormat.format(100.0 * enrlSections95 / enrlSections) + "% assignments)\n" : "") +
    1362                    (nbrSections90 > nbrSections95 ? nbrSections90 + " with &leq; 10% available (" + sDecimalFormat.format(100.0 * nbrSections90 / nbrSections) + "% of all sections, " +
    1363                    sDecimalFormat.format(100.0 * enrlSections90 / enrlSections) + "% assignments)" : ""));
    1364        if (getStudentQuality() != null) {
    1365            int shareCR = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.CourseTimeOverlap, assignment);
    1366            int shareFT = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.FreeTimeOverlap, assignment);
    1367            int shareUN = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.Unavailability, assignment);
    1368            int shareUND = getStudentQuality().getContext(assignment).countTotalPenalty(StudentQuality.Type.UnavailabilityDistance, assignment);
    1369            if (shareCR > 0) {
    1370                Set<Student> students = new HashSet<Student>();
    1371                for (StudentQuality.Conflict c: getStudentQuality().getContext(assignment).computeAllConflicts(StudentQuality.Type.CourseTimeOverlap, assignment)) {
    1372                    students.add(c.getStudent());
    1373                }
    1374                info.put("Time overlaps: courses", students.size() + " students (avg " + sDoubleFormat.format(5.0 * shareCR / students.size()) + " mins)");
    1375            }
    1376            if (shareFT > 0) {
    1377                Set<Student> students = new HashSet<Student>();
    1378                for (StudentQuality.Conflict c: getStudentQuality().getContext(assignment).computeAllConflicts(StudentQuality.Type.FreeTimeOverlap, assignment)) {
    1379                    students.add(c.getStudent());
    1380                }
    1381                info.put("Time overlaps: free times", students.size() + " students (avg " + sDoubleFormat.format(5.0 * shareFT / students.size()) + " mins)");
    1382            }
    1383            if (shareUN > 0) {
    1384                Set<Student> students = new HashSet<Student>();
    1385                for (StudentQuality.Conflict c: getStudentQuality().getContext(assignment).computeAllConflicts(StudentQuality.Type.Unavailability, assignment)) {
    1386                    students.add(c.getStudent());
    1387                }
    1388                info.put("Unavailabilities: Time conflicts", students.size() + " students (avg " + sDoubleFormat.format(5.0 * shareUN / students.size()) + " mins)");
    1389            }
    1390            if (shareUND > 0) {
    1391                Set<Student> students = new HashSet<Student>();
    1392                for (StudentQuality.Conflict c: getStudentQuality().getContext(assignment).computeAllConflicts(StudentQuality.Type.UnavailabilityDistance, assignment)) {
    1393                    students.add(c.getStudent());
    1394                }
    1395                info.put("Unavailabilities: Distance conflicts", students.size() + " students (avg " + sDoubleFormat.format(shareUND / students.size()) + " travels)");
    1396            }
    1397        } else if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts(assignment) != 0) {
    1398            Set<TimeOverlapsCounter.Conflict> conf = getTimeOverlaps().getContext(assignment).computeAllConflicts(assignment);
    1399            int shareCR = 0, shareFT = 0, shareUN = 0;
    1400            Set<Student> studentsCR = new HashSet<Student>();
    1401            Set<Student> studentsFT = new HashSet<Student>();
    1402            Set<Student> studentsUN = new HashSet<Student>();
    1403            for (TimeOverlapsCounter.Conflict c: conf) {
    1404                if (c.getR1() instanceof CourseRequest && c.getR2() instanceof CourseRequest) {
    1405                    shareCR += c.getShare(); studentsCR.add(c.getStudent());
    1406                } else if (c.getS2() instanceof Unavailability) {
    1407                    shareUN += c.getShare(); studentsUN.add(c.getStudent());
    1408                } else {
    1409                    shareFT += c.getShare(); studentsFT.add(c.getStudent());
    1410                }
    1411            }
    1412            if (shareCR > 0)
    1413                info.put("Time overlaps: courses", studentsCR.size() + " students (avg " + sDoubleFormat.format(5.0 * shareCR / studentsCR.size()) + " mins)");
    1414            if (shareFT > 0)
    1415                info.put("Time overlaps: free times", studentsFT.size() + " students (avg " + sDoubleFormat.format(5.0 * shareFT / studentsFT.size()) + " mins)");
    1416            if (shareUN > 0)
    1417                info.put("Time overlaps: teaching assignments", studentsUN.size() + " students (avg " + sDoubleFormat.format(5.0 * shareUN / studentsUN.size()) + " mins)");
    1418        }
    1419
    1420
    1421        return info;
    1422    }
    1423
    1424    @Override
    1425    public void restoreBest(Assignment<Request, Enrollment> assignment) {
    1426        restoreBest(assignment, new Comparator<Request>() {
    1427            @Override
    1428            public int compare(Request r1, Request r2) {
    1429                Enrollment e1 = r1.getBestAssignment();
    1430                Enrollment e2 = r2.getBestAssignment();
    1431                // Reservations first
    1432                if (e1.getReservation() != null && e2.getReservation() == null) return -1;
    1433                if (e1.getReservation() == null && e2.getReservation() != null) return 1;
    1434                // Then assignment iteration (i.e., order in which assignments were made)
    1435                if (r1.getBestAssignmentIteration() != r2.getBestAssignmentIteration())
    1436                    return (r1.getBestAssignmentIteration() < r2.getBestAssignmentIteration() ? -1 : 1);
    1437                // Then student and priority
    1438                return r1.compareTo(r2);
    1439            }
    1440        });
    1441        recomputeTotalValue(assignment);
    1442    }
    1443
    1444    public void recomputeTotalValue(Assignment<Request, Enrollment> assignment) {
    1445        getContext(assignment).iTotalValue = getTotalValue(assignment, true);
    1446    }
    1447
    1448    @Override
    1449    public void saveBest(Assignment<Request, Enrollment> assignment) {
    1450        recomputeTotalValue(assignment);
    1451        iBestAssignedCourseRequestWeight = getContext(assignment).getAssignedCourseRequestWeight();
    1452        super.saveBest(assignment);
    1453    }
    1454
    1455    public double getBestAssignedCourseRequestWeight() {
    1456        return iBestAssignedCourseRequestWeight;
    1457    }
    1458
    1459    @Override
    1460    public String toString(Assignment<Request, Enrollment> assignment) {
    1461        double groupSpread = 0.0; double groupCount = 0;
    1462        for (Offering offering: iOfferings) {
    1463            for (Course course: offering.getCourses()) {
    1464                for (RequestGroup group: course.getRequestGroups()) {
    1465                    groupSpread += group.getAverageSpread(assignment) * group.getEnrollmentWeight(assignment, null);
    1466                    groupCount += group.getEnrollmentWeight(assignment, null);
    1467                }
    1468            }
    1469        }
    1470        String priority = "";
    1471        for (StudentPriority sp: StudentPriority.values()) {
    1472            if (sp.ordinal() < StudentPriority.Normal.ordinal()) {
    1473                if (iTotalPriorityCRWeight[sp.ordinal()] > 0.0)
    1474                    priority += sp.code() + "PCR:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCRWeight[sp.ordinal()] / iTotalPriorityCRWeight[sp.ordinal()]) + "%, ";
    1475                if (iTotalPriorityCriticalCRWeight[RequestPriority.LC.ordinal()][sp.ordinal()] > 0.0)
    1476                    priority += sp.code() + "PCL:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCriticalCRWeight[RequestPriority.LC.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[RequestPriority.LC.ordinal()][sp.ordinal()]) + "%, ";
    1477                if (iTotalPriorityCriticalCRWeight[RequestPriority.Critical.ordinal()][sp.ordinal()] > 0.0)
    1478                    priority += sp.code() + "PCC:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCriticalCRWeight[RequestPriority.Critical.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[RequestPriority.Critical.ordinal()][sp.ordinal()]) + "%, ";
    1479                if (iTotalPriorityCriticalCRWeight[RequestPriority.Important.ordinal()][sp.ordinal()] > 0.0)
    1480                    priority += sp.code() + "PCI:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCriticalCRWeight[RequestPriority.Important.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[RequestPriority.Important.ordinal()][sp.ordinal()]) + "%, ";
    1481                if (iTotalPriorityCriticalCRWeight[RequestPriority.Vital.ordinal()][sp.ordinal()] > 0.0)
    1482                    priority += sp.code() + "PCV:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCriticalCRWeight[RequestPriority.Vital.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[RequestPriority.Vital.ordinal()][sp.ordinal()]) + "%, ";
    1483                if (iTotalPriorityCriticalCRWeight[RequestPriority.VisitingF2F.ordinal()][sp.ordinal()] > 0.0)
    1484                    priority += sp.code() + "PCVF:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedPriorityCriticalCRWeight[RequestPriority.VisitingF2F.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[RequestPriority.VisitingF2F.ordinal()][sp.ordinal()]) + "%, ";
    1485            }
    1486        }
    1487        return   (getNrRealStudents(false) > 0 ? "RRq:" + getNrAssignedRealRequests(assignment, false) + "/" + getNrRealRequests(false) + ", " : "")
    1488                + (getNrLastLikeStudents(false) > 0 ? "DRq:" + getNrAssignedLastLikeRequests(assignment, false) + "/" + getNrLastLikeRequests(false) + ", " : "")
    1489                + (getNrRealStudents(false) > 0 ? "RS:" + getNrCompleteRealStudents(assignment, false) + "/" + getNrRealStudents(false) + ", " : "")
    1490                + (getNrLastLikeStudents(false) > 0 ? "DS:" + getNrCompleteLastLikeStudents(assignment, false) + "/" + getNrLastLikeStudents(false) + ", " : "")
    1491                + (iTotalCRWeight > 0.0 ? "CR:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCourseRequestWeight() / iTotalCRWeight) + "%, " : "")
    1492                + (iTotalSelCRWeight > 0.0 ? "S:" + sDoubleFormat.format(100.0 * (0.3 * getContext(assignment).iAssignedSelectedConfigWeight + 0.7 * getContext(assignment).iAssignedSelectedSectionWeight) / iTotalSelCRWeight) + "%, ": "")
    1493                + (iTotalCriticalCRWeight[RequestPriority.LC.ordinal()] > 0.0 ? "LC:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCriticalCourseRequestWeight(RequestPriority.LC) / iTotalCriticalCRWeight[RequestPriority.LC.ordinal()]) + "%, " : "")
    1494                + (iTotalCriticalCRWeight[RequestPriority.Critical.ordinal()] > 0.0 ? "CC:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCriticalCourseRequestWeight(RequestPriority.Critical) / iTotalCriticalCRWeight[RequestPriority.Critical.ordinal()]) + "%, " : "")
    1495                + (iTotalCriticalCRWeight[RequestPriority.Important.ordinal()] > 0.0 ? "IC:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCriticalCourseRequestWeight(RequestPriority.Important) / iTotalCriticalCRWeight[RequestPriority.Important.ordinal()]) + "%, " : "")
    1496                + (iTotalCriticalCRWeight[RequestPriority.Vital.ordinal()] > 0.0 ? "VC:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCriticalCourseRequestWeight(RequestPriority.Vital) / iTotalCriticalCRWeight[RequestPriority.Vital.ordinal()]) + "%, " : "")
    1497                + (iTotalCriticalCRWeight[RequestPriority.VisitingF2F.ordinal()] > 0.0 ? "VFC:" + sDecimalFormat.format(100.0 * getContext(assignment).getAssignedCriticalCourseRequestWeight(RequestPriority.VisitingF2F) / iTotalCriticalCRWeight[RequestPriority.VisitingF2F.ordinal()]) + "%, " : "")
    1498                + priority
    1499                + "V:" + sDecimalFormat.format(-getTotalValue(assignment))
    1500                + (getDistanceConflict() == null ? "" : ", DC:" + getDistanceConflict().getTotalNrConflicts(assignment))
    1501                + (getTimeOverlaps() == null ? "" : ", TOC:" + getTimeOverlaps().getTotalNrConflicts(assignment))
    1502                + (iMPP ? ", IS:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedSameSectionWeight / iTotalMPPCRWeight) + "%" : "")
    1503                + (iMPP ? ", IT:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedSameTimeWeight / iTotalMPPCRWeight) + "%" : "")
    1504                + ", %:" + sDecimalFormat.format(-100.0 * getTotalValue(assignment) / (getStudents().size() - iNrDummyStudents +
    1505                        (iProjectedStudentWeight < 0.0 ? iNrDummyStudents * (iTotalDummyWeight / iNrDummyRequests) :iProjectedStudentWeight * iTotalDummyWeight)))
    1506                + (groupCount > 0 ? ", SG:" + sDecimalFormat.format(100.0 * groupSpread / groupCount) + "%" : "")
    1507                + (getStudentQuality() == null ? "" : ", SQ:{" + getStudentQuality().toString(assignment) + "}");
    1508    }
    1509
    1510    /**
    1511     * Quadratic average of two weights.
    1512     * @param w1 first weight
    1513     * @param w2 second weight
    1514     * @return average of the two weights
    1515     */
    1516    public double avg(double w1, double w2) {
    1517        return Math.sqrt(w1 * w2);
    1518    }
    1519
    1520    /**
    1521     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
    1522     * @return maximal domain size, -1 if unlimited
    1523     */
    1524    public int getMaxDomainSize() { return iMaxDomainSize; }
    1525
    1526    /**
    1527     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
    1528     * @param maxDomainSize maximal domain size, -1 if unlimited
    1529     */
    1530    public void setMaxDomainSize(int maxDomainSize) { iMaxDomainSize = maxDomainSize; }
    1531
    1532    public int getDayOfWeekOffset() { return iDayOfWeekOffset; }
    1533    public void setDayOfWeekOffset(int dayOfWeekOffset) {
    1534        iDayOfWeekOffset = dayOfWeekOffset;
    1535        if (iProperties != null)
    1536            iProperties.setProperty("DatePattern.DayOfWeekOffset", Integer.toString(dayOfWeekOffset));
    1537    }
    1538
    1539    @Override
    1540    public StudentSectioningModelContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
    1541        return new StudentSectioningModelContext(assignment);
    1542    }
    1543
    1544    public class StudentSectioningModelContext implements AssignmentConstraintContext<Request, Enrollment>, InfoProvider<Request, Enrollment>{
    1545        private Set<Student> iCompleteStudents = new HashSet<Student>();
    1546        private double iTotalValue = 0.0;
    1547        private int iNrAssignedDummyRequests = 0, iNrCompleteDummyStudents = 0;
    1548        private double iAssignedCRWeight = 0.0, iAssignedDummyCRWeight = 0.0;
    1549        private double[] iAssignedCriticalCRWeight;
    1550        private double[][] iAssignedPriorityCriticalCRWeight;
    1551        private double iReservedSpace = 0.0, iTotalReservedSpace = 0.0;
    1552        private double iAssignedSameSectionWeight = 0.0, iAssignedSameChoiceWeight = 0.0, iAssignedSameTimeWeight = 0.0;
    1553        private double iAssignedSelectedSectionWeight = 0.0, iAssignedSelectedConfigWeight = 0.0;
    1554        private double iAssignedNoTimeSectionWeight = 0.0;
    1555        private double iAssignedOnlineSectionWeight = 0.0;
    1556        private double iAssignedPastSectionWeight = 0.0;
    1557        private int[] iNrCompletePriorityStudents = null;
    1558        private double[] iAssignedPriorityCRWeight = null;
    1559
    1560        public StudentSectioningModelContext(StudentSectioningModelContext parent) {
    1561            iCompleteStudents = new HashSet<Student>(parent.iCompleteStudents);
    1562            iTotalValue = parent.iTotalValue;
    1563            iNrAssignedDummyRequests = parent.iNrAssignedDummyRequests;
    1564            iNrCompleteDummyStudents = parent.iNrCompleteDummyStudents;
    1565            iAssignedCRWeight = parent.iAssignedCRWeight;
    1566            iAssignedDummyCRWeight = parent.iAssignedDummyCRWeight;
    1567            iReservedSpace = parent.iReservedSpace;
    1568            iTotalReservedSpace = parent.iTotalReservedSpace;
    1569            iAssignedSameSectionWeight = parent.iAssignedSameSectionWeight;
    1570            iAssignedSameChoiceWeight = parent.iAssignedSameChoiceWeight;
    1571            iAssignedSameTimeWeight = parent.iAssignedSameTimeWeight;
    1572            iAssignedSelectedSectionWeight = parent.iAssignedSelectedSectionWeight;
    1573            iAssignedSelectedConfigWeight = parent.iAssignedSelectedConfigWeight;
    1574            iAssignedNoTimeSectionWeight = parent.iAssignedNoTimeSectionWeight;
    1575            iAssignedOnlineSectionWeight = parent.iAssignedOnlineSectionWeight;
    1576            iAssignedPastSectionWeight = parent.iAssignedPastSectionWeight;
    1577            iAssignedCriticalCRWeight = new double[RequestPriority.values().length];
    1578            iAssignedPriorityCriticalCRWeight = new double[RequestPriority.values().length][StudentPriority.values().length];
    1579            for (int i = 0; i < RequestPriority.values().length; i++) {
    1580                iAssignedCriticalCRWeight[i] = parent.iAssignedCriticalCRWeight[i];
    1581                for (int j = 0; j < StudentPriority.values().length; j++) {
    1582                    iAssignedPriorityCriticalCRWeight[i][j] = parent.iAssignedPriorityCriticalCRWeight[i][j];
    1583                }
    1584            }
    1585            iNrCompletePriorityStudents = new int[StudentPriority.values().length];
    1586            iAssignedPriorityCRWeight = new double[StudentPriority.values().length];
    1587            for (int i = 0; i < StudentPriority.values().length; i++) {
    1588                iNrCompletePriorityStudents[i] = parent.iNrCompletePriorityStudents[i];
    1589                iAssignedPriorityCRWeight[i] = parent.iAssignedPriorityCRWeight[i];
    1590            }
    1591        }
    1592
    1593        public StudentSectioningModelContext(Assignment<Request, Enrollment> assignment) {
    1594            iAssignedCriticalCRWeight = new double[RequestPriority.values().length];
    1595            iAssignedPriorityCriticalCRWeight = new double[RequestPriority.values().length][StudentPriority.values().length];
    1596            for (int i = 0; i < RequestPriority.values().length; i++) {
    1597                iAssignedCriticalCRWeight[i] = 0.0;
    1598                for (int j = 0; j < StudentPriority.values().length; j++) {
    1599                    iAssignedPriorityCriticalCRWeight[i][j] = 0.0;
    1600                }
    1601            }
    1602            iNrCompletePriorityStudents = new int[StudentPriority.values().length];
    1603            iAssignedPriorityCRWeight = new double[StudentPriority.values().length];
    1604            for (int i = 0; i < StudentPriority.values().length; i++) {
    1605                iNrCompletePriorityStudents[i] = 0;
    1606                iAssignedPriorityCRWeight[i] = 0.0;
    1607            }
    1608            for (Request request: variables()) {
    1609                Enrollment enrollment = assignment.getValue(request);
    1610                if (enrollment != null)
    1611                    assigned(assignment, enrollment);
    1612            }
    1613        }
    1614
    1615        /**
    1616         * Called after an enrollment was assigned to a request. The list of
    1617         * complete students and the overall solution value are updated.
    1618         */
    1619        @Override
    1620        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    1621            Student student = enrollment.getStudent();
    1622            if (student.isComplete(assignment) && iCompleteStudents.add(student)) {
    1623                if (student.isDummy()) iNrCompleteDummyStudents++;
    1624                iNrCompletePriorityStudents[student.getPriority().ordinal()]++;
    1625            }
    1626            double value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(assignment, enrollment);
    1627            iTotalValue -= value;
    1628            enrollment.variable().getContext(assignment).setLastWeight(value);
    1629            if (enrollment.isCourseRequest())
    1630                iAssignedCRWeight += enrollment.getRequest().getWeight();
    1631            if (enrollment.isCourseRequest() && enrollment.getRequest().getRequestPriority() != RequestPriority.Normal && !enrollment.getStudent().isDummy() && !enrollment.getRequest().isAlternative())
    1632                iAssignedCriticalCRWeight[enrollment.getRequest().getRequestPriority().ordinal()] += enrollment.getRequest().getWeight();
    1633            if (enrollment.isCourseRequest() && enrollment.getRequest().getRequestPriority() != RequestPriority.Normal && !enrollment.getRequest().isAlternative())
    1634                iAssignedPriorityCriticalCRWeight[enrollment.getRequest().getRequestPriority().ordinal()][enrollment.getStudent().getPriority().ordinal()] += enrollment.getRequest().getWeight();
    1635            if (enrollment.getRequest().isMPP()) {
    1636                iAssignedSameSectionWeight += enrollment.getRequest().getWeight() * enrollment.percentInitial();
    1637                iAssignedSameChoiceWeight += enrollment.getRequest().getWeight() * enrollment.percentSelected();
    1638                iAssignedSameTimeWeight += enrollment.getRequest().getWeight() * enrollment.percentSameTime();
    1639            }
    1640            if (enrollment.getRequest().hasSelection()) {
    1641                iAssignedSelectedSectionWeight += enrollment.getRequest().getWeight() * enrollment.percentSelectedSameSection();
    1642                iAssignedSelectedConfigWeight += enrollment.getRequest().getWeight() * enrollment.percentSelectedSameConfig();
    1643            }
    1644            if (enrollment.getReservation() != null)
    1645                iReservedSpace += enrollment.getRequest().getWeight();
    1646            if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
    1647                iTotalReservedSpace += enrollment.getRequest().getWeight();
    1648            if (student.isDummy()) {
    1649                iNrAssignedDummyRequests++;
    1650                if (enrollment.isCourseRequest())
    1651                    iAssignedDummyCRWeight += enrollment.getRequest().getWeight();
    1652            }
    1653            if (enrollment.isCourseRequest())
    1654                iAssignedPriorityCRWeight[enrollment.getStudent().getPriority().ordinal()] += enrollment.getRequest().getWeight();
    1655            if (enrollment.isCourseRequest()) {
    1656                int noTime = 0;
    1657                int online = 0;
    1658                int past = 0;
    1659                for (Section section: enrollment.getSections()) {
    1660                    if (!section.hasTime()) noTime ++;
    1661                    if (section.isOnline()) online ++;
    1662                    if (section.isPast()) past ++;
    1663                }
    1664                if (noTime > 0)
    1665                    iAssignedNoTimeSectionWeight += enrollment.getRequest().getWeight() * noTime / enrollment.getSections().size();
    1666                if (online > 0)
    1667                    iAssignedOnlineSectionWeight += enrollment.getRequest().getWeight() * online / enrollment.getSections().size();
    1668                if (past > 0)
    1669                    iAssignedPastSectionWeight += enrollment.getRequest().getWeight() * past / enrollment.getSections().size();
    1670            }
    1671        }
    1672
    1673        /**
    1674         * Called before an enrollment was unassigned from a request. The list of
    1675         * complete students and the overall solution value are updated.
    1676         */
    1677        @Override
    1678        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    1679            Student student = enrollment.getStudent();
    1680            if (enrollment.isCourseRequest() && iCompleteStudents.contains(student)) {
    1681                iCompleteStudents.remove(student);
    1682                if (student.isDummy())
    1683                    iNrCompleteDummyStudents--;
    1684                iNrCompletePriorityStudents[student.getPriority().ordinal()]--;
    1685            }
    1686            Request.RequestContext cx = enrollment.variable().getContext(assignment);
    1687            Double value = cx.getLastWeight();
    1688            if (value == null)
    1689                value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(assignment, enrollment);
    1690            iTotalValue += value;
    1691            cx.setLastWeight(null);
    1692            if (enrollment.isCourseRequest())
    1693                iAssignedCRWeight -= enrollment.getRequest().getWeight();
    1694            if (enrollment.isCourseRequest() && enrollment.getRequest().getRequestPriority() != RequestPriority.Normal && !enrollment.getStudent().isDummy() && !enrollment.getRequest().isAlternative())
    1695                iAssignedCriticalCRWeight[enrollment.getRequest().getRequestPriority().ordinal()] -= enrollment.getRequest().getWeight();
    1696            if (enrollment.isCourseRequest() && enrollment.getRequest().getRequestPriority() != RequestPriority.Normal && !enrollment.getRequest().isAlternative())
    1697                iAssignedPriorityCriticalCRWeight[enrollment.getRequest().getRequestPriority().ordinal()][enrollment.getStudent().getPriority().ordinal()] -= enrollment.getRequest().getWeight();
    1698            if (enrollment.getRequest().isMPP()) {
    1699                iAssignedSameSectionWeight -= enrollment.getRequest().getWeight() * enrollment.percentInitial();
    1700                iAssignedSameChoiceWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelected();
    1701                iAssignedSameTimeWeight -= enrollment.getRequest().getWeight() * enrollment.percentSameTime();
    1702            }
    1703            if (enrollment.getRequest().hasSelection()) {
    1704                iAssignedSelectedSectionWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelectedSameSection();
    1705                iAssignedSelectedConfigWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelectedSameConfig();
    1706            }
    1707            if (enrollment.getReservation() != null)
    1708                iReservedSpace -= enrollment.getRequest().getWeight();
    1709            if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
    1710                iTotalReservedSpace -= enrollment.getRequest().getWeight();
    1711            if (student.isDummy()) {
    1712                iNrAssignedDummyRequests--;
    1713                if (enrollment.isCourseRequest())
    1714                    iAssignedDummyCRWeight -= enrollment.getRequest().getWeight();
    1715            }
    1716            if (enrollment.isCourseRequest())
    1717                iAssignedPriorityCRWeight[enrollment.getStudent().getPriority().ordinal()] -= enrollment.getRequest().getWeight();
    1718            if (enrollment.isCourseRequest()) {
    1719                int noTime = 0;
    1720                int online = 0;
    1721                int past = 0;
    1722                for (Section section: enrollment.getSections()) {
    1723                    if (!section.hasTime()) noTime ++;
    1724                    if (section.isOnline()) online ++;
    1725                    if (section.isPast()) past ++;
    1726                }
    1727                if (noTime > 0)
    1728                    iAssignedNoTimeSectionWeight -= enrollment.getRequest().getWeight() * noTime / enrollment.getSections().size();
    1729                if (online > 0)
    1730                    iAssignedOnlineSectionWeight -= enrollment.getRequest().getWeight() * online / enrollment.getSections().size();
    1731                if (past > 0)
    1732                    iAssignedPastSectionWeight -= enrollment.getRequest().getWeight() * past / enrollment.getSections().size();
    1733            }
    1734        }
    1735
    1736        public void add(Assignment<Request, Enrollment> assignment, DistanceConflict.Conflict c) {
    1737            iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
    1738        }
    1739
    1740        public void remove(Assignment<Request, Enrollment> assignment, DistanceConflict.Conflict c) {
    1741            iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
    1742        }
    1743
    1744        public void add(Assignment<Request, Enrollment> assignment, TimeOverlapsCounter.Conflict c) {
    1745            if (c.getR1() != null) iTotalValue += c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
    1746            if (c.getR2() != null) iTotalValue += c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
    1747        }
    1748
    1749        public void remove(Assignment<Request, Enrollment> assignment, TimeOverlapsCounter.Conflict c) {
    1750            if (c.getR1() != null) iTotalValue -= c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
    1751            if (c.getR2() != null) iTotalValue -= c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
    1752        }
    1753
    1754        public void add(Assignment<Request, Enrollment> assignment, StudentQuality.Conflict c) {
    1755            switch (c.getType().getType()) {
    1756                case REQUEST:
    1757                    if (c.getR1() instanceof CourseRequest)
    1758                        iTotalValue += c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1759                    else
    1760                        iTotalValue += c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    1761                    break;
    1762                case BOTH:
    1763                    iTotalValue += c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1764                    iTotalValue += c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    1765                    break;
    1766                case LOWER:
    1767                    iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1768                    break;
    1769                case HIGHER:
    1770                    iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1771                    break;
    1772            }
    1773        }
    1774
    1775        public void remove(Assignment<Request, Enrollment> assignment, StudentQuality.Conflict c) {
    1776            switch (c.getType().getType()) {
    1777                case REQUEST:
    1778                    if (c.getR1() instanceof CourseRequest)
    1779                        iTotalValue -= c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1780                    else
    1781                        iTotalValue -= c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    1782                    break;
    1783                case BOTH:
    1784                    iTotalValue -= c.getR1Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1785                    iTotalValue -= c.getR2Weight() * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE2(), c);
    1786                    break;
    1787                case LOWER:
    1788                    iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1789                    break;
    1790                case HIGHER:
    1791                    iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getStudentQualityConflictWeight(assignment, c.getE1(), c);
    1792                    break;
    1793            }
    1794        }
    1795
    1796        /**
    1797         * Students with complete schedules (see {@link Student#isComplete(Assignment)})
    1798         * @return students with complete schedule
    1799         */
    1800        public Set<Student> getCompleteStudents() {
    1801            return iCompleteStudents;
    1802        }
    1803
    1804        /**
    1805         * Number of students with complete schedule
    1806         * @return number of students with complete schedule
    1807         */
    1808        public int nrComplete() {
    1809            return getCompleteStudents().size();
    1810        }
    1811
    1812        /**
    1813         * Recompute cached request weights
    1814         * @param assignment curent assignment
    1815         */
    1816        public void requestWeightsChanged(Assignment<Request, Enrollment> assignment) {
    1817            iTotalCRWeight = 0.0;
    1818            iTotalDummyWeight = 0.0; iTotalDummyCRWeight = 0.0;
    1819            iTotalPriorityCRWeight = new double[StudentPriority.values().length];
    1820            iAssignedCRWeight = 0.0;
    1821            iAssignedDummyCRWeight = 0.0;
    1822            iAssignedCriticalCRWeight = new double[RequestPriority.values().length];
    1823            iAssignedPriorityCriticalCRWeight = new double[RequestPriority.values().length][StudentPriority.values().length];
    1824            for (int i = 0; i < RequestPriority.values().length; i++) {
    1825                iAssignedCriticalCRWeight[i] = 0.0;
    1826                for (int j = 0; j < StudentPriority.values().length; j++) {
    1827                    iAssignedPriorityCriticalCRWeight[i][j] = 0.0;
    1828                }
    1829            }
    1830            iAssignedPriorityCRWeight = new double[StudentPriority.values().length];
    1831            for (int i = 0; i < StudentPriority.values().length; i++) {
    1832                iAssignedPriorityCRWeight[i] = 0.0;
    1833            }
    1834            iNrDummyRequests = 0; iNrAssignedDummyRequests = 0;
    1835            iTotalReservedSpace = 0.0; iReservedSpace = 0.0;
    1836            iTotalMPPCRWeight = 0.0;
    1837            iTotalSelCRWeight = 0.0;
    1838            iAssignedNoTimeSectionWeight = 0.0;
    1839            iAssignedOnlineSectionWeight = 0.0;
    1840            iAssignedPastSectionWeight = 0.0;
    1841            for (Request request: variables()) {
    1842                boolean cr = (request instanceof CourseRequest);
    1843                if (cr && !request.isAlternative())
    1844                    iTotalCRWeight += request.getWeight();
    1845                if (request.getStudent().isDummy()) {
    1846                    iTotalDummyWeight += request.getWeight();
    1847                    iNrDummyRequests ++;
    1848                    if (cr && !request.isAlternative())
    1849                        iTotalDummyCRWeight += request.getWeight();
    1850                }
    1851                if (cr && !request.isAlternative()) {
    1852                    iTotalPriorityCRWeight[request.getStudent().getPriority().ordinal()] += request.getWeight();
    1853                }
    1854                if (request.isMPP())
    1855                    iTotalMPPCRWeight += request.getWeight();
    1856                if (request.hasSelection())
    1857                    iTotalSelCRWeight += request.getWeight();
    1858                Enrollment e = assignment.getValue(request);
    1859                if (e != null) {
    1860                    if (cr)
    1861                        iAssignedCRWeight += request.getWeight();
    1862                    if (cr && request.getRequestPriority() != RequestPriority.Normal && !request.getStudent().isDummy() && !request.isAlternative())
    1863                        iAssignedCriticalCRWeight[request.getRequestPriority().ordinal()] += request.getWeight();
    1864                    if (cr && request.getRequestPriority() != RequestPriority.Normal && !request.isAlternative())
    1865                        iAssignedPriorityCriticalCRWeight[request.getRequestPriority().ordinal()][request.getStudent().getPriority().ordinal()] += request.getWeight();
    1866                    if (request.isMPP()) {
    1867                        iAssignedSameSectionWeight += request.getWeight() * e.percentInitial();
    1868                        iAssignedSameChoiceWeight += request.getWeight() * e.percentSelected();
    1869                        iAssignedSameTimeWeight += request.getWeight() * e.percentSameTime();
    1870                    }
    1871                    if (request.hasSelection()) {
    1872                        iAssignedSelectedSectionWeight += request.getWeight() * e.percentSelectedSameSection();
    1873                        iAssignedSelectedConfigWeight += request.getWeight() * e.percentSelectedSameConfig();
    1874                    }
    1875                    if (e.getReservation() != null)
    1876                        iReservedSpace += request.getWeight();
    1877                    if (cr && ((CourseRequest)request).hasReservations())
    1878                        iTotalReservedSpace += request.getWeight();
    1879                    if (request.getStudent().isDummy()) {
    1880                        iNrAssignedDummyRequests ++;
    1881                        if (cr)
    1882                            iAssignedDummyCRWeight += request.getWeight();
    1883                    }
    1884                    if (cr) {
    1885                        iAssignedPriorityCRWeight[request.getStudent().getPriority().ordinal()] += request.getWeight();
    1886                    }
    1887                    if (cr) {
    1888                        int noTime = 0;
    1889                        int online = 0;
    1890                        int past = 0;
    1891                        for (Section section: e.getSections()) {
    1892                            if (!section.hasTime()) noTime ++;
    1893                            if (section.isOnline()) online ++;
    1894                            if (section.isPast()) past ++;
    1895                        }
    1896                        if (noTime > 0)
    1897                            iAssignedNoTimeSectionWeight += request.getWeight() * noTime / e.getSections().size();
    1898                        if (online > 0)
    1899                            iAssignedOnlineSectionWeight += request.getWeight() * online / e.getSections().size();
    1900                        if (past > 0)
    1901                            iAssignedPastSectionWeight += request.getWeight() * past / e.getSections().size();
    1902                    }
    1903                }
    1904            }
    1905        }
    1906
    1907        /**
    1908         * Overall solution value
    1909         * @return solution value
    1910         */
    1911        public double getTotalValue() {
    1912            return iTotalValue;
    1913        }
    1914
    1915        /**
    1916         * Number of last like ({@link Student#isDummy()} equals true) students with
    1917         * a complete schedule ({@link Student#isComplete(Assignment)} equals true).
    1918         * @return number of last like (projected) students with a complete schedule
    1919         */
    1920        public int getNrCompleteLastLikeStudents() {
    1921            return iNrCompleteDummyStudents;
    1922        }
    1923
    1924        /**
    1925         * Number of requests from projected ({@link Student#isDummy()} equals true)
    1926         * students that are assigned.
    1927         * @return number of real students with a complete schedule
    1928         */
    1929        public int getNrAssignedLastLikeRequests() {
    1930            return iNrAssignedDummyRequests;
    1931        }
    1932
    1933        @Override
    1934        public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) {
    1935            if (iTotalCRWeight > 0.0) {
    1936                info.put("Assigned course requests", sDecimalFormat.format(100.0 * iAssignedCRWeight / iTotalCRWeight) + "% (" + (int)Math.round(iAssignedCRWeight) + "/" + (int)Math.round(iTotalCRWeight) + ")");
    1937                if (iNrDummyStudents > 0 && iNrDummyStudents != getStudents().size() && iTotalCRWeight != iTotalDummyCRWeight) {
    1938                    if (iTotalDummyCRWeight > 0.0)
    1939                        info.put("Projected assigned course requests", sDecimalFormat.format(100.0 * iAssignedDummyCRWeight / iTotalDummyCRWeight) + "% (" + (int)Math.round(iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalDummyCRWeight) + ")");
    1940                    info.put("Real assigned course requests", sDecimalFormat.format(100.0 * (iAssignedCRWeight - iAssignedDummyCRWeight) / (iTotalCRWeight - iTotalDummyCRWeight)) +
    1941                            "% (" + (int)Math.round(iAssignedCRWeight - iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalCRWeight - iTotalDummyCRWeight) + ")");
    1942                }
    1943                if (iAssignedNoTimeSectionWeight > 0.0) {
    1944                    info.put("Using classes w/o time", sDecimalFormat.format(100.0 * iAssignedNoTimeSectionWeight / iAssignedCRWeight) + "% (" + sDecimalFormat.format(iAssignedNoTimeSectionWeight) + ")");
    1945                }
    1946                if (iAssignedOnlineSectionWeight > 0.0) {
    1947                    info.put("Using online classes", sDecimalFormat.format(100.0 * iAssignedOnlineSectionWeight / iAssignedCRWeight) + "% (" + sDecimalFormat.format(iAssignedOnlineSectionWeight) + ")");
    1948                }
    1949                if (iAssignedPastSectionWeight > 0.0) {
    1950                    info.put("Using past classes", sDecimalFormat.format(100.0 * iAssignedPastSectionWeight / iAssignedCRWeight) + "% (" + sDecimalFormat.format(iAssignedPastSectionWeight) + ")");
    1951                }
    1952            }
    1953            String priorityAssignedCR = "";
    1954            for (StudentPriority sp: StudentPriority.values()) {
    1955                if (sp != StudentPriority.Dummy && iTotalPriorityCRWeight[sp.ordinal()] > 0.0) {
    1956                    priorityAssignedCR += (priorityAssignedCR.isEmpty() ? "" : "\n") +
    1957                            sp.name() + ": " + sDecimalFormat.format(100.0 * iAssignedPriorityCRWeight[sp.ordinal()] / iTotalPriorityCRWeight[sp.ordinal()]) + "% (" + (int)Math.round(iAssignedPriorityCRWeight[sp.ordinal()]) + "/" + (int)Math.round(iTotalPriorityCRWeight[sp.ordinal()]) + ")";
    1958                }
    1959            }
    1960            if (!priorityAssignedCR.isEmpty())
    1961                info.put("Assigned course requests (priority students)", priorityAssignedCR);
    1962            for (RequestPriority rp: RequestPriority.values()) {
    1963                if (rp == RequestPriority.Normal) continue;
    1964                if (iTotalCriticalCRWeight[rp.ordinal()] > 0.0) {
    1965                    info.put("Assigned " + rp.name().toLowerCase() + " course requests", sDoubleFormat.format(100.0 * iAssignedCriticalCRWeight[rp.ordinal()] / iTotalCriticalCRWeight[rp.ordinal()]) + "% (" + (int)Math.round(iAssignedCriticalCRWeight[rp.ordinal()]) + "/" + (int)Math.round(iTotalCriticalCRWeight[rp.ordinal()]) + ")");
    1966                }
    1967                priorityAssignedCR = "";
    1968                for (StudentPriority sp: StudentPriority.values()) {
    1969                    if (sp != StudentPriority.Dummy && iTotalPriorityCriticalCRWeight[rp.ordinal()][sp.ordinal()] > 0.0) {
    1970                        priorityAssignedCR += (priorityAssignedCR.isEmpty() ? "" : "\n") +
    1971                                sp.name() + ": " + sDoubleFormat.format(100.0 * iAssignedPriorityCriticalCRWeight[rp.ordinal()][sp.ordinal()] / iTotalPriorityCriticalCRWeight[rp.ordinal()][sp.ordinal()]) + "% (" + (int)Math.round(iAssignedPriorityCriticalCRWeight[rp.ordinal()][sp.ordinal()]) + "/" + (int)Math.round(iTotalPriorityCriticalCRWeight[rp.ordinal()][sp.ordinal()]) + ")";
    1972                    }
    1973                }
    1974                if (!priorityAssignedCR.isEmpty())
    1975                    info.put("Assigned " + rp.name().toLowerCase() + " course requests (priority students)", priorityAssignedCR);
    1976            }
    1977            if (iTotalReservedSpace > 0.0)
    1978                info.put("Reservations", sDoubleFormat.format(100.0 * iReservedSpace / iTotalReservedSpace) + "% (" + Math.round(iReservedSpace) + "/" + Math.round(iTotalReservedSpace) + ")");
    1979            if (iMPP && iTotalMPPCRWeight > 0.0) {
    1980                info.put("Perturbations: same section", sDoubleFormat.format(100.0 * iAssignedSameSectionWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameSectionWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
    1981                if (iAssignedSameChoiceWeight > iAssignedSameSectionWeight)
    1982                    info.put("Perturbations: same choice",sDoubleFormat.format(100.0 * iAssignedSameChoiceWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameChoiceWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
    1983                if (iAssignedSameTimeWeight > iAssignedSameChoiceWeight)
    1984                    info.put("Perturbations: same time", sDoubleFormat.format(100.0 * iAssignedSameTimeWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameTimeWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
    1985            }
    1986            if (iTotalSelCRWeight > 0.0) {
    1987                info.put("Selection",sDoubleFormat.format(100.0 * (0.3 * iAssignedSelectedConfigWeight + 0.7 * iAssignedSelectedSectionWeight) / iTotalSelCRWeight) +
    1988                        "% (" + Math.round(0.3 * iAssignedSelectedConfigWeight + 0.7 * iAssignedSelectedSectionWeight) + "/" + Math.round(iTotalSelCRWeight) + ")");
    1989            }
    1990        }
    1991
    1992        @Override
    1993        public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) {
    1994        }
    1995
    1996        public double getAssignedCourseRequestWeight() {
    1997            return iAssignedCRWeight;
    1998        }
    1999
    2000        public double getAssignedCriticalCourseRequestWeight(RequestPriority rp) {
    2001            return iAssignedCriticalCRWeight[rp.ordinal()];
    2002        }
    2003    }
    2004
    2005    @Override
    2006    public InheritedAssignment<Request, Enrollment> createInheritedAssignment(Solution<Request, Enrollment> solution, int index) {
    2007        return new OptimisticInheritedAssignment<Request, Enrollment>(solution, index);
    2008    }
    2009
    2010    public DistanceMetric getDistanceMetric() {
    2011        return (iStudentQuality != null ? iStudentQuality.getDistanceMetric() : iDistanceConflict != null ? iDistanceConflict.getDistanceMetric() : null);
    2012    }
    2013
    2014    @Override
    2015    public StudentSectioningModelContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, StudentSectioningModelContext parentContext) {
    2016        return new StudentSectioningModelContext(parentContext);
    2017    }
    2018
    2019    @Override
    2020    public void addGlobalConstraint(GlobalConstraint<Request, Enrollment> constraint) {
    2021        super.addGlobalConstraint(constraint);
    2022        if (constraint instanceof DependentCourses)
    2023            iDependentCourses = (DependentCourses)constraint;
    2024    }
    2025
    2026    @Override
    2027    public void removeGlobalConstraint(GlobalConstraint<Request, Enrollment> constraint) {
    2028        super.removeGlobalConstraint(constraint);
    2029        if (constraint instanceof DependentCourses)
    2030            iDependentCourses = null;
    2031    }
    2032
    2033    public DependentCourses getDependentCoursesConstraint() {
    2034        return iDependentCourses;
    2035    }
    2036}