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