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