001package org.cpsolver.coursett.constraint; 002 003import java.util.ArrayList; 004import java.util.HashMap; 005import java.util.HashSet; 006import java.util.List; 007import java.util.Map; 008import java.util.Set; 009import java.util.concurrent.ConcurrentHashMap; 010 011import org.cpsolver.coursett.model.Lecture; 012import org.cpsolver.coursett.model.Placement; 013import org.cpsolver.coursett.model.Student; 014import org.cpsolver.coursett.model.TimetableModel; 015import org.cpsolver.ifs.assignment.Assignment; 016import org.cpsolver.ifs.model.Constraint; 017import org.cpsolver.ifs.model.GlobalConstraint; 018import org.cpsolver.ifs.model.Model; 019import org.cpsolver.ifs.model.ModelListener; 020import org.cpsolver.ifs.solver.Solver; 021import org.cpsolver.ifs.util.DataProperties; 022import org.cpsolver.ifs.util.DistanceMetric; 023 024/** 025 * An experimental global constraint that does not allow any two classes that can be attended 026 * by the same student to have a conflict. The constraint checks any two classes of different 027 * offerings that share at least one student and that the student is allowed to take (not restricted 028 * by reservations). Class pairs included in the Ignore Student Conflicts constraints are ignored. 029 * Some classes may be excluded by using ExtendedStudentConflicts.IgnoreClasses parameter which may 030 * contain a regular expression matching class name(s). 031 * <br> 032 * Pairs of classes of the same offering are checked, too. In this case, the course structure must 033 * allow the two classes to be attended together (e.g., they are from the same configuration), and at 034 * least one student in the offering is allowed to take both classes. This feature can be disabled by 035 * setting ExtendedStudentConflicts.CheckSameCourse to false. 036 * <br> 037 * @author Tomáš Müller 038 * @version CourseTT 1.3 (University Course Timetabling)<br> 039 * Copyright (C) 2013 - 2024 Tomáš Müller<br> 040 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 041 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 042 * <br> 043 * This library is free software; you can redistribute it and/or modify 044 * it under the terms of the GNU Lesser General Public License as 045 * published by the Free Software Foundation; either version 3 of the 046 * License, or (at your option) any later version. <br> 047 * <br> 048 * This library is distributed in the hope that it will be useful, but 049 * WITHOUT ANY WARRANTY; without even the implied warranty of 050 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 051 * Lesser General Public License for more details. <br> 052 * <br> 053 * You should have received a copy of the GNU Lesser General Public 054 * License along with this library; if not see 055 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 056 */ 057public class ExtendedStudentConflicts extends GlobalConstraint<Lecture, Placement> implements ModelListener<Lecture, Placement>{ 058 private String iIgnoreClasses = null; 059 private Map<Long, Map<Long, List<Student>>> iCommonStudents = null; 060 private Set<Long> iIgnoreClassIds = null; 061 private Map<Long, Map<Long, Boolean>> iClassCache = new ConcurrentHashMap<Long, Map<Long, Boolean>>(); 062 private boolean iCheckSameCourse = true; 063 064 @Override 065 public void setModel(Model<Lecture, Placement> model) { 066 super.setModel(model); 067 if (model != null && model instanceof TimetableModel) { 068 DataProperties config = ((TimetableModel)model).getProperties(); 069 iIgnoreClasses = config.getProperty("ExtendedStudentConflicts.IgnoreClasses"); 070 iCheckSameCourse = config.getPropertyBoolean("ExtendedStudentConflicts.CheckSameCourse", true); 071 } 072 } 073 074 protected void clearCache() { 075 iClassCache.clear(); 076 iCommonStudents = null; 077 iIgnoreClassIds = null; 078 } 079 080 private DistanceMetric getDistanceMetric() { 081 return (getModel() == null ? null : ((TimetableModel)getModel()).getDistanceMetric()); 082 } 083 084 protected List<Student> getCommonStudents(Long offeringId1, Long offeringId2) { 085 if (iCommonStudents == null) { 086 iCommonStudents = new ConcurrentHashMap<Long, Map<Long, List<Student>>>(); 087 for (Lecture lecture: getModel().variables()) { 088 if (lecture.isCommitted() || lecture.getConfiguration() == null) continue; 089 Map<Long, List<Student>> commonStudents = iCommonStudents.get(lecture.getConfiguration().getOfferingId()); 090 if (commonStudents != null) continue; 091 commonStudents = new ConcurrentHashMap<Long, List<Student>>(); 092 iCommonStudents.put(lecture.getConfiguration().getOfferingId(), commonStudents); 093 for (Lecture other: getModel().variables()) { 094 if (other.isCommitted() || other.getConfiguration() == null) continue; 095 // if (other.getConfiguration().getOfferingId().equals(lecture.getConfiguration().getOfferingId())) continue; 096 if (commonStudents.containsKey(other.getConfiguration().getOfferingId())) continue; 097 List<Student> students = new ArrayList<Student>(); 098 for (Student student: ((TimetableModel)getModel()).getAllStudents()) { 099 if (student.getOfferings().contains(lecture.getConfiguration().getOfferingId()) && student.getOfferings().contains(other.getConfiguration().getOfferingId())) 100 students.add(student); 101 } 102 commonStudents.put(other.getConfiguration().getOfferingId(), students); 103 } 104 } 105 } 106 Map<Long, List<Student>> offeringIds = iCommonStudents.get(offeringId1); 107 return (offeringIds == null ? null : offeringIds.get(offeringId2)); 108 } 109 110 protected boolean isIgnoreClass(Lecture lecture) { 111 if (iIgnoreClassIds == null) { 112 iIgnoreClassIds = new HashSet<Long>(); 113 if (iIgnoreClasses != null && !iIgnoreClasses.isEmpty()) 114 for (Lecture l: getModel().variables()) { 115 if (l.getName().matches(iIgnoreClasses)) iIgnoreClassIds.add(l.getClassId()); 116 } 117 } 118 return iIgnoreClassIds.contains(lecture.getClassId()); 119 120 } 121 122 private Boolean getCachedPair(Lecture l1, Lecture l2) { 123 if (l1.getClassId() < l2.getClassId()) { 124 Map<Long, Boolean> cache = iClassCache.get(l1.getClassId()); 125 return (cache == null ? null : cache.get(l2.getClassId())); 126 } else { 127 Map<Long, Boolean> cache = iClassCache.get(l2.getClassId()); 128 return (cache == null ? null : cache.get(l1.getClassId())); 129 } 130 } 131 132 private void setCachedPair(Lecture l1, Lecture l2, boolean value) { 133 if (l1.getClassId() < l2.getClassId()) { 134 Map<Long, Boolean> cache = iClassCache.get(l1.getClassId()); 135 if (cache == null) { 136 cache = new ConcurrentHashMap<Long, Boolean>(); 137 iClassCache.put(l1.getClassId(), cache); 138 } 139 cache.put(l2.getClassId(), value); 140 } else { 141 Map<Long, Boolean> cache = iClassCache.get(l2.getClassId()); 142 if (cache == null) { 143 cache = new ConcurrentHashMap<Long, Boolean>(); 144 iClassCache.put(l2.getClassId(), cache); 145 } 146 cache.put(l1.getClassId(), value); 147 } 148 } 149 150 private boolean checkSameCourseCanTakeTogether(Lecture l1, Lecture l2) { 151 // check if the feature is disabled 152 if (!iCheckSameCourse) return false; 153 // same subpart -> cannot take together 154 if (l1.getSchedulingSubpartId().equals(l2.getSchedulingSubpartId())) return false; 155 // different config -> cannot take together 156 if (!l1.getConfiguration().equals(l2.getConfiguration())) return false; 157 // subpart id > class id (classes that are given by class l1 and its parents) 158 Map<Long, Long> mustTake = new HashMap<Long, Long>(); 159 for (Lecture l = l1; l != null; l = l.getParent()) { 160 mustTake.put(l.getSchedulingSubpartId(), l.getClassId()); 161 } 162 // also include top-level subparts of the same configuration that have only one class 163 for (Map.Entry<Long, Set<Lecture>> e: l1.getConfiguration().getTopLectures().entrySet()) { 164 if (e.getValue().size() == 1) { 165 Lecture l = e.getValue().iterator().next(); 166 mustTake.put(l.getSchedulingSubpartId(), l.getClassId()); 167 } 168 } 169 // check l2 and its parents, if any of them does not follow mustTake -> cannot take together 170 for (Lecture l = l2; l != null; l = l.getParent()) { 171 Long id = mustTake.get(l.getSchedulingSubpartId()); 172 if (id != null && !l.getClassId().equals(id)) return false; 173 } 174 // no issue found -> can take together 175 return true; 176 } 177 178 protected boolean checkStudentForStudentConflicts(Lecture l1, Lecture l2) { 179 // are student conflicts between the two classes to be ignored ? 180 if (l1.isToIgnoreStudentConflictsWith(l2)) return false; 181 // check the cache 182 Boolean cache = getCachedPair(l1, l2); 183 if (cache != null) return cache.booleanValue(); 184 // classes of the same offering that cannot be taken together 185 if (l1.getConfiguration().getOfferingId().equals(l2.getConfiguration().getOfferingId()) && !checkSameCourseCanTakeTogether(l1, l2)) { 186 setCachedPair(l1, l2, false); 187 return false; 188 } 189 // ignore matching class pairs 190 if (isIgnoreClass(l1) && isIgnoreClass(l2)) { 191 setCachedPair(l1, l2, false); 192 return false; 193 } 194 // check offerings 195 List<Student> commonStudents = getCommonStudents(l1.getConfiguration().getOfferingId(), l2.getConfiguration().getOfferingId()); 196 // less then two students in common > do not check for conflicts 197 if (commonStudents == null || commonStudents.size() <= 1) { 198 setCachedPair(l1, l2, false); 199 return false; 200 } 201 // check if there is a student that can attend l1 and l2 together 202 for (Student student: commonStudents) 203 if (student.canEnroll(l1) && student.canEnroll(l2)) { 204 setCachedPair(l1, l2, true); 205 return true; 206 } 207 // no common students that can attend both classes 208 setCachedPair(l1, l2, false); 209 return false; 210 } 211 212 @Override 213 public void computeConflicts(Assignment<Lecture, Placement> assignment, Placement placement, Set<Placement> conflicts) { 214 Lecture lecture = placement.variable(); 215 for (Lecture other: getModel().assignedVariables(assignment)) { 216 Placement otherPlacement = assignment.getValue(other); 217 if (checkStudentForStudentConflicts(lecture, other) && JenrlConstraint.isInConflict(placement, otherPlacement, getDistanceMetric(), 0)) 218 conflicts.add(otherPlacement); 219 } 220 } 221 222 @Override 223 public boolean inConflict(Assignment<Lecture, Placement> assignment, Placement placement) { 224 Lecture lecture = placement.variable(); 225 for (Lecture other: getModel().assignedVariables(assignment)) { 226 Placement otherPlacement = assignment.getValue(other); 227 if (checkStudentForStudentConflicts(lecture, other) && JenrlConstraint.isInConflict(placement, otherPlacement, getDistanceMetric(), 0)) 228 return true; 229 } 230 return false; 231 } 232 233 @Override 234 public boolean isConsistent(Placement p1, Placement p2) { 235 return p1 != null && p2 != null && 236 checkStudentForStudentConflicts(p1.variable(), p2.variable()) && 237 JenrlConstraint.isInConflict(p1, p2, getDistanceMetric(), 0); 238 } 239 240 @Override 241 public String getName() { 242 return "Extended Student Conflicts"; 243 } 244 245 @Override 246 public String toString() { 247 return "Extended Student Conflicts"; 248 } 249 250 @Override 251 public void variableAdded(Lecture variable) { 252 clearCache(); 253 } 254 255 @Override 256 public void variableRemoved(Lecture variable) { 257 clearCache(); 258 } 259 260 @Override 261 public void constraintAdded(Constraint<Lecture, Placement> constraint) {} 262 263 @Override 264 public void constraintRemoved(Constraint<Lecture, Placement> constraint) {} 265 266 @Override 267 public void beforeAssigned(Assignment<Lecture, Placement> assignment, long iteration, Placement value) {} 268 269 @Override 270 public void beforeUnassigned(Assignment<Lecture, Placement> assignment, long iteration, Placement value) {} 271 272 @Override 273 public void afterAssigned(Assignment<Lecture, Placement> assignment, long iteration, Placement value) {} 274 275 @Override 276 public void afterUnassigned(Assignment<Lecture, Placement> assignment, long iteration, Placement value) {} 277 278 @Override 279 public boolean init(Solver<Lecture, Placement> solver) { 280 clearCache(); 281 return true; 282 } 283}