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