lib/GridOptions.js

'use strict';
import * as FS from 'fs';
import * as Utils from './Utils';
//NOTE: TODO: Not entirely thrilled with this class. It has a lot in common with
//            ModelStatics, but the two are implemented very differently. Part
//            of that is due to ModelStatics's dependence on baked-in
//            TensorFlow keys, but it's also due to project churn and the
//            TypeScript conversion process.
//            Revisit both classes, and settle on a common approach.
/**
 * The config of a grid search. Takes all settings that are not hyperparameters
 * or parts of a network model, all of them optional.
 */
class GridOptions {
    /**
     * Creates an instance of GridOptions.
     * @param {Types.StringKeyedSimpleObject} userOptions
     * @example
     * new tngs.GridOptions({
     *   epochStatsDepth: 10,         // Length of the trailing average; default 5
     *   repetitions: 3,              // Number of times to repeat each iteration of the grid; default 1
     *   resultsDirectory: ''         // Directory in which results files are written; default './' (current directory)
     *   validationSetSizeMin: 50,    // Fewer than x validation cases triggers a warning; default 100
     *   writeResultsAsCSV: true      // Whether to output results as a CSV file; default true
     * });
     */
    constructor(userOptions) {
        this._options = {};
        let keysFound = false;
        // validate each key supplied by the user
        for (const k in userOptions) {
            keysFound = true;
            if (ALL_AVAILABLE_OPTIONS[k] !== undefined) {
                continue;
            }
            //NOTE: This complex error report is here to help the user, because looking up
            //      config params in documentation sucks.
            //		This way, we're explicit about what's supported (in this version).
            // First we console-error, to get the actual problem out of the way...
            const ERROR_TEXT = 'Unknown grid-options parameter encountered: ' + k;
            //NOTE: If the IDE/terminal is set to break on these - before we get our details
            //      out - so be it (that's likely a power-user anyway).
            console.error(ERROR_TEXT);
            // ...next we print the list of available option keys...
            console.log('The following parameter are suppored:');
            for (const l in ALL_AVAILABLE_OPTIONS) {
                //TODO: Include defaults with these.
                console.log(l);
            }
            // ...then we throw, having given the user the exact info they need.
            throw new Error(ERROR_TEXT);
        }
        const DEFAULT_OPTIONS = {
            epochStatsDepth: GridOptions.DEFAULT_EPOCH_STATS_DEPTH,
            repetitions: GridOptions.DEFAULT_REPETITIONS,
            resultsDirectory: GridOptions.DEFAULT_RESULTS_DIRECTORY,
            validationSetSizeMin: GridOptions.DEFAULT_VALIDATION_SET_SIZE_MIN,
            writeResultsAsCSV: GridOptions.DEFAULT_WRITE_RESULTS_AS_CSV
        };
        if (!keysFound) {
            // empty object received; set pure defaults
            userOptions = DEFAULT_OPTIONS;
            //NOTE: We send these defaults through validation, to double-check our values,
            //      and (especially) catch bad edits in the future.
            //PERF: It's technically an unnecessary hit, but it's negligible.
        }
        const ERROR_PREFIX = 'Grid option ';
        // validate each value supplied by the user
        for (const k in userOptions) {
            const OPTION = userOptions[k];
            switch (k) {
                case 'epochStatsDepth':
                case 'repetitions':
                case 'validationSetSizeMin':
                    {
                        if (typeof OPTION !== 'number') {
                            throw new Error(ERROR_PREFIX + '"' + k + '" must be a '
                                + 'number.');
                        }
                        if (!Utils.CheckPositiveInteger(OPTION)) {
                            throw new Error(ERROR_PREFIX + '"' + k + '" must be a '
                                + 'positive integer.');
                        }
                    }
                    break;
                case 'resultsDirectory':
                    {
                        if (typeof OPTION !== 'string') {
                            throw new Error(ERROR_PREFIX + '"' + k + '" expects a '
                                + 'string (the path of an existing directory).');
                        }
                        const WRITE_TO_ROOT = OPTION === '' || OPTION === './';
                        const MESSAGE_PREFIX = 'Results will be written to ';
                        if (WRITE_TO_ROOT) {
                            console.log(MESSAGE_PREFIX + 'the current directory.');
                        }
                        else {
                            // confirm this is a valid path of an existing directory
                            // as opposed to a file
                            //TODO: Move these to FileIO (or FileIOSync).
                            //TODO: (low-pri) Have that lib create the directory, if it doesn't exist ... maybe.
                            if (!FS.existsSync(OPTION)) {
                                throw new Error(ERROR_PREFIX + '"' + k + '" path '
                                    + 'not found: "' + OPTION + '"');
                            }
                            const FILE_STAT = FS.lstatSync(OPTION);
                            if (!FILE_STAT.isDirectory()) {
                                throw new Error(ERROR_PREFIX + '"' + k + '" path '
                                    + 'is not a directory: "' + OPTION + '"');
                            }
                            console.log(MESSAGE_PREFIX + '"' + OPTION + '".');
                        }
                    }
                    break;
                case 'writeResultsAsCSV':
                    {
                        if (typeof OPTION !== 'boolean') {
                            throw new Error(ERROR_PREFIX + '"' + k + '" must be '
                                + 'true or false.');
                        }
                    }
                    break;
                /* istanbul ignore next */ //[FUTURE PROOFING]
                default: {
                    throw new Error('unsupported GridOptions key: ' + k);
                }
            }
        }
        // now merge the defaults into the user's options; any for which we
        // provide a value, but the user sent nothing
        for (const k in DEFAULT_OPTIONS) {
            if (userOptions[k] !== undefined) {
                continue;
            }
            userOptions[k] = DEFAULT_OPTIONS[k];
        }
        // now save this processed object; NOTE: It's not a private c'tor member
        // because it's optional
        this._options = userOptions;
    }
    /**
     * Returns the value for a setting key, or undefined, if none was supplied
     * to the constructor.
     * @param {string} key
     * @return {(string | number | boolean | undefined)}
     */
    GetOption(key) {
        switch (key) {
            case 'epochStatsDepth':
            case 'repetitions':
            case 'resultsDirectory':
            case 'validationSetSizeMin':
            case 'writeResultsAsCSV': {
                //NOTE: The value may be undefined. That's expected. We enforce that all
                //      user-supplied keys be known, but we do not require the user to set a
                //      value for every possible key. We have defaults for that case.
                return this._options[key];
            }
            default: {
                throw new Error('unknown option key: ' + key);
            }
        }
    }
}
GridOptions.DEFAULT_EPOCH_STATS_DEPTH = 5;
GridOptions.DEFAULT_REPETITIONS = 1;
GridOptions.DEFAULT_RESULTS_DIRECTORY = './';
GridOptions.DEFAULT_VALIDATION_SET_SIZE_MIN = 100;
GridOptions.DEFAULT_WRITE_RESULTS_AS_CSV = false;
const ALL_AVAILABLE_OPTIONS = {
    epochStatsDepth: null,
    repetitions: null,
    resultsDirectory: null,
    validationSetSizeMin: null,
    writeResultsAsCSV: null
};
Object.freeze(GridOptions);
export { GridOptions };
//# sourceMappingURL=GridOptions.js.map