fdo
Version:
A brute force Finite Domain Solver
873 lines (763 loc) • 32.4 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 {
SUB,
SUP,
ASSERT,
ASSERT_NORDOM,
ASSERT_VARDOMS_SLOW,
getTerm,
INSPECT,
THROW,
} from '../../fdlib/src/helpers';
import {
TRIE_KEY_NOT_FOUND,
trie_add,
trie_create,
trie_get,
trie_has,
} from '../../fdlib/src/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_getValue,
domain_max,
domain_min,
domain_isSolved,
domain_toSmallest,
domain_anyToSmallest,
} from '../../fdlib/src/domain';
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: {},
beforeSpace: undefined,
afterSpace: 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,
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
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 FDO, 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.priorityByName] 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.priorityByName,
_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');
}
let fallback = false;
switch (optionName) {
case 'varStrategyFallback':
fallback = true;
// fall-through
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');
ASSERT(!optionTarget, 'optionTarget is not used for varStrategy (this is not "per-var strat")');
let vsc = config_createVarStratConfig(optionValue);
if (fallback) {
let rvsc = config.varStratConfig;
ASSERT(rvsc, 'there must be an existing config to add a fallback');
while (rvsc.fallback) rvsc = rvsc.fallback;
rvsc.fallback = vsc;
} else {
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('FDO: 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;
// Hooks called before and after propagating each space.
// The callback receives the targeted Space object.
// If it returns truthy it immediately aborts the search entirely.
// (Can be used for timeout, inspection, or manual selection)
case 'beforeSpace':
config.beforeSpace = optionValue;
break;
case 'afterSpace':
config.afterSpace = 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.priorityByName] Only if name=list
* @property {boolean} [options.varStrategy.inverted] Only if name=list
* @property {Object} [options.varStrategy.fallback] Same struct as options.varStrategy (recursive)
* @property {Function} [options.beforeSpace] To be called before each Space propagation
* @property {Function} [options.afterSpace] To be called after each Space propagation
*/
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) {
getTerm().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.beforeSpace) config_setOption(config, 'beforeSpace', options.beforeSpace);
if (options.afterSpace) config_setOption(config, 'afterSpace', options.afterSpace);
}
/**
* @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);
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);
if (varIndex === TRIE_KEY_NOT_FOUND) THROW('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;
}
/**
* 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) {
getTerm().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;
}
if (constraint.varIndexes[1] === -1) throw new Error('nope? ' + INSPECT(constraint));
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 (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,
};