finitedomain
Version:
A fast feature rich finite domain solver
144 lines (122 loc) • 4.71 kB
JavaScript
/*
Markov Distribution Helpers
======================================================================
Helpers for Markov-style probabilistic value & var distributions.
const markov = {
legend: ['small', 'med', 'large'],
matrix: [{
vector: [.5, 1, 1],
condition: function (S, varId) {
var prev = S.readMatrix(varId, S.cursor() - 1)
return S.isEqual(prev, 'small');
},
}, {
vector: [1, 1, 1],
condition: function () { return true; },
},
],
};
const markov = {
legend: ['small', 'med', 'large'],
matrix: [{
vector: [.5, 1, 1],
condition: function (S, varId) {
var prev = S.readMatrix(varId, S.cursor() - 1);
var result = {
value: S.isEqual(prev, 'small'),
deps: ...,
};
return result;
},
}, {
vector: [1, 1, 1],
condition: function () { return true; },
}],
};
Inhomogenous Markov chains [see](https://cw.fel.cvut.cz/wiki/_media/courses/a6m33bin/markov-chains-2.pdf)
in an inhomogeneous Markov model, we can have different distributions at different positions in the sequence
https://en.wikipedia.org/wiki/Markov_chain#Music
*/
import {
ASSERT,
} from '../helpers';
import {
domain_containsValue,
} from '../domain';
// BODY_START
/**
* Given a domain, probability vector, value legend, and rng
* function; return one of the values in the value legend
* according to the outcome of the rng and considering the
* prob weight of each value in the legend.
* The rng should be normalized (returning values from 0 including
* up to but not including 1), unless the argument says otherwise
* (that is used for testing only, to get around rounding errors).
*
* @param {$domain} domain A regular domain. It's values only determine whether a legend value can be used, it may have values that can never be picked. It's only a filter mask.
* @param {number[]} probVector List of probabilities, maps 1:1 to val_legend.
* @param {number[]} valLegend List of values eligible for picking. Maps 1:1 to prob_vector. Only values in the current domain are actually eligible.
* @param {Function} randomFunc
* @param {boolean} [rngIsNormalized=true] Is 0<=rng()<1 or 0<=rng()<total_prob ? The latter is only used for testing to avoid rounding errors.
* @return {number | undefined}
*/
function distribution_markovSampleNextFromDomain(domain, probVector, valLegend, randomFunc, rngIsNormalized = true) {
ASSERT(!!valLegend, 'A_SHOULD_HAVE_VAL_LEGEND');
ASSERT(probVector.length <= valLegend.length, 'A_PROB_VECTOR_SIZE_SHOULD_BE_LTE_LEGEND');
// make vector & legend for available values only
let filteredLegend = [];
let cumulativeFilteredProbVector = [];
let totalProb = 0;
for (let index = 0; index < probVector.length; index++) {
let prob = probVector[index];
if (prob > 0) {
let value = valLegend[index];
if (domain_containsValue(domain, value)) {
totalProb += prob;
cumulativeFilteredProbVector.push(totalProb);
filteredLegend.push(value);
}
}
}
// no more values left to search
if (cumulativeFilteredProbVector.length === 0) {
return;
}
// only one value left
if (cumulativeFilteredProbVector.length === 1) {
return filteredLegend[0];
}
// TOFIX: could set `cumulativeFilteredProbVector[cumulativeFilteredProbVector.length-1] = 1` here...
return _distribution_markovRoll(randomFunc, totalProb, cumulativeFilteredProbVector, filteredLegend, rngIsNormalized);
}
/**
* @private
* @param {Function} rng A function ("random number generator"), which is usually normalized, but in tests may not be
* @param {number} totalProb
* @param {number[]} cumulativeProbVector Maps 1:1 to the value legend. `[prob0, prob0+prob1, prob0+prob1+prob2, etc]`
* @param {number[]} valueLegend
* @param {boolean} rngIsNormalized
* @returns {number}
*/
function _distribution_markovRoll(rng, totalProb, cumulativeProbVector, valueLegend, rngIsNormalized) {
let rngRoll = rng();
let probVal = rngRoll;
if (rngIsNormalized) { // 0 <= rng < 1
// roll should yield; 0<=value<1
ASSERT(rngRoll >= 0, 'RNG_SHOULD_BE_NORMALIZED');
ASSERT(rngRoll < 1, 'RNG_SHOULD_BE_NORMALIZED');
probVal = rngRoll * totalProb;
}
// else 0 <= rng < totalProb (mostly to avoid precision problems in tests)
for (var index = 0; index < cumulativeProbVector.length; index++) {
// note: if first element is 0.1 and roll is 0.1 this will pick the
// SECOND item. by design. so prob domains are `[x, y)`
let prob = cumulativeProbVector[index];
if (prob > probVal) {
break;
}
}
return valueLegend[index];
}
// BODY_STOP
export default distribution_markovSampleNextFromDomain;