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 * @author  Tomáš Müller
030 * @version StudentSct 1.3 (Student Sectioning)<br>
031 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
032 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
033 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
034 * <br>
035 *          This library is free software; you can redistribute it and/or modify
036 *          it under the terms of the GNU Lesser General Public License as
037 *          published by the Free Software Foundation; either version 3 of the
038 *          License, or (at your option) any later version. <br>
039 * <br>
040 *          This library is distributed in the hope that it will be useful, but
041 *          WITHOUT ANY WARRANTY; without even the implied warranty of
042 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
043 *          Lesser General Public License for more details. <br>
044 * <br>
045 *          You should have received a copy of the GNU Lesser General Public
046 *          License along with this library; if not see
047 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
048 */
049public class Config extends AbstractClassWithContext<Request, Enrollment, Config.ConfigContext> implements CanInheritContext<Request, Enrollment, Config.ConfigContext> {
050    private long iId = -1;
051    private String iName = null;
052    private Offering iOffering = null;
053    private int iLimit = -1;
054    private List<Subpart> iSubparts = new ArrayList<Subpart>();
055    private Long iInstrMethodId;
056    private String iInstrMethodName;
057    private String iInstrMethodReference;
058
059    /**
060     * Constructor
061     * 
062     * @param id
063     *            instructional offering configuration unique id
064     * @param limit
065     *            configuration limit (-1 for unlimited)
066     * @param name
067     *            configuration name
068     * @param offering
069     *            instructional offering to which this configuration belongs
070     */
071    public Config(long id, int limit, String name, Offering offering) {
072        iId = id;
073        iLimit = limit;
074        iName = name;
075        iOffering = offering;
076        iOffering.getConfigs().add(this);
077    }
078
079    /** Configuration id 
080     * @return instructional offering configuration unique id
081     **/
082    public long getId() {
083        return iId;
084    }
085    
086    /**
087     * Configuration limit. This is defines the maximal number of students that can be
088     * enrolled into this configuration at the same time. It is -1 in the case of an
089     * unlimited configuration
090     * @return configuration limit
091     */
092    public int getLimit() {
093        return iLimit;
094    }
095
096    /** Set configuration limit 
097     * @param limit configuration limit, -1 if unlimited
098     **/
099    public void setLimit(int limit) {
100        iLimit = limit;
101    }
102
103
104
105    /** Configuration name 
106     * @return configuration name
107     **/
108    public String getName() {
109        return iName;
110    }
111
112    /** Instructional offering to which this configuration belongs. 
113     * @return instructional offering
114     **/
115    public Offering getOffering() {
116        return iOffering;
117    }
118
119    /** List of subparts 
120     * @return scheduling subparts
121     **/
122    public List<Subpart> getSubparts() {
123        return iSubparts;
124    }
125    
126    /**
127     * Return instructional method id
128     * @return instructional method id
129     */
130    public Long getInstructionalMethodId() { return iInstrMethodId; }
131    
132    /**
133     * Set instructional method id
134     * @param instrMethodId instructional method id
135     */
136    public void setInstructionalMethodId(Long instrMethodId) { iInstrMethodId = instrMethodId; }
137    
138    /**
139     * Return instructional method name
140     * @return instructional method name
141     */
142    public String getInstructionalMethodName() { return iInstrMethodName; }
143    
144    /**
145     * Set instructional method name
146     * @param instrMethodName instructional method name
147     */
148    public void setInstructionalMethodName(String instrMethodName) { iInstrMethodName = instrMethodName; }
149    
150    /**
151     * Return instructional method reference
152     * @return instructional method reference
153     */
154    public String getInstructionalMethodReference() { return iInstrMethodReference; }
155    
156    /**
157     * Set instructional method reference
158     * @param instrMethodReference instructional method reference
159     */
160    public void setInstructionalMethodReference(String instrMethodReference) { iInstrMethodReference = instrMethodReference; }
161
162    @Override
163    public String toString() {
164        return getName();
165    }
166
167    /** Average minimal penalty from {@link Subpart#getMinPenalty()} 
168     * @return minimal penalty
169     **/
170    public double getMinPenalty() {
171        double min = 0.0;
172        for (Subpart subpart : getSubparts()) {
173            min += subpart.getMinPenalty();
174        }
175        return min / getSubparts().size();
176    }
177
178    /** Average maximal penalty from {@link Subpart#getMaxPenalty()} 
179     * @return maximal penalty
180     **/
181    public double getMaxPenalty() {
182        double max = 0.0;
183        for (Subpart subpart : getSubparts()) {
184            max += subpart.getMinPenalty();
185        }
186        return max / getSubparts().size();
187    }
188    
189    /**
190     * Available space in the configuration that is not reserved by any config reservation
191     * @param assignment current assignment
192     * @param excludeRequest excluding given request (if not null)
193     * @return available space
194     **/
195    public double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
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(this) < 0) return 0.0;
205            }
206            return Double.MAX_VALUE;
207        }
208        
209        double available = getLimit() - getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
210        // exclude reservations that are not directly set on this section
211        for (Reservation r: getConfigReservations()) {
212            // ignore expired reservations
213            if (r.isExpired()) continue;
214            // unlimited reservation -> all the space is reserved
215            if (r.getLimit(this) < 0.0) return 0.0;
216            // compute space that can be potentially taken by this reservation
217            double reserved = r.getContext(assignment).getReservedAvailableSpace(assignment, this, excludeRequest);
218            // deduct the space from available space
219            available -= Math.max(0.0, reserved);
220        }
221        
222        return available;
223    }
224    
225    /**
226     * Total space in the configuration that cannot be reserved by any config reservation
227     * @return total unreserved space
228     **/
229    public synchronized double getTotalUnreservedSpace() {
230        if (iTotalUnreservedSpace == null)
231            iTotalUnreservedSpace = getTotalUnreservedSpaceNoCache();
232        return iTotalUnreservedSpace;
233    }
234    private Double iTotalUnreservedSpace = null;
235    private double getTotalUnreservedSpaceNoCache() {
236        // configuration is unlimited -> there is unreserved space unless there is an unlimited reservation too 
237        // (in which case there is no unreserved space)
238        if (getLimit() < 0) {
239            // exclude reservations that are not directly set on this section
240            for (Reservation r: getConfigReservations()) {
241                // ignore expired reservations
242                if (r.isExpired()) continue;
243                // there is an unlimited reservation -> no unreserved space
244                if (r.getLimit(this) < 0) return 0.0;
245            }
246            return Double.MAX_VALUE;
247        }
248        
249        // we need to check all reservations linked with this section
250        double available = getLimit(), reserved = 0, exclusive = 0;
251        Set<Config> configs = new HashSet<Config>();
252        reservations: for (Reservation r: getConfigReservations()) {
253            // ignore expired reservations
254            if (r.isExpired()) continue;
255            // unlimited reservation -> no unreserved space
256            if (r.getLimit(this) < 0) return 0.0;
257            for (Config s: r.getConfigs()) {
258                if (s.equals(this)) continue;
259                if (s.getLimit() < 0) continue reservations;
260                if (configs.add(s))
261                    available += s.getLimit();
262            }
263            reserved += r.getLimit(this);
264            if (r.getConfigs().size() == 1)
265                exclusive += r.getLimit(this);
266        }
267        
268        return Math.min(available - reserved, getLimit() - exclusive);
269    }
270    
271    /**
272     * Get reservations for this configuration
273     * @return related reservations
274     */
275    public synchronized List<Reservation> getReservations() {
276        if (iReservations == null) {
277            iReservations = new ArrayList<Reservation>();
278            for (Reservation r: getOffering().getReservations()) {
279                if (r.getConfigs().isEmpty() || r.getConfigs().contains(this))
280                    iReservations.add(r);
281            }
282        }
283        return iReservations;
284    }
285    List<Reservation> iReservations = null;
286    
287    /**
288     * Get reservations that require this configuration
289     * @return related reservations
290     */
291    public synchronized List<Reservation> getConfigReservations() {
292        if (iConfigReservations == null) {
293            iConfigReservations = new ArrayList<Reservation>();
294            for (Reservation r: getOffering().getReservations()) {
295                if (!r.getConfigs().isEmpty() && r.getConfigs().contains(this))
296                    iConfigReservations.add(r);
297            }
298        }
299        return iConfigReservations;
300    }
301    List<Reservation> iConfigReservations = null;
302    
303    /**
304     * Clear reservation information that was cached on this configuration or below
305     */
306    public synchronized void clearReservationCache() {
307        for (Subpart s: getSubparts())
308            s.clearReservationCache();
309        iReservations = null;
310        iConfigReservations = null;
311        iTotalUnreservedSpace = null;
312    }
313    
314    @Override
315    public boolean equals(Object o) {
316        if (o == null || !(o instanceof Config)) return false;
317        return getId() == ((Config)o).getId();
318    }
319    
320    @Override
321    public int hashCode() {
322        return Long.valueOf(getId()).hashCode();
323    }
324    
325    @Override
326    public Model<Request, Enrollment> getModel() {
327        return getOffering().getModel();
328    }
329    
330    /** Set of assigned enrollments 
331     * @param assignment current assignment
332     * @return enrollments in this configuration
333     **/
334    public Set<Enrollment> getEnrollments(Assignment<Request, Enrollment> assignment) {
335        return getContext(assignment).getEnrollments();
336    }
337    
338    /**
339     * Enrollment weight -- weight of all requests which have an enrollment that
340     * contains this config, excluding the given one. See
341     * {@link Request#getWeight()}.
342     * @param assignment current assignment
343     * @param excludeRequest request to exclude, null if all requests are to be included
344     * @return enrollment weight
345     */
346    public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
347        return getContext(assignment).getEnrollmentWeight(assignment, excludeRequest);
348    }
349    
350    /**
351     * Enrollment weight including over the limit enrollments.
352     * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true.
353     * {@link Request#getWeight()}.
354     * @param assignment current assignment
355     * @param excludeRequest request to exclude, null if all requests are to be included
356     * @return enrollment weight
357     */
358    public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
359        return getContext(assignment).getEnrollmentTotalWeight(assignment, excludeRequest);
360    }
361    
362    /**
363     * Maximal weight of a single enrollment in the config
364     * @param assignment current assignment
365     * @return maximal enrollment weight
366     */
367    public double getMaxEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
368        return getContext(assignment).getMaxEnrollmentWeight();
369    }
370
371    /**
372     * Minimal weight of a single enrollment in the config
373     * @param assignment current assignment
374     * @return minimal enrollment weight
375     */
376    public double getMinEnrollmentWeight(Assignment<Request, Enrollment> assignment) {
377        return getContext(assignment).getMinEnrollmentWeight();
378    }
379
380    @Override
381    public ConfigContext createAssignmentContext(Assignment<Request, Enrollment> assignment) {
382        return new ConfigContext(assignment);
383    }
384    
385
386    @Override
387    public ConfigContext inheritAssignmentContext(Assignment<Request, Enrollment> assignment, ConfigContext parentContext) {
388        return new ConfigContext(parentContext);
389    }
390    
391    public class ConfigContext implements AssignmentConstraintContext<Request, Enrollment> {
392        private double iEnrollmentWeight = 0.0;
393        private double iEnrollmentTotalWeight = 0.0;
394        private double iMaxEnrollmentWeight = 0.0;
395        private double iMinEnrollmentWeight = 0.0;
396        private Set<Enrollment> iEnrollments = null;
397        private boolean iReadOnly = false;
398
399        public ConfigContext(Assignment<Request, Enrollment> assignment) {
400            iEnrollments = new HashSet<Enrollment>();
401            for (Course course: getOffering().getCourses()) {
402                for (CourseRequest request: course.getRequests()) {
403                    Enrollment enrollment = assignment.getValue(request);
404                    if (enrollment != null && Config.this.equals(enrollment.getConfig()))
405                        assigned(assignment, enrollment);
406                }
407            }
408        }
409        
410        public ConfigContext(ConfigContext parent) {
411            iEnrollmentWeight = parent.iEnrollmentWeight;
412            iEnrollmentTotalWeight = parent.iEnrollmentTotalWeight;
413            iMaxEnrollmentWeight = parent.iMaxEnrollmentWeight;
414            iMinEnrollmentWeight = parent.iMinEnrollmentWeight;
415            iEnrollments = parent.iEnrollments;
416            iReadOnly = true;
417        }
418
419        /** Called when an enrollment with this config is assigned to a request */
420        @Override
421        public void assigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
422            if (iReadOnly) {
423                iEnrollments = new HashSet<Enrollment>(iEnrollments);
424                iReadOnly = false;
425            }
426            if (iEnrollments.isEmpty()) {
427                iMinEnrollmentWeight = iMaxEnrollmentWeight = enrollment.getRequest().getWeight();
428            } else {
429                iMaxEnrollmentWeight = Math.max(iMaxEnrollmentWeight, enrollment.getRequest().getWeight());
430                iMinEnrollmentWeight = Math.min(iMinEnrollmentWeight, enrollment.getRequest().getWeight());
431            }
432            if (iEnrollments.add(enrollment)) {
433                iEnrollmentTotalWeight += enrollment.getRequest().getWeight();
434                if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())
435                    iEnrollmentWeight += enrollment.getRequest().getWeight();
436            }
437        }
438
439        /** Called when an enrollment with this config is unassigned from a request */
440        @Override
441        public void unassigned(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
442            if (iReadOnly) {
443                iEnrollments = new HashSet<Enrollment>(iEnrollments);
444                iReadOnly = false;
445            }
446            if (iEnrollments.remove(enrollment)) {
447                iEnrollmentTotalWeight -= enrollment.getRequest().getWeight();
448                if (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit())
449                    iEnrollmentWeight -= enrollment.getRequest().getWeight();
450            }
451            if (iEnrollments.isEmpty()) {
452                iMinEnrollmentWeight = iMaxEnrollmentWeight = 0;
453            } else if (iMinEnrollmentWeight != iMaxEnrollmentWeight) {
454                if (iMinEnrollmentWeight == enrollment.getRequest().getWeight()) {
455                    double newMinEnrollmentWeight = Double.MAX_VALUE;
456                    for (Enrollment e : iEnrollments) {
457                        if (e.getRequest().getWeight() == iMinEnrollmentWeight) {
458                            newMinEnrollmentWeight = iMinEnrollmentWeight;
459                            break;
460                        } else {
461                            newMinEnrollmentWeight = Math.min(newMinEnrollmentWeight, e.getRequest().getWeight());
462                        }
463                    }
464                    iMinEnrollmentWeight = newMinEnrollmentWeight;
465                }
466                if (iMaxEnrollmentWeight == enrollment.getRequest().getWeight()) {
467                    double newMaxEnrollmentWeight = Double.MIN_VALUE;
468                    for (Enrollment e : iEnrollments) {
469                        if (e.getRequest().getWeight() == iMaxEnrollmentWeight) {
470                            newMaxEnrollmentWeight = iMaxEnrollmentWeight;
471                            break;
472                        } else {
473                            newMaxEnrollmentWeight = Math.max(newMaxEnrollmentWeight, e.getRequest().getWeight());
474                        }
475                    }
476                    iMaxEnrollmentWeight = newMaxEnrollmentWeight;
477                }
478            }
479        }
480        
481        /**
482         * Enrollment weight -- weight of all requests which have an enrollment that
483         * contains this config, excluding the given one. See
484         * {@link Request#getWeight()}.
485         * @param assignment current assignment
486         * @param excludeRequest request to exclude
487         * @return enrollment weight
488         */
489        public double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
490            double weight = iEnrollmentWeight;
491            if (excludeRequest != null) {
492                Enrollment enrollment = assignment.getValue(excludeRequest);
493                if (enrollment != null && iEnrollments.contains(enrollment) && (enrollment.getReservation() == null || !enrollment.getReservation().canBatchAssignOverLimit()))
494                    weight -= excludeRequest.getWeight();
495            }
496            return weight;
497        }
498        
499        /**
500         * Enrollment weight including over the limit enrollments.
501         * That is enrollments that have reservation with {@link Reservation#canBatchAssignOverLimit()} set to true.
502         * @param assignment current assignment
503         * @param excludeRequest request to exclude
504         * @return enrollment weight
505         */
506        public double getEnrollmentTotalWeight(Assignment<Request, Enrollment> assignment, Request excludeRequest) {
507            double weight = iEnrollmentTotalWeight;
508            if (excludeRequest != null) {
509                Enrollment enrollment = assignment.getValue(excludeRequest);
510                if (enrollment != null && iEnrollments.contains(enrollment))
511                    weight -= excludeRequest.getWeight();
512            }
513            return weight;
514        }
515        
516        /** Set of assigned enrollments 
517         * @return assigned enrollments (using this configuration)
518         **/
519        public Set<Enrollment> getEnrollments() {
520            return iEnrollments;
521        }
522
523        /**
524         * Maximal weight of a single enrollment in the config
525         * @return maximal enrollment weight
526         */
527        public double getMaxEnrollmentWeight() {
528            return iMaxEnrollmentWeight;
529        }
530
531        /**
532         * Minimal weight of a single enrollment in the config
533         * @return minimal enrollment weight
534         */
535        public double getMinEnrollmentWeight() {
536            return iMinEnrollmentWeight;
537        }
538    }
539    
540    /**
541     * True if at least one subpart of this config has a credit value set
542     * @return true if a there is a subpart credit
543     */
544    public boolean hasCreditValue() {
545        for (Subpart subpart: getSubparts())
546            if (subpart.hasCreditValue()) return true;
547        return false;
548    }
549    
550    /**
551     * Sum of subpart credit of this config
552     * return config credit value
553     */
554    public Float getCreditValue() {
555        float credit = 0f; boolean hasCredit = false;
556        for (Subpart subpart: getSubparts())
557            if (subpart.hasCreditValue()) {
558                hasCredit = true;
559                credit += subpart.getCreditValue();
560            }
561        return (hasCredit ? Float.valueOf(credit) : null);
562    }
563    
564    public int getNrOnline() {
565        int online = 0;
566        for (Subpart subpart: getSubparts())
567            if (subpart.isOnline()) online ++;
568        return online;
569    }
570    
571    public int getNrArrHours() {
572        int arrHrs = 0;
573        for (Subpart subpart: getSubparts())
574            if (!subpart.hasTime()) arrHrs ++;
575        return arrHrs;
576    }
577    
578    public int getNrPast() {
579        int past = 0;
580        for (Subpart subpart: getSubparts())
581            if (subpart.isPast()) past ++;
582        return past;
583    }
584}