001package org.cpsolver.studentsct.constraint;
002
003import java.util.ArrayList;
004import java.util.Collection;
005import java.util.HashMap;
006import java.util.HashSet;
007import java.util.List;
008import java.util.Map;
009import java.util.Set;
010import java.util.TreeSet;
011
012import org.cpsolver.ifs.assignment.Assignment;
013import org.cpsolver.ifs.model.Constraint;
014import org.cpsolver.studentsct.model.Course;
015import org.cpsolver.studentsct.model.CourseRequest;
016import org.cpsolver.studentsct.model.Enrollment;
017import org.cpsolver.studentsct.model.Offering;
018import org.cpsolver.studentsct.model.Request;
019import org.cpsolver.studentsct.model.Section;
020import org.cpsolver.studentsct.model.Student;
021import org.cpsolver.studentsct.model.Subpart;
022
023
024/**
025 * Linked sections are sections (of different courses) that should be attended by the
026 * same students. If there are multiple sections of the same subpart, one or can be
027 * chosen randomly. For instance, if section A1 (of a course A) and section B1 (of a course
028 * B) are linked, a student requesting both courses must attend A1 if and only if he
029 * also attends B1. 
030 * 
031 * @author  Tomáš Müller
032 * @version StudentSct 1.3 (Student Sectioning)<br>
033 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
034 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
035 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
036 * <br>
037 *          This library is free software; you can redistribute it and/or modify
038 *          it under the terms of the GNU Lesser General Public License as
039 *          published by the Free Software Foundation; either version 3 of the
040 *          License, or (at your option) any later version. <br>
041 * <br>
042 *          This library is distributed in the hope that it will be useful, but
043 *          WITHOUT ANY WARRANTY; without even the implied warranty of
044 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
045 *          Lesser General Public License for more details. <br>
046 * <br>
047 *          You should have received a copy of the GNU Lesser General Public
048 *          License along with this library; if not see
049 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
050 */
051public class LinkedSections {
052    private Map<Offering, Map<Subpart, Set<Section>>> iSections = new HashMap<Offering, Map<Subpart,Set<Section>>>();
053    private boolean iMustBeUsed;
054    
055    /**
056     * Constructor
057     * @param sections sections that are to be linked
058     */
059    public LinkedSections(Section... sections) {
060        for (Section section: sections)
061            addSection(section);
062        
063    }
064    
065    /**
066     * Constructor
067     * @param sections sections that are to be linked
068     */
069    public LinkedSections(Collection<Section> sections) {
070        for (Section section: sections)
071            addSection(section);
072    }
073
074    /**
075     * Add a section to this link
076     * @param section
077     */
078    private void addSection(Section section) {
079        Map<Subpart, Set<Section>> subparts = iSections.get(section.getSubpart().getConfig().getOffering());
080        if (subparts == null) {
081            subparts = new HashMap<Subpart, Set<Section>>();
082            iSections.put(section.getSubpart().getConfig().getOffering(), subparts);
083        }
084        Set<Section> sections = subparts.get(section.getSubpart());
085        if (sections == null) {
086            sections = new HashSet<Section>();
087            subparts.put(section.getSubpart(), sections);
088        }
089        sections.add(section);
090
091    }
092    
093    /**
094     * Return offerings of this link
095     * @return offerings of this link
096     */
097    public Set<Offering> getOfferings() { return iSections.keySet(); }
098    
099    /**
100     * Return subpart (or subparts) of an offering of this link
101     * @param offering an offering of this link
102     * @return subpart (or subparts) of this offering in this link
103     */
104    public Set<Subpart> getSubparts(Offering offering) { return iSections.get(offering).keySet(); }
105    
106    /**
107     * Return section (or sections) of a subpart of this link
108     * @param subpart subpart of this link
109     * @return section (or sections) of this subpart in this link
110     */
111    public Set<Section> getSections(Subpart subpart) { return iSections.get(subpart.getConfig().getOffering()).get(subpart); }
112    
113    /**
114     * Create linked-section constraints for a given student
115     */
116    private LinkedSectionsConstraint createConstraint(Student student) {
117        List<Request> requests = new ArrayList<Request>();
118        int nrOfferings = 0;
119        requests: for (Request request: student.getRequests()) {
120            if (request instanceof CourseRequest) {
121                for (Course course: ((CourseRequest)request).getCourses()) {
122                    Map<Subpart, Set<Section>> subpartsThisOffering = iSections.get(course.getOffering());
123                    if (subpartsThisOffering != null) {
124                        requests.add(request);
125                        nrOfferings++;
126                        continue requests;
127                    }
128                }
129            }
130        }
131        if (nrOfferings <= 1) return null;
132        LinkedSectionsConstraint constraint = new LinkedSectionsConstraint(student, requests);
133        student.getRequests().get(0).getModel().addConstraint(constraint);
134        return constraint;
135    }
136    
137    /**
138     * Create linked-section constraints for this link. A constraint is created for each
139     * student that has two or more offerings of this link.
140     */
141    public void createConstraints() {
142        Set<Student> students = new HashSet<Student>();
143        for (Offering offering: iSections.keySet())
144            for (Course course: offering.getCourses())
145                for (Request request: course.getRequests())
146                    if (students.add(request.getStudent())) {
147                        if (createConstraint(request.getStudent()) != null)
148                            request.getStudent().getLinkedSections().add(this);
149                    }
150    }
151    
152    /**
153     * Compute conflicting enrollments. If the given enrollment contains sections of this link
154     * (one for each subpart in {@link LinkedSections#getSubparts(Offering)}), another assignment
155     * of this student is in a conflict, if it does not contain the appropriate sections from
156     * {@link LinkedSections#getSubparts(Offering)} and {@link LinkedSections#getSections(Subpart)}.
157     * 
158     * @param assignment current assignment
159     * @param enrollment given enrollment 
160     * @param conflicts found conflicts are given to this interface, see {@link ConflictHandler#onConflict(Enrollment)}
161     */
162    public void computeConflicts(Assignment<Request, Enrollment> assignment, Enrollment enrollment, ConflictHandler conflicts) {
163        computeConflicts(enrollment, new CurrentAssignment(assignment), conflicts);
164    }
165    
166    /**
167     * Compute conflicting enrollments. If the given enrollment contains sections of this link
168     * (one for each subpart in {@link LinkedSections#getSubparts(Offering)}), another assignment
169     * of this student is in a conflict, if it does not contain the appropriate sections from
170     * {@link LinkedSections#getSubparts(Offering)} and {@link LinkedSections#getSections(Subpart)}.
171     * 
172     * @param enrollment given enrollment
173     * @param assignment custom assignment 
174     * @param conflicts found conflicts are given to this interface, see {@link ConflictHandler#onConflict(Enrollment)}
175     */
176    public void computeConflicts(Enrollment enrollment, EnrollmentAssignment assignment, ConflictHandler conflicts) {
177        if (enrollment == null || enrollment.getCourse() == null) return;
178        if (enrollment.getReservation() != null && enrollment.getReservation().canBreakLinkedSections()) return;
179        Map<Subpart, Set<Section>> subparts = iSections.get(enrollment.getCourse().getOffering());
180        if (subparts == null || subparts.isEmpty()) return;
181        boolean match = false, partial = false;
182        for (Section section: enrollment.getSections()) {
183            Set<Section> sections = subparts.get(section.getSubpart());
184            if (sections != null) {
185                if (sections.contains(section))
186                    match = true;
187                else
188                    partial = true;
189            }
190        }
191        boolean full = match && !partial;
192        if (isMustBeUsed()) {
193            if (!full) { // not full match -> conflict if there is no other linked section constraint with a full match
194                // check if there is some other constraint taking care of this case
195                boolean hasOtherMatch = false;
196                for (LinkedSections other: enrollment.getStudent().getLinkedSections()) {
197                    if (other.hasFullMatch(enrollment) && nrSharedOfferings(other) > 1) { hasOtherMatch = true; break; }
198                }
199                // no other match -> problem
200                if (!hasOtherMatch && !conflicts.onConflict(enrollment)) return;
201            }
202        }
203        if (full) { // full match -> check other enrollments
204            for (int i = 0; i < enrollment.getStudent().getRequests().size(); i++) {
205                Request request = enrollment.getStudent().getRequests().get(i);
206                if (request.equals(enrollment.getRequest())) continue; // given enrollment
207                Enrollment otherEnrollment = assignment.getEnrollment(request, i);
208                if (otherEnrollment == null || otherEnrollment.getCourse() == null) continue; // not assigned or not course request
209                if (otherEnrollment.getReservation() != null && otherEnrollment.getReservation().canBreakLinkedSections()) continue;
210                Map<Subpart, Set<Section>> otherSubparts = iSections.get(otherEnrollment.getCourse().getOffering());
211                if (otherSubparts == null || otherSubparts.isEmpty()) continue; // offering is not in the link
212                boolean otherMatch = false, otherPartial = false;
213                for (Section section: otherEnrollment.getSections()) {
214                    Set<Section> otherSections = otherSubparts.get(section.getSubpart());
215                    if (otherSections != null) {
216                        if (otherSections.contains(section))
217                            otherMatch = true;
218                        else
219                            otherPartial = true;
220                    }
221                }
222                boolean otherFull = otherMatch && !otherPartial;
223                // not full match -> conflict
224                if (!otherFull) {
225                    // unless there is some other matching distribution for the same offering pair
226                    boolean hasOtherMatch = false;
227                    for (LinkedSections other: enrollment.getStudent().getLinkedSections()) {
228                        if (other.hasFullMatch(enrollment) && other.hasFullMatch(otherEnrollment))
229                            { hasOtherMatch = true; break; }
230                    }
231                    if (!hasOtherMatch && !conflicts.onConflict(otherEnrollment)) return;
232                }
233            }
234        } else { // no or only partial match -> there should be no match in other offerings too
235            for (int i = 0; i < enrollment.getStudent().getRequests().size(); i++) {
236                Request request = enrollment.getStudent().getRequests().get(i);
237                if (request.equals(enrollment.getRequest())) continue; // given enrollment
238                Enrollment otherEnrollment = assignment.getEnrollment(request, i);
239                if (otherEnrollment == null || otherEnrollment.getCourse() == null) continue; // not assigned or not course request
240                if (otherEnrollment.getReservation() != null && otherEnrollment.getReservation().canBreakLinkedSections()) continue;
241                Map<Subpart, Set<Section>> otherSubparts = iSections.get(otherEnrollment.getCourse().getOffering());
242                if (otherSubparts == null || otherSubparts.isEmpty()) continue; // offering is not in the link
243                boolean otherMatch = false, otherPartial = false;
244                for (Section section: otherEnrollment.getSections()) {
245                    Set<Section> otherSections = otherSubparts.get(section.getSubpart());
246                    if (otherSections != null) {
247                        if (otherSections.contains(section))
248                            otherMatch = true;
249                        else
250                            otherPartial = true;
251                    }
252                }
253                boolean otherFull = otherMatch && !otherPartial;
254                // full match -> conflict
255                if (otherFull) {
256                    // unless there is some other matching distribution for the same offering pair
257                    boolean hasOtherMatch = false;
258                    for (LinkedSections other: enrollment.getStudent().getLinkedSections()) {
259                        if (other.hasFullMatch(enrollment) && other.hasFullMatch(otherEnrollment))
260                            { hasOtherMatch = true; break; }
261                    }
262                    if (!hasOtherMatch && !conflicts.onConflict(otherEnrollment)) return;
263                }
264            }
265        }
266    }
267    
268    /**
269     * Check if the given enrollment fully matches this constraint
270     * @param enrollment an enrollment
271     * @return true, if there is a full match
272     */
273    protected boolean hasFullMatch(Enrollment enrollment) {
274        if (enrollment == null || enrollment.getCourse() == null) return false; // not assigned or not course request
275        Map<Subpart, Set<Section>> subparts = iSections.get(enrollment.getCourse().getOffering());
276        if (subparts == null || subparts.isEmpty()) return false; // offering is not in the link
277        boolean match = false, partial = false;
278        for (Section section: enrollment.getSections()) {
279            Set<Section> sections = subparts.get(section.getSubpart());
280            if (sections != null) {
281                if (sections.contains(section))
282                    match = true;
283                else
284                    partial = true;
285            }
286        }
287        return match && !partial;
288    }
289    
290    /**
291     * Number of offerings that are shared with some other linked sections constraint
292     * @param other the other constraint
293     * @return number of offerings in common
294     */
295    protected int nrSharedOfferings(LinkedSections other) {
296        int shared = 0;
297        for (Offering offering: other.getOfferings())
298            if (iSections.containsKey(offering)) shared ++;
299        return shared;
300    }
301    
302    /**
303     * Check for conflicts. If the given enrollment contains sections of this link
304     * (one for each subpart in {@link LinkedSections#getSubparts(Offering)}), another assignment
305     * of this student is in a conflict, if it does not contain the appropriate sections from
306     * {@link LinkedSections#getSubparts(Offering)} and {@link LinkedSections#getSections(Subpart)}.
307     * 
308     * @param assignment current assignment
309     * @param enrollment given enrollment 
310     * @return conflicting enrollment
311     */
312    public Enrollment inConflict(Assignment<Request, Enrollment> assignment, Enrollment enrollment) {
313        return inConflict(enrollment, new CurrentAssignment(assignment));
314    }
315    
316    /**
317     * Check for conflicts. If the given enrollment contains sections of this link
318     * (one for each subpart in {@link LinkedSections#getSubparts(Offering)}), another assignment
319     * of this student is in a conflict, if it does not contain the appropriate sections from
320     * {@link LinkedSections#getSubparts(Offering)} and {@link LinkedSections#getSections(Subpart)}.
321     * 
322     * @param enrollment given enrollment
323     * @param assignment custom assignment 
324     * @return conflicting enrollment
325     */
326    public Enrollment inConflict(Enrollment enrollment, EnrollmentAssignment assignment) {
327        final Toggle<Enrollment> ret = new Toggle<Enrollment>(null);
328        computeConflicts(enrollment, assignment, new ConflictHandler() {
329            @Override
330            public boolean onConflict(Enrollment conflict) {
331                ret.set(conflict);
332                return false;
333            }
334        });
335        return ret.get();
336    }
337    
338    /**
339     * Interface to be able to provide a custom assignment to {@link LinkedSections#computeConflicts(Enrollment, EnrollmentAssignment, ConflictHandler)}
340     */
341    public static interface EnrollmentAssignment {
342        /**
343         * Return enrollment of the given request
344         * @param request given request
345         * @param index index of the request
346         * @return an enrollment
347         */
348        public Enrollment getEnrollment(Request request, int index);
349    }
350    
351    /**
352     * Helper interface to process conflicts in {@link LinkedSections#computeConflicts(Enrollment, EnrollmentAssignment, ConflictHandler)}
353     */
354    public static interface ConflictHandler {
355        /**
356         * Called when there is a conflict, if false the computation of other conflicts is stopped.
357         * @param conflict a conflicting enrollment
358         * @return stop the computation when false
359         */
360        public boolean onConflict(Enrollment conflict);
361    }
362    
363    /**
364     * Current assignment -- default for {@link LinkedSections#computeConflicts(Enrollment, EnrollmentAssignment, ConflictHandler)}
365     */
366    public static class CurrentAssignment implements EnrollmentAssignment {
367        protected Assignment<Request, Enrollment> iAssignment;
368        
369        public CurrentAssignment(Assignment<Request, Enrollment> assignment) {
370            iAssignment = assignment;
371        }
372        
373        /**
374         * Return {@link Request#getAssignment(Assignment)}
375         */
376        @Override
377        public Enrollment getEnrollment(Request request, int index) {
378            return iAssignment.getValue(request);
379        }
380    }
381    
382    /**
383     * Return whether this constraint must be used
384     * @return if true, a pair of linked sections must be used when a student requests both courses
385     */
386    public boolean isMustBeUsed() { return iMustBeUsed; }
387
388    /**
389     * Set whether this constraint must be used
390     * @param mustBeUsed if true,  a pair of linked sections must be used when a student requests both courses
391     */
392    public void setMustBeUsed(boolean mustBeUsed) { iMustBeUsed = mustBeUsed; }
393    
394    @Override
395    public String toString() {
396        String sections = "";
397        for (Map.Entry<Offering, Map<Subpart, Set<Section>>> e: iSections.entrySet()) {
398            sections += (sections.isEmpty() ? "" : "; ") + e.getKey().getName();
399            Set<String> ids = new TreeSet<String>();
400            for (Map.Entry<Subpart, Set<Section>> f: e.getValue().entrySet()) {
401                for (Section s: f.getValue())
402                    ids.add(s.getName());
403                sections += ":" + ids;
404            }
405        }
406        return "LinkedSections{" + sections + "}";
407    }
408    
409    private static class Toggle<T> {
410        private T iValue;
411        Toggle(T value) { set(value); }
412        void set(T value) { iValue = value; }
413        T get() { return iValue; }
414    }
415    
416    /**
417     * Linked sections constraint -- to be created for each student that requests two
418     * or more offerings of this link
419     */
420    public class LinkedSectionsConstraint extends Constraint<Request, Enrollment> {
421        private Student iStudent;
422        
423        /**
424         * Constructor
425         * @param student a student
426         * @param requests sub-set of student requests {@link Student#getRequests()} that contains offerings of this link
427         */
428        protected LinkedSectionsConstraint(Student student, Collection<Request> requests) {
429            iStudent = student;
430            for (Request request: requests)
431                addVariable(request);
432        }
433        
434        /**
435         * Return student
436         * @return student
437         */
438        public Student getStudent() { return iStudent; }
439
440        /**
441         * Return linked section
442         * @return linked sections constraint
443         */
444        public LinkedSections getLinkedSections() { return LinkedSections.this; }
445
446        /**
447         * Compute conflicts using {@link LinkedSections#computeConflicts(Assignment, Enrollment, ConflictHandler)}
448         */
449        @Override
450        public void computeConflicts(Assignment<Request, Enrollment> assignment, Enrollment value, final Set<Enrollment> conflicts) {
451            getLinkedSections().computeConflicts(assignment, value, new ConflictHandler() {
452                @Override
453                public boolean onConflict(Enrollment conflict) {
454                    conflicts.add(conflict);
455                    return true;
456                }
457            });
458        }
459        
460        /**
461         * Check consistency using {@link LinkedSections#inConflict(Enrollment, EnrollmentAssignment)}
462         */
463        @Override
464        public boolean isConsistent(Enrollment enrollment, final Enrollment other) {
465            return getLinkedSections().inConflict(enrollment, new LinkedSections.EnrollmentAssignment() {
466                @Override
467                public Enrollment getEnrollment(Request request, int indext) {
468                    return (request.equals(other.getRequest()) ? other : null);
469                }
470            }) == null;
471        }
472        
473        /**
474         * Check for conflict using {@link LinkedSections#inConflict(Assignment, Enrollment)}
475         */
476        @Override
477        public boolean inConflict(Assignment<Request, Enrollment> assignment, Enrollment value) {
478            return getLinkedSections().inConflict(assignment, value) != null;
479        }
480        
481        @Override
482        public String toString() {
483            return getLinkedSections().toString();
484        }
485    }
486}