finitedomain
Version:
A fast feature rich finite domain solver
187 lines (149 loc) • 8.62 kB
JavaScript
import expect from '../../fixtures/mocha_proxy.fixt';
import {
fixt_dom_range,
fixt_dom_solved,
} from '../../fixtures/domain.fixt';
import distribution_markovSampleNextFromDomain from '../../../src/distribution/markov';
const RNG_UNNORMALIZED = false;
const RNG_NORMALIZED = true;
const funcRngMin = function() { return 0; };
const rngFuncMid = function() { return 0.5; }; // middle (ok, technically slightly above the middle... is that bug?)
const funcRngMax = function() { return 1 - Number.EPSILON; }; // always pick last in legend
describe('distribution/markov.spec', function() {
it('should return a number', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [5];
let probVector = [1];
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, funcRngMax)).to.equal(5);
});
it('should return first value in legend if rng is 0', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [2, 5];
let probVector = [1, 1]; // equal odds (irrelevant for this test)
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, funcRngMin)).to.equal(2);
});
it('should return first value in legend if rng is 1', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [2, 8];
let probVector = [1, 1]; // equal odds (irrelevant for this test)
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, funcRngMax, RNG_NORMALIZED)).to.equal(8);
});
it('should throw if normalized rng returns 1', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [2, 8];
let probVector = [1, 1]; // equal odds (irrelevant for this test)
let rngFunc = () => 1; // not a valid normalized value
expect(() => distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFunc, RNG_NORMALIZED)).to.throw('RNG_SHOULD_BE_NORMALIZED');
});
it('should throw if normalized rng returns 1.1', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [2, 8];
let probVector = [1, 1]; // equal odds (irrelevant for this test)
let rngFunc = () => 1.1; // not a valid normalized value
expect(() => distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFunc, RNG_NORMALIZED)).to.throw('RNG_SHOULD_BE_NORMALIZED');
});
it('should throw if normalized rng returns -1', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [2, 8];
let probVector = [1, 1]; // equal odds (irrelevant for this test)
let rngFunc = () => -1; // not a valid normalized value
expect(() => distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFunc, RNG_NORMALIZED)).to.throw('RNG_SHOULD_BE_NORMALIZED');
});
it('should return middle value in legend if rng is .5 with equal probs', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [1, 2, 3];
let probVector = [1, 1, 1]; // equal odds (irrelevant for this test)
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFuncMid)).to.equal(2);
});
it('should not consider values with zero probability', function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [1, 2, 3, 4, 5, 6];
let probVector = [0, 0, 0, 0, 1, 0]; // only `5` has non-zero chance
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFuncMid)).to.equal(5);
});
it('should ignore values not in current domain', function() {
let domain = fixt_dom_range(3, 10); // note: 1 and 2 are not part of domain!
let valueLegend = [1, 2, 3, 4, 5, 6];
let probVector = [1, 1, 0, 0, 1, 0]; // 1, 2, and 5 have non-zero probability
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, funcRngMin)).to.equal(5);
});
describe('distribute according to probability weight', function() {
it('should solve an explicit case', function() {
// tricky to explain. rng works on the total of probabilities and "consumes" left-to-right
// so since `2` has probability of `5/total` and the only value before it has a prob of
// `1/total`, the `2` will be chosen if rng outcome is one of [1, 6]. We'll fix it to `4`
// in this test so we'll know this must be the case.
let domain = fixt_dom_range(0, 10);
let valueLegend = [1, 2, 3, 4, 5, 6];
let probVector = [1, 5, 1, 1, 1, 1]; // total probability is 10
let rngFunc = () => 4; // whole number to prevent precision errors (-> RNG_UNNORMALIZED)
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFunc, RNG_UNNORMALIZED)).to.equal(2);
});
describe('more cases, unnormalized', function() {
// Make sure that: rngRoll < sum(probVector).
// The roll must be less! Not lte.
function caseIt(probVector, rngRoll, outcome, desc) {
if (rngRoll >= probVector.reduce((a, b) => a + b)) {
throw new Error(`Test fail, roll must be < prob sum (${rngRoll} >= ${probVector.reduce((a, b) => a + b)})`);
}
it(`should solve case probs: ${probVector} roll: ${rngRoll} out: ${outcome} ${desc && ('desc: ' + desc) || ''}`, function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [1, 2, 3, 4, 5, 6];
let rngFunc = function() { return rngRoll; };
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rngFunc, RNG_UNNORMALIZED)).to.equal(outcome);
});
}
caseIt([1, 1, 1, 1, 1, 1], 0, 1);
caseIt([1, 1, 1, 1, 1, 1], 1, 2);
caseIt([1, 1, 1, 1, 1, 1], 4, 5);
caseIt([1, 5, 1, 1, 1, 1], 1, 2);
caseIt([1, 5, 1, 1, 1, 1], 4, 2, 'the 5 "adds" the second index 5x to the pool so 4 ends up picking the second index');
caseIt([1, 5, 1, 1, 1, 1], 6, 3, 'the 6 roll skips over index 1 and 5x index 2 so it picks index 3');
caseIt([1, 5, 1, 1, 1, 1], 9, 6);
caseIt([1, 8, 1, 12, 1, 1], 22, 5, 'roll 23, skips 1x 1, 8x 2, 1x 3, 12x 4 (=22 indices) to get index 5 (not 6 because it offsets at 0)');
});
describe('more cases, normalized', function() {
// Make sure that: rngRoll < sum(probVector).
// The roll must be less! Not lte.
let case_it = function(probVector, rngRoll, outcome, desc) {
if (rngRoll >= 1) {
throw new Error(`Test fail, roll must be < 1 (${rngRoll} >= 1`);
}
if (Math.abs(probVector.reduce((a, b) => a + b) - 1) > 1e-4) {
throw new Error(`Test fail, prob total should be 1 (1-${probVector.reduce(function(a, b) { return a + b; })} > ${1e-4})`);
}
it(`should solve case probs: ${probVector} roll: ${rngRoll} out: ${outcome} ${desc && ('desc: ' + desc) || ''}`, function() {
let domain = fixt_dom_range(0, 10);
let valueLegend = [1, 2, 3, 4, 5, 6];
let rng_func = _ => rngRoll;
expect(distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rng_func, RNG_NORMALIZED)).to.equal(outcome);
});
};
case_it([1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6], 0, 1);
case_it([1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6, 1 / 6], 1 - 1e-6, 6);
//case_it [1/6, 1/6, 1/6, 1/6, 1/6, 1/6], 1/6, 2 # rounding makes this test difficult
//case_it [1/6, 0, 2/6, 1/6, 1/6, 1/6], 1/6, 3, 'Second index has zero prob so it becomes 3' # rounding makes this test difficult
case_it([1 / 6, 0, 2 / 6, 1 / 6, 1 / 6, 1 / 6], 2 / 6, 3, 'Second index has zero prob so it becomes 3');
});
});
it('should always return a value from legend (100x)', function() {
let generateNormalizedProbs = function(r, n) {
// returns an array of 2^n random values, which should sum up to 1
let x = r * Math.random();
let y = r - x;
if (--n) {
return [].concat(generateNormalizedProbs(x, n), generateNormalizedProbs(y, n));
}
return [x, y];
};
for (var i = 0; i < 100; ++i) {
let domain = fixt_dom_solved(100);
let valueLegend = [100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100, 100]; // 16x 100
let probVector = generateNormalizedProbs(1, 4); // 16 values
let r = Math.random();
let rng = () => r;
let out = distribution_markovSampleNextFromDomain(domain, probVector, valueLegend, rng, RNG_NORMALIZED);
expect(out, `domain: ${domain} valueLegend: ${valueLegend} probVector: ${probVector} r: ${r} out: ${out}`).to.equal(100); // should end up picking a valid index
}
});
});