001package org.cpsolver.instructor.constraints; 002 003import java.util.ArrayList; 004import java.util.BitSet; 005import java.util.Collection; 006import java.util.Comparator; 007import java.util.HashSet; 008import java.util.List; 009import java.util.Set; 010import java.util.TreeSet; 011import java.util.regex.Matcher; 012import java.util.regex.Pattern; 013 014import org.cpsolver.coursett.Constants; 015import org.cpsolver.coursett.model.TimeLocation; 016import org.cpsolver.ifs.assignment.Assignment; 017import org.cpsolver.ifs.model.GlobalConstraint; 018import org.cpsolver.ifs.util.ToolBox; 019import org.cpsolver.instructor.model.Instructor; 020import org.cpsolver.instructor.model.Section; 021import org.cpsolver.instructor.model.TeachingAssignment; 022import org.cpsolver.instructor.model.TeachingRequest; 023import org.cpsolver.instructor.model.TeachingRequest.Variable; 024 025/** 026 * Group Constraint. This constraint implements many of the course timetabling group and flexible constraints. 027 * See {@link ConstraintType} for the list, and the appropriate {@link org.cpsolver.coursett.constraint.GroupConstraint.ConstraintType} 028 * or {@link org.cpsolver.coursett.constraint.FlexibleConstraint} constraint for their description. 029 * Because the instructor - class assignments are dynamic, the constraints are implemented by a single group constraint. 030 * 031 * @author Tomáš Müller 032 * @version IFS 1.4 (Instructor Sectioning)<br> 033 * Copyright (C) 2024 Tomáš Müller<br> 034 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 035 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 036 * <br> 037 * This library is free software; you can redistribute it and/or modify 038 * it under the terms of the GNU Lesser General Public License as 039 * published by the Free Software Foundation; either version 3 of the 040 * License, or (at your option) any later version. <br> 041 * <br> 042 * This library is distributed in the hope that it will be useful, but 043 * WITHOUT ANY WARRANTY; without even the implied warranty of 044 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 045 * Lesser General Public License for more details. <br> 046 * <br> 047 * You should have received a copy of the GNU Lesser General Public 048 * License along with this library; if not see 049 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 050 */ 051public class GroupConstraint extends GlobalConstraint<TeachingRequest.Variable, TeachingAssignment> { 052 053 public GroupConstraint() {} 054 055 @Override 056 public void computeConflicts(Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 057 for (Distribution d: value.getInstructor().getDistributions()) { 058 if (!d.isHard()) continue; 059 d.getType().computeConflicts(d, assignment, value, conflicts); 060 } 061 } 062 063 @Override 064 public String getName() { 065 return "Distribution"; 066 } 067 068 @Override 069 public String toString() { 070 return getName(); 071 } 072 073 /** 074 * Wrapper class representing one distribution preference set on an instructor. 075 */ 076 public static class Distribution { 077 GroupConstraint.ConstraintTypeInterface iType; 078 boolean iRequired = false; 079 boolean iProhibited = false; 080 int iPenalty = 0; 081 082 public Distribution(GroupConstraint.ConstraintTypeInterface type, String preference) { 083 iType = type; 084 iRequired = Constants.sPreferenceRequired.equals(preference); 085 iProhibited = Constants.sPreferenceProhibited.equals(preference); 086 iPenalty = Constants.preference2preferenceLevel(preference); 087 } 088 089 public GroupConstraint.ConstraintTypeInterface getType() { return iType; } 090 public int getPenalty() { return iPenalty; } 091 public boolean isRequired() { return iRequired; } 092 public boolean isProhibited() { return iProhibited; } 093 public boolean isHard() { return isRequired() || isProhibited(); } 094 public String getPreference() { 095 if (isRequired()) return Constants.sPreferenceRequired; 096 if (isProhibited()) return Constants.sPreferenceProhibited; 097 return Constants.preferenceLevel2preference(getPenalty()); 098 } 099 public boolean isPositive() { 100 if (isRequired()) return true; 101 if (isProhibited()) return false; 102 return getPenalty() <= 0; 103 } 104 105 @SuppressWarnings("unchecked") 106 public <P> P getParameter() { 107 if (iType instanceof ParametrizedConstraintType) 108 return ((ParametrizedConstraintType<P>)iType).getParameter(); 109 else 110 return null; 111 } 112 } 113 114 /** 115 * Group constraint check interface 116 */ 117 public static interface Check { 118 public void computeConflicts(Distribution distribution, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts); 119 public double getValue(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value); 120 } 121 122 /** 123 * Group constraint check interface for constraints that can be computed on individual class pairs. 124 */ 125 public static abstract class PairCheck implements Check { 126 public abstract boolean isSatisfied(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, Section s1, Section s2); 127 public abstract boolean isViolated(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, Section s1, Section s2); 128 @Override 129 public double getValue(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 130 int ret = 0; 131 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 132 if (value == null) 133 for (TeachingAssignment ta1: assignments) { 134 for (Section s1: ta1.variable().getSections()) { 135 for (TeachingAssignment ta2: instructor.getContext(assignment).getAssignments()) { 136 for (Section s2: ta2.variable().getSections()) { 137 if (s1.equals(s2)) continue; 138 if (distribution.isPositive()) { 139 if (!isSatisfied(distribution, assignment, instructor, s1, s2)) ret ++; 140 } else { 141 if (!isViolated(distribution, assignment, instructor, s1, s2)) ret ++; 142 } 143 } 144 } 145 } 146 } 147 else 148 for (Section s1: value.variable().getSections()) { 149 for (TeachingAssignment ta2: assignments) { 150 for (Section s2: ta2.variable().getSections()) { 151 if (s1.equals(s2)) continue; 152 if (distribution.isPositive()) { 153 if (!isSatisfied(distribution, assignment, instructor, s1, s2)) ret ++; 154 } else { 155 if (!isViolated(distribution, assignment, instructor, s1, s2)) ret ++; 156 } 157 } 158 } 159 } 160 if (ret == 0) return 0.0; 161 int n = assignments.size() + (value == null || assignment.getValue(value.variable()) != null ? 0 : 1); 162 return (2.0 * ret) / (n * (n - 1)); 163 } 164 @Override 165 public void computeConflicts(Distribution distribution, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 166 for (Section s1: value.variable().getSections()) { 167 for (TeachingAssignment ta2: value.getInstructor().getContext(assignment).getAssignments()) { 168 for (Section s2: ta2.variable().getSections()) { 169 if (s1.equals(s2)) continue; 170 if (distribution.isPositive()) { 171 if (!isSatisfied(distribution, assignment, value.getInstructor(), s1, s2)) conflicts.add(ta2); 172 } else { 173 if (!isViolated(distribution, assignment, value.getInstructor(), s1, s2)) conflicts.add(ta2); 174 } 175 } 176 } 177 } 178 } 179 } 180 181 /** 182 * Simplified group constraint check interface for constraints that can be computed on individual class pairs. 183 */ 184 public static abstract class SimpleCheck extends PairCheck { 185 public abstract boolean isSatisfied(Distribution d, Section s1, Section s2); 186 public boolean isViolated(Distribution d, Section s1, Section s2) { return true; } 187 @Override 188 public boolean isSatisfied(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, Section s1, Section s2) { 189 return isSatisfied(distribution, s1, s2); 190 } 191 @Override 192 public boolean isViolated(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, Section s1, Section s2) { 193 return isViolated(distribution, s1, s2); 194 } 195 } 196 197 /** 198 * Simplified group constraint check interface for time-only constraints that can be computed on individual class pairs. 199 */ 200 public static abstract class SimpleTimeCheck extends SimpleCheck { 201 public abstract boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2); 202 public boolean isViolated(Distribution d, TimeLocation t1, TimeLocation t2) { return true; } 203 @Override 204 public boolean isSatisfied(Distribution d, Section s1, Section s2) { 205 if (s1.getTime() == null || s2.getTime() == null) return true; 206 return isSatisfied(d, s1.getTime(), s2.getTime()); 207 } 208 @Override 209 public boolean isViolated(Distribution d, Section s1, Section s2) { 210 if (s1.getTime() == null || s2.getTime() == null) return true; 211 return isViolated(d, s1.getTime(), s2.getTime()); 212 } 213 } 214 215 /** 216 * Factory class for group constraints with parameters 217 */ 218 public static interface ConstraintCreator<P> { 219 public ParametrizedConstraintType<P> create(String reference, String referenceRegExp); 220 } 221 222 /** 223 * Interface for a distribution constraint type, including its implementation 224 */ 225 public static interface ConstraintTypeInterface extends Check { 226 public ConstraintType type(); 227 public String reference(); 228 public String getName(); 229 } 230 231 /** 232 * Distribution constraint with parameters. 233 */ 234 public static class ParametrizedConstraintType<P> implements ConstraintTypeInterface { 235 private String iReference; 236 private ConstraintType iType; 237 private P iParameter; 238 private String iName; 239 240 public ParametrizedConstraintType(ConstraintType type, P parameter, String reference) { 241 iType = type; iParameter = parameter; iReference = reference; 242 } 243 244 public ParametrizedConstraintType<P> setName(String name) { iName = name; return this; } 245 246 @Override 247 public double getValue(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 248 return iType.check().getValue(distribution, assignment, instructor, value); 249 } 250 251 @Override 252 public void computeConflicts(Distribution distribution, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 253 iType.check().computeConflicts(distribution, assignment, value, conflicts); 254 } 255 256 public P getParameter() { return iParameter; } 257 @Override 258 public ConstraintType type() { return iType; } 259 @Override 260 public String reference() { return iReference; } 261 @Override 262 public String getName() { return (iName == null ? iType.getName() : iName); } 263 } 264 265 /** 266 * Distribution types and their implementation 267 */ 268 public static enum ConstraintType implements ConstraintTypeInterface { 269 /** 270 * Same Days: Given classes must be taught on the same days. In case of classes of different time patterns, a class 271 * with fewer meetings must meet on a subset of the days used by the class with more meetings. For example, if one 272 * class pattern is 3x50, all others given in the constraint can only be taught on Monday, Wednesday, or Friday. 273 * For a 2x100 class MW, MF, WF is allowed but TTh is prohibited.<BR> 274 * When prohibited or (strongly) discouraged: any pair of classes classes cannot be taught on the same days (cannot 275 * overlap in days). For instance, if one class is MFW, the second has to be TTh. 276 */ 277 SAME_DAYS("SAME_DAYS", "Same Days", new SimpleTimeCheck() { 278 @Override 279 public boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2) { 280 return sameDays(t1.getDaysArray(), t2.getDaysArray()); 281 } 282 @Override 283 public boolean isViolated(Distribution d, TimeLocation t1, TimeLocation t2) { 284 return !t1.shareDays(t2); 285 } 286 private boolean sameDays(int[] days1, int[] days2) { 287 if (days2.length < days1.length) 288 return sameDays(days2, days1); 289 int i2 = 0; 290 for (int i1 = 0; i1 < days1.length; i1++) { 291 int d1 = days1[i1]; 292 while (true) { 293 if (i2 == days2.length) 294 return false; 295 int d2 = days2[i2]; 296 if (d1 == d2) 297 break; 298 i2++; 299 if (i2 == days2.length) 300 return false; 301 } 302 i2++; 303 } 304 return true; 305 } 306 }), 307 /** 308 * Same Start Time: Given classes must start during the same half-hour period of a day (independent of the actual 309 * day the classes meet). For instance, MW 7:30 is compatible with TTh 7:30 but not with MWF 8:00.<BR> 310 * When prohibited or (strongly) discouraged: any pair of classes in the given constraint cannot start during the 311 * same half-hour period of any day of the week. 312 */ 313 SAME_START("SAME_START", "Same Start Time", new SimpleTimeCheck() { 314 @Override 315 public boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2) { 316 return (t1.getStartSlot() % Constants.SLOTS_PER_DAY) == (t2.getStartSlot() % Constants.SLOTS_PER_DAY); 317 } 318 @Override 319 public boolean isViolated(Distribution d, TimeLocation t1, TimeLocation t2) { 320 return (t1.getStartSlot() % Constants.SLOTS_PER_DAY) != (t2.getStartSlot() % Constants.SLOTS_PER_DAY); 321 }}), 322 /** 323 * Same Room: Given classes must be taught in the same room.<BR> 324 * When prohibited or (strongly) discouraged: any pair of classes in the constraint cannot be taught in the same room. 325 */ 326 SAME_ROOM("SAME_ROOM", "Same Room", new SimpleCheck() { 327 @Override 328 public boolean isSatisfied(Distribution d, Section s1, Section s2) { 329 return s1.isSameRoom(s2); 330 } 331 @Override 332 public boolean isViolated(Distribution d, Section s1, Section s2) { 333 return !s1.isSameRoom(s2); 334 }}), 335 /** 336 * At Most X Hours A Day: Classes are to be placed in a way that there is no more than given number of hours in any day. 337 */ 338 MAX_HRS_DAY("MAX_HRS_DAY\\(([0-9\\.]+)\\)", "At Most N Hours A Day", new Check() { 339 protected int nrSlotsADay(Set<TeachingAssignment> assignments, BitSet week, int dayCode, TeachingRequest.Variable variable, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 340 Set<Integer> slots = new HashSet<Integer>(); 341 for (TeachingAssignment ta: assignments) { 342 if (variable != null && variable.equals(ta.variable())) continue; 343 if (conflicts != null && conflicts.contains(ta)) continue; 344 for (Section section: ta.variable().getSections()) { 345 TimeLocation t = section.getTime(); 346 if (t == null || (t.getDayCode() & dayCode) == 0 || (week != null && !t.shareWeeks(week))) continue; 347 for (int i = 0; i < t.getLength(); i++) 348 slots.add(i + t.getStartSlot()); 349 } 350 } 351 if (value != null) { 352 for (Section section: value.variable().getSections()) { 353 TimeLocation t = section.getTime(); 354 if (t == null || (t.getDayCode() & dayCode) == 0 || (week != null && !t.shareWeeks(week))) continue; 355 for (int i = 0; i < t.getLength(); i++) 356 slots.add(i + t.getStartSlot()); 357 } 358 } 359 return slots.size(); 360 } 361 @Override 362 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 363 Integer max = d.getParameter(); 364 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 365 double over = 0.0; 366 for (int dayCode: Constants.DAY_CODES) { 367 for (BitSet week: instructor.getModel().getWeeks()) { 368 if (value == null) { 369 over += Math.max(0, nrSlotsADay(assignments, week, dayCode, null, null, null) - max); 370 } else { 371 int before = Math.max(0, nrSlotsADay(assignments, week, dayCode, value.variable(), null, null) - max); 372 int after = Math.max(0, nrSlotsADay(assignments, week, dayCode, value.variable(), value, null) - max); 373 over += after - before; 374 } 375 } 376 } 377 return over/ (60.0 * instructor.getModel().getWeeks().size()); 378 } 379 @Override 380 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 381 Integer max = d.getParameter(); 382 Set<TeachingAssignment> assignments = value.getInstructor().getContext(assignment).getAssignments(); 383 for (int dayCode: Constants.DAY_CODES) { 384 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 385 if (nrSlotsADay(assignments, week, dayCode, value.variable(), value, conflicts) > max) { 386 List<TeachingAssignment> adepts = new ArrayList<TeachingAssignment>(); 387 for (TeachingAssignment ta: assignments) { 388 if (ta.variable().equals(value.variable())) continue; 389 if (conflicts.contains(ta)) continue; 390 boolean hasDate = false; 391 for (Section section: ta.variable().getSections()) { 392 TimeLocation t = section.getTime(); 393 if (t != null && (t.getDayCode() & dayCode) != 0 && t.shareWeeks(week)) { 394 hasDate = true; 395 break; 396 } 397 } 398 if (hasDate) adepts.add(ta); 399 } 400 do { 401 if (adepts.isEmpty()) { conflicts.add(value); break; } 402 TeachingAssignment conflict = ToolBox.random(adepts); 403 adepts.remove(conflict); 404 conflicts.add(conflict); 405 } while (nrSlotsADay(assignments, week, dayCode, value.variable(), value, conflicts) > max); 406 } 407 } 408 } 409 }}, new ConstraintCreator<Integer>() { 410 @Override 411 public ParametrizedConstraintType<Integer> create(String reference, String regexp) { 412 Matcher matcher = Pattern.compile(regexp).matcher(reference); 413 if (matcher.find()) { 414 double hours = Double.parseDouble(matcher.group(1)); 415 int slots = (int)Math.round(12.0 * hours); 416 return new ParametrizedConstraintType<Integer>(ConstraintType.MAX_HRS_DAY, slots, reference) 417 .setName("At Most " + matcher.group(1) + " Hours A Day"); 418 } 419 return null; 420 }}), 421 /** 422 * Given classes must be taught during the same weeks (i.e., must have the same date pattern).<br> 423 * When prohibited or (strongly) discouraged: any two classes must have non overlapping date patterns. 424 */ 425 SAME_WEEKS("SAME_WEEKS", "Same Weeks", new SimpleTimeCheck() { 426 @Override 427 public boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2) { 428 return t1.getWeekCode().equals(t2.getWeekCode()); 429 } 430 @Override 431 public boolean isViolated(Distribution d, TimeLocation t1, TimeLocation t2) { 432 return !t1.shareWeeks(t2); 433 }}), 434 /** 435 * Work Day: Classes are to be placed in a way that there is no more than given number of hours between the start of the first class and the end of the class one on any day. 436 */ 437 WORKDAY("WORKDAY\\(([0-9\\.]+)\\)", "Work Day", new SimpleTimeCheck() { 438 @Override 439 public boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2) { 440 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return true; 441 Integer parameter = d.getParameter(); 442 return Math.max(t1.getStartSlot() + t1.getLength(), t2.getStartSlot() + t2.getLength()) - Math.min(t1.getStartSlot(), t2.getStartSlot()) <= parameter; 443 }}, new ConstraintCreator<Integer>() { 444 @Override 445 public ParametrizedConstraintType<Integer> create(String reference, String regexp) { 446 Matcher matcher = Pattern.compile(regexp).matcher(reference); 447 if (matcher.find()) { 448 double hours = Double.parseDouble(matcher.group(1)); 449 int slots = (int)Math.round(12.0 * hours); 450 return new ParametrizedConstraintType<Integer>(ConstraintType.WORKDAY, slots, reference) 451 .setName(matcher.group(1) + " Hour Work Day"); 452 } 453 return null; 454 }}), 455 /** 456 * Minimal gap between classes. 457 */ 458 MIN_GAP("MIN_GAP\\(([0-9\\.]+)\\)", "Mininal Gap Between Classes", new SimpleTimeCheck() { 459 @Override 460 public boolean isSatisfied(Distribution d, TimeLocation t1, TimeLocation t2) { 461 if (t1 == null || t2 == null || !t1.shareDays(t2) || !t1.shareWeeks(t2)) return true; 462 Integer parameter = d.getParameter(); 463 return t1.getStartSlot() + t1.getLength() + parameter <= t2.getStartSlot() || 464 t2.getStartSlot() + t2.getLength() + parameter <= t1.getStartSlot(); 465 }}, new ConstraintCreator<Integer>() { 466 @Override 467 public ParametrizedConstraintType<Integer> create(String reference, String regexp) { 468 Matcher matcher = Pattern.compile(regexp).matcher(reference); 469 if (matcher.find()) { 470 double hours = Double.parseDouble(matcher.group(1)); 471 int slots = (int)Math.round(12.0 * hours); 472 return new ParametrizedConstraintType<Integer>(ConstraintType.MIN_GAP, slots, reference) 473 .setName("At Least " + matcher.group(1) + " Hours Between Classes"); 474 } 475 return null; 476 }}), 477 /** 478 * Given classes must be taught in a way there is a break between two blocks of classes. 479 */ 480 MAX_BLOCK("_(MaxBlock):([0-9]+):([0-9]+)_", "Max Block", new Check() { 481 @Override 482 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 483 int [] params = d.getParameter(); 484 int maxBlockSlotsBTB = params[0]; 485 int maxBreakBetweenBTB = params[1]; 486 double penalty = 0; 487 // constraint is checked for every day in week 488 for (int dayCode : Constants.DAY_CODES) { 489 // constraint is checked for every week in semester (or for the whole semester) 490 for (BitSet week : instructor.getModel().getWeeks()) { 491 // each blocks contains placements which are BTB 492 List<Block> blocks = getBlocks(assignment, instructor, dayCode, null, (value == null ? null : value.variable()), value, week, maxBreakBetweenBTB); 493 for (Block block : blocks) { 494 // ignore single-start/signle-class blocks 495 if (block.getNbrPlacements() == 1 || block.haveSameStartTime()) continue; 496 // violated if there is a block containing more than one 497 // class longer than maxBlockSlotsBTB 498 if (block.getLengthInSlots() > maxBlockSlotsBTB) { 499 int blockLengthPenalty = block.getLengthInSlots() / maxBlockSlotsBTB; 500 penalty += blockLengthPenalty; 501 } 502 } 503 } 504 } 505 return penalty / (5.0 * instructor.getModel().getWeeks().size()); 506 } 507 @Override 508 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 509 int [] params = d.getParameter(); 510 int maxBlockSlotsBTB = params[0]; 511 int maxBreakBetweenBTB = params[1]; 512 List<BitSet> weeks = value.getInstructor().getModel().getWeeks(); 513 // constraint is checked for every day in week 514 for (int dayCode : Constants.DAY_CODES) { 515 // constraint is checked for every week in semester (or for the whole semester) 516 for (BitSet week : weeks) { 517 boolean isProblem = false; 518 do { 519 isProblem = false; 520 // each blocks contains placements which are BTB 521 List<Block> blocks = getBlocks(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week, maxBreakBetweenBTB); 522 for (Block block : blocks) { 523 // if the block is not affected by the current placement, continue 524 if (!block.contains(value)) continue; 525 Set<TeachingAssignment> adepts = new HashSet<TeachingAssignment>(); 526 // if there is only 1 placement in block, the block cannot be shortened 527 // if placements of a block start at the same time, they intersect 528 // this solves problem when between 2 classes is required MEET_TOGETHER 529 if (block.getNbrPlacements() == 1 || block.haveSameStartTime()) continue; 530 // if block is longer than maximum size, some of its placements are conflicts 531 if (block.getLengthInSlots() > maxBlockSlotsBTB) { 532 // classes from block are potential conflicts 533 for (TeachingAssignmentSection tas: block.getSections()) 534 adepts.add(tas.getTeachingAssignment()); 535 // do not set currently assigned value as conflict 536 adepts.remove(value); 537 isProblem = true; 538 // pick random placement 539 TeachingAssignment conflict = ToolBox.random(adepts); 540 if (conflict != null) { 541 conflicts.add(conflict); 542 } 543 } 544 } 545 } while (isProblem); 546 } 547 } 548 }}, new ConstraintCreator<int[]>() { 549 @Override 550 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 551 Matcher matcher = Pattern.compile(regexp).matcher(reference); 552 if (matcher.find()) { 553 int maxBlockSlotsBTB = Integer.parseInt(matcher.group(2))/Constants.SLOT_LENGTH_MIN; 554 int maxBreakBetweenBTB = Integer.parseInt(matcher.group(3))/Constants.SLOT_LENGTH_MIN; 555 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_BLOCK, new int[] {maxBlockSlotsBTB, maxBreakBetweenBTB}, reference) 556 .setName("Max " + (maxBlockSlotsBTB / 12.0) + "h Blocks"); 557 } 558 return null; 559 }}), 560 /** 561 * Limit number of breaks between adjacent classes on a day. 562 */ 563 MAX_BREAKS("_(MaxBreaks):([0-9]+):([0-9]+)_", "Max Breaks", new Check() { 564 @Override 565 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 566 int [] params = d.getParameter(); 567 int maxBlocksOnADay = params[0]; 568 int maxBreakBetweenBTB = params[1]; 569 double penalty = 0; 570 // constraint is checked for every day in week 571 for (int dayCode : Constants.DAY_CODES) { 572 // constraint is checked for every week in semester (or for the whole semester) 573 for (BitSet week : instructor.getModel().getWeeks()) { 574 // each blocks contains placements which are BTB 575 List<Block> blocks = getBlocks(assignment, instructor, dayCode, null, (value == null ? null : value.variable()), value, week, maxBreakBetweenBTB); 576 // too many blocks -> increase penalty 577 if (blocks.size() > maxBlocksOnADay) 578 penalty += (blocks.size() - maxBlocksOnADay); 579 } 580 } 581 return penalty / (5.0 * instructor.getModel().getWeeks().size()); 582 } 583 @Override 584 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 585 int [] params = d.getParameter(); 586 int maxBlocksOnADay = params[0]; 587 int maxBreakBetweenBTB = params[1]; 588 List<BitSet> weeks = value.getInstructor().getModel().getWeeks(); 589 // constraint is checked for every day in week 590 for (int dayCode : Constants.DAY_CODES) { 591 // constraint is checked for every week in semester (or for the whole semester) 592 for (BitSet week : weeks) { 593 // each blocks contains placements which are BTB 594 List<Block> blocks = getBlocks(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week, maxBreakBetweenBTB); 595 while (blocks.size() > maxBlocksOnADay) { 596 // too many blocks -> identify adepts for unassignment 597 List<Block> adepts = new ArrayList<Block>(); int size = 0; 598 for (Block block: blocks) { 599 if (block.contains(value)) continue; // skip block containing given value 600 // select adepts of the smallest size 601 if (adepts.isEmpty() || size > block.getSections().size()) { 602 adepts.clear(); 603 adepts.add(block); 604 size = block.getSections().size(); 605 } else if (size == block.getSections().size()) { 606 adepts.add(block); 607 } 608 } 609 // pick one randomly 610 Block block = ToolBox.random(adepts); 611 blocks.remove(block); 612 for (TeachingAssignmentSection tas: block.getSections()) 613 if (tas.getTeachingAssignment().equals(assignment.getValue(tas.getTeachingAssignment().variable()))) 614 conflicts.add(tas.getTeachingAssignment()); 615 } 616 } 617 } 618 }}, new ConstraintCreator<int[]>() { 619 @Override 620 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 621 Matcher matcher = Pattern.compile(regexp).matcher(reference); 622 if (matcher.find()) { 623 int maxBlocksOnADay = 1 + Integer.parseInt(matcher.group(2)); 624 int maxBreakBetweenBTB = Integer.parseInt(matcher.group(3)) / Constants.SLOT_LENGTH_MIN; 625 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_BREAKS, new int[] {maxBlocksOnADay, maxBreakBetweenBTB}, reference) 626 .setName(maxBlocksOnADay == 1 ? "No Break" : maxBlocksOnADay == 2 ? "Max 1 Break" : "Max " + (maxBlocksOnADay - 1) + " Breaks"); 627 } 628 return null; 629 }}), 630 /** 631 * Limit number of days of a week. 632 */ 633 MAX_DAYS("_(MaxDays):([0-9]+)_", "Max Days", new Check() { 634 protected boolean hasDay(BitSet week, int dayOfWeek, Section section) { 635 if (section.getTime() == null || !section.getTime().getWeekCode().intersects(week)) return false; 636 return (section.getTime().getDayCode() & Constants.DAY_CODES[dayOfWeek]) != 0; 637 } 638 protected boolean hasDay(BitSet week, int dayOfWeek, TeachingAssignment ta) { 639 for (Section section: ta.variable().getSections()) 640 if (hasDay(week, dayOfWeek, section)) return true; 641 return false; 642 } 643 @Override 644 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 645 Integer maxDays = d.getParameter(); 646 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 647 double penalty = 0; 648 for (BitSet week: instructor.getModel().getWeeks()) { 649 Set<Integer> days = new HashSet<Integer>(); 650 for (TeachingAssignment ta: assignments) { 651 if (value != null && value.variable().equals(ta.variable())) continue; 652 for (int day = 0; day < Constants.DAY_CODES.length; day++) 653 if (hasDay(week, day, ta)) 654 days.add(day); 655 } 656 if (value != null) { 657 for (int day = 0; day < Constants.DAY_CODES.length; day++) 658 if (hasDay(week, day, value) && days.add(day) && days.size() > maxDays) 659 penalty += 1.0; 660 } else { 661 penalty += Math.max(0, days.size() - maxDays); 662 } 663 } 664 return penalty / (Math.max(1.0, 5.0 - maxDays) * instructor.getModel().getWeeks().size()); 665 } 666 @Override 667 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 668 Integer maxDays = d.getParameter(); 669 Set<TeachingAssignment> assignments = value.getInstructor().getContext(assignment).getAssignments(); 670 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 671 Set<Integer> selectedDays = new HashSet<Integer>(); 672 for (int day = 0; day < Constants.DAY_CODES.length; day++) 673 if (hasDay(week, day, value)) 674 selectedDays.add(day); 675 // selected value has no days -> next week 676 if (selectedDays.isEmpty()) continue; 677 // selected value is over -> it cannot be assigned 678 if (selectedDays.size() > maxDays) { 679 conflicts.add(value); continue; 680 } 681 // check other days 682 while (true) { 683 Set<Integer> otherDays = new HashSet<Integer>(); 684 for (TeachingAssignment ta: assignments) { 685 if (value != null && value.variable().equals(ta.variable())) continue; 686 if (conflicts.contains(ta)) continue; 687 for (int day = 0; day < Constants.DAY_CODES.length; day++) 688 if (!selectedDays.contains(day) && hasDay(week, day, ta)) 689 otherDays.add(day); 690 } 691 if (otherDays.size() + selectedDays.size() <= maxDays) break; 692 int day = ToolBox.random(otherDays); 693 for (TeachingAssignment ta: assignments) { 694 if (value != null && value.variable().equals(ta.variable())) continue; 695 if (conflicts.contains(ta)) continue; 696 if (hasDay(week, day, ta)) 697 conflicts.add(ta); 698 } 699 } 700 } 701 }}, new ConstraintCreator<Integer>() { 702 @Override 703 public ParametrizedConstraintType<Integer> create(String reference, String regexp) { 704 Matcher matcher = Pattern.compile(regexp).matcher(reference); 705 if (matcher.find()) { 706 int maxDays = Integer.parseInt(matcher.group(2)); 707 return new ParametrizedConstraintType<Integer>(ConstraintType.MAX_DAYS, maxDays, reference) 708 .setName(maxDays == 1 ? "Max 1 Day" : "Max " + maxDays + " Days"); 709 } 710 return null; 711 }}), 712 /** 713 * There must be a break of a given length in a given time interval. 714 */ 715 BREAK("_(Break):([0-9]+):([0-9]+):([0-9]+)_", "Break", new Check() { 716 @Override 717 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 718 int [] params = d.getParameter(); 719 int breakStart = params[0]; 720 int breakEnd = params[1]; 721 int breakLength = params[2]; 722 double penalty = 0; 723 // constraint is checked for every day in week 724 for (int dayCode : Constants.DAY_CODES) { 725 // constraint is checked for every week in semester (or for the whole semester) 726 for (BitSet week : instructor.getModel().getWeeks()) { 727 // each blocks contains placements which are BTB 728 List<Block> blocks = getBlocks(assignment, instructor, dayCode, null, (value == null ? null : value.variable()) ,value, week, breakLength); 729 // too many blocks -> increase penalty 730 List<Block> matchingBlocks = new ArrayList<Block>(); 731 for(Block block: blocks) { 732 // if block intersects with break interval, it will be used in conflict selection 733 if (block.getStartSlotCurrentBlock() <= breakEnd && block.getEndSlotCurrentBlock() >= breakStart) 734 matchingBlocks.add(block); 735 } 736 int size = matchingBlocks.size(); 737 if (size == 1) { 738 Block block = matchingBlocks.get(0); 739 // the matching block does not contain the given placement -> ignore 740 if (value != null && !block.contains(value)) continue; 741 // check whether the block leaves enough space for break 742 if (block.getStartSlotCurrentBlock() - breakStart >= breakLength || breakEnd - block.getEndSlotCurrentBlock() >= breakLength) 743 continue; 744 // if it doesn't 745 penalty += 1.0; 746 } 747 } 748 } 749 return penalty / (5.0 * instructor.getModel().getWeeks().size()); 750 } 751 @Override 752 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 753 int [] params = d.getParameter(); 754 int breakStart = params[0]; 755 int breakEnd = params[1]; 756 int breakLength = params[2]; 757 // constraint is checked for every day in week 758 for (int dayCode : Constants.DAY_CODES) { 759 // constraint is checked for every week in semester (or for the whole semester) 760 for (BitSet week : value.getInstructor().getModel().getWeeks()) { 761 while (true) { 762 // each blocks contains placements which are BTB 763 List<Block> blocks = getBlocks(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week, breakLength); 764 // too many blocks -> increase penalty 765 List<Block> matchingBlocks = new ArrayList<Block>(); 766 for(Block block: blocks) { 767 // if block intersects with break interval, it will be used in conflict selection 768 if (block.getStartSlotCurrentBlock() <= breakEnd && block.getEndSlotCurrentBlock() >= breakStart) 769 matchingBlocks.add(block); 770 } 771 int size = matchingBlocks.size(); 772 if (size == 1) { 773 Block block = matchingBlocks.get(0); 774 // the matching block does not contain the given placement -> ignore 775 if (!block.contains(value)) break; 776 // check whether the block leaves enough space for break 777 if (block.getStartSlotCurrentBlock() - breakStart >= breakLength || breakEnd - block.getEndSlotCurrentBlock() >= breakLength) 778 break; 779 // if it doesn't 780 List<TeachingAssignmentSection> adepts = new ArrayList<TeachingAssignmentSection>(); 781 // every placement intersecting with break interval might be potential conflict 782 for (TeachingAssignmentSection p: block.getSections()) 783 if (p.getTime().getStartSlot() <= breakEnd && p.getTime().getStartSlot()+ p.getTime().getLength() >= breakStart) 784 adepts.add(p); 785 if (adepts.size() > 0) { 786 conflicts.add(ToolBox.random(adepts).getTeachingAssignment()); 787 continue; 788 } 789 break; 790 } 791 } 792 } 793 } 794 }}, new ConstraintCreator<int[]>() { 795 @Override 796 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 797 Matcher matcher = Pattern.compile(regexp).matcher(reference); 798 if (matcher.find()) { 799 int breakStart = Integer.parseInt(matcher.group(2)); 800 int breakEnd = Integer.parseInt(matcher.group(3)); 801 int breakLength = Integer.parseInt(matcher.group(4))/Constants.SLOT_LENGTH_MIN; 802 return new ParametrizedConstraintType<int[]>(ConstraintType.BREAK, new int[] {breakStart, breakEnd, breakLength}, reference) 803 .setName((breakEnd * 5) + " Min Break"); 804 } 805 return null; 806 }}), 807 /** 808 * Limit number of weeks on which an a class can take place. 809 */ 810 MAX_WEEKS("_(MaxWeeks):([0-9]+):([0-9]+)_", "Max Weeks", new Check() { 811 @Override 812 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 813 int [] params = d.getParameter(); 814 int maxWeeks = params[0]; 815 int dayCode = params[1]; 816 if (value != null && !isCorectDayOfWeek(value, dayCode)) return 0.0; 817 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 818 double penalty = 0; 819 Set<BitSet> weeks = new HashSet<BitSet>(); 820 for (BitSet week: instructor.getModel().getWeeks()) { 821 for (TeachingAssignment ta: assignments) { 822 if (value != null && value.variable().equals(ta.variable())) continue; 823 if (isCorectDayAndWeek(ta, dayCode, week)) 824 weeks.add(week); 825 } 826 } 827 if (value != null) { 828 for (BitSet week: instructor.getModel().getWeeks()) { 829 if (isCorectDayAndWeek(value, dayCode, week) && weeks.add(week) && weeks.size() > maxWeeks) 830 penalty += 1.0; 831 } 832 } else { 833 penalty += Math.max(0.0, weeks.size() - maxWeeks); 834 } 835 return penalty / Math.max(1.0, instructor.getModel().getWeeks().size() - maxWeeks); 836 } 837 @Override 838 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 839 int [] params = d.getParameter(); 840 int maxWeeks = params[0]; 841 int dayCode = params[1]; 842 if (!isCorectDayOfWeek(value, dayCode)) return; 843 844 Set<BitSet> selectedWeeks = new HashSet<BitSet>(); 845 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 846 if (isCorectDayAndWeek(value, dayCode, week)) 847 selectedWeeks.add(week); 848 } 849 if (selectedWeeks.size() > maxWeeks) { 850 conflicts.add(value); 851 return; 852 } 853 Set<TeachingAssignment> assignments = value.getInstructor().getContext(assignment).getAssignments(); 854 while (true) { 855 Set<BitSet> otherWeeks = new HashSet<BitSet>(); 856 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 857 if (selectedWeeks.contains(week)) continue; 858 for (TeachingAssignment ta: assignments) { 859 if (value != null && value.variable().equals(ta.variable())) continue; 860 if (conflicts.contains(ta)) continue; 861 if (isCorectDayAndWeek(ta, dayCode, week)) 862 otherWeeks.add(week); 863 } 864 } 865 if (otherWeeks.size() + selectedWeeks.size() <= maxWeeks) break; 866 BitSet week = ToolBox.random(otherWeeks); 867 for (TeachingAssignment ta: assignments) { 868 if (value != null && value.variable().equals(ta.variable())) continue; 869 if (conflicts.contains(ta)) continue; 870 if (isCorectDayAndWeek(ta, dayCode, week)) { 871 conflicts.add(ta); 872 break; 873 } 874 } 875 } 876 }}, new ConstraintCreator<int[]>() { 877 @Override 878 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 879 Matcher matcher = Pattern.compile(regexp).matcher(reference); 880 if (matcher.find()) { 881 int maxWeeks = Integer.parseInt(matcher.group(2)); 882 int dayCode = Integer.parseInt(matcher.group(3)); 883 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_WEEKS, new int[] {maxWeeks, dayCode}, reference) 884 .setName("Max " + maxWeeks + " Weeks"); 885 } 886 return null; 887 }}), 888 /** 889 * Minimize free time of an instructor during a day (between the first and the last class). 890 */ 891 MAX_HOLES("_(MaxHoles):([0-9]+)_", "Max Holes", new Check() { 892 int countHoles(Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor,int dayCode, Set<TeachingAssignment> conflicts, TeachingRequest.Variable variable, TeachingAssignment value, BitSet week) { 893 Set<TeachingAssignmentSection> placements = getRelevantPlacements(assignment, instructor, dayCode, conflicts, variable, value, week); 894 int lastSlot = -1; 895 int holes = 0; 896 for (TeachingAssignmentSection placement: placements) { 897 if (lastSlot >= 0 && placement.getTime().getStartSlot() > lastSlot) { 898 holes += (placement.getTime().getStartSlot() - lastSlot); 899 } 900 lastSlot = Math.max(lastSlot, placement.getTime().getStartSlot() + placement.getTime().getLength()); 901 } 902 return holes; 903 } 904 905 @Override 906 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 907 int [] params = d.getParameter(); 908 int maxHolesOnADay = params[0]; 909 double penalty = 0; 910 for (int dayCode : Constants.DAY_CODES) { 911 for (BitSet week: instructor.getModel().getWeeks()) { 912 if (value != null && !isCorectDayAndWeek(value, dayCode, week)) continue; 913 if (value == null) { 914 penalty += Math.max(0, countHoles(assignment, instructor, dayCode, null, null, null, week) - maxHolesOnADay); 915 } else { 916 int before = Math.max(0, countHoles(assignment, instructor, dayCode, null, value.variable(), null, week) - maxHolesOnADay); 917 int after = Math.max(0, countHoles(assignment, instructor, dayCode, null, value.variable(), value, week) - maxHolesOnADay); 918 penalty += after - before; 919 } 920 } 921 } 922 return penalty / (60.0 * instructor.getModel().getWeeks().size()); 923 } 924 @Override 925 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 926 int [] params = d.getParameter(); 927 int maxHolesOnADay = params[0]; 928 for (int dayCode : Constants.DAY_CODES) { 929 for (BitSet week : value.getInstructor().getModel().getWeeks()) { 930 if (!isCorectDayAndWeek(value, dayCode, week)) continue; 931 int penalty = countHoles(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week); 932 while (penalty > maxHolesOnADay) { 933 List<TeachingAssignmentSection> adepts = new ArrayList<TeachingAssignmentSection>(); 934 for (TeachingAssignmentSection placement: getRelevantPlacements(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week)) { 935 if (placement.getTeachingAssignment().equals(value)) continue; // skip given value 936 // check if removing placement would improve the penalty 937 Set<TeachingAssignment> test = new HashSet<TeachingAssignment>(conflicts); test.add(placement.getTeachingAssignment()); 938 int newPenalty = countHoles(assignment, value.getInstructor(), dayCode, test, value.variable(), value, week); 939 if (newPenalty <= penalty) 940 adepts.add(placement); 941 } 942 943 // no adepts -> fail 944 if (adepts.isEmpty()) { 945 conflicts.add(value); return; 946 } 947 948 // pick one randomly 949 conflicts.add(ToolBox.random(adepts).getTeachingAssignment()); 950 penalty = countHoles(assignment, value.getInstructor(), dayCode, conflicts, value.variable(), value, week); 951 } 952 } 953 } 954 }}, new ConstraintCreator<int[]>() { 955 @Override 956 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 957 Matcher matcher = Pattern.compile(regexp).matcher(reference); 958 if (matcher.find()) { 959 int maxHolesOnADay = Integer.parseInt(matcher.group(2)) / Constants.SLOT_LENGTH_MIN; 960 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_HOLES, new int[] {maxHolesOnADay}, reference) 961 .setName("Max " + (maxHolesOnADay/12.0) + " Free Hours"); 962 } 963 return null; 964 }}), 965 /** 966 * Limit number of half-days of a week. 967 */ 968 MAX_HALF_DAYS("_(MaxHalfDays):([0-9]+)_", "Max Half-Days", new Check() { 969 private Integer iNoonSlot = null; 970 protected boolean hasHalfDay(BitSet week, int dayOfWeek, Section section, boolean morning) { 971 if (section.getTime() == null || !section.getTime().getWeekCode().intersects(week)) return false; 972 if ((section.getTime().getDayCode() & Constants.DAY_CODES[dayOfWeek]) == 0) return false; 973 if (morning) 974 return section.getTime().getStartSlot() < iNoonSlot; 975 else 976 return section.getTime().getStartSlot() >= iNoonSlot; 977 } 978 protected boolean hasHalfDay(BitSet week, int dayOfWeek, TeachingAssignment ta, boolean morning) { 979 for (Section section: ta.variable().getSections()) 980 if (hasHalfDay(week, dayOfWeek, section, morning)) return true; 981 return false; 982 } 983 984 @Override 985 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 986 if (iNoonSlot == null) 987 iNoonSlot = instructor.getModel().getProperties().getPropertyInteger("General.HalfDaySlot", 144); 988 int [] params = d.getParameter(); 989 int maxHalfDays = params[0]; 990 double penalty = 0; 991 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 992 for (BitSet week: instructor.getModel().getWeeks()) { 993 Set<Integer> mornings = new HashSet<Integer>(); 994 Set<Integer> afternoons = new HashSet<Integer>(); 995 for (TeachingAssignment ta: assignments) { 996 if (value != null && value.variable().equals(ta.variable())) continue; 997 for (int day = 0; day < Constants.DAY_CODES.length; day++) { 998 if (hasHalfDay(week, day, ta, true)) 999 mornings.add(day); 1000 if (hasHalfDay(week, day, ta, false)) 1001 afternoons.add(day); 1002 } 1003 } 1004 if (value != null) { 1005 for (int day = 0; day < Constants.DAY_CODES.length; day++) { 1006 if (hasHalfDay(week, day, value, true) && mornings.add(day) && (mornings.size() + afternoons.size()) > maxHalfDays) 1007 penalty += 1.0; 1008 if (hasHalfDay(week, day, value, false) && afternoons.add(day) && (mornings.size() + afternoons.size()) > maxHalfDays) 1009 penalty += 1.0; 1010 } 1011 } else { 1012 penalty += Math.max(0, mornings.size() + afternoons.size() - maxHalfDays); 1013 } 1014 } 1015 return penalty / (Math.max(1.0, 10.0 - maxHalfDays) * instructor.getModel().getWeeks().size()); 1016 } 1017 @Override 1018 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 1019 if (iNoonSlot == null) 1020 iNoonSlot = value.getInstructor().getModel().getProperties().getPropertyInteger("General.HalfDaySlot", 144); 1021 int [] params = d.getParameter(); 1022 int maxHalfDays = params[0]; 1023 Set<TeachingAssignment> assignments = value.getInstructor().getContext(assignment).getAssignments(); 1024 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 1025 Set<Integer> selectedHalfDays = new HashSet<Integer>(); 1026 for (int day = 0; day < Constants.DAY_CODES.length; day++) { 1027 if (hasHalfDay(week, day, value, true)) 1028 selectedHalfDays.add(2 * day); 1029 if (hasHalfDay(week, day, value, false)) 1030 selectedHalfDays.add(2 * day + 1); 1031 } 1032 // selected value has no days -> next week 1033 if (selectedHalfDays.size() == 0) continue; 1034 // selected value is over -> it cannot be assigned 1035 if (selectedHalfDays.size() > maxHalfDays) { 1036 conflicts.add(value); continue; 1037 } 1038 // check other days 1039 while (true) { 1040 Set<Integer> otherHalfDays = new HashSet<Integer>(); 1041 for (TeachingAssignment ta: assignments) { 1042 if (value != null && value.variable().equals(ta.variable())) continue; 1043 if (conflicts.contains(ta)) continue; 1044 for (int day = 0; day < Constants.DAY_CODES.length; day++) { 1045 if (!selectedHalfDays.contains(2 * day) && hasHalfDay(week, day, ta, true)) 1046 otherHalfDays.add(2 * day); 1047 if (!selectedHalfDays.contains(2 * day + 1) && hasHalfDay(week, day, ta, false)) 1048 otherHalfDays.add(2 * day + 1); 1049 } 1050 } 1051 if (selectedHalfDays.size() + otherHalfDays.size() <= maxHalfDays) break; 1052 int halfday = ToolBox.random(otherHalfDays); 1053 int day = halfday / 2; 1054 boolean morning = ((halfday % 2) == 0); 1055 for (TeachingAssignment ta: assignments) { 1056 if (value != null && value.variable().equals(ta.variable())) continue; 1057 if (conflicts.contains(ta)) continue; 1058 if (hasHalfDay(week, day, ta, morning)) 1059 conflicts.add(ta); 1060 } 1061 } 1062 } 1063 }}, new ConstraintCreator<int[]>() { 1064 @Override 1065 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 1066 Matcher matcher = Pattern.compile(regexp).matcher(reference); 1067 if (matcher.find()) { 1068 int maxHalfDays = Integer.parseInt(matcher.group(2)); 1069 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_HALF_DAYS, new int[] {maxHalfDays}, reference) 1070 .setName("Max " + maxHalfDays + " Half-Days"); 1071 } 1072 return null; 1073 }}), 1074 /** 1075 * Limit number of consecutive days of a week. 1076 */ 1077 MAX_CONSECUTIVE_DAYS("_(MaxConsDays):([0-9]+)_", "Max Consecutive Days", new Check() { 1078 protected boolean hasDay(BitSet week, int dayOfWeek, Section section) { 1079 if (section.getTime() == null || !section.getTime().getWeekCode().intersects(week)) return false; 1080 return (section.getTime().getDayCode() & Constants.DAY_CODES[dayOfWeek]) != 0; 1081 } 1082 protected boolean hasDay(BitSet week, int dayOfWeek, TeachingAssignment ta) { 1083 for (Section section: ta.variable().getSections()) 1084 if (hasDay(week, dayOfWeek, section)) return true; 1085 return false; 1086 } 1087 protected int countDays(TreeSet<Integer> days) { 1088 if (days.isEmpty()) return 0; 1089 return days.last() - days.first() + 1; 1090 } 1091 protected int countDays(TreeSet<Integer> days1, TreeSet<Integer> days2) { 1092 if (days1.isEmpty()) return countDays(days2); 1093 if (days2.isEmpty()) return countDays(days1); 1094 return Math.max(days1.last(), days2.last()) - Math.min(days1.first(), days2.first()) + 1; 1095 } 1096 @Override 1097 public double getValue(Distribution d, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 1098 int [] params = d.getParameter(); 1099 int maxDays = params[0]; 1100 double penalty = 0; 1101 Set<TeachingAssignment> assignments = instructor.getContext(assignment).getAssignments(); 1102 for (BitSet week: instructor.getModel().getWeeks()) { 1103 TreeSet<Integer> days = new TreeSet<Integer>(); 1104 for (TeachingAssignment ta: assignments) { 1105 if (value != null && value.variable().equals(ta.variable())) continue; 1106 for (int day = 0; day < Constants.DAY_CODES.length; day++) 1107 if (hasDay(week, day, ta)) 1108 days.add(day); 1109 } 1110 if (value != null) { 1111 int before = Math.max(0, countDays(days) - maxDays); 1112 for (int day = 0; day < Constants.DAY_CODES.length; day++) 1113 if (hasDay(week, day, value)) 1114 days.add(day); 1115 int after = Math.max(0, countDays(days) - maxDays); 1116 penalty += after - before; 1117 } else { 1118 penalty += Math.max(0, countDays(days) - maxDays); 1119 } 1120 } 1121 return penalty / (Math.max(1.0, 5.0 - maxDays) * instructor.getModel().getWeeks().size()); 1122 } 1123 @Override 1124 public void computeConflicts(Distribution d, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 1125 int [] params = d.getParameter(); 1126 int maxDays = params[0]; 1127 Set<TeachingAssignment> assignments = value.getInstructor().getContext(assignment).getAssignments(); 1128 for (BitSet week: value.getInstructor().getModel().getWeeks()) { 1129 TreeSet<Integer> selectedDays = new TreeSet<Integer>(); 1130 for (int day = 0; day < Constants.DAY_CODES.length; day++) 1131 if (hasDay(week, day, value)) 1132 selectedDays.add(day); 1133 // selected value has no days -> next week 1134 if (selectedDays.isEmpty()) continue; 1135 // selected value is over -> it cannot be assigned 1136 if (countDays(selectedDays) > maxDays) { 1137 conflicts.add(value); continue; 1138 } 1139 // check other days 1140 while (true) { 1141 TreeSet<Integer> otherDays = new TreeSet<Integer>(); 1142 for (TeachingAssignment ta: assignments) { 1143 if (value != null && value.variable().equals(ta.variable())) continue; 1144 if (conflicts.contains(ta)) continue; 1145 for (int day = 0; day < Constants.DAY_CODES.length; day++) 1146 if (!selectedDays.contains(day) && hasDay(week, day, ta)) 1147 otherDays.add(day); 1148 } 1149 if (countDays(selectedDays, otherDays) <= maxDays) break; 1150 int day = (ToolBox.random(1) == 0 ? otherDays.first() : otherDays.last()); 1151 for (TeachingAssignment ta: assignments) { 1152 if (value != null && value.variable().equals(ta.variable())) continue; 1153 if (conflicts.contains(ta)) continue; 1154 if (hasDay(week, day, ta)) 1155 conflicts.add(ta); 1156 } 1157 } 1158 } 1159 }}, new ConstraintCreator<int[]>() { 1160 @Override 1161 public ParametrizedConstraintType<int[]> create(String reference, String regexp) { 1162 Matcher matcher = Pattern.compile(regexp).matcher(reference); 1163 if (matcher.find()) { 1164 int maxDays = Integer.parseInt(matcher.group(2)); 1165 return new ParametrizedConstraintType<int[]>(ConstraintType.MAX_CONSECUTIVE_DAYS, new int[] {maxDays}, reference) 1166 .setName("Max " + maxDays + " Consecutive Days"); 1167 } 1168 return null; 1169 }}), 1170 ; 1171 1172 private String iReference, iName; 1173 private Check iCheck = null; 1174 private ConstraintCreator<?> iCretor = null; 1175 1176 ConstraintType(String reference, String name, Check check) { 1177 iReference = reference; iName = name; iCheck = check; 1178 } 1179 1180 ConstraintType(String reference, String name, Check check, ConstraintCreator<?> creator) { 1181 iReference = reference; iName = name; iCheck = check; iCretor = creator; 1182 } 1183 1184 @Override 1185 public ConstraintType type() { return this; } 1186 1187 @Override 1188 public String reference() { return iReference; } 1189 1190 @Override 1191 public String getName() { return iName; } 1192 1193 @Override 1194 public double getValue(Distribution distribution, Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, TeachingAssignment value) { 1195 return iCheck.getValue(distribution, assignment, instructor, value); 1196 } 1197 1198 @Override 1199 public void computeConflicts(Distribution distribution, Assignment<Variable, TeachingAssignment> assignment, TeachingAssignment value, Set<TeachingAssignment> conflicts) { 1200 iCheck.computeConflicts(distribution, assignment, value, conflicts); 1201 } 1202 1203 private Check check() { return iCheck; } 1204 private ConstraintCreator<?> creator() { return iCretor; } 1205 } 1206 1207 /** 1208 * Create a constraint type from the provided reference. Returns null when there is no match. 1209 */ 1210 public static ConstraintTypeInterface getConstraintType(String reference, String name) { 1211 for (ConstraintType t: ConstraintType.values()) { 1212 if (t.reference().equals(reference)) { 1213 if (name == null) return t; 1214 return new ParametrizedConstraintType<Integer>(t, null, reference).setName(name); 1215 } 1216 if (t.creator() != null && reference.matches(t.reference())) { 1217 if (name == null) 1218 return t.creator().create(reference, t.reference()); 1219 else 1220 return t.creator().create(reference, t.reference()).setName(name); 1221 } 1222 } 1223 return null; 1224 } 1225 1226 /** 1227 * Teaching assignment and a section (from the assignment) combination. 1228 */ 1229 static class TeachingAssignmentSection { 1230 private TeachingAssignment iTeachingAssignment; 1231 private Section iSection; 1232 1233 TeachingAssignmentSection(TeachingAssignment ta, Section section) { 1234 iTeachingAssignment = ta; 1235 iSection = section; 1236 } 1237 1238 public TeachingAssignment getTeachingAssignment() { return iTeachingAssignment; } 1239 public Section getSection() { return iSection; } 1240 public TimeLocation getTime() { return getSection().getTime(); } 1241 1242 @Override 1243 public int hashCode() { 1244 return getTeachingAssignment().hashCode() ^ getSection().hashCode(); 1245 } 1246 1247 @Override 1248 public boolean equals(Object o) { 1249 if (o == null || !(o instanceof TeachingAssignmentSection)) return false; 1250 TeachingAssignmentSection tas = (TeachingAssignmentSection)o; 1251 return tas.getTeachingAssignment().equals(getTeachingAssignment()) && tas.getSection().equals(getSection()); 1252 } 1253 } 1254 1255 /** 1256 * Block of sections that go one after the other without any gap. 1257 */ 1258 public static class Block { 1259 // start slot of the block 1260 private int startSlotCurrentBlock = -1; 1261 // end slot of the block 1262 private int endSlotCurrentBlock = -1; 1263 // max number of slots between classes to be considered Back-To-Back; 4 slots default 1264 private int maxSlotsBetweenBackToBack = 4; 1265 // the list of placements of this block 1266 private List<TeachingAssignmentSection> sections = new ArrayList<TeachingAssignmentSection>(); 1267 1268 public Block(int maxSlotsBetweenBTB){ 1269 this.maxSlotsBetweenBackToBack = maxSlotsBetweenBTB; 1270 } 1271 1272 public boolean addSection(TeachingAssignmentSection section) { 1273 if (section == null) return false; 1274 1275 TimeLocation t = section.getTime(); 1276 if (t == null) return false; 1277 1278 // if placements is empty, the block only contains currently added placement -> set start and end 1279 if (sections.isEmpty()){ 1280 sections.add(section); 1281 startSlotCurrentBlock = t.getStartSlot(); 1282 endSlotCurrentBlock = t.getStartSlot() + t.getLength(); 1283 return true; 1284 } 1285 1286 // if startSlotCurrentBlock equals placement's start slot, add placement and adjust endSlotCurrentBlock 1287 if (t.getStartSlot() == startSlotCurrentBlock){ 1288 sections.add(section); 1289 int tEnd = t.getStartSlot() + t.getLength(); 1290 if (tEnd > endSlotCurrentBlock){ 1291 endSlotCurrentBlock = tEnd; 1292 } 1293 return true; 1294 } 1295 1296 // if placement starts among endSlotCurrentBlock + modifier and startSlotCurrentBlock, add the placement 1297 if (endSlotCurrentBlock + maxSlotsBetweenBackToBack >= t.getStartSlot() && t.getStartSlot() >= startSlotCurrentBlock ){ 1298 sections.add(section); 1299 int tEnd = t.getStartSlot() + t.getLength(); 1300 if (tEnd > endSlotCurrentBlock){ 1301 endSlotCurrentBlock = tEnd; 1302 } 1303 return true; 1304 } 1305 1306 return false; 1307 } 1308 1309 public boolean haveSameStartTime() { 1310 if (!sections.isEmpty()) { 1311 int startSlot = sections.get(0).getTime().getStartSlot(); 1312 for (TeachingAssignmentSection p : getSections()) { 1313 if (p.getTime().getStartSlot() != startSlot) 1314 return false; 1315 } 1316 } 1317 return true; 1318 } 1319 1320 public int getStartSlotCurrentBlock() { 1321 return startSlotCurrentBlock; 1322 } 1323 1324 public int getEndSlotCurrentBlock() { 1325 return endSlotCurrentBlock; 1326 } 1327 1328 public int getNbrPlacements() { 1329 return sections.size(); 1330 } 1331 1332 public List<TeachingAssignmentSection> getSections() { 1333 return sections; 1334 } 1335 1336 public int getLengthInSlots() { 1337 return endSlotCurrentBlock - startSlotCurrentBlock; 1338 } 1339 1340 public boolean contains(TeachingAssignment ta) { 1341 for (TeachingAssignmentSection tas: getSections()) 1342 if (tas.getTeachingAssignment().equals(ta)) return true; 1343 return false; 1344 } 1345 1346 @Override 1347 public String toString(){ 1348 return "[" + startSlotCurrentBlock + ", " + endSlotCurrentBlock + "]" + " PlacementsNbr: "+ getNbrPlacements(); 1349 } 1350 } 1351 1352 /** 1353 * Returns true if the time overlaps with the provided week date pattern and days of the week. 1354 */ 1355 protected static boolean shareWeeksAndDay(TimeLocation t, BitSet week, int dayCode){ 1356 boolean matchDay = (t.getDayCode() & dayCode) != 0; 1357 boolean matchWeek = (week == null || t.shareWeeks(week)); 1358 return matchDay && matchWeek; 1359 } 1360 1361 /** 1362 * Return teaching assignment + section combinations for the given instructor, considering the selected value and the provided conflicts. 1363 * The sections are sorted by time, and must meet during the given week and days of the week. 1364 */ 1365 protected static Set<TeachingAssignmentSection> getRelevantPlacements(Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor, int dayCode, Set<TeachingAssignment> conflicts, TeachingRequest.Variable variable, TeachingAssignment value, BitSet week) { 1366 Set<TeachingAssignmentSection> placements = new TreeSet<TeachingAssignmentSection>(new Comparator<TeachingAssignmentSection>() { 1367 @Override 1368 public int compare(TeachingAssignmentSection p1, TeachingAssignmentSection p2) { 1369 TimeLocation t1 = p1.getTime(), t2 = p2.getTime(); 1370 // compare by start time (earlier first) 1371 if (t1.getStartSlot() < t2.getStartSlot()) 1372 return -1; 1373 if (t1.getStartSlot() > t2.getStartSlot()) 1374 return 1; 1375 // same start -> compare by length (shorter first) 1376 if (t1.getLength() < t2.getLength()) 1377 return -1; 1378 if (t1.getLength() > t2.getLength()) 1379 return 1; 1380 // fallback 1381 return 0; 1382 } 1383 }); 1384 1385 for (TeachingAssignment ta : instructor.getContext(assignment).getAssignments()) { 1386 // lecture of the value is already assigned 1387 if (variable != null && ta.variable().equals(variable)) continue; 1388 if (conflicts != null && conflicts.contains(ta)) continue; 1389 1390 for (Section s: ta.variable().getSections()) { 1391 if (s.getTime() != null && shareWeeksAndDay(s.getTime(), week, dayCode)) 1392 placements.add(new TeachingAssignmentSection(ta, s)); 1393 } 1394 } 1395 1396 if (value != null) { 1397 for (Section s: value.variable().getSections()) { 1398 if (s.getTime() != null && shareWeeksAndDay(s.getTime(), week, dayCode)) 1399 placements.add(new TeachingAssignmentSection(value, s)); 1400 } 1401 } 1402 1403 return placements; 1404 } 1405 1406 /** 1407 * Merge sorted teaching assignment + section combinations into block. 1408 */ 1409 protected static List<Block> mergeToBlocks(Collection<TeachingAssignmentSection> sorted, int maxBreakBetweenBTB){ 1410 List<Block> blocks = new ArrayList<Block>(); 1411 // add placements to blocks 1412 for (TeachingAssignmentSection placement: sorted) { 1413 boolean added = false; 1414 // add placement to a suitable block 1415 for (int j = 0; j < blocks.size(); j++) { 1416 if (blocks.get(j).addSection(placement)) { 1417 added = true; 1418 } 1419 } 1420 // create a new block if a lecture does not fit into any block 1421 if (!added) { 1422 Block block = new Block(maxBreakBetweenBTB); 1423 block.addSection(placement); 1424 blocks.add(block); 1425 } 1426 } 1427 return blocks; 1428 } 1429 1430 /** 1431 * Return blocks for an instructor, meeting the provided parameters. 1432 * Combining {@link GroupConstraint#getRelevantPlacements(Assignment, Instructor, int, Set, Variable, TeachingAssignment, BitSet)} 1433 * and {@link GroupConstraint#mergeToBlocks(Collection, int)}. 1434 */ 1435 protected static List<Block> getBlocks(Assignment<TeachingRequest.Variable, TeachingAssignment> assignment, Instructor instructor,int dayCode, Set<TeachingAssignment> conflicts, TeachingRequest.Variable variable, TeachingAssignment value, BitSet week, int maxBreakBetweenBTB) { 1436 return mergeToBlocks(getRelevantPlacements(assignment, instructor, dayCode, conflicts, variable, value, week), maxBreakBetweenBTB); 1437 } 1438 1439 /** 1440 * Check if the given assignment has a section on the given days of the week. 1441 */ 1442 protected static boolean isCorectDayOfWeek(TeachingAssignment value, int dayCode) { 1443 if (value == null) return true; 1444 for (Section section: value.variable().getSections()) 1445 if (section.getTime() != null && (dayCode == 0 || (dayCode & section.getTime().getDayCode()) != 0)) return true; 1446 return false; 1447 } 1448 1449 /** 1450 * Check if the given assignment has a section on the given days of the week and weeks. 1451 */ 1452 protected static boolean isCorectDayAndWeek(TeachingAssignment value, int dayCode, BitSet week) { 1453 if (value == null) return true; 1454 for (Section section: value.variable().getSections()) 1455 if (section.getTime() != null && (dayCode == 0 || (dayCode & section.getTime().getDayCode()) != 0) 1456 && section.getTime().getWeekCode().intersects(week)) return true; 1457 return false; 1458 } 1459}