001package org.cpsolver.exam.model;
002
003import java.util.ArrayList;
004import java.util.Collection;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.HashSet;
008import java.util.HashMap;
009import java.util.Iterator;
010import java.util.List;
011import java.util.Locale;
012import java.util.Map;
013import java.util.Set;
014import java.util.TreeSet;
015
016
017import org.apache.log4j.Logger;
018import org.cpsolver.exam.criteria.DistributionPenalty;
019import org.cpsolver.exam.criteria.ExamRotationPenalty;
020import org.cpsolver.exam.criteria.RoomPenalty;
021import org.cpsolver.exam.criteria.RoomSizePenalty;
022import org.cpsolver.exam.criteria.RoomSplitPenalty;
023import org.cpsolver.ifs.assignment.Assignment;
024import org.cpsolver.ifs.model.Constraint;
025import org.cpsolver.ifs.model.Model;
026import org.cpsolver.ifs.model.Variable;
027import org.cpsolver.ifs.util.Progress;
028import org.cpsolver.ifs.util.ToolBox;
029
030/**
031 * Representation of an exam (problem variable). Each exam has defined a length
032 * (in minutes), type (whether it is a section or a course exam), seating type
033 * (whether it requires normal or alternate seating) and a maximal number of
034 * rooms. If the maximal number of rooms is zero, the exam will be timetabled
035 * only in time (it does not require a room). <br>
036 * <br>
037 * An exam can be only assigned to a period {@link ExamPeriod} that is long
038 * enough (see {@link ExamPeriod#getLength()}) and that is available for the
039 * exam (see {@link Exam#getPeriodPlacements()}). <br>
040 * <br>
041 * A set of rooms that are available in the given period (see
042 * {@link ExamRoom#isAvailable(ExamPeriod)},
043 * {@link ExamRoomPlacement#isAvailable(ExamPeriod)}), and which together cover
044 * the size of exam (number of students attending the exam) has to be assigned
045 * to an exam. Based on the type of seating (see {@link Exam#hasAltSeating()}),
046 * either room sizes (see {@link ExamRoom#getSize()}) or alternative seating
047 * sizes (see {@link ExamRoom#getAltSize()}) are used. An exam has a list of
048 * available rooms with their penalties assiciated with (see
049 * {@link Exam#getRoomPlacements()}). <br>
050 * <br>
051 * Various penalties for an assignment of a period or a set of rooms may apply.
052 * See {@link ExamPlacement} for more details. <br>
053 * <br>
054 * 
055 * @version ExamTT 1.3 (Examination Timetabling)<br>
056 *          Copyright (C) 2008 - 2014 Tomáš Müller<br>
057 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
058 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
059 * <br>
060 *          This library is free software; you can redistribute it and/or modify
061 *          it under the terms of the GNU Lesser General Public License as
062 *          published by the Free Software Foundation; either version 3 of the
063 *          License, or (at your option) any later version. <br>
064 * <br>
065 *          This library is distributed in the hope that it will be useful, but
066 *          WITHOUT ANY WARRANTY; without even the implied warranty of
067 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
068 *          Lesser General Public License for more details. <br>
069 * <br>
070 *          You should have received a copy of the GNU Lesser General Public
071 *          License along with this library; if not see
072 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
073 */
074public class Exam extends Variable<Exam, ExamPlacement> {
075    private static boolean sAlterMaxSize = false;
076    private static Logger sLog = Logger.getLogger(Exam.class);
077    protected static java.text.DecimalFormat sDoubleFormat = new java.text.DecimalFormat("0.00",
078            new java.text.DecimalFormatSymbols(Locale.US));
079
080    private ArrayList<ExamStudent> iStudents = new ArrayList<ExamStudent>();
081    private ArrayList<ExamInstructor> iInstructors = new ArrayList<ExamInstructor>();
082    private ArrayList<ExamDistributionConstraint> iDistConstraints = new ArrayList<ExamDistributionConstraint>();
083
084    private boolean iAllowDirectConflicts = true;
085
086    private String iName = null;
087    private int iLength = 0;
088    private int iMaxRooms = 0;
089    private int iMinSize = 0;
090    private boolean iAltSeating = false;
091    private int iAveragePeriod = -1;
092    private Integer iSize = null;
093    private Integer iPrintOffset = null;
094
095    private ArrayList<ExamOwner> iOwners = new ArrayList<ExamOwner>();
096    private List<ExamPeriodPlacement> iPeriodPlacements = null;
097    private List<ExamPeriodPlacement> iAvailablePeriodPlacements = null;
098    private List<ExamRoomPlacement> iRoomPlacements = null;
099    private List<ExamRoomPlacement> iRoomPreferredPlacements = null;
100
101    /**
102     * Constructor
103     * 
104     * @param id
105     *            exam unique id
106     * @param name exam name
107     * @param length
108     *            exam length in minutes
109     * @param altSeating
110     *            true if alternative seating is requested
111     * @param maxRooms
112     *            maximum number of rooms to be used
113     * @param minSize
114     *            minimal size of rooms into which an exam can be assigned (see
115     *            {@link Exam#getSize()})
116     * @param periodPlacements
117     *            list of periods and their penalties
118     *            {@link ExamPeriodPlacement} into which an exam can be assigned
119     * @param roomPlacements
120     *            list of rooms and their penalties {@link ExamRoomPlacement}
121     *            into which an exam can be assigned
122     */
123    public Exam(long id, String name, int length, boolean altSeating, int maxRooms, int minSize,
124            java.util.List<ExamPeriodPlacement> periodPlacements, java.util.List<ExamRoomPlacement> roomPlacements) {
125        super();
126        iId = id;
127        iName = name;
128        iLength = length;
129        iAltSeating = altSeating;
130        iMaxRooms = maxRooms;
131        iMinSize = minSize;
132        iPeriodPlacements = new ArrayList<ExamPeriodPlacement>(periodPlacements);
133        Collections.sort(iPeriodPlacements, new Comparator<ExamPeriodPlacement>() {
134            @Override
135            public int compare(ExamPeriodPlacement p1, ExamPeriodPlacement p2) {
136                return p1.getPeriod().compareTo(p2.getPeriod());
137            }
138        });
139        iRoomPlacements = new ArrayList<ExamRoomPlacement>(roomPlacements);
140        Collections.sort(iRoomPlacements, new Comparator<ExamRoomPlacement>() {
141            @Override
142            public int compare(ExamRoomPlacement p1, ExamRoomPlacement p2) {
143                int cmp = -Double.compare(p1.getSize(hasAltSeating()), p2.getSize(hasAltSeating()));
144                if (cmp != 0)
145                    return cmp;
146                return p1.getRoom().compareTo(p2.getRoom());
147            }
148        });
149    }
150
151    /**
152     * Exam size, it is bigger from {@link Exam#getMinSize()} and the number of
153     * students enrolled into the exam {@link Exam#getStudents()}. If
154     * {@link Exam#getMaxRooms()} is greater than zero, an exam must be assigned
155     * into rooms which overall size (or alternative seating size if
156     * {@link Exam#hasAltSeating()}) must be equal or greater than this size.
157     * @return examination size
158     */
159    public int getSize() {
160        return (iSize == null ? Math.max(iMinSize, getStudents().size()) : iSize.intValue());
161    }
162
163    /**
164     * Override exam size with given value (revert to default when null)
165     * @param size examination size override
166     */
167    public void setSizeOverride(Integer size) {
168        iSize = size;
169    }
170
171    /**
172     * Override exam size with given value (revert to default when null)
173     * @return examination size override
174     */
175    public Integer getSizeOverride() {
176        return iSize;
177    }
178
179    /**
180     * Print offset -- for reporting purposes
181     * @return print offset in minutes
182     */
183    public Integer getPrintOffset() {
184        return iPrintOffset;
185    }
186
187    /**
188     * Print offset -- for reporting purposes
189     * @param printOffset print offset in minutes
190     */
191    public void setPrintOffset(Integer printOffset) {
192        iPrintOffset = printOffset;
193    }
194
195    /**
196     * Minimal exam size, see {@link Exam#getSize()}
197     * @return minimal examination size
198     */
199    public int getMinSize() {
200        return iMinSize;
201    }
202
203    /**
204     * Minimal exam size, see {@link Exam#getSize()}
205     * @param minSize minimal examination size
206     */
207    public void setMinSize(int minSize) {
208        iMinSize = minSize;
209    }
210
211    /**
212     * Values (assignment of a period and a set of rooms)
213     * 
214     * @return list of {@link ExamPlacement}
215     */
216    @Override
217    public List<ExamPlacement> values(Assignment<Exam, ExamPlacement> assignment) {
218        if (super.values(assignment) == null)
219            init();
220        return super.values(assignment);
221    }
222
223    /**
224     * Return list of possible room placements.
225     * 
226     * @return list of {@link ExamRoomPlacement}
227     */
228    public List<ExamRoomPlacement> getRoomPlacements() {
229        return iRoomPlacements;
230    }
231
232    /**
233     * Return list of possible room placements that are strongly preferred.
234     * 
235     * @return list of {@link ExamRoomPlacement}
236     */
237    public synchronized List<ExamRoomPlacement> getPreferredRoomPlacements() {
238        if (iRoomPreferredPlacements == null) {
239            iRoomPreferredPlacements = new ArrayList<ExamRoomPlacement>();
240            for (ExamRoomPlacement room: iRoomPlacements) {
241                if (room.getPenalty() < -2)
242                    iRoomPreferredPlacements.add(room);
243            }
244        }
245        return iRoomPreferredPlacements;
246    }
247
248    /**
249     * Return list of possible period placements.
250     * 
251     * @return list of {@link ExamPeriodPlacement}
252     */
253    public List<ExamPeriodPlacement> getPeriodPlacements() {
254        if (iAvailablePeriodPlacements == null) {
255            iAvailablePeriodPlacements = new ArrayList<ExamPeriodPlacement>(iPeriodPlacements.size());
256            for (ExamPeriodPlacement p: iPeriodPlacements) {
257                boolean available = true;
258                for (ExamInstructor i: getInstructors())
259                    if (!i.isAllowDirectConflicts() && !i.isAvailable(p.getPeriod())) { available = false; break; }
260                for (ExamStudent s: getStudents())
261                    if (!s.isAllowDirectConflicts() && !s.isAvailable(p.getPeriod())) { available = false; break; }
262                if (available)
263                    iAvailablePeriodPlacements.add(p);
264            }
265            if (iAvailablePeriodPlacements.isEmpty())
266                sLog.error("  Exam " + getName() + " has no available periods.");
267        }
268        return iAvailablePeriodPlacements;
269    }
270    
271    /**
272     * Initialize exam's domain.
273     */
274    private boolean init() {
275        if (sAlterMaxSize && iRoomPlacements.size() > 50) {
276            ExamRoomPlacement med = iRoomPlacements.get(Math.min(50, iRoomPlacements.size() / 2));
277            setMaxRooms(Math.min(getMaxRooms(), 1 + getSize() / med.getSize(hasAltSeating())));
278        }
279        ArrayList<ExamPlacement> values = new ArrayList<ExamPlacement>();
280        if (getMaxRooms() == 0) {
281            for (ExamPeriodPlacement periodPlacement : getPeriodPlacements()) {
282                values.add(new ExamPlacement(this, periodPlacement, new HashSet<ExamRoomPlacement>()));
283            }
284        } else {
285            if (getRoomPlacements().isEmpty()) {
286                sLog.error("  Exam " + getName() + " has no rooms.");
287                setValues(new ArrayList<ExamPlacement>(0));
288                return false;
289            }
290            for (ExamPeriodPlacement periodPlacement : getPeriodPlacements()) {
291                TreeSet<RoomSet> roomSets = new TreeSet<RoomSet>();
292                genRoomSets(periodPlacement.getPeriod(), Math.min(100, iRoomPlacements.size()), roomSets, 0,
293                        getMaxRooms(), new HashSet<ExamRoomPlacement>(), 0, 0);
294                for (RoomSet roomSet : roomSets) {
295                    values.add(new ExamPlacement(this, periodPlacement, roomSet.rooms()));
296                }
297            }
298        }
299        if (values.isEmpty())
300            sLog.error("Exam " + getName() + " has no placement.");
301        setValues(values);
302        return !values.isEmpty();
303    }
304
305    private void genRoomSets(ExamPeriod period, int maxRoomSets, TreeSet<RoomSet> roomSets, int roomIdx, int maxRooms,
306            Set<ExamRoomPlacement> roomsSoFar, int sizeSoFar, int penaltySoFar) {
307        if (sizeSoFar >= getSize()) {
308            double penalty = 
309                    getModel().getCriterion(RoomSplitPenalty.class).getWeight() * (roomsSoFar.size() - 1) * (roomsSoFar.size() - 1) +
310                    getModel().getCriterion(RoomSizePenalty.class).getWeight() * (sizeSoFar - getSize()) +
311                    getModel().getCriterion(RoomPenalty.class).getWeight() * penaltySoFar;
312            if (roomSets.size() >= maxRoomSets) {
313                RoomSet last = roomSets.last();
314                if (penalty < last.penalty()) {
315                    roomSets.remove(last);
316                    roomSets.add(new RoomSet(roomsSoFar, penalty));
317                }
318            } else
319                roomSets.add(new RoomSet(roomsSoFar, penalty));
320            return;
321        }
322        if (!roomSets.isEmpty()) {
323            RoomSet roomSet = roomSets.first();
324            maxRooms = Math.min(maxRooms, (1 + roomSet.rooms().size()) - roomsSoFar.size());
325        }
326        if (maxRooms == 0)
327            return;
328        int sizeBound = sizeSoFar;
329        for (int i = 0; i < maxRooms && roomIdx + i < iRoomPlacements.size(); i++)
330            sizeBound += iRoomPlacements.get(roomIdx + i).getSize(hasAltSeating());
331        while (roomIdx < iRoomPlacements.size()) {
332            if (sizeBound < getSize())
333                break;
334            ExamRoomPlacement room = iRoomPlacements.get(roomIdx);
335            if (room.isAvailable(period)) {
336                roomsSoFar.add(room);
337                genRoomSets(period, maxRoomSets, roomSets, roomIdx + 1, maxRooms - 1, roomsSoFar,
338                        sizeSoFar + room.getSize(hasAltSeating()), penaltySoFar + room.getPenalty(period));
339                roomsSoFar.remove(room);
340            }
341            sizeBound -= room.getSize(hasAltSeating());
342            if (roomIdx + maxRooms < iRoomPlacements.size())
343                sizeBound += iRoomPlacements.get(roomIdx + maxRooms).getSize(hasAltSeating());
344            roomIdx++;
345            if (roomSets.size() == maxRoomSets) {
346                RoomSet last = roomSets.last();
347                if (last.rooms().size() < roomsSoFar.size() + 1)
348                    return;
349            }
350        }
351    }
352
353    private class RoomSet implements Comparable<RoomSet> {
354        private Set<ExamRoomPlacement> iRooms;
355        private double iPenalty;
356
357        public RoomSet(Set<ExamRoomPlacement> rooms, double penalty) {
358            iRooms = new HashSet<ExamRoomPlacement>(rooms);
359            iPenalty = penalty;
360        }
361
362        public Set<ExamRoomPlacement> rooms() {
363            return iRooms;
364        }
365
366        public double penalty() {
367            return iPenalty;
368        }
369
370        public int compareTo(Set<ExamRoomPlacement> rooms, double penalty) {
371            int cmp = Double.compare(iRooms.size(), rooms.size());
372            if (cmp != 0)
373                return cmp;
374            cmp = Double.compare(penalty(), penalty);
375            if (cmp != 0)
376                return cmp;
377            return rooms().toString().compareTo(rooms.toString());
378        }
379
380        @Override
381        public int compareTo(RoomSet r) {
382            return compareTo(r.rooms(), r.penalty());
383        }
384    }
385
386    /**
387     * True if alternative seating is required ({@link ExamRoom#getAltSize()} is
388     * to be used), false if normal seating is required (
389     * {@link ExamRoom#getSize()} is to be used).
390     * 
391     * @return true if alternative seating is required, false otherwise
392     */
393    public boolean hasAltSeating() {
394        return iAltSeating;
395    }
396
397    /**
398     * Length of the exam in minutes. The assigned period has to be of the same
399     * or greater length.
400     * 
401     * @return length of the exam in minutes
402     */
403    public int getLength() {
404        return iLength;
405    }
406
407    /**
408     * Set average period. This represents an average period that the exam was
409     * assigned to in the past. If set, it is used in exam rotation penalty
410     * {@link ExamRotationPenalty} in order to put more weight on
411     * exams that were badly assigned last time(s) and ensuring some form of
412     * fairness.
413     * 
414     * @param period
415     *            average period
416     */
417    public void setAveragePeriod(int period) {
418        iAveragePeriod = period;
419    }
420
421    /**
422     * Average period. This represents an average period that the exam was
423     * assigned to in the past. If set, it is used in exam rotation penalty
424     * {@link ExamRotationPenalty} in order to put more weight on
425     * exams that were badly assigned last time(s) and ensuring some form of
426     * fairness.
427     * 
428     * @return average period
429     */
430    public int getAveragePeriod() {
431        return iAveragePeriod;
432    }
433
434    /**
435     * True if there is an average period assigned to the exam. This represents
436     * an average period that the exam was assigned to in the past. If set, it
437     * is used in exam rotation penalty
438     * {@link ExamRotationPenalty} in order to put more weight on
439     * exams that were badly assigned last time(s) and ensuring some form of
440     * fairness.
441     * @return true if the exam has an average period set
442     */
443    public boolean hasAveragePeriod() {
444        return iAveragePeriod >= 0;
445    }
446
447    /**
448     * True if a direct student conflict is allowed, see
449     * {@link ExamStudent#canConflict(Exam, Exam)}
450     * 
451     * @return true if a direct student conflict is allowed
452     */
453    public boolean isAllowDirectConflicts() {
454        return iAllowDirectConflicts;
455    }
456
457    /**
458     * Set whether a direct student conflict is allowed, see
459     * {@link ExamStudent#canConflict(Exam, Exam)}
460     * 
461     * @param allowDirectConflicts
462     *            true if a direct student conflict is allowed
463     */
464    public void setAllowDirectConflicts(boolean allowDirectConflicts) {
465        iAllowDirectConflicts = allowDirectConflicts;
466    }
467
468    /**
469     * Adds a constraint. Called automatically when the constraint is added to
470     * the model, i.e., {@link Model#addConstraint(Constraint)} is called.
471     * 
472     * @param constraint
473     *            added constraint
474     */
475    @Override
476    public void addContstraint(Constraint<Exam, ExamPlacement> constraint) {
477        if (constraint instanceof ExamStudent)
478            iStudents.add((ExamStudent) constraint);
479        if (constraint instanceof ExamDistributionConstraint)
480            iDistConstraints.add((ExamDistributionConstraint) constraint);
481        if (constraint instanceof ExamInstructor)
482            iInstructors.add((ExamInstructor) constraint);
483        super.addContstraint(constraint);
484        iAvailablePeriodPlacements = null;
485    }
486
487    /**
488     * Removes a constraint. Called automatically when the constraint is removed
489     * from the model, i.e., {@link Model#removeConstraint(Constraint)} is
490     * called.
491     * 
492     * @param constraint
493     *            added constraint
494     */
495    @Override
496    public void removeContstraint(Constraint<Exam, ExamPlacement> constraint) {
497        if (constraint instanceof ExamStudent)
498            iStudents.remove(constraint);
499        if (constraint instanceof ExamDistributionConstraint)
500            iDistConstraints.remove(constraint);
501        if (constraint instanceof ExamInstructor)
502            iInstructors.remove(constraint);
503        super.removeContstraint(constraint);
504        iAvailablePeriodPlacements = null;
505    }
506
507    /**
508     * List of students that are enrolled in the exam
509     * 
510     * @return list of {@link ExamStudent}
511     */
512    public List<ExamStudent> getStudents() {
513        return iStudents;
514    }
515
516    /**
517     * List of distribution constraints that this exam is involved in
518     * 
519     * @return list of {@link ExamDistributionConstraint}
520     */
521    public List<ExamDistributionConstraint> getDistributionConstraints() {
522        return iDistConstraints;
523    }
524
525    /**
526     * List of instructors that are assigned to this exam
527     * 
528     * @return list of {@link ExamInstructor}
529     */
530    public List<ExamInstructor> getInstructors() {
531        return iInstructors;
532    }
533
534    /**
535     * Check all distribution constraint that this exam is involved in
536     * 
537     * @param assignment current assignment
538     * @param period
539     *            a period to be assigned to this exam
540     * @return true, if there is no assignment of some other exam in conflict
541     *         with the given period
542     */
543    public boolean checkDistributionConstraints(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period) {
544        for (ExamDistributionConstraint dc : iDistConstraints) {
545            if (!dc.isHard())
546                continue;
547            boolean before = true;
548            for (Exam exam : dc.variables()) {
549                if (exam.equals(this)) {
550                    before = false;
551                    continue;
552                }
553                ExamPlacement placement = assignment.getValue(exam);
554                if (placement == null)
555                    continue;
556                switch (dc.getType()) {
557                    case ExamDistributionConstraint.sDistSamePeriod:
558                        if (period.getIndex() != placement.getPeriod().getIndex())
559                            return false;
560                        break;
561                    case ExamDistributionConstraint.sDistDifferentPeriod:
562                        if (period.getIndex() == placement.getPeriod().getIndex())
563                            return false;
564                        break;
565                    case ExamDistributionConstraint.sDistPrecedence:
566                        if (before) {
567                            if (period.getIndex() <= placement.getPeriod().getIndex())
568                                return false;
569                        } else {
570                            if (period.getIndex() >= placement.getPeriod().getIndex())
571                                return false;
572                        }
573                        break;
574                    case ExamDistributionConstraint.sDistPrecedenceRev:
575                        if (before) {
576                            if (period.getIndex() >= placement.getPeriod().getIndex())
577                                return false;
578                        } else {
579                            if (period.getIndex() <= placement.getPeriod().getIndex())
580                                return false;
581                        }
582                        break;
583                    
584                    case ExamDistributionConstraint.sDistSameDay:
585                        if (period.getPeriod().getDay() != placement.getPeriod().getDay())
586                            return false;
587                        break;
588                    case ExamDistributionConstraint.sDistDifferentDay:
589                        if (period.getPeriod().getDay() == placement.getPeriod().getDay())
590                            return false;
591                        break;
592                }
593            }
594        }
595        return true;
596    }
597
598    /**
599     * Check all distribution constraint that this exam is involved in
600     * 
601     * @param assignment current assignment
602     * @param room
603     *            a room to be assigned to this exam
604     * @return true, if there is no assignment of some other exam in conflict
605     *         with the given room
606     */
607    public boolean checkDistributionConstraints(Assignment<Exam, ExamPlacement> assignment, ExamRoomPlacement room) {
608        for (ExamDistributionConstraint dc : iDistConstraints) {
609            if (!dc.isHard())
610                continue;
611            for (Exam exam : dc.variables()) {
612                if (exam.equals(this))
613                    continue;
614                ExamPlacement placement = assignment.getValue(exam);
615                if (placement == null)
616                    continue;
617                switch (dc.getType()) {
618                    case ExamDistributionConstraint.sDistSameRoom:
619                        if (!placement.getRoomPlacements().contains(room))
620                            return false;
621                        break;
622                    case ExamDistributionConstraint.sDistDifferentRoom:
623                        if (placement.getRoomPlacements().contains(room))
624                            return false;
625                        break;
626                }
627            }
628        }
629        return true;
630    }
631
632    /**
633     * Check all soft distribution constraint that this exam is involved in
634     * 
635     * @param assignment current assignment
636     * @param room
637     *            a room to be assigned to this exam
638     * @return sum of penalties of violated distribution constraints
639     */
640    public int getDistributionConstraintPenalty(Assignment<Exam, ExamPlacement> assignment, ExamRoomPlacement room) {
641        int penalty = 0;
642        for (ExamDistributionConstraint dc : iDistConstraints) {
643            if (dc.isHard())
644                continue;
645            for (Exam exam : dc.variables()) {
646                if (exam.equals(this))
647                    continue;
648                ExamPlacement placement = assignment.getValue(exam);
649                if (placement == null)
650                    continue;
651                switch (dc.getType()) {
652                    case ExamDistributionConstraint.sDistSameRoom:
653                        if (!placement.getRoomPlacements().contains(room))
654                            penalty += dc.getWeight();
655                        break;
656                    case ExamDistributionConstraint.sDistDifferentRoom:
657                        if (placement.getRoomPlacements().contains(room))
658                            penalty += dc.getWeight();
659                        break;
660                }
661            }
662        }
663        return penalty;
664    }
665
666    /**
667     * Maximal number of rooms that can be assigned to the exam
668     * 
669     * @return maximal number of rooms that can be assigned to the exam
670     */
671    public int getMaxRooms() {
672        return iMaxRooms;
673    }
674
675    /**
676     * Set maximal number of rooms that can be assigned to the exam
677     * 
678     * @param maxRooms
679     *            maximal number of rooms that can be assigned to the exam
680     */
681    public void setMaxRooms(int maxRooms) {
682        iMaxRooms = maxRooms;
683    }
684
685    /**
686     * Find best available rooms for the exam in the given period. First of all,
687     * it tries to find the minimal number of rooms that cover the size of the
688     * exam. Among these, a set of rooms of total smallest size is preferred. If
689     * the original room is available and of enough size, it is returned. All
690     * necessary checks are made (availability of rooms, room penalties, room
691     * sizes etc.).
692     * 
693     * @param assignment current assignment
694     * @param period
695     *            given period.
696     * @return best available rooms for the exam in the given period, null if
697     *         there is no valid assignment
698     */
699    public Set<ExamRoomPlacement> findBestAvailableRooms(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period) {
700        if (getMaxRooms() == 0)
701            return new HashSet<ExamRoomPlacement>();
702        double sw = getModel().getCriterion(RoomSizePenalty.class).getWeight();
703        double pw = getModel().getCriterion(RoomPenalty.class).getWeight();
704        double cw = getModel().getCriterion(DistributionPenalty.class).getWeight();
705        ExamRoomSharing sharing = ((ExamModel) getModel()).getRoomSharing();
706        loop: for (int nrRooms = 1; nrRooms <= getMaxRooms(); nrRooms++) {
707            HashSet<ExamRoomPlacement> rooms = new HashSet<ExamRoomPlacement>();
708            int size = 0;
709            while (rooms.size() < nrRooms && size < getSize()) {
710                int minSize = (getSize() - size) / (nrRooms - rooms.size());
711                ExamRoomPlacement best = null;
712                double bestWeight = 0;
713                int bestSize = 0;
714                for (ExamRoomPlacement room : getRoomPlacements()) {
715                    if (!room.isAvailable(period.getPeriod()))
716                        continue;
717                    if (nrRooms == 1 && sharing != null) {
718                        if (sharing.inConflict(this, room.getRoom().getPlacements(assignment, period.getPeriod()), room.getRoom()))
719                            continue;
720                    } else {
721                        if (!room.getRoom().getPlacements(assignment, period.getPeriod()).isEmpty())
722                            continue;
723                    }
724                    if (rooms.contains(room))
725                        continue;
726                    if (!checkDistributionConstraints(assignment, room))
727                        continue;
728                    int s = room.getSize(hasAltSeating());
729                    if (s < minSize)
730                        break;
731                    int p = room.getPenalty(period.getPeriod());
732                    double w = pw * p + sw * (s - minSize) + cw * getDistributionConstraintPenalty(assignment, room);
733                    double d = 0;
734                    if (!rooms.isEmpty()) {
735                        for (ExamRoomPlacement r : rooms) {
736                            d += r.getDistanceInMeters(room);
737                        }
738                        w += d / rooms.size();
739                    }
740                    if (best == null || bestWeight > w) {
741                        best = room;
742                        bestSize = s;
743                        bestWeight = w;
744                    }
745                }
746                if (best == null)
747                    continue loop;
748                rooms.add(best);
749                size += bestSize;
750            }
751            if (size >= getSize())
752                return rooms;
753        }
754        return null;
755    }
756
757    /**
758     * Randomly find a set of available rooms for the exam in the given period.
759     * First of all, it tries to find the minimal number of rooms that cover the
760     * size of the exam. Among these, a set of rooms of total smallest size is
761     * preferred. All necessary checks are made (availability of rooms, room
762     * penalties, room sizes etc.).
763     * 
764     * @param assignment current assignment
765     * @param period
766     *            given period.
767     * @return randomly computed set of available rooms for the exam in the
768     *         given period, null if there is no valid assignment
769     */
770    public Set<ExamRoomPlacement> findRoomsRandom(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period) {
771        return findRoomsRandom(assignment, period, true);
772    }
773
774    /**
775     * Randomly find a set of available rooms for the exam in the given period.
776     * First of all, it tries to find the minimal number of rooms that cover the
777     * size of the exam. Among these, a set of rooms of total smallest size is
778     * preferred. All necessary checks are made (availability of rooms, room
779     * penalties, room sizes etc.).
780     * 
781     * @param assignment current assignment
782     * @param period
783     *            given period.
784     * @param checkConflicts
785     *            if false, room and distribution conflicts are not checked
786     * @return randomly computed set of available rooms for the exam in the
787     *         given period, null if there is no valid assignment
788     */
789    public Set<ExamRoomPlacement> findRoomsRandom(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period, boolean checkConflicts) {
790        if (getMaxRooms() == 0)
791            return new HashSet<ExamRoomPlacement>();
792        HashSet<ExamRoomPlacement> rooms = new HashSet<ExamRoomPlacement>();
793        int size = 0;
794        ExamRoomSharing sharing = ((ExamModel) getModel()).getRoomSharing();
795        loop: while (rooms.size() < getMaxRooms()) {
796            int rx = ToolBox.random(getRoomPlacements().size());
797            int minSize = (getSize() - size + (getMaxRooms() - rooms.size() - 1)) / (getMaxRooms() - rooms.size());
798            for (int r = 0; r < getRoomPlacements().size(); r++) {
799                ExamRoomPlacement room = getRoomPlacements().get((r + rx) % getRoomPlacements().size());
800                int s = room.getSize(hasAltSeating());
801                if (s < minSize)
802                    continue;
803                if (!room.isAvailable(period.getPeriod()))
804                    continue;
805                if (checkConflicts) {
806                    if (rooms.isEmpty() && sharing != null && !room.getRoom().getPlacements(assignment, period.getPeriod()).isEmpty()) {
807                        if (sharing.inConflict(this, room.getRoom().getPlacements(assignment, period.getPeriod()), room.getRoom()))
808                            continue;
809                    } else {
810                        if (!room.getRoom().getPlacements(assignment, period.getPeriod()).isEmpty())
811                            continue;
812                    }
813                }
814                if (rooms.contains(room))
815                    continue;
816                if (checkConflicts && !checkDistributionConstraints(assignment, room))
817                    continue;
818                size += s;
819                rooms.add(room);
820                if (size >= getSize()) {
821                    for (Iterator<ExamRoomPlacement> j = rooms.iterator(); j.hasNext();) {
822                        ExamRoomPlacement rp = j.next();
823                        if (size - rp.getSize(hasAltSeating()) >= getSize()) {
824                            j.remove();
825                            size -= rp.getSize(hasAltSeating());
826                        }
827                    }
828                    return rooms;
829                }
830                continue loop;
831            }
832            break;
833        }
834        return null;
835    }
836
837    private HashSet<Exam> iCorrelatedExams = null;
838
839    /**
840     * Number of exams that are correlated with this exam (there is at least one
841     * student attending both exams).
842     * 
843     * @return number of correlated exams
844     */
845    public int nrStudentCorrelatedExams() {
846        return getStudentCorrelatedExams().size();
847    }
848    
849    /**
850     * Exams that are correlated with this exam (there is at least one
851     * student attending both exams).
852     * 
853     * @return number of correlated exams
854     */
855    public synchronized Set<Exam> getStudentCorrelatedExams() {
856        if (iCorrelatedExams == null) {
857            iCorrelatedExams = new HashSet<Exam>();
858            for (ExamStudent student : iStudents) {
859                iCorrelatedExams.addAll(student.variables());
860            }
861            iCorrelatedExams.remove(this);
862        }
863        return iCorrelatedExams;
864    }
865
866    private Integer iEstimatedDomainSize = null;
867
868    private synchronized int estimatedDomainSize() {
869        if (iEstimatedDomainSize == null) {
870            int periods = getPeriodPlacements().size();
871            int rooms = -1;
872            int split = 0;
873            while (rooms < split && split <= getMaxRooms()) {
874                rooms = 0;
875                split++;
876                for (ExamRoomPlacement room : getRoomPlacements()) {
877                    if (room.getSize(hasAltSeating()) >= (getSize() / split))
878                        rooms++;
879                }
880            }
881            iEstimatedDomainSize = new Integer(periods * rooms / split);
882        }
883        return iEstimatedDomainSize.intValue();
884    }
885
886    /**
887     * An exam with more correlated exams is preferred (
888     * {@link Exam#nrStudentCorrelatedExams()}). If it is the same, ratio number
889     * of students / number of available periods is used. If the same, exam ids
890     * are used.
891     */
892    @Override
893    public int compareTo(Exam o) {
894        Exam e = o;
895        int cmp = Double.compare(estimatedDomainSize(), e.estimatedDomainSize());
896        if (cmp != 0)
897            return cmp;
898        cmp = -Double.compare(nrStudentCorrelatedExams(), e.nrStudentCorrelatedExams());
899        if (cmp != 0)
900            return cmp;
901        cmp = -Double.compare(((double) getSize()) / getPeriodPlacements().size(), ((double) e.getSize())
902                / e.getPeriodPlacements().size());
903        if (cmp != 0)
904            return cmp;
905        return super.compareTo(o);
906    }
907
908    /**
909     * True, if there is a student of this exam (that does not have direct
910     * conflicts allowed, see {@link ExamStudent#canConflict(Exam, Exam)}) that
911     * attends some other exam in the given period.
912     * 
913     * @param assignment current assignment
914     * @param period
915     *            a period
916     * @return true if there is a student conflict
917     */
918    public boolean hasStudentConflictWithPreAssigned(Assignment<Exam, ExamPlacement> assignment, ExamPeriod period) {
919        Map<ExamStudent, Set<Exam>> studentsOfPeriod = ((ExamModel)getModel()).getStudentsOfPeriod(assignment, period);
920        for (ExamStudent s : getStudents()) {
921            Set<Exam> exams = studentsOfPeriod.get(s);
922            if (exams == null) continue;
923            for (Exam exam : exams) {
924                if (!exam.equals(this) && !s.canConflict(this, exam)) return true;
925            }
926        }
927        return false;
928    }
929
930    /**
931     * Number of students of this exam (that does not have direct conflicts
932     * allowed, see {@link ExamStudent#canConflict(Exam, Exam)}) that attend
933     * some other exam in the given period.
934     * 
935     * @param assignment current assignment
936     * @param period
937     *            a period
938     * @return number of direct student conflicts that are prohibited
939     */
940    public int countStudentConflicts(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period) {
941        int conf = 0;
942        Map<ExamStudent, Set<Exam>> studentsOfPeriod = ((ExamModel)getModel()).getStudentsOfPeriod(assignment, period.getPeriod());
943        for (ExamStudent s : getStudents()) {
944            Set<Exam> exams = studentsOfPeriod.get(s);
945            if (exams == null) continue;
946            for (Exam exam : exams) {
947                if (!exam.equals(this) && !s.canConflict(this, exam)) conf++;
948            }
949        }
950        return conf;
951    }
952    
953    /**
954     * Number of instructor of this exam (that does not have direct conflicts
955     * allowed, see {@link ExamInstructor#canConflict(Exam, Exam)}) that attend
956     * some other exam in the given period.
957     * 
958     * @param assignment current assignment
959     * @param period
960     *            a period
961     * @return number of direct student conflicts that are prohibited
962     */
963    public int countInstructorConflicts(Assignment<Exam, ExamPlacement> assignment, ExamPeriodPlacement period) {
964        int conf = 0;
965        Map<ExamInstructor, Set<Exam>> instructorsOfPeriod = ((ExamModel)getModel()).getInstructorsOfPeriod(assignment, period.getPeriod());
966        for (ExamInstructor i : getInstructors()) {
967            Set<Exam> exams = instructorsOfPeriod.get(i);
968            if (exams == null) continue;
969            for (Exam exam : exams) {
970                if (!exam.equals(this) && !i.canConflict(this, exam)) conf++;
971            }
972        }
973        return conf;
974    }
975
976    /**
977     * List of exams that are assigned to the given period and share one or more
978     * students with this exam (that does not have direct conflicts allowed, see
979     * {@link ExamStudent#canConflict(Exam, Exam)}).
980     * 
981     * @param assignment current assignment
982     * @param period
983     *            a period
984     * @return list of {@link Exam} (other than this exam, that are placed in
985     *         the given period and create prohibited direct conflicts)
986     */
987    public HashSet<Exam> getStudentConflicts(Assignment<Exam, ExamPlacement> assignment, ExamPeriod period) {
988        HashSet<Exam> conf = new HashSet<Exam>();
989        Map<ExamStudent, Set<Exam>> studentsOfPeriod = ((ExamModel)getModel()).getStudentsOfPeriod(assignment, period);
990        for (ExamStudent s : getStudents()) {
991            Set<Exam> exams = studentsOfPeriod.get(s);
992            if (exams == null) continue;
993            for (Exam exam : exams) {
994                if (!exam.equals(this) && !s.canConflict(this, exam)) conf.add(exam);
995            }
996        }
997        return conf;
998    }
999
1000    /**
1001     * Allow all direct student conflict for the given period (see
1002     * {@link ExamStudent#canConflict(Exam, Exam)}).
1003     * 
1004     * @param assignment current assignment
1005     * @param period
1006     *            a period
1007     */
1008    public void allowAllStudentConflicts(Assignment<Exam, ExamPlacement> assignment, ExamPeriod period) {
1009        Map<ExamStudent, Set<Exam>> studentsOfPeriod = ((ExamModel)getModel()).getStudentsOfPeriod(assignment, period);
1010        for (ExamStudent s : getStudents()) {
1011            Set<Exam> exams = studentsOfPeriod.get(s);
1012            if (exams == null) continue;
1013            for (Exam exam : exams) {
1014                if (exam.equals(this)) continue;
1015                exam.setAllowDirectConflicts(true);
1016                setAllowDirectConflicts(true);
1017                s.setAllowDirectConflicts(true);
1018            }
1019        }
1020    }
1021
1022    /**
1023     * String representation
1024     * 
1025     * @return exam id (periods: number of periods, rooms: number of rooms,
1026     *         student: number of students, maxRooms: max rooms[, alt if
1027     *         alternate seating is required])
1028     */
1029    @Override
1030    public String toString() {
1031        return getName() + " (periods:" + getPeriodPlacements().size() + ", rooms:" + getRoomPlacements().size()
1032                + ", size:" + getSize() + " ,maxRooms:" + getMaxRooms() + (hasAltSeating() ? ", alt" : "") + ")";
1033    }
1034
1035    /** Exam name */
1036    @Override
1037    public String getName() {
1038        return (hasName() ? iName : String.valueOf(getId()));
1039    }
1040
1041    /** Exam name 
1042     * @param name examination name
1043     **/
1044    public void setName(String name) {
1045        iName = name;
1046    }
1047
1048    /** Exam name 
1049     * @return true if the examination name is set and it is not empty
1050     **/
1051    public boolean hasName() {
1052        return iName != null && iName.length() > 0;
1053    }
1054
1055    private HashMap<Exam, List<ExamStudent>> iJenrls = null;
1056
1057    /**
1058     * Joint enrollments
1059     * 
1060     * @return table {@link Exam} (an exam that has at least one student in
1061     *         common with this exam) &rarr; {@link List} (list of students in
1062     *         common)
1063     */
1064    public Map<Exam, List<ExamStudent>> getJointEnrollments() {
1065        if (iJenrls != null)
1066            return iJenrls;
1067        iJenrls = new HashMap<Exam, List<ExamStudent>>();
1068        for (ExamStudent student : getStudents()) {
1069            for (Exam other : student.variables()) {
1070                if (other.equals(this))
1071                    continue;
1072                List<ExamStudent> students = iJenrls.get(other);
1073                if (students == null) {
1074                    students = new ArrayList<ExamStudent>();
1075                    iJenrls.put(other, students);
1076                }
1077                students.add(student);
1078            }
1079        }
1080        return iJenrls;
1081    }
1082
1083    /**
1084     * Courses and/or sections that are having this exam
1085     * 
1086     * @return list of {@link ExamOwner}
1087     */
1088    public List<ExamOwner> getOwners() {
1089        return iOwners;
1090    }
1091
1092    /**
1093     * Courses/sections of this exam into which the given student is enrolled
1094     * into
1095     * 
1096     * @param student
1097     *            a student that is enrolled into this exam
1098     * @return list of courses/sections {@link ExamOwner} which are having this
1099     *         exam with the given student enrolled in
1100     */
1101    public Collection<ExamOwner> getOwners(ExamStudent student) {
1102        Collection<ExamOwner> ret = new ArrayList<ExamOwner>();
1103        for (ExamOwner owner : iOwners) {
1104            if (owner.getStudents().contains(student))
1105                ret.add(owner);
1106        }
1107        return ret;
1108    }
1109
1110    /**
1111     * Courses/sections of this exam into which the given instructor is enrolled
1112     * into
1113     * 
1114     * @param instructor
1115     *            an instructor that is enrolled into this exam
1116     * @return list of courses/sections {@link ExamOwner} which are having this
1117     *         exam with the given instructor enrolled in
1118     */
1119    public Collection<ExamOwner> getOwners(ExamInstructor instructor) {
1120        Collection<ExamOwner> ret = new ArrayList<ExamOwner>();
1121        for (ExamOwner owner : iOwners) {
1122            if (owner.getIntructors().contains(instructor))
1123                ret.add(owner);
1124        }
1125        return ret;
1126    }
1127
1128    /**
1129     * Returns appropriate {@link ExamPeriodPlacement} for the given period, if
1130     * it is available for this exam, null otherwise.
1131     * @param periodId period unique id
1132     * @return the appropriate period placement
1133     */
1134    public ExamPeriodPlacement getPeriodPlacement(Long periodId) {
1135        for (ExamPeriodPlacement periodPlacement : getPeriodPlacements()) {
1136            if (periodPlacement.getId().equals(periodId))
1137                return periodPlacement;
1138        }
1139        return null;
1140    }
1141
1142    /**
1143     * Returns appropriate {@link ExamRoomPlacement} for the given room, if it
1144     * is available for this exam, null otherwise.
1145     * @param roomId room unique id
1146     * @return the appropriate room placement
1147     */
1148    public ExamRoomPlacement getRoomPlacement(long roomId) {
1149        for (ExamRoomPlacement roomPlacement : iRoomPlacements) {
1150            if (roomPlacement.getId() == roomId)
1151                return roomPlacement;
1152        }
1153        return null;
1154    }
1155
1156    /**
1157     * Returns appropriate {@link ExamPeriodPlacement} for the given period, if
1158     * it is available for this exam, null otherwise.
1159     * @param period period in question
1160     * @return the appropriate period placement
1161     */
1162    public ExamPeriodPlacement getPeriodPlacement(ExamPeriod period) {
1163        for (ExamPeriodPlacement periodPlacement : getPeriodPlacements()) {
1164            if (periodPlacement.getPeriod().equals(period))
1165                return periodPlacement;
1166        }
1167        return null;
1168    }
1169
1170    /**
1171     * Returns appropriate {@link ExamRoomPlacement} for the given room, if it
1172     * is available for this exam, null otherwise.
1173     * @param room room in question
1174     * @return the appropriate room placement
1175     */
1176    public ExamRoomPlacement getRoomPlacement(ExamRoom room) {
1177        for (ExamRoomPlacement roomPlacement : getRoomPlacements()) {
1178            if (roomPlacement.getRoom().equals(room))
1179                return roomPlacement;
1180        }
1181        return null;
1182    }
1183
1184    /** Return true if there are some values in the domain of this variable */
1185    @Override
1186    public boolean hasValues() {
1187        return !getPeriodPlacements().isEmpty() && (getMaxRooms() == 0 || !getRoomPlacements().isEmpty());
1188    }
1189    
1190    @Override
1191    public void variableAssigned(Assignment<Exam, ExamPlacement> assignment, long iteration, ExamPlacement placement) {
1192        if (getMaxRooms() > 0) {
1193            int size = 0;
1194            for (ExamRoomPlacement room : placement.getRoomPlacements()) {
1195                size += room.getSize(hasAltSeating());
1196            }
1197            if (size < getSize() && !placement.getRoomPlacements().isEmpty()) {
1198                Progress.getInstance(getModel()).warn("Selected room(s) are too small " + size + "<" + getSize() + " (" + getName() + " " + placement.getName() + ")");
1199            }
1200        }        
1201    }
1202}