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