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