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.log4j.Logger;
014import org.cpsolver.ifs.assignment.Assignment;
015import org.cpsolver.ifs.assignment.InheritedAssignment;
016import org.cpsolver.ifs.assignment.OptimisticInheritedAssignment;
017import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
018import org.cpsolver.ifs.assignment.context.ModelWithContext;
019import org.cpsolver.ifs.model.Constraint;
020import org.cpsolver.ifs.model.ConstraintListener;
021import org.cpsolver.ifs.model.InfoProvider;
022import org.cpsolver.ifs.model.Model;
023import org.cpsolver.ifs.solution.Solution;
024import org.cpsolver.ifs.util.DataProperties;
025import org.cpsolver.ifs.util.DistanceMetric;
026import org.cpsolver.studentsct.constraint.CancelledSections;
027import org.cpsolver.studentsct.constraint.ConfigLimit;
028import org.cpsolver.studentsct.constraint.CourseLimit;
029import org.cpsolver.studentsct.constraint.FixInitialAssignments;
030import org.cpsolver.studentsct.constraint.LinkedSections;
031import org.cpsolver.studentsct.constraint.RequiredReservation;
032import org.cpsolver.studentsct.constraint.ReservationLimit;
033import org.cpsolver.studentsct.constraint.SectionLimit;
034import org.cpsolver.studentsct.constraint.StudentConflict;
035import org.cpsolver.studentsct.constraint.StudentNotAvailable;
036import org.cpsolver.studentsct.extension.DistanceConflict;
037import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
038import org.cpsolver.studentsct.model.Config;
039import org.cpsolver.studentsct.model.Course;
040import org.cpsolver.studentsct.model.CourseRequest;
041import org.cpsolver.studentsct.model.Enrollment;
042import org.cpsolver.studentsct.model.Offering;
043import org.cpsolver.studentsct.model.Request;
044import org.cpsolver.studentsct.model.RequestGroup;
045import org.cpsolver.studentsct.model.Section;
046import org.cpsolver.studentsct.model.Student;
047import org.cpsolver.studentsct.model.Subpart;
048import org.cpsolver.studentsct.reservation.Reservation;
049import org.cpsolver.studentsct.weights.PriorityStudentWeights;
050import org.cpsolver.studentsct.weights.StudentWeights;
051
052/**
053 * Student sectioning model.
054 * 
055 * <br>
056 * <br>
057 * 
058 * @version StudentSct 1.3 (Student Sectioning)<br>
059 *          Copyright (C) 2007 - 2014 Tomas Muller<br>
060 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
061 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
062 * <br>
063 *          This library is free software; you can redistribute it and/or modify
064 *          it under the terms of the GNU Lesser General Public License as
065 *          published by the Free Software Foundation; either version 3 of the
066 *          License, or (at your option) any later version. <br>
067 * <br>
068 *          This library is distributed in the hope that it will be useful, but
069 *          WITHOUT ANY WARRANTY; without even the implied warranty of
070 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
071 *          Lesser General Public License for more details. <br>
072 * <br>
073 *          You should have received a copy of the GNU Lesser General Public
074 *          License along with this library; if not see
075 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
076 */
077public class StudentSectioningModel extends ModelWithContext<Request, Enrollment, StudentSectioningModel.StudentSectioningModelContext> {
078    private static Logger sLog = Logger.getLogger(StudentSectioningModel.class);
079    protected static DecimalFormat sDecimalFormat = new DecimalFormat("0.00");
080    private List<Student> iStudents = new ArrayList<Student>();
081    private List<Offering> iOfferings = new ArrayList<Offering>();
082    private List<LinkedSections> iLinkedSections = new ArrayList<LinkedSections>();
083    private DataProperties iProperties;
084    private DistanceConflict iDistanceConflict = null;
085    private TimeOverlapsCounter iTimeOverlaps = null;
086    private int iNrDummyStudents = 0, iNrDummyRequests = 0;
087    private double iTotalDummyWeight = 0.0;
088    private double iTotalCRWeight = 0.0, iTotalDummyCRWeight = 0.0;
089    private double iTotalMPPCRWeight = 0.0;
090    private double iTotalSelCRWeight = 0.0;
091    private StudentWeights iStudentWeights = null;
092    private boolean iReservationCanAssignOverTheLimit;
093    private boolean iMPP;
094    private boolean iKeepInitials;
095    protected double iProjectedStudentWeight = 0.0100;
096    private int iMaxDomainSize = -1; 
097
098
099    /**
100     * Constructor
101     * 
102     * @param properties
103     *            configuration
104     */
105    @SuppressWarnings("unchecked")
106    public StudentSectioningModel(DataProperties properties) {
107        super();
108        iReservationCanAssignOverTheLimit =  properties.getPropertyBoolean("Reservation.CanAssignOverTheLimit", false);
109        iMPP = properties.getPropertyBoolean("General.MPP", false);
110        iKeepInitials = properties.getPropertyBoolean("Sectioning.KeepInitialAssignments", false);
111        iStudentWeights = new PriorityStudentWeights(properties);
112        iMaxDomainSize = properties.getPropertyInt("Sectioning.MaxDomainSize", iMaxDomainSize);
113        if (properties.getPropertyBoolean("Sectioning.SectionLimit", true)) {
114            SectionLimit sectionLimit = new SectionLimit(properties);
115            addGlobalConstraint(sectionLimit);
116            if (properties.getPropertyBoolean("Sectioning.SectionLimit.Debug", false)) {
117                sectionLimit.addConstraintListener(new ConstraintListener<Request, Enrollment>() {
118                    @Override
119                    public void constraintBeforeAssigned(Assignment<Request, Enrollment> assignment, long iteration, Constraint<Request, Enrollment> constraint, Enrollment enrollment, Set<Enrollment> unassigned) {
120                        if (enrollment.getStudent().isDummy())
121                            for (Enrollment conflict : unassigned) {
122                                if (!conflict.getStudent().isDummy()) {
123                                    sLog.warn("Enrolment of a real student " + conflict.getStudent() + " is unassigned "
124                                            + "\n  -- " + conflict + "\ndue to an enrollment of a dummy student "
125                                            + enrollment.getStudent() + " " + "\n  -- " + enrollment);
126                                }
127                            }
128                    }
129
130                    @Override
131                    public void constraintAfterAssigned(Assignment<Request, Enrollment> assignment, long iteration, Constraint<Request, Enrollment> constraint, Enrollment assigned, Set<Enrollment> unassigned) {
132                    }
133                });
134            }
135        }
136        if (properties.getPropertyBoolean("Sectioning.ConfigLimit", true)) {
137            ConfigLimit configLimit = new ConfigLimit(properties);
138            addGlobalConstraint(configLimit);
139        }
140        if (properties.getPropertyBoolean("Sectioning.CourseLimit", true)) {
141            CourseLimit courseLimit = new CourseLimit(properties);
142            addGlobalConstraint(courseLimit);
143        }
144        if (properties.getPropertyBoolean("Sectioning.ReservationLimit", true)) {
145            ReservationLimit reservationLimit = new ReservationLimit(properties);
146            addGlobalConstraint(reservationLimit);
147        }
148        if (properties.getPropertyBoolean("Sectioning.RequiredReservations", true)) {
149            RequiredReservation requiredReservation = new RequiredReservation();
150            addGlobalConstraint(requiredReservation);
151        }
152        if (properties.getPropertyBoolean("Sectioning.CancelledSections", true)) {
153            CancelledSections cancelledSections = new CancelledSections();
154            addGlobalConstraint(cancelledSections);
155        }
156        if (properties.getPropertyBoolean("Sectioning.StudentNotAvailable", true)) {
157            StudentNotAvailable studentNotAvailable = new StudentNotAvailable();
158            addGlobalConstraint(studentNotAvailable);
159        }
160        if (iMPP && iKeepInitials) {
161            addGlobalConstraint(new FixInitialAssignments());
162        }
163        try {
164            Class<StudentWeights> studentWeightsClass = (Class<StudentWeights>)Class.forName(properties.getProperty("StudentWeights.Class", PriorityStudentWeights.class.getName()));
165            iStudentWeights = studentWeightsClass.getConstructor(DataProperties.class).newInstance(properties);
166        } catch (Exception e) {
167            sLog.error("Unable to create custom student weighting model (" + e.getMessage() + "), using default.", e);
168            iStudentWeights = new PriorityStudentWeights(properties);
169        }
170        iProjectedStudentWeight = properties.getPropertyDouble("StudentWeights.ProjectedStudentWeight", iProjectedStudentWeight);
171        iProperties = properties;
172    }
173    
174    /**
175     * Return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit
176     * @return true if reservation that has {@link Reservation#canAssignOverLimit()} can assign enrollments over the limit
177     */
178    public boolean getReservationCanAssignOverTheLimit() {
179        return iReservationCanAssignOverTheLimit;
180    }
181    
182    /**
183     * Return true if the problem is minimal perturbation problem 
184     * @return true if MPP is enabled
185     */
186    public boolean isMPP() {
187        return iMPP;
188    }
189    
190    /**
191     * Return true if the inital assignments are to be kept unchanged 
192     * @return true if the initial assignments are to be kept at all cost
193     */
194    public boolean getKeepInitialAssignments() {
195        return iKeepInitials;
196    }
197    
198    /**
199     * Return student weighting model
200     * @return student weighting model
201     */
202    public StudentWeights getStudentWeights() {
203        return iStudentWeights;
204    }
205
206    /**
207     * Set student weighting model
208     * @param weights student weighting model
209     */
210    public void setStudentWeights(StudentWeights weights) {
211        iStudentWeights = weights;
212    }
213
214    /**
215     * Students
216     * @return all students in the problem
217     */
218    public List<Student> getStudents() {
219        return iStudents;
220    }
221
222    /**
223     * Add a student into the model
224     * @param student a student to be added into the problem
225     */
226    public void addStudent(Student student) {
227        iStudents.add(student);
228        if (student.isDummy())
229            iNrDummyStudents++;
230        for (Request request : student.getRequests())
231            addVariable(request);
232        if (getProperties().getPropertyBoolean("Sectioning.StudentConflict", true)) {
233            addConstraint(new StudentConflict(student));
234        }
235    }
236    
237    @Override
238    public void addVariable(Request request) {
239        super.addVariable(request);
240        if (request instanceof CourseRequest)
241            iTotalCRWeight += request.getWeight();
242        if (request.getStudent().isDummy()) {
243            iNrDummyRequests++;
244            iTotalDummyWeight += request.getWeight();
245            if (request instanceof CourseRequest)
246                iTotalDummyCRWeight += request.getWeight();
247        }
248        if (request.isMPP())
249            iTotalMPPCRWeight += request.getWeight();
250        if (request.hasSelection())
251            iTotalSelCRWeight += request.getWeight();
252    }
253    
254    /** 
255     * Recompute cached request weights
256     * @param assignment current assignment
257     */
258    public void requestWeightsChanged(Assignment<Request, Enrollment> assignment) {
259        getContext(assignment).requestWeightsChanged(assignment);
260    }
261
262    /**
263     * Remove a student from the model
264     * @param student a student to be removed from the problem
265     */
266    public void removeStudent(Student student) {
267        iStudents.remove(student);
268        if (student.isDummy())
269            iNrDummyStudents--;
270        StudentConflict conflict = null;
271        for (Request request : student.getRequests()) {
272            for (Constraint<Request, Enrollment> c : request.constraints()) {
273                if (c instanceof StudentConflict) {
274                    conflict = (StudentConflict) c;
275                    break;
276                }
277            }
278            if (conflict != null) 
279                conflict.removeVariable(request);
280            removeVariable(request);
281        }
282        if (conflict != null) 
283            removeConstraint(conflict);
284    }
285    
286    @Override
287    public void removeVariable(Request request) {
288        super.removeVariable(request);
289        if (request instanceof CourseRequest) {
290            CourseRequest cr = (CourseRequest)request;
291            for (Course course: cr.getCourses())
292                course.getRequests().remove(request);
293        }
294        if (request.getStudent().isDummy()) {
295            iNrDummyRequests--;
296            iTotalDummyWeight -= request.getWeight();
297            if (request instanceof CourseRequest)
298                iTotalDummyCRWeight -= request.getWeight();
299        }
300        if (request.isMPP())
301            iTotalMPPCRWeight -= request.getWeight();
302        if (request.hasSelection())
303            iTotalSelCRWeight -= request.getWeight();
304        if (request instanceof CourseRequest)
305            iTotalCRWeight -= request.getWeight();
306    }
307
308
309    /**
310     * List of offerings
311     * @return all instructional offerings of the problem
312     */
313    public List<Offering> getOfferings() {
314        return iOfferings;
315    }
316
317    /**
318     * Add an offering into the model
319     * @param offering an instructional offering to be added into the problem
320     */
321    public void addOffering(Offering offering) {
322        iOfferings.add(offering);
323        offering.setModel(this);
324    }
325    
326    /**
327     * Link sections using {@link LinkedSections}
328     * @param mustBeUsed if true,  a pair of linked sections must be used when a student requests both courses 
329     * @param sections a linked section constraint to be added into the problem
330     */
331    public void addLinkedSections(boolean mustBeUsed, Section... sections) {
332        LinkedSections constraint = new LinkedSections(sections);
333        constraint.setMustBeUsed(mustBeUsed);
334        iLinkedSections.add(constraint);
335        constraint.createConstraints();
336    }
337    
338    /**
339     * Link sections using {@link LinkedSections}
340     * @param sections a linked section constraint to be added into the problem
341     */
342    @Deprecated
343    public void addLinkedSections(Section... sections) {
344        addLinkedSections(false, sections);
345    }
346
347    /**
348     * Link sections using {@link LinkedSections}
349     * @param mustBeUsed if true,  a pair of linked sections must be used when a student requests both courses 
350     * @param sections a linked section constraint to be added into the problem
351     */
352    public void addLinkedSections(boolean mustBeUsed, Collection<Section> sections) {
353        LinkedSections constraint = new LinkedSections(sections);
354        constraint.setMustBeUsed(mustBeUsed);
355        iLinkedSections.add(constraint);
356        constraint.createConstraints();
357    }
358    
359    /**
360     * Link sections using {@link LinkedSections}
361     * @param sections a linked section constraint to be added into the problem
362     */
363    @Deprecated
364    public void addLinkedSections(Collection<Section> sections) {
365        addLinkedSections(false, sections);
366    }
367
368    /**
369     * List of linked sections
370     * @return all linked section constraints of the problem
371     */
372    public List<LinkedSections> getLinkedSections() {
373        return iLinkedSections;
374    }
375
376    /**
377     * Model info
378     */
379    @Override
380    public Map<String, String> getInfo(Assignment<Request, Enrollment> assignment) {
381        Map<String, String> info = super.getInfo(assignment);
382        StudentSectioningModelContext context = getContext(assignment);
383        if (!getStudents().isEmpty())
384            info.put("Students with complete schedule", sDoubleFormat.format(100.0 * context.nrComplete() / getStudents().size()) + "% (" + context.nrComplete() + "/" + getStudents().size() + ")");
385        if (getDistanceConflict() != null) {
386            int confs = getDistanceConflict().getTotalNrConflicts(assignment);
387            if (confs > 0) {
388                int shortConfs = getDistanceConflict().getTotalNrShortConflicts(assignment);
389                info.put("Student distance conflicts", confs + (shortConfs == 0 ? "" : " (" + getDistanceConflict().getDistanceMetric().getShortDistanceAccommodationReference() + ": " + shortConfs + ")"));
390            }
391        }
392        if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts(assignment) != 0)
393            info.put("Time overlapping conflicts", String.valueOf(getTimeOverlaps().getTotalNrConflicts(assignment)));
394        int nrLastLikeStudents = getNrLastLikeStudents(false);
395        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
396            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
397            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(assignment, false);
398            int nrRealCompleteStudents = context.nrComplete() - nrLastLikeCompleteStudents;
399            if (nrLastLikeStudents > 0)
400                info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
401                        * nrLastLikeCompleteStudents / nrLastLikeStudents)
402                        + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
403            if (nrRealStudents > 0)
404                info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
405                        / nrRealStudents)
406                        + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
407            int nrLastLikeRequests = getNrLastLikeRequests(false);
408            int nrRealRequests = variables().size() - nrLastLikeRequests;
409            int nrLastLikeAssignedRequests = context.getNrAssignedLastLikeRequests();
410            int nrRealAssignedRequests = assignment.nrAssignedVariables() - nrLastLikeAssignedRequests;
411            if (nrLastLikeRequests > 0)
412                info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests / nrLastLikeRequests)
413                        + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
414            if (nrRealRequests > 0)
415                info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
416                        + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
417        }
418        context.getInfo(assignment, info);
419        
420        
421        double groupSpread = 0.0; double groupCount = 0;
422        for (Offering offering: iOfferings) {
423            for (Course course: offering.getCourses()) {
424                for (RequestGroup group: course.getRequestGroups()) {
425                    groupSpread += group.getAverageSpread(assignment) * group.getEnrollmentWeight(assignment, null);
426                    groupCount += group.getEnrollmentWeight(assignment, null);
427                }
428            }
429        }
430        if (groupCount > 0)
431            info.put("Same group", sDecimalFormat.format(100.0 * groupSpread / groupCount) + "%");
432
433        return info;
434    }
435
436    /**
437     * Overall solution value
438     * @param assignment current assignment
439     * @param precise true if should be computed
440     * @return solution value
441     */
442    public double getTotalValue(Assignment<Request, Enrollment> assignment, boolean precise) {
443        if (precise) {
444            double total = 0;
445            for (Request r: assignment.assignedVariables())
446                total += r.getWeight() * iStudentWeights.getWeight(assignment, assignment.getValue(r));
447            if (iDistanceConflict != null)
448                for (DistanceConflict.Conflict c: iDistanceConflict.computeAllConflicts(assignment))
449                    total -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
450            if (iTimeOverlaps != null)
451                for (TimeOverlapsCounter.Conflict c: iTimeOverlaps.getContext(assignment).computeAllConflicts(assignment)) {
452                    if (c.getR1() != null) total -= c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
453                    if (c.getR2() != null) total -= c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
454                }
455            return -total;
456        }
457        return getContext(assignment).getTotalValue();
458    }
459    
460    /**
461     * Overall solution value
462     */
463    @Override
464    public double getTotalValue(Assignment<Request, Enrollment> assignment) {
465        return getContext(assignment).getTotalValue();
466    }
467
468    /**
469     * Configuration
470     * @return solver configuration
471     */
472    public DataProperties getProperties() {
473        return iProperties;
474    }
475
476    /**
477     * Empty online student sectioning infos for all sections (see
478     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
479     */
480    public void clearOnlineSectioningInfos() {
481        for (Offering offering : iOfferings) {
482            for (Config config : offering.getConfigs()) {
483                for (Subpart subpart : config.getSubparts()) {
484                    for (Section section : subpart.getSections()) {
485                        section.setSpaceExpected(0);
486                        section.setSpaceHeld(0);
487                    }
488                }
489            }
490        }
491    }
492
493    /**
494     * Compute online student sectioning infos for all sections (see
495     * {@link Section#getSpaceExpected()} and {@link Section#getSpaceHeld()}).
496     * @param assignment current assignment
497     */
498    public void computeOnlineSectioningInfos(Assignment<Request, Enrollment> assignment) {
499        clearOnlineSectioningInfos();
500        for (Student student : getStudents()) {
501            if (!student.isDummy())
502                continue;
503            for (Request request : student.getRequests()) {
504                if (!(request instanceof CourseRequest))
505                    continue;
506                CourseRequest courseRequest = (CourseRequest) request;
507                Enrollment enrollment = assignment.getValue(courseRequest);
508                if (enrollment != null) {
509                    for (Section section : enrollment.getSections()) {
510                        section.setSpaceHeld(courseRequest.getWeight() + section.getSpaceHeld());
511                    }
512                }
513                List<Enrollment> feasibleEnrollments = new ArrayList<Enrollment>();
514                int totalLimit = 0;
515                for (Enrollment enrl : courseRequest.values(assignment)) {
516                    boolean overlaps = false;
517                    for (Request otherRequest : student.getRequests()) {
518                        if (otherRequest.equals(courseRequest) || !(otherRequest instanceof CourseRequest))
519                            continue;
520                        Enrollment otherErollment = assignment.getValue(otherRequest);
521                        if (otherErollment == null)
522                            continue;
523                        if (enrl.isOverlapping(otherErollment)) {
524                            overlaps = true;
525                            break;
526                        }
527                    }
528                    if (!overlaps) {
529                        feasibleEnrollments.add(enrl);
530                        if (totalLimit >= 0) {
531                            int limit = enrl.getLimit();
532                            if (limit < 0) totalLimit = -1;
533                            else totalLimit += limit;
534                        }
535                    }
536                }
537                double increment = courseRequest.getWeight() / (totalLimit > 0 ? totalLimit : feasibleEnrollments.size());
538                for (Enrollment feasibleEnrollment : feasibleEnrollments) {
539                    for (Section section : feasibleEnrollment.getSections()) {
540                        if (totalLimit > 0) {
541                            section.setSpaceExpected(section.getSpaceExpected() + increment * feasibleEnrollment.getLimit());
542                        } else {
543                            section.setSpaceExpected(section.getSpaceExpected() + increment);
544                        }
545                    }
546                }
547            }
548        }
549    }
550
551    /**
552     * Sum of weights of all requests that are not assigned (see
553     * {@link Request#getWeight()}).
554     * @param assignment current assignment
555     * @return unassigned request weight
556     */
557    public double getUnassignedRequestWeight(Assignment<Request, Enrollment> assignment) {
558        double weight = 0.0;
559        for (Request request : assignment.unassignedVariables(this)) {
560            weight += request.getWeight();
561        }
562        return weight;
563    }
564
565    /**
566     * Sum of weights of all requests (see {@link Request#getWeight()}).
567     * @return total request weight
568     */
569    public double getTotalRequestWeight() {
570        double weight = 0.0;
571        for (Request request : variables()) {
572            weight += request.getWeight();
573        }
574        return weight;
575    }
576
577    /**
578     * Set distance conflict extension
579     * @param dc distance conflicts extension
580     */
581    public void setDistanceConflict(DistanceConflict dc) {
582        iDistanceConflict = dc;
583    }
584
585    /**
586     * Return distance conflict extension
587     * @return distance conflicts extension
588     */
589    public DistanceConflict getDistanceConflict() {
590        return iDistanceConflict;
591    }
592
593    /**
594     * Set time overlaps extension
595     * @param toc time overlapping conflicts extension
596     */
597    public void setTimeOverlaps(TimeOverlapsCounter toc) {
598        iTimeOverlaps = toc;
599    }
600
601    /**
602     * Return time overlaps extension
603     * @return time overlapping conflicts extension
604     */
605    public TimeOverlapsCounter getTimeOverlaps() {
606        return iTimeOverlaps;
607    }
608
609    /**
610     * Average priority of unassigned requests (see
611     * {@link Request#getPriority()})
612     * @param assignment current assignment
613     * @return average priority of unassigned requests
614     */
615    public double avgUnassignPriority(Assignment<Request, Enrollment> assignment) {
616        double totalPriority = 0.0;
617        for (Request request : assignment.unassignedVariables(this)) {
618            if (request.isAlternative())
619                continue;
620            totalPriority += request.getPriority();
621        }
622        return 1.0 + totalPriority / assignment.nrUnassignedVariables(this);
623    }
624
625    /**
626     * Average number of requests per student (see {@link Student#getRequests()}
627     * )
628     * @return average number of requests per student
629     */
630    public double avgNrRequests() {
631        double totalRequests = 0.0;
632        int totalStudents = 0;
633        for (Student student : getStudents()) {
634            if (student.nrRequests() == 0)
635                continue;
636            totalRequests += student.nrRequests();
637            totalStudents++;
638        }
639        return totalRequests / totalStudents;
640    }
641
642    /** Number of last like ({@link Student#isDummy()} equals true) students. 
643     * @param precise true if to be computed
644     * @return number of last like (projected) students
645     **/
646    public int getNrLastLikeStudents(boolean precise) {
647        if (!precise)
648            return iNrDummyStudents;
649        int nrLastLikeStudents = 0;
650        for (Student student : getStudents()) {
651            if (student.isDummy())
652                nrLastLikeStudents++;
653        }
654        return nrLastLikeStudents;
655    }
656
657    /** Number of real ({@link Student#isDummy()} equals false) students. 
658     * @param precise true if to be computed
659     * @return number of real students
660     **/
661    public int getNrRealStudents(boolean precise) {
662        if (!precise)
663            return getStudents().size() - iNrDummyStudents;
664        int nrRealStudents = 0;
665        for (Student student : getStudents()) {
666            if (!student.isDummy())
667                nrRealStudents++;
668        }
669        return nrRealStudents;
670    }
671
672    /**
673     * Number of last like ({@link Student#isDummy()} equals true) students with
674     * a complete schedule ({@link Student#isComplete(Assignment)} equals true).
675     * @param assignment current assignment
676     * @param precise true if to be computed
677     * @return number of last like (projected) students with a complete schedule
678     */
679    public int getNrCompleteLastLikeStudents(Assignment<Request, Enrollment> assignment, boolean precise) {
680        if (!precise)
681            return getContext(assignment).getNrCompleteLastLikeStudents();
682        int nrLastLikeStudents = 0;
683        for (Student student : getStudents()) {
684            if (student.isComplete(assignment) && student.isDummy())
685                nrLastLikeStudents++;
686        }
687        return nrLastLikeStudents;
688    }
689
690    /**
691     * Number of real ({@link Student#isDummy()} equals false) students with a
692     * complete schedule ({@link Student#isComplete(Assignment)} equals true).
693     * @param assignment current assignment
694     * @param precise true if to be computed
695     * @return number of real students with a complete schedule
696     */
697    public int getNrCompleteRealStudents(Assignment<Request, Enrollment> assignment, boolean precise) {
698        if (!precise)
699            return getContext(assignment).nrComplete() - getContext(assignment).getNrCompleteLastLikeStudents();
700        int nrRealStudents = 0;
701        for (Student student : getStudents()) {
702            if (student.isComplete(assignment) && !student.isDummy())
703                nrRealStudents++;
704        }
705        return nrRealStudents;
706    }
707
708    /**
709     * Number of requests from projected ({@link Student#isDummy()} equals true)
710     * students.
711     * @param precise true if to be computed
712     * @return number of requests from projected students 
713     */
714    public int getNrLastLikeRequests(boolean precise) {
715        if (!precise)
716            return iNrDummyRequests;
717        int nrLastLikeRequests = 0;
718        for (Request request : variables()) {
719            if (request.getStudent().isDummy())
720                nrLastLikeRequests++;
721        }
722        return nrLastLikeRequests;
723    }
724
725    /**
726     * Number of requests from real ({@link Student#isDummy()} equals false)
727     * students.
728     * @param precise true if to be computed
729     * @return number of requests from real students 
730     */
731    public int getNrRealRequests(boolean precise) {
732        if (!precise)
733            return variables().size() - iNrDummyRequests;
734        int nrRealRequests = 0;
735        for (Request request : variables()) {
736            if (!request.getStudent().isDummy())
737                nrRealRequests++;
738        }
739        return nrRealRequests;
740    }
741
742    /**
743     * Number of requests from projected ({@link Student#isDummy()} equals true)
744     * students that are assigned.
745     * @param assignment current assignment
746     * @param precise true if to be computed
747     * @return number of requests from projected students that are assigned
748     */
749    public int getNrAssignedLastLikeRequests(Assignment<Request, Enrollment> assignment, boolean precise) {
750        if (!precise)
751            return getContext(assignment).getNrAssignedLastLikeRequests();
752        int nrLastLikeRequests = 0;
753        for (Request request : assignment.assignedVariables()) {
754            if (request.getStudent().isDummy())
755                nrLastLikeRequests++;
756        }
757        return nrLastLikeRequests;
758    }
759
760    /**
761     * Number of requests from real ({@link Student#isDummy()} equals false)
762     * students that are assigned.
763     * @param assignment current assignment
764     * @param precise true if to be computed
765     * @return number of requests from real students that are assigned
766     */
767    public int getNrAssignedRealRequests(Assignment<Request, Enrollment> assignment, boolean precise) {
768        if (!precise)
769            return assignment.nrAssignedVariables() - getContext(assignment).getNrAssignedLastLikeRequests();
770        int nrRealRequests = 0;
771        for (Request request : assignment.assignedVariables()) {
772            if (!request.getStudent().isDummy())
773                nrRealRequests++;
774        }
775        return nrRealRequests;
776    }
777
778    /**
779     * Model extended info. Some more information (that is more expensive to
780     * compute) is added to an ordinary {@link Model#getInfo(Assignment)}.
781     */
782    @Override
783    public Map<String, String> getExtendedInfo(Assignment<Request, Enrollment> assignment) {
784        Map<String, String> info = getInfo(assignment);
785        /*
786        int nrLastLikeStudents = getNrLastLikeStudents(true);
787        if (nrLastLikeStudents != 0 && nrLastLikeStudents != getStudents().size()) {
788            int nrRealStudents = getStudents().size() - nrLastLikeStudents;
789            int nrLastLikeCompleteStudents = getNrCompleteLastLikeStudents(true);
790            int nrRealCompleteStudents = getCompleteStudents().size() - nrLastLikeCompleteStudents;
791            info.put("Projected students with complete schedule", sDecimalFormat.format(100.0
792                    * nrLastLikeCompleteStudents / nrLastLikeStudents)
793                    + "% (" + nrLastLikeCompleteStudents + "/" + nrLastLikeStudents + ")");
794            info.put("Real students with complete schedule", sDecimalFormat.format(100.0 * nrRealCompleteStudents
795                    / nrRealStudents)
796                    + "% (" + nrRealCompleteStudents + "/" + nrRealStudents + ")");
797            int nrLastLikeRequests = getNrLastLikeRequests(true);
798            int nrRealRequests = variables().size() - nrLastLikeRequests;
799            int nrLastLikeAssignedRequests = getNrAssignedLastLikeRequests(true);
800            int nrRealAssignedRequests = assignedVariables().size() - nrLastLikeAssignedRequests;
801            info.put("Projected assigned requests", sDecimalFormat.format(100.0 * nrLastLikeAssignedRequests
802                    / nrLastLikeRequests)
803                    + "% (" + nrLastLikeAssignedRequests + "/" + nrLastLikeRequests + ")");
804            info.put("Real assigned requests", sDecimalFormat.format(100.0 * nrRealAssignedRequests / nrRealRequests)
805                    + "% (" + nrRealAssignedRequests + "/" + nrRealRequests + ")");
806        }
807        */
808        // info.put("Average unassigned priority", sDecimalFormat.format(avgUnassignPriority()));
809        // info.put("Average number of requests", sDecimalFormat.format(avgNrRequests()));
810        
811        /*
812        double total = 0;
813        for (Request r: variables())
814            if (r.getAssignment() != null)
815                total += r.getWeight() * iStudentWeights.getWeight(r.getAssignment());
816        */
817        double dc = 0;
818        if (getDistanceConflict() != null && getDistanceConflict().getTotalNrConflicts(assignment) != 0) {
819            Set<DistanceConflict.Conflict> conf = getDistanceConflict().getAllConflicts(assignment);
820            int sdc = 0;
821            for (DistanceConflict.Conflict c: conf) {
822                dc += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
823                if (c.getStudent().isNeedShortDistances()) sdc ++;
824            }
825            if (!conf.isEmpty())
826                info.put("Student distance conflicts", conf.size() + (sdc > 0 ? " (" + getDistanceConflict().getDistanceMetric().getShortDistanceAccommodationReference() + ": " + sdc + ", weighted: " : " (weighted: ") + sDecimalFormat.format(dc) + ")");
827        }
828        double toc = 0;
829        if (getTimeOverlaps() != null && getTimeOverlaps().getTotalNrConflicts(assignment) != 0) {
830            Set<TimeOverlapsCounter.Conflict> conf = getTimeOverlaps().getAllConflicts(assignment);
831            int share = 0;
832            for (TimeOverlapsCounter.Conflict c: conf) {
833                if (c.getR1() != null)
834                    toc += c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
835                if (c.getR2() != null) 
836                    toc += c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
837                share += c.getShare();
838            }
839            if (toc != 0.0)
840                info.put("Time overlapping conflicts", share + " (average: " + sDecimalFormat.format(5.0 * share / getStudents().size()) + " min, weighted: " + sDoubleFormat.format(toc) + ")");
841        }
842        /*
843        info.put("Overall solution value", sDecimalFormat.format(total - dc - toc) + (dc == 0.0 && toc == 0.0 ? "" :
844            " (" + (dc != 0.0 ? "distance: " + sDecimalFormat.format(dc): "") + (dc != 0.0 && toc != 0.0 ? ", " : "") + 
845            (toc != 0.0 ? "overlap: " + sDecimalFormat.format(toc) : "") + ")")
846            );
847        */
848        
849        double disbWeight = 0;
850        int disbSections = 0;
851        int disb10Sections = 0;
852        int disb10Limit = getProperties().getPropertyInt("Info.ListDisbalancedSections", 0);
853        Set<String> disb10SectionList = (disb10Limit == 0 ? null : new TreeSet<String>()); 
854        for (Offering offering: getOfferings()) {
855            for (Config config: offering.getConfigs()) {
856                double enrl = config.getEnrollmentTotalWeight(assignment, null);
857                for (Subpart subpart: config.getSubparts()) {
858                    if (subpart.getSections().size() <= 1) continue;
859                    if (subpart.getLimit() > 0) {
860                        // sections have limits -> desired size is section limit x (total enrollment / total limit)
861                        double ratio = enrl / subpart.getLimit();
862                        for (Section section: subpart.getSections()) {
863                            double desired = ratio * section.getLimit();
864                            disbWeight += Math.abs(section.getEnrollmentTotalWeight(assignment, null) - desired);
865                            disbSections ++;
866                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, 0.1 * section.getLimit())) {
867                                disb10Sections++;
868                                if (disb10SectionList != null)
869                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName()); 
870                            }
871                        }
872                    } else {
873                        // unlimited sections -> desired size is total enrollment / number of sections
874                        for (Section section: subpart.getSections()) {
875                            double desired = enrl / subpart.getSections().size();
876                            disbWeight += Math.abs(section.getEnrollmentTotalWeight(assignment, null) - desired);
877                            disbSections ++;
878                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, 0.1 * desired)) {
879                                disb10Sections++;
880                                if (disb10SectionList != null)
881                                        disb10SectionList.add(section.getSubpart().getConfig().getOffering().getName() + " " + section.getSubpart().getName() + " " + section.getName());
882                            }
883                        }
884                    }
885                }
886            }
887        }
888        if (disbSections != 0) {
889            double assignedCRWeight = getContext(assignment).getAssignedCourseRequestWeight();
890            info.put("Average disbalance", sDecimalFormat.format(disbWeight / disbSections) + " (" + sDecimalFormat.format(assignedCRWeight == 0 ? 0.0 : 100.0 * disbWeight / assignedCRWeight) + "%)");
891            String list = "";
892            if (disb10SectionList != null) {
893                int i = 0;
894                for (String section: disb10SectionList) {
895                    if (i == disb10Limit) {
896                        list += "<br>...";
897                        break;
898                    }
899                    list += "<br>" + section;
900                    i++;
901                }
902            }
903            info.put("Sections disbalanced by 10% or more", disb10Sections + " (" + sDecimalFormat.format(disbSections == 0 ? 0.0 : 100.0 * disb10Sections / disbSections) + "%)" + list);
904        }
905        
906        return info;
907    }
908    
909    @Override
910    public void restoreBest(Assignment<Request, Enrollment> assignment) {
911        restoreBest(assignment, new Comparator<Request>() {
912            @Override
913            public int compare(Request r1, Request r2) {
914                Enrollment e1 = r1.getBestAssignment();
915                Enrollment e2 = r2.getBestAssignment();
916                // Reservations first
917                if (e1.getReservation() != null && e2.getReservation() == null) return -1;
918                if (e1.getReservation() == null && e2.getReservation() != null) return 1;
919                // Then assignment iteration (i.e., order in which assignments were made)
920                if (r1.getBestAssignmentIteration() != r2.getBestAssignmentIteration())
921                    return (r1.getBestAssignmentIteration() < r2.getBestAssignmentIteration() ? -1 : 1);
922                // Then student and priority
923                return r1.compareTo(r2);
924            }
925        });
926    }
927        
928    @Override
929    public String toString(Assignment<Request, Enrollment> assignment) {
930        double groupSpread = 0.0; double groupCount = 0;
931        for (Offering offering: iOfferings) {
932            for (Course course: offering.getCourses()) {
933                for (RequestGroup group: course.getRequestGroups()) {
934                    groupSpread += group.getAverageSpread(assignment) * group.getEnrollmentWeight(assignment, null);
935                    groupCount += group.getEnrollmentWeight(assignment, null);
936                }
937            }
938        }
939        return   (getNrRealStudents(false) > 0 ? "RRq:" + getNrAssignedRealRequests(assignment, false) + "/" + getNrRealRequests(false) + ", " : "")
940                + (getNrLastLikeStudents(false) > 0 ? "DRq:" + getNrAssignedLastLikeRequests(assignment, false) + "/" + getNrLastLikeRequests(false) + ", " : "")
941                + (getNrRealStudents(false) > 0 ? "RS:" + getNrCompleteRealStudents(assignment, false) + "/" + getNrRealStudents(false) + ", " : "")
942                + (getNrLastLikeStudents(false) > 0 ? "DS:" + getNrCompleteLastLikeStudents(assignment, false) + "/" + getNrLastLikeStudents(false) + ", " : "")
943                + "V:"
944                + sDecimalFormat.format(-getTotalValue(assignment))
945                + (getDistanceConflict() == null ? "" : ", DC:" + getDistanceConflict().getTotalNrConflicts(assignment))
946                + (getTimeOverlaps() == null ? "" : ", TOC:" + getTimeOverlaps().getTotalNrConflicts(assignment))
947                + (iMPP ? ", IS:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedSameSectionWeight / iTotalMPPCRWeight) + "%" : "")
948                + (iMPP ? ", IT:" + sDecimalFormat.format(100.0 * getContext(assignment).iAssignedSameTimeWeight / iTotalMPPCRWeight) + "%" : "")
949                + ", %:" + sDecimalFormat.format(-100.0 * getTotalValue(assignment) / (getStudents().size() - iNrDummyStudents + 
950                        (iProjectedStudentWeight < 0.0 ? iNrDummyStudents * (iTotalDummyWeight / iNrDummyRequests) :iProjectedStudentWeight * iTotalDummyWeight)))
951                + (groupCount > 0 ? ", SG:" + sDecimalFormat.format(100.0 * groupSpread / groupCount) + "%" : "");
952
953    }
954    
955    /**
956     * Quadratic average of two weights.
957     * @param w1 first weight
958     * @param w2 second weight
959     * @return average of the two weights
960     */
961    public double avg(double w1, double w2) {
962        return Math.sqrt(w1 * w2);
963    }
964
965    /**
966     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
967     * @return maximal domain size, -1 if unlimited
968     */
969    public int getMaxDomainSize() { return iMaxDomainSize; }
970
971    /**
972     * Maximal domain size (i.e., number of enrollments of a course request), -1 if there is no limit.
973     * @param maxDomainSize maximal domain size, -1 if unlimited
974     */
975    public void setMaxDomainSize(int maxDomainSize) { iMaxDomainSize = maxDomainSize; }
976    
977
978    @Override
979    public StudentSectioningModelContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
980        return new StudentSectioningModelContext(assignment);
981    }
982    
983    public class StudentSectioningModelContext implements AssignmentConstraintContext<Request, Enrollment>, InfoProvider<Request, Enrollment>{
984        private Set<Student> iCompleteStudents = new HashSet<Student>();
985        private double iTotalValue = 0.0;
986        private int iNrAssignedDummyRequests = 0, iNrCompleteDummyStudents = 0;
987        private double iAssignedCRWeight = 0.0, iAssignedDummyCRWeight = 0.0;
988        private double iReservedSpace = 0.0, iTotalReservedSpace = 0.0;
989        private double iAssignedSameSectionWeight = 0.0, iAssignedSameChoiceWeight = 0.0, iAssignedSameTimeWeight = 0.0;
990        private double iAssignedSelectedSectionWeight = 0.0, iAssignedSelectedConfigWeight = 0.0;
991
992        public StudentSectioningModelContext(Assignment<Request, Enrollment> assignment) {
993            for (Request request: variables()) {
994                Enrollment enrollment = assignment.getValue(request);
995                if (enrollment != null)
996                    assigned(assignment, enrollment);
997            }
998        }
999
1000        /**
1001         * Called after an enrollment was assigned to a request. The list of
1002         * complete students and the overall solution value are updated.
1003         */
1004        @Override
1005        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
1006            Student student = enrollment.getStudent();
1007            if (student.isComplete(assignment))
1008                iCompleteStudents.add(student);
1009            double value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(assignment, enrollment);
1010            iTotalValue -= value;
1011            enrollment.variable().getContext(assignment).setLastWeight(value);
1012            if (enrollment.isCourseRequest())
1013                iAssignedCRWeight += enrollment.getRequest().getWeight();
1014            if (enrollment.getRequest().isMPP()) {
1015                iAssignedSameSectionWeight += enrollment.getRequest().getWeight() * enrollment.percentInitial();
1016                iAssignedSameChoiceWeight += enrollment.getRequest().getWeight() * enrollment.percentSelected();
1017                iAssignedSameTimeWeight += enrollment.getRequest().getWeight() * enrollment.percentSameTime();
1018            }
1019            if (enrollment.getRequest().hasSelection()) {
1020                iAssignedSelectedSectionWeight += enrollment.getRequest().getWeight() * enrollment.percentSelectedSameSection();
1021                iAssignedSelectedConfigWeight += enrollment.getRequest().getWeight() * enrollment.percentSelectedSameConfig();
1022            }
1023            if (enrollment.getReservation() != null)
1024                iReservedSpace += enrollment.getRequest().getWeight();
1025            if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
1026                iTotalReservedSpace += enrollment.getRequest().getWeight();
1027            if (student.isDummy()) {
1028                iNrAssignedDummyRequests++;
1029                if (enrollment.isCourseRequest())
1030                    iAssignedDummyCRWeight += enrollment.getRequest().getWeight();
1031                if (student.isComplete(assignment))
1032                    iNrCompleteDummyStudents++;
1033            }
1034        }
1035
1036        /**
1037         * Called before an enrollment was unassigned from a request. The list of
1038         * complete students and the overall solution value are updated.
1039         */
1040        @Override
1041        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
1042            Student student = enrollment.getStudent();
1043            if (iCompleteStudents.contains(student)) {
1044                iCompleteStudents.remove(student);
1045                if (student.isDummy())
1046                    iNrCompleteDummyStudents--;
1047            }
1048            Request.RequestContext cx = enrollment.variable().getContext(assignment);
1049            Double value = cx.getLastWeight();
1050            if (value == null)
1051                value = enrollment.getRequest().getWeight() * iStudentWeights.getWeight(assignment, enrollment);
1052            iTotalValue += value;
1053            cx.setLastWeight(null);
1054            if (enrollment.isCourseRequest())
1055                iAssignedCRWeight -= enrollment.getRequest().getWeight();
1056            if (enrollment.getRequest().isMPP()) {
1057                iAssignedSameSectionWeight -= enrollment.getRequest().getWeight() * enrollment.percentInitial();
1058                iAssignedSameChoiceWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelected();
1059                iAssignedSameTimeWeight -= enrollment.getRequest().getWeight() * enrollment.percentSameTime();
1060            }
1061            if (enrollment.getRequest().hasSelection()) {
1062                iAssignedSelectedSectionWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelectedSameSection();
1063                iAssignedSelectedConfigWeight -= enrollment.getRequest().getWeight() * enrollment.percentSelectedSameConfig();
1064            }
1065            if (enrollment.getReservation() != null)
1066                iReservedSpace -= enrollment.getRequest().getWeight();
1067            if (enrollment.isCourseRequest() && ((CourseRequest)enrollment.getRequest()).hasReservations())
1068                iTotalReservedSpace -= enrollment.getRequest().getWeight();
1069            if (student.isDummy()) {
1070                iNrAssignedDummyRequests--;
1071                if (enrollment.isCourseRequest())
1072                    iAssignedDummyCRWeight -= enrollment.getRequest().getWeight();
1073            }
1074        }
1075        
1076        public void add(Assignment<Request, Enrollment> assignment, DistanceConflict.Conflict c) {
1077            iTotalValue += avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
1078        }
1079
1080        public void remove(Assignment<Request, Enrollment> assignment, DistanceConflict.Conflict c) {
1081            iTotalValue -= avg(c.getR1().getWeight(), c.getR2().getWeight()) * iStudentWeights.getDistanceConflictWeight(assignment, c);
1082        }
1083        
1084        public void add(Assignment<Request, Enrollment> assignment, TimeOverlapsCounter.Conflict c) {
1085            if (c.getR1() != null) iTotalValue += c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
1086            if (c.getR2() != null) iTotalValue += c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
1087        }
1088
1089        public void remove(Assignment<Request, Enrollment> assignment, TimeOverlapsCounter.Conflict c) {
1090            if (c.getR1() != null) iTotalValue -= c.getR1Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE1(), c);
1091            if (c.getR2() != null) iTotalValue -= c.getR2Weight() * iStudentWeights.getTimeOverlapConflictWeight(assignment, c.getE2(), c);
1092        }
1093        
1094        /**
1095         * Students with complete schedules (see {@link Student#isComplete(Assignment)})
1096         * @return students with complete schedule
1097         */
1098        public Set<Student> getCompleteStudents() {
1099            return iCompleteStudents;
1100        }
1101        
1102        /**
1103         * Number of students with complete schedule
1104         * @return number of students with complete schedule
1105         */
1106        public int nrComplete() {
1107            return getCompleteStudents().size();
1108        }
1109        
1110        /** 
1111         * Recompute cached request weights
1112         * @param assignment curent assignment
1113         */
1114        public void requestWeightsChanged(Assignment<Request, Enrollment> assignment) {
1115            iTotalCRWeight = 0.0;
1116            iTotalDummyWeight = 0.0; iTotalDummyCRWeight = 0.0;
1117            iAssignedCRWeight = 0.0;
1118            iAssignedDummyCRWeight = 0.0;
1119            iNrDummyRequests = 0; iNrAssignedDummyRequests = 0;
1120            iTotalReservedSpace = 0.0; iReservedSpace = 0.0;
1121            iTotalMPPCRWeight = 0.0;
1122            iTotalSelCRWeight = 0.0;
1123            for (Request request: variables()) {
1124                boolean cr = (request instanceof CourseRequest);
1125                if (cr)
1126                    iTotalCRWeight += request.getWeight();
1127                if (request.getStudent().isDummy()) {
1128                    iTotalDummyWeight += request.getWeight();
1129                    iNrDummyRequests ++;
1130                    if (cr)
1131                        iTotalDummyCRWeight += request.getWeight();
1132                }
1133                if (request.isMPP())
1134                    iTotalMPPCRWeight += request.getWeight();
1135                if (request.hasSelection())
1136                    iTotalSelCRWeight += request.getWeight();
1137                Enrollment e = assignment.getValue(request);
1138                if (e != null) {
1139                    if (cr)
1140                        iAssignedCRWeight += request.getWeight();
1141                    if (request.isMPP()) {
1142                        iAssignedSameSectionWeight += request.getWeight() * e.percentInitial();
1143                        iAssignedSameChoiceWeight += request.getWeight() * e.percentSelected();
1144                        iAssignedSameTimeWeight += request.getWeight() * e.percentSameTime();
1145                    }
1146                    if (request.hasSelection()) {
1147                        iAssignedSelectedSectionWeight += request.getWeight() * e.percentSelectedSameSection();
1148                        iAssignedSelectedConfigWeight += request.getWeight() * e.percentSelectedSameConfig();
1149                    }
1150                    if (e.getReservation() != null)
1151                        iReservedSpace += request.getWeight();
1152                    if (cr && ((CourseRequest)request).hasReservations())
1153                        iTotalReservedSpace += request.getWeight();
1154                    if (request.getStudent().isDummy()) {
1155                        iNrAssignedDummyRequests ++;
1156                        if (cr)
1157                            iAssignedDummyCRWeight += request.getWeight();
1158                    }
1159                }
1160            }
1161        }
1162        
1163        /**
1164         * Overall solution value
1165         * @return solution value
1166         */
1167        public double getTotalValue() {
1168            return iTotalValue;
1169        }
1170        
1171        /**
1172         * Number of last like ({@link Student#isDummy()} equals true) students with
1173         * a complete schedule ({@link Student#isComplete(Assignment)} equals true).
1174         * @return number of last like (projected) students with a complete schedule
1175         */
1176        public int getNrCompleteLastLikeStudents() {
1177            return iNrCompleteDummyStudents;
1178        }
1179        
1180        /**
1181         * Number of requests from projected ({@link Student#isDummy()} equals true)
1182         * students that are assigned.
1183         * @return number of real students with a complete schedule
1184         */
1185        public int getNrAssignedLastLikeRequests() {
1186            return iNrAssignedDummyRequests;
1187        }
1188
1189        @Override
1190        public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info) {
1191            if (iTotalCRWeight > 0.0) {
1192                info.put("Assigned course requests", sDecimalFormat.format(100.0 * iAssignedCRWeight / iTotalCRWeight) + "% (" + (int)Math.round(iAssignedCRWeight) + "/" + (int)Math.round(iTotalCRWeight) + ")");
1193                if (iNrDummyStudents > 0 && iNrDummyStudents != getStudents().size() && iTotalCRWeight != iTotalDummyCRWeight) {
1194                    if (iTotalDummyCRWeight > 0.0)
1195                        info.put("Projected assigned course requests", sDecimalFormat.format(100.0 * iAssignedDummyCRWeight / iTotalDummyCRWeight) + "% (" + (int)Math.round(iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalDummyCRWeight) + ")");
1196                    info.put("Real assigned course requests", sDecimalFormat.format(100.0 * (iAssignedCRWeight - iAssignedDummyCRWeight) / (iTotalCRWeight - iTotalDummyCRWeight)) +
1197                            "% (" + (int)Math.round(iAssignedCRWeight - iAssignedDummyCRWeight) + "/" + (int)Math.round(iTotalCRWeight - iTotalDummyCRWeight) + ")");
1198                }
1199            }
1200            if (iTotalReservedSpace > 0.0)
1201                info.put("Reservations", sDoubleFormat.format(100.0 * iReservedSpace / iTotalReservedSpace) + "% (" + Math.round(iReservedSpace) + "/" + Math.round(iTotalReservedSpace) + ")");
1202            if (iMPP && iTotalMPPCRWeight > 0.0) {
1203                info.put("Perturbations: same section", sDoubleFormat.format(100.0 * iAssignedSameSectionWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameSectionWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
1204                if (iAssignedSameChoiceWeight > iAssignedSameSectionWeight)
1205                    info.put("Perturbations: same choice",sDoubleFormat.format(100.0 * iAssignedSameChoiceWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameChoiceWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
1206                if (iAssignedSameTimeWeight > iAssignedSameChoiceWeight)
1207                    info.put("Perturbations: same time", sDoubleFormat.format(100.0 * iAssignedSameTimeWeight / iTotalMPPCRWeight) + "% (" + Math.round(iAssignedSameTimeWeight) + "/" + Math.round(iTotalMPPCRWeight) + ")");
1208            }
1209            if (iTotalSelCRWeight > 0.0) {
1210                info.put("Selection",sDoubleFormat.format(100.0 * (0.3 * iAssignedSelectedConfigWeight + 0.7 * iAssignedSelectedSectionWeight) / iTotalSelCRWeight) +
1211                        "% (" + Math.round(0.3 * iAssignedSelectedSectionWeight + 0.7 * iAssignedSelectedSectionWeight) + "/" + Math.round(iTotalSelCRWeight) + ")");
1212            }
1213        }
1214
1215        @Override
1216        public void getInfo(Assignment<Request, Enrollment> assignment, Map<String, String> info, Collection<Request> variables) {
1217        }
1218        
1219        public double getAssignedCourseRequestWeight() {
1220            return iAssignedCRWeight;
1221        }
1222    }
1223    
1224    @Override
1225    public InheritedAssignment<Request, Enrollment> createInheritedAssignment(Solution<Request, Enrollment> solution, int index) {
1226        return new OptimisticInheritedAssignment<Request, Enrollment>(solution, index);
1227    }
1228    
1229    public DistanceMetric getDistanceMetric() {
1230        return (iDistanceConflict != null ? iDistanceConflict.getDistanceMetric() : null);
1231    }
1232
1233}