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