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