001package net.sf.cpsolver.studentsct; 002 003import java.io.BufferedReader; 004import java.io.File; 005import java.io.FileInputStream; 006import java.io.FileOutputStream; 007import java.io.FileReader; 008import java.io.FileWriter; 009import java.io.IOException; 010import java.io.PrintWriter; 011import java.text.DecimalFormat; 012import java.util.ArrayList; 013import java.util.Collection; 014import java.util.Collections; 015import java.util.Comparator; 016import java.util.Date; 017import java.util.HashSet; 018import java.util.HashMap; 019import java.util.Iterator; 020import java.util.List; 021import java.util.Map; 022import java.util.Set; 023import java.util.StringTokenizer; 024import java.util.TreeSet; 025 026import net.sf.cpsolver.ifs.heuristics.BacktrackNeighbourSelection; 027import net.sf.cpsolver.ifs.model.Neighbour; 028import net.sf.cpsolver.ifs.solution.Solution; 029import net.sf.cpsolver.ifs.solution.SolutionListener; 030import net.sf.cpsolver.ifs.solver.Solver; 031import net.sf.cpsolver.ifs.solver.SolverListener; 032import net.sf.cpsolver.ifs.util.DataProperties; 033import net.sf.cpsolver.ifs.util.JProf; 034import net.sf.cpsolver.ifs.util.ToolBox; 035import net.sf.cpsolver.studentsct.check.CourseLimitCheck; 036import net.sf.cpsolver.studentsct.check.InevitableStudentConflicts; 037import net.sf.cpsolver.studentsct.check.OverlapCheck; 038import net.sf.cpsolver.studentsct.check.SectionLimitCheck; 039import net.sf.cpsolver.studentsct.extension.DistanceConflict; 040import net.sf.cpsolver.studentsct.extension.TimeOverlapsCounter; 041import net.sf.cpsolver.studentsct.filter.CombinedStudentFilter; 042import net.sf.cpsolver.studentsct.filter.FreshmanStudentFilter; 043import net.sf.cpsolver.studentsct.filter.RandomStudentFilter; 044import net.sf.cpsolver.studentsct.filter.ReverseStudentFilter; 045import net.sf.cpsolver.studentsct.filter.StudentFilter; 046import net.sf.cpsolver.studentsct.heuristics.StudentSctNeighbourSelection; 047import net.sf.cpsolver.studentsct.heuristics.selection.BranchBoundSelection; 048import net.sf.cpsolver.studentsct.heuristics.selection.OnlineSelection; 049import net.sf.cpsolver.studentsct.heuristics.selection.SwapStudentSelection; 050import net.sf.cpsolver.studentsct.heuristics.selection.BranchBoundSelection.BranchBoundNeighbour; 051import net.sf.cpsolver.studentsct.heuristics.studentord.StudentOrder; 052import net.sf.cpsolver.studentsct.heuristics.studentord.StudentRandomOrder; 053import net.sf.cpsolver.studentsct.model.AcademicAreaCode; 054import net.sf.cpsolver.studentsct.model.Course; 055import net.sf.cpsolver.studentsct.model.CourseRequest; 056import net.sf.cpsolver.studentsct.model.Enrollment; 057import net.sf.cpsolver.studentsct.model.Offering; 058import net.sf.cpsolver.studentsct.model.Request; 059import net.sf.cpsolver.studentsct.model.Student; 060import net.sf.cpsolver.studentsct.report.SectionConflictTable; 061import net.sf.cpsolver.studentsct.report.CourseConflictTable; 062import net.sf.cpsolver.studentsct.report.DistanceConflictTable; 063import net.sf.cpsolver.studentsct.report.TimeOverlapConflictTable; 064import net.sf.cpsolver.studentsct.report.UnbalancedSectionsTable; 065 066import org.apache.log4j.Level; 067import org.apache.log4j.Logger; 068import org.dom4j.Document; 069import org.dom4j.DocumentHelper; 070import org.dom4j.Element; 071import org.dom4j.io.OutputFormat; 072import org.dom4j.io.SAXReader; 073import org.dom4j.io.XMLWriter; 074 075/** 076 * A main class for running of the student sectioning solver from command line. <br> 077 * <br> 078 * Usage:<br> 079 * java -Xmx1024m -jar studentsct-1.1.jar config.properties [input_file] 080 * [output_folder] [batch|online|simple]<br> 081 * <br> 082 * Modes:<br> 083 * batch ... batch sectioning mode (default mode -- IFS solver with 084 * {@link StudentSctNeighbourSelection} is used)<br> 085 * online ... online sectioning mode (students are sectioned one by 086 * one, sectioning info (expected/held space) is used)<br> 087 * simple ... simple sectioning mode (students are sectioned one by 088 * one, sectioning info is not used)<br> 089 * See http://www.unitime.org for example configuration files and benchmark data 090 * sets.<br> 091 * <br> 092 * 093 * The test does the following steps: 094 * <ul> 095 * <li>Provided property file is loaded (see {@link DataProperties}). 096 * <li>Output folder is created (General.Output property) and logging is setup 097 * (using log4j). 098 * <li>Input data are loaded from the given XML file (calling 099 * {@link StudentSectioningXMLLoader#load()}). 100 * <li>Solver is executed (see {@link Solver}). 101 * <li>Resultant solution is saved to an XML file (calling 102 * {@link StudentSectioningXMLSaver#save()}. 103 * </ul> 104 * Also, a log and some reports (e.g., {@link CourseConflictTable} and 105 * {@link DistanceConflictTable}) are created in the output folder. 106 * 107 * <br> 108 * <br> 109 * Parameters: 110 * <table border='1'> 111 * <tr> 112 * <th>Parameter</th> 113 * <th>Type</th> 114 * <th>Comment</th> 115 * </tr> 116 * <tr> 117 * <td>Test.LastLikeCourseDemands</td> 118 * <td>{@link String}</td> 119 * <td>Load last-like course demands from the given XML file (in the format that 120 * is being used for last like course demand table in the timetabling 121 * application)</td> 122 * </tr> 123 * <tr> 124 * <td>Test.StudentInfos</td> 125 * <td>{@link String}</td> 126 * <td>Load last-like course demands from the given XML file (in the format that 127 * is being used for last like course demand table in the timetabling 128 * application)</td> 129 * </tr> 130 * <tr> 131 * <td>Test.CrsReq</td> 132 * <td>{@link String}</td> 133 * <td>Load student requests from the given semi-colon separated list files (in 134 * the format that is being used by the old MSF system)</td> 135 * </tr> 136 * <tr> 137 * <td>Test.EtrChk</td> 138 * <td>{@link String}</td> 139 * <td>Load student information (academic area, classification, major, minor) 140 * from the given semi-colon separated list files (in the format that is being 141 * used by the old MSF system)</td> 142 * </tr> 143 * <tr> 144 * <td>Sectioning.UseStudentPreferencePenalties</td> 145 * <td>{@link Boolean}</td> 146 * <td>If true, {@link StudentPreferencePenalties} are used (applicable only for 147 * online sectioning)</td> 148 * </tr> 149 * <tr> 150 * <td>Test.StudentOrder</td> 151 * <td>{@link String}</td> 152 * <td>A class that is used for ordering of students (must be an interface of 153 * {@link StudentOrder}, default is {@link StudentRandomOrder}, not applicable 154 * only for batch sectioning)</td> 155 * </tr> 156 * <tr> 157 * <td>Test.CombineStudents</td> 158 * <td>{@link File}</td> 159 * <td>If provided, students are combined from the input file (last-like 160 * students) and the provided file (real students). Real non-freshmen students 161 * are taken from real data, last-like data are loaded on top of the real data 162 * (all students, but weighted to occupy only the remaining space).</td> 163 * </tr> 164 * <tr> 165 * <td>Test.CombineStudentsLastLike</td> 166 * <td>{@link File}</td> 167 * <td>If provided (together with Test.CombineStudents), students are combined 168 * from the this file (last-like students) and Test.CombineStudents file (real 169 * students). Real non-freshmen students are taken from real data, last-like 170 * data are loaded on top of the real data (all students, but weighted to occupy 171 * only the remaining space).</td> 172 * </tr> 173 * <tr> 174 * <td>Test.CombineAcceptProb</td> 175 * <td>{@link Double}</td> 176 * <td>Used in combining students, probability of a non-freshmen real student to 177 * be taken into the combined file (default is 1.0 -- all real non-freshmen 178 * students are taken).</td> 179 * </tr> 180 * <tr> 181 * <td>Test.FixPriorities</td> 182 * <td>{@link Boolean}</td> 183 * <td>If true, course/free time request priorities are corrected (to go from 184 * zero, without holes or duplicates).</td> 185 * </tr> 186 * <tr> 187 * <td>Test.ExtraStudents</td> 188 * <td>{@link File}</td> 189 * <td>If provided, students are loaded from the given file on top of the 190 * students loaded from the ordinary input file (students with the same id are 191 * skipped).</td> 192 * </tr> 193 * </table> 194 * <br> 195 * <br> 196 * 197 * @version StudentSct 1.2 (Student Sectioning)<br> 198 * Copyright (C) 2007 - 2010 Tomáš Müller<br> 199 * <a href="mailto:muller@unitime.org">muller@unitime.org</a><br> 200 * <a href="http://muller.unitime.org">http://muller.unitime.org</a><br> 201 * <br> 202 * This library is free software; you can redistribute it and/or modify 203 * it under the terms of the GNU Lesser General Public License as 204 * published by the Free Software Foundation; either version 3 of the 205 * License, or (at your option) any later version. <br> 206 * <br> 207 * This library is distributed in the hope that it will be useful, but 208 * WITHOUT ANY WARRANTY; without even the implied warranty of 209 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 210 * Lesser General Public License for more details. <br> 211 * <br> 212 * You should have received a copy of the GNU Lesser General Public 213 * License along with this library; if not see 214 * <a href='http://www.gnu.org/licenses/'>http://www.gnu.org/licenses/</a>. 215 */ 216 217public class Test { 218 private static org.apache.log4j.Logger sLog = org.apache.log4j.Logger.getLogger(Test.class); 219 private static java.text.SimpleDateFormat sDateFormat = new java.text.SimpleDateFormat("yyMMdd_HHmmss", 220 java.util.Locale.US); 221 private static DecimalFormat sDF = new DecimalFormat("0.000"); 222 223 /** Load student sectioning model */ 224 public static StudentSectioningModel loadModel(DataProperties cfg) { 225 StudentSectioningModel model = null; 226 try { 227 if (cfg.getProperty("Test.CombineStudents") == null) { 228 model = new StudentSectioningModel(cfg); 229 new StudentSectioningXMLLoader(model).load(); 230 } else { 231 model = combineStudents(cfg, new File(cfg.getProperty("Test.CombineStudentsLastLike", cfg.getProperty( 232 "General.Input", "." + File.separator + "solution.xml"))), new File(cfg 233 .getProperty("Test.CombineStudents"))); 234 } 235 if (cfg.getProperty("Test.ExtraStudents") != null) { 236 StudentSectioningXMLLoader extra = new StudentSectioningXMLLoader(model); 237 extra.setInputFile(new File(cfg.getProperty("Test.ExtraStudents"))); 238 extra.setLoadOfferings(false); 239 extra.setLoadStudents(true); 240 extra.setStudentFilter(new ExtraStudentFilter(model)); 241 extra.load(); 242 } 243 if (cfg.getProperty("Test.LastLikeCourseDemands") != null) 244 loadLastLikeCourseDemandsXml(model, new File(cfg.getProperty("Test.LastLikeCourseDemands"))); 245 if (cfg.getProperty("Test.StudentInfos") != null) 246 loadStudentInfoXml(model, new File(cfg.getProperty("Test.StudentInfos"))); 247 if (cfg.getProperty("Test.CrsReq") != null) 248 loadCrsReqFiles(model, cfg.getProperty("Test.CrsReq")); 249 } catch (Exception e) { 250 sLog.error("Unable to load model, reason: " + e.getMessage(), e); 251 return null; 252 } 253 if (cfg.getPropertyBoolean("Debug.DistanceConflict", false)) 254 DistanceConflict.sDebug = true; 255 if (cfg.getPropertyBoolean("Debug.BranchBoundSelection", false)) 256 BranchBoundSelection.sDebug = true; 257 if (cfg.getPropertyBoolean("Debug.SwapStudentsSelection", false)) 258 SwapStudentSelection.sDebug = true; 259 if (cfg.getPropertyBoolean("Debug.TimeOverlaps", false)) 260 TimeOverlapsCounter.sDebug = true; 261 if (cfg.getProperty("CourseRequest.SameTimePrecise") != null) 262 CourseRequest.sSameTimePrecise = cfg.getPropertyBoolean("CourseRequest.SameTimePrecise", false); 263 Logger.getLogger(BacktrackNeighbourSelection.class).setLevel( 264 cfg.getPropertyBoolean("Debug.BacktrackNeighbourSelection", false) ? Level.DEBUG : Level.INFO); 265 if (cfg.getPropertyBoolean("Test.FixPriorities", false)) 266 fixPriorities(model); 267 return model; 268 } 269 270 /** Batch sectioning test */ 271 public static Solution<Request, Enrollment> batchSectioning(DataProperties cfg) { 272 StudentSectioningModel model = loadModel(cfg); 273 if (model == null) 274 return null; 275 276 if (cfg.getPropertyBoolean("Test.ComputeSectioningInfo", true)) 277 model.clearOnlineSectioningInfos(); 278 279 Solution<Request, Enrollment> solution = solveModel(model, cfg); 280 281 printInfo(solution, cfg.getPropertyBoolean("Test.CreateReports", true), cfg.getPropertyBoolean( 282 "Test.ComputeSectioningInfo", true), cfg.getPropertyBoolean("Test.RunChecks", true)); 283 284 try { 285 Solver<Request, Enrollment> solver = new Solver<Request, Enrollment>(cfg); 286 solver.setInitalSolution(solution); 287 new StudentSectioningXMLSaver(solver).save(new File(new File(cfg.getProperty("General.Output", ".")), 288 "solution.xml")); 289 } catch (Exception e) { 290 sLog.error("Unable to save solution, reason: " + e.getMessage(), e); 291 } 292 293 saveInfoToXML(solution, null, new File(new File(cfg.getProperty("General.Output", ".")), "info.xml")); 294 295 return solution; 296 } 297 298 /** Online sectioning test */ 299 public static Solution<Request, Enrollment> onlineSectioning(DataProperties cfg) throws Exception { 300 StudentSectioningModel model = loadModel(cfg); 301 if (model == null) 302 return null; 303 304 Solution<Request, Enrollment> solution = new Solution<Request, Enrollment>(model, 0, 0); 305 solution.addSolutionListener(new TestSolutionListener()); 306 double startTime = JProf.currentTimeSec(); 307 308 Solver<Request, Enrollment> solver = new Solver<Request, Enrollment>(cfg); 309 solver.setInitalSolution(solution); 310 solver.initSolver(); 311 312 OnlineSelection onlineSelection = new OnlineSelection(cfg); 313 onlineSelection.init(solver); 314 315 double totalPenalty = 0, minPenalty = 0, maxPenalty = 0; 316 double minAvEnrlPenalty = 0, maxAvEnrlPenalty = 0; 317 double totalPrefPenalty = 0, minPrefPenalty = 0, maxPrefPenalty = 0; 318 double minAvEnrlPrefPenalty = 0, maxAvEnrlPrefPenalty = 0; 319 int nrChoices = 0, nrEnrollments = 0, nrCourseRequests = 0; 320 int chChoices = 0, chCourseRequests = 0, chStudents = 0; 321 322 int choiceLimit = model.getProperties().getPropertyInt("Test.ChoicesLimit", -1); 323 324 File outDir = new File(model.getProperties().getProperty("General.Output", ".")); 325 outDir.mkdirs(); 326 PrintWriter pw = new PrintWriter(new FileWriter(new File(outDir, "choices.csv"))); 327 328 List<Student> students = model.getStudents(); 329 try { 330 @SuppressWarnings("rawtypes") 331 Class studentOrdClass = Class.forName(model.getProperties().getProperty("Test.StudentOrder", 332 StudentRandomOrder.class.getName())); 333 @SuppressWarnings("unchecked") 334 StudentOrder studentOrd = (StudentOrder) studentOrdClass.getConstructor( 335 new Class[] { DataProperties.class }).newInstance(new Object[] { model.getProperties() }); 336 students = studentOrd.order(model.getStudents()); 337 } catch (Exception e) { 338 sLog.error("Unable to reorder students, reason: " + e.getMessage(), e); 339 } 340 341 for (Student student : students) { 342 if (student.nrAssignedRequests() > 0) 343 continue; // skip students with assigned courses (i.e., students 344 // already assigned by a batch sectioning process) 345 sLog.info("Sectioning student: " + student); 346 347 BranchBoundSelection.Selection selection = onlineSelection.getSelection(student); 348 BranchBoundNeighbour neighbour = selection.select(); 349 if (neighbour != null) { 350 StudentPreferencePenalties penalties = null; 351 if (selection instanceof OnlineSelection.EpsilonSelection) { 352 OnlineSelection.EpsilonSelection epsSelection = (OnlineSelection.EpsilonSelection) selection; 353 penalties = epsSelection.getPenalties(); 354 for (int i = 0; i < neighbour.getAssignment().length; i++) { 355 Request r = student.getRequests().get(i); 356 if (r instanceof CourseRequest) { 357 nrCourseRequests++; 358 chCourseRequests++; 359 int chChoicesThisRq = 0; 360 CourseRequest request = (CourseRequest) r; 361 for (Enrollment x : request.getAvaiableEnrollments()) { 362 nrEnrollments++; 363 if (epsSelection.isAllowed(i, x)) { 364 nrChoices++; 365 if (choiceLimit <= 0 || chChoicesThisRq < choiceLimit) { 366 chChoices++; 367 chChoicesThisRq++; 368 } 369 } 370 } 371 } 372 } 373 chStudents++; 374 if (chStudents == 100) { 375 pw.println(sDF.format(((double) chChoices) / chCourseRequests)); 376 pw.flush(); 377 chStudents = 0; 378 chChoices = 0; 379 chCourseRequests = 0; 380 } 381 } 382 for (int i = 0; i < neighbour.getAssignment().length; i++) { 383 if (neighbour.getAssignment()[i] == null) 384 continue; 385 Enrollment enrollment = neighbour.getAssignment()[i]; 386 if (enrollment.getRequest() instanceof CourseRequest) { 387 CourseRequest request = (CourseRequest) enrollment.getRequest(); 388 double[] avEnrlMinMax = getMinMaxAvailableEnrollmentPenalty(request); 389 minAvEnrlPenalty += avEnrlMinMax[0]; 390 maxAvEnrlPenalty += avEnrlMinMax[1]; 391 totalPenalty += enrollment.getPenalty(); 392 minPenalty += request.getMinPenalty(); 393 maxPenalty += request.getMaxPenalty(); 394 if (penalties != null) { 395 double[] avEnrlPrefMinMax = penalties.getMinMaxAvailableEnrollmentPenalty(enrollment 396 .getRequest()); 397 minAvEnrlPrefPenalty += avEnrlPrefMinMax[0]; 398 maxAvEnrlPrefPenalty += avEnrlPrefMinMax[1]; 399 totalPrefPenalty += penalties.getPenalty(enrollment); 400 minPrefPenalty += penalties.getMinPenalty(enrollment.getRequest()); 401 maxPrefPenalty += penalties.getMaxPenalty(enrollment.getRequest()); 402 } 403 } 404 } 405 neighbour.assign(solution.getIteration()); 406 sLog.info("Student " + student + " enrolls into " + neighbour); 407 onlineSelection.updateSpace(student); 408 } else { 409 sLog.warn("No solution found."); 410 } 411 solution.update(JProf.currentTimeSec() - startTime); 412 } 413 414 if (chCourseRequests > 0) 415 pw.println(sDF.format(((double) chChoices) / chCourseRequests)); 416 417 pw.flush(); 418 pw.close(); 419 420 solution.saveBest(); 421 422 printInfo(solution, cfg.getPropertyBoolean("Test.CreateReports", true), false, cfg.getPropertyBoolean( 423 "Test.RunChecks", true)); 424 425 HashMap<String, String> extra = new HashMap<String, String>(); 426 sLog.info("Overall penalty is " + getPerc(totalPenalty, minPenalty, maxPenalty) + "% (" 427 + sDF.format(totalPenalty) + "/" + sDF.format(minPenalty) + ".." + sDF.format(maxPenalty) + ")"); 428 extra.put("Overall penalty", getPerc(totalPenalty, minPenalty, maxPenalty) + "% (" + sDF.format(totalPenalty) 429 + "/" + sDF.format(minPenalty) + ".." + sDF.format(maxPenalty) + ")"); 430 extra.put("Overall available enrollment penalty", getPerc(totalPenalty, minAvEnrlPenalty, maxAvEnrlPenalty) 431 + "% (" + sDF.format(totalPenalty) + "/" + sDF.format(minAvEnrlPenalty) + ".." 432 + sDF.format(maxAvEnrlPenalty) + ")"); 433 if (onlineSelection.isUseStudentPrefPenalties()) { 434 sLog.info("Overall preference penalty is " + getPerc(totalPrefPenalty, minPrefPenalty, maxPrefPenalty) 435 + "% (" + sDF.format(totalPrefPenalty) + "/" + sDF.format(minPrefPenalty) + ".." 436 + sDF.format(maxPrefPenalty) + ")"); 437 extra.put("Overall preference penalty", getPerc(totalPrefPenalty, minPrefPenalty, maxPrefPenalty) + "% (" 438 + sDF.format(totalPrefPenalty) + "/" + sDF.format(minPrefPenalty) + ".." 439 + sDF.format(maxPrefPenalty) + ")"); 440 extra.put("Overall preference available enrollment penalty", getPerc(totalPrefPenalty, 441 minAvEnrlPrefPenalty, maxAvEnrlPrefPenalty) 442 + "% (" 443 + sDF.format(totalPrefPenalty) 444 + "/" 445 + sDF.format(minAvEnrlPrefPenalty) 446 + ".." 447 + sDF.format(maxAvEnrlPrefPenalty) + ")"); 448 extra.put("Average number of choices", sDF.format(((double) nrChoices) / nrCourseRequests) + " (" 449 + nrChoices + "/" + nrCourseRequests + ")"); 450 extra.put("Average number of enrollments", sDF.format(((double) nrEnrollments) / nrCourseRequests) + " (" 451 + nrEnrollments + "/" + nrCourseRequests + ")"); 452 } 453 454 try { 455 new StudentSectioningXMLSaver(solver).save(new File(new File(cfg.getProperty("General.Output", ".")), 456 "solution.xml")); 457 } catch (Exception e) { 458 sLog.error("Unable to save solution, reason: " + e.getMessage(), e); 459 } 460 461 saveInfoToXML(solution, extra, new File(new File(cfg.getProperty("General.Output", ".")), "info.xml")); 462 463 return solution; 464 } 465 466 /** 467 * Minimum and maximum enrollment penalty, i.e., 468 * {@link Enrollment#getPenalty()} of all enrollments 469 */ 470 public static double[] getMinMaxEnrollmentPenalty(CourseRequest request) { 471 List<Enrollment> enrollments = request.values(); 472 if (enrollments.isEmpty()) 473 return new double[] { 0, 0 }; 474 double min = Double.MAX_VALUE, max = Double.MIN_VALUE; 475 for (Enrollment enrollment : enrollments) { 476 double penalty = enrollment.getPenalty(); 477 min = Math.min(min, penalty); 478 max = Math.max(max, penalty); 479 } 480 return new double[] { min, max }; 481 } 482 483 /** 484 * Minimum and maximum available enrollment penalty, i.e., 485 * {@link Enrollment#getPenalty()} of all available enrollments 486 */ 487 public static double[] getMinMaxAvailableEnrollmentPenalty(CourseRequest request) { 488 List<Enrollment> enrollments = request.getAvaiableEnrollments(); 489 if (enrollments.isEmpty()) 490 return new double[] { 0, 0 }; 491 double min = Double.MAX_VALUE, max = Double.MIN_VALUE; 492 for (Enrollment enrollment : enrollments) { 493 double penalty = enrollment.getPenalty(); 494 min = Math.min(min, penalty); 495 max = Math.max(max, penalty); 496 } 497 return new double[] { min, max }; 498 } 499 500 /** 501 * Compute percentage 502 * 503 * @param value 504 * current value 505 * @param min 506 * minimal bound 507 * @param max 508 * maximal bound 509 * @return (value-min)/(max-min) 510 */ 511 public static String getPerc(double value, double min, double max) { 512 if (max == min) 513 return sDF.format(100.0); 514 return sDF.format(100.0 - 100.0 * (value - min) / (max - min)); 515 } 516 517 /** 518 * Print some information about the solution 519 * 520 * @param solution 521 * given solution 522 * @param computeTables 523 * true, if reports {@link CourseConflictTable} and 524 * {@link DistanceConflictTable} are to be computed as well 525 * @param computeSectInfos 526 * true, if online sectioning infou is to be computed as well 527 * (see 528 * {@link StudentSectioningModel#computeOnlineSectioningInfos()}) 529 * @param runChecks 530 * true, if checks {@link OverlapCheck} and 531 * {@link SectionLimitCheck} are to be performed as well 532 */ 533 public static void printInfo(Solution<Request, Enrollment> solution, boolean computeTables, 534 boolean computeSectInfos, boolean runChecks) { 535 StudentSectioningModel model = (StudentSectioningModel) solution.getModel(); 536 537 if (computeTables) { 538 if (solution.getModel().assignedVariables().size() > 0) { 539 try { 540 File outDir = new File(model.getProperties().getProperty("General.Output", ".")); 541 outDir.mkdirs(); 542 CourseConflictTable cct = new CourseConflictTable((StudentSectioningModel) solution.getModel()); 543 cct.createTable(true, false).save(new File(outDir, "conflicts-lastlike.csv")); 544 cct.createTable(false, true).save(new File(outDir, "conflicts-real.csv")); 545 546 DistanceConflictTable dct = new DistanceConflictTable((StudentSectioningModel) solution.getModel()); 547 dct.createTable(true, false).save(new File(outDir, "distances-lastlike.csv")); 548 dct.createTable(false, true).save(new File(outDir, "distances-real.csv")); 549 550 SectionConflictTable sct = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.OVERLAPS); 551 sct.createTable(true, false).save(new File(outDir, "time-conflicts-lastlike.csv")); 552 sct.createTable(false, true).save(new File(outDir, "time-conflicts-real.csv")); 553 554 SectionConflictTable ust = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.UNAVAILABILITIES); 555 ust.createTable(true, false).save(new File(outDir, "availability-conflicts-lastlike.csv")); 556 ust.createTable(false, true).save(new File(outDir, "availability-conflicts-real.csv")); 557 558 SectionConflictTable ct = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.OVERLAPS_AND_UNAVAILABILITIES); 559 ct.createTable(true, false).save(new File(outDir, "section-conflicts-lastlike.csv")); 560 ct.createTable(false, true).save(new File(outDir, "section-conflicts-real.csv")); 561 562 UnbalancedSectionsTable ubt = new UnbalancedSectionsTable((StudentSectioningModel) solution.getModel()); 563 ubt.createTable(true, false).save(new File(outDir, "unbalanced-lastlike.csv")); 564 ubt.createTable(false, true).save(new File(outDir, "unbalanced-real.csv")); 565 566 TimeOverlapConflictTable toc = new TimeOverlapConflictTable((StudentSectioningModel) solution.getModel()); 567 toc.createTable(true, false).save(new File(outDir, "time-overlaps-lastlike.csv")); 568 toc.createTable(false, true).save(new File(outDir, "time-overlaps-real.csv")); 569 } catch (IOException e) { 570 sLog.error(e.getMessage(), e); 571 } 572 } 573 574 solution.saveBest(); 575 } 576 577 if (computeSectInfos) 578 model.computeOnlineSectioningInfos(); 579 580 if (runChecks) { 581 try { 582 if (model.getProperties().getPropertyBoolean("Test.InevitableStudentConflictsCheck", false)) { 583 InevitableStudentConflicts ch = new InevitableStudentConflicts(model); 584 if (!ch.check()) 585 ch.getCSVFile().save( 586 new File(new File(model.getProperties().getProperty("General.Output", ".")), 587 "inevitable-conflicts.csv")); 588 } 589 } catch (IOException e) { 590 sLog.error(e.getMessage(), e); 591 } 592 new OverlapCheck(model).check(); 593 new SectionLimitCheck(model).check(); 594 try { 595 CourseLimitCheck ch = new CourseLimitCheck(model); 596 if (!ch.check()) 597 ch.getCSVFile().save( 598 new File(new File(model.getProperties().getProperty("General.Output", ".")), 599 "course-limits.csv")); 600 } catch (IOException e) { 601 sLog.error(e.getMessage(), e); 602 } 603 } 604 605 sLog.info("Best solution found after " + solution.getBestTime() + " seconds (" + solution.getBestIteration() 606 + " iterations)."); 607 sLog.info("Info: " + ToolBox.dict2string(solution.getExtendedInfo(), 2)); 608 } 609 610 /** Solve the student sectioning problem using IFS solver */ 611 public static Solution<Request, Enrollment> solveModel(StudentSectioningModel model, DataProperties cfg) { 612 Solver<Request, Enrollment> solver = new Solver<Request, Enrollment>(cfg); 613 Solution<Request, Enrollment> solution = new Solution<Request, Enrollment>(model, 0, 0); 614 solver.setInitalSolution(solution); 615 if (cfg.getPropertyBoolean("Test.Verbose", false)) { 616 solver.addSolverListener(new SolverListener<Request, Enrollment>() { 617 @Override 618 public boolean variableSelected(long iteration, Request variable) { 619 return true; 620 } 621 622 @Override 623 public boolean valueSelected(long iteration, Request variable, Enrollment value) { 624 return true; 625 } 626 627 @Override 628 public boolean neighbourSelected(long iteration, Neighbour<Request, Enrollment> neighbour) { 629 sLog.debug("Select[" + iteration + "]: " + neighbour); 630 return true; 631 } 632 }); 633 } 634 solution.addSolutionListener(new TestSolutionListener()); 635 636 solver.start(); 637 try { 638 solver.getSolverThread().join(); 639 } catch (InterruptedException e) { 640 } 641 642 solution = solver.lastSolution(); 643 solution.restoreBest(); 644 645 printInfo(solution, false, false, false); 646 647 return solution; 648 } 649 650 /** 651 * Compute last-like student weight for the given course 652 * 653 * @param course 654 * given course 655 * @param real 656 * number of real students for the course 657 * @param lastLike 658 * number of last-like students for the course 659 * @return weight of a student request for the given course 660 */ 661 public static double getLastLikeStudentWeight(Course course, int real, int lastLike) { 662 int projected = course.getProjected(); 663 int limit = course.getLimit(); 664 if (course.getLimit() < 0) { 665 sLog.debug(" -- Course " + course.getName() + " is unlimited."); 666 return 1.0; 667 } 668 if (projected <= 0) { 669 sLog.warn(" -- No projected demand for course " + course.getName() + ", using course limit (" + limit 670 + ")"); 671 projected = limit; 672 } else if (limit < projected) { 673 sLog.warn(" -- Projected number of students is over course limit for course " + course.getName() + " (" 674 + Math.round(projected) + ">" + limit + ")"); 675 projected = limit; 676 } 677 if (lastLike == 0) { 678 sLog.warn(" -- No last like info for course " + course.getName()); 679 return 1.0; 680 } 681 double weight = ((double) Math.max(0, projected - real)) / lastLike; 682 sLog.debug(" -- last like student weight for " + course.getName() + " is " + weight + " (lastLike=" + lastLike 683 + ", real=" + real + ", projected=" + projected + ")"); 684 return weight; 685 } 686 687 /** 688 * Load last-like students from an XML file (the one that is used to load 689 * last like course demands table in the timetabling application) 690 */ 691 public static void loadLastLikeCourseDemandsXml(StudentSectioningModel model, File xml) { 692 try { 693 Document document = (new SAXReader()).read(xml); 694 Element root = document.getRootElement(); 695 HashMap<Course, List<Request>> requests = new HashMap<Course, List<Request>>(); 696 long reqId = 0; 697 for (Iterator<?> i = root.elementIterator("student"); i.hasNext();) { 698 Element studentEl = (Element) i.next(); 699 Student student = new Student(Long.parseLong(studentEl.attributeValue("externalId"))); 700 student.setDummy(true); 701 int priority = 0; 702 HashSet<Course> reqCourses = new HashSet<Course>(); 703 for (Iterator<?> j = studentEl.elementIterator("studentCourse"); j.hasNext();) { 704 Element courseEl = (Element) j.next(); 705 String subjectArea = courseEl.attributeValue("subject"); 706 String courseNbr = courseEl.attributeValue("courseNumber"); 707 Course course = null; 708 offerings: for (Offering offering : model.getOfferings()) { 709 for (Course c : offering.getCourses()) { 710 if (c.getSubjectArea().equals(subjectArea) && c.getCourseNumber().equals(courseNbr)) { 711 course = c; 712 break offerings; 713 } 714 } 715 } 716 if (course == null && courseNbr.charAt(courseNbr.length() - 1) >= 'A' 717 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 718 String courseNbrNoSfx = courseNbr.substring(0, courseNbr.length() - 1); 719 offerings: for (Offering offering : model.getOfferings()) { 720 for (Course c : offering.getCourses()) { 721 if (c.getSubjectArea().equals(subjectArea) 722 && c.getCourseNumber().equals(courseNbrNoSfx)) { 723 course = c; 724 break offerings; 725 } 726 } 727 } 728 } 729 if (course == null) { 730 sLog.warn("Course " + subjectArea + " " + courseNbr + " not found."); 731 } else { 732 if (!reqCourses.add(course)) { 733 sLog.warn("Course " + subjectArea + " " + courseNbr + " already requested."); 734 } else { 735 List<Course> courses = new ArrayList<Course>(1); 736 courses.add(course); 737 CourseRequest request = new CourseRequest(reqId++, priority++, false, student, courses, false, null); 738 List<Request> requestsThisCourse = requests.get(course); 739 if (requestsThisCourse == null) { 740 requestsThisCourse = new ArrayList<Request>(); 741 requests.put(course, requestsThisCourse); 742 } 743 requestsThisCourse.add(request); 744 } 745 } 746 } 747 if (!student.getRequests().isEmpty()) 748 model.addStudent(student); 749 } 750 for (Map.Entry<Course, List<Request>> entry : requests.entrySet()) { 751 Course course = entry.getKey(); 752 List<Request> requestsThisCourse = entry.getValue(); 753 double weight = getLastLikeStudentWeight(course, 0, requestsThisCourse.size()); 754 for (Request request : requestsThisCourse) { 755 request.setWeight(weight); 756 } 757 } 758 } catch (Exception e) { 759 sLog.error(e.getMessage(), e); 760 } 761 } 762 763 /** 764 * Load course request from the given files (in the format being used by the 765 * old MSF system) 766 * 767 * @param model 768 * student sectioning model (with offerings loaded) 769 * @param files 770 * semi-colon separated list of files to be loaded 771 */ 772 public static void loadCrsReqFiles(StudentSectioningModel model, String files) { 773 try { 774 boolean lastLike = model.getProperties().getPropertyBoolean("Test.CrsReqIsLastLike", true); 775 boolean shuffleIds = model.getProperties().getPropertyBoolean("Test.CrsReqShuffleStudentIds", true); 776 boolean tryWithoutSuffix = model.getProperties().getPropertyBoolean("Test.CrsReqTryWithoutSuffix", false); 777 HashMap<Long, Student> students = new HashMap<Long, Student>(); 778 long reqId = 0; 779 for (StringTokenizer stk = new StringTokenizer(files, ";"); stk.hasMoreTokens();) { 780 String file = stk.nextToken(); 781 sLog.debug("Loading " + file + " ..."); 782 BufferedReader in = new BufferedReader(new FileReader(file)); 783 String line; 784 int lineIndex = 0; 785 while ((line = in.readLine()) != null) { 786 lineIndex++; 787 if (line.length() <= 150) 788 continue; 789 char code = line.charAt(13); 790 if (code == 'H' || code == 'T') 791 continue; // skip header and tail 792 long studentId = Long.parseLong(line.substring(14, 23)); 793 Student student = students.get(new Long(studentId)); 794 if (student == null) { 795 student = new Student(studentId); 796 if (lastLike) 797 student.setDummy(true); 798 students.put(new Long(studentId), student); 799 sLog.debug(" -- loading student " + studentId + " ..."); 800 } else 801 sLog.debug(" -- updating student " + studentId + " ..."); 802 line = line.substring(150); 803 while (line.length() >= 20) { 804 String subjectArea = line.substring(0, 4).trim(); 805 String courseNbr = line.substring(4, 8).trim(); 806 if (subjectArea.length() == 0 || courseNbr.length() == 0) { 807 line = line.substring(20); 808 continue; 809 } 810 /* 811 * // UNUSED String instrSel = line.substring(8,10); 812 * //ZZ - Remove previous instructor selection char 813 * reqPDiv = line.charAt(10); //P - Personal preference; 814 * C - Conflict resolution; //0 - (Zero) used by program 815 * only, for change requests to reschedule division // 816 * (used to reschedule canceled division) String reqDiv 817 * = line.substring(11,13); //00 - Reschedule division 818 * String reqSect = line.substring(13,15); //Contains 819 * designator for designator-required courses String 820 * credit = line.substring(15,19); char nameRaise = 821 * line.charAt(19); //N - Name raise 822 */ 823 char action = line.charAt(19); // A - Add; D - Drop; C - 824 // Change 825 sLog.debug(" -- requesting " + subjectArea + " " + courseNbr + " (action:" + action 826 + ") ..."); 827 Course course = null; 828 offerings: for (Offering offering : model.getOfferings()) { 829 for (Course c : offering.getCourses()) { 830 if (c.getSubjectArea().equals(subjectArea) && c.getCourseNumber().equals(courseNbr)) { 831 course = c; 832 break offerings; 833 } 834 } 835 } 836 if (course == null && tryWithoutSuffix && courseNbr.charAt(courseNbr.length() - 1) >= 'A' 837 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 838 String courseNbrNoSfx = courseNbr.substring(0, courseNbr.length() - 1); 839 offerings: for (Offering offering : model.getOfferings()) { 840 for (Course c : offering.getCourses()) { 841 if (c.getSubjectArea().equals(subjectArea) 842 && c.getCourseNumber().equals(courseNbrNoSfx)) { 843 course = c; 844 break offerings; 845 } 846 } 847 } 848 } 849 if (course == null) { 850 if (courseNbr.charAt(courseNbr.length() - 1) >= 'A' 851 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 852 } else { 853 sLog.warn(" -- course " + subjectArea + " " + courseNbr + " not found (file " 854 + file + ", line " + lineIndex + ")"); 855 } 856 } else { 857 CourseRequest courseRequest = null; 858 for (Request request : student.getRequests()) { 859 if (request instanceof CourseRequest 860 && ((CourseRequest) request).getCourses().contains(course)) { 861 courseRequest = (CourseRequest) request; 862 break; 863 } 864 } 865 if (action == 'A') { 866 if (courseRequest == null) { 867 List<Course> courses = new ArrayList<Course>(1); 868 courses.add(course); 869 courseRequest = new CourseRequest(reqId++, student.getRequests().size(), false, student, courses, false, null); 870 } else { 871 sLog.warn(" -- request for course " + course + " is already present"); 872 } 873 } else if (action == 'D') { 874 if (courseRequest == null) { 875 sLog.warn(" -- request for course " + course 876 + " is not present -- cannot be dropped"); 877 } else { 878 student.getRequests().remove(courseRequest); 879 } 880 } else if (action == 'C') { 881 if (courseRequest == null) { 882 sLog.warn(" -- request for course " + course 883 + " is not present -- cannot be changed"); 884 } else { 885 // ? 886 } 887 } else { 888 sLog.warn(" -- unknown action " + action); 889 } 890 } 891 line = line.substring(20); 892 } 893 } 894 in.close(); 895 } 896 HashMap<Course, List<Request>> requests = new HashMap<Course, List<Request>>(); 897 Set<Long> studentIds = new HashSet<Long>(); 898 for (Student student: students.values()) { 899 if (!student.getRequests().isEmpty()) 900 model.addStudent(student); 901 if (shuffleIds) { 902 long newId = -1; 903 while (true) { 904 newId = 1 + (long) (999999999L * Math.random()); 905 if (studentIds.add(new Long(newId))) 906 break; 907 } 908 student.setId(newId); 909 } 910 if (student.isDummy()) { 911 for (Request request : student.getRequests()) { 912 if (request instanceof CourseRequest) { 913 Course course = ((CourseRequest) request).getCourses().get(0); 914 List<Request> requestsThisCourse = requests.get(course); 915 if (requestsThisCourse == null) { 916 requestsThisCourse = new ArrayList<Request>(); 917 requests.put(course, requestsThisCourse); 918 } 919 requestsThisCourse.add(request); 920 } 921 } 922 } 923 } 924 Collections.sort(model.getStudents(), new Comparator<Student>() { 925 @Override 926 public int compare(Student o1, Student o2) { 927 return Double.compare(o1.getId(), o2.getId()); 928 } 929 }); 930 for (Map.Entry<Course, List<Request>> entry : requests.entrySet()) { 931 Course course = entry.getKey(); 932 List<Request> requestsThisCourse = entry.getValue(); 933 double weight = getLastLikeStudentWeight(course, 0, requestsThisCourse.size()); 934 for (Request request : requestsThisCourse) { 935 request.setWeight(weight); 936 } 937 } 938 if (model.getProperties().getProperty("Test.EtrChk") != null) { 939 for (StringTokenizer stk = new StringTokenizer(model.getProperties().getProperty("Test.EtrChk"), ";"); stk 940 .hasMoreTokens();) { 941 String file = stk.nextToken(); 942 sLog.debug("Loading " + file + " ..."); 943 BufferedReader in = new BufferedReader(new FileReader(file)); 944 String line; 945 while ((line = in.readLine()) != null) { 946 if (line.length() < 55) 947 continue; 948 char code = line.charAt(12); 949 if (code == 'H' || code == 'T') 950 continue; // skip header and tail 951 if (code == 'D' || code == 'K') 952 continue; // skip delete nad cancel 953 long studentId = Long.parseLong(line.substring(2, 11)); 954 Student student = students.get(new Long(studentId)); 955 if (student == null) { 956 sLog.info(" -- student " + studentId + " not found"); 957 continue; 958 } 959 sLog.info(" -- reading student " + studentId); 960 String area = line.substring(15, 18).trim(); 961 if (area.length() == 0) 962 continue; 963 String clasf = line.substring(18, 20).trim(); 964 String major = line.substring(21, 24).trim(); 965 String minor = line.substring(24, 27).trim(); 966 student.getAcademicAreaClasiffications().clear(); 967 student.getMajors().clear(); 968 student.getMinors().clear(); 969 student.getAcademicAreaClasiffications().add(new AcademicAreaCode(area, clasf)); 970 if (major.length() > 0) 971 student.getMajors().add(new AcademicAreaCode(area, major)); 972 if (minor.length() > 0) 973 student.getMinors().add(new AcademicAreaCode(area, minor)); 974 } 975 } 976 } 977 int without = 0; 978 for (Student student: students.values()) { 979 if (student.getAcademicAreaClasiffications().isEmpty()) 980 without++; 981 } 982 fixPriorities(model); 983 sLog.info("Students without academic area: " + without); 984 } catch (Exception e) { 985 sLog.error(e.getMessage(), e); 986 } 987 } 988 989 public static void fixPriorities(StudentSectioningModel model) { 990 for (Student student : model.getStudents()) { 991 Collections.sort(student.getRequests(), new Comparator<Request>() { 992 @Override 993 public int compare(Request r1, Request r2) { 994 int cmp = Double.compare(r1.getPriority(), r2.getPriority()); 995 if (cmp != 0) 996 return cmp; 997 return Double.compare(r1.getId(), r2.getId()); 998 } 999 }); 1000 int priority = 0; 1001 for (Request request : student.getRequests()) { 1002 if (priority != request.getPriority()) { 1003 sLog.debug("Change priority of " + request + " to " + priority); 1004 request.setPriority(priority); 1005 } 1006 } 1007 } 1008 } 1009 1010 /** Load student infos from a given XML file. */ 1011 public static void loadStudentInfoXml(StudentSectioningModel model, File xml) { 1012 try { 1013 sLog.info("Loading student infos from " + xml); 1014 Document document = (new SAXReader()).read(xml); 1015 Element root = document.getRootElement(); 1016 HashMap<Long, Student> studentTable = new HashMap<Long, Student>(); 1017 for (Student student : model.getStudents()) { 1018 studentTable.put(new Long(student.getId()), student); 1019 } 1020 for (Iterator<?> i = root.elementIterator("student"); i.hasNext();) { 1021 Element studentEl = (Element) i.next(); 1022 Student student = studentTable.get(Long.valueOf(studentEl.attributeValue("externalId"))); 1023 if (student == null) { 1024 sLog.debug(" -- student " + studentEl.attributeValue("externalId") + " not found"); 1025 continue; 1026 } 1027 sLog.debug(" -- loading info for student " + student); 1028 student.getAcademicAreaClasiffications().clear(); 1029 if (studentEl.element("studentAcadAreaClass") != null) 1030 for (Iterator<?> j = studentEl.element("studentAcadAreaClass").elementIterator("acadAreaClass"); j 1031 .hasNext();) { 1032 Element studentAcadAreaClassElement = (Element) j.next(); 1033 student.getAcademicAreaClasiffications().add( 1034 new AcademicAreaCode(studentAcadAreaClassElement.attributeValue("academicArea"), 1035 studentAcadAreaClassElement.attributeValue("academicClass"))); 1036 } 1037 sLog.debug(" -- acad areas classifs " + student.getAcademicAreaClasiffications()); 1038 student.getMajors().clear(); 1039 if (studentEl.element("studentMajors") != null) 1040 for (Iterator<?> j = studentEl.element("studentMajors").elementIterator("major"); j.hasNext();) { 1041 Element studentMajorElement = (Element) j.next(); 1042 student.getMajors().add( 1043 new AcademicAreaCode(studentMajorElement.attributeValue("academicArea"), 1044 studentMajorElement.attributeValue("code"))); 1045 } 1046 sLog.debug(" -- majors " + student.getMajors()); 1047 student.getMinors().clear(); 1048 if (studentEl.element("studentMinors") != null) 1049 for (Iterator<?> j = studentEl.element("studentMinors").elementIterator("minor"); j.hasNext();) { 1050 Element studentMinorElement = (Element) j.next(); 1051 student.getMinors().add( 1052 new AcademicAreaCode(studentMinorElement.attributeValue("academicArea", ""), 1053 studentMinorElement.attributeValue("code", ""))); 1054 } 1055 sLog.debug(" -- minors " + student.getMinors()); 1056 } 1057 } catch (Exception e) { 1058 sLog.error(e.getMessage(), e); 1059 } 1060 } 1061 1062 /** Save solution info as XML */ 1063 public static void saveInfoToXML(Solution<Request, Enrollment> solution, HashMap<String, String> extra, File file) { 1064 FileOutputStream fos = null; 1065 try { 1066 Document document = DocumentHelper.createDocument(); 1067 document.addComment("Solution Info"); 1068 1069 Element root = document.addElement("info"); 1070 TreeSet<Map.Entry<String, String>> entrySet = new TreeSet<Map.Entry<String, String>>( 1071 new Comparator<Map.Entry<String, String>>() { 1072 @Override 1073 public int compare(Map.Entry<String, String> e1, Map.Entry<String, String> e2) { 1074 return e1.getKey().compareTo(e2.getKey()); 1075 } 1076 }); 1077 entrySet.addAll(solution.getExtendedInfo().entrySet()); 1078 if (extra != null) 1079 entrySet.addAll(extra.entrySet()); 1080 for (Map.Entry<String, String> entry : entrySet) { 1081 root.addElement("property").addAttribute("name", entry.getKey()).setText(entry.getValue()); 1082 } 1083 1084 fos = new FileOutputStream(file); 1085 (new XMLWriter(fos, OutputFormat.createPrettyPrint())).write(document); 1086 fos.flush(); 1087 fos.close(); 1088 fos = null; 1089 } catch (Exception e) { 1090 sLog.error("Unable to save info, reason: " + e.getMessage(), e); 1091 } finally { 1092 try { 1093 if (fos != null) 1094 fos.close(); 1095 } catch (IOException e) { 1096 } 1097 } 1098 } 1099 1100 private static void fixWeights(StudentSectioningModel model) { 1101 HashMap<Course, Integer> lastLike = new HashMap<Course, Integer>(); 1102 HashMap<Course, Integer> real = new HashMap<Course, Integer>(); 1103 HashSet<Long> lastLikeIds = new HashSet<Long>(); 1104 HashSet<Long> realIds = new HashSet<Long>(); 1105 for (Student student : model.getStudents()) { 1106 if (student.isDummy()) { 1107 if (!lastLikeIds.add(new Long(student.getId()))) { 1108 sLog.error("Two last-like student with id " + student.getId()); 1109 } 1110 } else { 1111 if (!realIds.add(new Long(student.getId()))) { 1112 sLog.error("Two real student with id " + student.getId()); 1113 } 1114 } 1115 for (Request request : student.getRequests()) { 1116 if (request instanceof CourseRequest) { 1117 CourseRequest courseRequest = (CourseRequest) request; 1118 Course course = courseRequest.getCourses().get(0); 1119 Integer cnt = (student.isDummy() ? lastLike : real).get(course); 1120 (student.isDummy() ? lastLike : real).put(course, new Integer( 1121 (cnt == null ? 0 : cnt.intValue()) + 1)); 1122 } 1123 } 1124 } 1125 for (Student student : new ArrayList<Student>(model.getStudents())) { 1126 if (student.isDummy() && realIds.contains(new Long(student.getId()))) { 1127 sLog.warn("There is both last-like and real student with id " + student.getId()); 1128 long newId = -1; 1129 while (true) { 1130 newId = 1 + (long) (999999999L * Math.random()); 1131 if (!realIds.contains(new Long(newId)) && !lastLikeIds.contains(new Long(newId))) 1132 break; 1133 } 1134 lastLikeIds.remove(new Long(student.getId())); 1135 lastLikeIds.add(new Long(newId)); 1136 student.setId(newId); 1137 sLog.warn(" -- last-like student id changed to " + student.getId()); 1138 } 1139 for (Request request : new ArrayList<Request>(student.getRequests())) { 1140 if (!student.isDummy()) { 1141 request.setWeight(1.0); 1142 continue; 1143 } 1144 if (request instanceof CourseRequest) { 1145 CourseRequest courseRequest = (CourseRequest) request; 1146 Course course = courseRequest.getCourses().get(0); 1147 Integer lastLikeCnt = lastLike.get(course); 1148 Integer realCnt = real.get(course); 1149 courseRequest.setWeight(getLastLikeStudentWeight(course, realCnt == null ? 0 : realCnt.intValue(), 1150 lastLikeCnt == null ? 0 : lastLikeCnt.intValue())); 1151 } else 1152 request.setWeight(1.0); 1153 if (request.getWeight() <= 0.0) { 1154 model.removeVariable(request); 1155 student.getRequests().remove(request); 1156 } 1157 } 1158 if (student.getRequests().isEmpty()) { 1159 model.getStudents().remove(student); 1160 } 1161 } 1162 } 1163 1164 /** Combine students from the provided two files */ 1165 public static StudentSectioningModel combineStudents(DataProperties cfg, File lastLikeStudentData, 1166 File realStudentData) { 1167 try { 1168 RandomStudentFilter rnd = new RandomStudentFilter(1.0); 1169 1170 StudentSectioningModel model = null; 1171 1172 for (StringTokenizer stk = new StringTokenizer(cfg.getProperty("Test.CombineAcceptProb", "1.0"), ","); stk 1173 .hasMoreTokens();) { 1174 double acceptProb = Double.parseDouble(stk.nextToken()); 1175 sLog.info("Test.CombineAcceptProb=" + acceptProb); 1176 rnd.setProbability(acceptProb); 1177 1178 StudentFilter batchFilter = new CombinedStudentFilter(new ReverseStudentFilter( 1179 new FreshmanStudentFilter()), rnd, CombinedStudentFilter.OP_AND); 1180 1181 model = new StudentSectioningModel(cfg); 1182 StudentSectioningXMLLoader loader = new StudentSectioningXMLLoader(model); 1183 loader.setLoadStudents(false); 1184 loader.load(); 1185 1186 StudentSectioningXMLLoader lastLikeLoader = new StudentSectioningXMLLoader(model); 1187 lastLikeLoader.setInputFile(lastLikeStudentData); 1188 lastLikeLoader.setLoadOfferings(false); 1189 lastLikeLoader.setLoadStudents(true); 1190 lastLikeLoader.load(); 1191 1192 StudentSectioningXMLLoader realLoader = new StudentSectioningXMLLoader(model); 1193 realLoader.setInputFile(realStudentData); 1194 realLoader.setLoadOfferings(false); 1195 realLoader.setLoadStudents(true); 1196 realLoader.setStudentFilter(batchFilter); 1197 realLoader.load(); 1198 1199 fixWeights(model); 1200 1201 fixPriorities(model); 1202 1203 Solver<Request, Enrollment> solver = new Solver<Request, Enrollment>(model.getProperties()); 1204 solver.setInitalSolution(model); 1205 new StudentSectioningXMLSaver(solver).save(new File(new File(model.getProperties().getProperty( 1206 "General.Output", ".")), "solution-r" + ((int) (100.0 * acceptProb)) + ".xml")); 1207 1208 } 1209 1210 return model; 1211 1212 } catch (Exception e) { 1213 sLog.error("Unable to combine students, reason: " + e.getMessage(), e); 1214 return null; 1215 } 1216 } 1217 1218 /** Main */ 1219 public static void main(String[] args) { 1220 try { 1221 DataProperties cfg = new DataProperties(); 1222 cfg.setProperty("Termination.Class", "net.sf.cpsolver.ifs.termination.GeneralTerminationCondition"); 1223 cfg.setProperty("Termination.StopWhenComplete", "true"); 1224 cfg.setProperty("Termination.TimeOut", "600"); 1225 cfg.setProperty("Comparator.Class", "net.sf.cpsolver.ifs.solution.GeneralSolutionComparator"); 1226 cfg.setProperty("Value.Class", "net.sf.cpsolver.studentsct.heuristics.EnrollmentSelection");// net.sf.cpsolver.ifs.heuristics.GeneralValueSelection 1227 cfg.setProperty("Value.WeightConflicts", "1.0"); 1228 cfg.setProperty("Value.WeightNrAssignments", "0.0"); 1229 cfg.setProperty("Variable.Class", "net.sf.cpsolver.ifs.heuristics.GeneralVariableSelection"); 1230 cfg.setProperty("Neighbour.Class", "net.sf.cpsolver.studentsct.heuristics.StudentSctNeighbourSelection"); 1231 cfg.setProperty("General.SaveBestUnassigned", "0"); 1232 cfg.setProperty("Extensions.Classes", 1233 "net.sf.cpsolver.ifs.extension.ConflictStatistics;net.sf.cpsolver.studentsct.extension.DistanceConflict" + 1234 ";net.sf.cpsolver.studentsct.extension.TimeOverlapsCounter"); 1235 cfg.setProperty("Data.Initiative", "puWestLafayetteTrdtn"); 1236 cfg.setProperty("Data.Term", "Fal"); 1237 cfg.setProperty("Data.Year", "2007"); 1238 cfg.setProperty("General.Input", "pu-sectll-fal07-s.xml"); 1239 if (args.length >= 1) { 1240 cfg.load(new FileInputStream(args[0])); 1241 } 1242 cfg.putAll(System.getProperties()); 1243 1244 if (args.length >= 2) { 1245 cfg.setProperty("General.Input", args[1]); 1246 } 1247 1248 if (args.length >= 3) { 1249 File logFile = new File(ToolBox.configureLogging(args[2] + File.separator 1250 + (sDateFormat.format(new Date())), cfg, false, false)); 1251 cfg.setProperty("General.Output", logFile.getParentFile().getAbsolutePath()); 1252 } else if (cfg.getProperty("General.Output") != null) { 1253 cfg.setProperty("General.Output", cfg.getProperty("General.Output", ".") + File.separator 1254 + (sDateFormat.format(new Date()))); 1255 ToolBox.configureLogging(cfg.getProperty("General.Output", "."), cfg, false, false); 1256 } else { 1257 ToolBox.configureLogging(); 1258 cfg.setProperty("General.Output", System.getProperty("user.home", ".") + File.separator 1259 + "Sectioning-Test" + File.separator + (sDateFormat.format(new Date()))); 1260 } 1261 1262 if (args.length >= 4 && "online".equals(args[3])) { 1263 onlineSectioning(cfg); 1264 } else if (args.length >= 4 && "simple".equals(args[3])) { 1265 cfg.setProperty("Sectioning.UseOnlinePenalties", "false"); 1266 onlineSectioning(cfg); 1267 } else { 1268 batchSectioning(cfg); 1269 } 1270 } catch (Exception e) { 1271 sLog.error(e.getMessage(), e); 1272 e.printStackTrace(); 1273 } 1274 } 1275 1276 public static class ExtraStudentFilter implements StudentFilter { 1277 HashSet<Long> iIds = new HashSet<Long>(); 1278 1279 public ExtraStudentFilter(StudentSectioningModel model) { 1280 for (Student student : model.getStudents()) { 1281 iIds.add(new Long(student.getId())); 1282 } 1283 } 1284 1285 @Override 1286 public boolean accept(Student student) { 1287 return !iIds.contains(new Long(student.getId())); 1288 } 1289 } 1290 1291 public static class TestSolutionListener implements SolutionListener<Request, Enrollment> { 1292 @Override 1293 public void solutionUpdated(Solution<Request, Enrollment> solution) { 1294 StudentSectioningModel m = (StudentSectioningModel) solution.getModel(); 1295 if (m.getTimeOverlaps() != null && TimeOverlapsCounter.sDebug) 1296 m.getTimeOverlaps().checkTotalNrConflicts(); 1297 if (m.getDistanceConflict() != null && DistanceConflict.sDebug) 1298 m.getDistanceConflict().checkAllConflicts(); 1299 } 1300 1301 @Override 1302 public void getInfo(Solution<Request, Enrollment> solution, Map<String, String> info) { 1303 } 1304 1305 @Override 1306 public void getInfo(Solution<Request, Enrollment> solution, Map<String, String> info, Collection<Request> variables) { 1307 } 1308 1309 @Override 1310 public void bestCleared(Solution<Request, Enrollment> solution) { 1311 } 1312 1313 @Override 1314 public void bestSaved(Solution<Request, Enrollment> solution) { 1315 sLog.debug("**BEST** " + solution.getModel().toString() + ", TM:" + sDF.format(solution.getTime() / 3600.0) + "h"); 1316 } 1317 1318 @Override 1319 public void bestRestored(Solution<Request, Enrollment> solution) { 1320 } 1321 } 1322}