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