001package org.cpsolver.instructor.model; 002 003import java.util.ArrayList; 004import java.util.HashSet; 005import java.util.List; 006import java.util.Set; 007 008import org.cpsolver.coursett.Constants; 009import org.cpsolver.coursett.model.TimeLocation; 010import org.cpsolver.coursett.preference.MinMaxPreferenceCombination; 011import org.cpsolver.coursett.preference.PreferenceCombination; 012import org.cpsolver.ifs.assignment.Assignment; 013import org.cpsolver.ifs.assignment.context.AbstractClassWithContext; 014import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext; 015import org.cpsolver.ifs.assignment.context.CanInheritContext; 016import org.cpsolver.ifs.criteria.Criterion; 017import org.cpsolver.instructor.criteria.BackToBack; 018import org.cpsolver.instructor.criteria.DifferentLecture; 019import org.cpsolver.instructor.criteria.TimeOverlaps; 020 021/** 022 * Instructor. An instructor has an id, a name, a teaching preference, a maximal teaching load, a back-to-back preference. 023 * It can also have a set of attributes and course and time preferences. Availability is modeled with prohibited time preferences. 024 * 025 * @version IFS 1.3 (Instructor Sectioning)<br> 026 * Copyright (C) 2016 Tomas Muller<br> 027 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 028 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 029 * <br> 030 * This library is free software; you can redistribute it and/or modify 031 * it under the terms of the GNU Lesser General Public License as 032 * published by the Free Software Foundation; either version 3 of the 033 * License, or (at your option) any later version. <br> 034 * <br> 035 * This library is distributed in the hope that it will be useful, but 036 * WITHOUT ANY WARRANTY; without even the implied warranty of 037 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 038 * Lesser General Public License for more details. <br> 039 * <br> 040 * You should have received a copy of the GNU Lesser General Public 041 * License along with this library; if not see 042 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 043 */ 044public class Instructor extends AbstractClassWithContext<TeachingRequest, TeachingAssignment, Instructor.Context> implements CanInheritContext<TeachingRequest, TeachingAssignment, Instructor.Context> { 045 private List<Attribute> iAttributes = new ArrayList<Attribute>(); 046 private List<Preference<TimeLocation>> iTimePreferences = new ArrayList<Preference<TimeLocation>>(); 047 private List<Preference<Course>> iCoursePreferences = new ArrayList<Preference<Course>>(); 048 private InstructorSchedulingModel iModel; 049 private long iInstructorId; 050 private String iExternalId; 051 private String iName; 052 private int iPreference; 053 private float iMaxLoad; 054 private int iBackToBackPreference; 055 056 /** 057 * Constructor 058 * @param id instructor unique id 059 * @param externalId instructor external id 060 * @param name instructor name 061 * @param preference teaching preference 062 * @param maxLoad maximal teaching load 063 */ 064 public Instructor(long id, String externalId, String name, int preference, float maxLoad) { 065 iInstructorId = id; iExternalId = externalId; iName = name; iPreference = preference; iMaxLoad = maxLoad; 066 } 067 068 @Override 069 public InstructorSchedulingModel getModel() { return iModel; } 070 071 /** 072 * Set current model 073 * @param model instructional scheduling model 074 */ 075 public void setModel(InstructorSchedulingModel model) { iModel = model; } 076 077 /** 078 * Instructor unique id that was provided in the constructor 079 * @return instructor unique id 080 */ 081 public long getInstructorId() { return iInstructorId; } 082 083 /** 084 * Has instructor external id? 085 * @return true, if the instructor has an external id set 086 */ 087 public boolean hasExternalId() { return iExternalId != null && !iExternalId.isEmpty(); } 088 089 /** 090 * Instructor external Id that was provided in the constructor 091 * @return external id 092 */ 093 public String getExternalId() { return iExternalId; } 094 095 /** 096 * Has instructor name? 097 * @return true, if the instructor name is set 098 */ 099 public boolean hasName() { return iName != null && !iName.isEmpty(); } 100 101 /** 102 * Instructor name that was provided in the constructor 103 * @return instructor name 104 */ 105 public String getName() { return iName != null ? iName : iExternalId != null ? iExternalId : ("I" + iInstructorId); } 106 107 /** 108 * Set back-to-back preference (only soft preference can be set at the moment) 109 * @param backToBackPreference back-to-back preference (e.g., -1 for preferred, 1 for discouraged) 110 */ 111 public void setBackToBackPreference(int backToBackPreference) { iBackToBackPreference = backToBackPreference; } 112 113 /** 114 * Return back-to-back preference (only soft preference can be set at the moment) 115 * @return back-to-back preference (e.g., -1 for preferred, 1 for discouraged) 116 */ 117 public int getBackToBackPreference() { return iBackToBackPreference; } 118 119 /** 120 * Is back-to-back preferred? 121 * @return true if the back-to-back preference is negative 122 */ 123 public boolean isBackToBackPreferred() { return iBackToBackPreference < 0; } 124 125 /** 126 * Is back-to-back discouraged? 127 * @return true if the back-to-back preference is positive 128 */ 129 public boolean isBackToBackDiscouraged() { return iBackToBackPreference > 0; } 130 131 /** 132 * Instructor unavailability string generated from prohibited time preferences 133 * @return comma separated list of times during which the instructor is not available 134 */ 135 public String getAvailable() { 136 if (iTimePreferences == null) return ""; 137 String ret = ""; 138 for (Preference<TimeLocation> tl: iTimePreferences) { 139 if (tl.isProhibited()) { 140 if (!ret.isEmpty()) ret += ", "; 141 ret += tl.getTarget().getLongName(true).trim(); 142 } 143 } 144 return ret.isEmpty() ? "" : ret; 145 } 146 147 /** 148 * Return instructor attributes 149 * @return list of instructor attributes 150 */ 151 public List<Attribute> getAttributes() { return iAttributes; } 152 153 /** 154 * Add instructor attribute 155 * @param attribute instructor attribute 156 */ 157 public void addAttribute(Attribute attribute) { iAttributes.add(attribute); } 158 159 /** 160 * Return instructor attributes of given type 161 * @param type attribute type 162 * @return attributes of this instructor that are of the given type 163 */ 164 public Set<Attribute> getAttributes(Attribute.Type type) { 165 Set<Attribute> attributes = new HashSet<Attribute>(); 166 for (Attribute attribute: iAttributes) 167 if (type.equals(attribute.getType())) attributes.add(attribute); 168 return attributes; 169 } 170 171 /** 172 * Return instructor preferences 173 * @return list of instructor time preferences 174 */ 175 public List<Preference<TimeLocation>> getTimePreferences() { return iTimePreferences; } 176 177 /** 178 * Add instructor time preference 179 * @param pref instructor time preference 180 */ 181 public void addTimePreference(Preference<TimeLocation> pref) { iTimePreferences.add(pref); } 182 183 /** 184 * Compute time preference for a given time. This is using the {@link MinMaxPreferenceCombination} for all time preferences that are overlapping with the given time. 185 * @param time given time 186 * @return computed preference for the given time 187 */ 188 public PreferenceCombination getTimePreference(TimeLocation time) { 189 if (iTimePreferences.isEmpty()) return null; 190 PreferenceCombination comb = new MinMaxPreferenceCombination(); 191 for (Preference<TimeLocation> pref: iTimePreferences) 192 if (pref.getTarget().hasIntersection(time)) 193 comb.addPreferenceInt(pref.getPreference()); 194 return comb; 195 } 196 197 /** 198 * Compute time preference for a given teaching request. This is using the {@link MinMaxPreferenceCombination} for all time preferences that are overlapping with the given teaching request. 199 * When a section that allows for overlaps (see {@link Section#isAllowOverlap()}) overlap with a prohibited time preference, this is only counted as strongly discouraged. 200 * @param request teaching request that is being considered 201 * @return computed time preference 202 */ 203 public PreferenceCombination getTimePreference(TeachingRequest request) { 204 PreferenceCombination comb = new MinMaxPreferenceCombination(); 205 for (Preference<TimeLocation> pref: iTimePreferences) 206 for (Section section: request.getSections()) 207 if (section.hasTime() && section.getTime().hasIntersection(pref.getTarget())) { 208 if (section.isAllowOverlap() && pref.isProhibited()) 209 comb.addPreferenceInt(Constants.sPreferenceLevelStronglyDiscouraged); 210 else 211 comb.addPreferenceInt(pref.getPreference()); 212 } 213 return comb; 214 } 215 216 /** 217 * Return course preferences 218 * @return list of instructor course preferences 219 */ 220 public List<Preference<Course>> getCoursePreferences() { return iCoursePreferences; } 221 222 /** 223 * Add course preference 224 * @param pref instructor course preference 225 */ 226 public void addCoursePreference(Preference<Course> pref) { iCoursePreferences.add(pref); } 227 228 /** 229 * Return preference for the given course 230 * @param course course that is being considered 231 * @return course preference for the given course 232 */ 233 public Preference<Course> getCoursePreference(Course course) { 234 boolean hasRequired = false; 235 for (Preference<Course> pref: iCoursePreferences) 236 if (pref.isRequired()) { hasRequired = true; break; } 237 for (Preference<Course> pref: iCoursePreferences) 238 if (pref.getTarget().equals(course)) { 239 if (hasRequired && !pref.isRequired()) continue; 240 return pref; 241 } 242 if (hasRequired) 243 return new Preference<Course>(course, Constants.sPreferenceLevelProhibited); 244 return new Preference<Course>(course, Constants.sPreferenceLevelNeutral); 245 } 246 247 /** 248 * Return teaching preference 249 * @return teaching preference of this instructor 250 */ 251 public int getPreference() { return iPreference; } 252 253 /** 254 * Maximal load 255 * @return maximal load of this instructor 256 */ 257 public float getMaxLoad() { return iMaxLoad; } 258 259 /** 260 * Check if this instructor can teach the given request. This means that the given request is below the maximal teaching load, 261 * the instructor is available (time preference is not prohibited), the instructor does not prohibit the course (there is no 262 * prohibited course preference for the given course), and the request's instructor preference is also not prohibited. 263 * So, the only thing that is not checked are the attribute preferences. 264 * @param request teaching request that is being considered 265 * @return true, if the instructor can be assigned to the given teaching request 266 */ 267 public boolean canTeach(TeachingRequest request) { 268 if (request.getLoad() > getMaxLoad()) 269 return false; 270 if (getTimePreference(request).isProhibited()) 271 return false; 272 if (getCoursePreference(request.getCourse()).isProhibited()) 273 return false; 274 if (request.getInstructorPreference(this).isProhibited()) 275 return false; 276 return true; 277 } 278 279 @Override 280 public int hashCode() { 281 return new Long(iInstructorId).hashCode(); 282 } 283 284 @Override 285 public boolean equals(Object o) { 286 if (o == null || !(o instanceof Instructor)) return false; 287 Instructor i = (Instructor)o; 288 return getInstructorId() == i.getInstructorId(); 289 } 290 291 /** 292 * Compute time overlaps with instructor availability 293 * @param request teaching request that is being considered 294 * @return number of slots during which the instructor has a prohibited time preferences that are overlapping with a section of the request that is allowing for overlaps 295 */ 296 public int share(TeachingRequest request) { 297 int share = 0; 298 for (Section section: request.getSections()) { 299 if (!section.hasTime() || !section.isAllowOverlap()) continue; 300 for (Preference<TimeLocation> pref: iTimePreferences) 301 if (pref.isProhibited() && section.getTime().shareWeeks(pref.getTarget())) 302 share += section.getTime().nrSharedDays(pref.getTarget()) * section.getTime().nrSharedHours(pref.getTarget()); 303 } 304 return share; 305 } 306 307 /** 308 * Compute time overlaps with instructor availability and other teaching assignments of the instructor 309 * @param assignment current assignment 310 * @param value teaching assignment that is being considered 311 * @return number of overlapping time slots (of the requested assignment) during which the overlaps are allowed 312 */ 313 public int share(Assignment<TeachingRequest, TeachingAssignment> assignment, TeachingAssignment value) { 314 int share = 0; 315 if (value.getInstructor().equals(this)) { 316 for (TeachingAssignment other : value.getInstructor().getContext(assignment).getAssignments()) { 317 if (other.variable().equals(value.variable())) 318 continue; 319 share += value.variable().share(other.variable()); 320 } 321 share += share(value.variable()); 322 } 323 return share; 324 } 325 326 /** 327 * Compute different common sections of the given teaching assignment and the other assignments of the instructor 328 * @param assignment current assignment 329 * @param value teaching assignment that is being considered 330 * @return average {@link TeachingRequest#nrSameLectures(TeachingRequest)} between the given and the other existing assignments of the instructor 331 */ 332 public double differentLectures(Assignment<TeachingRequest, TeachingAssignment> assignment, TeachingAssignment value) { 333 double same = 0; int count = 0; 334 if (value.getInstructor().equals(this)) { 335 for (TeachingAssignment other : value.getInstructor().getContext(assignment).getAssignments()) { 336 if (other.variable().equals(value.variable())) 337 continue; 338 same += value.variable().nrSameLectures(other.variable()); 339 count ++; 340 } 341 } 342 return (count == 0 ? 0.0 : (count - same) / count); 343 } 344 345 /** 346 * Compute number of back-to-back assignments (weighted by the preference) of the given teaching assignment and the other assignments of the instructor 347 * @param assignment current assignment 348 * @param value teaching assignment that is being considered 349 * @param diffRoomWeight different room penalty 350 * @param diffTypeWeight different instructional type penalty 351 * @return weighted back-to-back preference, using {@link TeachingRequest#countBackToBacks(TeachingRequest, double, double)} 352 */ 353 public double countBackToBacks(Assignment<TeachingRequest, TeachingAssignment> assignment, TeachingAssignment value, double diffRoomWeight, double diffTypeWeight) { 354 double b2b = 0.0; 355 if (value.getInstructor().equals(this) && getBackToBackPreference() != 0) { 356 for (TeachingAssignment other : value.getInstructor().getContext(assignment).getAssignments()) { 357 if (other.variable().equals(value.variable())) 358 continue; 359 if (getBackToBackPreference() < 0) { // preferred 360 b2b += (value.variable().countBackToBacks(other.variable(), diffRoomWeight, diffTypeWeight) - 1.0) * getBackToBackPreference(); 361 } else { 362 b2b += value.variable().countBackToBacks(other.variable(), diffRoomWeight, diffTypeWeight) * getBackToBackPreference(); 363 } 364 } 365 } 366 return b2b; 367 } 368 369 @Override 370 public String toString() { 371 return getName(); 372 } 373 374 @Override 375 public Context createAssignmentContext(Assignment<TeachingRequest, TeachingAssignment> assignment) { 376 return new Context(assignment); 377 } 378 379 380 @Override 381 public Context inheritAssignmentContext(Assignment<TeachingRequest, TeachingAssignment> assignment, Context parentContext) { 382 return new Context(assignment, parentContext); 383 } 384 385 386 /** 387 * Instructor Constraint Context. It keeps the list of current assignments of an instructor. 388 */ 389 public class Context implements AssignmentConstraintContext<TeachingRequest, TeachingAssignment> { 390 private HashSet<TeachingAssignment> iAssignments = new HashSet<TeachingAssignment>(); 391 private int iTimeOverlaps; 392 private double iBackToBacks; 393 private double iDifferentLectures; 394 395 /** 396 * Constructor 397 * @param assignment current assignment 398 */ 399 public Context(Assignment<TeachingRequest, TeachingAssignment> assignment) { 400 for (TeachingRequest request: getModel().variables()) { 401 TeachingAssignment value = assignment.getValue(request); 402 if (value != null && value.getInstructor().equals(getInstructor())) 403 iAssignments.add(value); 404 } 405 if (!iAssignments.isEmpty()) 406 updateCriteria(assignment); 407 } 408 409 /** 410 * Constructor 411 * @param assignment current assignment 412 * @param parentContext parent context 413 */ 414 public Context(Assignment<TeachingRequest, TeachingAssignment> assignment, Context parentContext) { 415 iAssignments = new HashSet<TeachingAssignment>(parentContext.getAssignments()); 416 if (!iAssignments.isEmpty()) 417 updateCriteria(assignment); 418 } 419 420 /** 421 * Instructor 422 * @return instructor of this context 423 */ 424 public Instructor getInstructor() { return Instructor.this; } 425 426 @Override 427 public void assigned(Assignment<TeachingRequest, TeachingAssignment> assignment, TeachingAssignment value) { 428 if (value.getInstructor().equals(getInstructor())) { 429 iAssignments.add(value); 430 updateCriteria(assignment); 431 } 432 } 433 434 @Override 435 public void unassigned(Assignment<TeachingRequest, TeachingAssignment> assignment, TeachingAssignment value) { 436 if (value.getInstructor().equals(getInstructor())) { 437 iAssignments.remove(value); 438 updateCriteria(assignment); 439 } 440 } 441 442 /** 443 * Update optimization criteria 444 * @param assignment current assignment 445 */ 446 private void updateCriteria(Assignment<TeachingRequest, TeachingAssignment> assignment) { 447 // update back-to-backs 448 BackToBack b2b = (BackToBack)getModel().getCriterion(BackToBack.class); 449 if (b2b != null) { 450 b2b.inc(assignment, -iBackToBacks); 451 iBackToBacks = countBackToBackPreference(b2b.getDifferentRoomWeight(), b2b.getDifferentTypeWeight()); 452 b2b.inc(assignment, iBackToBacks); 453 } 454 455 // update time overlaps 456 Criterion<TeachingRequest, TeachingAssignment> overlaps = getModel().getCriterion(TimeOverlaps.class); 457 if (overlaps != null) { 458 overlaps.inc(assignment, -iTimeOverlaps); 459 iTimeOverlaps = countTimeOverlaps(); 460 overlaps.inc(assignment, iTimeOverlaps); 461 } 462 463 // update same lectures 464 Criterion<TeachingRequest, TeachingAssignment> diff = getModel().getCriterion(DifferentLecture.class); 465 if (diff != null) { 466 diff.inc(assignment, -iDifferentLectures); 467 iDifferentLectures = countDifferentLectures(); 468 diff.inc(assignment, iDifferentLectures); 469 } 470 471 } 472 473 /** 474 * Current assignments of this instructor 475 * @return current teaching assignments 476 */ 477 public Set<TeachingAssignment> getAssignments() { return iAssignments; } 478 479 /** 480 * Current load of this instructor 481 * @return current load 482 */ 483 public float getLoad() { 484 float load = 0; 485 for (TeachingAssignment assignment : iAssignments) 486 load += assignment.variable().getLoad(); 487 return load; 488 } 489 490 /** 491 * If there are classes that allow for overlap, the number of such overlapping slots of this instructor 492 * @return current time overlaps (number of overlapping slots) 493 */ 494 public int countTimeOverlaps() { 495 int share = 0; 496 for (TeachingAssignment a1 : iAssignments) { 497 for (TeachingAssignment a2 : iAssignments) { 498 if (a1.getId() < a2.getId()) 499 share += a1.variable().share(a2.variable()); 500 } 501 share += getInstructor().share(a1.variable()); 502 } 503 return share; 504 } 505 506 /** 507 * Number of teaching assignments that have a time assignment of this instructor 508 * @return current number of teaching assignments that have a time 509 */ 510 public int countAssignmentsWithTime() { 511 int ret = 0; 512 a1: for (TeachingAssignment a1 : iAssignments) { 513 for (Section s1: a1.variable().getSections()) 514 if (s1.hasTime()) { 515 ret++; continue a1; 516 } 517 } 518 return ret; 519 } 520 521 /** 522 * Percentage of common sections that are not same for the instructor (using {@link TeachingRequest#nrSameLectures(TeachingRequest)}) 523 * @return percentage of pairs of common sections that are not the same 524 */ 525 public double countDifferentLectures() { 526 double same = 0; 527 int pairs = 0; 528 for (TeachingAssignment a1 : iAssignments) { 529 for (TeachingAssignment a2 : iAssignments) { 530 if (a1.getId() < a2.getId()) { 531 same += a1.variable().nrSameLectures(a2.variable()); 532 pairs++; 533 } 534 } 535 } 536 return (pairs == 0 ? 0.0 : (pairs - same) / pairs); 537 } 538 539 /** 540 * Current back-to-back preference of the instructor (using {@link TeachingRequest#countBackToBacks(TeachingRequest, double, double)}) 541 * @param diffRoomWeight different room weight 542 * @param diffTypeWeight different instructional type weight 543 * @return current back-to-back preference 544 */ 545 public double countBackToBackPreference(double diffRoomWeight, double diffTypeWeight) { 546 double b2b = 0; 547 if (getInstructor().isBackToBackPreferred() || getInstructor().isBackToBackDiscouraged()) 548 for (TeachingAssignment a1 : iAssignments) { 549 for (TeachingAssignment a2 : iAssignments) { 550 if (a1.getId() >= a2.getId()) continue; 551 if (getInstructor().getBackToBackPreference() < 0) { // preferred 552 b2b += (a1.variable().countBackToBacks(a2.variable(), diffRoomWeight, diffTypeWeight) - 1.0) * getInstructor().getBackToBackPreference(); 553 } else { 554 b2b += a1.variable().countBackToBacks(a2.variable(), diffRoomWeight, diffTypeWeight) * getInstructor().getBackToBackPreference(); 555 } 556 } 557 } 558 return b2b; 559 } 560 561 /** 562 * Current back-to-back percentage for this instructor 563 * @return percentage of assignments that are back-to-back 564 */ 565 public double countBackToBackPercentage() { 566 BackToBack c = (BackToBack)getModel().getCriterion(BackToBack.class); 567 if (c == null) return 0.0; 568 double b2b = 0.0; 569 int pairs = 0; 570 for (TeachingAssignment a1 : iAssignments) { 571 for (TeachingAssignment a2 : iAssignments) { 572 if (a1.getId() >= a2.getId()) continue; 573 b2b += a1.variable().countBackToBacks(a2.variable(), c.getDifferentRoomWeight(), c.getDifferentTypeWeight()); 574 pairs ++; 575 } 576 } 577 return (pairs == 0 ? 0.0 : b2b / pairs); 578 } 579 } 580}