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}