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