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