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