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 File outDir = new File(model.getProperties().getProperty("General.Output", ".")); 535 outDir.mkdirs(); 536 CourseConflictTable cct = new CourseConflictTable((StudentSectioningModel) solution.getModel()); 537 cct.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "conflicts-lastlike.csv")); 538 cct.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "conflicts-real.csv")); 539 540 DistanceConflictTable dct = new DistanceConflictTable((StudentSectioningModel) solution.getModel()); 541 dct.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "distances-lastlike.csv")); 542 dct.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "distances-real.csv")); 543 544 SectionConflictTable sct = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.OVERLAPS); 545 sct.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "time-conflicts-lastlike.csv")); 546 sct.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "time-conflicts-real.csv")); 547 548 SectionConflictTable ust = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.UNAVAILABILITIES); 549 ust.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "availability-conflicts-lastlike.csv")); 550 ust.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "availability-conflicts-real.csv")); 551 552 SectionConflictTable ct = new SectionConflictTable((StudentSectioningModel) solution.getModel(), SectionConflictTable.Type.OVERLAPS_AND_UNAVAILABILITIES); 553 ct.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "section-conflicts-lastlike.csv")); 554 ct.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "section-conflicts-real.csv")); 555 556 UnbalancedSectionsTable ubt = new UnbalancedSectionsTable((StudentSectioningModel) solution.getModel()); 557 ubt.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "unbalanced-lastlike.csv")); 558 ubt.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "unbalanced-real.csv")); 559 560 TimeOverlapConflictTable toc = new TimeOverlapConflictTable((StudentSectioningModel) solution.getModel()); 561 toc.createTable(solution.getAssignment(), true, false, true).save(new File(outDir, "time-overlaps-lastlike.csv")); 562 toc.createTable(solution.getAssignment(), false, true, true).save(new File(outDir, "time-overlaps-real.csv")); 563 564 RequestGroupTable rqt = new RequestGroupTable((StudentSectioningModel) solution.getModel()); 565 rqt.create(solution.getAssignment(), model.getProperties()).save(new File(outDir, "request-groups.csv")); 566 567 RequestPriorityTable rpt = new RequestPriorityTable((StudentSectioningModel) solution.getModel()); 568 rpt.create(solution.getAssignment(), model.getProperties()).save(new File(outDir, "request-priorities.csv")); 569 570 TableauReport tr = new TableauReport((StudentSectioningModel) solution.getModel()); 571 tr.create(solution.getAssignment(), model.getProperties()).save(new File(outDir, "tableau.csv")); 572 } catch (IOException e) { 573 sLog.error(e.getMessage(), e); 574 } 575 } 576 577 solution.saveBest(); 578 } 579 580 if (computeSectInfos) 581 model.computeOnlineSectioningInfos(solution.getAssignment()); 582 583 if (runChecks) { 584 try { 585 if (model.getProperties().getPropertyBoolean("Test.InevitableStudentConflictsCheck", false)) { 586 InevitableStudentConflicts ch = new InevitableStudentConflicts(model); 587 if (!ch.check(solution.getAssignment())) 588 ch.getCSVFile().save( 589 new File(new File(model.getProperties().getProperty("General.Output", ".")), 590 "inevitable-conflicts.csv")); 591 } 592 } catch (IOException e) { 593 sLog.error(e.getMessage(), e); 594 } 595 new OverlapCheck(model).check(solution.getAssignment()); 596 new SectionLimitCheck(model).check(solution.getAssignment()); 597 try { 598 CourseLimitCheck ch = new CourseLimitCheck(model); 599 if (!ch.check()) 600 ch.getCSVFile().save( 601 new File(new File(model.getProperties().getProperty("General.Output", ".")), 602 "course-limits.csv")); 603 } catch (IOException e) { 604 sLog.error(e.getMessage(), e); 605 } 606 } 607 608 sLog.info("Best solution found after " + solution.getBestTime() + " seconds (" + solution.getBestIteration() 609 + " iterations)."); 610 sLog.info("Info: " + ToolBox.dict2string(solution.getExtendedInfo(), 2)); 611 } 612 613 /** Solve the student sectioning problem using IFS solver 614 * @param solution current solution 615 * @param cfg solver configuration 616 * @return resultant solution 617 **/ 618 public static Solution<Request, Enrollment> solve(Solution<Request, Enrollment> solution, DataProperties cfg) { 619 int nrSolvers = cfg.getPropertyInt("Parallel.NrSolvers", 1); 620 Solver<Request, Enrollment> solver = (nrSolvers == 1 ? new Solver<Request, Enrollment>(cfg) : new ParallelSolver<Request, Enrollment>(cfg)); 621 solver.setInitalSolution(solution); 622 if (cfg.getPropertyBoolean("Test.Verbose", false)) { 623 solver.addSolverListener(new SolverListener<Request, Enrollment>() { 624 @Override 625 public boolean variableSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable) { 626 return true; 627 } 628 629 @Override 630 public boolean valueSelected(Assignment<Request, Enrollment> assignment, long iteration, Request variable, Enrollment value) { 631 return true; 632 } 633 634 @Override 635 public boolean neighbourSelected(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) { 636 sLog.debug("Select[" + iteration + "]: " + neighbour); 637 return true; 638 } 639 640 @Override 641 public void neighbourFailed(Assignment<Request, Enrollment> assignment, long iteration, Neighbour<Request, Enrollment> neighbour) { 642 sLog.debug("Failed[" + iteration + "]: " + neighbour); 643 } 644 }); 645 } 646 solution.addSolutionListener(new TestSolutionListener()); 647 648 Runtime.getRuntime().addShutdownHook(new ShutdownHook(solver)); 649 650 solver.start(); 651 try { 652 solver.getSolverThread().join(); 653 } catch (InterruptedException e) { 654 } 655 656 return solution; 657 } 658 659 /** 660 * Compute last-like student weight for the given course 661 * 662 * @param course 663 * given course 664 * @param real 665 * number of real students for the course 666 * @param lastLike 667 * number of last-like students for the course 668 * @return weight of a student request for the given course 669 */ 670 public static double getLastLikeStudentWeight(Course course, int real, int lastLike) { 671 int projected = course.getProjected(); 672 int limit = course.getLimit(); 673 if (course.getLimit() < 0) { 674 sLog.debug(" -- Course " + course.getName() + " is unlimited."); 675 return 1.0; 676 } 677 if (projected <= 0) { 678 sLog.warn(" -- No projected demand for course " + course.getName() + ", using course limit (" + limit 679 + ")"); 680 projected = limit; 681 } else if (limit < projected) { 682 sLog.warn(" -- Projected number of students is over course limit for course " + course.getName() + " (" 683 + Math.round(projected) + ">" + limit + ")"); 684 projected = limit; 685 } 686 if (lastLike == 0) { 687 sLog.warn(" -- No last like info for course " + course.getName()); 688 return 1.0; 689 } 690 double weight = ((double) Math.max(0, projected - real)) / lastLike; 691 sLog.debug(" -- last like student weight for " + course.getName() + " is " + weight + " (lastLike=" + lastLike 692 + ", real=" + real + ", projected=" + projected + ")"); 693 return weight; 694 } 695 696 /** 697 * Load last-like students from an XML file (the one that is used to load 698 * last like course demands table in the timetabling application) 699 * @param model problem model 700 * @param xml an XML file 701 */ 702 public static void loadLastLikeCourseDemandsXml(StudentSectioningModel model, File xml) { 703 try { 704 Document document = (new SAXReader()).read(xml); 705 Element root = document.getRootElement(); 706 HashMap<Course, List<Request>> requests = new HashMap<Course, List<Request>>(); 707 long reqId = 0; 708 for (Iterator<?> i = root.elementIterator("student"); i.hasNext();) { 709 Element studentEl = (Element) i.next(); 710 Student student = new Student(Long.parseLong(studentEl.attributeValue("externalId"))); 711 student.setDummy(true); 712 int priority = 0; 713 HashSet<Course> reqCourses = new HashSet<Course>(); 714 for (Iterator<?> j = studentEl.elementIterator("studentCourse"); j.hasNext();) { 715 Element courseEl = (Element) j.next(); 716 String subjectArea = courseEl.attributeValue("subject"); 717 String courseNbr = courseEl.attributeValue("courseNumber"); 718 Course course = null; 719 offerings: for (Offering offering : model.getOfferings()) { 720 for (Course c : offering.getCourses()) { 721 if (c.getSubjectArea().equals(subjectArea) && c.getCourseNumber().equals(courseNbr)) { 722 course = c; 723 break offerings; 724 } 725 } 726 } 727 if (course == null && courseNbr.charAt(courseNbr.length() - 1) >= 'A' 728 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 729 String courseNbrNoSfx = courseNbr.substring(0, courseNbr.length() - 1); 730 offerings: for (Offering offering : model.getOfferings()) { 731 for (Course c : offering.getCourses()) { 732 if (c.getSubjectArea().equals(subjectArea) 733 && c.getCourseNumber().equals(courseNbrNoSfx)) { 734 course = c; 735 break offerings; 736 } 737 } 738 } 739 } 740 if (course == null) { 741 sLog.warn("Course " + subjectArea + " " + courseNbr + " not found."); 742 } else { 743 if (!reqCourses.add(course)) { 744 sLog.warn("Course " + subjectArea + " " + courseNbr + " already requested."); 745 } else { 746 List<Course> courses = new ArrayList<Course>(1); 747 courses.add(course); 748 CourseRequest request = new CourseRequest(reqId++, priority++, false, student, courses, false, null); 749 List<Request> requestsThisCourse = requests.get(course); 750 if (requestsThisCourse == null) { 751 requestsThisCourse = new ArrayList<Request>(); 752 requests.put(course, requestsThisCourse); 753 } 754 requestsThisCourse.add(request); 755 } 756 } 757 } 758 if (!student.getRequests().isEmpty()) 759 model.addStudent(student); 760 } 761 for (Map.Entry<Course, List<Request>> entry : requests.entrySet()) { 762 Course course = entry.getKey(); 763 List<Request> requestsThisCourse = entry.getValue(); 764 double weight = getLastLikeStudentWeight(course, 0, requestsThisCourse.size()); 765 for (Request request : requestsThisCourse) { 766 request.setWeight(weight); 767 } 768 } 769 } catch (Exception e) { 770 sLog.error(e.getMessage(), e); 771 } 772 } 773 774 /** 775 * Load course request from the given files (in the format being used by the 776 * old MSF system) 777 * 778 * @param model 779 * student sectioning model (with offerings loaded) 780 * @param files 781 * semi-colon separated list of files to be loaded 782 */ 783 public static void loadCrsReqFiles(StudentSectioningModel model, String files) { 784 try { 785 boolean lastLike = model.getProperties().getPropertyBoolean("Test.CrsReqIsLastLike", true); 786 boolean shuffleIds = model.getProperties().getPropertyBoolean("Test.CrsReqShuffleStudentIds", true); 787 boolean tryWithoutSuffix = model.getProperties().getPropertyBoolean("Test.CrsReqTryWithoutSuffix", false); 788 HashMap<Long, Student> students = new HashMap<Long, Student>(); 789 long reqId = 0; 790 for (StringTokenizer stk = new StringTokenizer(files, ";"); stk.hasMoreTokens();) { 791 String file = stk.nextToken(); 792 sLog.debug("Loading " + file + " ..."); 793 BufferedReader in = new BufferedReader(new FileReader(file)); 794 String line; 795 int lineIndex = 0; 796 while ((line = in.readLine()) != null) { 797 lineIndex++; 798 if (line.length() <= 150) 799 continue; 800 char code = line.charAt(13); 801 if (code == 'H' || code == 'T') 802 continue; // skip header and tail 803 long studentId = Long.parseLong(line.substring(14, 23)); 804 Student student = students.get(Long.valueOf(studentId)); 805 if (student == null) { 806 student = new Student(studentId); 807 if (lastLike) 808 student.setDummy(true); 809 students.put(Long.valueOf(studentId), student); 810 sLog.debug(" -- loading student " + studentId + " ..."); 811 } else 812 sLog.debug(" -- updating student " + studentId + " ..."); 813 line = line.substring(150); 814 while (line.length() >= 20) { 815 String subjectArea = line.substring(0, 4).trim(); 816 String courseNbr = line.substring(4, 8).trim(); 817 if (subjectArea.length() == 0 || courseNbr.length() == 0) { 818 line = line.substring(20); 819 continue; 820 } 821 /* 822 * // UNUSED String instrSel = line.substring(8,10); 823 * //ZZ - Remove previous instructor selection char 824 * reqPDiv = line.charAt(10); //P - Personal preference; 825 * C - Conflict resolution; //0 - (Zero) used by program 826 * only, for change requests to reschedule division // 827 * (used to reschedule canceled division) String reqDiv 828 * = line.substring(11,13); //00 - Reschedule division 829 * String reqSect = line.substring(13,15); //Contains 830 * designator for designator-required courses String 831 * credit = line.substring(15,19); char nameRaise = 832 * line.charAt(19); //N - Name raise 833 */ 834 char action = line.charAt(19); // A - Add; D - Drop; C - 835 // Change 836 sLog.debug(" -- requesting " + subjectArea + " " + courseNbr + " (action:" + action 837 + ") ..."); 838 Course course = null; 839 offerings: for (Offering offering : model.getOfferings()) { 840 for (Course c : offering.getCourses()) { 841 if (c.getSubjectArea().equals(subjectArea) && c.getCourseNumber().equals(courseNbr)) { 842 course = c; 843 break offerings; 844 } 845 } 846 } 847 if (course == null && tryWithoutSuffix && courseNbr.charAt(courseNbr.length() - 1) >= 'A' 848 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 849 String courseNbrNoSfx = courseNbr.substring(0, courseNbr.length() - 1); 850 offerings: for (Offering offering : model.getOfferings()) { 851 for (Course c : offering.getCourses()) { 852 if (c.getSubjectArea().equals(subjectArea) 853 && c.getCourseNumber().equals(courseNbrNoSfx)) { 854 course = c; 855 break offerings; 856 } 857 } 858 } 859 } 860 if (course == null) { 861 if (courseNbr.charAt(courseNbr.length() - 1) >= 'A' 862 && courseNbr.charAt(courseNbr.length() - 1) <= 'Z') { 863 } else { 864 sLog.warn(" -- course " + subjectArea + " " + courseNbr + " not found (file " 865 + file + ", line " + lineIndex + ")"); 866 } 867 } else { 868 CourseRequest courseRequest = null; 869 for (Request request : student.getRequests()) { 870 if (request instanceof CourseRequest 871 && ((CourseRequest) request).getCourses().contains(course)) { 872 courseRequest = (CourseRequest) request; 873 break; 874 } 875 } 876 if (action == 'A') { 877 if (courseRequest == null) { 878 List<Course> courses = new ArrayList<Course>(1); 879 courses.add(course); 880 courseRequest = new CourseRequest(reqId++, student.getRequests().size(), false, student, courses, false, null); 881 } else { 882 sLog.warn(" -- request for course " + course + " is already present"); 883 } 884 } else if (action == 'D') { 885 if (courseRequest == null) { 886 sLog.warn(" -- request for course " + course 887 + " is not present -- cannot be dropped"); 888 } else { 889 student.getRequests().remove(courseRequest); 890 } 891 } else if (action == 'C') { 892 if (courseRequest == null) { 893 sLog.warn(" -- request for course " + course 894 + " is not present -- cannot be changed"); 895 } else { 896 // ? 897 } 898 } else { 899 sLog.warn(" -- unknown action " + action); 900 } 901 } 902 line = line.substring(20); 903 } 904 } 905 in.close(); 906 } 907 HashMap<Course, List<Request>> requests = new HashMap<Course, List<Request>>(); 908 Set<Long> studentIds = new HashSet<Long>(); 909 for (Student student: students.values()) { 910 if (!student.getRequests().isEmpty()) 911 model.addStudent(student); 912 if (shuffleIds) { 913 long newId = -1; 914 while (true) { 915 newId = 1 + (long) (999999999L * Math.random()); 916 if (studentIds.add(Long.valueOf(newId))) 917 break; 918 } 919 student.setId(newId); 920 } 921 if (student.isDummy()) { 922 for (Request request : student.getRequests()) { 923 if (request instanceof CourseRequest) { 924 Course course = ((CourseRequest) request).getCourses().get(0); 925 List<Request> requestsThisCourse = requests.get(course); 926 if (requestsThisCourse == null) { 927 requestsThisCourse = new ArrayList<Request>(); 928 requests.put(course, requestsThisCourse); 929 } 930 requestsThisCourse.add(request); 931 } 932 } 933 } 934 } 935 Collections.sort(model.getStudents(), new Comparator<Student>() { 936 @Override 937 public int compare(Student o1, Student o2) { 938 return Double.compare(o1.getId(), o2.getId()); 939 } 940 }); 941 for (Map.Entry<Course, List<Request>> entry : requests.entrySet()) { 942 Course course = entry.getKey(); 943 List<Request> requestsThisCourse = entry.getValue(); 944 double weight = getLastLikeStudentWeight(course, 0, requestsThisCourse.size()); 945 for (Request request : requestsThisCourse) { 946 request.setWeight(weight); 947 } 948 } 949 if (model.getProperties().getProperty("Test.EtrChk") != null) { 950 for (StringTokenizer stk = new StringTokenizer(model.getProperties().getProperty("Test.EtrChk"), ";"); stk 951 .hasMoreTokens();) { 952 String file = stk.nextToken(); 953 sLog.debug("Loading " + file + " ..."); 954 BufferedReader in = new BufferedReader(new FileReader(file)); 955 try { 956 String line; 957 while ((line = in.readLine()) != null) { 958 if (line.length() < 55) 959 continue; 960 char code = line.charAt(12); 961 if (code == 'H' || code == 'T') 962 continue; // skip header and tail 963 if (code == 'D' || code == 'K') 964 continue; // skip delete nad cancel 965 long studentId = Long.parseLong(line.substring(2, 11)); 966 Student student = students.get(Long.valueOf(studentId)); 967 if (student == null) { 968 sLog.info(" -- student " + studentId + " not found"); 969 continue; 970 } 971 sLog.info(" -- reading student " + studentId); 972 String area = line.substring(15, 18).trim(); 973 if (area.length() == 0) 974 continue; 975 String clasf = line.substring(18, 20).trim(); 976 String major = line.substring(21, 24).trim(); 977 String minor = line.substring(24, 27).trim(); 978 student.getAreaClassificationMajors().clear(); 979 student.getAreaClassificationMinors().clear(); 980 if (major.length() > 0) 981 student.getAreaClassificationMajors().add(new AreaClassificationMajor(area, clasf, major)); 982 if (minor.length() > 0) 983 student.getAreaClassificationMajors().add(new AreaClassificationMajor(area, clasf, minor)); 984 } 985 } finally { 986 in.close(); 987 } 988 } 989 } 990 int without = 0; 991 for (Student student: students.values()) { 992 if (student.getAreaClassificationMajors().isEmpty()) 993 without++; 994 } 995 fixPriorities(model); 996 sLog.info("Students without academic area: " + without); 997 } catch (Exception e) { 998 sLog.error(e.getMessage(), e); 999 } 1000 } 1001 1002 public static void fixPriorities(StudentSectioningModel model) { 1003 for (Student student : model.getStudents()) { 1004 Collections.sort(student.getRequests(), new Comparator<Request>() { 1005 @Override 1006 public int compare(Request r1, Request r2) { 1007 int cmp = Double.compare(r1.getPriority(), r2.getPriority()); 1008 if (cmp != 0) 1009 return cmp; 1010 return Double.compare(r1.getId(), r2.getId()); 1011 } 1012 }); 1013 int priority = 0; 1014 for (Request request : student.getRequests()) { 1015 if (priority != request.getPriority()) { 1016 sLog.debug("Change priority of " + request + " to " + priority); 1017 request.setPriority(priority); 1018 } 1019 } 1020 } 1021 } 1022 1023 /** Save solution info as XML 1024 * @param solution current solution 1025 * @param extra solution extra info 1026 * @param file file to write 1027 **/ 1028 public static void saveInfoToXML(Solution<Request, Enrollment> solution, Map<String, String> extra, File file) { 1029 FileOutputStream fos = null; 1030 try { 1031 Document document = DocumentHelper.createDocument(); 1032 document.addComment("Solution Info"); 1033 1034 Element root = document.addElement("info"); 1035 TreeSet<Map.Entry<String, String>> entrySet = new TreeSet<Map.Entry<String, String>>( 1036 new Comparator<Map.Entry<String, String>>() { 1037 @Override 1038 public int compare(Map.Entry<String, String> e1, Map.Entry<String, String> e2) { 1039 return e1.getKey().compareTo(e2.getKey()); 1040 } 1041 }); 1042 entrySet.addAll(solution.getExtendedInfo().entrySet()); 1043 if (extra != null) 1044 entrySet.addAll(extra.entrySet()); 1045 for (Map.Entry<String, String> entry : entrySet) { 1046 root.addElement("property").addAttribute("name", entry.getKey()).setText(entry.getValue()); 1047 } 1048 1049 fos = new FileOutputStream(file); 1050 (new XMLWriter(fos, OutputFormat.createPrettyPrint())).write(document); 1051 fos.flush(); 1052 fos.close(); 1053 fos = null; 1054 } catch (Exception e) { 1055 sLog.error("Unable to save info, reason: " + e.getMessage(), e); 1056 } finally { 1057 try { 1058 if (fos != null) 1059 fos.close(); 1060 } catch (IOException e) { 1061 } 1062 } 1063 } 1064 1065 private static void fixWeights(StudentSectioningModel model) { 1066 HashMap<Course, Integer> lastLike = new HashMap<Course, Integer>(); 1067 HashMap<Course, Integer> real = new HashMap<Course, Integer>(); 1068 HashSet<Long> lastLikeIds = new HashSet<Long>(); 1069 HashSet<Long> realIds = new HashSet<Long>(); 1070 for (Student student : model.getStudents()) { 1071 if (student.isDummy()) { 1072 if (!lastLikeIds.add(Long.valueOf(student.getId()))) { 1073 sLog.error("Two last-like student with id " + student.getId()); 1074 } 1075 } else { 1076 if (!realIds.add(Long.valueOf(student.getId()))) { 1077 sLog.error("Two real student with id " + student.getId()); 1078 } 1079 } 1080 for (Request request : student.getRequests()) { 1081 if (request instanceof CourseRequest) { 1082 CourseRequest courseRequest = (CourseRequest) request; 1083 Course course = courseRequest.getCourses().get(0); 1084 Integer cnt = (student.isDummy() ? lastLike : real).get(course); 1085 (student.isDummy() ? lastLike : real).put(course, Integer.valueOf( 1086 (cnt == null ? 0 : cnt.intValue()) + 1)); 1087 } 1088 } 1089 } 1090 for (Student student : new ArrayList<Student>(model.getStudents())) { 1091 if (student.isDummy() && realIds.contains(Long.valueOf(student.getId()))) { 1092 sLog.warn("There is both last-like and real student with id " + student.getId()); 1093 long newId = -1; 1094 while (true) { 1095 newId = 1 + (long) (999999999L * Math.random()); 1096 if (!realIds.contains(Long.valueOf(newId)) && !lastLikeIds.contains(Long.valueOf(newId))) 1097 break; 1098 } 1099 lastLikeIds.remove(Long.valueOf(student.getId())); 1100 lastLikeIds.add(Long.valueOf(newId)); 1101 student.setId(newId); 1102 sLog.warn(" -- last-like student id changed to " + student.getId()); 1103 } 1104 for (Request request : new ArrayList<Request>(student.getRequests())) { 1105 if (!student.isDummy()) { 1106 request.setWeight(1.0); 1107 continue; 1108 } 1109 if (request instanceof CourseRequest) { 1110 CourseRequest courseRequest = (CourseRequest) request; 1111 Course course = courseRequest.getCourses().get(0); 1112 Integer lastLikeCnt = lastLike.get(course); 1113 Integer realCnt = real.get(course); 1114 courseRequest.setWeight(getLastLikeStudentWeight(course, realCnt == null ? 0 : realCnt.intValue(), 1115 lastLikeCnt == null ? 0 : lastLikeCnt.intValue())); 1116 } else 1117 request.setWeight(1.0); 1118 if (request.getWeight() <= 0.0) { 1119 model.removeVariable(request); 1120 student.getRequests().remove(request); 1121 } 1122 } 1123 if (student.getRequests().isEmpty()) { 1124 model.getStudents().remove(student); 1125 } 1126 } 1127 } 1128 1129 /** Combine students from the provided two files 1130 * @param cfg solver configuration 1131 * @param lastLikeStudentData a file containing last-like student data 1132 * @param realStudentData a file containing real student data 1133 * @return combined solution 1134 **/ 1135 public static Solution<Request, Enrollment> combineStudents(DataProperties cfg, File lastLikeStudentData, File realStudentData) { 1136 try { 1137 RandomStudentFilter rnd = new RandomStudentFilter(1.0); 1138 1139 StudentSectioningModel model = null; 1140 Assignment<Request, Enrollment> assignment = new DefaultSingleAssignment<Request, Enrollment>(); 1141 1142 for (StringTokenizer stk = new StringTokenizer(cfg.getProperty("Test.CombineAcceptProb", "1.0"), ","); stk.hasMoreTokens();) { 1143 double acceptProb = Double.parseDouble(stk.nextToken()); 1144 sLog.info("Test.CombineAcceptProb=" + acceptProb); 1145 rnd.setProbability(acceptProb); 1146 1147 StudentFilter batchFilter = new CombinedStudentFilter(new ReverseStudentFilter( 1148 new FreshmanStudentFilter()), rnd, CombinedStudentFilter.OP_AND); 1149 1150 model = new StudentSectioningModel(cfg); 1151 StudentSectioningXMLLoader loader = new StudentSectioningXMLLoader(model, assignment); 1152 loader.setLoadStudents(false); 1153 loader.load(); 1154 1155 StudentSectioningXMLLoader lastLikeLoader = new StudentSectioningXMLLoader(model, assignment); 1156 lastLikeLoader.setInputFile(lastLikeStudentData); 1157 lastLikeLoader.setLoadOfferings(false); 1158 lastLikeLoader.setLoadStudents(true); 1159 lastLikeLoader.load(); 1160 1161 StudentSectioningXMLLoader realLoader = new StudentSectioningXMLLoader(model, assignment); 1162 realLoader.setInputFile(realStudentData); 1163 realLoader.setLoadOfferings(false); 1164 realLoader.setLoadStudents(true); 1165 realLoader.setStudentFilter(batchFilter); 1166 realLoader.load(); 1167 1168 fixWeights(model); 1169 1170 fixPriorities(model); 1171 1172 Solver<Request, Enrollment> solver = new Solver<Request, Enrollment>(model.getProperties()); 1173 solver.setInitalSolution(model); 1174 new StudentSectioningXMLSaver(solver).save(new File(new File(model.getProperties().getProperty( 1175 "General.Output", ".")), "solution-r" + ((int) (100.0 * acceptProb)) + ".xml")); 1176 1177 } 1178 1179 return model == null ? null : new Solution<Request, Enrollment>(model, assignment); 1180 1181 } catch (Exception e) { 1182 sLog.error("Unable to combine students, reason: " + e.getMessage(), e); 1183 return null; 1184 } 1185 } 1186 1187 /** Main 1188 * @param args program arguments 1189 **/ 1190 public static void main(String[] args) { 1191 try { 1192 DataProperties cfg = new DataProperties(); 1193 cfg.setProperty("Termination.Class", "org.cpsolver.ifs.termination.GeneralTerminationCondition"); 1194 cfg.setProperty("Termination.StopWhenComplete", "true"); 1195 cfg.setProperty("Termination.TimeOut", "600"); 1196 cfg.setProperty("Comparator.Class", "org.cpsolver.ifs.solution.GeneralSolutionComparator"); 1197 cfg.setProperty("Value.Class", "org.cpsolver.studentsct.heuristics.EnrollmentSelection");// org.cpsolver.ifs.heuristics.GeneralValueSelection 1198 cfg.setProperty("Value.WeightConflicts", "1.0"); 1199 cfg.setProperty("Value.WeightNrAssignments", "0.0"); 1200 cfg.setProperty("Variable.Class", "org.cpsolver.ifs.heuristics.GeneralVariableSelection"); 1201 cfg.setProperty("Neighbour.Class", "org.cpsolver.studentsct.heuristics.StudentSctNeighbourSelection"); 1202 cfg.setProperty("General.SaveBestUnassigned", "0"); 1203 cfg.setProperty("Extensions.Classes", 1204 "org.cpsolver.ifs.extension.ConflictStatistics;org.cpsolver.studentsct.extension.DistanceConflict" + 1205 ";org.cpsolver.studentsct.extension.TimeOverlapsCounter"); 1206 cfg.setProperty("Data.Initiative", "puWestLafayetteTrdtn"); 1207 cfg.setProperty("Data.Term", "Fal"); 1208 cfg.setProperty("Data.Year", "2007"); 1209 cfg.setProperty("General.Input", "pu-sectll-fal07-s.xml"); 1210 if (args.length >= 1) { 1211 cfg.load(new FileInputStream(args[0])); 1212 } 1213 cfg.putAll(System.getProperties()); 1214 1215 if (args.length >= 2) { 1216 cfg.setProperty("General.Input", args[1]); 1217 } 1218 1219 File outDir = null; 1220 if (args.length >= 3) { 1221 outDir = new File(args[2], sDateFormat.format(new Date())); 1222 } else if (cfg.getProperty("General.Output") != null) { 1223 outDir = new File(cfg.getProperty("General.Output", "."), sDateFormat.format(new Date())); 1224 } else { 1225 outDir = new File(System.getProperty("user.home", ".") + File.separator + "Sectioning-Test" + File.separator + (sDateFormat.format(new Date()))); 1226 } 1227 outDir.mkdirs(); 1228 ToolBox.setupLogging(new File(outDir, "debug.log"), "true".equals(System.getProperty("debug", "false"))); 1229 cfg.setProperty("General.Output", outDir.getAbsolutePath()); 1230 1231 if (args.length >= 4 && "online".equals(args[3])) { 1232 onlineSectioning(cfg); 1233 } else if (args.length >= 4 && "simple".equals(args[3])) { 1234 cfg.setProperty("Sectioning.UseOnlinePenalties", "false"); 1235 onlineSectioning(cfg); 1236 } else { 1237 batchSectioning(cfg); 1238 } 1239 } catch (Exception e) { 1240 sLog.error(e.getMessage(), e); 1241 e.printStackTrace(); 1242 } 1243 } 1244 1245 public static class ExtraStudentFilter implements StudentFilter { 1246 HashSet<Long> iIds = new HashSet<Long>(); 1247 1248 public ExtraStudentFilter(StudentSectioningModel model) { 1249 for (Student student : model.getStudents()) { 1250 iIds.add(Long.valueOf(student.getId())); 1251 } 1252 } 1253 1254 @Override 1255 public boolean accept(Student student) { 1256 return !iIds.contains(Long.valueOf(student.getId())); 1257 } 1258 1259 @Override 1260 public String getName() { 1261 return "Extra"; 1262 } 1263 } 1264 1265 public static class TestSolutionListener implements SolutionListener<Request, Enrollment> { 1266 @Override 1267 public void solutionUpdated(Solution<Request, Enrollment> solution) { 1268 StudentSectioningModel m = (StudentSectioningModel) solution.getModel(); 1269 if (m.getTimeOverlaps() != null && TimeOverlapsCounter.sDebug) 1270 m.getTimeOverlaps().checkTotalNrConflicts(solution.getAssignment()); 1271 if (m.getDistanceConflict() != null && DistanceConflict.sDebug) 1272 m.getDistanceConflict().checkAllConflicts(solution.getAssignment()); 1273 if (m.getStudentQuality() != null && m.getStudentQuality().isDebug()) 1274 m.getStudentQuality().checkTotalPenalty(solution.getAssignment()); 1275 } 1276 1277 @Override 1278 public void getInfo(Solution<Request, Enrollment> solution, Map<String, String> info) { 1279 } 1280 1281 @Override 1282 public void getInfo(Solution<Request, Enrollment> solution, Map<String, String> info, Collection<Request> variables) { 1283 } 1284 1285 @Override 1286 public void bestCleared(Solution<Request, Enrollment> solution) { 1287 } 1288 1289 @Override 1290 public void bestSaved(Solution<Request, Enrollment> solution) { 1291 sLog.info("**BEST** " + ((StudentSectioningModel)solution.getModel()).toString(solution.getAssignment()) + ", TM:" + sDF.format(solution.getTime() / 3600.0) + "h" + 1292 (solution.getFailedIterations() > 0 ? ", F:" + sDF.format(100.0 * solution.getFailedIterations() / solution.getIteration()) + "%" : "")); 1293 } 1294 1295 @Override 1296 public void bestRestored(Solution<Request, Enrollment> solution) { 1297 } 1298 } 1299 1300 private static class ShutdownHook extends Thread { 1301 Solver<Request, Enrollment> iSolver = null; 1302 Map<String, String> iExtra = null; 1303 1304 private ShutdownHook(Solver<Request, Enrollment> solver) { 1305 setName("ShutdownHook"); 1306 iSolver = solver; 1307 } 1308 1309 void setExtra(Map<String, String> extra) { iExtra = extra; } 1310 1311 @Override 1312 public void run() { 1313 try { 1314 if (iSolver.isRunning()) iSolver.stopSolver(); 1315 Solution<Request, Enrollment> solution = iSolver.lastSolution(); 1316 solution.restoreBest(); 1317 DataProperties cfg = iSolver.getProperties(); 1318 1319 printInfo(solution, 1320 cfg.getPropertyBoolean("Test.CreateReports", true), 1321 cfg.getPropertyBoolean("Test.ComputeSectioningInfo", true), 1322 cfg.getPropertyBoolean("Test.RunChecks", true)); 1323 1324 try { 1325 new StudentSectioningXMLSaver(iSolver).save(new File(new File(cfg.getProperty("General.Output", ".")), "solution.xml")); 1326 } catch (Exception e) { 1327 sLog.error("Unable to save solution, reason: " + e.getMessage(), e); 1328 } 1329 1330 saveInfoToXML(solution, iExtra, new File(new File(cfg.getProperty("General.Output", ".")), "info.xml")); 1331 1332 Progress.removeInstance(solution.getModel()); 1333 } catch (Throwable t) { 1334 sLog.error("Test failed.", t); 1335 } 1336 } 1337 } 1338 1339}