fll.scheduler.TableOptimizer.java Source code

Java tutorial

Introduction

Here is the source code for fll.scheduler.TableOptimizer.java

Source

/*
 * Copyright (c) 2011 INSciTE.  All rights reserved
 * INSciTE is on the web at: http://www.hightechkids.org
 * This code is released under GPL; see LICENSE.txt for details.
 */

package fll.scheduler;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.text.ParseException;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.apache.commons.cli.CommandLine;
import org.apache.commons.cli.CommandLineParser;
import org.apache.commons.cli.HelpFormatter;
import org.apache.commons.cli.Option;
import org.apache.commons.cli.Options;
import org.apache.commons.cli.PosixParser;
import org.apache.log4j.Logger;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException;

import fll.Team;
import fll.Utilities;
import fll.scheduler.TournamentSchedule.ColumnInformation;
import fll.util.CSVCellReader;
import fll.util.CellFileReader;
import fll.util.CheckCanceled;
import fll.util.ExcelCellReader;
import fll.util.FLLInternalException;
import fll.util.FLLRuntimeException;
import fll.util.LogUtils;

/**
 * Optimize a schedule by rearranging the sides of tables used and the tables
 * used. No times will be changed.
 */
public class TableOptimizer {

    private static final Logger LOGGER = LogUtils.getLogger();

    private static final String SCHED_FILE_OPTION = "s";

    private final TournamentSchedule schedule;

    private final File basedir;

    private int numSolutions = 0;

    private File mBestScheduleOutputFile = null;

    private Map<PerformanceTime, Integer> bestPermutation = null;

    private int bestScore;

    /**
     * List of table colors from the schedule. Each list inside the list is a
     * group of tables that are scheduled together.
     * If not alternating tables then the outer list will have a length of 1.
     */
    private final List<List<String>> tableGroups;

    /**
     * The best schedule found so far. Starts out at null and
     * is modified by {@link #optimize(CheckCanceled)}.
     * 
     * @return the file containing the best schedule or null if no such schedule
     *         has been found
     */
    public File getBestScheduleOutputFile() {
        return mBestScheduleOutputFile;
    }

    private final ScheduleChecker checker;

    private static boolean isPerformanceViolation(final ConstraintViolation violation) {
        return null != violation.getPerformance();
    }

    /**
     * Compute score for the current schedule. The lowest score is best.
     */
    private int computeScheduleScore() {
        final int numWarnings = checker.verifySchedule().size();

        final int tableUseScore = computeTableUseScore();

        // warnings is most important, then table use
        return numWarnings * 1000 + tableUseScore;
    }

    /**
     * Compute the table use score. This is the difference between the minimum
     * number of times any table is used and the maximum number of times any table
     * is used. This should even out the table use.
     * 
     * @return score, lower is better
     */
    private int computeTableUseScore() {
        final Map<String, Integer> tableUse = new HashMap<>();
        for (final TeamScheduleInfo ti : this.schedule.getSchedule()) {
            for (int round = 0; round < ti.getNumberOfRounds(); ++round) {
                final String tableColor = ti.getPerfTableColor(round);
                int count;
                if (tableUse.containsKey(tableColor)) {
                    count = tableUse.get(tableColor);
                } else {
                    count = 0;
                }
                ++count;
                tableUse.put(tableColor, count);
            } // foreach round
        } // foreach team

        int minUse = Integer.MAX_VALUE;
        int maxUse = 0;
        for (Map.Entry<String, Integer> entry : tableUse.entrySet()) {
            minUse = Math.min(minUse, entry.getValue());
            maxUse = Math.max(maxUse, entry.getValue());
        } // foreach table

        if (0 == maxUse) {
            return 0;
        } else {
            return maxUse - minUse;
        }
    }

    /**
     * Get the current table assignments for the specified time so that
     * they can be re-applied if needed.
     * 
     * @return key=table info, value=team number
     */
    private Map<PerformanceTime, Integer> getCurrentTableAssignments(final LocalTime time) {
        final Map<PerformanceTime, Integer> assignments = new HashMap<>();

        for (final TeamScheduleInfo si : this.schedule.getSchedule()) {
            for (int round = 0; round < this.schedule.getNumberOfRounds(); ++round) {
                final PerformanceTime pt = si.getPerf(round);
                if (time.equals(pt.getTime())) {
                    assignments.put(pt, si.getTeamNumber());
                }
            }
        }

        return assignments;
    }

    /**
     * Compute the best table ordering for a set of teams at the
     * specified time.
     * 
     * @param checkCanceled if non-null, check if the optimization should finish
     *          early
     * @return best score found
     */
    private void computeBestTableOrdering(final List<Integer> teams, final LocalTime time,
            final List<String> tables, final CheckCanceled checkCanceled) {
        if (teams.isEmpty()) {
            throw new IllegalArgumentException("Must have some teams to check");
        }

        if (null == bestPermutation) {
            bestPermutation = getCurrentTableAssignments(time);
            bestScore = computeScheduleScore();
        }

        final List<Map<PerformanceTime, Integer>> possibleValues = computePossibleValues(teams, time, tables);
        for (final Map<PerformanceTime, Integer> possibleValue : possibleValues) {
            if (null != checkCanceled && checkCanceled.isCanceled()) {
                // user interrupt
                break;
            }

            applyPerformanceOrdering(possibleValue);

            // check for better value
            final int score = computeScheduleScore();
            if (score < bestScore) {
                try {
                    final File outputFile = new File(basedir,
                            String.format("%s-opt-%d.csv", schedule.getName(), numSolutions));
                    LOGGER.info(String.format("Found better schedule (%d -> %d), writing to: %s", bestScore, score,
                            outputFile.getAbsolutePath()));
                    schedule.writeToCSV(outputFile);

                    ++numSolutions;

                    if (null != mBestScheduleOutputFile) {
                        if (!mBestScheduleOutputFile.delete()) {
                            mBestScheduleOutputFile.deleteOnExit();
                        }
                    }
                    mBestScheduleOutputFile = outputFile;
                } catch (final IOException e) {
                    throw new RuntimeException(e);
                }

                bestPermutation = possibleValue;
                bestScore = score;

                if (bestScore == 0) {
                    break;
                }
            }
        }

        // assign the best value
        applyPerformanceOrdering(bestPermutation);
    }

    /**
     * Compute all possible orderings of teams on the current set
     * of tables.
     * 
     * @param teams
     * @return list of possible orderings, key=table information, value=team
     *         number
     */
    private List<Map<PerformanceTime, Integer>> computePossibleValues(final List<Integer> teams,
            final LocalTime time, final List<String> tables) {
        final List<Map<PerformanceTime, Integer>> possibleValues = new LinkedList<>();

        final boolean oddNumberOfTeams = Utilities.isOdd(teams.size());

        final List<List<Integer>> possibleTableOrderings = permutate(tables.size() * 2);
        for (final List<Integer> ordering : possibleTableOrderings) {
            if (ordering.size() != tables.size() * 2) {
                throw new FLLInternalException(String.format(
                        "All possible orderings must be twice the number of tables. ordering.size: %d tableColors: %d",
                        ordering.size(), tables.size()));
            }

            boolean validToHaveNullTeam = oddNumberOfTeams;

            // convert to valid ordering
            final Map<PerformanceTime, Integer> assignments = new HashMap<>();
            for (int tableColorIndex = 0; tableColorIndex < tables.size(); ++tableColorIndex) {
                final String tableColor = tables.get(tableColorIndex);

                // check in pairs to make sure we have a valid table assignment
                final int side1 = 1;
                final int orderIndex1 = tableColorIndex * 2;
                final int teamIndex1 = ordering.get(orderIndex1);
                final int teamNumber1 = getTeamNumber(teams, teamIndex1);

                final int side2 = 2;
                final int orderIndex2 = orderIndex1 + 1;
                final int teamIndex2 = ordering.get(orderIndex2);
                final int teamNumber2 = getTeamNumber(teams, teamIndex2);

                if (Team.NULL_TEAM_NUMBER != teamNumber1 && Team.NULL_TEAM_NUMBER != teamNumber2) {
                    assignments.put(new PerformanceTime(time, tableColor, side1), teamNumber1);
                    assignments.put(new PerformanceTime(time, tableColor, side2), teamNumber2);
                } else if (Team.NULL_TEAM_NUMBER != teamNumber1 && Team.NULL_TEAM_NUMBER == teamNumber2
                        && validToHaveNullTeam) {
                    assignments.put(new PerformanceTime(time, tableColor, side1), teamNumber1);

                    // can only have 1 uneven pairing at a time
                    validToHaveNullTeam = false;
                } else if (Team.NULL_TEAM_NUMBER == teamNumber1 && Team.NULL_TEAM_NUMBER != teamNumber2
                        && validToHaveNullTeam) {
                    assignments.put(new PerformanceTime(time, tableColor, side2), teamNumber2);

                    // can only have 1 uneven pairing at a time
                    validToHaveNullTeam = false;
                }

                if (!assignments.isEmpty()) {
                    possibleValues.add(assignments);
                }
            } // foreach table

        } // foreach possible ordering

        return possibleValues;
    }

    /**
     * Get team number from teams using teamIndex.
     * 
     * @param teams list of teams
     * @param teamIndex index into teams
     * @return the team number or {@see Team#NULL_TEAM_NUMBER} if the index is
     *         larger than the list of teams
     */
    private static int getTeamNumber(final List<Integer> teams, final int teamIndex) {
        final int teamNumber;
        if (teamIndex < teams.size()) {
            teamNumber = teams.get(teamIndex);
        } else {
            teamNumber = Team.NULL_TEAM_NUMBER;
        }
        return teamNumber;
    }

    private void applyPerformanceOrdering(final Map<PerformanceTime, Integer> possibleValue) {
        for (Map.Entry<PerformanceTime, Integer> entry : possibleValue.entrySet()) {
            final int teamNumber = entry.getValue();
            final PerformanceTime perfTime = entry.getKey();

            // can use perfTime.getTime() as oldTime since we know that we're just
            // moving teams across tables
            schedule.reassignTable(teamNumber, perfTime.getTime(), perfTime);
        }

    }

    /**
     * Compute permutations of the integers [0, numElements]
     * 
     * @param numElements how many elements to be permuted
     * @return all possible orderings
     */
    static public List<List<Integer>> permutate(final int numElements) {
        final List<Integer> allElements = new ArrayList<>();
        for (int i = 0; i < numElements; ++i) {
            allElements.add(i);
        }

        final List<Integer> order = new ArrayList<>(numElements);
        for (int i = 0; i < numElements; ++i) {
            order.add(i);
        }

        final List<List<Integer>> permutations = new LinkedList<>();
        permutate(numElements, allElements, order, permutations);
        return permutations;
    }

    /**
     * Recursive function that computes permutations. To
     * be called from {@see #permutate(int)}.
     * 
     * @param arrayCount
     * @param elements the elements to compute permutations of
     * @param order
     * @param permutations the resulting permutations
     */
    static private void permutate(final int arrayCount, final List<Integer> elements, final List<Integer> order,
            final List<List<Integer>> permutations) {
        if (elements.isEmpty()) {
            throw new IllegalArgumentException("Cannot permutate 0 elements");
        }

        final int position = arrayCount - elements.size();

        if (elements.size() == 1) {
            order.set(position, elements.get(0));
            permutations.add(order);
        } else {
            for (int i = 0; i < elements.size(); ++i) {
                final int element = elements.get(i);
                final List<Integer> newOrder = new ArrayList<Integer>(order);
                newOrder.set(position, element);

                final List<Integer> newElements = new ArrayList<Integer>(elements);
                newElements.remove(i);
                permutate(arrayCount, newElements, newOrder, permutations);
            }
        }
    }

    /**
     * Gather up all performance times in the specified list of violations.
     */
    private Set<LocalTime> gatherPerformanceTimes(final Collection<ConstraintViolation> violations) {
        final Set<LocalTime> perfTimes = new HashSet<>();
        for (final ConstraintViolation violation : violations) {
            final LocalTime d = violation.getPerformance();
            if (null != d) {
                perfTimes.add(d);
            }
        }
        return perfTimes;
    }

    /**
     * Pick team with most violations and isn't in the set of optimizedTeams.
     */
    private List<ConstraintViolation> pickTeamWithMostViolations(final Set<Integer> optimizedTeams) {
        final List<ConstraintViolation> violations = checker.verifySchedule();
        // team->violations
        final Map<Integer, List<ConstraintViolation>> teamViolations = new HashMap<Integer, List<ConstraintViolation>>();
        for (final ConstraintViolation violation : violations) {
            if (isPerformanceViolation(violation)) {
                final List<ConstraintViolation> vs;
                if (teamViolations.containsKey(violation.getTeam())) {
                    vs = teamViolations.get(violation.getTeam());
                } else {
                    vs = new LinkedList<ConstraintViolation>();
                    teamViolations.put(violation.getTeam(), vs);
                }
                vs.add(violation);
            }
        }

        // find max
        List<ConstraintViolation> retval = new LinkedList<ConstraintViolation>();
        for (final Map.Entry<Integer, List<ConstraintViolation>> entry : teamViolations.entrySet()) {
            if (!optimizedTeams.contains(entry.getKey())) {
                if (entry.getValue().size() > retval.size()) {
                    retval = entry.getValue();
                }
            }
        }

        return retval;
    }

    private static Options buildOptions() {
        final Options options = new Options();
        Option option = new Option(SCHED_FILE_OPTION, "schedfile", true, "<file> the schedule file ");
        option.setRequired(true);
        options.addOption(option);

        return options;
    }

    private static void usage(final Options options) {
        final HelpFormatter formatter = new HelpFormatter();
        formatter.printHelp("TableOptimizer", options);
    }

    /**
     * @param args
     */
    public static void main(final String[] args) {
        LogUtils.initializeLogging();

        File schedfile = null;
        final Options options = buildOptions();
        try {
            final CommandLineParser parser = new PosixParser();
            final CommandLine cmd = parser.parse(options, args);

            schedfile = new File(cmd.getOptionValue(SCHED_FILE_OPTION));

        } catch (final org.apache.commons.cli.ParseException pe) {
            LOGGER.error(pe.getMessage());
            usage(options);
            System.exit(1);
        }

        FileInputStream fis = null;
        try {
            if (!schedfile.canRead()) {
                LOGGER.fatal(schedfile.getAbsolutePath() + " is not readable");
                System.exit(4);
            }

            final boolean csv = schedfile.getName().endsWith("csv");
            final CellFileReader reader;
            final String sheetName;
            if (csv) {
                reader = new CSVCellReader(schedfile);
                sheetName = null;
            } else {
                sheetName = SchedulerUI.promptForSheetName(schedfile);
                if (null == sheetName) {
                    return;
                }
                fis = new FileInputStream(schedfile);
                reader = new ExcelCellReader(fis, sheetName);
            }

            final ColumnInformation columnInfo = TournamentSchedule.findColumns(reader, new LinkedList<String>());
            if (null != fis) {
                fis.close();
                fis = null;
            }

            final List<SubjectiveStation> subjectiveStations = SchedulerUI.gatherSubjectiveStationInformation(null,
                    columnInfo);

            // not bothering to get the schedule params as we're just tweaking table
            // assignments, which wont't be effected by the schedule params.
            final SchedParams params = new SchedParams(subjectiveStations, SchedParams.DEFAULT_PERFORMANCE_MINUTES,
                    SchedParams.MINIMUM_CHANGETIME_MINUTES, SchedParams.MINIMUM_PERFORMANCE_CHANGETIME_MINUTES);
            final List<String> subjectiveHeaders = new LinkedList<String>();
            for (final SubjectiveStation station : subjectiveStations) {
                subjectiveHeaders.add(station.getName());
            }

            final String name = Utilities.extractBasename(schedfile);

            final TournamentSchedule schedule;
            if (csv) {
                schedule = new TournamentSchedule(name, schedfile, subjectiveHeaders);
            } else {
                fis = new FileInputStream(schedfile);
                schedule = new TournamentSchedule(name, fis, sheetName, subjectiveHeaders);
            }

            final TableOptimizer optimizer = new TableOptimizer(params, schedule,
                    schedfile.getAbsoluteFile().getParentFile());
            final long start = System.currentTimeMillis();
            optimizer.optimize(null);
            final long stop = System.currentTimeMillis();
            LOGGER.info("Optimization took: " + (stop - start) / 1000.0 + " seconds");

        } catch (final ParseException e) {
            LOGGER.fatal(e, e);
            System.exit(5);
        } catch (final ScheduleParseException e) {
            LOGGER.fatal(e, e);
            System.exit(6);
        } catch (final IOException e) {
            LOGGER.fatal("Error reading file", e);
            System.exit(4);
        } catch (final RuntimeException e) {
            LOGGER.fatal(e, e);
            throw e;
        } catch (InvalidFormatException e) {
            LOGGER.fatal(e, e);
            System.exit(7);
        } finally {
            try {
                if (null != fis) {
                    fis.close();
                }
            } catch (final IOException e) {
                if (LOGGER.isDebugEnabled()) {
                    LOGGER.debug("Error closing stream", e);
                }
            }
        }

    }

    /**
     * Compute map of tables at each time.
     * 
     * @param schedule the schedule to work with
     * @return key=time, value=tables used at this time
     */
    private static Map<LocalTime, Set<String>> gatherTablesAtTime(final TournamentSchedule schedule) {
        final Map<LocalTime, Set<String>> tablesAtTime = new HashMap<>();

        for (int round = 0; round < schedule.getNumberOfRounds(); ++round) {
            for (final TeamScheduleInfo si : schedule.getSchedule()) {
                final PerformanceTime perf = si.getPerf(round);

                Set<String> tables;
                if (tablesAtTime.containsKey(perf.getTime())) {
                    tables = tablesAtTime.get(perf.getTime());
                } else {
                    tables = new HashSet<>();
                }
                tables.add(perf.getTable());
                tablesAtTime.put(perf.getTime(), tables);
            }
        }

        return tablesAtTime;
    }

    /**
     * Check if any elements in set2 are in set1.
     */
    private static boolean containsAny(final Collection<String> set1, final Collection<String> set2) {
        for (final String needle : set2) {
            if (set1.contains(needle)) {
                return true;
            }
        }
        return false;
    }

    /**
     * Walk over the schedule and figure out how tables are grouped. If the
     * schedule uses alternating tables the returned list will have more than 1
     * element.
     */
    private static List<List<String>> determineTableGroups(final TournamentSchedule schedule) {
        final Map<LocalTime, Set<String>> tablesAtTime = gatherTablesAtTime(schedule);

        final List<Set<String>> tableGroups = new ArrayList<>();
        for (final Map.Entry<LocalTime, Set<String>> entry : tablesAtTime.entrySet()) {
            final Set<String> toFind = entry.getValue();

            boolean found = false;
            for (int i = 0; i < tableGroups.size() && !found; ++i) {
                final Set<String> group = tableGroups.get(i);
                if (containsAny(group, toFind)) {
                    group.addAll(toFind);
                    found = true;
                }
            } // foreach known table group

            if (!found) {
                // create new grouping
                tableGroups.add(toFind);
            }
        } // foreach group of tables in the schedule

        // consolidate the existing groups
        final List<List<String>> finalGroups = new ArrayList<>();
        if (tableGroups.size() > 1) {
            final List<String> firstGroup = new ArrayList<>(tableGroups.remove(0));
            finalGroups.add(firstGroup);

            while (!tableGroups.isEmpty()) {
                final List<String> toFind = new ArrayList<>(tableGroups.remove(0));

                boolean found = false;
                for (int i = 0; i < finalGroups.size() && !found; ++i) {
                    final List<String> group = finalGroups.get(i);
                    if (containsAny(group, toFind)) {
                        group.addAll(toFind);
                        found = true;
                    }
                } // foreach known table group

                if (!found) {
                    // create new grouping
                    finalGroups.add(toFind);
                }
            }

        } else {
            for (final Set<String> group : tableGroups) {
                finalGroups.add(new ArrayList<String>(group));
            }
        }
        return finalGroups;

    }

    /**
     * @param params the schedule parameters
     * @param schedule the schedule to optimize (will be modified)
     * @param basedir the directory to store better schedules in
     * @throws IllegalArgumentException if the schedule has hard constraint
     *           violations
     */
    public TableOptimizer(final SchedParams params, final TournamentSchedule schedule, final File basedir)
            throws IllegalArgumentException {
        this.schedule = schedule;
        this.basedir = basedir;
        this.checker = new ScheduleChecker(params, schedule);
        this.tableGroups = determineTableGroups(schedule);

        if (tableGroups.isEmpty()) {
            throw new FLLInternalException("Something went wrong. Table groups list is empty");
        }

        final List<ConstraintViolation> violations = checker.verifySchedule();
        for (final ConstraintViolation v : violations) {
            if (v.isHard()) {
                throw new IllegalArgumentException(
                        "Should not have any hard constraint violations: " + v.getMessage());
            }
        }

        if (!this.basedir.isDirectory()) {
            throw new IllegalArgumentException("Basedir must be a directory");
        }
    }

    /**
     * Run the table optimizer.
     * 
     * @param checkCanceled if non-null, checked to see if the optimizer should
     *          exit early
     */
    public void optimize(final CheckCanceled checkCanceled) {
        final Set<Integer> optimizedTeams = new HashSet<Integer>();
        final Set<LocalTime> optimizedTimes = new HashSet<>();

        List<ConstraintViolation> teamViolations = pickTeamWithMostViolations(optimizedTeams);
        while ((null != checkCanceled && !checkCanceled.isCanceled()) && !teamViolations.isEmpty()) {
            final int team = teamViolations.get(0).getTeam();
            optimizedTeams.add(team);

            if (LOGGER.isTraceEnabled()) {
                LOGGER.trace("Optimize tables for team: " + team);
            }

            final Set<LocalTime> perfTimes = gatherPerformanceTimes(teamViolations);
            optimize(perfTimes, checkCanceled);

            optimizedTimes.addAll(perfTimes);

            teamViolations = pickTeamWithMostViolations(optimizedTeams);
        } // while team violations

        if (null != checkCanceled && !checkCanceled.isCanceled()) {
            // optimize non-full table times if we haven't already touched them while
            // optimizing teams
            final Set<LocalTime> perfTimes = findNonFullTableTimes();
            perfTimes.removeAll(optimizedTimes);
            if (!perfTimes.isEmpty()) {
                optimize(perfTimes, checkCanceled);
            }
        }

    }

    /**
     * Find all times in the schedule where the number of teams
     * competing doesn't equal the number of tables available.
     */
    private Set<LocalTime> findNonFullTableTimes() {
        final Map<LocalTime, Integer> perfCounts = new HashMap<>();
        final Map<LocalTime, List<String>> perfTables = new HashMap<>();
        for (final TeamScheduleInfo ti : this.schedule.getSchedule()) {
            for (int round = 0; round < ti.getNumberOfRounds(); ++round) {
                final PerformanceTime pt = ti.getPerf(round);
                final LocalTime time = pt.getTime();

                List<String> tables = perfTables.get(time);
                for (int i = 0; null == tables && i < tableGroups.size(); ++i) {
                    final List<String> group = this.tableGroups.get(i);
                    if (group.contains(pt.getTable())) {
                        tables = group;
                    }
                }
                perfTables.put(time, tables);

                int count = 0;
                if (perfCounts.containsKey(time)) {
                    count = perfCounts.get(time);
                }
                ++count;
                perfCounts.put(time, count);
            }
        }

        final Set<LocalTime> perfTimes = new HashSet<>();

        for (final Map.Entry<LocalTime, Integer> entry : perfCounts.entrySet()) {
            final LocalTime time = entry.getKey();
            final int useCount = entry.getValue();

            final List<String> tables = perfTables.get(time);
            if (tables.isEmpty()) {
                throw new FLLInternalException("No tables found at time: " + TournamentSchedule.formatTime(time));
            }

            // 2 teams on each table at a given time
            final int expectedTableUse = tables.size() * 2;

            if (useCount < expectedTableUse) {
                perfTimes.add(time);
            }
        }

        return perfTimes;
    }

    /**
     * Optimize the table use at the specified times.
     * 
     * @param perfTimes the times to optimize at
     * @param checkCancled used to check if the optimization should exit early
     */
    private void optimize(final Set<LocalTime> perfTimes, final CheckCanceled checkCanceled) {
        for (final LocalTime time : perfTimes) {
            final List<Integer> teams = new ArrayList<Integer>();

            List<String> tables = null;
            for (final TeamScheduleInfo si : schedule.getSchedule()) {
                for (int round = 0; round < schedule.getNumberOfRounds(); ++round) {
                    final PerformanceTime pt = si.getPerf(round);
                    if (time.equals(pt.getTime())) {
                        teams.add(si.getTeamNumber());

                        // choose the tables to use for assignments
                        if (null == tables) {
                            for (int i = 0; null == tables && i < tableGroups.size(); ++i) {
                                final List<String> group = this.tableGroups.get(i);
                                if (group.contains(pt.getTable())) {
                                    tables = group;
                                }
                            }
                            if (null == tables) {
                                throw new FLLRuntimeException("Cannot find table group for " + pt.getTable());
                            }
                        }

                    }
                }
            } // foreach schedule item

            computeBestTableOrdering(teams, time, tables, checkCanceled);

        } // foreach time
    }

}