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