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