001package org.cpsolver.coursett.sectioning;
002
003import java.util.ArrayList;
004import java.util.Collections;
005import java.util.Comparator;
006import java.util.HashSet;
007import java.util.LinkedList;
008import java.util.List;
009import java.util.Map;
010import java.util.Queue;
011import java.util.Set;
012
013import org.cpsolver.coursett.constraint.JenrlConstraint;
014import org.cpsolver.coursett.criteria.StudentConflict;
015import org.cpsolver.coursett.model.Configuration;
016import org.cpsolver.coursett.model.Lecture;
017import org.cpsolver.coursett.model.Placement;
018import org.cpsolver.coursett.model.Student;
019
020/**
021 * A class wrapping a student, including an ordered set of possible enrollment into a given
022 * course.
023 * 
024 * @version CourseTT 1.3 (University Course Timetabling)<br>
025 *          Copyright (C) 2017 Tomas Muller<br>
026 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
027 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
028 * <br>
029 *          This library is free software; you can redistribute it and/or modify
030 *          it under the terms of the GNU Lesser General Public License as
031 *          published by the Free Software Foundation; either version 3 of the
032 *          License, or (at your option) any later version. <br>
033 * <br>
034 *          This library is distributed in the hope that it will be useful, but
035 *          WITHOUT ANY WARRANTY; without even the implied warranty of
036 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
037 *          Lesser General Public License for more details. <br>
038 * <br>
039 *          You should have received a copy of the GNU Lesser General Public
040 *          License along with this library; if not see
041 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
042 */
043public class SctStudent implements Comparable<SctStudent> {
044    private SctModel iModel;
045    private Student iStudent;
046    private List<SctEnrollment> iEnrollments = null;
047    private double iTotalEnrollmentWeight = 0.0;
048    private Double iOfferingWeight = null;
049    private List<Lecture> iInstructing = null;
050    
051    /**
052     * Constructor.
053     * @param model student sectioning model
054     * @param student student to represent
055     */
056    public SctStudent(SctModel model, Student student) {
057        iStudent = student;
058        iModel = model;
059    }
060    
061    /**
062     * Student sectioning model
063     */
064    public SctModel getModel() { return iModel; }
065    
066    /**
067     * Student for which the possible enrollments are being computed
068     */
069    public Student getStudent() { return iStudent; }
070    
071    /**
072     * Current enrollment of this student
073     * @param checkInstructor if the student is also instructor and he/she is instructing this class, return classes he/she is instructing instead
074     * @return current enrollment of this student into the given course
075     */
076    public SctEnrollment getCurrentEnrollment(boolean checkInstructor) {
077        if (checkInstructor && isInstructing())
078            return new SctEnrollment(-1, this, getInstructingLectures());
079        List<Lecture> lectures = new ArrayList<Lecture>();
080        for (Lecture lecture: getStudent().getLectures())
081            if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId()))
082                lectures.add(lecture);
083        if (!lectures.isEmpty())
084            return new SctEnrollment(-1, this, lectures);
085        return null;
086    }
087    
088    /**
089     * List of lectures of the given course that the student is instructing (if he/she is also an instructor, using {@link Student#getInstructor()})
090     * @return list of lectures of the given course that the student is instructing
091     */
092    public List<Lecture> getInstructingLectures() {
093        if (iInstructing == null && getStudent().getInstructor() != null) {
094            iInstructing = new ArrayList<Lecture>();
095            for (Lecture lecture: getStudent().getInstructor().variables())
096                if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId()))
097                    iInstructing.add(lecture);
098        }
099        return iInstructing;
100    }
101    
102    /**
103     * Is student also an instructor of the given course?
104     */
105    public boolean isInstructing() {
106        return getStudent().getInstructor() != null && !getInstructingLectures().isEmpty();
107    }
108    
109    /**
110     * Conflict weight of a lecture pair
111     */
112    public double getJenrConflictWeight(Lecture l1, Lecture l2) {
113        Placement p1 = getModel().getAssignment().getValue(l1);
114        Placement p2 = getModel().getAssignment().getValue(l2);
115        if (p1 == null || p2 == null) return 0.0;
116        if (getModel().getStudentConflictCriteria() == null) {
117            if (JenrlConstraint.isInConflict(p1, p2, getModel().getTimetableModel().getDistanceMetric(), getModel().getTimetableModel().getStudentWorkDayLimit()))
118                return getStudent().getJenrlWeight(l1, l2);
119            return 0.0;
120        }
121        double weight = 0.0;
122        for (StudentConflict sc: getModel().getStudentConflictCriteria())
123            if (sc.isApplicable(getStudent(), p1.variable(), p2.variable()) && sc.inConflict(p1, p2))
124                weight += sc.getWeight() * getStudent().getJenrlWeight(l1, l2);
125        return weight;
126    }
127    
128    /**
129     * All scheduling subpart ids of a configuration.
130     */
131    private List<Long> getSubpartIds(Configuration configuration) {
132        List<Long> subpartIds = new ArrayList<Long>();
133        Queue<Lecture> queue = new LinkedList<Lecture>();
134        for (Map.Entry<Long, Set<Lecture>> e: configuration.getTopLectures().entrySet()) {
135            subpartIds.add(e.getKey());
136            queue.add(e.getValue().iterator().next());
137        }
138        Lecture lecture = null;
139        while ((lecture = queue.poll()) != null) {
140            if (lecture.getChildren() != null)
141                for (Map.Entry<Long, List<Lecture>> e: lecture.getChildren().entrySet()) {
142                    subpartIds.add(e.getKey());
143                    queue.add(e.getValue().iterator().next());
144                }
145        }
146        return subpartIds;
147    }
148    
149    /**
150     * Compute all possible enrollments
151     */
152    private void computeEnrollments(Configuration configuration, Map<Long, Set<Lecture>> subparts, List<Long> subpartIds, Set<Lecture> enrollment, double conflictWeight) {
153        if (enrollment.size() == subpartIds.size()) {
154            iEnrollments.add(new SctEnrollment(iEnrollments.size(), this, enrollment, conflictWeight));
155            iTotalEnrollmentWeight += conflictWeight;
156        } else {
157            Set<Lecture> lectures = subparts.get(subpartIds.get(enrollment.size()));
158            for (Lecture lecture: lectures) {
159                if (lecture.getParent() != null && !enrollment.contains(lecture.getParent())) continue;
160                if (!getStudent().canEnroll(lecture)) continue;
161                double delta = 0.0;
162                for (Lecture other: getStudent().getLectures())
163                    if (!configuration.getOfferingId().equals(other.getConfiguration().getOfferingId()))
164                        delta += getJenrConflictWeight(lecture, other);
165                for (Lecture other: enrollment)
166                    delta += getJenrConflictWeight(lecture, other);
167                enrollment.add(lecture);
168                computeEnrollments(configuration, subparts, subpartIds, enrollment, conflictWeight + delta);
169                enrollment.remove(lecture);
170            }
171        }
172    }
173    
174    /**
175     * Compute all possible enrollments
176     */
177    private void computeEnrollments() {
178        iEnrollments = new ArrayList<SctEnrollment>();
179        if (isInstructing()) {
180            double conflictWeight = 0.0;
181            for (Lecture lecture: getInstructingLectures()) {
182                for (Lecture other: getStudent().getLectures())
183                    if (!getModel().getOfferingId().equals(other.getConfiguration().getOfferingId()))
184                        conflictWeight += getJenrConflictWeight(lecture, other);
185            }
186            iEnrollments.add(new SctEnrollment(0, this, getInstructingLectures(), conflictWeight));
187            return;
188        }
189        for (Configuration configuration: getModel().getConfigurations()) {
190            Map<Long, Set<Lecture>> subparts = getModel().getSubparts(configuration);
191            List<Long> subpartIds = getSubpartIds(configuration);
192            computeEnrollments(configuration, subparts, subpartIds, new HashSet<Lecture>(), 0.0);
193        }
194        Collections.sort(iEnrollments);
195    }
196    
197    /**
198     * Return all possible enrollments of the given student into the given course
199     */
200    public List<SctEnrollment> getEnrollments() {
201        if (iEnrollments == null) computeEnrollments();
202        return iEnrollments;
203    }
204    
205    public List<SctEnrollment> getEnrollments(Comparator<SctEnrollment> cmp) {
206        if (iEnrollments == null) computeEnrollments();
207        if (cmp != null)
208            Collections.sort(iEnrollments, cmp);
209        return iEnrollments;
210    }
211    
212    /**
213     * Number of all possible enrollments of the given student into the given course
214     */
215    public int getNumberOfEnrollments() {
216        if (iEnrollments == null) computeEnrollments();
217        return iEnrollments.size();
218    }
219    
220    /**
221     * Average conflict weight
222     */
223    public double getAverageConflictWeight() {
224        if (iEnrollments == null) computeEnrollments();
225        return iTotalEnrollmentWeight / iEnrollments.size();
226    }
227    
228    /**
229     * Offering weight using {@link Student#getOfferingWeight(Long)}
230     */
231    public double getOfferingWeight() {
232        if (iOfferingWeight == null)
233            iOfferingWeight = getStudent().getOfferingWeight(getModel().getOfferingId());
234        return iOfferingWeight;
235    }
236    
237    /**
238     * Compare two students using their curriculum information
239     */
240    public int compare(Student s1, Student s2) {
241        int cmp = (s1.getCurriculum() == null ? "" : s1.getCurriculum()).compareToIgnoreCase(s2.getCurriculum() == null ? "" : s2.getCurriculum());
242        if (cmp != 0) return cmp;
243        cmp = (s1.getAcademicArea() == null ? "" : s1.getAcademicArea()).compareToIgnoreCase(s2.getAcademicArea() == null ? "" : s2.getAcademicArea());
244        if (cmp != 0) return cmp;
245        cmp = (s1.getMajor() == null ? "" : s1.getMajor()).compareToIgnoreCase(s2.getMajor() == null ? "" : s2.getMajor());
246        if (cmp != 0) return cmp;
247        cmp = (s1.getAcademicClassification() == null ? "" : s1.getAcademicClassification()).compareToIgnoreCase(s2.getAcademicClassification() == null ? "" : s2.getAcademicClassification());
248        if (cmp != 0) return cmp;
249        return s1.getId().compareTo(s2.getId());
250    }
251
252    /**
253     * Compare two student, using average conflict weight, number of possible enrollments and their curriculum information.
254     * Student that is harder to schedule goes first.
255     */
256    @Override
257    public int compareTo(SctStudent s) {
258        int cmp = Double.compare(s.getAverageConflictWeight(), getAverageConflictWeight());
259        if (cmp != 0) return cmp;
260        cmp = Double.compare(getNumberOfEnrollments(), s.getNumberOfEnrollments());
261        if (cmp != 0) return cmp;
262        return compare(getStudent(), s.getStudent());
263    }
264    
265    @Override
266    public String toString() {
267        return getStudent() + "{enrls:" + getEnrollments().size() + (getEnrollments().isEmpty() ? "" : ", best:" + getEnrollments().get(0).getConflictWeight()) + ", avg:" + getAverageConflictWeight() + "}";
268    }
269
270}