001package org.cpsolver.coursett.criteria.additional;
002
003import java.util.BitSet;
004import java.util.Collection;
005import java.util.HashMap;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010import java.util.TreeSet;
011
012import org.cpsolver.coursett.Constants;
013import org.cpsolver.coursett.constraint.InstructorConstraint;
014import org.cpsolver.coursett.constraint.InstructorConstraint.InstructorConstraintContext;
015import org.cpsolver.coursett.criteria.TimetablingCriterion;
016import org.cpsolver.coursett.model.Lecture;
017import org.cpsolver.coursett.model.Placement;
018import org.cpsolver.coursett.model.TimetableModel;
019import org.cpsolver.ifs.assignment.Assignment;
020import org.cpsolver.ifs.util.DataProperties;
021
022
023/**
024 * The class represents various criteria concerning compact timetables of
025 * instructors. The criteria are checked and updated when a variable is
026 * (un)assigned.
027 * <br>
028 * implemented criterion: lunch break
029 * <br>
030 * @version CourseTT 1.3 (University Course Timetabling)<br>
031 *          Copyright (C) 2012 Matej Lukac<br>
032 * <br>
033 *          This library is free software; you can redistribute it and/or modify
034 *          it under the terms of the GNU Lesser General Public License as
035 *          published by the Free Software Foundation; either version 3 of the
036 *          License, or (at your option) any later version. <br>
037 * <br>
038 *          This library is distributed in the hope that it will be useful, but
039 *          WITHOUT ANY WARRANTY; without even the implied warranty of
040 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
041 *          Lesser General Public License for more details. <br>
042 * <br>
043 *          You should have received a copy of the GNU Lesser General Public
044 *          License along with this library; if not see
045 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
046 */
047public class InstructorLunchBreak extends TimetablingCriterion {
048    // lunch attributes
049    private double iMultiplier;
050    private int iLunchStart, iLunchEnd, iLunchLength;
051    private boolean iFullInfo;
052    private List<BitSet> iWeeks = null;
053    
054    public InstructorLunchBreak() {
055        setValueUpdateType(ValueUpdateType.AfterUnassignedAfterAssigned);
056    }
057
058    @Override
059    public void configure(DataProperties properties) {
060        super.configure(properties);
061
062        iWeight = properties.getPropertyDouble("InstructorLunch.Weight", 0.3d);
063
064        // lunch parameters
065        iLunchStart = properties.getPropertyInt("InstructorLunch.StartSlot", (11 * 60) / 5);
066        iLunchEnd = properties.getPropertyInt("InstructorLunch.EndSlot", (13 * 60 + 30) / 5);
067        iLunchLength = properties.getPropertyInt("InstructorLunch.Length", 30 / 5);
068        iMultiplier = properties.getPropertyDouble("InstructorLunch.Multiplier", 1.2d);
069        iFullInfo = properties.getPropertyBoolean("InstructorLunch.InfoShowViolations", false);
070    }
071    
072    /**
073     * The method creates date patterns (bitsets) which represent the weeks of a
074     * semester.
075     * 
076     * @return a list of BitSets which represents the weeks of a semester.
077     */
078    protected List<BitSet> getWeeks() {
079        if (iWeeks == null) {
080            TimetableModel model = (TimetableModel) getModel();
081            iWeeks = model.getWeeks();
082        }
083        return iWeeks;            
084    }
085
086    private boolean isEmpty(InstructorConstraintContext ic, int slot, BitSet week, Placement p) {
087        if (p.getTimeLocation().getStartSlot() <= slot && slot < p.getTimeLocation().getStartSlot() + p.getTimeLocation().getLength() && p.getTimeLocation().shareWeeks(week))
088            return false;
089        List<Placement> placements = ic.getPlacements(slot, week);
090        return placements.isEmpty() || (placements.size() == 1 && placements.get(0).variable().equals(p.variable()));
091    }
092    
093    @Override
094    public double getValue(Assignment<Lecture, Placement> assignment, Placement value, Set<Placement> conflicts) {
095        double ret = 0.0;
096        if (value.getTimeLocation().getStartSlot() <= iLunchEnd && value.getTimeLocation().getStartSlot() + value.getTimeLocation().getLength() > iLunchStart) {
097            InstructorLunchBreakContext context = (InstructorLunchBreakContext)getContext(assignment);
098            for (InstructorConstraint constraint: value.variable().getInstructorConstraints()) {
099                InstructorConstraintContext icx = constraint.getContext(assignment);
100                CompactInfo compactInfo = context.getCompactInfo(constraint);
101                for (int i = 0; i < Constants.NR_DAYS; i++) {
102                    // checks only days affected by the placement
103                    if ((value.getTimeLocation().getDayCode() & Constants.DAY_CODES[i]) != 0) {
104                        int currentLunchStartSlot = Constants.SLOTS_PER_DAY * i + iLunchStart;
105                        int currentLunchEndSlot = Constants.SLOTS_PER_DAY * i + iLunchEnd;
106                        int semesterViolations = 0;
107                        for (BitSet week : getWeeks()) {
108                            int maxBreak = 0;
109                            int currentBreak = 0;
110                            for (int slot = currentLunchStartSlot; slot < currentLunchEndSlot; slot++) {
111                                if (isEmpty(icx, slot, week, value)) {
112                                    currentBreak++;
113                                    if (maxBreak < currentBreak) {
114                                        maxBreak = currentBreak;
115                                    }
116                                } else {
117                                    currentBreak = 0;
118                                }
119                            }
120                            if (maxBreak < iLunchLength) {
121                                semesterViolations++;
122                            }
123                        }
124                        // add the difference to the result
125                        ret += semesterViolations - compactInfo.getLunchDayViolations()[i];
126                    }
127                }
128            }
129        }
130        return ret;
131    }
132
133    @Override
134    public double getValue(Assignment<Lecture, Placement> assignment, Collection<Lecture> variables) {
135        double lunchValue = 0.0d;
136        Set<InstructorConstraint> constraints = new HashSet<InstructorConstraint>();
137        for (Lecture lecture : variables) {
138            constraints.addAll(lecture.getInstructorConstraints());
139        }
140        for (InstructorConstraint instructor : constraints) {
141            lunchValue += ((InstructorLunchBreakContext)getContext(assignment)).getLunchPreference(assignment, instructor);
142        }
143        return lunchValue;
144    }
145
146    @Override
147    public void getInfo(Assignment<Lecture, Placement> assignment, Map<String, String> info) {
148        Set<String> violatedLunchBreaks = new TreeSet<String>();
149        int lunchViolations = 0;
150        for (InstructorConstraint c : ((TimetableModel)getModel()).getInstructorConstraints()) {
151            String days = "";
152            CompactInfo compactInfo = ((InstructorLunchBreakContext)getContext(assignment)).getCompactInfo(c);
153            for (int i = 0; i < Constants.NR_DAYS; i++) {
154                if (compactInfo.getLunchDayViolations()[i] > 0) {
155                    if (iFullInfo)
156                        days += (days.isEmpty() ? "" : ", ") + compactInfo.getLunchDayViolations()[i] + " &times; " + Constants.DAY_NAMES_SHORT[i];
157                    lunchViolations += compactInfo.getLunchDayViolations()[i];
158                }
159            }
160            if (iFullInfo && !days.isEmpty())
161                violatedLunchBreaks.add(c.getName() + ": " + days);
162        }
163        if (lunchViolations > 0) {
164            info.put("Lunch breaks", getPerc(lunchViolations, 0, ((TimetableModel)getModel()).getInstructorConstraints().size() * Constants.NR_DAYS * getWeeks().size()) + "% (" + lunchViolations + ")");
165            if (iFullInfo && !violatedLunchBreaks.isEmpty()) {
166                String message = "";
167                for (String s: violatedLunchBreaks)
168                    message += (message.isEmpty() ? "" : "<br>") + s;
169                info.put("Lunch break violations", message);
170            }
171        }
172    }
173
174    @Override
175    public void getInfo(Assignment<Lecture, Placement> assignment, Map<String, String> info, Collection<Lecture> variables) {
176        Set<InstructorConstraint> constraints = new HashSet<InstructorConstraint>();
177        for (Lecture lecture : variables) {
178            for (InstructorConstraint c : lecture.getInstructorConstraints()) {
179                constraints.add(c);
180            }
181        }
182        Set<String> violatedLunchBreaks = new TreeSet<String>();
183        int lunchViolations = 0;
184        for (InstructorConstraint c : constraints) {
185            String days = "";
186            CompactInfo compactInfo = ((InstructorLunchBreakContext)getContext(assignment)).getCompactInfo(c);
187            for (int i = 0; i < Constants.NR_DAYS; i++) {
188                if (compactInfo.getLunchDayViolations()[i] > 0) {
189                    if (iFullInfo)
190                        days += (days.isEmpty() ? "" : ", ") + compactInfo.getLunchDayViolations()[i] + " &times; " + Constants.DAY_NAMES_SHORT[i];
191                    lunchViolations += compactInfo.getLunchDayViolations()[i];
192                }
193            }
194            if (iFullInfo && !days.isEmpty())
195                violatedLunchBreaks.add(c.getName() + ": " + days);
196        }
197        if (lunchViolations > 0) {
198            info.put("Lunch breaks", getPerc(lunchViolations, 0, constraints.size() * Constants.NR_DAYS * getWeeks().size()) + "% (" + lunchViolations + ")");
199            if (iFullInfo && !violatedLunchBreaks.isEmpty()) {
200                String message = "";
201                for (String s: violatedLunchBreaks)
202                    message += (message.isEmpty() ? "" : "; ") + s;
203                info.put("Lunch break violations", message);
204            }
205        }
206    }
207    
208    /**
209     * The class is used as a container of information concerning lunch break
210     * of instructors. It is designed as an attribute of an
211     * InstructorConstraint.
212     */
213    public static class CompactInfo {
214        // lunch attributes
215        private int[] iLunchDayViolations = new int[Constants.NR_DAYS];
216
217        public CompactInfo() {
218        }
219        
220        public int[] getLunchDayViolations() { return iLunchDayViolations; }
221    }
222    
223    public class InstructorLunchBreakContext extends ValueContext {
224        private Map<InstructorConstraint, CompactInfo> iCompactInfos = new HashMap<InstructorConstraint, CompactInfo>();
225
226        protected InstructorLunchBreakContext(Assignment<Lecture, Placement> assignment) {
227            for (InstructorConstraint constraint: ((TimetableModel)getModel()).getInstructorConstraints())
228                iTotal += computeLunchPenalty(assignment, constraint);
229        }
230        
231        @Override
232        protected void unassigned(Assignment<Lecture, Placement> assignment, Placement value) {
233            for (InstructorConstraint constraint: value.variable().getInstructorConstraints())
234                updateCriterion(assignment, constraint, value);
235        }
236        
237        @Override
238        protected void assigned(Assignment<Lecture, Placement> assignment, Placement value) {
239            for (InstructorConstraint constraint: value.variable().getInstructorConstraints())
240                updateCriterion(assignment, constraint, value);
241        }
242        
243        /**
244         * Method checks or sets the CompactInfo of an InstructorConstraint. It
245         * updates the preference of chosen criteria. The update consists of
246         * decrementing the criterion value by previous preference, finding the
247         * current preference and incrementing the criterion value by the current
248         * preference.
249         * 
250         * @param assignment current assignment 
251         * @param instructorConstraint
252         *            the Instructor constraint of an instructor checked for
253         *            criteria
254         * @param placement
255         *            placement of a lecture currently (un)assigned
256         */
257        public void updateCriterion(Assignment<Lecture, Placement> assignment, InstructorConstraint instructorConstraint, Placement placement) {
258            iTotal -= getLunchPreference(assignment, instructorConstraint);
259            updateLunchPenalty(assignment, instructorConstraint, placement);
260            iTotal += getLunchPreference(assignment, instructorConstraint);       
261        }
262
263        /**
264         * Get compact info that is associated with an instructor constraint.
265         * Create a new one if none has been created yet.
266         * @param constraint instructor constraint
267         * @return compact info for the given constraint
268         */
269        protected CompactInfo getCompactInfo(InstructorConstraint constraint) {
270            CompactInfo info = iCompactInfos.get(constraint);
271            if (info == null) {
272                info = new CompactInfo();
273                iCompactInfos.put(constraint, info);
274            }
275            return info;
276        }
277        
278        /**
279         * Method updates number of violations in days (Mo, Tue, Wed,..) considering
280         * each week in the semester separately. The current number of violations
281         * for a day is stored in the CompactInfo.lunchDayViolations of the
282         * constraint, which must be set properly before the calling of the method.
283         * 
284         * @param assignment current assignment 
285         * @param constraint
286         *            the Instructor constraint of an instructor checked for a lunch
287         *            break
288         * @param p
289         *            placement of a lecture currently (un)assigned
290         */
291        public void updateLunchPenalty(Assignment<Lecture, Placement> assignment, InstructorConstraint constraint, Placement p) {
292            // checks only placements in the lunch time
293            if (p.getTimeLocation().getStartSlot() <= iLunchEnd && p.getTimeLocation().getStartSlot() + p.getTimeLocation().getLength() > iLunchStart) {
294                CompactInfo compactInfo = getCompactInfo(constraint);
295                for (int i = 0; i < Constants.NR_DAYS; i++) {
296                    // checks only days affected by the placement
297                    if ((p.getTimeLocation().getDayCode() & Constants.DAY_CODES[i]) != 0) {
298                        int currentLunchStartSlot = Constants.SLOTS_PER_DAY * i + iLunchStart;
299                        int currentLunchEndSlot = Constants.SLOTS_PER_DAY * i + iLunchEnd;
300                        int semesterViolations = 0;
301                        for (BitSet week : getWeeks()) {
302                            int maxBreak = 0;
303                            int currentBreak = 0;
304                            for (int slot = currentLunchStartSlot; slot < currentLunchEndSlot; slot++) {
305                                if (constraint.getContext(assignment).getPlacements(slot, week).isEmpty()) {
306                                    currentBreak++;
307                                    if (maxBreak < currentBreak) {
308                                        maxBreak = currentBreak;
309                                    }
310                                } else {
311                                    currentBreak = 0;
312                                }
313                            }
314                            if (maxBreak < iLunchLength) {
315                                semesterViolations++;
316                            }
317                        }
318                        // saving the result in the CompactInfo of the
319                        // InstructorConstraint
320                        compactInfo.getLunchDayViolations()[i] = semesterViolations;
321                    }
322                }
323            }
324        }
325        
326        /**
327         * Method computes number of violations in days (Mo, Tue, Wed,..) considering
328         * each week in the semester separately. Updates the compact infos accordingly.
329         * @param assignment current assignment 
330         * @param constraint instructor constraint
331         * @return current penalty for the given instructor
332         */
333        public double computeLunchPenalty(Assignment<Lecture, Placement> assignment, InstructorConstraint constraint) {
334            double violations = 0d;
335            CompactInfo compactInfo = getCompactInfo(constraint);
336            for (int i = 0; i < Constants.NR_DAYS; i++) {
337                int currentLunchStartSlot = Constants.SLOTS_PER_DAY * i + iLunchStart;
338                int currentLunchEndSlot = Constants.SLOTS_PER_DAY * i + iLunchEnd;
339                int semesterViolations = 0;
340                for (BitSet week : getWeeks()) {
341                    int maxBreak = 0;
342                    int currentBreak = 0;
343                    for (int slot = currentLunchStartSlot; slot < currentLunchEndSlot; slot++) {
344                        if (constraint.getContext(assignment).getPlacements(slot, week).isEmpty()) {
345                            currentBreak++;
346                            if (maxBreak < currentBreak) {
347                                maxBreak = currentBreak;
348                            }
349                        } else {
350                            currentBreak = 0;
351                        }
352                    }
353                    if (maxBreak < iLunchLength) {
354                        semesterViolations++;
355                    }
356                }
357                // saving the result in the CompactInfo of the
358                // InstructorConstraint
359                compactInfo.getLunchDayViolations()[i] = semesterViolations;
360                violations += semesterViolations;
361            }
362            return Math.pow(violations, iMultiplier);
363        }
364        
365        /**
366         * Method uses the CompactInfo of the InstructorConstraint and returns the
367         * lunch preference for this constraint. Calculation formula does not use
368         * linear function, the number of violations is multiplied by a power of
369         * iMultiplier.
370         * 
371         * @param instructorConstraint
372         *            the Instructor constraint of an instructor checked for a lunch
373         *            break
374         * @return the lunch preference for this constraint
375         */
376        private double getLunchPreference(Assignment<Lecture, Placement> assignment, InstructorConstraint instructorConstraint) {
377            double violations = 0d;
378            CompactInfo info = getCompactInfo(instructorConstraint);
379            for (int i = 0; i < Constants.NR_DAYS; i++)
380                violations += info.getLunchDayViolations()[i];
381            return Math.pow(violations, iMultiplier); 
382        }
383    }
384    
385    @Override
386    public ValueContext createAssignmentContext(Assignment<Lecture, Placement> assignment) {
387        return new InstructorLunchBreakContext(assignment);
388    }
389}