001package org.cpsolver.studentsct.model;
002
003import java.util.ArrayList;
004import java.util.HashSet;
005import java.util.List;
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.model.Model;
012import org.cpsolver.studentsct.reservation.Reservation;
013
014
015
016
017/**
018 * Representation of a configuration of an offering. A configuration contains
019 * id, name, an offering and a list of subparts. <br>
020 * <br>
021 * Each instructional offering (see {@link Offering}) contains one or more
022 * configurations. Each configuration contain one or more subparts. Each student
023 * has to take a class of each subpart of one of the possible configurations.
024 * 
025 * <br>
026 * <br>
027 * 
028 * @version StudentSct 1.3 (Student Sectioning)<br>
029 *          Copyright (C) 2007 - 2014 Tomas Muller<br>
030 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
031 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
032 * <br>
033 *          This library is free software; you can redistribute it and/or modify
034 *          it under the terms of the GNU Lesser General Public License as
035 *          published by the Free Software Foundation; either version 3 of the
036 *          License, or (at your option) any later version. <br>
037 * <br>
038 *          This library is distributed in the hope that it will be useful, but
039 *          WITHOUT ANY WARRANTY; without even the implied warranty of
040 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
041 *          Lesser General Public License for more details. <br>
042 * <br>
043 *          You should have received a copy of the GNU Lesser General Public
044 *          License along with this library; if not see
045 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
046 */
047public class Config extends AbstractClassWithContext<Request, Enrollment, Config.ConfigContext> {
048    private long iId = -1;
049    private String iName = null;
050    private Offering iOffering = null;
051    private int iLimit = -1;
052    private List<Subpart> iSubparts = new ArrayList<Subpart>();
053
054    /**
055     * Constructor
056     * 
057     * @param id
058     *            instructional offering configuration unique id
059     * @param limit
060     *            configuration limit (-1 for unlimited)
061     * @param name
062     *            configuration name
063     * @param offering
064     *            instructional offering to which this configuration belongs
065     */
066    public Config(long id, int limit, String name, Offering offering) {
067        iId = id;
068        iLimit = limit;
069        iName = name;
070        iOffering = offering;
071        iOffering.getConfigs().add(this);
072    }
073
074    /** Configuration id 
075     * @return instructional offering configuration unique id
076     **/
077    public long getId() {
078        return iId;
079    }
080    
081    /**
082     * Configuration limit. This is defines the maximal number of students that can be
083     * enrolled into this configuration at the same time. It is -1 in the case of an
084     * unlimited configuration
085     * @return configuration limit
086     */
087    public int getLimit() {
088        return iLimit;
089    }
090
091    /** Set configuration limit 
092     * @param limit configuration limit, -1 if unlimited
093     **/
094    public void setLimit(int limit) {
095        iLimit = limit;
096    }
097
098
099
100    /** Configuration name 
101     * @return configuration name
102     **/
103    public String getName() {
104        return iName;
105    }
106
107    /** Instructional offering to which this configuration belongs. 
108     * @return instructional offering
109     **/
110    public Offering getOffering() {
111        return iOffering;
112    }
113
114    /** List of subparts 
115     * @return scheduling subparts
116     **/
117    public List<Subpart> getSubparts() {
118        return iSubparts;
119    }
120
121    @Override
122    public String toString() {
123        return getName();
124    }
125
126    /** Average minimal penalty from {@link Subpart#getMinPenalty()} 
127     * @return minimal penalty
128     **/
129    public double getMinPenalty() {
130        double min = 0.0;
131        for (Subpart subpart : getSubparts()) {
132            min += subpart.getMinPenalty();
133        }
134        return min / getSubparts().size();
135    }
136
137    /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 
138     * @return maximal penalty
139     **/
140    public double getMaxPenalty() {
141        double max = 0.0;
142        for (Subpart subpart : getSubparts()) {
143            max += subpart.getMinPenalty();
144        }
145        return max / getSubparts().size();
146    }
147    
148    /**
149     * Available space in the configuration that is not reserved by any config reservation
150     * @param assignment current assignment
151     * @param excludeRequest excluding given request (if not null)
152     * @return available space
153     **/
154    public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
155        // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 
156        // (in which case there is no unreserved space)
157        if (getLimit() < 0) {
158            // exclude reservations that are not directly set on this section
159            for (Reservation r: getConfigReservations()) {
160                // ignore expired reservations
161                if (r.isExpired()) continue;
162                // there is an unlimited reservation -> no unreserved space
163                if (r.getLimit() < 0) return 0.0;
164            }
165            return Double.MAX_VALUE;
166        }
167        
168        double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
169        // exclude reservations that are not directly set on this section
170        for (Reservation r: getConfigReservations()) {
171            // ignore expired reservations
172            if (r.isExpired()) continue;
173            // unlimited reservation -> all the space is reserved
174            if (r.getLimit() < 0.0) return 0.0;
175            // compute space that can be potentially taken by this reservation
176            double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, excludeRequest);
177            // deduct the space from available space
178            available -= Math.max(0.0, reserved);
179        }
180        
181        return available;
182    }
183    
184    /**
185     * Total space in the configuration that cannot be reserved by any config reservation
186     * @return total unreserved space
187     **/
188    public synchronized double getTotalUnreservedSpace() {
189        if (iTotalUnreservedSpace == null)
190            iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache();
191        return iTotalUnreservedSpace;
192    }
193    private Double iTotalUnreservedSpace = null;
194    private double getTotalUnreservedSpaceNoCache() {
195        // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 
196        // (in which case there is no unreserved space)
197        if (getLimit() < 0) {
198            // exclude reservations that are not directly set on this section
199            for (Reservation r: getConfigReservations()) {
200                // ignore expired reservations
201                if (r.isExpired()) continue;
202                // there is an unlimited reservation -> no unreserved space
203                if (r.getLimit() < 0) return 0.0;
204            }
205            return Double.MAX_VALUE;
206        }
207        
208        // we need to check all reservations linked with this section
209        double available = getLimit(), reserved = 0, exclusive = 0;
210        Set<Config> configs = new HashSet<Config>();
211        reservations: for (Reservation r: getConfigReservations()) {
212            // ignore expired reservations
213            if (r.isExpired()) continue;
214            // unlimited reservation -> no unreserved space
215            if (r.getLimit() < 0) return 0.0;
216            for (Config s: r.getConfigs()) {
217                if (s.equals(this)) continue;
218                if (s.getLimit() < 0) continue reservations;
219                if (configs.add(s))
220                    available += s.getLimit();
221            }
222            reserved += r.getLimit();
223            if (r.getConfigs().size() == 1)
224                exclusive += r.getLimit();
225        }
226        
227        return Math.min(available - reserved, getLimit() - exclusive);
228    }
229    
230    /**
231     * Get reservations for this configuration
232     * @return related reservations
233     */
234    public synchronized List<Reservation> getReservations() {
235        if (iReservations == null) {
236            iReservations = new ArrayList<Reservation>();
237            for (Reservation r: getOffering().getReservations()) {
238                if (r.getConfigs().isEmpty() || r.getConfigs().contains(this))
239                    iReservations.add(r);
240            }
241        }
242        return iReservations;
243    }
244    List<Reservation> iReservations = null;
245    
246    /**
247     * Get reservations that require this configuration
248     * @return related reservations
249     */
250    public synchronized List<Reservation> getConfigReservations() {
251        if (iConfigReservations == null) {
252            iConfigReservations = new ArrayList<Reservation>();
253            for (Reservation r: getOffering().getReservations()) {
254                if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this))
255                    iConfigReservations.add(r);
256            }
257        }
258        return iConfigReservations;
259    }
260    List<Reservation> iConfigReservations = null;
261    
262    /**
263     * Clear reservation information that was cached on this configuration or below
264     */
265    public synchronized void clearReservationCache() {
266        for (Subpart s: getSubparts())
267            s.clearReservationCache();
268        iReservations = null;
269        iConfigReservations = null;
270        iTotalUnreservedSpace = null;
271    }
272    
273    @Override
274    public boolean equals(Object o) {
275        if (o == null || !(o instanceof Config)) return false;
276        return getId() == ((Config)o).getId();
277    }
278    
279    @Override
280    public int hashCode() {
281        return new Long(getId()).hashCode();
282    }
283    
284    @Override
285    public Model<Request, Enrollment> getModel() {
286        return getOffering().getModel();
287    }
288    
289    /** Set of assigned enrollments 
290     * @param assignment current assignment
291     * @return enrollments in this configuration
292     **/
293    public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) {
294        return getContext(assignment).getEnrollments();
295    }
296    
297    /**
298     * Enrollment weight -- weight of all requests which have an enrollment that
299     * contains this config, excluding the given one. See
300     * {@link Request#getWeight()}.
301     * @param assignment current assignment
302     * @param excludeRequest request to exclude, null if all requests are to be included
303     * @return enrollment weight
304     */
305    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
306        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
307    }
308    
309    /**
310     * Maximal weight of a single enrollment in the config
311     * @param assignment current assignment
312     * @return maximal enrollment weight
313     */
314    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
315        return getContext(assignment).getMaxEnrollmentWeight();
316    }
317
318    /**
319     * Minimal weight of a single enrollment in the config
320     * @param assignment current assignment
321     * @return minimal enrollment weight
322     */
323    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
324        return getContext(assignment).getMinEnrollmentWeight();
325    }
326
327    @Override
328    public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
329        return new ConfigContext(assignment);
330    }
331    
332    public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> {
333        private double iEnrollmentWeight = 0.0;
334        private double iMaxEnrollmentWeight = 0.0;
335        private double iMinEnrollmentWeight = 0.0;
336        private Set<Enrollment> iEnrollments = new HashSet<Enrollment>();
337
338        public ConfigContext(Assignment<Request, Enrollment> assignment) {
339            for (Course course: getOffering().getCourses()) {
340                for (CourseRequest request: course.getRequests()) {
341                    Enrollment enrollment = assignment.getValue(request);
342                    if (enrollment != null && Config.this.equals(enrollment.getConfig()))
343                        assigned(assignment, enrollment);
344                }
345            }
346        }
347
348        /** Called when an enrollment with this config is assigned to a request */
349        @Override
350        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
351            if (iEnrollments.isEmpty()) {
352                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
353            } else {
354                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
355                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
356            }
357            if (iEnrollments.add(enrollment))
358                iEnrollmentWeight += enrollment.getRequest().getWeight();
359        }
360
361        /** Called when an enrollment with this config is unassigned from a request */
362        @Override
363        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
364            if (iEnrollments.remove(enrollment))
365                iEnrollmentWeight -= enrollment.getRequest().getWeight();
366            if (iEnrollments.isEmpty()) {
367                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
368            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
369                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
370                    double newMinEnrollmentWeight = Double.MAX_VALUE;
371                    for (Enrollment e : iEnrollments) {
372                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
373                            newMinEnrollmentWeight = iMinEnrollmentWeight;
374                            break;
375                        } else {
376                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
377                        }
378                    }
379                    iMinEnrollmentWeight = newMinEnrollmentWeight;
380                }
381                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
382                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
383                    for (Enrollment e : iEnrollments) {
384                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
385                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
386                            break;
387                        } else {
388                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
389                        }
390                    }
391                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
392                }
393            }
394        }
395        
396        /**
397         * Enrollment weight -- weight of all requests which have an enrollment that
398         * contains this config, excluding the given one. See
399         * {@link Request#getWeight()}.
400         * @param assignment current assignment
401         * @param excludeRequest request to exclude
402         * @return enrollment weight
403         */
404        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
405            double weight = iEnrollmentWeight;
406            if (excludeRequest != null && assignment.getValue(excludeRequest) != null && iEnrollments.contains(assignment.getValue(excludeRequest)))
407                weight -= excludeRequest.getWeight();
408            return weight;
409        }
410        
411        /** Set of assigned enrollments 
412         * @return assigned enrollments (using this configuration)
413         **/
414        public Set<Enrollment> getEnrollments() {
415            return iEnrollments;
416        }
417
418        /**
419         * Maximal weight of a single enrollment in the config
420         * @return maximal enrollment weight
421         */
422        public double getMaxEnrollmentWeight() {
423            return iMaxEnrollmentWeight;
424        }
425
426        /**
427         * Minimal weight of a single enrollment in the config
428         * @return minimal enrollment weight
429         */
430        public double getMinEnrollmentWeight() {
431            return iMinEnrollmentWeight;
432        }
433    }
434}