001package org.cpsolver.studentsct.constraint;
002
003import java.util.ArrayList;
004import java.util.List;
005import java.util.Set;
006
007import org.cpsolver.ifs.assignment.Assignment;
008import org.cpsolver.ifs.model.GlobalConstraint;
009import org.cpsolver.ifs.util.DataProperties;
010import org.cpsolver.studentsct.constraint.ConfigLimit.Adepts;
011import org.cpsolver.studentsct.model.Enrollment;
012import org.cpsolver.studentsct.model.Request;
013import org.cpsolver.studentsct.model.Section;
014import org.cpsolver.studentsct.reservation.Reservation;
015
016
017/**
018 * Section limit constraint. This global constraint ensures that a limit of each
019 * section is not exceeded. This means that the total sum of weights of course
020 * requests (see {@link Request#getWeight()}) enrolled into a section is below
021 * the section's limit (see {@link Section#getLimit()}).
022 * 
023 * <br>
024 * <br>
025 * Sections with negative limit are considered unlimited, and therefore
026 * completely ignored by this constraint.
027 * 
028 * <br>
029 * <br>
030 * This constraint also check section reservations.
031 * 
032 * <br>
033 * <br>
034 * Parameters:
035 * <table border='1'><caption>Related Solver Parameters</caption>
036 * <tr>
037 * <th>Parameter</th>
038 * <th>Type</th>
039 * <th>Comment</th>
040 * </tr>
041 * <tr>
042 * <td>SectionLimit.PreferDummyStudents</td>
043 * <td>{@link Boolean}</td>
044 * <td>If true, requests of dummy (last-like) students are preferred to be
045 * selected as conflicting.</td>
046 * </tr>
047 * </table>
048 * <br>
049 * <br>
050 * 
051 * @author  Tomáš Müller
052 * @version StudentSct 1.3 (Student Sectioning)<br>
053 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
054 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
055 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
056 * <br>
057 *          This library is free software; you can redistribute it and/or modify
058 *          it under the terms of the GNU Lesser General Public License as
059 *          published by the Free Software Foundation; either version 3 of the
060 *          License, or (at your option) any later version. <br>
061 * <br>
062 *          This library is distributed in the hope that it will be useful, but
063 *          WITHOUT ANY WARRANTY; without even the implied warranty of
064 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
065 *          Lesser General Public License for more details. <br>
066 * <br>
067 *          You should have received a copy of the GNU Lesser General Public
068 *          License along with this library; if not see
069 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
070 */
071public class SectionLimit extends GlobalConstraint<Request, Enrollment> {
072    private static double sNominalWeight = 0.00001;
073    private boolean iPreferDummyStudents = false;
074    private boolean iPreferPriorityStudents = true;
075
076    /**
077     * Constructor
078     * 
079     * @param cfg
080     *            solver configuration
081     */
082    public SectionLimit(DataProperties cfg) {
083        super();
084        iPreferDummyStudents = cfg.getPropertyBoolean("SectionLimit.PreferDummyStudents", false);
085        iPreferPriorityStudents = cfg.getPropertyBoolean("Sectioning.PriorityStudentsFirstSelection.AllIn", true);
086    }
087
088    /**
089     * Enrollment weight of a section if the given request is assigned. In order
090     * to overcome rounding problems with last-like students ( e.g., 5 students
091     * are projected to two sections of limit 2 -- each section can have up to 3
092     * of these last-like students), the weight of the request with the highest
093     * weight in the section is changed to a small nominal weight.
094     * 
095     * @param assignment current assignment
096     * @param section
097     *            a section that is of concern
098     * @param request
099     *            a request of a student to be assigned containing the given
100     *            section
101     * @return section's new weight
102     */
103    public static double getEnrollmentWeight(Assignment<Request, Enrollment> assignment, Section section, Request request) {
104        return section.getEnrollmentWeight(assignment, request) + request.getWeight() - Math.max(section.getMaxEnrollmentWeight(assignment), request.getWeight()) + sNominalWeight;
105    }
106    
107    /**
108     * Remaining unreserved space in a section if the given request is assigned. In order
109     * to overcome rounding problems with last-like students ( e.g., 5 students
110     * are projected to two sections of limit 2 -- each section can have up to 3
111     * of these last-like students), the weight of the request with the highest
112     * weight in the section is changed to a small nominal weight.
113     * 
114     * @param assignment current assignment
115     * @param section
116     *            a section that is of concern
117     * @param request
118     *            a request of a student to be assigned containing the given
119     *            section
120     * @return section's new unreserved space
121     */
122    public static double getUnreservedSpace(Assignment<Request, Enrollment> assignment, Section section, Request request) {
123        return section.getUnreservedSpace(assignment, request) - request.getWeight() + Math.max(section.getMaxEnrollmentWeight(assignment), request.getWeight()) - sNominalWeight;
124    }
125
126    
127    /**
128     * True if the enrollment has reservation for this section.
129     * Everything else is checked in the {@link ReservationLimit} constraint.
130     **/
131    private boolean hasSectionReservation(Enrollment enrollment, Section section) {
132        Reservation reservation = enrollment.getReservation();
133        if (reservation == null) return false;
134        Set<Section> sections = reservation.getSections(section.getSubpart());
135        return sections != null && sections.contains(section);
136    }
137
138    /**
139     * A given enrollment is conflicting, if there is a section which limit
140     * (computed by {@link SectionLimit#getEnrollmentWeight(Assignment, Section, Request)})
141     * exceeds the section limit. <br>
142     * For each of such sections, one or more existing enrollments are
143     * (randomly) selected as conflicting until the overall weight is under the
144     * limit.
145     * 
146     * @param enrollment
147     *            {@link Enrollment} that is being considered
148     * @param conflicts
149     *            all computed conflicting requests are added into this set
150     */
151    @Override
152    public void computeConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment, Set<Enrollment> conflicts) {
153        // check reservation can assign over the limit
154        if (enrollment.getReservation() != null && enrollment.getReservation().canBatchAssignOverLimit())
155            return;
156
157        // exclude free time requests
158        if (!enrollment.isCourseRequest())
159            return;
160
161        // for each section
162        for (Section section : enrollment.getSections()) {
163
164            // no reservation -- check the space in the unreserved space in the section
165            if (enrollment.getConfig().getOffering().hasReservations() && !hasSectionReservation(enrollment, section)) {
166                // section is fully reserved by section reservations
167                if (section.getTotalUnreservedSpace() < enrollment.getRequest().getWeight()) {
168                    conflicts.add(enrollment);
169                    return;
170                }
171                
172                double unreserved = getUnreservedSpace(assignment, section, enrollment.getRequest()); 
173                
174                if (unreserved < 0.0) {
175                    // no unreserved space available -> cannot be assigned
176                    // try to unassign some other enrollments that also do not have reservation
177                    
178                    List<Enrollment> adepts = new ArrayList<Enrollment>(section.getEnrollments(assignment).size());
179                    for (Enrollment e : section.getEnrollments(assignment)) {
180                        if (e.getRequest().equals(enrollment.getRequest()))
181                            continue;
182                        if (e.getReservation() != null && e.getReservation().canBatchAssignOverLimit())
183                            continue;
184                        if (hasSectionReservation(e, section))
185                            continue;
186                        if (conflicts.contains(e))
187                            unreserved += e.getRequest().getWeight();
188                        else
189                            adepts.add(e);
190                    }
191                    
192                    while (unreserved < 0.0) {
193                        if (adepts.isEmpty()) {
194                            // no adepts -> enrollment cannot be assigned
195                            conflicts.add(enrollment);
196                            return;
197                        }
198                        
199                        // pick adept (prefer dummy students), decrease unreserved space,
200                        // make conflict
201                        Enrollment conflict = new Adepts(iPreferDummyStudents, iPreferPriorityStudents, adepts, assignment).get();
202                        adepts.remove(conflict);
203                        unreserved += conflict.getRequest().getWeight();
204                        conflicts.add(conflict);
205                    }
206                }
207            }
208
209            // unlimited section
210            if (section.getLimit() < 0)
211                continue;
212
213            // new enrollment weight
214            double enrlWeight = getEnrollmentWeight(assignment, section, enrollment.getRequest());
215
216            // below limit -> ok
217            if (enrlWeight <= section.getLimit())
218                continue;
219
220            // above limit -> compute adepts (current assignments that are not
221            // yet conflicting) exclude all conflicts as well
222            List<Enrollment> adepts = new ArrayList<Enrollment>(section.getEnrollments(assignment).size());
223            for (Enrollment e : section.getEnrollments(assignment)) {
224                if (e.getRequest().equals(enrollment.getRequest()))
225                    continue;
226                if (conflicts.contains(e))
227                    enrlWeight -= e.getRequest().getWeight();
228                else
229                    adepts.add(e);
230            }
231
232            // while above limit -> pick an adept and make it conflicting
233            while (enrlWeight > section.getLimit()) {
234                if (adepts.isEmpty()) {
235                    // no adepts -> enrollment cannot be assigned
236                    conflicts.add(enrollment);
237                    return;
238                }
239                
240                // pick adept (prefer dummy students & students w/o reservation), decrease enrollment
241                // weight, make conflict
242                Enrollment conflict = new Adepts(iPreferDummyStudents, iPreferPriorityStudents, adepts, assignment).get();
243                adepts.remove(conflict);
244                enrlWeight -= conflict.getRequest().getWeight();
245                conflicts.add(conflict);
246            }
247        }
248    }
249
250    /**
251     * A given enrollment is conflicting, if there is a section which
252     * limit(computed by
253     * {@link SectionLimit#getEnrollmentWeight(Assignment, Section, Request)}) exceeds the
254     * section limit.
255     * 
256     * @param enrollment
257     *            {@link Enrollment} that is being considered
258     * @return true, if there is a section which will exceed its limit when the
259     *         given enrollment is assigned
260     */
261    @Override
262    public boolean inConflict(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
263        // check reservation can assign over the limit
264        if (enrollment.getReservation() != null && enrollment.getReservation().canBatchAssignOverLimit())
265            return false;
266
267        // exclude free time requests
268        if (!enrollment.isCourseRequest())
269            return false;
270
271        // for each section
272        for (Section section : enrollment.getSections()) {
273
274            // not have reservation -> check unreserved space
275            if (enrollment.getConfig().getOffering().hasReservations() &&
276                !hasSectionReservation(enrollment, section) && (
277                section.getTotalUnreservedSpace() < enrollment.getRequest().getWeight() ||
278                getUnreservedSpace(assignment, section, enrollment.getRequest()) < 0.0))
279                return true;
280
281            // unlimited section
282            if (section.getLimit() < 0)
283                continue;
284            
285            // new enrollment weight
286            double enrlWeight = getEnrollmentWeight(assignment, section, enrollment.getRequest());
287
288            // above limit -> conflict
289            if (enrlWeight > section.getLimit())
290                return true;
291        }
292
293        // no conflicting section -> no conflict
294        return false;
295    }
296
297    @Override
298    public String toString() {
299        return "SectionLimit";
300    }
301}