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