    001package org.cpsolver.studentsct.model;
    002
    003import java.util.HashMap;
    004import java.util.HashSet;
    005import java.util.Map;
    006import java.util.Set;
    007
    008import org.cpsolver.ifs.assignment.Assignment;
    009import org.cpsolver.ifs.assignment.context.AbstractClassWithContext;
    010import org.cpsolver.ifs.assignment.context.AssignmentConstraintContext;
    011import org.cpsolver.ifs.assignment.context.CanInheritContext;
    012import org.cpsolver.ifs.model.Model;
    013import org.cpsolver.studentsct.reservation.Reservation;
    014
    015/**
    016 * Representation of a group of students requesting the same course that
    017 * should be scheduled in the same set of sections.<br>
    018 * <br>
    019 *
    020 * @author  Tomáš Müller
    021 * @version StudentSct 1.3 (Student Sectioning)<br>
    022 *          Copyright (C) 2015 Tomáš Müller<br>
    023 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    024 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    025 * <br>
    026 *          This library is free software; you can redistribute it and/or modify
    027 *          it under the terms of the GNU Lesser General Public License as
    028 *          published by the Free Software Foundation; either version 3 of the
    029 *          License, or (at your option) any later version. <br>
    030 * <br>
    031 *          This library is distributed in the hope that it will be useful, but
    032 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    033 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    034 *          Lesser General Public License for more details. <br>
    035 * <br>
    036 *          You should have received a copy of the GNU Lesser General Public
    037 *          License along with this library; if not see
    038 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
    039 */
    040public class RequestGroup extends AbstractClassWithContext<Request, Enrollment, RequestGroup.RequestGroupContext>
    041    implements CanInheritContext<Request, Enrollment, RequestGroup.RequestGroupContext>{
    042    private long iId = -1;
    043    private String iName = null;
    044    private Course iCourse;
    045    private Set<CourseRequest> iRequests = new HashSet<CourseRequest>();
    046    private double iTotalWeight = 0.0;
    047
    048    /**
    049     * Creates request group. Pair (id, course) must be unique.
    050     * @param id identification of the group
    051     * @param name group name
    052     * @param course course for which the group is created (only course requests for this course can be of this group)
    053     */
    054    public RequestGroup(long id, String name, Course course) {
    055        iId = id;
    056        iName = name;
    057        iCourse = course;
    058        iCourse.getRequestGroups().add(this);
    059    }
    060
    061    /**
    062     * Add course request to the group. It has to contain the course of this group {@link RequestGroup#getCourse()}.
    063     * This is done automatically by {@link CourseRequest#addRequestGroup(RequestGroup)}.
    064     * @param request course request to be added to this group
    065     */
    066    public void addRequest(CourseRequest request) {
    067        if (iRequests.add(request))
    068            iTotalWeight += request.getWeight();
    069    }
    070
    071    /**
    072     * Remove course request from the group. This is done automatically by {@link CourseRequest#removeRequestGroup(RequestGroup)}.
    073     * @param request course request to be removed from this group
    074     */
    075    public void removeRequest(CourseRequest request) {
    076        if (iRequests.remove(request))
    077            iTotalWeight -= request.getWeight();
    078    }
    079
    080    /**
    081     * Return the set of course requests that are associated with this group.
    082     * @return course requests of this group
    083     */
    084    public Set<CourseRequest> getRequests() {
    085        return iRequests;
    086    }
    087
    088    /**
    089     * Total weight (using {@link CourseRequest#getWeight()}) of the course requests of this group
    090     * @return total weight of course requests in this group
    091     */
    092    public double getTotalWeight() {
    093        return iTotalWeight;
    094    }
    095
    096    /**
    097     * Request group id
    098     * @return request group id
    099     */
    100    public long getId() {
    101        return iId;
    102    }
    103
    104    /**
    105     * Request group name
    106     * @return request group name
    107     */
    108    public String getName() {
    109        return iName;
    110    }
    111
    112    /**
    113     * Course associated with this group. Only course requests for this course can be of this group.
    114     * @return course of this request group
    115     */
    116    public Course getCourse() {
    117        return iCourse;
    118    }
    119
    120    @Override
    121    public boolean equals(Object o) {
    122        if (o == null || !(o instanceof RequestGroup)) return false;
    123        return getId() == ((RequestGroup)o).getId() && getCourse().getId() == ((RequestGroup)o).getCourse().getId();
    124    }
    125
    126    @Override
    127    public int hashCode() {
    128        return (int) (iId ^ (iCourse.getId() >>> 32));
    129    }
    130
    131    /** Called when an enrollment is assigned to a request of this request group */
    132    public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    133        getContext(assignment).assigned(assignment, enrollment);
    134    }
    135
    136    /** Called when an enrollment is unassigned from a request of this request group */
    137    public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    138        getContext(assignment).unassigned(assignment, enrollment);
    139    }
    140
    141    /**
    142     * Enrollment weight -- weight of all requests which have an enrollment that
    143     * is of this request group, excluding the given one. See
    144     * {@link Request#getWeight()}.
    145     * @param assignment current assignment
    146     * @param excludeRequest course request to ignore, if any
    147     * @return enrollment weight
    148     */
    149    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    150        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
    151    }
    152
    153    /**
    154     * Section weight -- weight of all requests which have an enrollment that
    155     * is of this request group and that includes the given section, excluding the given one. See
    156     * {@link Request#getWeight()}.
    157     * @param assignment current assignment
    158     * @param section section in question
    159     * @param excludeRequest course request to ignore, if any
    160     * @return enrollment weight
    161     */
    162    public double getSectionWeight(Assignment<Request, Enrollment> assignment, Section section, Request excludeRequest) {
    163        return getContext(assignment).getSectionWeight(assignment, section, excludeRequest);
    164    }
    165
    166    /**
    167     * Return how much is the given enrollment similar to other enrollments of this group.
    168     * @param assignment current assignment
    169     * @param enrollment enrollment in question
    170     * @param bestRatio how much of the weight should be used on estimation of the enrollment potential
    171     *        (considering that students of this group that are not yet enrolled can take the same enrollment)
    172     * @param fillRatio how much of the weight should be used in estimation how well are the sections of this enrollments going to be filled
    173     *        (bestRatio + fillRatio &lt;= 1.0)
    174     * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    175     */
    176    public double getEnrollmentSpread(Assignment<Request, Enrollment> assignment, Enrollment enrollment, double bestRatio, double fillRatio) {
    177        return getContext(assignment).getEnrollmentSpread(assignment, enrollment, bestRatio, fillRatio);
    178    }
    179
    180    /**
    181     * Return average section spread of this group. It reflects the probability of two students of this group
    182     * being enrolled in the same section.
    183     * @param assignment current assignment
    184     * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    185     */
    186    public double getAverageSpread(Assignment<Request, Enrollment> assignment) {
    187        return getContext(assignment).getAverageSpread();
    188    }
    189
    190    /**
    191     * Return section spread of this group. It reflects the probability of two students of this group
    192     * being enrolled in this section.
    193     * @param assignment current assignment
    194     * @param section given section
    195     * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    196     */
    197    public double getSectionSpread(Assignment<Request, Enrollment> assignment, Section section) {
    198        return getContext(assignment).getSectionSpread(section);
    199    }
    200
    201    public class RequestGroupContext implements AssignmentConstraintContext<Request, Enrollment> {
    202        private Set<Enrollment> iEnrollments = null;
    203        private double iEnrollmentWeight = 0.0;
    204        private Map<Long, Double> iSectionWeight = null;
    205        private boolean iReadOnly = false;
    206
    207        public RequestGroupContext(Assignment<Request, Enrollment> assignment) {
    208            iEnrollments = new HashSet<Enrollment>();
    209            iSectionWeight = new HashMap<Long, Double>();
    210            for (CourseRequest request: getCourse().getRequests()) {
    211                if (request.getRequestGroups().contains(RequestGroup.this)) {
    212                    Enrollment enrollment = assignment.getValue(request);
    213                    if (enrollment != null && getCourse().equals(enrollment.getCourse()))
    214                        assigned(assignment, enrollment);
    215                }
    216            }
    217        }
    218
    219        public RequestGroupContext(RequestGroupContext parent) {
    220            iEnrollmentWeight = parent.iEnrollmentWeight;
    221            iEnrollments = parent.iEnrollments;
    222            iSectionWeight = parent.iSectionWeight;
    223            iReadOnly = true;
    224        }
    225
    226        /** Called when an enrollment is assigned to a request of this request group */
    227        @Override
    228        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    229            if (iReadOnly) {
    230                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    231                iSectionWeight = new HashMap<Long, Double>(iSectionWeight);
    232                iReadOnly = false;
    233            }
    234            if (iEnrollments.add(enrollment)) {
    235                iEnrollmentWeight += enrollment.getRequest().getWeight();
    236                for (Section section: enrollment.getSections()) {
    237                    Double weight = iSectionWeight.get(section.getId());
    238                    iSectionWeight.put(section.getId(), enrollment.getRequest().getWeight() + (weight == null ? 0.0 : weight.doubleValue()));
    239                }
    240            }
    241        }
    242
    243        /** Called when an enrollment is unassigned from a request of this request group */
    244        @Override
    245        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
    246            if (iReadOnly) {
    247                iEnrollments = new HashSet<Enrollment>(iEnrollments);
    248                iSectionWeight = new HashMap<Long, Double>(iSectionWeight);
    249                iReadOnly = false;
    250            }
    251            if (iEnrollments.remove(enrollment)) {
    252                iEnrollmentWeight -= enrollment.getRequest().getWeight();
    253                for (Section section: enrollment.getSections()) {
    254                    Double weight = iSectionWeight.get(section.getId());
    255                    iSectionWeight.put(section.getId(), weight - enrollment.getRequest().getWeight());
    256                }
    257            }
    258        }
    259
    260        /** Set of assigned enrollments
    261         * @return assigned enrollments of this request group
    262         **/
    263        public Set<Enrollment> getEnrollments() {
    264            return iEnrollments;
    265        }
    266
    267        /**
    268         * Enrollment weight -- weight of all requests which have an enrollment that
    269         * is of this request group, excluding the given one. See
    270         * {@link Request#getWeight()}.
    271         * @param assignment current assignment
    272         * @param excludeRequest course request to ignore, if any
    273         * @return enrollment weight
    274         */
    275        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
    276            double weight = iEnrollmentWeight;
    277            if (excludeRequest != null) {
    278                Enrollment enrollment = assignment.getValue(excludeRequest);
    279                if (enrollment!= null && iEnrollments.contains(enrollment))
    280                    weight -= excludeRequest.getWeight();
    281            }
    282            return weight;
    283        }
    284
    285        /**
    286         * Section weight -- weight of all requests which have an enrollment that
    287         * is of this request group and that includes the given section, excluding the given one. See
    288         * {@link Request#getWeight()}.
    289         * @param assignment current assignment
    290         * @param section section in question
    291         * @param excludeRequest course request to ignore, if any
    292         * @return enrollment weight
    293         */
    294        public double getSectionWeight(Assignment<Request, Enrollment> assignment, Section section, Request excludeRequest) {
    295            Double weight = iSectionWeight.get(section.getId());
    296            if (excludeRequest != null && weight != null) {
    297                Enrollment enrollment = assignment.getValue(excludeRequest);
    298                if (enrollment!= null && iEnrollments.contains(enrollment) && enrollment.getSections().contains(section))
    299                    weight -= excludeRequest.getWeight();
    300            }
    301            return (weight == null ? 0.0 : weight.doubleValue());
    302        }
    303
    304        /**
    305         * Return space available in the given section
    306         * @param assignment current assignment
    307         * @param section section to check
    308         * @param enrollment enrollment in question
    309         * @return check section reservations, if present; use unreserved space otherwise
    310         */
    311        private double getAvailableSpace(Assignment<Request, Enrollment> assignment, Section section, Enrollment enrollment) {
    312            Reservation reservation = enrollment.getReservation();
    313            Set<Section> sections = (reservation == null ? null : reservation.getSections(section.getSubpart()));
    314            if (reservation != null && sections != null && sections.contains(section) && !reservation.isExpired()) {
    315                double sectionAvailable = (section.getLimit() < 0 ? Double.MAX_VALUE : section.getLimit() - section.getEnrollmentWeight(assignment, enrollment.getRequest()));
    316                double reservationAvailable = reservation.getReservedAvailableSpace(assignment, enrollment.getConfig(), enrollment.getRequest());
    317                return Math.min(sectionAvailable, reservationAvailable) + (reservation.mustBeUsed() ? 0.0 : section.getUnreservedSpace(assignment, enrollment.getRequest()));
    318            } else {
    319                return section.getUnreservedSpace(assignment, enrollment.getRequest());
    320            }
    321        }
    322
    323        /**
    324         * Return how much is the given enrollment (which is not part of the request group) creating an issue for this request group
    325         * @param assignment current assignment
    326         * @param enrollment enrollment in question
    327         * @param bestRatio how much of the weight should be used on estimation of the enrollment potential
    328         *      (considering that students of this group that are not yet enrolled can take the same enrollment)
    329         * @param fillRatio how much of the weight should be used in estimation how well are the sections of this enrollments going to be filled
    330         *      (bestRatio + fillRatio &lt;= 1.0)
    331         * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    332         */
    333        public double getEnrollmentSpread(Assignment<Request, Enrollment> assignment, Enrollment enrollment, double bestRatio, double fillRatio) {
    334            if (iTotalWeight <= 1.0) return 1.0;
    335
    336            // enrollment weight (excluding the given enrollment)
    337            double totalEnrolled = getEnrollmentWeight(assignment, enrollment.getRequest());
    338            double totalRemaining = iTotalWeight - totalEnrolled;
    339
    340            // section weight (also excluding the given enrollment)
    341            Enrollment e = assignment.getValue(enrollment.getRequest());
    342            double enrollmentPairs = 0.0, bestPairs = 0.0, fill = 0.0;
    343            for (Section section: enrollment.getSections()) {
    344                double potential = Math.max(Math.min(totalRemaining, getAvailableSpace(assignment, section, enrollment)), enrollment.getRequest().getWeight());
    345                Double enrolled = iSectionWeight.get(section.getId());
    346                if (enrolled != null) {
    347                    if (e != null && e.getSections().contains(section))
    348                        enrolled -= enrollment.getRequest().getWeight();
    349                    potential += enrolled;
    350                    enrollmentPairs += enrolled * (enrolled + 1.0);
    351                }
    352                bestPairs += potential * (potential - 1.0);
    353                if (section.getLimit() > potential) {
    354                    fill += potential / section.getLimit();
    355                } else {
    356                    fill += 1.0;
    357                }
    358            }
    359            double pEnrl = (totalEnrolled < 1.0 ? 0.0 : (enrollmentPairs / enrollment.getSections().size()) / (totalEnrolled * (totalEnrolled + 1.0)));
    360            double pBest = (bestPairs / enrollment.getSections().size()) / (iTotalWeight * (iTotalWeight - 1.0));
    361            double pFill = fill / enrollment.getSections().size();
    362
    363            return (1.0 - bestRatio - fillRatio) * pEnrl + bestRatio * pBest + fillRatio * pFill;
    364        }
    365
    366        /**
    367         * Return average section spread of this group. It reflects the probability of two students of this group
    368         * being enrolled in the same section.
    369         * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    370         */
    371        public double getAverageSpread() {
    372            // none or just one enrollment -> all the same
    373            if (iEnrollmentWeight <= 1.0) return 1.0;
    374
    375            double weight = 0.0;
    376            for (Config config: getCourse().getOffering().getConfigs()) {
    377                double pairs = 0.0;
    378                for (Subpart subpart: config.getSubparts())
    379                    for (Section section: subpart.getSections()) {
    380                        Double enrollment = iSectionWeight.get(section.getId());
    381                        if (enrollment != null && enrollment > 1.0)
    382                            pairs += enrollment * (enrollment - 1);
    383                    }
    384                weight += (pairs / config.getSubparts().size()) / (iEnrollmentWeight * (iEnrollmentWeight - 1.0));
    385            }
    386            return weight;
    387        }
    388
    389        /**
    390         * Return section spread of this group. It reflects the probability of two students of this group
    391         * being enrolled in this section.
    392         * @param section given section
    393         * @return 1.0 if all enrollments have the same sections as the given one, 0.0 if there is no match at all
    394         */
    395        public double getSectionSpread(Section section) {
    396            Double w = iSectionWeight.get(section.getId());
    397            if (w != null && w > 1.0) {
    398                return (w * (w - 1.0)) / (iEnrollmentWeight * (iEnrollmentWeight - 1.0));
    399            } else {
    400                return 0.0;
    401            }
    402        }
    403    }
    404
    405    @Override
    406    public RequestGroupContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
    407        return new RequestGroupContext(assignment);
    408    }
    409
    410    @Override
    411    public RequestGroupContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, RequestGroupContext parentContext) {
    412        return new RequestGroupContext(parentContext);
    413    }
    414
    415    @Override
    416    public Model<Request, Enrollment> getModel() {
    417        return getCourse().getModel();
    418    }
    419}