    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}