finitedomain
Version:
A fast feature rich finite domain solver
494 lines (415 loc) • 18.5 kB
JavaScript
/*
The functions in this file are supposed to determine the next
value while solving a Space. The functions are supposed to
return the new domain for some given var index. If there's no
new choice left it should return undefined to signify the end.
*/
import {
LOG_FLAG_CHOICE,
NO_SUCH_VALUE,
ASSERT,
ASSERT_LOG,
ASSERT_NORDOM,
THROW,
} from '../helpers';
import {
domain__debug,
domain_containsValue,
domain_createValue,
domain_createRange,
domain_getFirstIntersectingValue,
domain_intersection,
domain_isSolved,
domain_max,
domain_middleElement,
domain_min,
domain_removeValue,
} from '../domain';
import distribution_markovSampleNextFromDomain from './markov';
import {
markov_createLegend,
markov_createProbVector,
} from '../markov';
// BODY_START
const FIRST_CHOICE = 0;
const SECOND_CHOICE = 1;
const THIRD_CHOICE = 2;
const NO_CHOICE = undefined;
function distribute_getNextDomainForVar(space, config, varIndex, choiceIndex) {
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(space.vardoms[varIndex] && !domain_isSolved(space.vardoms[varIndex]), 'CALLSITE_SHOULD_PREVENT_DETERMINED'); // TODO: test
let valueStrategy = config.valueStratName;
// each var can override the value distributor
let configVarDistOptions = config.varDistOptions;
let varName = config.allVarNames[varIndex];
ASSERT(typeof varName === 'string', 'VAR_NAME_SHOULD_BE_STRING');
let valueDistributorName = configVarDistOptions[varName] && configVarDistOptions[varName].valtype;
if (valueDistributorName) valueStrategy = valueDistributorName;
if (typeof valueStrategy === 'function') return valueStrategy(space, varIndex, choiceIndex);
return _distribute_getNextDomainForVar(valueStrategy, space, config, varIndex, choiceIndex);
}
function _distribute_getNextDomainForVar(stratName, space, config, varIndex, choiceIndex) {
ASSERT(space._class === '$space', 'EXPECTING_SPACE');
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
switch (stratName) {
case 'max':
return distribution_valueByMax(space, varIndex, choiceIndex);
case 'markov':
return distribution_valueByMarkov(space, config, varIndex, choiceIndex);
case 'mid':
return distribution_valueByMid(space, varIndex, choiceIndex);
case 'min':
return distribution_valueByMin(space, varIndex, choiceIndex);
case 'minMaxCycle':
return distribution_valueByMinMaxCycle(space, varIndex, choiceIndex);
case 'list':
return distribution_valueByList(space, config, varIndex, choiceIndex);
case 'naive':
ASSERT_NORDOM(space.vardoms[varIndex]);
ASSERT(space.vardoms[varIndex], 'NON_EMPTY_DOMAIN_EXPECTED');
return domain_createValue(domain_min(space.vardoms[varIndex]));
case 'splitMax':
return distribution_valueBySplitMax(space, varIndex, choiceIndex);
case 'splitMin':
return distribution_valueBySplitMin(space, varIndex, choiceIndex);
case 'throw':
return ASSERT(false, 'not expecting to pick this distributor');
}
THROW('unknown next var func', stratName);
}
/**
* Attempt to solve by setting var domain to values in the order
* given as a list. This may also be a function which should
* return a new domain given the space, var index, and choice index.
*
* @param {$space} space
* @param {$config} config
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain for this var index in the next space TOFIX: support small domains
*/
function distribution_valueByList(space, config, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByList', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(config._class === '$config', 'EXPECTING_CONFIG');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
let varName = config.allVarNames[varIndex];
ASSERT(typeof varName === 'string', 'VAR_NAME_SHOULD_BE_STRING');
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
let configVarDistOptions = config.varDistOptions;
ASSERT(configVarDistOptions, 'space should have config.varDistOptions');
ASSERT(configVarDistOptions[varName], 'there should be distribution options available for every var', varName);
ASSERT(configVarDistOptions[varName].list, 'there should be a distribution list available for every var', varName);
let varDistOptions = configVarDistOptions[varName];
let listSource = varDistOptions.list;
let fallbackName = '';
if (varDistOptions.fallback) {
fallbackName = varDistOptions.fallback.valtype;
ASSERT(fallbackName, 'should have a fallback type');
ASSERT(fallbackName !== 'list', 'prevent recursion loops');
}
let list = listSource;
if (typeof listSource === 'function') {
// Note: callback should return the actual list
list = listSource(space, varName, choiceIndex);
}
switch (choiceIndex) {
case FIRST_CHOICE:
let nextValue = domain_getFirstIntersectingValue(domain, list);
if (nextValue === NO_SUCH_VALUE) {
if (fallbackName) {
return _distribute_getNextDomainForVar(fallbackName, space, config, varIndex, choiceIndex);
}
return NO_CHOICE;
} else {
space._lastChosenValue = nextValue;
}
return domain_createValue(nextValue);
case SECOND_CHOICE:
if (space._lastChosenValue >= 0) {
return domain_removeValue(domain, space._lastChosenValue);
}
if (fallbackName) {
return _distribute_getNextDomainForVar(fallbackName, space, config, varIndex, choiceIndex);
}
return NO_CHOICE;
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Searches through a var's values from min to max.
* For each value in the domain it first attempts just
* that value, then attempts the domain without this value.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueByMin(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByMin', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
switch (choiceIndex) {
case FIRST_CHOICE:
let minValue = domain_min(domain);
space._lastChosenValue = minValue;
return domain_createValue(minValue);
case SECOND_CHOICE:
// Cannot lead to empty domain because lo can only be SUP if
// domain was solved and we assert it wasn't.
ASSERT(space._lastChosenValue >= 0, 'first choice should set this property and it should at least be 0', space._lastChosenValue);
return domain_removeValue(domain, space._lastChosenValue);
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Searches through a var's values from max to min.
* For each value in the domain it first attempts just
* that value, then attempts the domain without this value.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueByMax(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByMax', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
switch (choiceIndex) {
case FIRST_CHOICE:
let maxValue = domain_max(domain);
space._lastChosenValue = maxValue;
return domain_createValue(maxValue);
case SECOND_CHOICE:
// Cannot lead to empty domain because hi can only be SUB if
// domain was solved and we assert it wasn't.
ASSERT(space._lastChosenValue > 0, 'first choice should set this property and it should at least be 1', space._lastChosenValue);
return domain_removeValue(domain, space._lastChosenValue);
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Searches through a var's values by taking the middle value.
* This version targets the value closest to `(max-min)/2`
* For each value in the domain it first attempts just
* that value, then attempts the domain without this value.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueByMid(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByMid', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
switch (choiceIndex) {
case FIRST_CHOICE:
let middle = domain_middleElement(domain);
space._lastChosenValue = middle;
return domain_createValue(middle);
case SECOND_CHOICE:
ASSERT(space._lastChosenValue >= 0, 'first choice should set this property and it should at least be 0', space._lastChosenValue);
return domain_removeValue(domain, space._lastChosenValue);
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Search a domain by splitting it up through the (max-min)/2 middle.
* First simply tries the lower half, then tries the upper half.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueBySplitMin(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueBySplitMin', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
let max = domain_max(domain);
switch (choiceIndex) {
case FIRST_CHOICE: {
// TOFIX: can do this more optimal if coding it out explicitly
let min = domain_min(domain);
let mmhalf = min + Math.floor((max - min) / 2);
space._lastChosenValue = mmhalf;
// Note: domain is not determined so the operation cannot fail
// Note: this must do some form of intersect, though maybe not constrain
return domain_intersection(domain, domain_createRange(min, mmhalf));
}
case SECOND_CHOICE: {
ASSERT(space._lastChosenValue >= 0, 'first choice should set this property and it should at least be 0', space._lastChosenValue);
// Note: domain is not determined so the operation cannot fail
// Note: this must do some form of intersect, though maybe not constrain
return domain_intersection(domain, domain_createRange(space._lastChosenValue + 1, max));
}
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Search a domain by splitting it up through the (max-min)/2 middle.
* First simply tries the upper half, then tries the lower half.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueBySplitMax(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueBySplitMax', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
let min = domain_min(domain);
switch (choiceIndex) {
case FIRST_CHOICE: {
// TOFIX: can do this more optimal if coding it out explicitly
let max = domain_max(domain);
let mmhalf = min + Math.floor((max - min) / 2);
space._lastChosenValue = mmhalf;
// Note: domain is not determined so the operation cannot fail
// Note: this must do some form of intersect, though maybe not constrain
return domain_intersection(domain, domain_createRange(mmhalf + 1, max));
}
case SECOND_CHOICE: {
ASSERT(space._lastChosenValue >= 0, 'first choice should set this property and it should at least be 0', space._lastChosenValue);
// Note: domain is not determined so the operation cannot fail
// Note: this must do some form of intersect, though maybe not constrain
return domain_intersection(domain, domain_createRange(min, space._lastChosenValue));
}
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
/**
* Applies distribution_valueByMin and distribution_valueByMax alternatingly
* depending on the position of the given var in the list of vars.
*
* @param {$space} space
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueByMinMaxCycle(space, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByMinMaxCycle', varIndex, choiceIndex));
if (_isEven(varIndex)) {
return distribution_valueByMin(space, varIndex, choiceIndex);
} else {
return distribution_valueByMax(space, varIndex, choiceIndex);
}
}
/**
* @param {number} n
* @returns {boolean}
*/
function _isEven(n) { return n % 2 === 0; }
/**
* Search a domain by applying a markov chain to determine an optimal value
* checking path.
*
* @param {$space} space
* @param {$config} config
* @param {number} varIndex
* @param {number} choiceIndex
* @returns {$domain|undefined} The new domain this var index should get in the next space
*/
function distribution_valueByMarkov(space, config, varIndex, choiceIndex) {
ASSERT_LOG(LOG_FLAG_CHOICE, log => log('distribution_valueByMarkov', varIndex, choiceIndex));
ASSERT(space._class === '$space', 'SPACE_SHOULD_BE_SPACE');
ASSERT(typeof varIndex === 'number', 'VAR_INDEX_SHOULD_BE_NUMBER');
ASSERT(typeof choiceIndex === 'number', 'CHOICE_SHOULD_BE_NUMBER');
let domain = space.vardoms[varIndex];
ASSERT_NORDOM(domain);
ASSERT(domain && !domain_isSolved(domain), 'DOMAIN_SHOULD_BE_UNDETERMINED');
switch (choiceIndex) {
case FIRST_CHOICE: {
// THIS IS AN EXPENSIVE STEP!
let varName = config.allVarNames[varIndex];
ASSERT(typeof varName === 'string', 'VAR_NAME_SHOULD_BE_STRING');
let configVarDistOptions = config.varDistOptions;
ASSERT(configVarDistOptions, 'space should have config.varDistOptions');
let distOptions = configVarDistOptions[varName];
ASSERT(distOptions, 'markov vars should have distribution options');
let expandVectorsWith = distOptions.expandVectorsWith;
ASSERT(distOptions.matrix, 'there should be a matrix available for every var');
ASSERT(distOptions.legend || (typeof expandVectorsWith === 'number' && expandVectorsWith >= 0), 'every var should have a legend or expandVectorsWith set');
let random = distOptions.random || config._defaultRng;
ASSERT(typeof random === 'function', 'RNG_SHOULD_BE_FUNCTION');
// note: expandVectorsWith can be 0, so check with null
let values = markov_createLegend(typeof expandVectorsWith === 'number', distOptions.legend, domain);
let valueCount = values.length;
if (!valueCount) {
return NO_CHOICE;
}
let probabilities = markov_createProbVector(space, distOptions.matrix, expandVectorsWith, valueCount);
let value = distribution_markovSampleNextFromDomain(domain, probabilities, values, random);
if (value == null) {
return NO_CHOICE;
}
ASSERT(domain_containsValue(domain, value), 'markov picks a value from the existing domain so no need for a constrain below');
space._lastChosenValue = value;
return domain_createValue(value);
}
case SECOND_CHOICE: {
let lastValue = space._lastChosenValue;
ASSERT(typeof lastValue === 'number', 'should have cached previous value');
let newDomain = domain_removeValue(domain, lastValue);
ASSERT(domain, 'domain cannot be empty because only one value was removed and the domain is asserted to be not solved above');
ASSERT_NORDOM(newDomain, true, domain__debug);
return newDomain;
}
}
ASSERT(choiceIndex === THIRD_CHOICE, 'SHOULD_NOT_CALL_MORE_THAN_TRHICE');
return NO_CHOICE;
}
// BODY_STOP
export default distribute_getNextDomainForVar;
export {
FIRST_CHOICE,
SECOND_CHOICE,
THIRD_CHOICE,
NO_CHOICE,
// __REMOVE_BELOW_FOR_DIST__
// for testing:
_distribute_getNextDomainForVar,
distribution_valueByList,
distribution_valueByMarkov,
distribution_valueByMax,
distribution_valueByMid,
distribution_valueByMin,
distribution_valueByMinMaxCycle,
distribution_valueBySplitMax,
distribution_valueBySplitMin,
// __REMOVE_ABOVE_FOR_DIST__
};