finitedomain
Version:
A fast feature rich finite domain solver
1,268 lines (1,120 loc) • 49.2 kB
JavaScript
// Config for a search tree where each node is a Space
// TOFIX: may want to rename this to "tree-state" or something; it's not just config
// Note: all domains in this class should be array based!
// This prevents leaking the small domain artifact outside of the library.
import {
NO_SUCH_VALUE,
SUB,
SUP,
ASSERT,
ASSERT_NORDOM,
ASSERT_VARDOMS_SLOW,
THROW,
} from './helpers';
import {
TRIE_KEY_NOT_FOUND,
trie_add,
trie_create,
trie_get,
trie_has,
} from './trie';
import {
propagator_addDistinct,
propagator_addDiv,
propagator_addEq,
propagator_addGt,
propagator_addGte,
propagator_addLt,
propagator_addLte,
propagator_addMarkov,
propagator_addMul,
propagator_addNeq,
propagator_addPlus,
propagator_addMin,
propagator_addProduct,
propagator_addReified,
propagator_addRingMul,
propagator_addSum,
} from './propagator';
import {
NOT_FOUND,
domain__debug,
domain_createRange,
domain_createValue,
domain_getValue,
domain_max,
domain_min,
domain_mul,
domain_hasNoZero,
domain_intersection,
domain_isSolved,
domain_isZero,
domain_removeGte,
domain_removeLte,
domain_removeValue,
domain_resolveAsBooly,
domain_toSmallest,
domain_anyToSmallest,
} from './domain';
import domain_plus from './doms/domain_plus';
import {
constraint_create,
} from './constraint';
import distribution_getDefaults from './distribution/defaults';
// BODY_START
/**
* @returns {$config}
*/
function config_create() {
let config = {
_class: '$config',
// names of all vars in this search tree
allVarNames: [],
// doing `indexOf` for 5000+ names is _not_ fast. so use a trie
_varNamesTrie: trie_create(),
varStratConfig: config_createVarStratConfig(),
valueStratName: 'min',
targetedVars: 'all',
varDistOptions: {},
timeoutCallback: undefined,
// this is for the rng stuff in this library. in due time all calls
// should happen through this function. and it should be initialized
// with the rngCode string for exportability. this would be required
// for webworkers and DSL imports which can't have functions. tests
// can initialize it to something static, prod can use a seeded rng.
rngCode: '', // string. Function(rngCode) should return a callable rng
_defaultRng: undefined, // Function. if not exist at init time it'll be `rngCode ? Function(rngCode) : Math.random`
// the propagators are generated from the constraints when a space
// is created from this config. constraints are more higher level.
allConstraints: [],
constantCache: {}, // <value:varIndex>, generally anonymous vars but pretty much first come first serve
initialDomains: [], // $nordom[] : initial domains for each var, maps 1:1 to allVarNames
_propagators: [], // initialized later
_varToPropagators: [], // initialized later
_constrainedAway: [], // list of var names that were constrained but whose constraint was optimized away. they will still be "targeted" if target is all. TODO: fix all tests that depend on this and eliminate this. it is a hack.
_constraintHash: {}, // every constraint is logged here (note: for results only the actual constraints are stored). if it has a result, the value is the result var _name_. otherwise just `true` if it exists and `false` if it was optimized away.
};
ASSERT(!void (config._propagates = 0), 'number of propagate() calls');
return config;
}
function config_clone(config, newDomains) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
let {
varStratConfig,
valueStratName,
targetedVars,
varDistOptions,
timeoutCallback,
constantCache,
allVarNames,
allConstraints,
initialDomains,
_propagators,
_varToPropagators,
_constrainedAway,
} = config;
let clone = {
_class: '$config',
_varNamesTrie: trie_create(allVarNames), // just create a new trie with (should be) the same names
varStratConfig,
valueStratName,
targetedVars: targetedVars instanceof Array ? targetedVars.slice(0) : targetedVars,
varDistOptions: JSON.parse(JSON.stringify(varDistOptions)), // TOFIX: clone this more efficiently
timeoutCallback, // by reference because it's a function if passed on...
rngCode: config.rngCode,
_defaultRng: config.rngCode ? undefined : config._defaultRng,
constantCache, // is by reference ok?
allVarNames: allVarNames.slice(0),
allConstraints: allConstraints.slice(0),
initialDomains: newDomains ? newDomains.map(domain_toSmallest) : initialDomains, // <varName:domain>
_propagators: _propagators && _propagators.slice(0), // in case it is initialized
_varToPropagators: _varToPropagators && _varToPropagators.slice(0), // inited elsewhere
_constrainedAway: _constrainedAway && _constrainedAway.slice(0), // list of var names that were constrained but whose constraint was optimized away. they will still be "targeted" if target is all. TODO: fix all tests that depend on this and eliminate this. it is a hack.
// not sure what to do with this in the clone...
_constraintHash: {},
};
ASSERT(!void (clone._propagates = 0), 'number of propagate() calls');
return clone;
}
/**
* Add an anonymous var with max allowed range
*
* @param {$config} config
* @returns {number} varIndex
*/
function config_addVarAnonNothing(config) {
return config_addVarNothing(config, true);
}
/**
* @param {$config} config
* @param {string|boolean} varName (If true, is anonymous)
* @returns {number} varIndex
*/
function config_addVarNothing(config, varName) {
return _config_addVar(config, varName, domain_createRange(SUB, SUP));
}
/**
* @param {$config} config
* @param {number} lo
* @param {number} hi
* @returns {number} varIndex
*/
function config_addVarAnonRange(config, lo, hi) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof lo === 'number', 'A_LO_MUST_BE_NUMBER');
ASSERT(typeof hi === 'number', 'A_HI_MUST_BE_NUMBER');
if (lo === hi) return config_addVarAnonConstant(config, lo);
return config_addVarRange(config, true, lo, hi);
}
/**
* @param {$config} config
* @param {string|boolean} varName (If true, is anonymous)
* @param {number} lo
* @param {number} hi
* @returns {number} varIndex
*/
function config_addVarRange(config, varName, lo, hi) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof varName === 'string' || varName === true, 'A_VARNAME_SHOULD_BE_STRING_OR_TRUE');
ASSERT(typeof lo === 'number', 'A_LO_MUST_BE_NUMBER');
ASSERT(typeof hi === 'number', 'A_HI_MUST_BE_NUMBER');
ASSERT(lo <= hi, 'A_RANGES_SHOULD_ASCEND');
let domain = domain_createRange(lo, hi);
return _config_addVar(config, varName, domain);
}
/**
* @param {$config} config
* @param {string|boolean} varName (If true, anon)
* @param {$arrdom} domain Small domain format not allowed here. this func is intended to be called from Solver, which only accepts arrdoms
* @returns {number} varIndex
*/
function config_addVarDomain(config, varName, domain, _allowEmpty, _override) {
ASSERT(domain instanceof Array, 'DOMAIN_MUST_BE_ARRAY_HERE');
return _config_addVar(config, varName, domain_anyToSmallest(domain), _allowEmpty, _override);
}
/**
* @param {$config} config
* @param {number} value
* @returns {number} varIndex
*/
function config_addVarAnonConstant(config, value) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof value === 'number', 'A_VALUE_SHOULD_BE_NUMBER');
if (config.constantCache[value] !== undefined) {
return config.constantCache[value];
}
return config_addVarConstant(config, true, value);
}
/**
* @param {$config} config
* @param {string|boolean} varName (True means anon)
* @param {number} value
* @returns {number} varIndex
*/
function config_addVarConstant(config, varName, value) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof varName === 'string' || varName === true, 'varName must be a string or true for anon');
ASSERT(typeof value === 'number', 'A_VALUE_SHOULD_BE_NUMBER');
let domain = domain_createRange(value, value);
return _config_addVar(config, varName, domain);
}
/**
* @param {$config} config
* @param {string|true} varName If true, the varname will be the same as the index it gets on allVarNames
* @param {$nordom} domain
* @returns {number} varIndex
*/
function _config_addVar(config, varName, domain, _allowEmpty, _override = false) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(_allowEmpty || domain, 'NON_EMPTY_DOMAIN');
ASSERT(_allowEmpty || domain_min(domain) >= SUB, 'domain lo should be >= SUB', domain);
ASSERT(_allowEmpty || domain_max(domain) <= SUP, 'domain hi should be <= SUP', domain);
if (_override) {
ASSERT(trie_has(config._varNamesTrie, varName), 'Assuming var exists when explicitly overriding');
let index = trie_get(config._varNamesTrie, varName);
ASSERT(index >= 0, 'should exist');
ASSERT_NORDOM(domain, true, domain__debug);
config.initialDomains[index] = domain;
return;
}
let allVarNames = config.allVarNames;
let varIndex = allVarNames.length;
if (varName === true) {
varName = '__' + String(varIndex) + '__';
} else {
if (typeof varName !== 'string') THROW('Var names should be a string or anonymous, was: ' + JSON.stringify(varName));
if (!varName) THROW('Var name cannot be empty string');
if (String(parseInt(varName, 10)) === varName) THROW('Don\'t use numbers as var names (' + varName + ')');
}
// note: 100 is an arbitrary number but since large sets are probably
// automated it's very unlikely we'll need this check in those cases
if (varIndex < 100) {
if (trie_has(config._varNamesTrie, varName)) THROW('Var name already part of this config. Probably a bug?', varName);
}
let solvedTo = domain_getValue(domain);
if (solvedTo !== NOT_FOUND && !config.constantCache[solvedTo]) config.constantCache[solvedTo] = varIndex;
ASSERT_NORDOM(domain, true, domain__debug);
config.initialDomains[varIndex] = domain;
config.allVarNames.push(varName);
trie_add(config._varNamesTrie, varName, varIndex);
return varIndex;
}
/**
* Initialize the config of this space according to certain presets
*
* @param {$config} config
* @param {string} varName
*/
function config_setDefaults(config, varName) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
let defs = distribution_getDefaults(varName);
for (let key in defs) config_setOption(config, key, defs[key]);
}
/**
* Create a config object for the var distribution
*
* @param {Object} obj
* @property {string} [obj.type] Map to the internal names for var distribution strategies
* @property {string} [obj.priorityList] An ordered list of var names to prioritize. Names not in the list go implicitly and unordered last.
* @property {boolean} [obj.inverted] Should the list be interpreted inverted? Unmentioned names still go last, regardless.
* @property {Object} [obj.fallback] Same struct as obj. If current strategy is inconclusive it can fallback to another strategy.
* @returns {$var_strat_config}
*/
function config_createVarStratConfig(obj) {
/**
* @typedef {$var_strat_config}
*/
return {
_class: '$var_strat_config',
type: (obj && obj.type) || 'naive',
priorityByName: obj && obj.priorityList,
_priorityByIndex: undefined,
inverted: !!(obj && obj.inverted),
fallback: obj && obj.fallback,
};
}
/**
* Configure an option for the solver
*
* @param {$config} config
* @param {string} optionName
* @param {*} optionValue
* @param {string} [optionTarget] For certain options, this is the target var name
*/
function config_setOption(config, optionName, optionValue, optionTarget) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof optionName === 'string', 'option name is a string');
ASSERT(optionValue !== undefined, 'should get a value');
ASSERT(optionTarget === undefined || typeof optionTarget === 'string', 'the optional name is a string');
if (optionName === 'varStratOverride') {
THROW('deprecated, should be wiped internally');
}
switch (optionName) {
case 'varStrategy':
if (typeof optionValue === 'function') THROW('functions no longer supported', optionValue);
if (typeof optionValue === 'string') THROW('strings should be passed on as {type:value}', optionValue);
if (typeof optionValue !== 'object') THROW('varStrategy should be object', optionValue);
if (optionValue.name) THROW('name should be type');
if (optionValue.dist_name) THROW('dist_name should be type');
let vsc = config_createVarStratConfig(optionValue);
config.varStratConfig = vsc;
while (vsc.fallback) {
vsc.fallback = config_createVarStratConfig(vsc.fallback);
vsc = vsc.fallback;
}
break;
case 'valueStrategy':
// determine how the next value of a variable is picked when creating a new space
config.valueStratName = optionValue;
break;
case 'targeted_var_names':
if (!optionValue || !optionValue.length) THROW('ONLY_USE_WITH_SOME_TARGET_VARS'); // omit otherwise to target all
// which vars must be solved for this space to be solved
// string: 'all'
// string[]: list of vars that must be solved
// function: callback to return list of names to be solved
config.targetedVars = optionValue;
break;
case 'varStratOverrides':
// An object which defines a value distributor per variable
// which overrides the globally set value distributor.
// See Bvar#distributeOptions (in multiverse)
for (let key in optionValue) {
config_setOption(config, 'varValueStrat', optionValue[key], key);
}
break;
case 'varValueStrat':
// override all the specific strategy parameters for one variable
ASSERT(typeof optionTarget === 'string', 'expecting a name');
if (!config.varDistOptions) config.varDistOptions = {};
ASSERT(!config.varDistOptions[optionTarget], 'should not be known yet');
config.varDistOptions[optionTarget] = optionValue;
if (optionValue.valtype === 'markov') {
let matrix = optionValue.matrix;
if (!matrix) {
if (optionValue.expandVectorsWith) {
matrix = optionValue.matrix = [{vector: []}];
} else {
THROW('Solver: markov var missing distribution (needs matrix or expandVectorsWith)');
}
}
for (let i = 0, n = matrix.length; i < n; ++i) {
let row = matrix[i];
if (row.boolean) THROW('row.boolean was deprecated in favor of row.boolVarName');
if (row.booleanId !== undefined) THROW('row.booleanId is no longer used, please use row.boolVarName');
let boolFuncOrName = row.boolVarName;
if (typeof boolFuncOrName === 'function') {
boolFuncOrName = boolFuncOrName(optionValue);
}
if (boolFuncOrName) {
if (typeof boolFuncOrName !== 'string') {
THROW('row.boolVarName, if it exists, should be the name of a var or a func that returns that name, was/got: ' + boolFuncOrName + ' (' + typeof boolFuncOrName + ')');
}
// store the var index
row._boolVarIndex = trie_get(config._varNamesTrie, boolFuncOrName);
}
}
}
break;
case 'timeoutCallback':
// A function that returns true if the current search should stop
// Can be called multiple times after the search is stopped, should
// keep returning false (or assume an uncertain outcome).
// The function is called after the first batch of propagators is
// called so it won't immediately stop. But it stops quickly.
config.timeoutCallback = optionValue;
break;
case 'var': return THROW('REMOVED. Replace `var` with `varStrategy`');
case 'val': return THROW('REMOVED. Replace `var` with `valueStrategy`');
case 'rng':
// sets the default rng for this solve. a string should be raw js
// code, number will be a static return value, a function is used
// as is. the resulting function should return a value `0<=v<1`
if (typeof optionValue === 'string') {
config.rngCode = optionValue;
} else if (typeof optionValue === 'number') {
config.rngCode = 'return ' + optionValue + ';'; // dont use arrow function. i dont think this passes through babel.
} else {
ASSERT(typeof optionValue === 'function', 'rng should be a preferably a string and otherwise a function');
config._defaultRng = optionValue;
}
break;
default: THROW('unknown option');
}
}
/**
* This function should be removed once we can update mv
*
* @deprecated in favor of config_setOption
* @param {$config} config
* @param {Object} options
* @property {Object} [options.varStrategy]
* @property {string} [options.varStrategy.name]
* @property {string[]} [options.varStrategy.list] Only if name=list
* @property {string[]} [options.varStrategy.priorityList] Only if name=list
* @property {boolean} [options.varStrategy.inverted] Only if name=list
* @property {Object} [options.varStrategy.fallback] Same struct as options.varStrategy (recursive)
*/
function config_setOptions(config, options) {
if (!options) return;
if (options.varStrategy) config_setOption(config, 'varStrategy', options.varStrategy);
if (options.valueStrategy) config_setOption(config, 'valueStrategy', options.valueStrategy);
if (options.targeted_var_names) config_setOption(config, 'targeted_var_names', options.targeted_var_names);
if (options.varStratOverrides) config_setOption(config, 'varStratOverrides', options.varStratOverrides);
if (options.varStratOverride) {
console.warn('deprecated "varStratOverride" in favor of "varValueStrat"');
config_setOption(config, 'varValueStrat', options.varStratOverride, options.varStratOverrideName);
}
if (options.varValueStrat) config_setOption(config, 'varValueStrat', options.varValueStrat, options.varStratOverrideName);
if (options.timeoutCallback) config_setOption(config, 'timeoutCallback', options.timeoutCallback);
}
/**
* @param {$config} config
* @param {$propagator} propagator
*/
function config_addPropagator(config, propagator) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(propagator._class === '$propagator', 'EXPECTING_PROPAGATOR');
config._propagators.push(propagator);
}
/**
* Creates a mapping from a varIndex to a set of propagatorIndexes
* These propagators are the ones that use the varIndex
* This is useful for quickly determining which propagators
* need to be stepped while propagating them.
*
* @param {$config} config
*/
function config_populateVarPropHash(config) {
let hash = new Array(config.allVarNames.length);
let propagators = config._propagators;
let initialDomains = config.initialDomains;
for (let propagatorIndex = 0, plen = propagators.length; propagatorIndex < plen; ++propagatorIndex) {
let propagator = propagators[propagatorIndex];
_config_addVarConditionally(propagator.index1, initialDomains, hash, propagatorIndex);
if (propagator.index2 >= 0) _config_addVarConditionally(propagator.index2, initialDomains, hash, propagatorIndex);
if (propagator.index3 >= 0) _config_addVarConditionally(propagator.index3, initialDomains, hash, propagatorIndex);
}
config._varToPropagators = hash;
}
function _config_addVarConditionally(varIndex, initialDomains, hash, propagatorIndex) {
// (at some point this could be a strings, or array, or whatever)
ASSERT(typeof varIndex === 'number', 'must be number');
// dont bother adding props on unsolved vars because they can't affect
// anything anymore. seems to prevent about 10% in our case so worth it.
let domain = initialDomains[varIndex];
ASSERT_NORDOM(domain, true, domain__debug);
if (!domain_isSolved(domain)) {
if (!hash[varIndex]) hash[varIndex] = [propagatorIndex];
else if (hash[varIndex].indexOf(propagatorIndex) < 0) hash[varIndex].push(propagatorIndex);
}
}
/**
* Create a constraint. If the constraint has a result var it
* will return (only) the variable name that ends up being
* used (anonymous or not).
*
* In some edge cases the constraint can be resolved immediately.
* There are two ways a constraint can resolve: solved or reject.
* A solved constraint is omitted and if there is a result var it
* will become a constant that is set to the outcome of the
* constraint. If rejected the constraint will still be added and
* will immediately reject the search once it starts.
*
* Due to constant optimization and mapping the result var name
* may differ from the input var name. In that case both names
* should map to the same var index internally. Only constraints
* with a result var have a return value here.
*
* @param {$config} config
* @param {string} name Type of constraint (hardcoded values)
* @param {<string,number,undefined>[]} varNames All the argument var names for target constraint
* @param {string} [param] The result var name for certain. With reifiers param is the actual constraint to reflect.
* @returns {string|undefined} Actual result vars only, undefined otherwise. See desc above.
*/
function config_addConstraint(config, name, varNames, param) {
// should return a new var name for most props
ASSERT(config && config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(varNames.every(e => typeof e === 'string' || typeof e === 'number' || e === undefined), 'all var names should be strings or numbers or undefined', varNames);
let inputConstraintKeyOp = name;
let resultVarName;
let anonIsBool = false;
switch (name) { /* eslint no-fallthrough: "off" */
case 'reifier':
anonIsBool = true;
inputConstraintKeyOp = param;
// fall-through
case 'plus':
case 'min':
case 'ring-mul':
case 'ring-div':
case 'mul':
ASSERT(varNames.length === 3, 'MISSING_RESULT_VAR'); // note that the third value may still be "undefined"
// fall-through
case 'sum':
case 'product': {
let sumOrProduct = name === 'product' || name === 'sum';
resultVarName = sumOrProduct ? param : varNames[2];
let resultVarIndex;
if (resultVarName === undefined) {
if (anonIsBool) resultVarIndex = config_addVarAnonRange(config, 0, 1);
else resultVarIndex = config_addVarAnonNothing(config);
resultVarName = config.allVarNames[resultVarIndex];
} else if (typeof resultVarName === 'number') {
resultVarIndex = config_addVarAnonConstant(config, resultVarName);
resultVarName = config.allVarNames[resultVarIndex];
} else if (typeof resultVarName !== 'string') {
THROW(`expecting result var name to be absent or a number or string: \`${resultVarName}\``);
} else {
resultVarIndex = trie_get(config._varNamesTrie, resultVarName);
if (resultVarIndex < 0) THROW('Vars must be defined before using them (' + resultVarName + ')');
}
if (sumOrProduct) param = resultVarIndex;
else varNames[2] = resultVarName;
break;
}
case 'distinct':
case 'eq':
case 'neq':
case 'lt':
case 'lte':
case 'gt':
case 'gte':
break;
default:
THROW(`UNKNOWN_PROPAGATOR ${name}`);
}
// note: if param is a var constant then that case is already resolved above
config_compileConstants(config, varNames);
if (config_dedupeConstraint(config, inputConstraintKeyOp + '|' + varNames.join(','), resultVarName)) return resultVarName;
let varIndexes = config_varNamesToIndexes(config, varNames);
if (!config_solvedAtCompileTime(config, name, varIndexes, param)) {
let constraint = constraint_create(name, varIndexes, param);
config.allConstraints.push(constraint);
}
return resultVarName;
}
/**
* Go through the list of var names and create an anonymous var for
* each value that is actually a number rather than a string.
* Replaces the values inline.
*
* @param {$config} config
* @param {string|number} varNames
*/
function config_compileConstants(config, varNames) {
for (let i = 0, n = varNames.length; i < n; ++i) {
if (typeof varNames[i] === 'number') {
let varIndex = config_addVarAnonConstant(config, varNames[i]);
varNames[i] = config.allVarNames[varIndex];
}
}
}
/**
* Convert a list of var names to a list of their indexes
*
* @param {$config} config
* @param {string[]} varNames
* @returns {number[]}
*/
function config_varNamesToIndexes(config, varNames) {
let varIndexes = [];
for (let i = 0, n = varNames.length; i < n; ++i) {
let varName = varNames[i];
ASSERT(typeof varName === 'string', 'var names should be strings here', varName, i, varNames);
let varIndex = trie_get(config._varNamesTrie, varName);
ASSERT(varIndex !== TRIE_KEY_NOT_FOUND, 'CONSTRAINT_VARS_SHOULD_BE_DECLARED', 'name=', varName, 'index=', i, 'names=', varNames);
varIndexes[i] = varIndex;
}
return varIndexes;
}
/**
* Check whether we already know a given constraint (represented by a unique string).
* If we don't, add the string to the cache with the expected result name, if any.
*
* @param config
* @param constraintUI
* @param resultVarName
* @returns {boolean}
*/
function config_dedupeConstraint(config, constraintUI, resultVarName) {
if (!config._constraintHash) config._constraintHash = {}; // can happen for imported configs that are extended or smt
let haveConstraint = config._constraintHash[constraintUI];
if (haveConstraint === true) {
if (resultVarName !== undefined) {
throw new Error('How is this possible?'); // either a constraint-with-value gets a result var, or it's a constraint-sans-value
}
return true;
}
if (haveConstraint !== undefined) {
ASSERT(typeof haveConstraint === 'string', 'if not true or undefined, it should be a string');
ASSERT(resultVarName && typeof resultVarName === 'string', 'if it was recorded as a constraint-with-value then it should have a result var now as well');
// the constraint exists and had a result. map that result to this result for equivalent results.
config_addConstraint(config, 'eq', [resultVarName, haveConstraint]); // _could_ also be optimized away ;)
return true;
}
config._constraintHash[constraintUI] = resultVarName || true;
return false;
}
/**
* If either side of certain constraints are solved at compile time, which
* is right now, then the constraint should not be recorded at all because
* it will never "unsolve". This can cause vars to become rejected before
* the search even begins and that is okay.
*
* @param {$config} config
* @param {string} constraintName
* @param {number[]} varIndexes
* @param {*} [param] The extra parameter for constraints
* @returns {boolean}
*/
function config_solvedAtCompileTime(config, constraintName, varIndexes, param) {
if (constraintName === 'lte' || constraintName === 'lt') {
return _config_solvedAtCompileTimeLtLte(config, constraintName, varIndexes);
} else if (constraintName === 'gte' || constraintName === 'gt') {
return _config_solvedAtCompileTimeGtGte(config, constraintName, varIndexes);
} else if (constraintName === 'eq') {
return _config_solvedAtCompileTimeEq(config, constraintName, varIndexes);
} else if (constraintName === 'neq') {
return _config_solvedAtCompileTimeNeq(config, constraintName, varIndexes);
} else if (constraintName === 'reifier') {
return _config_solvedAtCompileTimeReifier(config, constraintName, varIndexes, param);
} else if (constraintName === 'sum') {
return _config_solvedAtCompileTimeSumProduct(config, constraintName, varIndexes, param);
} else if (constraintName === 'product') {
return _config_solvedAtCompileTimeSumProduct(config, constraintName, varIndexes, param);
}
return false;
}
function _config_solvedAtCompileTimeLtLte(config, constraintName, varIndexes) {
let initialDomains = config.initialDomains;
let varIndexLeft = varIndexes[0];
let varIndexRight = varIndexes[1];
let domainLeft = initialDomains[varIndexLeft];
let domainRight = initialDomains[varIndexRight];
ASSERT_NORDOM(domainLeft, true, domain__debug);
ASSERT_NORDOM(domainRight, true, domain__debug);
ASSERT(domainLeft && domainRight, 'NON_EMPTY_DOMAINS_EXPECTED'); // empty domains should be caught by addvar/decl
let v = domain_getValue(domainLeft);
if (v !== NO_SUCH_VALUE) {
let targetValue = v - (constraintName === 'lt' ? 0 : 1);
initialDomains[varIndexRight] = domain_removeLte(domainRight, targetValue);
// do not add constraint; this constraint is already solved
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
v = domain_getValue(domainRight);
if (v !== NO_SUCH_VALUE) {
let targetValue = v + (constraintName === 'lt' ? 0 : 1);
initialDomains[varIndexLeft] = domain_removeGte(domainLeft, targetValue);
// do not add constraint; this constraint is already solved
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
ASSERT(domainLeft, 'left should not be empty');
ASSERT(domainRight, 'right should not be empty');
let targetGte = domain_max(domainRight) + (constraintName === 'lt' ? 0 : 1);
let newLeft = initialDomains[varIndexLeft] = domain_removeGte(domainLeft, targetGte);
let targetLte = domain_min(domainLeft) - (constraintName === 'lt' ? 0 : 1);
let newRight = initialDomains[varIndexRight] = domain_removeLte(domainRight, targetLte);
if (domainLeft !== newLeft || domainRight !== newRight) return _config_solvedAtCompileTimeLtLte(config, constraintName, varIndexes);
return false;
}
function _config_solvedAtCompileTimeGtGte(config, constraintName, varIndexes) {
let initialDomains = config.initialDomains;
let varIndexLeft = varIndexes[0];
let varIndexRight = varIndexes[1];
let domainLeft = initialDomains[varIndexLeft];
let domainRight = initialDomains[varIndexRight];
ASSERT_NORDOM(domainLeft, true, domain__debug);
ASSERT_NORDOM(domainRight, true, domain__debug);
ASSERT(domainLeft && domainRight, 'NON_EMPTY_DOMAINS_EXPECTED'); // empty domains should be caught by addvar/decl
let v = domain_getValue(domainLeft);
if (v !== NO_SUCH_VALUE) {
let targetValue = v + (constraintName === 'gt' ? 0 : 1);
initialDomains[varIndexRight] = domain_removeGte(domainRight, targetValue, true);
// do not add constraint; this constraint is already solved
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
v = domain_getValue(domainRight);
if (v !== NO_SUCH_VALUE) {
let targetValue = v - (constraintName === 'gt' ? 0 : 1);
initialDomains[varIndexLeft] = domain_removeLte(domainLeft, targetValue);
// do not add constraint; this constraint is already solved
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
// A > B or A >= B. smallest number in A must be larger than the smallest number in B. largest number in B must be smaller than smallest number in A
let targetLte = domain_min(domainRight) - (constraintName === 'gt' ? 0 : 1);
let newLeft = initialDomains[varIndexLeft] = domain_removeLte(domainLeft, targetLte);
let targetGte = domain_max(domainLeft) + (constraintName === 'gt' ? 0 : 1);
let newRight = initialDomains[varIndexRight] = domain_removeGte(domainRight, targetGte);
// if the domains changed there's a chance this propagator is now removable
if (domainLeft !== newLeft || domainRight !== newRight) return _config_solvedAtCompileTimeGtGte(config, constraintName, varIndexes);
return false;
}
function _config_solvedAtCompileTimeEq(config, constraintName, varIndexes) {
let initialDomains = config.initialDomains;
let varIndexLeft = varIndexes[0];
let varIndexRight = varIndexes[1];
let a = initialDomains[varIndexLeft];
let b = initialDomains[varIndexRight];
let v = domain_getValue(a);
if (v === NO_SUCH_VALUE) v = domain_getValue(b);
if (v !== NO_SUCH_VALUE) {
let r = domain_intersection(a, b);
initialDomains[varIndexLeft] = r;
initialDomains[varIndexRight] = r;
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
return false;
}
function _config_solvedAtCompileTimeNeq(config, constraintName, varIndexes) {
let initialDomains = config.initialDomains;
let varIndexLeft = varIndexes[0];
let varIndexRight = varIndexes[1];
let v = domain_getValue(initialDomains[varIndexLeft]);
if (v !== NO_SUCH_VALUE) {
initialDomains[varIndexRight] = domain_removeValue(initialDomains[varIndexRight], v);
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
v = domain_getValue(initialDomains[varIndexRight]);
if (v !== NO_SUCH_VALUE) {
initialDomains[varIndexLeft] = domain_removeValue(initialDomains[varIndexLeft], v);
config._constrainedAway.push(varIndexLeft, varIndexRight);
return true;
}
return false;
}
function _config_solvedAtCompileTimeReifier(config, constraintName, varIndexes, opName) {
let initialDomains = config.initialDomains;
let varIndexLeft = varIndexes[0];
let varIndexRight = varIndexes[1];
let varIndexResult = varIndexes[2];
let domain1 = initialDomains[varIndexLeft];
let domain2 = initialDomains[varIndexRight];
let domain3 = initialDomains[varIndexResult];
ASSERT_NORDOM(domain1, true, domain__debug);
ASSERT_NORDOM(domain2, true, domain__debug);
ASSERT_NORDOM(domain3, true, domain__debug);
let v1 = domain_getValue(initialDomains[varIndexLeft]);
let v2 = domain_getValue(initialDomains[varIndexRight]);
let hasLeft = v1 !== NO_SUCH_VALUE;
let hasRight = v2 !== NO_SUCH_VALUE;
if (hasLeft && hasRight) { // just left or right would not force anything. but both does.
return _config_solvedAtCompileTimeReifierBoth(config, varIndexes, opName, v1, v2);
}
let resultIsFalsy = domain_isZero(domain3);
let resultIsTruthy = domain_hasNoZero(domain3);
if (resultIsFalsy !== resultIsTruthy) { // if it has either no zero or is zero then C is solved
if (hasLeft) {
// resolve right and eliminate reifier
return _config_solvedAtCompileTimeReifierLeft(config, opName, varIndexRight, v1, resultIsTruthy, domain1, domain2);
} else if (hasRight) {
// resolve right and eliminate reifier
return _config_solvedAtCompileTimeReifierRight(config, opName, varIndexLeft, v2, resultIsTruthy, domain1, domain2);
}
}
if (opName !== 'eq' && opName !== 'neq') {
// must be lt lte gt gte. these are solved completely when either param is solved
ASSERT(opName === 'lt' || opName === 'lte' || opName === 'gt' || opName === 'gte', 'should be lt lte gt gte now because there are no other reifiers atm');
const PASSED = true;
const FAILED = false;
if (opName === 'lt') {
// A < B. solved if max(A) < min(B). rejected if min(A) >= max(B)
if (domain_max(domain1) < domain_min(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, PASSED);
if (domain_min(domain1) >= domain_max(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, FAILED);
} else if (opName === 'lte') {
// A <= B. solved if max(A) <= min(B). rejected if min(A) > max(B)
if (domain_max(domain1) <= domain_min(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, PASSED);
if (domain_min(domain1) > domain_max(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, FAILED);
} else if (opName === 'gt') {
// A > B. solved if min(A) > max(B). rejected if max(A) <= min(B)
if (domain_min(domain1) > domain_max(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, PASSED);
if (domain_max(domain1) <= domain_min(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, FAILED);
} else if (opName === 'gte') {
// A > B. solved if min(A) >= max(B). rejected if max(A) < min(B)
if (domain_min(domain1) >= domain_max(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, PASSED);
if (domain_max(domain1) < domain_min(domain2)) return _config_eliminateReifier(config, varIndexLeft, varIndexRight, varIndexResult, domain3, FAILED);
} else {
THROW('UNKNOWN_OP');
}
}
return false;
}
function _config_eliminateReifier(config, leftVarIndex, rightVarIndex, resultVarIndex, resultDomain, result) {
config.initialDomains[resultVarIndex] = domain_resolveAsBooly(resultDomain, result);
config._constrainedAway.push(leftVarIndex, rightVarIndex, resultVarIndex);
return true;
}
function _config_solvedAtCompileTimeReifierBoth(config, varIndexes, opName, v1, v2) {
let initialDomains = config.initialDomains;
let varIndexResult = varIndexes[2];
let bool = false;
switch (opName) {
case 'lt':
bool = v1 < v2;
break;
case 'lte':
bool = v1 <= v2;
break;
case 'gt':
bool = v1 > v2;
break;
case 'gte':
bool = v1 >= v2;
break;
case 'eq':
bool = v1 === v2;
break;
case 'neq':
bool = v1 !== v2;
break;
default:
return false;
}
initialDomains[varIndexResult] = domain_resolveAsBooly(initialDomains[varIndexResult], bool);
config._constrainedAway.push(varIndexResult); // note: left and right have been solved already so no need to push those here
return true;
}
function _config_solvedAtCompileTimeReifierLeft(config, opName, varIndex, value, result, domain1, domain2) {
let initialDomains = config.initialDomains;
let domain = initialDomains[varIndex];
switch (opName) {
case 'lt':
if (result) domain = domain_removeLte(domain, value);
else domain = domain_removeGte(domain, value + 1);
break;
case 'lte':
if (result) domain = domain_removeLte(domain, value - 1);
else domain = domain_removeGte(domain, value);
break;
case 'gt':
if (result) domain = domain_removeGte(domain, value);
else domain = domain_removeLte(domain, value - 1);
break;
case 'gte':
if (result) domain = domain_removeGte(domain, value + 1);
else domain = domain_removeLte(domain, value);
break;
case 'eq':
if (result) domain = domain_intersection(domain1, domain2);
else domain = domain_removeValue(domain, value);
break;
case 'neq':
if (result) domain = domain_removeValue(domain, value);
else domain = domain_intersection(domain1, domain2);
break;
default:
return false;
}
ASSERT_NORDOM(domain, true, domain__debug);
initialDomains[varIndex] = domain;
config._constrainedAway.push(varIndex); // note: left and result have been solved already so no need to push those here
return true;
}
function _config_solvedAtCompileTimeReifierRight(config, opName, varIndex, value, result, domain1, domain2) {
let initialDomains = config.initialDomains;
let domain = initialDomains[varIndex];
switch (opName) {
case 'lt':
if (result) domain = domain_removeGte(domain, value);
else domain = domain_removeLte(domain, value - 1);
break;
case 'lte':
if (result) domain = domain_removeGte(domain, value + 1);
else domain = domain_removeLte(domain, value);
break;
case 'gt':
if (result) domain = domain_removeLte(domain, value);
else domain = domain_removeGte(domain, value + 1);
break;
case 'gte':
if (result) domain = domain_removeLte(domain, value - 1);
else domain = domain_removeGte(domain, value);
break;
case 'eq':
if (result) domain = domain_intersection(domain1, domain2);
else domain = domain_removeValue(domain, value);
break;
case 'neq':
if (result) domain = domain_removeValue(domain, value);
else domain = domain_intersection(domain1, domain2);
break;
default:
return false;
}
ASSERT_NORDOM(domain, true, domain__debug);
initialDomains[varIndex] = domain;
config._constrainedAway.push(varIndex); // note: right and result have been solved already so no need to push those here
return true;
}
function _config_solvedAtCompileTimeSumProduct(config, constraintName, varIndexes, resultIndex) {
ASSERT(constraintName === 'sum' || constraintName === 'product', 'if this changes update the function accordingly');
let initialDomains = config.initialDomains;
// for product, multiply by 1 to get identity. for sum it's add 0 for identity.
const SUM_IDENT = 0;
const PROD_IDENT = 1;
const IDENT = constraintName === 'product' ? PROD_IDENT : SUM_IDENT;
// if there are no vars then the next step would fail. could happen as an artifact.
if (initialDomains.length && varIndexes.length) {
// limit result var to the min/max possible sum
let maxDomain = initialDomains[varIndexes[0]]; // dont start with EMPTY or [0,0]!
for (let i = 1, n = varIndexes.length; i < n; ++i) {
let varIndex = varIndexes[i];
let domain = initialDomains[varIndex];
if (constraintName === 'sum') maxDomain = domain_plus(maxDomain, domain);
else maxDomain = domain_mul(maxDomain, domain);
}
initialDomains[resultIndex] = domain_intersection(maxDomain, initialDomains[resultIndex]);
}
// eliminate multiple constants
if (varIndexes.length > 1) {
let newVarIndexes = [];
let total = IDENT;
for (let i = 0, n = varIndexes.length; i < n; ++i) {
let varIndex = varIndexes[i];
let domain = initialDomains[varIndex];
let value = domain_getValue(domain);
if (value === NO_SUCH_VALUE) {
newVarIndexes.push(varIndex);
} else if (constraintName === 'sum') {
total += value;
} else if (constraintName === 'product') {
total *= value;
}
}
// we cant just remove constants from the result like a math equation; different paradigms
// if there are no vars left then the result must equal the constant (put it back in the list, even if identity)
if (!newVarIndexes.length || (constraintName === 'sum' && total !== SUM_IDENT) || (constraintName === 'product' && total !== PROD_IDENT)) {
let varIndex = config_addVarAnonConstant(config, total);
newVarIndexes.push(varIndex);
}
// copy new list inline
for (let i = 0, n = newVarIndexes.length; i < n; ++i) {
varIndexes[i] = newVarIndexes[i];
}
varIndexes.length = newVarIndexes.length;
}
// shouldnt be zero here unless it was declared empty
if (varIndexes.length === 0) {
// TOFIX: should a product without args equal 1 or 0? currently we set it to 0 for both sum/product
config.initialDomains[resultIndex] = domain_intersection(config.initialDomains[resultIndex], domain_createValue(0));
return true;
}
if (varIndexes.length === 1) {
// both in the case of sum and product, if there is only one value in the set, the result must be that value
// so here we do an intersect that one value with the result because that's what must happen anyways
let domain = domain_intersection(config.initialDomains[resultIndex], config.initialDomains[varIndexes[0]]);
config.initialDomains[resultIndex] = domain;
config.initialDomains[varIndexes[0]] = domain;
if (domain_isSolved(domain)) {
config._constrainedAway.push(varIndexes[0], resultIndex);
return true;
}
// cant eliminate constraint; sum will compile an `eq` for it.
}
return false;
}
/**
* Generate all propagators from the constraints in given config
* Puts these back into the same config.
*
* @param {$config} config
*/
function config_generatePropagators(config) {
ASSERT(config && config._class === '$config', 'EXPECTING_CONFIG');
let constraints = config.allConstraints;
config._propagators = [];
for (let i = 0, n = constraints.length; i < n; ++i) {
let constraint = constraints[i];
if (constraint.varNames) {
console.warn('saw constraint.varNames, converting to varIndexes, log out result and update test accordingly');
constraint.varIndexes = constraint.varNames.map(name => trie_get(config._varNamesTrie, name));
let p = constraint.param;
delete constraint.param;
delete constraint.varNames;
constraint.param = p;
}
config_generatePropagator(config, constraint.name, constraint.varIndexes, constraint.param, constraint);
}
}
/**
* @param {$config} config
* @param {string} name
* @param {number[]} varIndexes
* @param {string|undefined} param Depends on the prop; reifier=op name, product/sum=result var
*/
function config_generatePropagator(config, name, varIndexes, param, _constraint) {
ASSERT(config && config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof name === 'string', 'NAME_SHOULD_BE_STRING');
ASSERT(varIndexes instanceof Array, 'INDEXES_SHOULD_BE_ARRAY', JSON.stringify(_constraint));
switch (name) {
case 'plus':
return propagator_addPlus(config, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'min':
return propagator_addMin(config, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'ring-mul':
return propagator_addRingMul(config, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'ring-div':
return propagator_addDiv(config, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'mul':
return propagator_addMul(config, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'sum':
return propagator_addSum(config, varIndexes.slice(0), param);
case 'product':
return propagator_addProduct(config, varIndexes.slice(0), param);
case 'distinct':
return propagator_addDistinct(config, varIndexes.slice(0));
case 'reifier':
return propagator_addReified(config, param, varIndexes[0], varIndexes[1], varIndexes[2]);
case 'neq':
return propagator_addNeq(config, varIndexes[0], varIndexes[1]);
case 'eq':
return propagator_addEq(config, varIndexes[0], varIndexes[1]);
case 'gte':
return propagator_addGte(config, varIndexes[0], varIndexes[1]);
case 'lte':
return propagator_addLte(config, varIndexes[0], varIndexes[1]);
case 'gt':
return propagator_addGt(config, varIndexes[0], varIndexes[1]);
case 'lt':
return propagator_addLt(config, varIndexes[0], varIndexes[1]);
default:
THROW('UNEXPECTED_NAME: ' + name);
}
}
function config_generateMarkovs(config) {
let varDistOptions = config.varDistOptions;
for (let varName in varDistOptions) {
let varIndex = trie_get(config._varNamesTrie, varName);
if (varIndex < 0) THROW('Found markov var options for an unknown var name (' + varName + ')');
let options = varDistOptions[varName];
if (options && options.valtype === 'markov') {
return propagator_addMarkov(config, varIndex);
}
}
}
function config_populateVarStrategyListHash(config) {
let vsc = config.varStratConfig;
while (vsc) {
if (vsc.priorityByName) {
let obj = {};
let list = vsc.priorityByName;
for (let i = 0, len = list.length; i < len; ++i) {
let varIndex = trie_get(config._varNamesTrie, list[i]);
ASSERT(varIndex !== TRIE_KEY_NOT_FOUND, 'VARS_IN_PRIO_LIST_SHOULD_BE_KNOWN_NOW');
obj[varIndex] = len - i; // never 0, offset at 1. higher value is higher prio
}
vsc._priorityByIndex = obj;
}
vsc = vsc.fallback;
}
}
/**
* At the start of a search, populate this config with the dynamic data
*
* @param {$config} config
*/
function config_init(config) {
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
if (!config._varNamesTrie) {
config._varNamesTrie = trie_create(config.allVarNames);
}
// Generate the default rng ("Random Number Generator") to use in stuff like markov
// We prefer the rngCode because that way we can serialize the config (required for stuff like webworkers)
if (!config._defaultRng) config._defaultRng = config.rngCode ? Function(config.rngCode) : Math.random; /* eslint no-new-func: "off" */
ASSERT_VARDOMS_SLOW(config.initialDomains, domain__debug);
config_generatePropagators(config);
config_generateMarkovs(config);
config_populateVarPropHash(config);
config_populateVarStrategyListHash(config);
ASSERT_VARDOMS_SLOW(config.initialDomains, domain__debug);
ASSERT(config._varToPropagators, 'should have generated hash');
}
// BODY_STOP
export {
config_addConstraint,
config_addPropagator,
config_addVarAnonConstant,
config_addVarAnonNothing,
config_addVarAnonRange,
config_addVarConstant,
config_addVarDomain,
config_addVarNothing,
config_addVarRange,
config_clone,
config_create,
config_createVarStratConfig,
config_generatePropagators,
config_init,
config_populateVarPropHash,
config_setDefaults,
config_setOption,
config_setOptions,
// testing
_config_addVar,
};