001 package net.sf.cpsolver.studentsct.report;
002
003 import java.text.DecimalFormat;
004 import java.util.Collections;
005 import java.util.Comparator;
006 import java.util.HashSet;
007 import java.util.HashMap;
008 import java.util.Iterator;
009 import java.util.Map;
010 import java.util.Set;
011 import java.util.TreeSet;
012
013 import net.sf.cpsolver.ifs.util.CSVFile;
014 import net.sf.cpsolver.ifs.util.DistanceMetric;
015 import net.sf.cpsolver.studentsct.StudentSectioningModel;
016 import net.sf.cpsolver.studentsct.extension.DistanceConflict;
017 import net.sf.cpsolver.studentsct.extension.DistanceConflict.Conflict;
018 import net.sf.cpsolver.studentsct.model.Course;
019 import net.sf.cpsolver.studentsct.model.Enrollment;
020 import net.sf.cpsolver.studentsct.model.Request;
021 import net.sf.cpsolver.studentsct.model.Section;
022 import net.sf.cpsolver.studentsct.model.Student;
023
024 /**
025 * This class lists distance student conflicts in a {@link CSVFile} comma
026 * separated text file. Two sections that are attended by the same student are
027 * considered in a distance conflict if they are back-to-back taught in
028 * locations that are two far away. See {@link DistanceConflict} for more
029 * details. <br>
030 * <br>
031 *
032 * Each line represent a pair if courses that have one or more distance
033 * conflicts in between (columns Course1, Course2), column NrStud displays the
034 * number of student distance conflicts (weighted by requests weights), and
035 * column AvgDist displays the average distance for all the distance conflicts
036 * between these two courses. The column NoAlt is Y when every possible
037 * enrollment of the first course is either overlapping or there is a distance
038 * conflict with every possible enrollment of the second course (it is N
039 * otherwise) and a column Reason which lists the sections that are involved in
040 * a distance conflict.
041 *
042 * <br>
043 * <br>
044 *
045 * Usage: new DistanceConflictTable(model),createTable(true, true).save(aFile);
046 *
047 * <br>
048 * <br>
049 *
050 * @version StudentSct 1.2 (Student Sectioning)<br>
051 * Copyright (C) 2007 - 2010 Tomas Muller<br>
052 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
053 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
054 * <br>
055 * This library is free software; you can redistribute it and/or modify
056 * it under the terms of the GNU Lesser General Public License as
057 * published by the Free Software Foundation; either version 3 of the
058 * License, or (at your option) any later version. <br>
059 * <br>
060 * This library is distributed in the hope that it will be useful, but
061 * WITHOUT ANY WARRANTY; without even the implied warranty of
062 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
063 * Lesser General Public License for more details. <br>
064 * <br>
065 * You should have received a copy of the GNU Lesser General Public
066 * License along with this library; if not see
067 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>.
068 */
069 public class DistanceConflictTable {
070 private static org.apache.log4j.Logger sLog = org.apache.log4j.Logger.getLogger(DistanceConflictTable.class);
071 private static DecimalFormat sDF = new DecimalFormat("0.000");
072
073 private StudentSectioningModel iModel = null;
074 private DistanceConflict iDC = null;
075 private DistanceMetric iDM = null;
076
077 /**
078 * Constructor
079 *
080 * @param model
081 * student sectioning model
082 */
083 public DistanceConflictTable(StudentSectioningModel model) {
084 iModel = model;
085 iDC = model.getDistanceConflict();
086 if (iDC == null) {
087 iDM = new DistanceMetric(model.getProperties());
088 iDC = new DistanceConflict(iDM, model.getProperties());
089 } else {
090 iDM = iDC.getDistanceMetric();
091 }
092 }
093
094 /** Return student sectioning model */
095 public StudentSectioningModel getModel() {
096 return iModel;
097 }
098
099 /**
100 * True, if there is no pair of enrollments of r1 and r2 that is not in a
101 * hard conflict and without a distance conflict
102 */
103 private boolean areInHardConfict(Request r1, Request r2) {
104 for (Enrollment e1 : r1.values()) {
105 for (Enrollment e2 : r2.values()) {
106 if (!e1.isOverlapping(e2) && iDC.nrConflicts(e1, e2) == 0)
107 return false;
108 }
109 }
110 return true;
111 }
112
113 /**
114 * Create report
115 *
116 * @param includeLastLikeStudents
117 * true, if last-like students should be included (i.e.,
118 * {@link Student#isDummy()} is true)
119 * @param includeRealStudents
120 * true, if real students should be included (i.e.,
121 * {@link Student#isDummy()} is false)
122 * @return report as comma separated text file
123 */
124 @SuppressWarnings("unchecked")
125 public CSVFile createTable(boolean includeLastLikeStudents, boolean includeRealStudents) {
126 CSVFile csv = new CSVFile();
127 csv.setHeader(new CSVFile.CSVField[] { new CSVFile.CSVField("Course1"), new CSVFile.CSVField("Course2"),
128 new CSVFile.CSVField("NrStud"), new CSVFile.CSVField("AvgDist"),
129 new CSVFile.CSVField("NoAlt"), new CSVFile.CSVField("Reason") });
130 Set<Conflict> confs = iDC.computeAllConflicts();
131 HashMap<Course, HashMap<Course, Object[]>> distConfTable = new HashMap<Course, HashMap<Course, Object[]>>();
132 for (Conflict conflict : confs) {
133 if (conflict.getStudent().isDummy() && !includeLastLikeStudents)
134 continue;
135 if (!conflict.getStudent().isDummy() && !includeRealStudents)
136 continue;
137 Section s1 = conflict.getS1(), s2 = conflict.getS2();
138 Course c1 = null, c2 = null;
139 Request r1 = null, r2 = null;
140 for (Request request : conflict.getStudent().getRequests()) {
141 Enrollment enrollment = request.getAssignment();
142 if (enrollment == null || !enrollment.isCourseRequest())
143 continue;
144 if (c1 == null && enrollment.getAssignments().contains(s1)) {
145 c1 = enrollment.getCourse();
146 r1 = request;
147 }
148 if (c2 == null && enrollment.getAssignments().contains(s2)) {
149 c2 = enrollment.getCourse();
150 r2 = request;
151 }
152 }
153 if (c1 == null) {
154 sLog.error("Unable to find a course for " + s1);
155 continue;
156 }
157 if (c2 == null) {
158 sLog.error("Unable to find a course for " + s2);
159 continue;
160 }
161 if (c1.getName().compareTo(c2.getName()) > 0) {
162 Course x = c1;
163 c1 = c2;
164 c2 = x;
165 Section y = s1;
166 s1 = s2;
167 s2 = y;
168 }
169 if (c1.equals(c2) && s1.getName().compareTo(s2.getName()) > 0) {
170 Section y = s1;
171 s1 = s2;
172 s2 = y;
173 }
174 HashMap<Course, Object[]> firstCourseTable = distConfTable.get(c1);
175 if (firstCourseTable == null) {
176 firstCourseTable = new HashMap<Course, Object[]>();
177 distConfTable.put(c1, firstCourseTable);
178 }
179 Object[] secondCourseTable = firstCourseTable.get(c2);
180 double nrStud = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[0]).doubleValue()) + 1.0;
181 double dist = (secondCourseTable == null ? 0.0 : ((Double) secondCourseTable[1]).doubleValue()) + (conflict.getDistance(iDM));
182 boolean hard = (secondCourseTable == null ? areInHardConfict(r1, r2) : ((Boolean) secondCourseTable[2]).booleanValue());
183 HashSet<String> expl = (HashSet<String>) (secondCourseTable == null ? null : secondCourseTable[3]);
184 if (expl == null)
185 expl = new HashSet<String>();
186 expl.add(s1.getSubpart().getName() + " " + s1.getTime().getLongName() + " "
187 + s1.getPlacement().getRoomName(",") + " vs " + s2.getSubpart().getName() + " "
188 + s2.getTime().getLongName() + " " + s2.getPlacement().getRoomName(","));
189 firstCourseTable.put(c2, new Object[] { new Double(nrStud), new Double(dist), new Boolean(hard), expl });
190 }
191 for (Map.Entry<Course, HashMap<Course, Object[]>> entry : distConfTable.entrySet()) {
192 Course c1 = entry.getKey();
193 HashMap<Course, Object[]> firstCourseTable = entry.getValue();
194 for (Map.Entry<Course, Object[]> entry2 : firstCourseTable.entrySet()) {
195 Course c2 = entry2.getKey();
196 Object[] secondCourseTable = entry2.getValue();
197 HashSet<String> expl = (HashSet<String>) secondCourseTable[3];
198 String explStr = "";
199 for (Iterator<String> k = new TreeSet<String>(expl).iterator(); k.hasNext();)
200 explStr += k.next() + (k.hasNext() ? "\n" : "");
201 double nrStud = ((Double) secondCourseTable[0]).doubleValue();
202 double dist = ((Double) secondCourseTable[1]).doubleValue() / nrStud;
203 csv.addLine(new CSVFile.CSVField[] { new CSVFile.CSVField(c1.getName()),
204 new CSVFile.CSVField(c2.getName()), new CSVFile.CSVField(sDF.format(nrStud)),
205 new CSVFile.CSVField(sDF.format(dist)),
206 new CSVFile.CSVField(((Boolean) secondCourseTable[2]).booleanValue() ? "Y" : "N"),
207 new CSVFile.CSVField(explStr) });
208 }
209 }
210 if (csv.getLines() != null)
211 Collections.sort(csv.getLines(), new Comparator<CSVFile.CSVLine>() {
212 @Override
213 public int compare(CSVFile.CSVLine l1, CSVFile.CSVLine l2) {
214 // int cmp =
215 // l2.getField(4).toString().compareTo(l1.getField(4).toString());
216 // if (cmp!=0) return cmp;
217 int cmp = Double.compare(l2.getField(2).toDouble(), l1.getField(2).toDouble());
218 if (cmp != 0)
219 return cmp;
220 cmp = l1.getField(0).toString().compareTo(l2.getField(0).toString());
221 if (cmp != 0)
222 return cmp;
223 return l1.getField(1).toString().compareTo(l2.getField(1).toString());
224 }
225 });
226 return csv;
227 }
228 }