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}