001    package net.sf.cpsolver.exam.model;
002    
003    import java.text.DecimalFormat;
004    import java.util.HashSet;
005    import java.util.Iterator;
006    import java.util.Set;
007    
008    import net.sf.cpsolver.ifs.model.Value;
009    
010    /**
011     * Representation of an exam placement (problem value), i.e., assignment of an
012     * exam to period and room(s). Each placement has defined a period and a set of
013     * rooms. The exam as well as the rooms have to be available during the given
014     * period (see {@link Exam#getPeriodPlacements()} and
015     * {@link Exam#getRoomPlacements()}). The total size of rooms have to be equal
016     * or greater than the number of students enrolled in the exam
017     * {@link Exam#getSize()}, using either {@link ExamRoom#getSize()} or
018     * {@link ExamRoom#getAltSize()}, depending on {@link Exam#hasAltSeating()}.
019     * Also, the number of rooms has to be smaller or equal to
020     * {@link Exam#getMaxRooms()}. If {@link Exam#getMaxRooms()} is zero, the exam
021     * is only to be assigned to period (the set of rooms is empty). <br>
022     * <br>
023     * The cost of an assignment consists of the following criteria:
024     * <ul>
025     * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()},
026     * weighted by {@link ExamModel#getDirectConflictWeight()}
027     * <li>More than two exams a day student conflicts
028     * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by
029     * {@link ExamModel#getMoreThanTwoADayWeight()}
030     * <li>Back-to-back student conflicts
031     * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by
032     * {@link ExamModel#getBackToBackConflictWeight()}
033     * <li>Distance back-to-back student conflicts
034     * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by
035     * {@link ExamModel#getDistanceBackToBackConflictWeight()}
036     * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by
037     * {@link ExamModel#getPeriodWeight()}
038     * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()}, weighted by
039     * {@link ExamModel#getRoomSizeWeight()}
040     * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()}, weighted
041     * by {@link ExamModel#getRoomSplitWeight()}
042     * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by
043     * {@link ExamModel#getRoomWeight()}
044     * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()},
045     * weighted by {@link ExamModel#getExamRotationWeight()}
046     * <li>Direct instructor conflicts
047     * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by
048     * {@link ExamModel#getInstructorDirectConflictWeight()}
049     * <li>More than two exams a day instructor conflicts
050     * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted by
051     * {@link ExamModel#getInstructorMoreThanTwoADayWeight()}
052     * <li>Back-to-back instructor conflicts
053     * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by
054     * {@link ExamModel#getInstructorBackToBackConflictWeight()}
055     * <li>Distance back-to-back instructor conflicts
056     * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()}, weighted
057     * by {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()}
058     * </ul>
059     * <br>
060     * <br>
061     * 
062     * @version ExamTT 1.2 (Examination Timetabling)<br>
063     *          Copyright (C) 2008 - 2010 Tomas Muller<br>
064     *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
065     *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
066     * <br>
067     *          This library is free software; you can redistribute it and/or modify
068     *          it under the terms of the GNU Lesser General Public License as
069     *          published by the Free Software Foundation; either version 3 of the
070     *          License, or (at your option) any later version. <br>
071     * <br>
072     *          This library is distributed in the hope that it will be useful, but
073     *          WITHOUT ANY WARRANTY; without even the implied warranty of
074     *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
075     *          Lesser General Public License for more details. <br>
076     * <br>
077     *          You should have received a copy of the GNU Lesser General Public
078     *          License along with this library; if not see
079     *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
080     */
081    public class ExamPlacement extends Value<Exam, ExamPlacement> {
082        private ExamPeriodPlacement iPeriodPlacement;
083        private Set<ExamRoomPlacement> iRoomPlacements;
084        private int iSize;
085        private int iRoomPenalty;
086        private double iRoomSplitDistance;
087    
088        private int iHashCode;
089    
090        /**
091         * Constructor
092         * 
093         * @param exam
094         *            an exam
095         * @param periodPlacement
096         *            period placement
097         * @param roomPlacements
098         *            a set of room placements {@link ExamRoomPlacement}
099         */
100        public ExamPlacement(Exam exam, ExamPeriodPlacement periodPlacement, Set<ExamRoomPlacement> roomPlacements) {
101            super(exam);
102            iPeriodPlacement = periodPlacement;
103            if (roomPlacements == null)
104                iRoomPlacements = new HashSet<ExamRoomPlacement>();
105            else
106                iRoomPlacements = roomPlacements;
107            iSize = 0;
108            iRoomPenalty = 0;
109            iRoomSplitDistance = 0.0;
110            for (ExamRoomPlacement r : iRoomPlacements) {
111                iSize += r.getSize(exam.hasAltSeating());
112                iRoomPenalty += r.getPenalty(periodPlacement.getPeriod());
113                if (iRoomPlacements.size() > 1) {
114                    for (ExamRoomPlacement w : iRoomPlacements) {
115                        if (r.getRoom().getId() < w.getRoom().getId())
116                            iRoomSplitDistance += r.getRoom().getDistanceInMeters(w.getRoom());
117                    }
118                }
119            }
120            if (iRoomPlacements.size() > 2) {
121                iRoomSplitDistance /= iRoomPlacements.size() * (iRoomPlacements.size() - 1) / 2;
122            }
123            iHashCode = getName().hashCode();
124        }
125    
126        /**
127         * Assigned period
128         */
129        public ExamPeriod getPeriod() {
130            return iPeriodPlacement.getPeriod();
131        }
132    
133        /**
134         * Assigned period placement
135         */
136        public ExamPeriodPlacement getPeriodPlacement() {
137            return iPeriodPlacement;
138        }
139    
140        /**
141         * Assigned rooms (it is empty when {@link Exam#getMaxRooms()} is zero)
142         * 
143         * @return list of {@link ExamRoomPlacement}
144         */
145        public Set<ExamRoomPlacement> getRoomPlacements() {
146            return iRoomPlacements;
147        }
148    
149        /**
150         * Overall size of assigned rooms
151         */
152        public int getSize() {
153            return iSize;
154        }
155    
156        /**
157         * Number of direct student conflicts, i.e., number of cases when this exam
158         * is attended by a student that attends some other exam at the same period
159         */
160        public int getNrDirectConflicts() {
161            Exam exam = variable();
162            // if (!exam.isAllowDirectConflicts()) return 0;
163            int penalty = 0;
164            for (ExamStudent s : exam.getStudents()) {
165                Set<Exam> exams = s.getExams(getPeriod());
166                int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1);
167                if (nrExams > 1)
168                    penalty++;
169                else if (!s.isAvailable(getPeriod()))
170                    penalty++;
171            }
172            return penalty;
173        }
174    
175        /**
176         * Number of direct student conflicts caused by the fact that a student is
177         * not available
178         */
179        public int getNrNotAvailableConflicts() {
180            Exam exam = variable();
181            int penalty = 0;
182            for (ExamStudent s : exam.getStudents()) {
183                if (!s.isAvailable(getPeriod()))
184                    penalty++;
185            }
186            return penalty;
187        }
188    
189        /**
190         * Number of back-to-back student conflicts, i.e., number of cases when this
191         * exam is attended by a student that attends some other exam at the
192         * previous {@link ExamPeriod#prev()} or following {@link ExamPeriod#next()}
193         * period. If {@link ExamModel#isDayBreakBackToBack()} is false,
194         * back-to-back conflicts are only considered between consecutive periods
195         * that are of the same day.
196         */
197        public int getNrBackToBackConflicts() {
198            Exam exam = variable();
199            ExamModel model = (ExamModel) exam.getModel();
200            int penalty = 0;
201            for (ExamStudent s : exam.getStudents()) {
202                if (getPeriod().prev() != null) {
203                    if (model.isDayBreakBackToBack() || getPeriod().prev().getDay() == getPeriod().getDay()) {
204                        Set<Exam> exams = s.getExams(getPeriod().prev());
205                        int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0);
206                        penalty += nrExams;
207                    }
208                }
209                if (getPeriod().next() != null) {
210                    if (model.isDayBreakBackToBack() || getPeriod().next().getDay() == getPeriod().getDay()) {
211                        Set<Exam> exams = s.getExams(getPeriod().next());
212                        int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0);
213                        penalty += nrExams;
214                    }
215                }
216            }
217            return penalty;
218        }
219    
220        /**
221         * Distance between two placements, i.e., maximal distance between a room of
222         * this placement and a room of the given placement. Method
223         * {@link ExamRoom#getDistanceInMeters(ExamRoom)} is used to get a distance between
224         * two rooms.
225         */
226        public double getDistanceInMeters(ExamPlacement other) {
227            if (getRoomPlacements().isEmpty() || other.getRoomPlacements().isEmpty())
228                return 0;
229            double maxDistance = 0;
230            for (ExamRoomPlacement r1 : getRoomPlacements()) {
231                for (ExamRoomPlacement r2 : other.getRoomPlacements()) {
232                    maxDistance = Math.max(maxDistance, r1.getDistanceInMeters(r2));
233                }
234            }
235            return maxDistance;
236        }
237    
238        /**
239         * Number of back-to-back distance student conflicts, i.e., number of cases
240         * when this exam is attended by a student that attends some other exam at
241         * the previous {@link ExamPeriod#prev()} or following
242         * {@link ExamPeriod#next()} period and the distance
243         * {@link ExamPlacement#getDistanceInMeters(ExamPlacement)} between these two exams
244         * is greater than {@link ExamModel#getBackToBackDistance()}. Distance
245         * back-to-back conflicts are only considered between consecutive periods
246         * that are of the same day.
247         */
248        public int getNrDistanceBackToBackConflicts() {
249            Exam exam = variable();
250            ExamModel model = (ExamModel) exam.getModel();
251            double btbDist = model.getBackToBackDistance();
252            if (btbDist < 0)
253                return 0;
254            int penalty = 0;
255            for (ExamStudent s : exam.getStudents()) {
256                if (getPeriod().prev() != null) {
257                    if (getPeriod().prev().getDay() == getPeriod().getDay()) {
258                        for (Exam x : s.getExams(getPeriod().prev())) {
259                            if (x.equals(exam))
260                                continue;
261                            if (getDistanceInMeters(x.getAssignment()) > btbDist)
262                                penalty++;
263                        }
264                    }
265                }
266                if (getPeriod().next() != null) {
267                    if (getPeriod().next().getDay() == getPeriod().getDay()) {
268                        for (Exam x : s.getExams(getPeriod().next())) {
269                            if (x.equals(exam))
270                                continue;
271                            if (getDistanceInMeters(x.getAssignment()) > btbDist)
272                                penalty++;
273                        }
274                    }
275                }
276            }
277            return penalty;
278        }
279    
280        /**
281         * Number of more than two exams a day student conflicts, i.e., when this
282         * exam is attended by a student that attends two or more other exams at the
283         * same day.
284         */
285        public int getNrMoreThanTwoADayConflicts() {
286            Exam exam = variable();
287            int penalty = 0;
288            for (ExamStudent s : exam.getStudents()) {
289                Set<Exam> exams = s.getExamsADay(getPeriod());
290                int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1);
291                if (nrExams > 2)
292                    penalty++;
293            }
294            return penalty;
295        }
296    
297        /**
298         * Number of direct instructor conflicts, i.e., number of cases when this
299         * exam is attended by an instructor that attends some other exam at the
300         * same period
301         */
302        public int getNrInstructorDirectConflicts() {
303            Exam exam = variable();
304            // if (!exam.isAllowDirectConflicts()) return 0;
305            int penalty = 0;
306            for (ExamInstructor s : exam.getInstructors()) {
307                Set<Exam> exams = s.getExams(getPeriod());
308                int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1);
309                if (nrExams > 1)
310                    penalty++;
311                else if (!s.isAvailable(getPeriod()))
312                    penalty++;
313            }
314            return penalty;
315        }
316    
317        /**
318         * Number of direct instructor conflicts caused by the fact that a student
319         * is not available
320         */
321        public int getNrInstructorNotAvailableConflicts() {
322            Exam exam = variable();
323            int penalty = 0;
324            for (ExamInstructor s : exam.getInstructors()) {
325                if (!s.isAvailable(getPeriod()))
326                    penalty++;
327            }
328            return penalty;
329        }
330    
331        /**
332         * Number of back-to-back instructor conflicts, i.e., number of cases when
333         * this exam is attended by an instructor that attends some other exam at
334         * the previous {@link ExamPeriod#prev()} or following
335         * {@link ExamPeriod#next()} period. If
336         * {@link ExamModel#isDayBreakBackToBack()} is false, back-to-back conflicts
337         * are only considered between consecutive periods that are of the same day.
338         */
339        public int getNrInstructorBackToBackConflicts() {
340            Exam exam = variable();
341            ExamModel model = (ExamModel) exam.getModel();
342            int penalty = 0;
343            for (ExamInstructor s : exam.getInstructors()) {
344                if (getPeriod().prev() != null) {
345                    if (model.isDayBreakBackToBack() || getPeriod().prev().getDay() == getPeriod().getDay()) {
346                        Set<Exam> exams = s.getExams(getPeriod().prev());
347                        int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0);
348                        penalty += nrExams;
349                    }
350                }
351                if (getPeriod().next() != null) {
352                    if (model.isDayBreakBackToBack() || getPeriod().next().getDay() == getPeriod().getDay()) {
353                        Set<Exam> exams = s.getExams(getPeriod().next());
354                        int nrExams = exams.size() + (exams.contains(exam) ? -1 : 0);
355                        penalty += nrExams;
356                    }
357                }
358            }
359            return penalty;
360        }
361    
362        /**
363         * Number of back-to-back distance instructor conflicts, i.e., number of
364         * cases when this exam is attended by an instructor that attends some other
365         * exam at the previous {@link ExamPeriod#prev()} or following
366         * {@link ExamPeriod#next()} period and the distance
367         * {@link ExamPlacement#getDistanceInMeters(ExamPlacement)} between these two exams
368         * is greater than {@link ExamModel#getBackToBackDistance()}. Distance
369         * back-to-back conflicts are only considered between consecutive periods
370         * that are of the same day.
371         */
372        public int getNrInstructorDistanceBackToBackConflicts() {
373            Exam exam = variable();
374            ExamModel model = (ExamModel) exam.getModel();
375            double btbDist = model.getBackToBackDistance();
376            if (btbDist < 0)
377                return 0;
378            int penalty = 0;
379            for (ExamInstructor s : exam.getInstructors()) {
380                if (getPeriod().prev() != null) {
381                    if (getPeriod().prev().getDay() == getPeriod().getDay()) {
382                        for (Exam x : s.getExams(getPeriod().prev())) {
383                            if (x.equals(exam))
384                                continue;
385                            if (getDistanceInMeters(x.getAssignment()) > btbDist)
386                                penalty++;
387                        }
388                    }
389                }
390                if (getPeriod().next() != null) {
391                    if (getPeriod().next().getDay() == getPeriod().getDay()) {
392                        for (Exam x : s.getExams(getPeriod().next())) {
393                            if (x.equals(exam))
394                                continue;
395                            if (getDistanceInMeters(x.getAssignment()) > btbDist)
396                                penalty++;
397                        }
398                    }
399                }
400            }
401            return penalty;
402        }
403    
404        /**
405         * Number of more than two exams a day instructor conflicts, i.e., when this
406         * exam is attended by an instructor student that attends two or more other
407         * exams at the same day.
408         */
409        public int getNrInstructorMoreThanTwoADayConflicts() {
410            Exam exam = variable();
411            int penalty = 0;
412            for (ExamInstructor s : exam.getInstructors()) {
413                Set<Exam> exams = s.getExamsADay(getPeriod());
414                int nrExams = exams.size() + (exams.contains(exam) ? 0 : 1);
415                if (nrExams > 2)
416                    penalty++;
417            }
418            return penalty;
419        }
420    
421        /**
422         * Cost for using room(s) that are too big
423         * 
424         * @return difference between total room size (computed using either
425         *         {@link ExamRoom#getSize()} or {@link ExamRoom#getAltSize()} based
426         *         on {@link Exam#hasAltSeating()}) and the number of students
427         *         {@link Exam#getSize()}
428         */
429        public int getRoomSizePenalty() {
430            Exam exam = variable();
431            int diff = getSize() - exam.getSize();
432            return (diff < 0 ? 0 : diff);
433        }
434    
435        /**
436         * Cost for using more than one room (nrSplits^2).
437         * 
438         * @return penalty (1 for 2 rooms, 4 for 3 rooms, 9 for 4 rooms, etc.)
439         */
440        public int getRoomSplitPenalty() {
441            return (iRoomPlacements.size() <= 1 ? 0 : (iRoomPlacements.size() - 1) * (iRoomPlacements.size() - 1));
442        }
443    
444        /**
445         * Cost for using a period, i.e., {@link ExamPeriodPlacement#getPenalty()}
446         */
447        public int getPeriodPenalty() {
448            return iPeriodPlacement.getPenalty();
449        }
450    
451        /**
452         * Rotation penalty (an exam that has been in later period last times tries
453         * to be in an earlier period)
454         */
455        public int getRotationPenalty() {
456            Exam exam = variable();
457            if (exam.getAveragePeriod() < 0)
458                return 0;
459            return (1 + getPeriod().getIndex()) * (1 + exam.getAveragePeriod());
460        }
461    
462        /**
463         * Front load penalty (large exam is discouraged to be placed on or after a
464         * certain period)
465         * 
466         * @return zero if not large exam or if before
467         *         {@link ExamModel#getLargePeriod()}, one otherwise
468         */
469        public int getLargePenalty() {
470            Exam exam = variable();
471            ExamModel model = (ExamModel) exam.getModel();
472            if (model.getLargeSize() < 0 || exam.getSize() < model.getLargeSize())
473                return 0;
474            int periodIdx = (int) Math.round(model.getPeriods().size() * model.getLargePeriod());
475            return (getPeriod().getIndex() < periodIdx ? 0 : 1);
476        }
477    
478        /**
479         * Room penalty (penalty for using given rooms), i.e., sum of
480         * {@link ExamRoomPlacement#getPenalty(ExamPeriod)} of assigned rooms
481         */
482        public int getRoomPenalty() {
483            return iRoomPenalty;
484        }
485    
486        /**
487         * Perturbation penalty, i.e., penalty for using a different assignment than
488         * initial. Only applicable when {@link ExamModel#isMPP()} is true (minimal
489         * perturbation problem).
490         * 
491         * @return |period index - initial period index | * exam size
492         */
493        public int getPerturbationPenalty() {
494            Exam exam = variable();
495            if (!((ExamModel) exam.getModel()).isMPP())
496                return 0;
497            ExamPlacement initial = exam.getInitialAssignment();
498            if (initial == null)
499                return 0;
500            return Math.abs(initial.getPeriod().getIndex() - getPeriod().getIndex()) * (1 + exam.getSize());
501        }
502    
503        /**
504         * Room perturbation penalty, i.e., number of assigned rooms different from
505         * initial. Only applicable when {@link ExamModel#isMPP()} is true (minimal
506         * perturbation problem).
507         * 
508         * @return |period index - initial period index | * exam size
509         */
510        public int getRoomPerturbationPenalty() {
511            Exam exam = variable();
512            if (!((ExamModel) exam.getModel()).isMPP())
513                return 0;
514            ExamPlacement initial = exam.getInitialAssignment();
515            if (initial == null)
516                return 0;
517            int penalty = 0;
518            for (ExamRoomPlacement rp : getRoomPlacements()) {
519                if (!initial.getRoomPlacements().contains(rp))
520                    penalty++;
521            }
522            return penalty;
523        }
524    
525        /**
526         * Room split distance penalty, i.e., average distance between two rooms of
527         * this placement
528         */
529        public double getRoomSplitDistancePenalty() {
530            return iRoomSplitDistance;
531        }
532    
533        /**
534         * Distribution penalty, i.e., sum weights of violated distribution
535         * constraints
536         */
537        public double getDistributionPenalty() {
538            int penalty = 0;
539            for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) {
540                if (dc.isHard())
541                    continue;
542                boolean sat = dc.isSatisfied(this);
543                if (sat != dc.isSatisfied())
544                    penalty += (sat ? -dc.getWeight() : dc.getWeight());
545            }
546            return penalty;
547        }
548    
549        /**
550         * Room related distribution penalty, i.e., sum weights of violated
551         * distribution constraints
552         */
553        public double getRoomDistributionPenalty() {
554            int penalty = 0;
555            for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) {
556                if (dc.isHard() || !dc.isRoomRelated())
557                    continue;
558                boolean sat = dc.isSatisfied(this);
559                if (sat != dc.isSatisfied())
560                    penalty += (sat ? -dc.getWeight() : dc.getWeight());
561            }
562            return penalty;
563        }
564    
565        /**
566         * Period related distribution penalty, i.e., sum weights of violated
567         * distribution constraints
568         */
569        public double getPeriodDistributionPenalty() {
570            int penalty = 0;
571            for (ExamDistributionConstraint dc : variable().getDistributionConstraints()) {
572                if (dc.isHard() || !dc.isPeriodRelated())
573                    continue;
574                boolean sat = dc.isSatisfied(this);
575                if (sat != dc.isSatisfied())
576                    penalty += (sat ? -dc.getWeight() : dc.getWeight());
577            }
578            return penalty;
579        }
580    
581        /**
582         * Overall cost of using this placement. The cost of an assignment consists
583         * of the following criteria:
584         * <ul>
585         * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()}
586         * , weighted by {@link ExamModel#getDirectConflictWeight()}
587         * <li>More than two exams a day student conflicts
588         * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by
589         * {@link ExamModel#getMoreThanTwoADayWeight()}
590         * <li>Back-to-back student conflicts
591         * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by
592         * {@link ExamModel#getBackToBackConflictWeight()}
593         * <li>Distance back-to-back student conflicts
594         * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by
595         * {@link ExamModel#getDistanceBackToBackConflictWeight()}
596         * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by
597         * {@link ExamModel#getPeriodWeight()}
598         * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()},
599         * weighted by {@link ExamModel#getRoomSizeWeight()}
600         * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()},
601         * weighted by {@link ExamModel#getRoomSplitWeight()}
602         * <li>Room split distance penalty
603         * {@link ExamPlacement#getRoomSplitDistancePenalty()}, weighted by
604         * {@link ExamModel#getRoomSplitDistanceWeight()}
605         * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by
606         * {@link ExamModel#getRoomWeight()}
607         * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()},
608         * weighted by {@link ExamModel#getExamRotationWeight()}
609         * <li>Direct instructor conflicts
610         * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by
611         * {@link ExamModel#getInstructorDirectConflictWeight()}
612         * <li>More than two exams a day instructor conflicts
613         * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted
614         * by {@link ExamModel#getInstructorMoreThanTwoADayWeight()}
615         * <li>Back-to-back instructor conflicts
616         * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by
617         * {@link ExamModel#getInstructorBackToBackConflictWeight()}
618         * <li>Distance back-to-back instructor conflicts
619         * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()},
620         * weighted by
621         * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()}
622         * <li>Front load penalty {@link ExamPlacement#getLargePenalty()}, weighted
623         * by {@link ExamModel#getLargeWeight()}
624         * </ul>
625         */
626        @Override
627        public double toDouble() {
628            Exam exam = variable();
629            ExamModel model = (ExamModel) exam.getModel();
630            return model.getDirectConflictWeight() * getNrDirectConflicts() + model.getMoreThanTwoADayWeight()
631                    * getNrMoreThanTwoADayConflicts() + model.getBackToBackConflictWeight() * getNrBackToBackConflicts()
632                    + model.getDistanceBackToBackConflictWeight() * getNrDistanceBackToBackConflicts()
633                    + model.getPeriodWeight() * getPeriodPenalty() + model.getPeriodSizeWeight() * getPeriodPenalty()
634                    * (exam.getSize() + 1) + model.getPeriodIndexWeight() * getPeriod().getIndex()
635                    + model.getRoomSizeWeight() * getRoomSizePenalty() + model.getRoomSplitWeight() * getRoomSplitPenalty()
636                    + model.getExamRotationWeight() * getRotationPenalty() + model.getRoomWeight() * getRoomPenalty()
637                    + model.getInstructorDirectConflictWeight() * getNrInstructorDirectConflicts()
638                    + model.getInstructorMoreThanTwoADayWeight() * getNrInstructorMoreThanTwoADayConflicts()
639                    + model.getInstructorBackToBackConflictWeight() * getNrInstructorBackToBackConflicts()
640                    + model.getInstructorDistanceBackToBackConflictWeight() * getNrInstructorDistanceBackToBackConflicts()
641                    + model.getRoomSplitDistanceWeight() * getRoomSplitDistancePenalty() + model.getPerturbationWeight()
642                    * getPerturbationPenalty() + model.getRoomPerturbationWeight() * getRoomPerturbationPenalty()
643                    + model.getDistributionWeight() * getDistributionPenalty() + model.getLargeWeight() * getLargePenalty();
644        }
645    
646        /**
647         * Overall cost of using this period. The time cost of an assignment
648         * consists of the following criteria:
649         * <ul>
650         * <li>Direct student conflicts {@link ExamPlacement#getNrDirectConflicts()}
651         * , weighted by {@link ExamModel#getDirectConflictWeight()}
652         * <li>More than two exams a day student conflicts
653         * {@link ExamPlacement#getNrMoreThanTwoADayConflicts()}, weighted by
654         * {@link ExamModel#getMoreThanTwoADayWeight()}
655         * <li>Back-to-back student conflicts
656         * {@link ExamPlacement#getNrBackToBackConflicts()}, weighted by
657         * {@link ExamModel#getBackToBackConflictWeight()}
658         * <li>Period penalty {@link ExamPlacement#getPeriodPenalty()}, weighted by
659         * {@link ExamModel#getPeriodWeight()}
660         * <li>Exam rotation penalty {@link ExamPlacement#getRotationPenalty()},
661         * weighted by {@link ExamModel#getExamRotationWeight()}
662         * <li>Direct instructor conflicts
663         * {@link ExamPlacement#getNrInstructorDirectConflicts()}, weighted by
664         * {@link ExamModel#getInstructorDirectConflictWeight()}
665         * <li>More than two exams a day instructor conflicts
666         * {@link ExamPlacement#getNrInstructorMoreThanTwoADayConflicts()}, weighted
667         * by {@link ExamModel#getInstructorMoreThanTwoADayWeight()}
668         * <li>Back-to-back instructor conflicts
669         * {@link ExamPlacement#getNrInstructorBackToBackConflicts()}, weighted by
670         * {@link ExamModel#getInstructorBackToBackConflictWeight()}
671         * <li>Distance back-to-back instructor conflicts
672         * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()},
673         * weighted by
674         * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()}
675         * <li>Front load penalty {@link ExamPlacement#getLargePenalty()}, weighted
676         * by {@link ExamModel#getLargeWeight()}
677         * </ul>
678         */
679        public double getTimeCost() {
680            Exam exam = variable();
681            ExamModel model = (ExamModel) exam.getModel();
682            return model.getDirectConflictWeight() * getNrDirectConflicts() + model.getBackToBackConflictWeight()
683                    * getNrBackToBackConflicts() + model.getMoreThanTwoADayWeight() * getNrMoreThanTwoADayConflicts()
684                    + model.getPeriodWeight() * getPeriodPenalty() + model.getPeriodSizeWeight() * getPeriodPenalty()
685                    * (exam.getSize() + 1) + model.getPeriodIndexWeight() * getPeriod().getIndex()
686                    + model.getExamRotationWeight() * getRotationPenalty() + model.getInstructorDirectConflictWeight()
687                    * getNrInstructorDirectConflicts() + model.getInstructorMoreThanTwoADayWeight()
688                    * getNrInstructorMoreThanTwoADayConflicts() + model.getInstructorBackToBackConflictWeight()
689                    * getNrInstructorBackToBackConflicts() + model.getPerturbationWeight() * getPerturbationPenalty()
690                    + model.getDistributionWeight() * getPeriodDistributionPenalty() + model.getLargeWeight()
691                    * getLargePenalty();
692        }
693    
694        /**
695         * Overall cost of using this set or rooms. The room cost of an assignment
696         * consists of the following criteria:
697         * <ul>
698         * <li>Distance back-to-back student conflicts
699         * {@link ExamPlacement#getNrDistanceBackToBackConflicts()}, weighted by
700         * {@link ExamModel#getDistanceBackToBackConflictWeight()}
701         * <li>Distance back-to-back instructor conflicts
702         * {@link ExamPlacement#getNrInstructorDistanceBackToBackConflicts()},
703         * weighted by
704         * {@link ExamModel#getInstructorDistanceBackToBackConflictWeight()}
705         * <li>Room size penalty {@link ExamPlacement#getRoomSizePenalty()},
706         * weighted by {@link ExamModel#getRoomSizeWeight()}
707         * <li>Room split penalty {@link ExamPlacement#getRoomSplitPenalty()},
708         * weighted by {@link ExamModel#getRoomSplitWeight()}
709         * <li>Room split distance penalty
710         * {@link ExamPlacement#getRoomSplitDistancePenalty()}, weighted by
711         * {@link ExamModel#getRoomSplitDistanceWeight()}
712         * <li>Room penalty {@link ExamPlacement#getRoomPenalty()}, weighted by
713         * {@link ExamModel#getRoomWeight()}
714         * </ul>
715         */
716        public double getRoomCost() {
717            Exam exam = variable();
718            ExamModel model = (ExamModel) exam.getModel();
719            return model.getDistanceBackToBackConflictWeight() * getNrDistanceBackToBackConflicts()
720                    + model.getRoomSizeWeight() * getRoomSizePenalty() + model.getRoomSplitWeight() * getRoomSplitPenalty()
721                    + model.getRoomWeight() * getRoomPenalty() + model.getInstructorDistanceBackToBackConflictWeight()
722                    * getNrInstructorDistanceBackToBackConflicts() + model.getRoomSplitDistanceWeight()
723                    * getRoomSizePenalty() + model.getDistributionWeight() * getRoomDistributionPenalty()
724                    + model.getRoomPerturbationWeight() * getRoomPerturbationPenalty();
725        }
726    
727        /**
728         * Room names separated with the given delimiter
729         */
730        public String getRoomName(String delim) {
731            String roomName = "";
732            for (Iterator<ExamRoomPlacement> i = getRoomPlacements().iterator(); i.hasNext();) {
733                ExamRoomPlacement r = i.next();
734                roomName += r.getRoom().getName();
735                if (i.hasNext())
736                    roomName += delim;
737            }
738            return roomName;
739        }
740    
741        /**
742         * Assignment name (period / room(s))
743         */
744        @Override
745        public String getName() {
746            return getPeriod() + "/" + getRoomName(",");
747        }
748    
749        /**
750         * String representation -- returns a list of assignment costs
751         */
752        @Override
753        public String toString() {
754            DecimalFormat df = new DecimalFormat("0.00");
755            Exam exam = variable();
756            ExamModel model = (ExamModel) exam.getModel();
757            return variable().getName() + " = " + getName() + " (" + df.format(toDouble()) + "/" + "DC:"
758                    + getNrDirectConflicts() + "," + "M2D:" + getNrMoreThanTwoADayConflicts() + "," + "BTB:"
759                    + getNrBackToBackConflicts() + ","
760                    + (model.getBackToBackDistance() < 0 ? "" : "dBTB:" + getNrDistanceBackToBackConflicts() + ",") + "PP:"
761                    + getPeriodPenalty() + "," + "@P:" + getRotationPenalty() + "," + "RSz:" + getRoomSizePenalty() + ","
762                    + "RSp:" + getRoomSplitPenalty() + "," + "RD:" + df.format(getRoomSplitDistancePenalty()) + "," + "RP:"
763                    + getRoomPenalty()
764                    + (model.isMPP() ? ",IP:" + getPerturbationPenalty() + ",IRP:" + getRoomPerturbationPenalty() : "")
765                    + ")";
766        }
767    
768        /**
769         * Compare two assignments for equality
770         */
771        @Override
772        public boolean equals(Object o) {
773            if (o == null || !(o instanceof ExamPlacement))
774                return false;
775            ExamPlacement p = (ExamPlacement) o;
776            return p.variable().equals(variable()) && p.getPeriod().equals(getPeriod())
777                    && p.getRoomPlacements().equals(getRoomPlacements());
778        }
779    
780        /**
781         * Hash code
782         */
783        @Override
784        public int hashCode() {
785            return iHashCode;
786        }
787    
788        /**
789         * True if given room is between {@link ExamPlacement#getRoomPlacements()}
790         */
791        public boolean contains(ExamRoom room) {
792            return getRoomPlacements().contains(new ExamRoomPlacement(room));
793        }
794    }