    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}