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