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 * @author  Tomáš Müller
025 * @version CourseTT 1.3 (University Course Timetabling)<br>
026 *          Copyright (C) 2017 Tomáš Müller<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 SctStudent implements Comparable<SctStudent> {
045    private SctModel iModel;
046    private Student iStudent;
047    private List<SctEnrollment> iEnrollments = null;
048    private double iTotalEnrollmentWeight = 0.0;
049    private Double iOfferingWeight = null;
050    private List<Lecture> iInstructing = null;
051    
052    /**
053     * Constructor.
054     * @param model student sectioning model
055     * @param student student to represent
056     */
057    public SctStudent(SctModel model, Student student) {
058        iStudent = student;
059        iModel = model;
060    }
061    
062    /**
063     * Student sectioning model
064     */
065    public SctModel getModel() { return iModel; }
066    
067    /**
068     * Student for which the possible enrollments are being computed
069     */
070    public Student getStudent() { return iStudent; }
071    
072    /**
073     * Current enrollment of this student
074     * @param checkInstructor if the student is also instructor and he/she is instructing this class, return classes he/she is instructing instead
075     * @return current enrollment of this student into the given course
076     */
077    public SctEnrollment getCurrentEnrollment(boolean checkInstructor) {
078        if (checkInstructor && isInstructing())
079            return new SctEnrollment(-1, this, getInstructingLectures());
080        List<Lecture> lectures = new ArrayList<Lecture>();
081        for (Lecture lecture: getStudent().getLectures())
082            if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId()))
083                lectures.add(lecture);
084        return new SctEnrollment(-1, this, lectures);
085    }
086    
087    /**
088     * List of lectures of the given course that the student is instructing (if he/she is also an instructor, using {@link Student#getInstructor()})
089     * @return list of lectures of the given course that the student is instructing
090     */
091    public List<Lecture> getInstructingLectures() {
092        if (iInstructing == null && getStudent().getInstructor() != null) {
093            iInstructing = new ArrayList<Lecture>();
094            for (Lecture lecture: getStudent().getInstructor().variables())
095                if (getModel().getOfferingId().equals(lecture.getConfiguration().getOfferingId()))
096                    iInstructing.add(lecture);
097        }
098        return iInstructing;
099    }
100    
101    /**
102     * Is student also an instructor of the given course?
103     */
104    public boolean isInstructing() {
105        return getStudent().getInstructor() != null && !getInstructingLectures().isEmpty();
106    }
107    
108    /**
109     * Conflict weight of a lecture pair
110     */
111    public double getJenrConflictWeight(Lecture l1, Lecture l2) {
112        Placement p1 = getModel().getAssignment().getValue(l1);
113        Placement p2 = getModel().getAssignment().getValue(l2);
114        if (p1 == null || p2 == null) return 0.0;
115        if (getModel().getStudentConflictCriteria() == null) {
116            if (JenrlConstraint.isInConflict(p1, p2, getModel().getTimetableModel().getDistanceMetric(), getModel().getTimetableModel().getStudentWorkDayLimit()))
117                return getStudent().getJenrlWeight(l1, l2);
118            return 0.0;
119        }
120        double weight = 0.0;
121        for (StudentConflict sc: getModel().getStudentConflictCriteria())
122            if (sc.isApplicable(getStudent(), p1.variable(), p2.variable()) && sc.inConflict(p1, p2))
123                weight += sc.getWeight() * getStudent().getJenrlWeight(l1, l2);
124        return weight;
125    }
126    
127    /**
128     * All scheduling subpart ids of a configuration.
129     */
130    private List<Long> getSubpartIds(Configuration configuration) {
131        List<Long> subpartIds = new ArrayList<Long>();
132        Queue<Lecture> queue = new LinkedList<Lecture>();
133        for (Map.Entry<Long, Set<Lecture>> e: configuration.getTopLectures().entrySet()) {
134            subpartIds.add(e.getKey());
135            queue.add(e.getValue().iterator().next());
136        }
137        Lecture lecture = null;
138        while ((lecture = queue.poll()) != null) {
139            if (lecture.getChildren() != null)
140                for (Map.Entry<Long, List<Lecture>> e: lecture.getChildren().entrySet()) {
141                    subpartIds.add(e.getKey());
142                    queue.add(e.getValue().iterator().next());
143                }
144        }
145        return subpartIds;
146    }
147    
148    /**
149     * Compute all possible enrollments
150     */
151    private void computeEnrollments(Configuration configuration, Map<Long, Set<Lecture>> subparts, List<Long> subpartIds, Set<Lecture> enrollment, double conflictWeight) {
152        if (enrollment.size() == subpartIds.size()) {
153            iEnrollments.add(new SctEnrollment(iEnrollments.size(), this, enrollment, conflictWeight));
154            iTotalEnrollmentWeight += conflictWeight;
155        } else {
156            Set<Lecture> lectures = subparts.get(subpartIds.get(enrollment.size()));
157            for (Lecture lecture: lectures) {
158                if (lecture.getParent() != null && !enrollment.contains(lecture.getParent())) continue;
159                if (!getStudent().canEnroll(lecture)) continue;
160                double delta = 0.0;
161                for (Lecture other: getStudent().getLectures())
162                    if (!configuration.getOfferingId().equals(other.getConfiguration().getOfferingId()))
163                        delta += getJenrConflictWeight(lecture, other);
164                for (Lecture other: enrollment)
165                    delta += getJenrConflictWeight(lecture, other);
166                enrollment.add(lecture);
167                computeEnrollments(configuration, subparts, subpartIds, enrollment, conflictWeight + delta);
168                enrollment.remove(lecture);
169            }
170        }
171    }
172    
173    /**
174     * Compute all possible enrollments
175     */
176    private void computeEnrollments() {
177        iEnrollments = new ArrayList<SctEnrollment>();
178        if (isInstructing()) {
179            double conflictWeight = 0.0;
180            for (Lecture lecture: getInstructingLectures()) {
181                for (Lecture other: getStudent().getLectures())
182                    if (!getModel().getOfferingId().equals(other.getConfiguration().getOfferingId()))
183                        conflictWeight += getJenrConflictWeight(lecture, other);
184            }
185            iEnrollments.add(new SctEnrollment(0, this, getInstructingLectures(), conflictWeight));
186            return;
187        }
188        for (Configuration configuration: getModel().getConfigurations()) {
189            Map<Long, Set<Lecture>> subparts = getModel().getSubparts(configuration);
190            List<Long> subpartIds = getSubpartIds(configuration);
191            computeEnrollments(configuration, subparts, subpartIds, new HashSet<Lecture>(), 0.0);
192        }
193        Collections.sort(iEnrollments);
194    }
195    
196    /**
197     * Return all possible enrollments of the given student into the given course
198     */
199    public List<SctEnrollment> getEnrollments() {
200        if (iEnrollments == null) computeEnrollments();
201        return iEnrollments;
202    }
203    
204    public List<SctEnrollment> getEnrollments(Comparator<SctEnrollment> cmp) {
205        if (iEnrollments == null) computeEnrollments();
206        if (cmp != null)
207            Collections.sort(iEnrollments, cmp);
208        return iEnrollments;
209    }
210    
211    /**
212     * Number of all possible enrollments of the given student into the given course
213     */
214    public int getNumberOfEnrollments() {
215        if (iEnrollments == null) computeEnrollments();
216        return iEnrollments.size();
217    }
218    
219    /**
220     * Average conflict weight
221     */
222    public double getAverageConflictWeight() {
223        if (iEnrollments == null) computeEnrollments();
224        return iTotalEnrollmentWeight / iEnrollments.size();
225    }
226    
227    /**
228     * Offering weight using {@link Student#getOfferingWeight(Long)}
229     */
230    public double getOfferingWeight() {
231        if (iOfferingWeight == null)
232            iOfferingWeight = getStudent().getOfferingWeight(getModel().getOfferingId());
233        return iOfferingWeight;
234    }
235    
236    /**
237     * Compare two students using their curriculum information
238     */
239    public int compare(Student s1, Student s2) {
240        int cmp = (s1.getCurriculum() == null ? "" : s1.getCurriculum()).compareToIgnoreCase(s2.getCurriculum() == null ? "" : s2.getCurriculum());
241        if (cmp != 0) return cmp;
242        cmp = (s1.getAcademicArea() == null ? "" : s1.getAcademicArea()).compareToIgnoreCase(s2.getAcademicArea() == null ? "" : s2.getAcademicArea());
243        if (cmp != 0) return cmp;
244        cmp = (s1.getMajor() == null ? "" : s1.getMajor()).compareToIgnoreCase(s2.getMajor() == null ? "" : s2.getMajor());
245        if (cmp != 0) return cmp;
246        cmp = (s1.getAcademicClassification() == null ? "" : s1.getAcademicClassification()).compareToIgnoreCase(s2.getAcademicClassification() == null ? "" : s2.getAcademicClassification());
247        if (cmp != 0) return cmp;
248        return s1.getId().compareTo(s2.getId());
249    }
250
251    /**
252     * Compare two student, using average conflict weight, number of possible enrollments and their curriculum information.
253     * Student that is harder to schedule goes first.
254     */
255    @Override
256    public int compareTo(SctStudent s) {
257        int cmp = Double.compare(s.getAverageConflictWeight(), getAverageConflictWeight());
258        if (cmp != 0) return cmp;
259        cmp = Double.compare(getNumberOfEnrollments(), s.getNumberOfEnrollments());
260        if (cmp != 0) return cmp;
261        return compare(getStudent(), s.getStudent());
262    }
263    
264    @Override
265    public String toString() {
266        return getStudent() + "{enrls:" + getEnrollments().size() + (getEnrollments().isEmpty() ? "" : ", best:" + getEnrollments().get(0).getConflictWeight()) + ", avg:" + getAverageConflictWeight() + "}";
267    }
268
269}