001package org.cpsolver.studentsct.report;
002
003import java.text.DecimalFormat;
004import java.util.ArrayList;
005import java.util.Collections;
006import java.util.Comparator;
007import java.util.HashSet;
008import java.util.HashMap;
009import java.util.Iterator;
010import java.util.List;
011import java.util.Map;
012import java.util.Set;
013import java.util.TreeSet;
014
015import org.cpsolver.ifs.assignment.Assignment;
016import org.cpsolver.ifs.model.GlobalConstraint;
017import org.cpsolver.ifs.util.CSVFile;
018import org.cpsolver.ifs.util.DataProperties;
019import org.cpsolver.studentsct.StudentSectioningModel;
020import org.cpsolver.studentsct.constraint.ConfigLimit;
021import org.cpsolver.studentsct.constraint.CourseLimit;
022import org.cpsolver.studentsct.constraint.SectionLimit;
023import org.cpsolver.studentsct.model.Course;
024import org.cpsolver.studentsct.model.CourseRequest;
025import org.cpsolver.studentsct.model.Enrollment;
026import org.cpsolver.studentsct.model.Request;
027import org.cpsolver.studentsct.model.Section;
028
029
030/**
031 * This class lists conflicting courses in a {@link CSVFile} comma separated
032 * text file. <br>
033 * <br>
034 * 
035 * Each line represent a course that has some unassigned course requests (column
036 * UnasgnCrs), course that was conflicting with that course (column ConflCrs),
037 * and number of students with that conflict. So, for instance if there was a
038 * student which cannot attend course A with weight 1.5 (e.g., 10 last-like
039 * students projected to 15), and when A had two possible assignments for that
040 * student, one conflicting with C (assigned to that student) and the other with
041 * D, then 0.75 (1.5/2) was added to rows A, B and A, C. The column NoAlt is Y
042 * when every possible enrollment of the first course is overlapping with every
043 * possible enrollment of the second course (it is N otherwise) and a column
044 * Reason which lists the overlapping sections.
045 * 
046 * <br>
047 * <br>
048 * 
049 * Usage: new CourseConflictTable(model),createTable(true, true).save(aFile);
050 * 
051 * <br>
052 * <br>
053 * 
054 * @version StudentSct 1.3 (Student Sectioning)<br>
055 *          Copyright (C) 2007 - 2014 Tomáš Müller<br>
056 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
057 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
058 * <br>
059 *          This library is free software; you can redistribute it and/or modify
060 *          it under the terms of the GNU Lesser General Public License as
061 *          published by the Free Software Foundation; either version 3 of the
062 *          License, or (at your option) any later version. <br>
063 * <br>
064 *          This library is distributed in the hope that it will be useful, but
065 *          WITHOUT ANY WARRANTY; without even the implied warranty of
066 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
067 *          Lesser General Public License for more details. <br>
068 * <br>
069 *          You should have received a copy of the GNU Lesser General Public
070 *          License along with this library; if not see
071 *          <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
072 */
073
074public class CourseConflictTable extends AbstractStudentSectioningReport {
075    private static org.apache.logging.log4j.Logger sLog = org.apache.logging.log4j.LogManager.getLogger(CourseConflictTable.class);
076    private static DecimalFormat sDF = new DecimalFormat("0.000");
077
078    /**
079     * Constructor
080     * 
081     * @param model
082     *            student sectioning model
083     */
084    public CourseConflictTable(StudentSectioningModel model) {
085        super(model);
086    }
087
088    /**
089     * True, if there is no pair of enrollments of r1 and r2 that is not in a
090     * hard conflict
091     */
092    private boolean areInHardConfict(Assignment<Request, Enrollment> assignment, Request r1, Request r2) {
093        for (Enrollment e1 : r1.values(assignment)) {
094            for (Enrollment e2 : r2.values(assignment)) {
095                if (!e1.isOverlapping(e2))
096                    return false;
097            }
098        }
099        return true;
100    }
101
102    /**
103     * Return a set of explanations (Strings) for conflicts between the given
104     * enrollments
105     * 
106     * @param enrl
107     *            an enrollment
108     * @param conflict
109     *            an enrollment conflicting with enrl
110     * @return a set of explanations, (e.g., AB 101 Lec 1 MWF 7:30 - 8:20 vs AB
111     *         201 Lec 1 F 7:30 - 9:20)
112     */
113    private Set<String> explanations(Assignment<Request, Enrollment> assignment, Enrollment enrl, Enrollment conflict, boolean useAmPm) {
114        Set<String> expl = new HashSet<String>();
115        for (Section s1 : enrl.getSections()) {
116            for (Section s2 : conflict.getSections()) {
117                if (s1.isOverlapping(s2))
118                    expl.add(s1.getSubpart().getName() + " " + s1.getTime().getLongName(useAmPm) + " vs "
119                            + s2.getSubpart().getName() + " " + s2.getTime().getLongName(useAmPm));
120            }
121        }
122        for (Section s1 : enrl.getSections()) {
123            if (conflict.getAssignments().contains(s1)
124                    && SectionLimit.getEnrollmentWeight(assignment, s1, enrl.getRequest()) > s1.getLimit()) {
125                expl.add(s1.getSubpart().getName() + " n/a");
126            }
127        }
128        if (enrl.getConfig() != null && enrl.getConfig().equals(conflict.getConfig())) {
129            if (ConfigLimit.getEnrollmentWeight(assignment, enrl.getConfig(), enrl.getRequest()) > enrl.getConfig().getLimit()) {
130                expl.add(enrl.getConfig().getName() + " n/a");
131            }
132        }
133        if (enrl.getCourse() != null && enrl.getCourse().equals(conflict.getCourse())) {
134            if (CourseLimit.getEnrollmentWeight(assignment, enrl.getCourse(), enrl.getRequest()) > enrl.getCourse().getLimit()) {
135                expl.add(enrl.getCourse().getName() + " n/a");
136            }
137        }
138        return expl;
139    }
140
141    /**
142     * Create report
143     * 
144     * @param assignment current assignment
145     * @return report as comma separated text file
146     */
147    @SuppressWarnings("unchecked")
148    @Override
149    public CSVFile createTable(Assignment<Request, Enrollment> assignment, DataProperties properties) {
150        CSVFile csv = new CSVFile();
151        csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("UnasgnCrs"), new CSVFile.CSVField("ConflCrs"),
152                new CSVFile.CSVField("NrStud"), new CSVFile.CSVField("StudWeight"), new CSVFile.CSVField("NoAlt"),
153                new CSVFile.CSVField("Reason") });
154        HashMap<Course, HashMap<Course, Object[]>> unassignedCourseTable = new HashMap<Course, HashMap<Course, Object[]>>();
155        for (Request request : new ArrayList<Request>(getModel().unassignedVariables(assignment))) {
156            if (!matches(request)) continue;
157            if (request instanceof CourseRequest) {
158                CourseRequest courseRequest = (CourseRequest) request;
159                if (courseRequest.getStudent().isComplete(assignment))
160                    continue;
161
162                List<Enrollment> values = courseRequest.values(assignment);
163                SectionLimit limitConstraint = null;
164                for (GlobalConstraint<Request, Enrollment> c: getModel().globalConstraints()) {
165                    if (c instanceof SectionLimit) {
166                        limitConstraint = (SectionLimit)c;
167                        break;
168                    }
169                }
170                if (limitConstraint == null) {
171                    limitConstraint = new SectionLimit(new DataProperties());
172                    limitConstraint.setModel(getModel());
173                }
174                List<Enrollment> availableValues = new ArrayList<Enrollment>(values.size());
175                for (Enrollment enrollment : values) {
176                    if (!limitConstraint.inConflict(assignment, enrollment))
177                        availableValues.add(enrollment);
178                }
179
180                if (availableValues.isEmpty()) {
181                    Course course = courseRequest.getCourses().get(0);
182                    HashMap<Course, Object[]> conflictCourseTable = unassignedCourseTable.get(course);
183                    if (conflictCourseTable == null) {
184                        conflictCourseTable = new HashMap<Course, Object[]>();
185                        unassignedCourseTable.put(course, conflictCourseTable);
186                    }
187                    Object[] weight = conflictCourseTable.get(course);
188                    double nrStud = (weight == null ? 0.0 : ((Double) weight[0]).doubleValue()) + 1.0;
189                    double nrStudW = (weight == null ? 0.0 : ((Double) weight[1]).doubleValue()) + request.getWeight();
190                    boolean noAlt = (weight == null ? true : ((Boolean) weight[2]).booleanValue());
191                    HashSet<String> expl = (weight == null ? new HashSet<String>() : (HashSet<String>) weight[3]);
192                    expl.add(course.getName() + " n/a");
193                    conflictCourseTable.put(course, new Object[] { Double.valueOf(nrStud), Double.valueOf(nrStudW),
194                            Boolean.valueOf(noAlt), expl });
195                }
196
197                for (Enrollment enrollment : availableValues) {
198                    Set<Enrollment> conflicts = getModel().conflictValues(assignment, enrollment);
199                    if (conflicts.isEmpty()) {
200                        sLog.warn("Request " + courseRequest + " of student " + courseRequest.getStudent() + " not assigned, however, no conflicts were returned.");
201                        assignment.assign(0, enrollment);
202                        break;
203                    }
204                    Course course = null;
205                    for (Course c : courseRequest.getCourses()) {
206                        if (c.getOffering().equals(enrollment.getConfig().getOffering())) {
207                            course = c;
208                            break;
209                        }
210                    }
211                    if (course == null) {
212                        sLog.warn("Course not found for request " + courseRequest + " of student " + courseRequest.getStudent() + ".");
213                        continue;
214                    }
215                    HashMap<Course, Object[]> conflictCourseTable = unassignedCourseTable.get(course);
216                    if (conflictCourseTable == null) {
217                        conflictCourseTable = new HashMap<Course, Object[]>();
218                        unassignedCourseTable.put(course, conflictCourseTable);
219                    }
220                    for (Enrollment conflict : conflicts) {
221                        if (conflict.variable() instanceof CourseRequest) {
222                            CourseRequest conflictCourseRequest = (CourseRequest) conflict.variable();
223                            Course conflictCourse = null;
224                            for (Course c : conflictCourseRequest.getCourses()) {
225                                if (c.getOffering().equals(conflict.getConfig().getOffering())) {
226                                    conflictCourse = c;
227                                    break;
228                                }
229                            }
230                            if (conflictCourse == null) {
231                                sLog.warn("Course not found for request " + conflictCourseRequest + " of student "
232                                        + conflictCourseRequest.getStudent() + ".");
233                                continue;
234                            }
235                            double weightThisConflict = request.getWeight() / availableValues.size() / conflicts.size();
236                            double partThisConflict = 1.0 / availableValues.size() / conflicts.size();
237                            Object[] weight = conflictCourseTable.get(conflictCourse);
238                            double nrStud = (weight == null ? 0.0 : ((Double) weight[0]).doubleValue())
239                                    + partThisConflict;
240                            double nrStudW = (weight == null ? 0.0 : ((Double) weight[1]).doubleValue())
241                                    + weightThisConflict;
242                            boolean noAlt = (weight == null ? areInHardConfict(assignment, request, conflict.getRequest())
243                                    : ((Boolean) weight[2]).booleanValue());
244                            HashSet<String> expl = (weight == null ? new HashSet<String>()
245                                    : (HashSet<String>) weight[3]);
246                            expl.addAll(explanations(assignment, enrollment, conflict, isUseAmPm()));
247                            conflictCourseTable.put(conflictCourse, new Object[] { Double.valueOf(nrStud),
248                                    Double.valueOf(nrStudW), Boolean.valueOf(noAlt), expl });
249                        }
250                    }
251                }
252            }
253        }
254        for (Map.Entry<Course, HashMap<Course, Object[]>> entry : unassignedCourseTable.entrySet()) {
255            Course unassignedCourse = entry.getKey();
256            HashMap<Course, Object[]> conflictCourseTable = entry.getValue();
257            for (Map.Entry<Course, Object[]> entry2 : conflictCourseTable.entrySet()) {
258                Course conflictCourse = entry2.getKey();
259                Object[] weight = entry2.getValue();
260                HashSet<String> expl = (HashSet<String>) weight[3];
261                String explStr = "";
262                for (Iterator<String> k = new TreeSet<String>(expl).iterator(); k.hasNext();)
263                    explStr += k.next() + (k.hasNext() ? "\n" : "");
264                csv.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(unassignedCourse.getName()),
265                        new CSVFile.CSVField(conflictCourse.getName()), new CSVFile.CSVField(sDF.format(weight[0])),
266                        new CSVFile.CSVField(sDF.format(weight[1])),
267                        new CSVFile.CSVField(((Boolean) weight[2]).booleanValue() ? "Y" : "N"),
268                        new CSVFile.CSVField(explStr) });
269            }
270        }
271        if (csv.getLines() != null)
272            Collections.sort(csv.getLines(), new Comparator<CSVFile.CSVLine>() {
273                @Override
274                public int compare(CSVFile.CSVLine l1, CSVFile.CSVLine l2) {
275                    // int cmp =
276                    // l2.getField(3).toString().compareTo(l1.getField(3).toString());
277                    // if (cmp!=0) return cmp;
278                    int cmp = Double.compare(l2.getField(2).toDouble(), l1.getField(2).toDouble());
279                    if (cmp != 0)
280                        return cmp;
281                    cmp = l1.getField(0).toString().compareTo(l2.getField(0).toString());
282                    if (cmp != 0)
283                        return cmp;
284                    return l1.getField(1).toString().compareTo(l2.getField(1).toString());
285                }
286            });
287        return csv;
288    }
289}