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.SectionConflictTable;
061    import net.sf.cpsolver.studentsct.report.CourseConflictTable;
062    import net.sf.cpsolver.studentsct.report.DistanceConflictTable;
063    import net.sf.cpsolver.studentsct.report.TimeOverlapConflictTable;
064    import net.sf.cpsolver.studentsct.report.UnbalancedSectionsTable;
065    
066    import org.apache.log4j.Level;
067    import org.apache.log4j.Logger;
068    import org.dom4j.Document;
069    import org.dom4j.DocumentHelper;
070    import org.dom4j.Element;
071    import org.dom4j.io.OutputFormat;
072    import org.dom4j.io.SAXReader;
073    import 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 Tomas Muller<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    
217    public 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    }