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