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