    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 * &nbsp;&nbsp;batch ... batch sectioning mode (default mode -- IFS solver with
    092 * {@link StudentSctNeighbourSelection} is used)<br>
    093 * &nbsp;&nbsp;online ... online sectioning mode (students are sectioned one by
    094 * one, sectioning info (expected/held space) is used)<br>
    095 * &nbsp;&nbsp;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(new Long(studentId));
    805                    if (student == null) {
    806                        student = new Student(studentId);
    807                        if (lastLike)
    808                            student.setDummy(true);
    809                        students.put(new Long(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(new Long(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(new Long(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(new Long(student.getId()))) {
    1073                    sLog.error("Two last-like student with id " + student.getId());
    1074                }
    1075            } else {
    1076                if (!realIds.add(new Long(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, new Integer(
    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(new Long(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(new Long(newId)) && !lastLikeIds.contains(new Long(newId)))
    1097                        break;
    1098                }
    1099                lastLikeIds.remove(new Long(student.getId()));
    1100                lastLikeIds.add(new Long(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(new Long(student.getId()));
    1251            }
    1252        }
    1253
    1254        @Override
    1255        public boolean accept(Student student) {
    1256            return !iIds.contains(new Long(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}