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}