    001package org.cpsolver.studentsct.online;
    002
    003import java.io.File;
    004import java.io.FileWriter;
    005import java.io.IOException;
    006import java.io.PrintWriter;
    007import java.text.DecimalFormat;
    008import java.util.ArrayList;
    009import java.util.Collections;
    010import java.util.HashMap;
    011import java.util.HashSet;
    012import java.util.Hashtable;
    013import java.util.Iterator;
    014import java.util.List;
    015import java.util.Map;
    016import java.util.NoSuchElementException;
    017import java.util.Set;
    018import java.util.TreeSet;
    019
    020import org.apache.log4j.BasicConfigurator;
    021import org.apache.log4j.Logger;
    022import org.apache.log4j.PropertyConfigurator;
    023import org.cpsolver.ifs.assignment.Assignment;
    024import org.cpsolver.ifs.assignment.AssignmentMap;
    025import org.cpsolver.ifs.assignment.DefaultSingleAssignment;
    026import org.cpsolver.ifs.solver.Solver;
    027import org.cpsolver.ifs.util.DataProperties;
    028import org.cpsolver.ifs.util.DistanceMetric;
    029import org.cpsolver.ifs.util.JProf;
    030import org.cpsolver.ifs.util.ToolBox;
    031import org.cpsolver.studentsct.StudentPreferencePenalties;
    032import org.cpsolver.studentsct.StudentSectioningModel;
    033import org.cpsolver.studentsct.StudentSectioningXMLLoader;
    034import org.cpsolver.studentsct.StudentSectioningXMLSaver;
    035import org.cpsolver.studentsct.constraint.LinkedSections;
    036import org.cpsolver.studentsct.extension.DistanceConflict;
    037import org.cpsolver.studentsct.extension.StudentQuality;
    038import org.cpsolver.studentsct.extension.TimeOverlapsCounter;
    039import org.cpsolver.studentsct.heuristics.selection.BranchBoundSelection.BranchBoundNeighbour;
    040import org.cpsolver.studentsct.heuristics.studentord.StudentChoiceOrder;
    041import org.cpsolver.studentsct.model.Config;
    042import org.cpsolver.studentsct.model.Course;
    043import org.cpsolver.studentsct.model.CourseRequest;
    044import org.cpsolver.studentsct.model.Enrollment;
    045import org.cpsolver.studentsct.model.FreeTimeRequest;
    046import org.cpsolver.studentsct.model.Offering;
    047import org.cpsolver.studentsct.model.Request;
    048import org.cpsolver.studentsct.model.Section;
    049import org.cpsolver.studentsct.model.Student;
    050import org.cpsolver.studentsct.model.Subpart;
    051import org.cpsolver.studentsct.online.expectations.AvoidUnbalancedWhenNoExpectations;
    052import org.cpsolver.studentsct.online.expectations.FractionallyOverExpected;
    053import org.cpsolver.studentsct.online.expectations.FractionallyUnbalancedWhenNoExpectations;
    054import org.cpsolver.studentsct.online.expectations.PercentageOverExpected;
    055import org.cpsolver.studentsct.online.selection.MultiCriteriaBranchAndBoundSelection;
    056import org.cpsolver.studentsct.online.selection.MultiCriteriaBranchAndBoundSuggestions;
    057import org.cpsolver.studentsct.online.selection.OnlineSectioningSelection;
    058import org.cpsolver.studentsct.online.selection.StudentSchedulingAssistantWeights;
    059import org.cpsolver.studentsct.online.selection.SuggestionSelection;
    060import org.cpsolver.studentsct.online.selection.SuggestionsBranchAndBound;
    061import org.cpsolver.studentsct.reservation.CourseReservation;
    062import org.cpsolver.studentsct.reservation.Reservation;
    063
    064/**
    065 * An online student sectioning test. It loads the given problem (passed as the only argument) with no assignments. It sections all
    066 * students in the given order (given by -Dsort parameter, values shuffle, choice, reverse). Multiple threads can be used to section
    067 * students in parallel (given by -DnrConcurrent parameter). If parameter -Dsuggestions is set to true, the test also asks for suggestions
    068 * for each of the assigned class, preferring mid-day times. Over-expected criterion can be defined by the -Doverexp parameter (see the
    069 * examples bellow). Multi-criteria selection can be enabled by -DStudentWeights.MultiCriteria=true and equal weighting can be set by
    070 * -DStudentWeights.PriorityWeighting=equal).
    071 *
    072 * <br><br>
    073 * Usage:<ul>
    074 *      java -Xmx1g -cp studentsct-1.3.jar [parameters] org.cpsolver.studentsct.online.Test data/pu-sect-fal07.xml<br>
    075 * </ul>
    076 * Parameters:<ul>
    077 *      <li>-Dsort=shuffle|choice|reverse ... for taking students in random order, more choices first, or more choices last (defaults to shuffle)
    078 *      <li>-DnrConcurrent=N ... for the number of threads (concurrent computations of student schedules, defaults to 10)
    079 *      <li>-Dsuggestions=true|false ... true to use suggestions (to simulate students preferring mid-day, defaults to false)
    080 *      <li>-Doverexp=<i>x<sub>over</sub></i>|b<i>x<sub>over</sub></i>-<i>x<sub>disb</sub></i>%|<i>x<sub>over</sub></i>-<i>x<sub>max</sub></i>|b<i>x<sub>over</sub></i>-<i>x<sub>max</sub></i>-<i>x<sub>disb</sub></i>% for over-expected criterion, examples:<ul>
    081 *              <li>1.1 ... {@link PercentageOverExpected} with OverExpected.Percentage set to 1.1 (<i>x<sub>over</sub></i>)
    082 *              <li>b1-10 ... {@link AvoidUnbalancedWhenNoExpectations} with OverExpected.Percentage set to 1 and General.BalanceUnlimited set to 10/100 (<i>x<sub>disb</sub></i>%)
    083 *              <li>0.85-5 ... {@link FractionallyOverExpected} with OverExpected.Percentage set to 0.85 and OverExpected.Maximum set to 5 (<i>x<sub>max</sub></i>)
    084 *              <li>1.1-5-1 ... {@link FractionallyUnbalancedWhenNoExpectations} with OverExpected.Percentage set to 1.1, General.BalanceUnlimited set to 5/100, and OverExpected.Maximum set to 1
    085 *      </ul>
    086 *      <li>-DStudentWeights.PriorityWeighting=priority|equal ... priority or equal weighting (defaults to priority)
    087 *      <li>-DStudentWeights.MultiCriteria=true|false ... true for multi-criteria (lexicographic ordering), false for a weighted sum (default to true)
    088 *      <li>-DNeighbour.BranchAndBoundTimeout=M ... time limit for each student in milliseconds (CPU time, defaults to 1000)
    089 * </ul>
    090 *
    091 * @version StudentSct 1.3 (Student Sectioning)<br>
    092 *          Copyright (C) 2014 Tomáš Müller<br>
    093 *          <a href="mailto:muller@unitime.org">muller@unitime.org</a><br>
    094 *          <a href="http://muller.unitime.org">http://muller.unitime.org</a><br>
    095 * <br>
    096 *          This library is free software; you can redistribute it and/or modify
    097 *          it under the terms of the GNU Lesser General Public License as
    098 *          published by the Free Software Foundation; either version 3 of the
    099 *          License, or (at your option) any later version. <br>
    100 * <br>
    101 *          This library is distributed in the hope that it will be useful, but
    102 *          WITHOUT ANY WARRANTY; without even the implied warranty of
    103 *          MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
    104 *          Lesser General Public License for more details. <br>
    105 * <br>
    106 *          You should have received a copy of the GNU Lesser General Public
    107 *          License along with this library; if not see <a href='http://www.gnu.org/licenses'>http://www.gnu.org/licenses</a>.
    108 *
    109 */
    110public class Test {
    111    public static DecimalFormat sDF = new DecimalFormat("0.00000");
    112    public static Logger sLog = Logger.getLogger(Test.class);
    113
    114    private OnlineSectioningModel iModel;
    115    private Assignment<Request, Enrollment> iAssignment;
    116    private boolean iSuggestions = false;
    117
    118    private Map<String, Counter> iCounters = new HashMap<String, Counter>();
    119
    120    public Test(DataProperties config) {
    121        iModel = new TestModel(config);
    122        iModel.setDistanceConflict(new DistanceConflict(new DistanceMetric(iModel.getProperties()), iModel.getProperties()));
    123        iModel.getDistanceConflict().register(iModel);
    124        iModel.getDistanceConflict().setAssignmentContextReference(iModel.createReference(iModel.getDistanceConflict()));
    125        iModel.setTimeOverlaps(new TimeOverlapsCounter(null, iModel.getProperties()));
    126        iModel.getTimeOverlaps().register(iModel);
    127        iModel.getTimeOverlaps().setAssignmentContextReference(iModel.createReference(iModel.getTimeOverlaps()));
    128        iModel.setStudentQuality(new StudentQuality(new DistanceMetric(iModel.getProperties()), iModel.getProperties()));
    129        iModel.getStudentQuality().register(iModel);
    130        iModel.getStudentQuality().setAssignmentContextReference(iModel.createReference(iModel.getStudentQuality()));
    131        iModel.setStudentWeights(new StudentSchedulingAssistantWeights(iModel.getProperties()));
    132        iAssignment = new DefaultSingleAssignment<Request, Enrollment>();
    133        iSuggestions = "true".equals(System.getProperty("suggestions", iSuggestions ? "true" : "false"));
    134
    135        String overexp = System.getProperty("overexp");
    136        if (overexp != null) {
    137            boolean bal = false;
    138            if (overexp.startsWith("b")) {
    139                bal = true;
    140                overexp = overexp.substring(1);
    141            }
    142            String[] x = overexp.split("[/\\-]");
    143            if (x.length == 1) {
    144                iModel.setOverExpectedCriterion(new PercentageOverExpected(Double.valueOf(x[0])));
    145            } else if (x.length == 2) {
    146                iModel.setOverExpectedCriterion(bal ? new AvoidUnbalancedWhenNoExpectations(Double.valueOf(x[0]), Double.valueOf(x[1]) / 100.0) :
    147                    new FractionallyOverExpected(Double.valueOf(x[0]), Double.valueOf(x[1])));
    148            } else {
    149                iModel.setOverExpectedCriterion(new FractionallyUnbalancedWhenNoExpectations(Double.valueOf(x[0]),
    150                        Double.valueOf(x[1]), Double.valueOf(x[2]) / 100.0));
    151            }
    152        }
    153
    154        sLog.info("Using " + (config.getPropertyBoolean("StudentWeights.MultiCriteria", true) ? "multi-criteria " : "")
    155                + (config.getPropertyBoolean("StudentWeights.PriorityWeighting", true) ? "priority" : "equal")
    156                + " weighting model" + " with over-expected " + iModel.getOverExpectedCriterion()
    157                + (iSuggestions ? ", suggestions" : "") + ", " + System.getProperty("sort", "shuffle") + " order"
    158                + " and " + config.getPropertyInt("Neighbour.BranchAndBoundTimeout", 1000) + " ms time limit.");
    159    }
    160
    161    public OnlineSectioningModel model() {
    162        return iModel;
    163    }
    164
    165    public Assignment<Request, Enrollment> assignment() {
    166        return iAssignment;
    167    }
    168
    169    public void inc(String name, double value) {
    170        synchronized (iCounters) {
    171            Counter c = iCounters.get(name);
    172            if (c == null) {
    173                c = new Counter();
    174                iCounters.put(name, c);
    175            }
    176            c.inc(value);
    177        }
    178    }
    179
    180    public void inc(String name) {
    181        inc(name, 1.0);
    182    }
    183
    184    public Counter get(String name) {
    185        synchronized (iCounters) {
    186            Counter c = iCounters.get(name);
    187            if (c == null) {
    188                c = new Counter();
    189                iCounters.put(name, c);
    190            }
    191            return c;
    192        }
    193    }
    194
    195    public double getPercDisbalancedSections(Assignment<Request, Enrollment> assignment, double perc) {
    196        boolean balanceUnlimited = model().getProperties().getPropertyBoolean("General.BalanceUnlimited", false);
    197        double disb10Sections = 0, nrSections = 0;
    198        for (Offering offering : model().getOfferings()) {
    199            for (Config config : offering.getConfigs()) {
    200                double enrl = config.getEnrollmentTotalWeight(assignment, null);
    201                for (Subpart subpart : config.getSubparts()) {
    202                    if (subpart.getSections().size() <= 1)
    203                        continue;
    204                    nrSections += subpart.getSections().size();
    205                    if (subpart.getLimit() > 0) {
    206                        // sections have limits -> desired size is section limit
    207                        // x (total enrollment / total limit)
    208                        double ratio = enrl / subpart.getLimit();
    209                        for (Section section : subpart.getSections()) {
    210                            double desired = ratio * section.getLimit();
    211                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, perc * section.getLimit()))
    212                                disb10Sections++;
    213                        }
    214                    } else if (balanceUnlimited) {
    215                        // unlimited sections -> desired size is total
    216                        // enrollment / number of sections
    217                        for (Section section : subpart.getSections()) {
    218                            double desired = enrl / subpart.getSections().size();
    219                            if (Math.abs(desired - section.getEnrollmentTotalWeight(assignment, null)) >= Math.max(1.0, perc * desired))
    220                                disb10Sections++;
    221                        }
    222                    }
    223                }
    224            }
    225        }
    226        return 100.0 * disb10Sections / nrSections;
    227    }
    228
    229    protected Course clone(Course course, long studentId, Student originalStudent, Map<Long, Section> classTable, StudentSectioningModel model) {
    230        Offering clonedOffering = new Offering(course.getOffering().getId(), course.getOffering().getName());
    231        clonedOffering.setModel(model);
    232        int courseLimit = course.getLimit();
    233        if (courseLimit >= 0) {
    234            courseLimit -= course.getEnrollments(assignment()).size();
    235            if (courseLimit < 0)
    236                courseLimit = 0;
    237            for (Iterator<Enrollment> i = course.getEnrollments(assignment()).iterator(); i.hasNext();) {
    238                Enrollment enrollment = i.next();
    239                if (enrollment.getStudent().getId() == studentId) {
    240                    courseLimit++;
    241                    break;
    242                }
    243            }
    244        }
    245        Course clonedCourse = new Course(course.getId(), course.getSubjectArea(), course.getCourseNumber(),
    246                clonedOffering, courseLimit, course.getProjected());
    247        clonedCourse.setNote(course.getNote());
    248        Hashtable<Config, Config> configs = new Hashtable<Config, Config>();
    249        Hashtable<Subpart, Subpart> subparts = new Hashtable<Subpart, Subpart>();
    250        Hashtable<Section, Section> sections = new Hashtable<Section, Section>();
    251        for (Iterator<Config> e = course.getOffering().getConfigs().iterator(); e.hasNext();) {
    252            Config config = e.next();
    253            int configLimit = config.getLimit();
    254            int configEnrollment = config.getEnrollments(assignment()).size();
    255            if (configLimit >= 0) {
    256                configLimit -= config.getEnrollments(assignment()).size();
    257                if (configLimit < 0)
    258                    configLimit = 0;
    259                for (Iterator<Enrollment> i = config.getEnrollments(assignment()).iterator(); i.hasNext();) {
    260                    Enrollment enrollment = i.next();
    261                    if (enrollment.getStudent().getId() == studentId) {
    262                        configLimit++;
    263                        configEnrollment--;
    264                        break;
    265                    }
    266                }
    267            }
    268            OnlineConfig clonedConfig = new OnlineConfig(config.getId(), configLimit, config.getName(), clonedOffering);
    269            clonedConfig.setInstructionalMethodId(config.getInstructionalMethodId());
    270            clonedConfig.setInstructionalMethodName(config.getInstructionalMethodName());
    271            clonedConfig.setInstructionalMethodReference(config.getInstructionalMethodReference());
    272            clonedConfig.setEnrollment(configEnrollment);
    273            configs.put(config, clonedConfig);
    274            for (Iterator<Subpart> f = config.getSubparts().iterator(); f.hasNext();) {
    275                Subpart subpart = f.next();
    276                Subpart clonedSubpart = new Subpart(subpart.getId(), subpart.getInstructionalType(), subpart.getName(),
    277                        clonedConfig, (subpart.getParent() == null ? null : subparts.get(subpart.getParent())));
    278                clonedSubpart.setAllowOverlap(subpart.isAllowOverlap());
    279                clonedSubpart.setCredit(subpart.getCredit());
    280                subparts.put(subpart, clonedSubpart);
    281                for (Iterator<Section> g = subpart.getSections().iterator(); g.hasNext();) {
    282                    Section section = g.next();
    283                    int limit = section.getLimit();
    284                    int enrl = section.getEnrollments(assignment()).size();
    285                    if (limit >= 0) {
    286                        // limited section, deduct enrollments
    287                        limit -= section.getEnrollments(assignment()).size();
    288                        if (limit < 0)
    289                            limit = 0; // over-enrolled, but not unlimited
    290                        if (studentId >= 0)
    291                            for (Enrollment enrollment : section.getEnrollments(assignment()))
    292                                if (enrollment.getStudent().getId() == studentId) {
    293                                    limit++;
    294                                    enrl--;
    295                                    break;
    296                                }
    297                    }
    298                    OnlineSection clonedSection = new OnlineSection(section.getId(), limit,
    299                            section.getName(course .getId()), clonedSubpart, section.getPlacement(), section.getInstructors(), (section.getParent() == null ? null : sections.get(section.getParent())));
    300                    clonedSection.setName(-1l, section.getName(-1l));
    301                    clonedSection.setNote(section.getNote());
    302                    clonedSection.setSpaceExpected(section.getSpaceExpected());
    303                    clonedSection.setSpaceHeld(section.getSpaceHeld());
    304                    clonedSection.setEnrollment(enrl);
    305                    clonedSection.setCancelled(section.isCancelled());
    306                    clonedSection.setEnabled(section.isEnabled());
    307                    clonedSection.setOnline(section.isOnline());
    308                    if (section.getIgnoreConflictWithSectionIds() != null)
    309                        for (Long id : section.getIgnoreConflictWithSectionIds())
    310                            clonedSection.addIgnoreConflictWith(id);
    311                    if (limit > 0) {
    312                        double available = Math.round(section.getSpaceExpected() - limit);
    313                        clonedSection.setPenalty(available / section.getLimit());
    314                    }
    315                    sections.put(section, clonedSection);
    316                    classTable.put(section.getId(), clonedSection);
    317                }
    318            }
    319        }
    320        if (course.getOffering().hasReservations()) {
    321            for (Reservation reservation : course.getOffering().getReservations()) {
    322                int reservationLimit = (int) Math.round(reservation.getLimit());
    323                if (reservationLimit >= 0) {
    324                    reservationLimit -= reservation.getEnrollments(assignment()).size();
    325                    if (reservationLimit < 0)
    326                        reservationLimit = 0;
    327                    for (Iterator<Enrollment> i = reservation.getEnrollments(assignment()).iterator(); i.hasNext();) {
    328                        Enrollment enrollment = i.next();
    329                        if (enrollment.getStudent().getId() == studentId) {
    330                            reservationLimit++;
    331                            break;
    332                        }
    333                    }
    334                    if (reservationLimit <= 0 && !reservation.mustBeUsed())
    335                        continue;
    336                }
    337                boolean applicable = originalStudent != null && reservation.isApplicable(originalStudent);
    338                if (reservation instanceof CourseReservation)
    339                    applicable = (course.getId() == ((CourseReservation) reservation).getCourse().getId());
    340                if (reservation instanceof org.cpsolver.studentsct.reservation.DummyReservation) {
    341                    // Ignore by reservation only flag (dummy reservation) when
    342                    // the student is already enrolled in the course
    343                    for (Enrollment enrollment : course.getEnrollments(assignment()))
    344                        if (enrollment.getStudent().getId() == studentId) {
    345                            applicable = true;
    346                            break;
    347                        }
    348                }
    349                Reservation clonedReservation = new OnlineReservation(0, reservation.getId(), clonedOffering,
    350                        reservation.getPriority(), reservation.canAssignOverLimit(), reservationLimit, applicable,
    351                        reservation.mustBeUsed(), reservation.isAllowOverlap(), reservation.isExpired());
    352                for (Config config : reservation.getConfigs())
    353                    clonedReservation.addConfig(configs.get(config));
    354                for (Map.Entry<Subpart, Set<Section>> entry : reservation.getSections().entrySet()) {
    355                    Set<Section> clonedSections = new HashSet<Section>();
    356                    for (Section section : entry.getValue())
    357                        clonedSections.add(sections.get(section));
    358                    clonedReservation.getSections().put(subparts.get(entry.getKey()), clonedSections);
    359                }
    360            }
    361        }
    362        return clonedCourse;
    363    }
    364
    365    protected Request addRequest(Student student, Student original, Request request, Map<Long, Section> classTable,
    366            StudentSectioningModel model) {
    367        if (request instanceof FreeTimeRequest) {
    368            return new FreeTimeRequest(student.getRequests().size() + 1, student.getRequests().size(),
    369                    request.isAlternative(), student, ((FreeTimeRequest) request).getTime());
    370        } else if (request instanceof CourseRequest) {
    371            List<Course> courses = new ArrayList<Course>();
    372            for (Course course : ((CourseRequest) request).getCourses())
    373                courses.add(clone(course, student.getId(), original, classTable, model));
    374            CourseRequest clonnedRequest = new CourseRequest(student.getRequests().size() + 1, student.getRequests().size(),
    375                    request.isAlternative(), student, courses, ((CourseRequest) request).isWaitlist(), request.getRequestPriority(), null);
    376            for (Request originalRequest : original.getRequests()) {
    377                Enrollment originalEnrollment = assignment().getValue(originalRequest);
    378                for (Course clonnedCourse : clonnedRequest.getCourses()) {
    379                    if (!clonnedCourse.getOffering().hasReservations())
    380                        continue;
    381                    if (originalEnrollment != null && clonnedCourse.equals(originalEnrollment.getCourse())) {
    382                        boolean needReservation = clonnedCourse.getOffering().getUnreservedSpace(assignment(), clonnedRequest) < 1.0;
    383                        if (!needReservation) {
    384                            boolean configChecked = false;
    385                            for (Section originalSection : originalEnrollment.getSections()) {
    386                                Section clonnedSection = classTable.get(originalSection.getId());
    387                                if (clonnedSection.getUnreservedSpace(assignment(), clonnedRequest) < 1.0) {
    388                                    needReservation = true;
    389                                    break;
    390                                }
    391                                if (!configChecked
    392                                        && clonnedSection.getSubpart().getConfig()
    393                                                .getUnreservedSpace(assignment(), clonnedRequest) < 1.0) {
    394                                    needReservation = true;
    395                                    break;
    396                                }
    397                                configChecked = true;
    398                            }
    399                        }
    400                        if (needReservation) {
    401                            Reservation reservation = new OnlineReservation(0, -original.getId(),
    402                                    clonnedCourse.getOffering(), 5, false, 1, true, false, false, true);
    403                            for (Section originalSection : originalEnrollment.getSections())
    404                                reservation.addSection(classTable.get(originalSection.getId()));
    405                        }
    406                        break;
    407                    }
    408                }
    409            }
    410            return clonnedRequest;
    411        } else {
    412            return null;
    413        }
    414    }
    415
    416    public boolean section(Student original) {
    417        OnlineSectioningModel model = new TestModel(iModel.getProperties());
    418        model.setOverExpectedCriterion(iModel.getOverExpectedCriterion());
    419        Student student = new Student(original.getId());
    420        Hashtable<CourseRequest, Set<Section>> preferredSectionsForCourse = new Hashtable<CourseRequest, Set<Section>>();
    421        Map<Long, Section> classTable = new HashMap<Long, Section>();
    422
    423        synchronized (iModel) {
    424            for (Request request : original.getRequests()) {
    425                Request clonnedRequest = addRequest(student, original, request, classTable, model);
    426                Enrollment enrollment = assignment().getValue(request);
    427                if (enrollment != null && enrollment.isCourseRequest()) {
    428                    Set<Section> sections = new HashSet<Section>();
    429                    for (Section section : enrollment.getSections())
    430                        sections.add(classTable.get(section.getId()));
    431                    preferredSectionsForCourse.put((CourseRequest) clonnedRequest, sections);
    432                }
    433            }
    434        }
    435
    436        model.addStudent(student);
    437        model.setDistanceConflict(new DistanceConflict(iModel.getDistanceConflict().getDistanceMetric(), model.getProperties()));
    438        model.setTimeOverlaps(new TimeOverlapsCounter(null, model.getProperties()));
    439        for (LinkedSections link : iModel.getLinkedSections()) {
    440            List<Section> sections = new ArrayList<Section>();
    441            for (Offering offering : link.getOfferings())
    442                for (Subpart subpart : link.getSubparts(offering))
    443                    for (Section section : link.getSections(subpart)) {
    444                        Section x = classTable.get(section.getId());
    445                        if (x != null)
    446                            sections.add(x);
    447                    }
    448            if (sections.size() >= 2)
    449                model.addLinkedSections(link.isMustBeUsed(), sections);
    450        }
    451        OnlineSectioningSelection selection = null;
    452        if (model.getProperties().getPropertyBoolean("StudentWeights.MultiCriteria", true)) {
    453            selection = new MultiCriteriaBranchAndBoundSelection(iModel.getProperties());
    454        } else {
    455            selection = new SuggestionSelection(model.getProperties());
    456        }
    457
    458        selection.setModel(model);
    459        selection.setPreferredSections(preferredSectionsForCourse);
    460        selection.setRequiredSections(new Hashtable<CourseRequest, Set<Section>>());
    461        selection.setRequiredFreeTimes(new HashSet<FreeTimeRequest>());
    462
    463        long t0 = JProf.currentTimeMillis();
    464        Assignment<Request, Enrollment> newAssignment = new AssignmentMap<Request, Enrollment>();
    465        BranchBoundNeighbour neighbour = selection.select(newAssignment, student);
    466        long time = JProf.currentTimeMillis() - t0;
    467        inc("[C] CPU Time", time);
    468        if (neighbour == null) {
    469            inc("[F] Failure");
    470        } else {
    471            if (iSuggestions) {
    472                StudentPreferencePenalties penalties = new StudentPreferencePenalties(StudentPreferencePenalties.sDistTypePreference);
    473                double maxOverExpected = 0;
    474                int assigned = 0;
    475                double penalty = 0.0;
    476                Hashtable<CourseRequest, Set<Section>> enrollments = new Hashtable<CourseRequest, Set<Section>>();
    477                List<RequestSectionPair> pairs = new ArrayList<RequestSectionPair>();
    478
    479                for (int i = 0; i < neighbour.getAssignment().length; i++) {
    480                    Enrollment enrl = neighbour.getAssignment()[i];
    481                    if (enrl != null && enrl.isCourseRequest() && enrl.getAssignments() != null) {
    482                        assigned++;
    483                        for (Section section : enrl.getSections()) {
    484                            maxOverExpected += model.getOverExpected(newAssignment, section, enrl.getRequest());
    485                            pairs.add(new RequestSectionPair(enrl.variable(), section));
    486                        }
    487                        enrollments.put((CourseRequest) enrl.variable(), enrl.getSections());
    488                        penalty += penalties.getPenalty(enrl);
    489                    }
    490                }
    491                penalty /= assigned;
    492                inc("[S] Initial Penalty", penalty);
    493                double nrSuggestions = 0.0, nrAccepted = 0.0, totalSuggestions = 0.0, nrTries = 0.0;
    494                for (int i = 0; i < pairs.size(); i++) {
    495                    RequestSectionPair pair = pairs.get(i);
    496                    SuggestionsBranchAndBound suggestionBaB = null;
    497                    if (model.getProperties().getPropertyBoolean("StudentWeights.MultiCriteria", true)) {
    498                        suggestionBaB = new MultiCriteriaBranchAndBoundSuggestions(model.getProperties(), student,
    499                                newAssignment, new Hashtable<CourseRequest, Set<Section>>(),
    500                                new HashSet<FreeTimeRequest>(), enrollments, pair.getRequest(), pair.getSection(),
    501                                null, maxOverExpected, iModel.getProperties().getPropertyBoolean(
    502                                        "StudentWeights.PriorityWeighting", true));
    503                    } else {
    504                        suggestionBaB = new SuggestionsBranchAndBound(model.getProperties(), student, newAssignment,
    505                                new Hashtable<CourseRequest, Set<Section>>(), new HashSet<FreeTimeRequest>(),
    506                                enrollments, pair.getRequest(), pair.getSection(), null, maxOverExpected);
    507                    }
    508
    509                    long x0 = JProf.currentTimeMillis();
    510                    TreeSet<SuggestionsBranchAndBound.Suggestion> suggestions = suggestionBaB.computeSuggestions();
    511                    inc("[S] Suggestion CPU Time", JProf.currentTimeMillis() - x0);
    512                    totalSuggestions += suggestions.size();
    513                    if (!suggestions.isEmpty())
    514                        nrSuggestions += 1.0;
    515                    nrTries += 1.0;
    516
    517                    SuggestionsBranchAndBound.Suggestion best = null;
    518                    for (SuggestionsBranchAndBound.Suggestion suggestion : suggestions) {
    519                        int a = 0;
    520                        double p = 0.0;
    521                        for (int j = 0; j < suggestion.getEnrollments().length; j++) {
    522                            Enrollment e = suggestion.getEnrollments()[j];
    523                            if (e != null && e.isCourseRequest() && e.getAssignments() != null) {
    524                                p += penalties.getPenalty(e);
    525                                a++;
    526                            }
    527                        }
    528                        p /= a;
    529                        if (a > assigned || (assigned == a && p < penalty)) {
    530                            best = suggestion;
    531                        }
    532                    }
    533                    if (best != null) {
    534                        nrAccepted += 1.0;
    535                        Enrollment[] e = best.getEnrollments();
    536                        for (int j = 0; j < e.length; j++)
    537                            if (e[j] != null && e[j].getAssignments() == null)
    538                                e[j] = null;
    539                        neighbour = new BranchBoundNeighbour(student, best.getValue(), e);
    540                        assigned = 0;
    541                        penalty = 0.0;
    542                        enrollments.clear();
    543                        pairs.clear();
    544                        for (int j = 0; j < neighbour.getAssignment().length; j++) {
    545                            Enrollment enrl = neighbour.getAssignment()[j];
    546                            if (enrl != null && enrl.isCourseRequest() && enrl.getAssignments() != null) {
    547                                assigned++;
    548                                for (Section section : enrl.getSections())
    549                                    pairs.add(new RequestSectionPair(enrl.variable(), section));
    550                                enrollments.put((CourseRequest) enrl.variable(), enrl.getSections());
    551                                penalty += penalties.getPenalty(enrl);
    552                            }
    553                        }
    554                        penalty /= assigned;
    555                        inc("[S] Improved Penalty", penalty);
    556                    }
    557                }
    558                inc("[S] Final Penalty", penalty);
    559                if (nrSuggestions > 0) {
    560                    inc("[S] Classes with suggestion", nrSuggestions);
    561                    inc("[S] Avg. # of suggestions", totalSuggestions / nrSuggestions);
    562                    inc("[S] Suggestion acceptance rate [%]", nrAccepted / nrSuggestions);
    563                } else {
    564                    inc("[S] Student with no suggestions available", 1.0);
    565                }
    566                if (!pairs.isEmpty())
    567                    inc("[S] Probability that a class has suggestions [%]", nrSuggestions / nrTries);
    568            }
    569
    570            List<Enrollment> enrollments = new ArrayList<Enrollment>();
    571            i: for (int i = 0; i < neighbour.getAssignment().length; i++) {
    572                Request request = original.getRequests().get(i);
    573                Enrollment clonnedEnrollment = neighbour.getAssignment()[i];
    574                if (clonnedEnrollment != null && clonnedEnrollment.getAssignments() != null) {
    575                    if (request instanceof FreeTimeRequest) {
    576                        enrollments.add(((FreeTimeRequest) request).createEnrollment());
    577                    } else {
    578                        for (Course course : ((CourseRequest) request).getCourses())
    579                            if (course.getId() == clonnedEnrollment.getCourse().getId())
    580                                for (Config config : course.getOffering().getConfigs())
    581                                    if (config.getId() == clonnedEnrollment.getConfig().getId()) {
    582                                        Set<Section> assignments = new HashSet<Section>();
    583                                        for (Subpart subpart : config.getSubparts())
    584                                            for (Section section : subpart.getSections()) {
    585                                                if (clonnedEnrollment.getSections().contains(section)) {
    586                                                    assignments.add(section);
    587                                                }
    588                                            }
    589                                        Reservation reservation = null;
    590                                        if (clonnedEnrollment.getReservation() != null) {
    591                                            for (Reservation r : course.getOffering().getReservations())
    592                                                if (r.getId() == clonnedEnrollment.getReservation().getId()) {
    593                                                    reservation = r;
    594                                                    break;
    595                                                }
    596                                        }
    597                                        enrollments.add(new Enrollment(request, clonnedEnrollment.getPriority(),
    598                                                course, config, assignments, reservation));
    599                                        continue i;
    600                                    }
    601                    }
    602                }
    603            }
    604            synchronized (iModel) {
    605                for (Request r : original.getRequests()) {
    606                    Enrollment e = assignment().getValue(r);
    607                    r.setInitialAssignment(e);
    608                    if (e != null)
    609                        updateSpace(assignment(), e, true);
    610                }
    611                for (Request r : original.getRequests())
    612                    if (assignment().getValue(r) != null)
    613                        assignment().unassign(0, r);
    614                boolean fail = false;
    615                for (Enrollment enrl : enrollments) {
    616                    if (iModel.conflictValues(assignment(), enrl).isEmpty()) {
    617                        assignment().assign(0, enrl);
    618                    } else {
    619                        fail = true;
    620                        break;
    621                    }
    622                }
    623                if (fail) {
    624                    for (Request r : original.getRequests())
    625                        if (assignment().getValue(r) != null)
    626                            assignment().unassign(0, r);
    627                    for (Request r : original.getRequests())
    628                        if (r.getInitialAssignment() != null)
    629                            assignment().assign(0, r.getInitialAssignment());
    630                    for (Request r : original.getRequests())
    631                        if (assignment().getValue(r) != null)
    632                            updateSpace(assignment(), assignment().getValue(r), false);
    633                } else {
    634                    for (Enrollment enrl : enrollments)
    635                        updateSpace(assignment(), enrl, false);
    636                }
    637                if (fail)
    638                    return false;
    639            }
    640            neighbour.assign(newAssignment, 0);
    641            int a = 0, u = 0, np = 0, zp = 0, pp = 0, cp = 0;
    642            double over = 0;
    643            double p = 0.0;
    644            for (Request r : student.getRequests()) {
    645                if (r instanceof CourseRequest) {
    646                    Enrollment e = newAssignment.getValue(r);
    647                    if (e != null) {
    648                        for (Section s : e.getSections()) {
    649                            if (s.getPenalty() < 0.0)
    650                                np++;
    651                            if (s.getPenalty() == 0.0)
    652                                zp++;
    653                            if (s.getPenalty() > 0.0)
    654                                pp++;
    655                            if (s.getLimit() > 0) {
    656                                p += s.getPenalty();
    657                                cp++;
    658                            }
    659                            over += model.getOverExpected(newAssignment, s, r);
    660                        }
    661                        a++;
    662                    } else {
    663                        u++;
    664                    }
    665                }
    666            }
    667            inc("[A] Student");
    668            if (over > 0.0)
    669                inc("[O] Over", over);
    670            if (a > 0)
    671                inc("[A] Assigned", a);
    672            if (u > 0)
    673                inc("[A] Not Assigned", u);
    674            inc("[V] Value", neighbour.value(newAssignment));
    675            if (zp > 0)
    676                inc("[P] Zero penalty", zp);
    677            if (np > 0)
    678                inc("[P] Negative penalty", np);
    679            if (pp > 0)
    680                inc("[P] Positive penalty", pp);
    681            if (cp > 0)
    682                inc("[P] Average penalty", p / cp);
    683        }
    684        inc("[T0] Time <10ms", time < 10 ? 1 : 0);
    685        inc("[T1] Time <100ms", time < 100 ? 1 : 0);
    686        inc("[T2] Time <250ms", time < 250 ? 1 : 0);
    687        inc("[T3] Time <500ms", time < 500 ? 1 : 0);
    688        inc("[T4] Time <1s", time < 1000 ? 1 : 0);
    689        inc("[T5] Time >=1s", time >= 1000 ? 1 : 0);
    690        return true;
    691    }
    692
    693    public static void updateSpace(Assignment<Request, Enrollment> assignment, Enrollment enrollment, boolean increment) {
    694        if (enrollment == null || !enrollment.isCourseRequest())
    695            return;
    696        for (Section section : enrollment.getSections())
    697            section.setSpaceHeld(section.getSpaceHeld() + (increment ? 1.0 : -1.0));
    698        List<Enrollment> feasibleEnrollments = new ArrayList<Enrollment>();
    699        int totalLimit = 0;
    700        for (Enrollment enrl : enrollment.getRequest().values(assignment)) {
    701            if (!enrl.getCourse().equals(enrollment.getCourse()))
    702                continue;
    703            boolean overlaps = false;
    704            for (Request otherRequest : enrollment.getRequest().getStudent().getRequests()) {
    705                if (otherRequest.equals(enrollment.getRequest()) || !(otherRequest instanceof CourseRequest))
    706                    continue;
    707                Enrollment otherErollment = assignment.getValue(otherRequest);
    708                if (otherErollment == null)
    709                    continue;
    710                if (enrl.isOverlapping(otherErollment)) {
    711                    overlaps = true;
    712                    break;
    713                }
    714            }
    715            if (!overlaps) {
    716                feasibleEnrollments.add(enrl);
    717                if (totalLimit >= 0) {
    718                    int limit = enrl.getLimit();
    719                    if (limit < 0)
    720                        totalLimit = -1;
    721                    else
    722                        totalLimit += limit;
    723                }
    724            }
    725        }
    726        double change = enrollment.getRequest().getWeight()
    727                / (totalLimit > 0 ? totalLimit : feasibleEnrollments.size());
    728        for (Enrollment feasibleEnrollment : feasibleEnrollments)
    729            for (Section section : feasibleEnrollment.getSections()) {
    730                if (totalLimit > 0) {
    731                    section.setSpaceExpected(section.getSpaceExpected() + (increment ? +change : -change)
    732                            * feasibleEnrollment.getLimit());
    733                } else {
    734                    section.setSpaceExpected(section.getSpaceExpected() + (increment ? +change : -change));
    735                }
    736            }
    737    }
    738
    739    public void run() {
    740        sLog.info("Input: " + ToolBox.dict2string(model().getExtendedInfo(assignment()), 2));
    741
    742        List<Student> students = new ArrayList<Student>(model().getStudents());
    743        String sort = System.getProperty("sort", "shuffle");
    744        if ("shuffle".equals(sort)) {
    745            Collections.shuffle(students);
    746        } else if ("choice".equals(sort)) {
    747            StudentChoiceOrder ord = new StudentChoiceOrder(model().getProperties());
    748            ord.setReverse(false);
    749            Collections.sort(students, ord);
    750        } else if ("referse".equals(sort)) {
    751            StudentChoiceOrder ord = new StudentChoiceOrder(model().getProperties());
    752            ord.setReverse(true);
    753            Collections.sort(students, ord);
    754        }
    755
    756        Iterator<Student> iterator = students.iterator();
    757        int nrThreads = Integer.parseInt(System.getProperty("nrConcurrent", "10"));
    758        List<Executor> executors = new ArrayList<Executor>();
    759        for (int i = 0; i < nrThreads; i++) {
    760            Executor executor = new Executor(iterator);
    761            executor.start();
    762            executors.add(executor);
    763        }
    764
    765        long t0 = System.currentTimeMillis();
    766        while (iterator.hasNext()) {
    767            try {
    768                Thread.sleep(60000);
    769            } catch (InterruptedException e) {
    770            }
    771            long time = System.currentTimeMillis() - t0;
    772            synchronized (iModel) {
    773                sLog.info("Progress [" + (time / 60000) + "m]: " + ToolBox.dict2string(model().getExtendedInfo(assignment()), 2));
    774            }
    775        }
    776
    777        for (Executor executor : executors) {
    778            try {
    779                executor.join();
    780            } catch (InterruptedException e) {
    781            }
    782        }
    783
    784        sLog.info("Output: " + ToolBox.dict2string(model().getExtendedInfo(assignment()), 2));
    785        long time = System.currentTimeMillis() - t0;
    786        inc("[T] Run Time [m]", time / 60000.0);
    787
    788    }
    789
    790    public class Executor extends Thread {
    791        private Iterator<Student> iStudents = null;
    792
    793        public Executor(Iterator<Student> students) {
    794            iStudents = students;
    795        }
    796
    797        @Override
    798        public void run() {
    799            try {
    800                for (;;) {
    801                    Student student = iStudents.next();
    802                    int attempt = 1;
    803                    while (!section(student)) {
    804                        sLog.warn(attempt + ". attempt failed for " + student.getId());
    805                        inc("[F] Failed attempt", attempt);
    806                        attempt++;
    807                        if (attempt == 101)
    808                            break;
    809                        if (attempt > 10) {
    810                            try {
    811                                Thread.sleep(ToolBox.random(100 * attempt));
    812                            } catch (InterruptedException e) {
    813                            }
    814                        }
    815                    }
    816                    if (attempt > 100)
    817                        inc("[F] Failed enrollment (all 100 attempts)");
    818                }
    819            } catch (NoSuchElementException e) {
    820            }
    821        }
    822
    823    }
    824
    825    public class TestModel extends OnlineSectioningModel {
    826        public TestModel(DataProperties config) {
    827            super(config);
    828        }
    829
    830        @Override
    831        public Map<String, String> getExtendedInfo(Assignment<Request, Enrollment> assignment) {
    832            Map<String, String> ret = super.getExtendedInfo(assignment);
    833            for (Map.Entry<String, Counter> e : iCounters.entrySet())
    834                ret.put(e.getKey(), e.getValue().toString());
    835            ret.put("Weighting model",
    836                    (model().getProperties().getPropertyBoolean("StudentWeights.MultiCriteria", true) ? "multi-criteria " : "") +
    837                    (model().getProperties().getPropertyBoolean("StudentWeights.PriorityWeighting", true) ? "priority" : "equal"));
    838            ret.put("B&B time limit", model().getProperties().getPropertyInt("Neighbour.BranchAndBoundTimeout", 1000) + " ms");
    839            if (iSuggestions) {
    840                ret.put("Suggestion time limit", model().getProperties().getPropertyInt("Suggestions.Timeout", 1000) + " ms");
    841            }
    842            return ret;
    843        }
    844    }
    845
    846    private static class RequestSectionPair {
    847        private Request iRequest;
    848        private Section iSection;
    849
    850        RequestSectionPair(Request request, Section section) {
    851            iRequest = request;
    852            iSection = section;
    853        }
    854
    855        Request getRequest() {
    856            return iRequest;
    857        }
    858
    859        Section getSection() {
    860            return iSection;
    861        }
    862    }
    863
    864    private void stats(File input) throws IOException {
    865        File file = new File(input.getParentFile(), "stats.csv");
    866        DecimalFormat df = new DecimalFormat("0.0000");
    867        boolean ex = file.exists();
    868        PrintWriter pw = new PrintWriter(new FileWriter(file, true));
    869        if (!ex) {
    870            pw.println("Input File,Run Time [m],Model,Sort,Over Expected,Not Assigned,Disb. Sections [%],Distance Confs.,Time Confs. [m],"
    871                    + "CPU Assignment [ms],Has Suggestions [%],Nbr Suggestions,Acceptance [%],CPU Suggestions [ms]");
    872        }
    873        pw.print(input.getName() + ",");
    874        pw.print(df.format(get("[T] Run Time [m]").sum()) + ",");
    875        pw.print(model().getProperties().getPropertyBoolean("StudentWeights.MultiCriteria", true) ? "multi-criteria " : "");
    876        pw.print(model().getProperties().getPropertyBoolean("StudentWeights.PriorityWeighting", true) ? "priority" : "equal");
    877        pw.print(iSuggestions ? " with suggestions" : "");
    878        pw.print(",");
    879        pw.print(System.getProperty("sort", "shuffle") + ",");
    880        pw.print("\"" + model().getOverExpectedCriterion() + "\",");
    881
    882        pw.print(get("[A] Not Assigned").sum() + ",");
    883        pw.print(df.format(getPercDisbalancedSections(assignment(), 0.1)) + ",");
    884        if (model().getStudentQuality() != null) {
    885            pw.print(df.format(((double) model().getStudentQuality().getTotalPenalty(assignment(), StudentQuality.Type.Distance, StudentQuality.Type.ShortDistance)) / model().getStudents().size()) + ",");
    886            pw.print(df.format(5.0 * model().getStudentQuality().getTotalPenalty(assignment(), StudentQuality.Type.CourseTimeOverlap, StudentQuality.Type.FreeTimeOverlap, StudentQuality.Type.Unavailability) / model().getStudents().size()) + ",");
    887        } else {
    888            pw.print(df.format(((double) model().getDistanceConflict().getTotalNrConflicts(assignment())) / model().getStudents().size()) + ",");
    889            pw.print(df.format(5.0 * model().getTimeOverlaps().getTotalNrConflicts(assignment()) / model().getStudents().size()) + ",");
    890        }
    891        pw.print(df.format(get("[C] CPU Time").avg()) + ",");
    892        if (iSuggestions) {
    893            pw.print(df.format(get("[S] Probability that a class has suggestions [%]").avg()) + ",");
    894            pw.print(df.format(get("[S] Avg. # of suggestions").avg()) + ",");
    895            pw.print(df.format(get("[S] Suggestion acceptance rate [%]").avg()) + ",");
    896            pw.print(df.format(get("[S] Suggestion CPU Time").avg()));
    897        }
    898        pw.println();
    899
    900        pw.flush();
    901        pw.close();
    902    }
    903
    904    public static void main(String[] args) {
    905        try {
    906            System.setProperty("jprof", "cpu");
    907            BasicConfigurator.configure();
    908
    909            DataProperties cfg = new DataProperties();
    910            cfg.setProperty("Neighbour.BranchAndBoundTimeout", "5000");
    911            cfg.setProperty("Suggestions.Timeout", "1000");
    912            cfg.setProperty("Extensions.Classes", DistanceConflict.class.getName() + ";" + TimeOverlapsCounter.class.getName());
    913            cfg.setProperty("StudentWeights.Class", StudentSchedulingAssistantWeights.class.getName());
    914            cfg.setProperty("StudentWeights.PriorityWeighting", "true");
    915            cfg.setProperty("StudentWeights.LeftoverSpread", "true");
    916            cfg.setProperty("StudentWeights.BalancingFactor", "0.0");
    917            cfg.setProperty("Reservation.CanAssignOverTheLimit", "true");
    918            cfg.setProperty("Distances.Ellipsoid", DistanceMetric.Ellipsoid.WGS84.name());
    919            cfg.setProperty("StudentWeights.MultiCriteria", "true");
    920            cfg.setProperty("CourseRequest.SameTimePrecise", "true");
    921
    922            cfg.setProperty("log4j.rootLogger", "INFO, A1");
    923            cfg.setProperty("log4j.appender.A1", "org.apache.log4j.ConsoleAppender");
    924            cfg.setProperty("log4j.appender.A1.layout", "org.apache.log4j.PatternLayout");
    925            cfg.setProperty("log4j.appender.A1.layout.ConversionPattern", "%-5p %c{2}: %m%n");
    926            cfg.setProperty("log4j.logger.org.hibernate", "INFO");
    927            cfg.setProperty("log4j.logger.org.hibernate.cfg", "WARN");
    928            cfg.setProperty("log4j.logger.org.hibernate.cache.EhCacheProvider", "ERROR");
    929            cfg.setProperty("log4j.logger.org.unitime.commons.hibernate", "INFO");
    930            cfg.setProperty("log4j.logger.net", "INFO");
    931
    932            cfg.setProperty("Xml.LoadBest", "false");
    933            cfg.setProperty("Xml.LoadCurrent", "false");
    934
    935            cfg.putAll(System.getProperties());
    936
    937            PropertyConfigurator.configure(cfg);
    938
    939            final Test test = new Test(cfg);
    940
    941            final File input = new File(args[0]);
    942            StudentSectioningXMLLoader loader = new StudentSectioningXMLLoader(test.model(), test.assignment());
    943            loader.setInputFile(input);
    944            loader.load();
    945
    946            test.run();
    947
    948            Solver<Request, Enrollment> s = new Solver<Request, Enrollment>(cfg);
    949            s.setInitalSolution(test.model());
    950            StudentSectioningXMLSaver saver = new StudentSectioningXMLSaver(s);
    951            File output = new File(input.getParentFile(), input.getName().substring(0, input.getName().lastIndexOf('.')) +
    952                    "-" + cfg.getProperty("run", "r0") + ".xml");
    953            saver.save(output);
    954
    955            test.stats(input);
    956        } catch (Exception e) {
    957            sLog.error("Test failed: " + e.getMessage(), e);
    958        }
    959    }
    960
    961    private static class Counter {
    962        private double iTotal = 0.0, iMin = 0.0, iMax = 0.0, iTotalSquare = 0.0;
    963        private int iCount = 0;
    964
    965        void inc(double value) {
    966            if (iCount == 0) {
    967                iTotal = value;
    968                iMin = value;
    969                iMax = value;
    970                iTotalSquare = value * value;
    971            } else {
    972                iTotal += value;
    973                iMin = Math.min(iMin, value);
    974                iMax = Math.max(iMax, value);
    975                iTotalSquare += value * value;
    976            }
    977            iCount++;
    978        }
    979
    980        int count() {
    981            return iCount;
    982        }
    983
    984        double sum() {
    985            return iTotal;
    986        }
    987
    988        double min() {
    989            return iMin;
    990        }
    991
    992        double max() {
    993            return iMax;
    994        }
    995
    996        double rms() {
    997            return (iCount == 0 ? 0.0 : Math.sqrt(iTotalSquare / iCount));
    998        }
    999
    1000        double avg() {
    1001            return (iCount == 0 ? 0.0 : iTotal / iCount);
    1002        }
    1003
    1004        @Override
    1005        public String toString() {
    1006            return sDF.format(sum()) + " (min: " + sDF.format(min()) + ", max: " + sDF.format(max()) +
    1007                    ", avg: " + sDF.format(avg()) + ", rms: " + sDF.format(rms()) + ", cnt: " + count() + ")";
    1008        }
    1009    }
    1010
    1011}