total-serialism
Version:
A set of methods for the generation and transformation of number sequences useful in algorithmic composition
597 lines (564 loc) • 16.9 kB
JavaScript
//=======================================================================
// gen-stochastic.js
// part of 'total-serialism' Package
// by Timo Hoogland (@t.mo / @tmhglnd), www.timohoogland.com
// MIT License
//
// Stochastic and Probablity Theory algorithms to generate
// various forms of random
// number sequences
//
// credits:
// - Gratefully using the seedrandom package by David Bau
//=======================================================================
// require Generative methods
const { spread } = require('./gen-basic.js');
const { lookup } = require('./transform.js');
const { fold, size, toArray } = require('./utility');
const { change } = require('./statistic');
// require seedrandom package
let seedrandom = require('seedrandom');
// local pseudorandom number generator and seed storage
let rng = seedrandom();
let _seed = 0;
// Set the seed for all the Random Number Generators.
// 0 sets to unpredictable seeding
//
// @param {Number/String} -> the seed
// @return {Void}
//
function seed(v=0){
if (v === 0 || v === null || v === undefined){
rng = seedrandom();
_seed = 0;
} else {
rng = seedrandom(v);
_seed = v;
}
// also return the seed that has been set
return getSeed();
}
exports.seed = seed;
// Return the seed that was set
//
// @return {Value} -> the seed
//
function getSeed(){
return _seed;
}
exports.getSeed = getSeed;
// generate a list of random float values
// between a certain specified range (excluding high val)
//
// @param {Int} -> number of values to output
// @param {Number} -> minimum range (optional, default=0)
// @param {Number} -> maximum range (optional, defautl=1)
// @return {Array}
//
function randomFloat(len=1, lo=1, hi=0){
// swap if lo > hi
if (lo > hi){ var t=lo, lo=hi, hi=t; }
// len is positive and minimum of 1
len = size(len);
var arr = [];
for (var i=0; i<len; i++){
arr[i] = (rng() * (hi - lo)) + lo;
}
return arr;
}
exports.randomFloat = randomFloat;
exports.randomF = randomFloat;
// generate a list of random integer values
// between a certain specified range (excluding high val)
//
// @param {Int} -> number of values to output
// @param {Number} -> minimum range (optional, default=0)
// @param {Number} -> maximum range (optional, defautl=2)
// @return {Array}
//
function random(len=1, lo=12, hi=0){
var arr = randomFloat(len, lo, hi);
return arr.map(v => Math.floor(v));
}
exports.random = random;
// generate a list of random float values but the next random
// value is within a limited range of the previous value generating
// a random "drunk" walk, also referred to as brownian motion.
// Inspired by the [drunk]-object in MaxMSP
//
// @param {Int} -> length of output array
// @param {Number} -> step range for next random value
// @param {Number} -> minimum range (optional, default=null)
// @param {Number} -> maximum range (optional, default=null)
// @param {Number} -> starting point
// @param {Bool} -> fold between lo and hi range
// @return {Array}
//
function drunkFloat(len=1, step=1, lo=1, hi=0, p, bound=true){
// swap if lo > hi
if (lo > hi){ var t=lo, lo=hi, hi=t; }
p = (!p)? (lo+hi)/2 : p;
// len is positive and minimum of 1
len = size(len);
var arr = [];
for (var i=0; i<len; i++){
// direction of next random number (+ / -)
var dir = (rng() > 0.5) * 2 - 1;
// prev + random value * step * direction
p += rng() * step * dir;
if (bound && (p > hi || p < lo)){
p = fold(p, lo, hi);
}
arr.push(p);
}
return arr;
}
exports.drunkFloat = drunkFloat;
exports.drunkF = drunkFloat;
exports.walkFloat = drunkFloat;
// generate a list of random integer values but the next random
// value is within a limited range of the previous value generating
// a random "drunk" walk, also referred to as brownian motion.
// Inspired by the [drunk]-object in MaxMSP
//
// @param {Int} -> length of output array
// @param {Number} -> step range for next random value
// @param {Number} -> minimum range (optional, default=null)
// @param {Number} -> maximum range (optional, default=null)
// @param {Number} -> starting point
// @param {Bool} -> fold between lo and hi range
// @return {Array}
//
function drunk(len=1, step=1, lo=12, hi=0, p, bound=true){
let arr = drunkFloat(len, step, lo, hi, p, bound);
return arr.map(v => Math.floor(v));
}
exports.drunk = drunk;
exports.walk = drunk;
// generate a list of random integer values 0 or 1
// like a coin toss, heads/tails
//
// @param {Int} -> number of tosses to output
// @return {Array}
//
function coin(len=1){
var arr = randomFloat(len, 0, 2);
return arr.map(v => Math.floor(v));
}
exports.coin = coin;
// generate a list of random integer values 1 to 6
// like the roll of a dice
//
// @param {Int} -> number of tosses to output
// @param {Int} -> sides of the die (optional, default=6)
// @return {Array}
//
function dice(len=1, sides=6){
var arr = randomFloat(len, 1, sides+1);
return arr.map(v => Math.floor(v));
}
exports.dice = dice;
// Generate random clave patterns. Outputs a binary list as rhythm,
// where 1's represent onsets and 0's represent rests.
//
// @param {Int} -> output length of rhythm (default=8)
// @param {Int} -> maximum gap between onsets (default=3)
// @param {Int} -> minimum gap between onsets (default=2)
//
function clave(len=8, max=3, min=2){
let arr = [];
// set list length to minimum of 1
len = size(len);
// swap if lo > hi
if (min > max){ var t=min, min=max; max=t; }
// limit lower ranges
min = Math.max(1, min);
max = Math.max(min, max) + 1;
let sum = 0;
let rtm = [];
// randomly generate list of gap intervals
while (sum < len){
let r = Math.floor(rng() * (max - min)) + min;
rtm.push(r);
sum += r;
}
// convert rhythmic "gaps" to binary pattern
rtm.forEach((g) => {
for (let i=0; i<g; i++){
arr.push(!i ? 1 : 0);
}
});
return arr.slice(0, len);
}
exports.clave = clave;
// shuffle a list, based on the Fisher-Yates shuffle algorithm
// by Ronald Fisher and Frank Yates in 1938
// The algorithm has run time complexity of O(n)
//
// @param {Array} -> array to shuffle
// @return {Array}
//
function shuffle(a=[0]){
// slice array to avoid changing the original array
var arr = a.slice();
for (var i=arr.length-1; i>0; i-=1) {
var j = Math.floor(rng() * (i + 1));
var t = arr[i];
arr[i] = arr[j];
arr[j] = t;
}
return arr;
}
exports.shuffle = shuffle;
exports.scramble = shuffle;
// Generate a list of 12 semitones
// then shuffle the list based on a random seed
//
// @return {Array} -> twelve-tone series
//
function twelveTone(){
return shuffle(spread(12));
}
exports.twelveTone = twelveTone;
exports.toneRow = twelveTone;
// Generate a list of unique random integer values between a
// certain specified range (excluding high val). An 'urn' is filled
// with values and when one is picked it is removed from the urn.
// If the outputlist is longer then the range, the urn refills when
// empty. On refill it is made sure no repeating value can be picked.
// Inspired by the [urn]-object in MaxMSP
//
// @param {Int} -> number of values to output
// @param {Number} -> maximum range (optional, default=12)
// @param {Number} -> minimum range (optional, defautl=0)
// @return {Array} -> random values
//
function urn(len=1, hi=12, lo=0){
// swap if lo > hi
if (lo > hi){ var t=lo, lo=hi, hi=t; }
// generate array with values and pick
return pick(len, spread(hi-lo, lo, hi));
}
exports.urn = urn;
// Choose random items from an array provided
// The default array is an array of 0 and 1
//
// @param {Int} -> output length
// @param {Array} -> items to choose from
// @return {Array} -> randomly selected items
//
function choose(len=1, a=[0, 1]){
// if a is no Array make it an array
a = toArray(a);
// set the size to minimum of 1 or based on array length
len = size(len);
var arr = [];
for (var i=0; i<len; i++){
arr.push(a[Math.floor(rng()*a.length)]);
}
return arr;
}
exports.choose = choose;
// Pick random items from an array provided
// An 'urn' is filled with values and when one is picked it is removed
// from the urn. If the outputlist is longer then the range, the urn
// refills when empty. On refill it is made sure no repeating value
// can be picked.
//
// @param {Int} -> output length
// @param {Array} -> items to choose from
// @return {Array} -> randomly selected items
//
function pick(len=1, a=[0, 1]){
// set the size to minimum of 1 or based on array length
len = size(len);
// fill the jar with the input
// var jar = (!Array.isArray(a))? [a] : a;
let jar = toArray(a);
if (jar.length < 2){
return new Array(len).fill(jar[0]);
}
// shuffle the jar
let s = shuffle(jar);
// value, previous, output-array
let v, p, arr = [];
for (let i=0; i<len; i++){
v = s.pop();
if (v === undefined){
s = shuffle(jar);
v = s.pop();
if (v === p) {
v = s.pop();
s.push(p);
}
}
arr[i] = v;
p = v;
}
return arr;
}
exports.pick = pick;
// expand an array based upon the pattern within an array
// the pattern is derived from the rate in change between values
// the newly generated values are selected randomly from the list
// of changes.
//
// @param {Array} -> the array to expand
// @param {Number} -> the resulting array length
// @return {Array}
//
function expand(a=[0, 0], l=0){
a = toArray(a);
l = size(l);
// return a if output length is smaller/equal then input array
if (l <= a.length){ return a; }
// get the differences and pick the expansion options
let p = change(a);
let chg = pick(l-a.length, p);
// empty output array and axiom for output
let arr = a.slice();
let acc = arr[arr.length-1];
// accumulate the change and store in array
for (let c=0; c<chg.length; c++){
arr.push(acc += chg[c]);
}
return arr;
}
exports.expand = expand;
exports.extrapolate = expand;
// Initialize a Markov Chain Model (One of the simpelest forms of ML)
// A Markov chain is a stochastic model describing a sequence
// of possible events in which the probability of each event depends
// only on the state of the previous (multiple) events.
//
// @get table -> return transition table from Markov
// @method clear() -> erase the transition table
// @method train() -> train the markov model
// @param {Array} -> array of values as training data
// @method seed() -> seed the random number generator (global RNG)
// @param {Value} -> any value as random seed (0 = unpredictable seed)
// @method state() -> set the initial value to start the chain
// @method next() -> generate the next value based state or set axiom
// @method chain() -> generate an array of values (default length=2)
//
class MarkovChain {
constructor(data){
// transition probabilities table
this._table = {};
// train if dataset is provided
if (data) { this.train(data) };
// current state of markov chain
this._state;
}
get table(){
// output a copy of the table as an object
return { ...this._table };
}
read(t){
// read a markov chain table from a json file
if (Array.isArray(t) || typeof t !== 'object'){
console.error(`Error: input is not a valid json formatted table. If your input is an array use train() instead.`);
return false;
}
this._table = t;
return true;
}
clear(){
// empty the transition probabilities
this._table = {};
}
train(a){
if (!Array.isArray(a)){
return console.error(`Error: train() expected array but received: ${typeof a}`);
}
// build a transition table from array of values
for (let i=1; i<a.length; i++){
if (!this._table[a[i-1]]) {
this._table[a[i-1]] = [a[i]];
} else {
this._table[a[i-1]].push(a[i]);
}
}
}
seed(s){
// deprecated, seed is now also be set for the global rng
seed(s);
}
state(a){
// set the state
if (!this._table[a]){
console.error(`Warning: ${a} is not part of transition table`);
}
this._state = a;
}
randomState(){
let states = Object.keys(this._table);
this._state = states[Math.floor(rng() * states.length)];
}
next(){
// if the state is undefined or has no transition in table
// randomly choose from all
if (this._state === undefined || !this._table[this._state]){
this.randomState();
}
// get probabilities based on state
let probs = this._table[this._state];
// select pseudorandomly next value
this._state = probs[Math.floor(rng() * probs.length)];
return this._state;
}
chain(l=2){
// return an array of values generated with next()
let c = [];
for (let i=0; i<l; i++){
c.push(this.next());
}
return c;
}
}
exports.MarkovChain = MarkovChain;
// Initialize a Deep Markov Chain Model (with higher order n)
//
// @get table -> return transition table from Markov
// @method clear() -> erase the transition table
// @method train() -> train the markov model
// @param {Array} -> array of values as training data
// @param {Int+} -> order of markov analysis
// @method seed() -> seed the random number generator (global RNG)
// @param {Value} -> any value as random seed (0 = unpredictable seed)
// @method state() -> set the initial value to start the chain
// @method next() -> generate the next value based state or set axiom
// @method chain() -> generate an array of values (default length=2)
//
class DeepMarkov {
constructor(data, order){
// transition probabilities table
this._table = new Map();
// train if dataset is provided
if (data) { this.train(data, order) };
// current state of markov chain
this._state = '';
}
get table(){
// return copy of Map object
return new Map(JSON.parse(JSON.stringify(Array.from(this._table))));
}
read(t){
// read a markov chain table from a Map() generated with DeepMarkov
if (Array.isArray(t) || t instanceof Map === false){
console.error(`Error: input is not a valid Map() formatted table. If your input is an array use train() instead.`);
return false;
}
this._table = t;
return true;
}
stringify(){
// return stringified version of the DeepMarkov table
return JSON.stringify(this._table, replacer);
}
parse(p){
// parse an incoming string to a Map() for transition table
try {
let parsed = JSON.parse(p, reviver);
if (parsed instanceof Map === false){
console.error(`Error: input is not a valid string that can be parsed to a Map().`)
return false;
}
this._table = parsed;
return true;
} catch (e) {
console.error(`Error: input is not a valid string that can be parsed to a Map().`);
return false;
}
}
clear(){
// empty the transition probabilities
this._table = new Map();
}
train(a, o=2){
if (!Array.isArray(a)){
return console.error(`Error: train() expected array but received: ${typeof a}`);
}
if (o < 1){
return console.error(`Error: train() expected order greater then 1 but received ${o}`);
}
// build a transition table from array of values
for (let i=0; i<(a.length-o); i++) {
let slice = a.slice(i, i+o);
let key = JSON.stringify(slice);
let next = a[i+o];
if (this._table.has(key)) {
let arr = this._table.get(key);
arr.push(next);
this._table.set(key, arr);
} else {
this._table.set(key, [a[i+o]]);
}
}
}
seed(s){
// deprecated, seed is now also be set for the global rng
seed(s);
}
state(a){
// stringify the state
let s = JSON.stringify(a);
// set the state
if (!this._table.has(s)) {
console.error(`Warning: ${a} is not part of transition table`);
}
this._state = s;
}
randomState() {
let keys = Array.from(this._table.keys())
this._state = keys[Math.floor(rng() * keys.length)]
}
next(){
// if the state is undefined or has no transition in table
// randomly choose from all
if (this._state === undefined || !this._table.has(this._state)) {
this.randomState();
}
// get probabilities based on state
let probs = this._table.get(this._state);
let newState = probs[Math.floor(rng() * probs.length)]
// Now recreate a nice string representation
let prefix = JSON.parse(this._state);
prefix.shift();
prefix.push(newState);
this._state = JSON.stringify(prefix);
return newState;
}
chain(l=2){
// return an array of values generated with next()
let c = [];
for (let i=0; i<l; i++){
c.push(this.next());
}
return c;
}
}
exports.DeepMarkov = DeepMarkov;
exports.DeepMarkovChain = DeepMarkov;
// functions thanks to:
// https://stackoverflow.com/questions/29085197/how-do-you-json-stringify-an-es6-map
// helper function for Stringifying a Map() in DeepMarkov
function replacer(key, value) {
if (value instanceof Map) {
return {
dataType: 'Map',
value: [...value]
// value: [Array.from(value.entries())],
};
}
return value;
}
// helper function for parsing a Map() in DeepMarkov
function reviver(key, value) {
if (typeof value === 'object' && value !== null) {
if (value.dataType === 'Map') {
return new Map(value.value);
}
}
return value;
}