finitedomain
Version:
A fast feature rich finite domain solver
361 lines (321 loc) • 12.5 kB
JavaScript
// this is an export function for config
// it converts a $config to a DSL string
// see /docs/dsl.txt for syntax
// see importer.js to parse this DSL
import {
// __REMOVE_BELOW_FOR_DIST__
DEOPT,
// __REMOVE_ABOVE_FOR_DIST__
THROW,
} from './helpers';
import {
domain_getValue,
domain_toArr,
} from './domain';
import {
trie_get,
} from './trie';
// BODY_START
/**
* Export a given config with optional target domains
* (initial domains otherwise) to special DSL string.
* The resulting string should be usable with the
* importer to create a new solver with same state.
* This function only omits constraints when they only
* consist of constants. Optimization should occur elsewhere.
*
* @param {$config} config
* @param {$domain[]} [vardoms] If not given then config.initialDomains are used
* @param {boolean} [usePropagators] Output the low-level propagators instead of the higher level constraints
* @param {boolean} [minimal] Omit comments, use short var names, reduce whitespace where possible. etc
* @param {boolean} [withDomainComments] Put the input domains behind each constraint even if minimal=true
* @param {boolean} [realName] Use the original var names?
* @returns {string}
*/
function exporter_main(config, vardoms, usePropagators, minimal, withDomainComments, realName) {
// TODO: dont export contants that are not bound to constraints and not targeted explicitly
// TODO: deal export->import better wrt anonymous vars
let var_dist_options = config.varDistOptions;
let domains = vardoms || config.initialDomains;
let varNames = config.allVarNames;
let indexToString = realName ? index => exporter_encodeVarName(varNames[index]) : minimal ? exporter_varstrShort : exporter_varstrNum;
let vars = config.allVarNames.map((varName, varIndex) => {
let domain = exporter_domstr(domains[varIndex]);
let s = ': ' + indexToString(varIndex) + ' = ' + domain;
if (!realName && varName !== String(varIndex)) s += ' alias(' + exporter_encodeVarName(varName) + ')';
let overrides = var_dist_options[varName];
if (overrides && (overrides.valtype !== 'list' || (overrides.list && overrides.list.length))) {
s += ' @' + overrides.valtype;
switch (overrides.valtype) {
case 'markov':
if ('expandVectorsWith' in overrides) s += 'expand(' + (overrides.expandVectorsWith || 0) + ')';
if ('legend' in overrides) s += ' legend(' + overrides.legend.join(' ') + ')';
if ('matrix' in overrides) s += ' matrix(' + JSON.stringify(overrides.matrix).replace(/"/g, '') + ')';
break;
case 'list':
if (typeof overrides.list === 'function') s += ' prio(???func???)';
else s += ' prio(' + overrides.list.join(' ') + ')';
break;
case 'max':
case 'mid':
case 'min':
case 'minMaxCycle':
case 'naive':
case 'splitMax':
case 'splitMin':
break;
default:
console.warn('Unknown value strategy override: ' + overrides.valtype);
s += ' @? ' + JSON.stringify(overrides);
}
}
return s;
});
let constraints = usePropagators ? [] : config.allConstraints.map(constraint => {
let indexes = constraint.varIndexes;
// create var names for each index, unless solved, in that case use solved value as literal
let aliases = indexes.map(indexToString);
indexes.forEach((varIndex, i) => {
let v = domain_getValue(domains[varIndex]);
if (v >= 0) aliases[i] = v;
});
// __REMOVE_BELOW_FOR_DIST__
if (DEOPT) {
// dont inline solved vars
aliases = indexes.map(indexToString);
}
// __REMOVE_ABOVE_FOR_DIST__
// do same for param if it's an index
let paramName = '';
if (typeof constraint.param === 'number') {
let paramV = domain_getValue(domains[constraint.param]);
if (paramV >= 0) paramName = paramV;
else paramName = indexToString(constraint.param);
}
let s = '';
let comment = '';
switch (constraint.name) {
case 'reifier':
let op;
switch (constraint.param) {
case 'eq': op = '=='; break;
case 'neq': op = '!='; break;
case 'lt': op = '<'; break;
case 'lte': op = '<='; break;
case 'gt': op = '>'; break;
case 'gte': op = '>='; break;
default: THROW('what dis param: ' + op);
}
s += aliases[2] + ' = ' + aliases[0] + ' ' + op + '? ' + aliases[1];
break;
case 'plus':
s += aliases[2] + ' = ' + aliases[0] + ' + ' + aliases[1];
break;
case 'min':
s += aliases[2] + ' = ' + aliases[0] + ' - ' + aliases[1];
break;
case 'ring-mul':
s += aliases[2] + ' = ' + aliases[0] + ' * ' + aliases[1];
break;
case 'ring-div':
s += aliases[2] + ' = ' + aliases[0] + ' / ' + aliases[1];
break;
case 'mul':
s += aliases[2] + ' = ' + aliases[0] + ' * ' + aliases[1];
break;
case 'sum':
s += paramName + ' = sum(' + aliases.join(' ') + ')';
break;
case 'product':
s += paramName + ' = product(' + aliases.join(' ') + ')';
break;
case 'markov':
s += '# markov(' + aliases + ')';
break;
case 'distinct':
s += 'distinct(' + aliases + ')';
break;
case 'eq':
s += aliases[0] + ' == ' + aliases[1];
break;
case 'neq':
s += aliases[0] + ' != ' + aliases[1];
break;
case 'lt':
s += aliases[0] + ' < ' + aliases[1];
break;
case 'lte':
s += aliases[0] + ' <= ' + aliases[1];
break;
case 'gt':
s += aliases[0] + ' > ' + aliases[1];
break;
case 'gte':
s += aliases[0] + ' >= ' + aliases[1];
break;
default:
console.warn('unknown constraint: ' + constraint.name);
s += 'unknown = ' + JSON.stringify(constraint);
}
let t = s;
// if a constraint has no vars, ignore it.
// note: this assumes those constraints are not contradictions
if (s.indexOf(realName ? '\'' : '$') < 0 || (constraint.name === 'distinct' && aliases.length <= 1) || (((constraint.name === 'product' || constraint.name === 'sum') && aliases.length === 0))) {
if (!minimal) {
comment += (comment ? ', ' : ' # ') + 'dropped; constraint already solved (' + s + ') (' + indexes.map(indexToString) + ', ' + indexToString(constraint.param) + ')';
}
s = '';
}
if (!minimal || withDomainComments) {
// this is more for easier debugging...
aliases.forEach((alias, i) => { if (typeof alias === 'string') t = t.replace(alias, exporter_domstr(domains[indexes[i]])); });
if (typeof constraint.param === 'number' && typeof paramName === 'string') t = t.replace(paramName, exporter_domstr(domains[constraint.param]));
if (s || !minimal) {
// s += ' '.repeat(Math.max(0, 30 - s.length))
for (let i = Math.max(0, 30 - s.length); i >= 0; --i) s += ' ';
s += ' # ' + t;
}
s += comment;
}
return s;
}).filter(s => !!s);
let propagators = !usePropagators ? [] : config._propagators.map(propagator => {
let varIndex1 = propagator.index1;
let varIndex2 = propagator.index2;
let varIndex3 = propagator.index3;
let v1 = varIndex1 >= 0 ? domain_getValue(domains[varIndex1]) : -1;
let name1 = v1 >= 0 ? v1 : varIndex1 < 0 ? undefined : indexToString(varIndex1);
let v2 = varIndex2 >= 0 ? domain_getValue(domains[varIndex2]) : -1;
let name2 = v2 >= 0 ? v2 : varIndex2 < 0 ? undefined : indexToString(varIndex2);
let v3 = varIndex3 >= 0 ? domain_getValue(domains[varIndex3]) : -1;
let name3 = v3 >= 0 ? v3 : varIndex3 < 0 ? undefined : indexToString(varIndex3);
// __REMOVE_BELOW_FOR_DIST__
if (DEOPT) {
// dont inline solved vars
name1 = indexToString(varIndex1);
name2 = indexToString(varIndex2);
name3 = indexToString(varIndex3);
}
// __REMOVE_ABOVE_FOR_DIST__
let s = '';
let comment = '';
switch (propagator.name) {
case 'reified':
let op;
switch (propagator.arg3) {
case 'eq': op = '=='; break;
case 'neq': op = '!='; break;
case 'lt': op = '<'; break;
case 'lte': op = '<='; break;
case 'gt': op = '>'; break;
case 'gte': op = '>='; break;
default: THROW('what dis param: ' + op);
}
s += name3 + ' = ' + name1 + ' ' + op + '? ' + name2;
break;
case 'eq':
s += name1 + ' == ' + name2;
break;
case 'lt':
s += name1 + ' < ' + name2;
break;
case 'lte':
s += name1 + ' <= ' + name2;
break;
case 'mul':
s += name3 + ' = ' + name1 + ' * ' + name2;
break;
case 'div':
s += name3 + ' = ' + name1 + ' / ' + name2;
break;
case 'neq':
s += name1 + ' != ' + name2;
break;
case 'min':
s += name3 + ' = ' + name1 + ' - ' + name2;
break;
case 'ring':
switch (propagator.arg1) {
case 'plus':
s += name3 + ' = ' + name1 + ' + ' + name2;
break;
case 'min':
s += name3 + ' = ' + name1 + ' - ' + name2;
break;
case 'ring-mul':
s += name3 + ' = ' + name1 + ' * ' + name2;
break;
case 'ring-div':
s += name3 + ' = ' + name1 + ' / ' + name2;
break;
default:
throw new Error('Unexpected ring op:' + propagator.arg1);
}
break;
case 'markov':
// ignore. the var @markov modifier should cause this. it's not a real constraint.
return '';
default:
console.warn('unknown propagator: ' + propagator.name);
s += 'unknown = ' + JSON.stringify(propagator);
}
let t = s;
// if a propagator has no vars, ignore it.
// note: this assumes those constraints are not contradictions
if (s.indexOf('$') < 0) {
if (!minimal) comment += (comment ? ', ' : ' # ') + 'dropped; constraint already solved (' + s + ')';
s = '';
}
if (!minimal) {
// this is more for easier debugging...
if (typeof name1 === 'string') t = t.replace(name1, exporter_domstr(domains[varIndex1]));
if (typeof name2 === 'string') t = t.replace(name2, exporter_domstr(domains[varIndex2]));
if (typeof name3 === 'string') t = t.replace(name3, exporter_domstr(domains[varIndex3]));
s += ' '.repeat(Math.max(0, 30 - s.length)) + ' # initial: ' + t;
s += comment;
}
return s;
}).filter(s => !!s);
return [
'## constraint problem export',
'@custom var-strat = ' + JSON.stringify(config.varStratConfig), // TODO
'@custom val-strat = ' + config.valueStratName,
vars.join('\n') || '# no vars',
constraints.join('\n') || propagators.join('\n') || '# no constraints',
'@custom targets ' + (config.targetedVars === 'all' ? ' = all' : '(' + config.targetedVars.map(varName => indexToString(trie_get(config._varNamesTrie, varName))).join(' ') + ')'),
'## end of export',
].join('\n\n');
}
function exporter_encodeVarName(varName) {
if (typeof varName === 'number') return varName; // constant
return '\'' + varName + '\''; // "quoted var names" can contain any char.
}
function exporter_varstrNum(varIndex) {
// note: we put a `$` behind it so that we can search-n-replace for `$1` without matching `$100`
return '$' + varIndex + '$';
}
function exporter_varstrShort(varIndex) {
// take care not to start the name with a number
// note: .toString(36) uses a special (standard) base 36 encoding; 0-9a-z to represent 0-35
let name = varIndex.toString(36);
if (name[0] < 'a') name = '$' + name; // this is a little lazy but whatever
return name;
}
function exporter_domstr(domain) {
// represent domains as pairs, a single pair as [lo hi] and multiple as [[lo hi] [lo hi]]
let arrdom = domain_toArr(domain);
if (arrdom.length === 2 && arrdom[0] === arrdom[1]) return String(arrdom[0]);
if (arrdom.length > 2) {
let dom = [];
for (let i = 0, n = arrdom.length; i < n; i += 2) {
dom.push('[' + arrdom[i] + ' ' + arrdom[i + 1] + ']');
}
arrdom = dom;
}
return '[' + arrdom.join(' ') + ']';
}
// BODY_STOP
export default exporter_main;
export {
exporter_encodeVarName,
};