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    private Long iInstrMethodId;
055    private String iInstrMethodName;
056
057    /**
058     * Constructor
059     * 
060     * @param id
061     *            instructional offering configuration unique id
062     * @param limit
063     *            configuration limit (-1 for unlimited)
064     * @param name
065     *            configuration name
066     * @param offering
067     *            instructional offering to which this configuration belongs
068     */
069    public Config(long id, int limit, String name, Offering offering) {
070        iId = id;
071        iLimit = limit;
072        iName = name;
073        iOffering = offering;
074        iOffering.getConfigs().add(this);
075    }
076
077    /** Configuration id 
078     * @return instructional offering configuration unique id
079     **/
080    public long getId() {
081        return iId;
082    }
083    
084    /**
085     * Configuration limit. This is defines the maximal number of students that can be
086     * enrolled into this configuration at the same time. It is -1 in the case of an
087     * unlimited configuration
088     * @return configuration limit
089     */
090    public int getLimit() {
091        return iLimit;
092    }
093
094    /** Set configuration limit 
095     * @param limit configuration limit, -1 if unlimited
096     **/
097    public void setLimit(int limit) {
098        iLimit = limit;
099    }
100
101
102
103    /** Configuration name 
104     * @return configuration name
105     **/
106    public String getName() {
107        return iName;
108    }
109
110    /** Instructional offering to which this configuration belongs. 
111     * @return instructional offering
112     **/
113    public Offering getOffering() {
114        return iOffering;
115    }
116
117    /** List of subparts 
118     * @return scheduling subparts
119     **/
120    public List<Subpart> getSubparts() {
121        return iSubparts;
122    }
123    
124    /**
125     * Return instructional method id
126     * @return instructional method id
127     */
128    public Long getInstructionalMethodId() { return iInstrMethodId; }
129    
130    /**
131     * Set instructional method id
132     * @param instrMethodId instructional method id
133     */
134    public void setInstructionalMethodId(Long instrMethodId) { iInstrMethodId = instrMethodId; }
135    
136    /**
137     * Return instructional method name
138     * @return instructional method name
139     */
140    public String getInstructionalMethodName() { return iInstrMethodName; }
141    
142    /**
143     * Set instructional method name
144     * @param instrMethodName instructional method name
145     */
146    public void setInstructionalMethodName(String instrMethodName) { iInstrMethodName = instrMethodName; }
147
148    @Override
149    public String toString() {
150        return getName();
151    }
152
153    /** Average minimal penalty from {@link Subpart#getMinPenalty()} 
154     * @return minimal penalty
155     **/
156    public double getMinPenalty() {
157        double min = 0.0;
158        for (Subpart subpart : getSubparts()) {
159            min += subpart.getMinPenalty();
160        }
161        return min / getSubparts().size();
162    }
163
164    /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 
165     * @return maximal penalty
166     **/
167    public double getMaxPenalty() {
168        double max = 0.0;
169        for (Subpart subpart : getSubparts()) {
170            max += subpart.getMinPenalty();
171        }
172        return max / getSubparts().size();
173    }
174    
175    /**
176     * Available space in the configuration that is not reserved by any config reservation
177     * @param assignment current assignment
178     * @param excludeRequest excluding given request (if not null)
179     * @return available space
180     **/
181    public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
182        // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 
183        // (in which case there is no unreserved space)
184        if (getLimit() < 0) {
185            // exclude reservations that are not directly set on this section
186            for (Reservation r: getConfigReservations()) {
187                // ignore expired reservations
188                if (r.isExpired()) continue;
189                // there is an unlimited reservation -> no unreserved space
190                if (r.getLimit() < 0) return 0.0;
191            }
192            return Double.MAX_VALUE;
193        }
194        
195        double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
196        // exclude reservations that are not directly set on this section
197        for (Reservation r: getConfigReservations()) {
198            // ignore expired reservations
199            if (r.isExpired()) continue;
200            // unlimited reservation -> all the space is reserved
201            if (r.getLimit() < 0.0) return 0.0;
202            // compute space that can be potentially taken by this reservation
203            double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, excludeRequest);
204            // deduct the space from available space
205            available -= Math.max(0.0, reserved);
206        }
207        
208        return available;
209    }
210    
211    /**
212     * Total space in the configuration that cannot be reserved by any config reservation
213     * @return total unreserved space
214     **/
215    public synchronized double getTotalUnreservedSpace() {
216        if (iTotalUnreservedSpace == null)
217            iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache();
218        return iTotalUnreservedSpace;
219    }
220    private Double iTotalUnreservedSpace = null;
221    private double getTotalUnreservedSpaceNoCache() {
222        // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 
223        // (in which case there is no unreserved space)
224        if (getLimit() < 0) {
225            // exclude reservations that are not directly set on this section
226            for (Reservation r: getConfigReservations()) {
227                // ignore expired reservations
228                if (r.isExpired()) continue;
229                // there is an unlimited reservation -> no unreserved space
230                if (r.getLimit() < 0) return 0.0;
231            }
232            return Double.MAX_VALUE;
233        }
234        
235        // we need to check all reservations linked with this section
236        double available = getLimit(), reserved = 0, exclusive = 0;
237        Set<Config> configs = new HashSet<Config>();
238        reservations: for (Reservation r: getConfigReservations()) {
239            // ignore expired reservations
240            if (r.isExpired()) continue;
241            // unlimited reservation -> no unreserved space
242            if (r.getLimit() < 0) return 0.0;
243            for (Config s: r.getConfigs()) {
244                if (s.equals(this)) continue;
245                if (s.getLimit() < 0) continue reservations;
246                if (configs.add(s))
247                    available += s.getLimit();
248            }
249            reserved += r.getLimit();
250            if (r.getConfigs().size() == 1)
251                exclusive += r.getLimit();
252        }
253        
254        return Math.min(available - reserved, getLimit() - exclusive);
255    }
256    
257    /**
258     * Get reservations for this configuration
259     * @return related reservations
260     */
261    public synchronized List<Reservation> getReservations() {
262        if (iReservations == null) {
263            iReservations = new ArrayList<Reservation>();
264            for (Reservation r: getOffering().getReservations()) {
265                if (r.getConfigs().isEmpty() || r.getConfigs().contains(this))
266                    iReservations.add(r);
267            }
268        }
269        return iReservations;
270    }
271    List<Reservation> iReservations = null;
272    
273    /**
274     * Get reservations that require this configuration
275     * @return related reservations
276     */
277    public synchronized List<Reservation> getConfigReservations() {
278        if (iConfigReservations == null) {
279            iConfigReservations = new ArrayList<Reservation>();
280            for (Reservation r: getOffering().getReservations()) {
281                if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this))
282                    iConfigReservations.add(r);
283            }
284        }
285        return iConfigReservations;
286    }
287    List<Reservation> iConfigReservations = null;
288    
289    /**
290     * Clear reservation information that was cached on this configuration or below
291     */
292    public synchronized void clearReservationCache() {
293        for (Subpart s: getSubparts())
294            s.clearReservationCache();
295        iReservations = null;
296        iConfigReservations = null;
297        iTotalUnreservedSpace = null;
298    }
299    
300    @Override
301    public boolean equals(Object o) {
302        if (o == null || !(o instanceof Config)) return false;
303        return getId() == ((Config)o).getId();
304    }
305    
306    @Override
307    public int hashCode() {
308        return new Long(getId()).hashCode();
309    }
310    
311    @Override
312    public Model<Request, Enrollment> getModel() {
313        return getOffering().getModel();
314    }
315    
316    /** Set of assigned enrollments 
317     * @param assignment current assignment
318     * @return enrollments in this configuration
319     **/
320    public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) {
321        return getContext(assignment).getEnrollments();
322    }
323    
324    /**
325     * Enrollment weight -- weight of all requests which have an enrollment that
326     * contains this config, excluding the given one. See
327     * {@link Request#getWeight()}.
328     * @param assignment current assignment
329     * @param excludeRequest request to exclude, null if all requests are to be included
330     * @return enrollment weight
331     */
332    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
333        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
334    }
335    
336    /**
337     * Maximal weight of a single enrollment in the config
338     * @param assignment current assignment
339     * @return maximal enrollment weight
340     */
341    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
342        return getContext(assignment).getMaxEnrollmentWeight();
343    }
344
345    /**
346     * Minimal weight of a single enrollment in the config
347     * @param assignment current assignment
348     * @return minimal enrollment weight
349     */
350    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
351        return getContext(assignment).getMinEnrollmentWeight();
352    }
353
354    @Override
355    public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
356        return new ConfigContext(assignment);
357    }
358    
359
360    @Override
361    public ConfigContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ConfigContext parentContext) {
362        return new ConfigContext(parentContext);
363    }
364    
365    public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> {
366        private double iEnrollmentWeight = 0.0;
367        private double iMaxEnrollmentWeight = 0.0;
368        private double iMinEnrollmentWeight = 0.0;
369        private Set<Enrollment> iEnrollments = null;
370        private boolean iReadOnly = false;
371
372        public ConfigContext(Assignment<Request, Enrollment> assignment) {
373            iEnrollments = new HashSet<Enrollment>();
374            for (Course course: getOffering().getCourses()) {
375                for (CourseRequest request: course.getRequests()) {
376                    Enrollment enrollment = assignment.getValue(request);
377                    if (enrollment != null && Config.this.equals(enrollment.getConfig()))
378                        assigned(assignment, enrollment);
379                }
380            }
381        }
382        
383        public ConfigContext(ConfigContext parent) {
384            iEnrollmentWeight = parent.iEnrollmentWeight;
385            iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight;
386            iMinEnrollmentWeight = parent.iMinEnrollmentWeight;
387            iEnrollments = parent.iEnrollments;
388            iReadOnly = true;
389        }
390
391        /** Called when an enrollment with this config is assigned to a request */
392        @Override
393        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
394            if (iReadOnly) {
395                iEnrollments = new HashSet<Enrollment>(iEnrollments);
396                iReadOnly = false;
397            }
398            if (iEnrollments.isEmpty()) {
399                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
400            } else {
401                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
402                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
403            }
404            if (iEnrollments.add(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
405                iEnrollmentWeight += enrollment.getRequest().getWeight();
406        }
407
408        /** Called when an enrollment with this config is unassigned from a request */
409        @Override
410        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
411            if (iReadOnly) {
412                iEnrollments = new HashSet<Enrollment>(iEnrollments);
413                iReadOnly = false;
414            }
415            if (iEnrollments.remove(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
416                iEnrollmentWeight -= enrollment.getRequest().getWeight();
417            if (iEnrollments.isEmpty()) {
418                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
419            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
420                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
421                    double newMinEnrollmentWeight = Double.MAX_VALUE;
422                    for (Enrollment e : iEnrollments) {
423                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
424                            newMinEnrollmentWeight = iMinEnrollmentWeight;
425                            break;
426                        } else {
427                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
428                        }
429                    }
430                    iMinEnrollmentWeight = newMinEnrollmentWeight;
431                }
432                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
433                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
434                    for (Enrollment e : iEnrollments) {
435                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
436                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
437                            break;
438                        } else {
439                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
440                        }
441                    }
442                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
443                }
444            }
445        }
446        
447        /**
448         * Enrollment weight -- weight of all requests which have an enrollment that
449         * contains this config, excluding the given one. See
450         * {@link Request#getWeight()}.
451         * @param assignment current assignment
452         * @param excludeRequest request to exclude
453         * @return enrollment weight
454         */
455        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
456            double weight = iEnrollmentWeight;
457            if (excludeRequest != null) {
458                Enrollment enrollment = assignment.getValue(excludeRequest);
459                if (enrollment != null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
460                    weight -= excludeRequest.getWeight();
461            }
462            return weight;
463        }
464        
465        /** Set of assigned enrollments 
466         * @return assigned enrollments (using this configuration)
467         **/
468        public Set<Enrollment> getEnrollments() {
469            return iEnrollments;
470        }
471
472        /**
473         * Maximal weight of a single enrollment in the config
474         * @return maximal enrollment weight
475         */
476        public double getMaxEnrollmentWeight() {
477            return iMaxEnrollmentWeight;
478        }
479
480        /**
481         * Minimal weight of a single enrollment in the config
482         * @return minimal enrollment weight
483         */
484        public double getMinEnrollmentWeight() {
485            return iMinEnrollmentWeight;
486        }
487    }
488}