UNPKG

finitedomain

Version:

A fast feature rich finite domain solver

557 lines (481 loc) 18.2 kB
import { LOG_FLAG_PROPSTEPS, NO_SUCH_VALUE, ASSERT, ASSERT_LOG, ASSERT_NORDOM, THROW, } from './helpers'; import { TRIE_EMPTY, TRIE_KEY_NOT_FOUND, TRIE_NODE_SIZE, trie_addNum, trie_create, trie_get, trie_getNum, } from './trie'; import { config_clone, } from './config'; import { domain__debug, domain_getValue, domain_isEmpty, domain_isSolved, domain_toArr, domain_toSmallest, domain_toStr, } from './domain'; // BODY_START let space_uid = 0; /** * @returns {$space} */ function space_createRoot() { // only for debugging let _depth = 0; let _child = 0; let _path = ''; ASSERT(!(space_uid = 0)); return space_createNew([], undefined, _depth, _child, _path); } /** * @param {$config} config * @returns {$space} */ function space_createFromConfig(config) { ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let space = space_createRoot(); space_initFromConfig(space, config); return space; } /** * Create a space node that is a child of given space node * * @param {$space} space * @returns {$space} */ function space_createClone(space) { ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE'); let vardomsCopy = space.vardoms.slice(0); let unsolvedVarIndexes = space._unsolved.slice(0); // only for debugging let _depth; let _child; let _path; // do it inside ASSERTs so they are eliminated in the dist ASSERT(!void (_depth = space._depth + 1)); ASSERT(!void (_child = space._child_count++)); ASSERT(!void (_path = space._path)); return space_createNew(vardomsCopy, unsolvedVarIndexes, _depth, _child, _path); } /** * Create a new config with the configuration of the given Space * Basically clones its config but updates the `initialDomains` with fresh state * * @param {$space} space * @param {$config} config * @returns {$space} */ function space_toConfig(space, config) { ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let vardoms = space.vardoms; let newDomains = []; let names = config.allVarNames; for (let i = 0, n = names.length; i < n; i++) { let domain = vardoms[i]; newDomains[i] = domain_toStr(domain); } return config_clone(config, undefined, newDomains); } /** * Concept of a space that holds config, some named domains (referred to as "vars"), and some propagators * * @param {$domain[]} vardoms Maps 1:1 to config.allVarNames * @param {number[]|undefined} unsolvedVarIndexes * @param {number} _depth (Debugging only) How many parent nodes are there from this node? * @param {number} _child (Debugging only) How manieth child is this of the parent? * @param {string} _path (Debugging only) String of _child values from root to this node (should be unique per node and len=_depth+1) * @returns {$space} */ function space_createNew(vardoms, unsolvedVarIndexes, _depth, _child, _path) { ASSERT(typeof vardoms === 'object' && vardoms, 'vars should be an object', vardoms); let space = { _class: '$space', vardoms, _unsolved: unsolvedVarIndexes, next_distribution_choice: 0, updatedVarIndex: -1, // the varIndex that was updated when creating this space (-1 for root) _lastChosenValue: -1, // cache to prevent duplicate operations }; // search graph metrics // debug only. do it inside ASSERT so they are stripped in the dist ASSERT(!void (space._depth = _depth)); ASSERT(!void (space._child = _child)); ASSERT(!void (space._child_count = 0)); ASSERT(!void (space._path = _path + _child)); ASSERT(!void (space._uid = ++space_uid)); // this will not hold in distributed solving... return space; } /** * @param {$space} space * @param {$config} config */ function space_initFromConfig(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); space_generateVars(space, config); // config must be initialized (generating propas may introduce fresh vars) space_initializeUnsolvedVars(space, config); } /** * Return the current number of unsolved vars for given space. * This is only used for testing, prevents leaking internals into tests * * @param {$space} space * @param {$config} config * @returns {number} */ function space_getUnsolvedVarCount(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); return space._unsolved.length; } /** * Only use this for testing or debugging as it creates a fresh array * for the result. We don't use the names internally, anyways. * * @param {$space} space * @param {$config} config * @returns {string[]} var names of all unsolved vars of given space */ function _space_getUnsolvedVarNamesFresh(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); return space._unsolved.map(varIndex => config.allVarNames[varIndex]); } /** * Initialized the list of unsolved variables. These are either the explicitly * targeted variables, or any unsolved variables if none were explicitly targeted. * * @param {$space} space * @param {$config} config */ function space_initializeUnsolvedVars(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let targetVarNames = config.targetedVars; let vardoms = space.vardoms; let unsolvedVarIndexes = []; space._unsolved = unsolvedVarIndexes; if (targetVarNames === 'all') { for (let varIndex = 0, n = vardoms.length; varIndex < n; ++varIndex) { if (!domain_isSolved(vardoms[varIndex])) { if (config._varToPropagators[varIndex] || (config._constrainedAway && config._constrainedAway.indexOf(varIndex) >= 0)) { unsolvedVarIndexes.push(varIndex); } } } } else { ASSERT(targetVarNames instanceof Array, 'expecting targetVarNames to be an array or the string `all`', targetVarNames); ASSERT(targetVarNames.every(e => typeof e === 'string'), 'you must target var names only, they must all be strings', targetVarNames); let varNamesTrie = config._varNamesTrie; for (let i = 0, n = targetVarNames.length; i < n; ++i) { let varName = targetVarNames[i]; let varIndex = trie_get(varNamesTrie, varName); if (varIndex === TRIE_KEY_NOT_FOUND) THROW('E_TARGETED_VARS_SHOULD_EXIST_NOW [' + varName + ']'); if (!domain_isSolved(vardoms[varIndex])) { unsolvedVarIndexes.push(varIndex); } } } } /** * Run all the propagators until stability point. * Returns true if any propagator rejects. * * @param {$space} space * @param {$config} config * @returns {boolean} when true, a propagator rejects and the (current path to a) solution is invalid */ function space_propagate(space, config) { ASSERT_LOG(LOG_FLAG_PROPSTEPS, log => log('space_propagate()')); ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); ASSERT(!void (config._propagates = (config._propagates | 0) + 1), 'number of calls to space_propagate'); let propagators = config._propagators; // "cycle" is one step, "epoch" all steps until stable (but not solved per se) // worst case all unsolved vars change. but in general it's about 30% so run with that let cells = Math.ceil(space._unsolved.length * TRIE_NODE_SIZE * 0.3); let changedTrie = trie_create(TRIE_EMPTY, cells); // track changed vars per cycle in this epoch let cycles = 0; ASSERT(typeof cycles === 'number', 'cycles is a number?'); ASSERT(changedTrie._class === '$trie', 'trie is a trie?'); let changedVars = []; // in one cycle let minimal = 1; if (space.updatedVarIndex >= 0) { changedVars.push(space.updatedVarIndex); } else { // very first cycle of first epoch of the search. all propagators must be visited at least once now. let rejected = space_propagateAll(space, config, propagators, changedVars, changedTrie, ++cycles); if (rejected) { return true; } } if (space_abortSearch(space, config)) { return true; } let returnValue = false; while (changedVars.length) { let newChangedVars = []; let rejected = space_propagateChanges(space, config, propagators, minimal, changedVars, newChangedVars, changedTrie, ++cycles); if (rejected) { returnValue = true; break; } if (space_abortSearch(space, config)) { returnValue = true; break; } changedVars = newChangedVars; minimal = 2; // see space_propagateChanges } return returnValue; } function space_propagateAll(space, config, propagators, changedVars, changedTrie, cycleIndex) { ASSERT_LOG(LOG_FLAG_PROPSTEPS, log => log('space_propagateAll(' + propagators.length + 'x)')); for (let i = 0, n = propagators.length; i < n; i++) { let propagator = propagators[i]; let rejected = space_propagateStep(space, config, propagator, changedVars, changedTrie, cycleIndex); if (rejected) return true; } return false; } function space_propagateByIndexes(space, config, propagators, propagatorIndexes, changedVars, changedTrie, cycleIndex) { ASSERT_LOG(LOG_FLAG_PROPSTEPS, log => log('space_propagateByIndexes(' + propagators.length + 'x)')); for (let i = 0, n = propagatorIndexes.length; i < n; i++) { let propagatorIndex = propagatorIndexes[i]; let propagator = propagators[propagatorIndex]; let rejected = space_propagateStep(space, config, propagator, changedVars, changedTrie, cycleIndex); if (rejected) return true; } return false; } function space_propagateStep(space, config, propagator, changedVars, changedTrie, cycleIndex) { ASSERT(propagator._class === '$propagator', 'EXPECTING_PROPAGATOR'); let vardoms = space.vardoms; let index1 = propagator.index1; let index2 = propagator.index2; let index3 = propagator.index3; ASSERT(index1 !== 'undefined', 'all props at least use the first var...'); let domain1 = vardoms[index1]; let domain2 = index2 !== undefined && vardoms[index2]; let domain3 = index3 !== undefined && vardoms[index3]; ASSERT_NORDOM(domain1, true, domain__debug); ASSERT(domain2 === undefined || ASSERT_NORDOM(domain2, true, domain__debug)); ASSERT(domain3 === undefined || ASSERT_NORDOM(domain3, true, domain__debug)); let stepper = propagator.stepper; ASSERT(typeof stepper === 'function', 'stepper should be a func'); // TODO: if we can get a "solved" state here we can prevent an isSolved check later... stepper(space, config, index1, index2, index3, propagator.arg1, propagator.arg2, propagator.arg3, propagator.arg4, propagator.arg5, propagator.arg6); if (domain1 !== vardoms[index1]) { if (domain_isEmpty(vardoms[index1])) { return true; // fail } space_recordChange(index1, changedTrie, changedVars, cycleIndex); } if (index2 !== undefined && domain2 !== vardoms[index2]) { if (domain_isEmpty(vardoms[index2])) { return true; // fail } space_recordChange(index2, changedTrie, changedVars, cycleIndex); } if (index3 !== undefined && domain3 !== vardoms[index3]) { if (domain_isEmpty(vardoms[index3])) { return true; // fail } space_recordChange(index3, changedTrie, changedVars, cycleIndex); } return false; } function space_recordChange(varIndex, changedTrie, changedVars, cycleIndex) { if (typeof varIndex === 'number') { let status = trie_getNum(changedTrie, varIndex); if (status !== cycleIndex) { changedVars.push(varIndex); trie_addNum(changedTrie, varIndex, cycleIndex); } } else { ASSERT(varIndex instanceof Array, 'index1 is always used'); for (let i = 0, len = varIndex.length; i < len; ++i) { space_recordChange(varIndex[i], changedTrie, changedVars, cycleIndex); } } } function space_propagateChanges(space, config, allPropagators, minimal, targetVars, changedVars, changedTrie, cycleIndex) { ASSERT_LOG(LOG_FLAG_PROPSTEPS, log => log('space_propagateChanges(' + changedVars.length + 'x)')); ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let varToPropagators = config._varToPropagators; for (let i = 0, vlen = targetVars.length; i < vlen; i++) { let propagatorIndexes = varToPropagators[targetVars[i]]; // note: the first loop of propagate() should require all propagators affected, even if // it is just one. after that, if a var was updated that only has one propagator it can // only have been updated by that one propagator. however, this step is queueing up // propagators to check, again, since one of its vars changed. a propagator that runs // twice without other changes will change nothing. so we do it for the initial loop, // where the var is updated externally, after that the change can only occur from within // a propagator so we skip it. // ultimately a list of propagators should perform better but the indexOf negates that perf // (this doesn't affect a whole lot of vars... most of them touch multiple propas) if (propagatorIndexes && propagatorIndexes.length >= minimal) { let result = space_propagateByIndexes(space, config, allPropagators, propagatorIndexes, changedVars, changedTrie, cycleIndex); if (result) return true; // rejected } } return false; } /** * @param {$space} space * @param {$config} config * @returns {boolean} */ function space_abortSearch(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let callback = config.timeoutCallback; if (callback) { return callback(space); } return false; } /** * Returns true if this space is solved - i.e. when * all the vars in the space have a singleton domain. * * This is a *very* strong condition that might not need * to be satisfied for a space to be considered to be * solved. For example, the propagators may determine * ranges for all variables under which all conditions * are met and there would be no further need to enumerate * those solutions. * * @param {$space} space * @param {$config} config * @returns {boolean} */ function space_updateUnsolvedVarList(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let vardoms = space.vardoms; let unsolvedVarIndexes = space._unsolved; let m = 0; for (let i = 0, len = unsolvedVarIndexes.length; i < len; ++i) { let varIndex = unsolvedVarIndexes[i]; let domain = vardoms[varIndex]; if (!domain_isSolved(domain)) { unsolvedVarIndexes[m++] = varIndex; } } unsolvedVarIndexes.length = m; return m === 0; // 0 unsolved means we've solved it :) } /** * Returns an object whose field names are the var names * and whose values are the solved values. The space *must* * be already in a solved state for this to work. * * @param {$space} space * @param {$config} config * @returns {Object} */ function space_solution(space, config) { ASSERT(space._class === '$space', 'EXPECTING_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let allVarNames = config.allVarNames; let result = {}; for (let varIndex = 0, n = allVarNames.length; varIndex < n; varIndex++) { let varName = allVarNames[varIndex]; result[varName] = space_getVarSolveState(space, varIndex); } return result; } /** * Note: this is the (shared) second most called function of the library * (by a third of most, but still significantly more than the rest) * * @param {$space} space * @param {number} varIndex * @returns {number|number[]|boolean} The solve state for given var index, also put into result */ function space_getVarSolveState(space, varIndex) { ASSERT(typeof varIndex === 'number', 'VAR_SHOULD_BE_INDEX'); let domain = space.vardoms[varIndex]; if (domain_isEmpty(domain)) { return false; } let value = domain_getValue(domain); if (value !== NO_SUCH_VALUE) return value; return domain_toArr(domain); } function space_getDomainArr(space, varIndex) { return domain_toArr(space.vardoms[varIndex]); } /** * Initialize the vardoms array on the first space node. * * @param {$space} space * @param {$config} config */ function space_generateVars(space, config) { ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE'); ASSERT(config._class === '$config', 'EXPECTING_CONFIG'); let vardoms = space.vardoms; ASSERT(vardoms, 'expecting var domains'); let initialDomains = config.initialDomains; ASSERT(initialDomains, 'config should have initial vars'); let allVarNames = config.allVarNames; ASSERT(allVarNames, 'config should have a list of vars'); for (let varIndex = 0, len = allVarNames.length; varIndex < len; varIndex++) { let domain = initialDomains[varIndex]; ASSERT_NORDOM(domain, true, domain__debug); vardoms[varIndex] = domain_toSmallest(domain); } } /** * @param {$space} space * @param {$config} [config] * @param {boolean} [printPath] */ function _space_debug(space, config, printPath) { console.log('\n## Space:'); // __REMOVE_BELOW_FOR_ASSERTS__ console.log('# Meta:'); console.log('uid:', space._uid); console.log('depth:', space._depth); console.log('child:', space._child); console.log('children:', space._child_count); if (printPath) console.log('path:', space._path); // __REMOVE_ABOVE_FOR_ASSERTS__ console.log('# Domains:'); console.log(space.vardoms.map(domain_toArr).map((d, i) => (d + '').padEnd(15, ' ') + ((!config || config.allVarNames[i] === String(i)) ? '' : ' (' + config.allVarNames[i] + ')')).join('\n')); console.log('##\n'); } // BODY_STOP export { space_createClone, space_createFromConfig, space_createRoot, space_generateVars, space_getDomainArr, space_getUnsolvedVarCount, _space_getUnsolvedVarNamesFresh, space_getVarSolveState, space_initFromConfig, space_updateUnsolvedVarList, space_propagate, space_solution, space_toConfig, _space_debug, };