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     * Enrollment weight including over the limit enrollments.
338     * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true.
339     * {@link Request#getWeight()}.
340     * @param assignment current assignment
341     * @param excludeRequest request to exclude, null if all requests are to be included
342     * @return enrollment weight
343     */
344    public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
345        return getContext(assignment).getEnrollmentTotalWeight(assignment, excludeRequest);
346    }
347    
348    /**
349     * Maximal weight of a single enrollment in the config
350     * @param assignment current assignment
351     * @return maximal enrollment weight
352     */
353    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
354        return getContext(assignment).getMaxEnrollmentWeight();
355    }
356
357    /**
358     * Minimal weight of a single enrollment in the config
359     * @param assignment current assignment
360     * @return minimal enrollment weight
361     */
362    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
363        return getContext(assignment).getMinEnrollmentWeight();
364    }
365
366    @Override
367    public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
368        return new ConfigContext(assignment);
369    }
370    
371
372    @Override
373    public ConfigContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ConfigContext parentContext) {
374        return new ConfigContext(parentContext);
375    }
376    
377    public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> {
378        private double iEnrollmentWeight = 0.0;
379        private double iEnrollmentTotalWeight = 0.0;
380        private double iMaxEnrollmentWeight = 0.0;
381        private double iMinEnrollmentWeight = 0.0;
382        private Set<Enrollment> iEnrollments = null;
383        private boolean iReadOnly = false;
384
385        public ConfigContext(Assignment<Request, Enrollment> assignment) {
386            iEnrollments = new HashSet<Enrollment>();
387            for (Course course: getOffering().getCourses()) {
388                for (CourseRequest request: course.getRequests()) {
389                    Enrollment enrollment = assignment.getValue(request);
390                    if (enrollment != null && Config.this.equals(enrollment.getConfig()))
391                        assigned(assignment, enrollment);
392                }
393            }
394        }
395        
396        public ConfigContext(ConfigContext parent) {
397            iEnrollmentWeight = parent.iEnrollmentWeight;
398            iEnrollmentTotalWeight = parent.iEnrollmentTotalWeight;
399            iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight;
400            iMinEnrollmentWeight = parent.iMinEnrollmentWeight;
401            iEnrollments = parent.iEnrollments;
402            iReadOnly = true;
403        }
404
405        /** Called when an enrollment with this config is assigned to a request */
406        @Override
407        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
408            if (iReadOnly) {
409                iEnrollments = new HashSet<Enrollment>(iEnrollments);
410                iReadOnly = false;
411            }
412            if (iEnrollments.isEmpty()) {
413                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
414            } else {
415                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
416                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
417            }
418            if (iEnrollments.add(enrollment)) {
419                iEnrollmentTotalWeight += enrollment.getRequest().getWeight();
420                if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())
421                    iEnrollmentWeight += enrollment.getRequest().getWeight();
422            }
423        }
424
425        /** Called when an enrollment with this config is unassigned from a request */
426        @Override
427        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
428            if (iReadOnly) {
429                iEnrollments = new HashSet<Enrollment>(iEnrollments);
430                iReadOnly = false;
431            }
432            if (iEnrollments.remove(enrollment)) {
433                iEnrollmentTotalWeight -= enrollment.getRequest().getWeight();
434                if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())
435                    iEnrollmentWeight -= enrollment.getRequest().getWeight();
436            }
437            if (iEnrollments.isEmpty()) {
438                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
439            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
440                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
441                    double newMinEnrollmentWeight = Double.MAX_VALUE;
442                    for (Enrollment e : iEnrollments) {
443                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
444                            newMinEnrollmentWeight = iMinEnrollmentWeight;
445                            break;
446                        } else {
447                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
448                        }
449                    }
450                    iMinEnrollmentWeight = newMinEnrollmentWeight;
451                }
452                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
453                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
454                    for (Enrollment e : iEnrollments) {
455                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
456                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
457                            break;
458                        } else {
459                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
460                        }
461                    }
462                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
463                }
464            }
465        }
466        
467        /**
468         * Enrollment weight -- weight of all requests which have an enrollment that
469         * contains this config, excluding the given one. See
470         * {@link Request#getWeight()}.
471         * @param assignment current assignment
472         * @param excludeRequest request to exclude
473         * @return enrollment weight
474         */
475        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
476            double weight = iEnrollmentWeight;
477            if (excludeRequest != null) {
478                Enrollment enrollment = assignment.getValue(excludeRequest);
479                if (enrollment != null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
480                    weight -= excludeRequest.getWeight();
481            }
482            return weight;
483        }
484        
485        /**
486         * Enrollment weight including over the limit enrollments.
487         * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true.
488         * @param assignment current assignment
489         * @param excludeRequest request to exclude
490         * @return enrollment weight
491         */
492        public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
493            double weight = iEnrollmentTotalWeight;
494            if (excludeRequest != null) {
495                Enrollment enrollment = assignment.getValue(excludeRequest);
496                if (enrollment != null && iEnrollments.contains(enrollment))
497                    weight -= excludeRequest.getWeight();
498            }
499            return weight;
500        }
501        
502        /** Set of assigned enrollments 
503         * @return assigned enrollments (using this configuration)
504         **/
505        public Set<Enrollment> getEnrollments() {
506            return iEnrollments;
507        }
508
509        /**
510         * Maximal weight of a single enrollment in the config
511         * @return maximal enrollment weight
512         */
513        public double getMaxEnrollmentWeight() {
514            return iMaxEnrollmentWeight;
515        }
516
517        /**
518         * Minimal weight of a single enrollment in the config
519         * @return minimal enrollment weight
520         */
521        public double getMinEnrollmentWeight() {
522            return iMinEnrollmentWeight;
523        }
524    }
525}