    001package net.sf.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 net.sf.cpsolver.ifs.model.Constraint;
    014import net.sf.cpsolver.ifs.model.ConstraintListener;
    015import net.sf.cpsolver.ifs.model.Model;
    016import net.sf.cpsolver.ifs.util.DataProperties;
    017import net.sf.cpsolver.studentsct.constraint.ConfigLimit;
    018import net.sf.cpsolver.studentsct.constraint.CourseLimit;
    019import net.sf.cpsolver.studentsct.constraint.LinkedSections;
    020import net.sf.cpsolver.studentsct.constraint.RequiredReservation;
    021import net.sf.cpsolver.studentsct.constraint.ReservationLimit;
    022import net.sf.cpsolver.studentsct.constraint.SectionLimit;
    023import net.sf.cpsolver.studentsct.constraint.StudentConflict;
    024import net.sf.cpsolver.studentsct.extension.DistanceConflict;
    025import net.sf.cpsolver.studentsct.extension.TimeOverlapsCounter;
    026import net.sf.cpsolver.studentsct.model.Config;
    027import net.sf.cpsolver.studentsct.model.Course;
    028import net.sf.cpsolver.studentsct.model.CourseRequest;
    029import net.sf.cpsolver.studentsct.model.Enrollment;
    030import net.sf.cpsolver.studentsct.model.Offering;
    031import net.sf.cpsolver.studentsct.model.Request;
    032import net.sf.cpsolver.studentsct.model.Section;
    033import net.sf.cpsolver.studentsct.model.Student;
    034import net.sf.cpsolver.studentsct.model.Subpart;
    035import net.sf.cpsolver.studentsct.reservation.Reservation;
    036import net.sf.cpsolver.studentsct.weights.PriorityStudentWeights;
    037import net.sf.cpsolver.studentsct.weights.StudentWeights;
    038
    039import org.apache.log4j.Logger;
    040
    041/**
    042 * Student sectioning model.
    043 *
    044 * <br>
    045 * <br>
    046 *
    047 * @version StudentSct 1.2 (Student Sectioning)<br>
    048 *          Copyright (C) 2007 - 2010 Tomáš Müller<br>
    049 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    050 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    051 * <br>
    052 *          This library is free software; you can redistribute it and/or modify
    053 *          it under the terms of the GNU Lesser General Public License as
    054 *          published by the Free Software Foundation; either version 3 of the
    055 *          License, or (at your option) any later version. <br>
    056 * <br>
    057 *          This library is distributed in the hope that it will be useful, but
    058 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    059 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    060 *          Lesser General Public License for more details. <br>
    061 * <br>
    062 *          You should have received a copy of the GNU Lesser General Public
    063 *          License along with this library; if not see
    064 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    065 */
    066public class StudentSectioningModel extends Model<Request, Enrollment> {
    067    private static Logger sLog = Logger.getLogger(StudentSectioningModel.class);
    068    protected static DecimalFormat sDecimalFormat = new DecimalFormat("0.000");
    069    private List<Student> iStudents = new ArrayList<Student>();
    070    private List<Offering> iOfferings = new ArrayList<Offering>();
    071    private List<LinkedSections> iLinkedSections = new ArrayList<LinkedSections>();
    072    private Set<Student> iCompleteStudents = new java.util.HashSet<Student>();
    073    private double iTotalValue = 0.0;
    074    private DataProperties iProperties;
    075    private DistanceConflict iDistanceConflict = null;
    076    private TimeOverlapsCounter iTimeOverlaps = null;
    077    private int iNrDummyStudents = 0, iNrDummyRequests = 0, iNrAssignedDummyRequests = 0, iNrCompleteDummyStudents = 0;
    078    private double iTotalDummyWeight = 0.0;
    079    private double iTotalCRWeight = 0.0, iTotalDummyCRWeight = 0.0, iAssignedCRWeight = 0.0, iAssignedDummyCRWeight = 0.0;
    080    private double iReservedSpace = 0.0, iTotalReservedSpace = 0.0;
    081    private StudentWeights iStudentWeights = null;
    082    private boolean iReservationCanAssignOverTheLimit;
    083    protected double iProjectedStudentWeight = 0.0100;
    084    private int iMaxDomainSize = -1;
    085
    086
    087    /**
    088     * Constructor
    089     *
    090     * @param properties
    091     *            configuration
    092     */
    093    @SuppressWarnings("unchecked")
    094    public StudentSectioningModel(DataProperties properties) {
    095        super();
    096        iReservationCanAssignOverTheLimit =  properties.getPropertyBoolean("Reservation.CanAssignOverTheLimit", false);
    097        iAssignedVariables = new HashSet<Request>();
    098        iUnassignedVariables = new HashSet<Request>();
    099        iPerturbVariables = new HashSet<Request>();
    100        iStudentWeights = new PriorityStudentWeights(properties);
    101        iMaxDomainSize = properties.getPropertyInt("Sectioning.MaxDomainSize", iMaxDomainSize);
    102        if (properties.getPropertyBoolean("Sectioning.SectionLimit", true)) {
    103            SectionLimit sectionLimit = new SectionLimit(properties);
    104            addGlobalConstraint(sectionLimit);
    105            if (properties.getPropertyBoolean("Sectioning.SectionLimit.Debug", false)) {
    106                sectionLimit.addConstraintListener(new ConstraintListener<Enrollment>() {
    107                    @Override
    108                    public void constraintBeforeAssigned(long iteration, Constraint<?, Enrollment> constraint,
    109                            Enrollment enrollment, Set<Enrollment> unassigned) {
    110                        if (enrollment.getStudent().isDummy())
    111                            for (Enrollment conflict : unassigned) {
    112                                if (!conflict.getStudent().isDummy()) {
    113                                    sLog.warn("Enrolment of a real student " + conflict.getStudent() + " is unassigned "
    114                                            + "\n  -- " + conflict + "\ndue to an enrollment of a dummy student "
    115                                            + enrollment.getStudent() + " " + "\n  -- " + enrollment);
    116                                }
    117                            }
    118                    }
    119
    120                    @Override
    121                    public void constraintAfterAssigned(long iteration, Constraint<?, Enrollment> constraint,
    122                            Enrollment assigned, Set<Enrollment> unassigned) {
    123                    }
    124                });
    125            }
    126        }
    127        if (properties.getPropertyBoolean("Sectioning.ConfigLimit", true)) {
    128            ConfigLimit configLimit = new ConfigLimit(properties);
    129            addGlobalConstraint(configLimit);
    130        }
    131        if (properties.getPropertyBoolean("Sectioning.CourseLimit", true)) {
    132            CourseLimit courseLimit = new CourseLimit(properties);
    133            addGlobalConstraint(courseLimit);
    134        }
    135        if (properties.getPropertyBoolean("Sectioning.ReservationLimit", true)) {
    136            ReservationLimit reservationLimit = new ReservationLimit(properties);
    137            addGlobalConstraint(reservationLimit);
    138        }
    139        if (properties.getPropertyBoolean("Sectioning.RequiredReservations", true)) {
    140            RequiredReservation requiredReservation = new RequiredReservation();
    141            addGlobalConstraint(requiredReservation);
    142        }
    143        try {
    144            Class<StudentWeights> studentWeightsClass = (Class<StudentWeights>)Class.forName(properties.getProperty("StudentWeights.Class", PriorityStudentWeights.class.getName()));
    145            iStudentWeights = studentWeightsClass.getConstructor(DataProperties.class).newInstance(properties);
    146        } catch (Exception e) {
    147            sLog.error("Unable to create custom student weighting model (" + e.getMessage() + "), using default.", e);
    148            iStudentWeights = new PriorityStudentWeights(properties);
    149        }
    150        iProjectedStudentWeight = properties.getPropertyDouble("StudentWeights.ProjectedStudentWeight", iProjectedStudentWeight);
    151        iProperties = properties;
    152    }
    153
    154    /**
    155     * Return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit
    156     */
    157    public boolean getReservationCanAssignOverTheLimit() {
    158        return iReservationCanAssignOverTheLimit;
    159    }
    160
    161    /**
    162     * Return student weighting model
    163     */
    164    public StudentWeights getStudentWeights() {
    165        return iStudentWeights;
    166    }
    167
    168    /**
    169     * Set student weighting model
    170     */
    171    public void setStudentWeights(StudentWeights weights) {
    172        iStudentWeights = weights;
    173    }
    174
    175    /**
    176     * Students
    177     */
    178    public List<Student> getStudents() {
    179        return iStudents;
    180    }
    181
    182    /**
    183     * Students with complete schedules (see {@link Student#isComplete()})
    184     */
    185    public Set<Student> getCompleteStudents() {
    186        return iCompleteStudents;
    187    }
    188
    189    /**
    190     * Add a student into the model
    191     */
    192    public void addStudent(Student student) {
    193        iStudents.add(student);
    194        if (student.isDummy())
    195            iNrDummyStudents++;
    196        for (Request request : student.getRequests())
    197            addVariable(request);
    198        if (getProperties().getPropertyBoolean("Sectioning.StudentConflict", true)) {
    199            addConstraint(new StudentConflict(student));
    200        }
    201        if (student.isComplete())
    202            iCompleteStudents.add(student);
    203    }
    204
    205    @Override
    206    public void addVariable(Request request) {
    207        super.addVariable(request);
    208        if (request instanceof CourseRequest)
    209            iTotalCRWeight += request.getWeight();
    210        if (request.getStudent().isDummy()) {
    211            iNrDummyRequests++;
    212            iTotalDummyWeight += request.getWeight();
    213            if (request instanceof CourseRequest)
    214                iTotalDummyCRWeight += request.getWeight();
    215        }
    216    }
    217
    218    /**
    219     * Recompute cached request weights
    220     */
    221    public void requestWeightsChanged() {
    222        iTotalCRWeight = 0.0;
    223        iTotalDummyWeight = 0.0; iTotalDummyCRWeight = 0.0;
    224        iAssignedCRWeight = 0.0;
    225        iAssignedDummyCRWeight = 0.0;
    226        iNrDummyRequests = 0; iNrAssignedDummyRequests = 0;
    227        iTotalReservedSpace = 0.0; iReservedSpace = 0.0;
    228        for (Request request: variables()) {
    229            boolean cr = (request instanceof CourseRequest);
    230            if (cr)
    231                iTotalCRWeight += request.getWeight();
    232            if (request.getStudent().isDummy()) {
    233                iTotalDummyWeight += request.getWeight();
    234                iNrDummyRequests ++;
    235                if (cr)
    236                    iTotalDummyCRWeight += request.getWeight();
    237            }
    238            if (request.getAssignment() != null) {
    239                if (cr)
    240                    iAssignedCRWeight += request.getWeight();
    241                if (request.getAssignment().getReservation() != null)
    242                    iReservedSpace += request.getWeight();
    243                if (cr && ((CourseRequest)request).hasReservations())
    244                    iTotalReservedSpace += request.getWeight();
    245                if (request.getStudent().isDummy()) {
    246                    iNrAssignedDummyRequests ++;
    247                    if (cr)
    248                        iAssignedDummyCRWeight += request.getWeight();
    249                }
    250            }
    251        }
    252    }
    253
    254    /**
    255     * Remove a student from the model
    256     */
    257    public void removeStudent(Student student) {
    258        iStudents.remove(student);
    259        if (student.isDummy())
    260            iNrDummyStudents--;
    261        if (student.isComplete())
    262            iCompleteStudents.remove(student);
    263        StudentConflict conflict = null;
    264        for (Request request : student.getRequests()) {
    265            for (Constraint<Request, Enrollment> c : request.constraints()) {
    266                if (c instanceof StudentConflict) {
    267                    conflict = (StudentConflict) c;
    268                    break;
    269                }
    270            }
    271            if (conflict != null)
    272                conflict.removeVariable(request);
    273            removeVariable(request);
    274        }
    275        if (conflict != null)
    276            removeConstraint(conflict);
    277    }
    278
    279    @Override
    280    public void removeVariable(Request request) {
    281        super.removeVariable(request);
    282        if (request instanceof CourseRequest) {
    283            CourseRequest cr = (CourseRequest)request;
    284            for (Course course: cr.getCourses())
    285                course.getRequests().remove(request);
    286        }
    287        if (request.getStudent().isDummy()) {
    288            iNrDummyRequests--;
    289            iTotalDummyWeight -= request.getWeight();
    290            if (request instanceof CourseRequest)
    291                iTotalDummyCRWeight -= request.getWeight();
    292        }
    293        if (request instanceof CourseRequest)
    294            iTotalCRWeight -= request.getWeight();
    295    }
    296
    297
    298    /**
    299     * List of offerings
    300     */
    301    public List<Offering> getOfferings() {
    302        return iOfferings;
    303    }
    304
    305    /**
    306     * Add an offering into the model
    307     */
    308    public void addOffering(Offering offering) {
    309        iOfferings.add(offering);
    310    }
    311
    312    /**
    313     * Link sections using {@link LinkedSections}
    314     */
    315    public void addLinkedSections(Section... sections) {
    316        LinkedSections constraint = new LinkedSections(sections);
    317        iLinkedSections.add(constraint);
    318        constraint.createConstraints();
    319    }
    320
    321    /**
    322     * Link sections using {@link LinkedSections}
    323     */
    324    public void addLinkedSections(Collection<Section> sections) {
    325        LinkedSections constraint = new LinkedSections(sections);
    326        iLinkedSections.add(constraint);
    327        constraint.createConstraints();
    328    }
    329
    330    /**
    331     * List of linked sections
    332     */
    333    public List<LinkedSections> getLinkedSections() {
    334        return iLinkedSections;
    335    }
    336
    337    /**
    338     * Number of students with complete schedule
    339     */
    340    public int nrComplete() {
    341        return getCompleteStudents().size();
    342    }
    343
    344    /**
    345     * Model info
    346     */
    347    @Override
    348    public Map<String, String> getInfo() {
    349        Map<String, String> info = super.getInfo();
    350        if (!getStudents().isEmpty())
    351            info.put("Students with complete schedule", sDoubleFormat.format(100.0 * nrComplete() / getStudents().size())
    352                    + "% (" + nrComplete() + "/" + getStudents().size() + ")");
    353        if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() != 0)
    354            info.put("Student distance conflicts", String.valueOf(getDistanceConflict().getTotalNrConflicts()));
    355        if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() != 0)
    356            info.put("Time overlapping conflicts", String.valueOf(getTimeOverlaps().getTotalNrConflicts()));
    357        int nrLastLikeStudents = getNrLastLikeStudents(false);
    358        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
    359            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
    360            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(false);
    361            int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents;
    362            if (nrLastLikeStudents > 0)
    363                info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
    364                        * nrLastLikeCompleteStudents / nrLastLikeStudents)
    365                        + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
    366            if (nrRealStudents > 0)
    367                info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
    368                        / nrRealStudents)
    369                        + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
    370            int nrLastLikeRequests = getNrLastLikeRequests(false);
    371            int nrRealRequests = variables().size() - nrLastLikeRequests;
    372            int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(false);
    373            int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests;
    374            if (nrLastLikeRequests > 0)
    375                info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests / nrLastLikeRequests)
    376                        + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
    377            if (nrRealRequests > 0)
    378                info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
    379                        + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
    380            if (iTotalCRWeight > 0.0) {
    381                info.put("Assigned course requests", sDecimalFormat.format(100.0 * iAssignedCRWeight / iTotalCRWeight) + "% (" + (int)Math.round(iAssignedCRWeight) + "/" + (int)Math.round(iTotalCRWeight) + ")");
    382                if (iTotalDummyCRWeight != iTotalCRWeight) {
    383                    if (iTotalDummyCRWeight > 0.0)
    384                        info.put("Projected assigned course requests", sDecimalFormat.format(100.0 * iAssignedDummyCRWeight / iTotalDummyCRWeight) + "% (" + (int)Math.round(iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalDummyCRWeight) + ")");
    385                    info.put("Real assigned course requests", sDecimalFormat.format(100.0 * (iAssignedCRWeight - iAssignedDummyCRWeight) / (iTotalCRWeight - iTotalDummyCRWeight)) +
    386                            "% (" + (int)Math.round(iAssignedCRWeight - iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalCRWeight - iTotalDummyCRWeight) + ")");
    387                }
    388            }
    389            if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() > 0)
    390                info.put("Student distance conflicts", String.valueOf(getDistanceConflict().getTotalNrConflicts()));
    391            if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() > 0)
    392                info.put("Time overlapping conflicts", String.valueOf(getTimeOverlaps().getTotalNrConflicts()));
    393        }
    394        if (iTotalReservedSpace > 0.0)
    395            info.put("Reservations", sDoubleFormat.format(100.0 * iReservedSpace / iTotalReservedSpace) + "% (" + Math.round(iReservedSpace) + "/" + Math.round(iTotalReservedSpace) + ")");
    396
    397        return info;
    398    }
    399
    400    /**
    401     * Overall solution value
    402     */
    403    public double getTotalValue(boolean precise) {
    404        if (precise) {
    405            double total = 0;
    406            for (Request r: assignedVariables())
    407                total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment());
    408            if (iDistanceConflict != null)
    409                for (DistanceConflict.Conflict c: iDistanceConflict.computeAllConflicts())
    410                    total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c);
    411            if (iTimeOverlaps != null)
    412                for (TimeOverlapsCounter.Conflict c: iTimeOverlaps.computeAllConflicts()) {
    413                    total -= c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c);
    414                    total -= c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c);
    415                }
    416            return -total;
    417        }
    418        return iTotalValue;
    419    }
    420
    421    /**
    422     * Overall solution value
    423     */
    424    @Override
    425    public double getTotalValue() {
    426        return iTotalValue;
    427    }
    428
    429
    430    /**
    431     * Called after an enrollment was assigned to a request. The list of
    432     * complete students and the overall solution value are updated.
    433     */
    434    @Override
    435    public void afterAssigned(long iteration, Enrollment enrollment) {
    436        super.afterAssigned(iteration, enrollment);
    437        Student student = enrollment.getStudent();
    438        if (student.isComplete())
    439            iCompleteStudents.add(student);
    440        double value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(enrollment);
    441        iTotalValue -= value;
    442        enrollment.setExtra(value);
    443        if (enrollment.isCourseRequest())
    444            iAssignedCRWeight += enrollment.getRequest().getWeight();
    445        if (enrollment.getReservation() != null)
    446            iReservedSpace += enrollment.getRequest().getWeight();
    447        if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
    448            iTotalReservedSpace += enrollment.getRequest().getWeight();
    449        if (student.isDummy()) {
    450            iNrAssignedDummyRequests++;
    451            if (enrollment.isCourseRequest())
    452                iAssignedDummyCRWeight += enrollment.getRequest().getWeight();
    453            if (student.isComplete())
    454                iNrCompleteDummyStudents++;
    455        }
    456    }
    457
    458    /**
    459     * Called before an enrollment was unassigned from a request. The list of
    460     * complete students and the overall solution value are updated.
    461     */
    462    @Override
    463    public void afterUnassigned(long iteration, Enrollment enrollment) {
    464        super.afterUnassigned(iteration, enrollment);
    465        Student student = enrollment.getStudent();
    466        if (iCompleteStudents.contains(student) && !student.isComplete()) {
    467            iCompleteStudents.remove(student);
    468            if (student.isDummy())
    469                iNrCompleteDummyStudents--;
    470        }
    471        Double value = (Double)enrollment.getExtra();
    472        if (value == null)
    473            value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(enrollment);
    474        iTotalValue += value;
    475        enrollment.setExtra(null);
    476        if (enrollment.isCourseRequest())
    477            iAssignedCRWeight -= enrollment.getRequest().getWeight();
    478        if (enrollment.getReservation() != null)
    479            iReservedSpace -= enrollment.getRequest().getWeight();
    480        if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
    481            iTotalReservedSpace -= enrollment.getRequest().getWeight();
    482        if (student.isDummy()) {
    483            iNrAssignedDummyRequests--;
    484            if (enrollment.isCourseRequest())
    485                iAssignedDummyCRWeight -= enrollment.getRequest().getWeight();
    486        }
    487    }
    488
    489    /**
    490     * Configuration
    491     */
    492    public DataProperties getProperties() {
    493        return iProperties;
    494    }
    495
    496    /**
    497     * Empty online student sectioning infos for all sections (see
    498     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
    499     */
    500    public void clearOnlineSectioningInfos() {
    501        for (Offering offering : iOfferings) {
    502            for (Config config : offering.getConfigs()) {
    503                for (Subpart subpart : config.getSubparts()) {
    504                    for (Section section : subpart.getSections()) {
    505                        section.setSpaceExpected(0);
    506                        section.setSpaceHeld(0);
    507                    }
    508                }
    509            }
    510        }
    511    }
    512
    513    /**
    514     * Compute online student sectioning infos for all sections (see
    515     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
    516     */
    517    public void computeOnlineSectioningInfos() {
    518        clearOnlineSectioningInfos();
    519        for (Student student : getStudents()) {
    520            if (!student.isDummy())
    521                continue;
    522            for (Request request : student.getRequests()) {
    523                if (!(request instanceof CourseRequest))
    524                    continue;
    525                CourseRequest courseRequest = (CourseRequest) request;
    526                Enrollment enrollment = courseRequest.getAssignment();
    527                if (enrollment != null) {
    528                    for (Section section : enrollment.getSections()) {
    529                        section.setSpaceHeld(courseRequest.getWeight() + section.getSpaceHeld());
    530                    }
    531                }
    532                List<Enrollment> feasibleEnrollments = new ArrayList<Enrollment>();
    533                int totalLimit = 0;
    534                for (Enrollment enrl : courseRequest.values()) {
    535                    boolean overlaps = false;
    536                    for (Request otherRequest : student.getRequests()) {
    537                        if (otherRequest.equals(courseRequest) || !(otherRequest instanceof CourseRequest))
    538                            continue;
    539                        Enrollment otherErollment = otherRequest.getAssignment();
    540                        if (otherErollment == null)
    541                            continue;
    542                        if (enrl.isOverlapping(otherErollment)) {
    543                            overlaps = true;
    544                            break;
    545                        }
    546                    }
    547                    if (!overlaps) {
    548                        feasibleEnrollments.add(enrl);
    549                        if (totalLimit >= 0) {
    550                            int limit = enrl.getLimit();
    551                            if (limit < 0) totalLimit = -1;
    552                            else totalLimit += limit;
    553                        }
    554                    }
    555                }
    556                double increment = courseRequest.getWeight() / (totalLimit > 0 ? totalLimit : feasibleEnrollments.size());
    557                for (Enrollment feasibleEnrollment : feasibleEnrollments) {
    558                    for (Section section : feasibleEnrollment.getSections()) {
    559                        if (totalLimit > 0) {
    560                            section.setSpaceExpected(section.getSpaceExpected() + increment * feasibleEnrollment.getLimit());
    561                        } else {
    562                            section.setSpaceExpected(section.getSpaceExpected() + increment);
    563                        }
    564                    }
    565                }
    566            }
    567        }
    568    }
    569
    570    /**
    571     * Sum of weights of all requests that are not assigned (see
    572     * {@link Request#getWeight()}).
    573     */
    574    public double getUnassignedRequestWeight() {
    575        double weight = 0.0;
    576        for (Request request : unassignedVariables()) {
    577            weight += request.getWeight();
    578        }
    579        return weight;
    580    }
    581
    582    /**
    583     * Sum of weights of all requests (see {@link Request#getWeight()}).
    584     */
    585    public double getTotalRequestWeight() {
    586        double weight = 0.0;
    587        for (Request request : unassignedVariables()) {
    588            weight += request.getWeight();
    589        }
    590        return weight;
    591    }
    592
    593    /**
    594     * Set distance conflict extension
    595     */
    596    public void setDistanceConflict(DistanceConflict dc) {
    597        iDistanceConflict = dc;
    598    }
    599
    600    /**
    601     * Return distance conflict extension
    602     */
    603    public DistanceConflict getDistanceConflict() {
    604        return iDistanceConflict;
    605    }
    606
    607    /**
    608     * Set time overlaps extension
    609     */
    610    public void setTimeOverlaps(TimeOverlapsCounter toc) {
    611        iTimeOverlaps = toc;
    612    }
    613
    614    /**
    615     * Return time overlaps extension
    616     */
    617    public TimeOverlapsCounter getTimeOverlaps() {
    618        return iTimeOverlaps;
    619    }
    620
    621    /**
    622     * Average priority of unassigned requests (see
    623     * {@link Request#getPriority()})
    624     */
    625    public double avgUnassignPriority() {
    626        double totalPriority = 0.0;
    627        for (Request request : unassignedVariables()) {
    628            if (request.isAlternative())
    629                continue;
    630            totalPriority += request.getPriority();
    631        }
    632        return 1.0 + totalPriority / unassignedVariables().size();
    633    }
    634
    635    /**
    636     * Average number of requests per student (see {@link Student#getRequests()}
    637     * )
    638     */
    639    public double avgNrRequests() {
    640        double totalRequests = 0.0;
    641        int totalStudents = 0;
    642        for (Student student : getStudents()) {
    643            if (student.nrRequests() == 0)
    644                continue;
    645            totalRequests += student.nrRequests();
    646            totalStudents++;
    647        }
    648        return totalRequests / totalStudents;
    649    }
    650
    651    /** Number of last like ({@link Student#isDummy()} equals true) students. */
    652    public int getNrLastLikeStudents(boolean precise) {
    653        if (!precise)
    654            return iNrDummyStudents;
    655        int nrLastLikeStudents = 0;
    656        for (Student student : getStudents()) {
    657            if (student.isDummy())
    658                nrLastLikeStudents++;
    659        }
    660        return nrLastLikeStudents;
    661    }
    662
    663    /** Number of real ({@link Student#isDummy()} equals false) students. */
    664    public int getNrRealStudents(boolean precise) {
    665        if (!precise)
    666            return getStudents().size() - iNrDummyStudents;
    667        int nrRealStudents = 0;
    668        for (Student student : getStudents()) {
    669            if (!student.isDummy())
    670                nrRealStudents++;
    671        }
    672        return nrRealStudents;
    673    }
    674
    675    /**
    676     * Number of last like ({@link Student#isDummy()} equals true) students with
    677     * a complete schedule ({@link Student#isComplete()} equals true).
    678     */
    679    public int getNrCompleteLastLikeStudents(boolean precise) {
    680        if (!precise)
    681            return iNrCompleteDummyStudents;
    682        int nrLastLikeStudents = 0;
    683        for (Student student : getCompleteStudents()) {
    684            if (student.isDummy())
    685                nrLastLikeStudents++;
    686        }
    687        return nrLastLikeStudents;
    688    }
    689
    690    /**
    691     * Number of real ({@link Student#isDummy()} equals false) students with a
    692     * complete schedule ({@link Student#isComplete()} equals true).
    693     */
    694    public int getNrCompleteRealStudents(boolean precise) {
    695        if (!precise)
    696            return getCompleteStudents().size() - iNrCompleteDummyStudents;
    697        int nrRealStudents = 0;
    698        for (Student student : getCompleteStudents()) {
    699            if (!student.isDummy())
    700                nrRealStudents++;
    701        }
    702        return nrRealStudents;
    703    }
    704
    705    /**
    706     * Number of requests from projected ({@link Student#isDummy()} equals true)
    707     * students.
    708     */
    709    public int getNrLastLikeRequests(boolean precise) {
    710        if (!precise)
    711            return iNrDummyRequests;
    712        int nrLastLikeRequests = 0;
    713        for (Request request : variables()) {
    714            if (request.getStudent().isDummy())
    715                nrLastLikeRequests++;
    716        }
    717        return nrLastLikeRequests;
    718    }
    719
    720    /**
    721     * Number of requests from real ({@link Student#isDummy()} equals false)
    722     * students.
    723     */
    724    public int getNrRealRequests(boolean precise) {
    725        if (!precise)
    726            return variables().size() - iNrDummyRequests;
    727        int nrRealRequests = 0;
    728        for (Request request : variables()) {
    729            if (!request.getStudent().isDummy())
    730                nrRealRequests++;
    731        }
    732        return nrRealRequests;
    733    }
    734
    735    /**
    736     * Number of requests from projected ({@link Student#isDummy()} equals true)
    737     * students that are assigned.
    738     */
    739    public int getNrAssignedLastLikeRequests(boolean precise) {
    740        if (!precise)
    741            return iNrAssignedDummyRequests;
    742        int nrLastLikeRequests = 0;
    743        for (Request request : assignedVariables()) {
    744            if (request.getStudent().isDummy())
    745                nrLastLikeRequests++;
    746        }
    747        return nrLastLikeRequests;
    748    }
    749
    750    /**
    751     * Number of requests from real ({@link Student#isDummy()} equals false)
    752     * students that are assigned.
    753     */
    754    public int getNrAssignedRealRequests(boolean precise) {
    755        if (!precise)
    756            return assignedVariables().size() - iNrAssignedDummyRequests;
    757        int nrRealRequests = 0;
    758        for (Request request : assignedVariables()) {
    759            if (!request.getStudent().isDummy())
    760                nrRealRequests++;
    761        }
    762        return nrRealRequests;
    763    }
    764
    765    /**
    766     * Model extended info. Some more information (that is more expensive to
    767     * compute) is added to an ordinary {@link Model#getInfo()}.
    768     */
    769    @Override
    770    public Map<String, String> getExtendedInfo() {
    771        Map<String, String> info = getInfo();
    772        /*
    773        int nrLastLikeStudents = getNrLastLikeStudents(true);
    774        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
    775            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
    776            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(true);
    777            int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents;
    778            info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
    779                    * nrLastLikeCompleteStudents / nrLastLikeStudents)
    780                    + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
    781            info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
    782                    / nrRealStudents)
    783                    + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
    784            int nrLastLikeRequests = getNrLastLikeRequests(true);
    785            int nrRealRequests = variables().size() - nrLastLikeRequests;
    786            int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(true);
    787            int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests;
    788            info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests
    789                    / nrLastLikeRequests)
    790                    + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
    791            info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
    792                    + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
    793        }
    794        */
    795        // info.put("Average unassigned priority", sDecimalFormat.format(avgUnassignPriority()));
    796        // info.put("Average number of requests", sDecimalFormat.format(avgNrRequests()));
    797
    798        /*
    799        double total = 0;
    800        for (Request r: variables())
    801            if (r.getAssignment() != null)
    802                total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment());
    803        */
    804        double dc = 0;
    805        if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts() != 0) {
    806            Set<DistanceConflict.Conflict> conf = getDistanceConflict().getAllConflicts();
    807            for (DistanceConflict.Conflict c: conf)
    808                dc += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c);
    809            if (!conf.isEmpty())
    810                info.put("Student distance conflicts", conf.size() + " (weighted: " + sDecimalFormat.format(dc) + ")");
    811        }
    812        double toc = 0;
    813        if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts() != 0) {
    814            Set<TimeOverlapsCounter.Conflict> conf = getTimeOverlaps().getAllConflicts();
    815            int share = 0;
    816            for (TimeOverlapsCounter.Conflict c: conf) {
    817                toc += c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c);
    818                toc += c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c);
    819                share += c.getShare();
    820            }
    821            if (toc != 0.0)
    822                info.put("Time overlapping conflicts", share + " (average: " + sDecimalFormat.format(5.0 * share / getStudents().size()) + " min, weighted: " + sDoubleFormat.format(toc) + ")");
    823        }
    824        /*
    825        info.put("Overall solution value", sDecimalFormat.format(total - dc - toc) + (dc == 0.0 && toc == 0.0 ? "" :
    826            " (" + (dc != 0.0 ? "distance: " + sDecimalFormat.format(dc): "") + (dc != 0.0 && toc != 0.0 ? ", " : "") +
    827            (toc != 0.0 ? "overlap: " + sDecimalFormat.format(toc) : "") + ")")
    828            );
    829        */
    830
    831        double disbWeight = 0;
    832        int disbSections = 0;
    833        int disb10Sections = 0;
    834        int disb10Limit = getProperties().getPropertyInt("Info.ListDisbalancedSections", 0);
    835        Set<String> disb10SectionList = (disb10Limit == 0 ? null : new TreeSet<String>());
    836        for (Offering offering: getOfferings()) {
    837            for (Config config: offering.getConfigs()) {
    838                double enrl = config.getEnrollmentWeight(null);
    839                for (Subpart subpart: config.getSubparts()) {
    840                    if (subpart.getSections().size() <= 1) continue;
    841                    if (subpart.getLimit() > 0) {
    842                        // sections have limits -> desired size is section limit x (total enrollment / total limit)
    843                        double ratio = enrl / subpart.getLimit();
    844                        for (Section section: subpart.getSections()) {
    845                            double desired = ratio * section.getLimit();
    846                            disbWeight += Math.abs(section.getEnrollmentWeight(null) - desired);
    847                            disbSections ++;
    848                            if (Math.abs(desired - section.getEnrollmentWeight(null)) >= Math.max(1.0, 0.1 * section.getLimit())) {
    849                                disb10Sections++;
    850                                if (disb10SectionList != null)
    851                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName());
    852                            }
    853                        }
    854                    } else {
    855                        // unlimited sections -> desired size is total enrollment / number of sections
    856                        for (Section section: subpart.getSections()) {
    857                            double desired = enrl / subpart.getSections().size();
    858                            disbWeight += Math.abs(section.getEnrollmentWeight(null) - desired);
    859                            disbSections ++;
    860                            if (Math.abs(desired - section.getEnrollmentWeight(null)) >= Math.max(1.0, 0.1 * desired)) {
    861                                disb10Sections++;
    862                                if (disb10SectionList != null)
    863                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName());
    864                            }
    865                        }
    866                    }
    867                }
    868            }
    869        }
    870        if (disbSections != 0) {
    871            info.put("Average disbalance", sDecimalFormat.format(disbWeight / disbSections) +
    872                    " (" + sDecimalFormat.format(iAssignedCRWeight == 0 ? 0.0 : 100.0 * disbWeight / iAssignedCRWeight) + "%)");
    873            String list = "";
    874            if (disb10SectionList != null) {
    875                int i = 0;
    876                for (String section: disb10SectionList) {
    877                    if (i == disb10Limit) {
    878                        list += "<br>...";
    879                        break;
    880                    }
    881                    list += "<br>" + section;
    882                    i++;
    883                }
    884            }
    885            info.put("Sections disbalanced by 10% or more", disb10Sections + " (" + sDecimalFormat.format(disbSections == 0 ? 0.0 : 100.0 * disb10Sections / disbSections) + "%)" + list);
    886        }
    887        return info;
    888    }
    889
    890    @Override
    891    public void restoreBest() {
    892        restoreBest(new Comparator<Request>() {
    893            @Override
    894            public int compare(Request r1, Request r2) {
    895                Enrollment e1 = r1.getBestAssignment();
    896                Enrollment e2 = r2.getBestAssignment();
    897                // Reservations first
    898                if (e1.getReservation() != null && e2.getReservation() == null) return -1;
    899                if (e1.getReservation() == null && e2.getReservation() != null) return 1;
    900                // Then assignment iteration (i.e., order in which assignments were made)
    901                if (r1.getBestAssignmentIteration() != r2.getBestAssignmentIteration())
    902                    return (r1.getBestAssignmentIteration() < r2.getBestAssignmentIteration() ? -1 : 1);
    903                // Then student and priority
    904                return r1.compareTo(r2);
    905            }
    906        });
    907    }
    908
    909    @Override
    910    public String toString() {
    911        return   (getNrRealStudents(false) > 0 ? "RRq:" + getNrAssignedRealRequests(false) + "/" + getNrRealRequests(false) + ", " : "")
    912                + (getNrLastLikeStudents(false) > 0 ? "DRq:" + getNrAssignedLastLikeRequests(false) + "/" + getNrLastLikeRequests(false) + ", " : "")
    913                + (getNrRealStudents(false) > 0 ? "RS:" + getNrCompleteRealStudents(false) + "/" + getNrRealStudents(false) + ", " : "")
    914                + (getNrLastLikeStudents(false) > 0 ? "DS:" + getNrCompleteLastLikeStudents(false) + "/" + getNrLastLikeStudents(false) + ", " : "")
    915                + "V:"
    916                + sDecimalFormat.format(-getTotalValue())
    917                + (getDistanceConflict() == null ? "" : ", DC:" + getDistanceConflict().getTotalNrConflicts())
    918                + (getTimeOverlaps() == null ? "" : ", TOC:" + getTimeOverlaps().getTotalNrConflicts())
    919                + ", %:" + sDecimalFormat.format(-100.0 * getTotalValue() / (getStudents().size() - iNrDummyStudents +
    920                        (iProjectedStudentWeight < 0.0 ? iNrDummyStudents * (iTotalDummyWeight / iNrDummyRequests) :iProjectedStudentWeight * iTotalDummyWeight)));
    921
    922    }
    923
    924    /**
    925     * Quadratic average of two weights.
    926     */
    927    public double avg(double w1, double w2) {
    928        return Math.sqrt(w1 * w2);
    929    }
    930
    931    public void add(DistanceConflict.Conflict c) {
    932        iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c);
    933    }
    934
    935    public void remove(DistanceConflict.Conflict c) {
    936        iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(c);
    937    }
    938
    939    public void add(TimeOverlapsCounter.Conflict c) {
    940        iTotalValue += c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c);
    941        iTotalValue += c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c);
    942    }
    943
    944    public void remove(TimeOverlapsCounter.Conflict c) {
    945        iTotalValue -= c.getR1().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE1(), c);
    946        iTotalValue -= c.getR2().getWeight() * iStudentWeights.getTimeOverlapConflictWeight(c.getE2(), c);
    947    }
    948
    949    /**
    950     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
    951     */
    952    public int getMaxDomainSize() { return iMaxDomainSize; }
    953
    954    /**
    955     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
    956     */
    957    public void setMaxDomainSize(int maxDomainSize) { iMaxDomainSize = maxDomainSize; }
    958}