UNPKG

finitedomain

Version:

A fast feature rich finite domain solver

840 lines (734 loc) 30.5 kB
import { LOG_NONE, LOG_STATS, LOG_SOLVES, LOG_MAX, LOG_MIN, SUB, SUP, ASSERT, ASSERT_ARRDOM, ASSERT_VARDOMS_SLOW, THROW, } from './helpers'; import { config_addConstraint, config_addVarAnonConstant, config_addVarDomain, config_create, config_init, config_setDefaults, config_setOption, config_setOptions, } from './config'; import exporter_main, { exporter_encodeVarName, } from './exporter'; import importer_main from './importer'; import { domain__debug, domain_createEmpty, domain_fromListToArrdom, domain_isEmpty, domain_toArr, domain_anyToSmallest, } from './domain'; import search_depthFirst, { search_afterPropagation, search_createNextSpace, } from './search'; import { space_createFromConfig, space_propagate, space_solution, space_toConfig, } from './space'; import { trie_get, } from './trie'; // BODY_START let GENERATE_BARE_DSL = false; // // It is extended by path_solver /** * This is a super class. * It is extended by PathSolver in a private project * * @type {Solver} */ class Solver { /** * @param {Object} options = {} * @property {string} [options.distribute='naive'] * @property {Object} [options.searchDefaults] * @property {$config} [options.config=config_create()] * @property {boolean} [options.exportBare] * @property {number} [options.logging=LOG_NONE] */ constructor(options = {}) { this._class = 'solver'; this.logging = options.log || LOG_NONE; this.distribute = options.distribute || 'naive'; ASSERT(!void (options.exportBare !== undefined && (GENERATE_BARE_DSL = options.exportBare || false), this.exported = ''), 'bare exports kind of log the api inputs of this class in a DSL and print it at .solve() time'); ASSERT(options._class !== '$config', 'config should be passed on in a config property of options'); if (options.config) { let config = this.config = options.config; if (config.initialDomains) { let initialDomains = config.initialDomains; for (let i = 0, len = initialDomains.length; i < len; ++i) { let domain = initialDomains[i]; if (domain.length === 0) domain = domain_createEmpty(); initialDomains[i] = domain_anyToSmallest(domain); } } if (config._propagators) config._propagators = undefined; // will be regenerated if (config._varToPropagators) config._varToPropagators = undefined; // will be regenerated } else { this.config = config_create(); } this.solutions = []; this.state = { space: null, more: false, }; this._prepared = false; } /** * Returns an anonymous var with given value as lo/hi for the domain * * @param {number} num * @returns {string} */ num(num) { if (typeof num !== 'number') { THROW(`Solver#num: expecting a number, got ${num} (a ${typeof num})`); } if (isNaN(num)) { THROW('Solver#num: expecting a number, got NaN'); } let varIndex = config_addVarAnonConstant(this.config, num); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += ': __' + varIndex + '__ = ' + num + '\n'))); return this.config.allVarNames[varIndex]; } /** * Declare a var with optional given domain or constant value and distribution options. * * @param {string} [varName] Optional, Note that you can use this.num() to declare a constant. * @param {$arrdom|number} [domainOrValue] Note: if number, it is a constant (so [domain,domain]) not a $numdom! If omitted it becomes [SUB, SUP] * @param {Object} [distributionOptions] Var distribution options. A defined non-object here will throw an error to prevent doing declRange * @param {boolean} [_allowEmpty=false] Temp (i hope) override for importer * @param {boolean} [_override=false] Explicitly override the initial domain for an already existing var (for importer) * @returns {string} */ decl(varName, domainOrValue, distributionOptions, _allowEmpty, _override) { if (varName === '') THROW('Var name can not be the empty string'); ASSERT(varName === undefined || typeof varName === 'string', 'var name should be undefined or a string'); ASSERT(distributionOptions === undefined || typeof distributionOptions === 'object', 'options must be omitted or an object'); let arrdom; if (typeof domainOrValue === 'number') arrdom = [domainOrValue, domainOrValue]; // just normalize it here. else if (!domainOrValue) arrdom = [SUB, SUP]; else arrdom = domainOrValue; ASSERT_ARRDOM(arrdom); if (!arrdom.length && !_allowEmpty) THROW('EMPTY_DOMAIN_NOT_ALLOWED'); let varIndex = config_addVarDomain(this.config, varName || true, arrdom, _allowEmpty, _override); varName = this.config.allVarNames[varIndex]; if (distributionOptions) { if (distributionOptions.distribute) THROW('Use `valtype` to set the value distribution strategy'); config_setOption(this.config, 'varValueStrat', distributionOptions, varName); } ASSERT(!void (GENERATE_BARE_DSL && (this.exported += ': ' + exporter_encodeVarName(varName) + ' = [' + arrdom + ']' + (distributionOptions ? (distributionOptions.valtype === 'markov' ? (' @markov' + (distributionOptions.matrix ? ' matrix(' + distributionOptions.matrix + ')' : '') + (distributionOptions.expandVectorsWith !== undefined ? ' expand(' + distributionOptions.expandVectorsWith + ')' : '') + (distributionOptions.legend ? ' legend(' + distributionOptions.legend + ')' : '')) : '') : '') + ' # options=' + JSON.stringify(distributionOptions) + '\n'))); return varName; } /** * Declare multiple variables with the same domain/options * * @param {string[]} varNames * @param {$arrdom|number} [domainOrValue] Note: if number, it is a constant (so [domain,domain]) not a $numdom! If omitted it becomes [SUB, SUP] * @param {Object} [options] Var distribution options. A number here will throw an error to prevent doing declRange */ decls(varNames, domainOrValue, options) { for (let i = 0, n = varNames.length; i < n; ++i) { this.decl(varNames[i], domainOrValue, options); } } /** * Declare a var with given range * * @param {string} varName * @param {number} lo Ensure SUB<=lo<=hi<=SUP * @param {number} hi Ensure SUB<=lo<=hi<=SUP * @param {Object} [options] Var distribution options */ declRange(varName, lo, hi, options) { ASSERT(typeof lo === 'number', 'LO_SHOULD_BE_NUMBER'); ASSERT(typeof hi === 'number', 'HI_SHOULD_BE_NUMBER'); ASSERT(typeof options === 'object' || options === undefined, 'EXPECTING_OPTIONS_OR_NOTHING'); return this.decl(varName, [lo, hi], options); } // Arithmetic Propagators plus(A, B, C) { let R = config_addConstraint(this.config, 'plus', [A, B, C]); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' + ' + exporter_encodeVarName(B) + ' # plus, result var was: ' + C + '\n'))); return R; } minus(A, B, C) { let R = config_addConstraint(this.config, 'min', [A, B, C]); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' - ' + exporter_encodeVarName(B) + ' # min, result var was: ' + C + '\n'))); return R; } mul(A, B, C) { let R = config_addConstraint(this.config, 'ring-mul', [A, B, C]); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' * ' + exporter_encodeVarName(B) + ' # ringmul, result var was: ' + C + '\n'))); return R; } div(A, B, C) { let R = config_addConstraint(this.config, 'ring-div', [A, B, C]); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' / ' + exporter_encodeVarName(B) + ' # ringdiv, result var was: ' + C + '\n'))); return R; } sum(A, C) { let R = config_addConstraint(this.config, 'sum', A, C); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = sum(' + A.map(exporter_encodeVarName) + ') # result var was: ' + C + '\n'))); return R; } product(A, C) { let R = config_addConstraint(this.config, 'product', A, C); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = product(' + A.map(exporter_encodeVarName) + ') # result var was: ' + C + '\n'))); return R; } // TODO // times_plus k1*v1 + k2*v2 // wsum ∑ k*v // scale k*v // (In)equality Propagators // only first expression can be array distinct(A) { ASSERT(!void (GENERATE_BARE_DSL && (this.exported += 'distinct(' + A.map(exporter_encodeVarName) + ')\n'))); config_addConstraint(this.config, 'distinct', A); } eq(e1, e2) { if (e1 instanceof Array) { for (let i = 0, n = e1.length; i < n; ++i) { this.eq(e1[i], e2); } } else if (e2 instanceof Array) { for (let i = 0, n = e2.length; i < n; ++i) { this.eq(e1, e2[i]); } } else { ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(e1) + ' == ' + exporter_encodeVarName(e2) + '\n'))); config_addConstraint(this.config, 'eq', [e1, e2]); } } neq(e1, e2) { if (e1 instanceof Array) { for (let i = 0, n = e1.length; i < n; ++i) { this.neq(e1[i], e2); } } else if (e2 instanceof Array) { for (let i = 0, n = e2.length; i < n; ++i) { this.neq(e1, e2[i]); } } else { ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(e1) + ' != ' + exporter_encodeVarName(e2) + '\n'))); config_addConstraint(this.config, 'neq', [e1, e2]); } } gte(A, B) { ASSERT(!(A instanceof Array), 'NOT_ACCEPTING_ARRAYS'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(A) + ' >= ' + exporter_encodeVarName(B) + '\n'))); config_addConstraint(this.config, 'gte', [A, B]); } lte(A, B) { ASSERT(!(A instanceof Array), 'NOT_ACCEPTING_ARRAYS'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(A) + ' <= ' + exporter_encodeVarName(B) + '\n'))); config_addConstraint(this.config, 'lte', [A, B]); } gt(A, B) { ASSERT(!(A instanceof Array), 'NOT_ACCEPTING_ARRAYS'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(A) + ' > ' + exporter_encodeVarName(B) + '\n'))); config_addConstraint(this.config, 'gt', [A, B]); } lt(A, B) { ASSERT(!(A instanceof Array), 'NOT_ACCEPTING_ARRAYS'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(A) + ' < ' + exporter_encodeVarName(B) + '\n'))); config_addConstraint(this.config, 'lt', [A, B]); } isNeq(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'neq'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' !=? ' + exporter_encodeVarName(B) + '\n'))); return R; } isEq(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'eq'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' ==? ' + exporter_encodeVarName(B) + '\n'))); return R; } isGte(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'gte'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' >=? ' + exporter_encodeVarName(B) + '\n'))); return R; } isLte(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'lte'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' <=? ' + exporter_encodeVarName(B) + '\n'))); return R; } isGt(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'gt'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' >? ' + exporter_encodeVarName(B) + '\n'))); return R; } isLt(A, B, C) { let R = config_addConstraint(this.config, 'reifier', [A, B, C], 'lt'); ASSERT(!void (GENERATE_BARE_DSL && (this.exported += exporter_encodeVarName(R) + ' = ' + exporter_encodeVarName(A) + ' <? ' + exporter_encodeVarName(B) + '\n'))); return R; } // Various rest /** * Solve this solver. It should be setup with all the constraints. * * @param {Object} options * @property {number} [options.max=1000] * @property {number} [options.log=this.logging] Logging level; one of: 0, 1 or 2 (see LOG_* constants) * @property {string|Array.<string|Bvar>} options.vars Target branch vars or var names to force solve. Defaults to all. * @property {string|Object} [options.distribute='naive'] Maps to FD.distribution.value, see config_setOptions * @property {boolean} [_debug] A more human readable print of the configuration for this solver * @property {boolean} [_debugConfig] Log out solver.config after prepare() but before run() * @property {boolean} [_debugSpace] Log out solver._space after prepare() but before run(). Only works in dev code (stripped from dist) * @property {boolean} [_debugSolver] Call solver._debugSolver() after prepare() but before run() * @property {boolean} [_tostring] Serialize the config into a DSL * @property {boolean} [_nosolve] Dont actually solve. Used for debugging when printing something but not interested in actually running. * @property {number} [_debugDelay=0] When debugging, how many propagate steps should the debugging wait? (0 is only preprocessing) * @return {Object[]} */ solve(options = {}) { let log = this.logging = options.log === undefined ? this.logging : options.log; let max = options.max || 1000; ASSERT(!void (GENERATE_BARE_DSL && console.log('## bare export:\n@mode constraints\n' + this.exported + '## end of exported\n'))); this._prepare(options, log); let dbgCallback; if (options._tostring || options._debug || options._debugConfig || options._debugSpace || options._debugSolver) { dbgCallback = epoch => { if ((options._debugDelay | 0) >= epoch) { if (options._tostring) console.log(exporter_main(this.config)); if (options._debug) this._debugLegible(); if (options._debugConfig) this._debugConfig(); // __REMOVE_BELOW_FOR_DIST__ if (options._debugSpace) console.log('## _debugSpace:\n', getInspector()(this._space)); // __REMOVE_ABOVE_FOR_DIST__ if (options._debugSolver) this._debugSolver(); return true; } return false; }; if (dbgCallback(0)) dbgCallback = undefined; } if (options._nosolve) return; this._run(max, log, dbgCallback); return this.solutions; } /** * Generate the next child from given space. Or none if there isn't any. * * @param {$space} space * @returns {$space|undefined} */ offspring(space) { return search_createNextSpace(space, this.config); } /** * Propagate the given space to stability. * Returns whether this ended in a reject. * * @param {$space} space * @returns {boolean} */ propagate(space) { return space_propagate(space, this.config); } /** * Checks the state of a space (search node). * Basically one depth first search loop step * without the propagation. Returns true if * the space was solved, false if the space * was rejected, and undefined otherwise. * * @param {$space} space * @returns {boolean|undefined} true=solved, false=rejected, undefined=neither */ checkStableSpace(space) { return search_afterPropagation(false, space, this.config, this.state.stack, this.state); } /** * Prepare internal configuration before actually solving * Collects one-time config data and sets up defaults * * @param {Object} [options={}] See @solve * @param {number} log One of the LOG_* constants */ _prepare(options, log) { ASSERT(log === undefined || log >= LOG_MIN && log <= LOG_MAX, 'log level should be a valid value or be undefined (in tests)'); if (log >= LOG_STATS) { console.log(' - FD Preparing...'); console.time(' - FD Prepare Time'); } this._prepareConfig(options, log); // create the root node of the search tree (each node is a Space) let rootSpace = this.createSpace(); // __REMOVE_BELOW_FOR_DIST__ this._space = rootSpace; // only exposed for easy access in tests, and so only available after .prepare() // __REMOVE_ABOVE_FOR_DIST__ this.state.space = rootSpace; this.state.more = true; this.state.stack = []; this._prepared = true; if (log >= LOG_STATS) console.timeEnd(' - FD Prepare Time'); } /** * Create the root space using the config of this solver as its base. * * @returns {$space} */ createSpace() { return space_createFromConfig(this.config); } /** * Prepare the config side of things for a solve. * No space is created in this function (that's the point). * * @param {Object} options See _prepare * @param {number} log */ _prepareConfig(options, log) { ASSERT(log === undefined || log >= LOG_MIN && log <= LOG_MAX, 'log level should be a valid value or be undefined (in tests)'); let config = this.config; ASSERT_VARDOMS_SLOW(config.initialDomains, domain__debug); if (options.vars && options.vars !== 'all') { config_setOption(config, 'targeted_var_names', options.vars); } // TODO: eliminate? let distributionSettings = options.distribute || this.distribute; if (typeof distributionSettings === 'string') config_setDefaults(config, distributionSettings); else config_setOptions(config, distributionSettings); // TOFIX: get rid of this in mv config_init(config); } /** * Run the solver. You should call @_prepare before calling this function. * * @param {number} max Hard stop the solver when this many solutions have been found * @param {number} log One of the LOG_* constants * @param {Function} [dbgCallback] Call after each epoch until it returns false, then stop calling it. */ _run(max, log, dbgCallback) { ASSERT(typeof max === 'number', 'max should be a number'); ASSERT(log >= LOG_MIN && log <= LOG_MAX, 'log level should be a valid value'); ASSERT(this._prepared, 'must run #prepare before #run'); this._prepared = false; let state = this.state; ASSERT(state); if (log >= LOG_STATS) { console.log(` - FD Var Count: ${this.config.allVarNames.length}`); console.log(` - FD Targeted: ${this.config.targetedVars === 'all' ? 'all' : this.config.targetedVars.length}`); console.log(` - FD Constraint Count: ${this.config.allConstraints.length}`); console.log(` - FD Propagator Count: ${this.config._propagators.length}`); console.log(' - FD Solving...'); console.time(' - FD Solving Time'); } let alreadyRejected = false; let vardoms = state.space.vardoms; for (let i = 0, n = vardoms.length; i < n; ++i) { if (domain_isEmpty(vardoms[i])) { alreadyRejected = true; if (log >= LOG_STATS) { console.log(' - FD: rejected without propagation (' + this.config.allVarNames[i] + ' is empty)'); } break; } } let solvedSpaces; if (alreadyRejected) { if (log >= LOG_STATS) { console.log(' - FD Input Problem Rejected Immediately'); } solvedSpaces = []; } else { solvedSpaces = solver_runLoop(state, this.config, max, dbgCallback); } if (log >= LOG_STATS) { console.timeEnd(' - FD Solving Time'); ASSERT(!void console.log(` - FD debug stats: called propagate(): ${this.config._propagates > 0 ? this.config._propagates + 'x' : 'never! Finished by only using precomputations.'}`)); console.log(` - FD Solutions: ${solvedSpaces.length}`); } solver_getSolutions(solvedSpaces, this.config, this.solutions, log); } generateSolutions(solvedSpaces, config, targetObject, log) { solver_getSolutions(solvedSpaces, config, targetObject, log); } /** * Expose a method to normalize the internal representation * of a domain to always return an array representation * * @param {$space} space * @param {number} varIndex * @returns {$arrdom} */ getDomain(space, varIndex) { return domain_toArr(space.vardoms[varIndex]); } hasVar(varName) { return trie_get(this.config._varNamesTrie, varName) >= 0; } /** * Get the current domains for all targeted vars (only) * Heavy operation. * * @param {$space} space * @returns {Object.<string,$arrdom>} */ getTargetState(space) { let result = {}; let targets = this.config.targetedVars; if (targets === 'all') { targets = this.config.allVarNames; } let varNamesTrie = this.config._varNamesTrie; for (let i = 0, n = targets.length; i < n; ++i) { let varName = targets[i]; let varIndex = trie_get(varNamesTrie, varName); result[varName] = domain_toArr(space.vardoms[varIndex]); } return result; } /** * Exposed for multiverse as a legacy api. * Sets the value distribution options for a var after declaring it. * * @param {string} varName * @param {Object} options */ setValueDistributionFor(varName, options) { ASSERT(typeof varName === 'string', 'var name should be a string', varName); ASSERT(typeof options === 'object', 'value strat options should be an object'); config_setOption(this.config, 'varValueStrat', options, varName); ASSERT(!void (GENERATE_BARE_DSL && (this.exported = this.exported.replace(new RegExp('^(: ' + exporter_encodeVarName(varName) + ' =.*)', 'm'), '$1 # markov (set below): ' + JSON.stringify(options)) + '@custom set-valdist ' + exporter_encodeVarName(varName) + ' ' + JSON.stringify(options) + '\n'))); } /** * @returns {Solver} */ branch_from_current_solution() { // get the _solved_ space, convert to config, // use new config as base for new solver let solvedConfig = space_toConfig(this.state.space, this.config); return new Solver({config: solvedConfig}); } _debugLegible() { let WITH_INDEX = true; let clone = JSON.parse(JSON.stringify(this.config)); // prefer this over config_clone, just in case. let names = clone.allVarNames; let targeted = clone.targetedVars; let constraints = clone.allConstraints; let domains = clone.initialDomains; let propagators = clone._propagators; for (let key in clone) { // underscored prefixed objects are generally auto-generated structs // we don't want to debug a 5mb buffer, one byte per line. if (key[0] === '_' && typeof clone[key] === 'object') { clone[key] = '<removed>'; } } clone.allVarNames = '<removed>'; clone.allConstraints = '<removed>'; clone.initialDomains = '<removed>'; clone.varDistOptions = '<removed>'; if (targeted !== 'all') clone.targetedVars = '<removed>'; console.log('\n## _debug:\n'); console.log('- config:'); console.log(getInspector()(clone)); console.log('- vars (' + names.length + '):'); console.log(names.map((name, index) => `${WITH_INDEX ? index : ''}: ${domain__debug(domains[index])} ${name === String(index) ? '' : ' // ' + name}`).join('\n')); if (targeted !== 'all') { console.log('- targeted vars (' + targeted.length + '): ' + targeted.join(', ')); } console.log('- constraints (' + constraints.length + ' -> ' + propagators.length + '):'); console.log(constraints.map((c, index) => { if (c.param === undefined) { return `${WITH_INDEX ? index : ''}: ${c.name}(${c.varIndexes}) ---> ${c.varIndexes.map(index => domain__debug(domains[index])).join(', ')}`; } else if (c.name === 'reifier') { return `${WITH_INDEX ? index : ''}: ${c.name}[${c.param}](${c.varIndexes}) ---> ${domain__debug(domains[c.varIndexes[0]])} ${c.param} ${domain__debug(domains[c.varIndexes[1]])} = ${domain__debug(domains[c.varIndexes[2]])}`; } else { return `${WITH_INDEX ? index : ''}: ${c.name}(${c.varIndexes}) = ${c.param} ---> ${c.varIndexes.map(index => domain__debug(domains[index])).join(', ')} -> ${domain__debug(domains[c.param])}`; } }).join('\n')); console.log('##/\n'); } _debugSolver() { console.log('## _debugSolver:\n'); //let inspect = getInspector(); let config = this.config; //console.log('# Config:'); //console.log(inspect(_clone(config))); let names = config.allVarNames; console.log('# Variables (' + names.length + 'x):'); console.log(' index name domain toArr'); for (let varIndex = 0; varIndex < names.length; ++varIndex) { console.log(' ', varIndex, ':', names[varIndex], ':', domain__debug(config.initialDomains[varIndex])); } let constraints = config.allConstraints; console.log('# Constraints (' + constraints.length + 'x):'); console.log(' index name vars param'); for (let i = 0; i < constraints.length; ++i) { console.log(' ', i, ':', constraints[i].name, ':', constraints[i].varIndexes.join(','), ':', constraints[i].param); } let propagators = config._propagators; console.log('# Propagators (' + propagators.length + 'x):'); console.log(' index name vars args'); for (let i = 0; i < propagators.length; ++i) { console.log( ' ', i, ':', propagators[i].name + (propagators[i].name === 'reified' ? '(' + propagators[i].arg3 + ')' : ''), ':', propagators[i].index1, propagators[i].index2, propagators[i].index3, '->', domain__debug(config.initialDomains[propagators[i].index1]), domain__debug(config.initialDomains[propagators[i].index2]), domain__debug(config.initialDomains[propagators[i].index3]) ); } console.log('##'); } _debugConfig() { let config = _clone(this.config); config.initialDomains = config.initialDomains.map(domain__debug); console.log('## _debugConfig:\n', getInspector()(config)); } /** * Import from a dsl into this solver * * @param {string} s * @param {boolean} [_debug] Log out entire input with error token on fail? * @returns {Solver} this */ imp(s, _debug) { //console.log('##x## Solver.imp(...)'); //console.log(s); //console.log('##y##'); if (this.logging) { console.log(' - FD Importing DSL; ' + s.length + ' bytes'); console.time(' - FD Import Time:'); } let solver = importer_main(s, this, _debug); if (this.logging) { console.timeEnd(' - FD Import Time:'); } return solver; } /** * Export this config to a dsl. Optionally pass on a * space whose vardoms state to use for initialization. * * @param {$space} [space] * @param {boolean} [usePropagators] * @param {boolean} [minimal] * @param {boolean} [withDomainComments] * @returns {string} */ exp(space, usePropagators, minimal, withDomainComments) { return exporter_main(this.config, space.vardoms, usePropagators, minimal, withDomainComments); } /** * Exposes internal method domain_fromList for subclass * (Used by PathSolver in a private project) * It will always create an array, never a "small domain" * (number that is bit-wise flags) because that should be * kept an internal finitedomain artifact. * * @param {number[]} list * @returns {$arrdom[]} */ static domainFromList(list) { return domain_fromListToArrdom(list); } } /** * Deep clone given object for debugging purposes (only) * Revise if used for anything concrete * * @param {*} value * @returns {*} */ function _clone(value) { switch (typeof value) { case 'object': if (!value) return null; if (value instanceof Array) { return value.map(v => _clone(v)); } let obj = {}; for (let key in value) { obj[key] = _clone(value[key]); } return obj; case 'function': let fobj = { __THIS_IS_A_FUNCTION: 1, __source: value.toString(), }; for (let key in value) { fobj[key] = _clone(value[key]); } return fobj; case 'string': case 'number': case 'boolean': case 'undefined': return value; } THROW('config value what?', value); } let inspectorCache; function getInspector() { if (!inspectorCache) { inspectorCache = typeof require === 'function' ? function(arg) { return require('util').inspect(arg, false, null); } : function(o) { return o; }; } return inspectorCache; } /** * This is the core search loop. Supports multiple solves although you * probably only need one solution. Won't return more solutions than max. * * @param {Object} state * @param {$config} config * @param {number} max Stop after finding this many solutions * @param {Function} [dbgCallback] Call after each epoch until it returns false, then stop calling it. * @returns {$space[]} All solved spaces that were found (until max or end was reached) */ function solver_runLoop(state, config, max, dbgCallback) { let list = []; while (state.more && list.length < max) { search_depthFirst(state, config, dbgCallback); if (state.status !== 'end') { list.push(state.space); } } return list; } function solver_getSolutions(solvedSpaces, config, solutions, log) { ASSERT(solutions instanceof Array, 'solutions target object should be an array'); if (log >= LOG_STATS) { console.time(' - FD Solution Construction Time'); } for (let i = 0; i < solvedSpaces.length; ++i) { let solution = space_solution(solvedSpaces[i], config); solutions.push(solution); if (log >= LOG_SOLVES) { console.log(' - FD solution() ::::::::::::::::::::::::::::'); console.log(JSON.stringify(solution)); console.log(' ::::::::::::::::::::::::::::'); } } if (log >= LOG_STATS) { console.timeEnd(' - FD Solution Construction Time'); } } // BODY_STOP export default Solver;