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