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