From 8b20380b282953248a2db7e788d716f8dafee1c4 Mon Sep 17 00:00:00 2001 From: Jesse Schwartzentruber Date: Wed, 12 Apr 2017 17:03:57 -0400 Subject: [PATCH] Add more random tests. Use chi-squared goodness of fit tests for random distributions. --- random/random.js | 4 +- tests/random/mersennetwister.js | 65 +++++-- tests/random/random.js | 332 ++++++++++++++++++++++++++------ 3 files changed, 318 insertions(+), 83 deletions(-) diff --git a/random/random.js b/random/random.js index b1d9ed2..8ef510e 100644 --- a/random/random.js +++ b/random/random.js @@ -40,8 +40,8 @@ var random = { return Math.exp(this.float() * Math.log(limit)); }, item: function (list) { - if (!(list instanceof Array || (typeof list != "string" && "length" in list))) { - Utils.traceback(); + if (!(list instanceof Array || (list !== undefined && typeof list != "string" && list.hasOwnProperty("length")))) { + //Utils.traceback(); throw new TypeError("this.item() received a non array type: '" + list + "'"); } return list[this.number(list.length)]; diff --git a/tests/random/mersennetwister.js b/tests/random/mersennetwister.js index 180bc43..b283529 100644 --- a/tests/random/mersennetwister.js +++ b/tests/random/mersennetwister.js @@ -1,29 +1,54 @@ QUnit.test("MersenneTwister test uniform distribution", function(assert) { - const N = Math.pow(2, 17), expected = N * 1.35; + const N = Math.pow(2, 17), TRIES = 10, XSQ = 293.25; // quantile of chi-square dist. k=255, p=.05 let mt = new MersenneTwister(); - mt.seed(new Date().getTime()); - let data = new Uint32Array(N); - for (let i = 0; i < data.length; ++i) { - data[i] = mt.int32(); - } - for (let sh = 0; sh <= 24; ++sh) { - let bins = new Uint32Array(256); - for (let b of data) { - ++bins[(b >>> sh) & 0xFF]; + mt.seed(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let data = new Uint32Array(N), sh; + for (let i = 0; i < data.length; ++i) { + data[i] = mt.int32(); + } + for (sh = 0; sh <= 24; ++sh) { + let bins = new Uint32Array(256); + for (let b of data) { + ++bins[(b >>> sh) & 0xFF]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 511) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq >= XSQ) + break; + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + } + if (sh == 25) { + return; } - let variance = bins.reduce(function(a, v){ return a + Math.pow(v - N / bins.length, 2); }, 0); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance); } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ); }); QUnit.test("MersenneTwister test float distribution", function(assert) { - const N = Math.pow(2, 17), expected = N * 1.3; - let mt = new MersenneTwister(); - mt.seed(new Date().getTime()); - let bins = new Uint32Array(512); - for (let i = 0; i < N; ++i) { - ++bins[(mt.real2() * bins.length) >>> 0]; + const N = Math.pow(2, 17), TRIES = 3, XSQ = 564.7; // quantile of chi-square dist. k=511, p=.05 + let tries = [], mt = new MersenneTwister(); + mt.seed(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(512); + for (let i = 0; i < N; ++i) { + let tmp = (mt.real2() * bins.length) >>> 0; + if (tmp >= bins.length) throw "random.float() >= 1.0"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 511) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); } - let variance = bins.reduce(function(a, v){ return a + Math.pow(v - N / bins.length, 2); }, 0); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance); + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); }); diff --git a/tests/random/random.js b/tests/random/random.js index 85b6d2a..07c1a50 100644 --- a/tests/random/random.js +++ b/tests/random/random.js @@ -1,24 +1,11 @@ -/* -QUnit.test("random.init() with no seed value", function(assert) { - random.init(); - assert.ok(random.seed, "random seed is not null."); -}); - -QUnit.test("random.init() with provided seed", function(assert) { - let seed = new Date().getTime(); - random.init(seed); - assert.equal(random.seed, seed, "seed is correct"); -}); -*/ - QUnit.test("random.init() is required", function(assert) { - assert.throws(random.number, /undefined/, "twister is uninitialized"); + assert.throws(random.number, /undefined/, "twister should be uninitialized before random.init()"); random.init(1); random.number(); }); QUnit.test("random.number() corner cases", function(assert) { - random.init(new Date().getTime()); + random.init(Math.random() * 0x100000000); let sum = 0; for (let i = 0; i < 100; ++i) sum += random.number(0); @@ -32,40 +19,115 @@ QUnit.test("random.number() corner cases", function(assert) { assert.equal(bins[0] + bins[1], 100); assert.ok(bins[0] > 20); sum = 0; - for (let i = 0; i < 12; ++i) + for (let i = 0; i < 15; ++i) sum |= random.number(); assert.equal(sum>>>0, 0xFFFFFFFF); }); -QUnit.test("random.float() uniform distribution", function(assert) { - const N = Math.pow(2, 17), expected = N * 2; - random.init(new Date().getTime()); - let bins = new Uint32Array(512), tmp; - for (let i = 0; i < N; ++i) { - tmp = (random.float() * bins.length) >>> 0; - if (tmp >= bins.length) throw "random.float() >= 1.0"; - ++bins[tmp]; +QUnit.test("random.number() uniform distribution", function(assert) { + const N = Math.pow(2, 17), TRIES = 3, XSQ = 564.7; // quantile of chi-square dist. k=511, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(512); + for (let i = 0; i < N; ++i) { + let tmp = random.number(bins.length); + if (tmp >= bins.length) throw "random.number() >= limit"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 511) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); } - let variance = bins.reduce(function(a, v){ return a + Math.pow(v - N / bins.length, 2); }, 0); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance); + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +QUnit.test("random.float() uniform distribution", function(assert) { + const N = Math.pow(2, 17), TRIES = 3, XSQ = 564.7; // quantile of chi-square dist. k=511, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(512); + for (let i = 0; i < N; ++i) { + let tmp = (random.float() * bins.length) >>> 0; + if (tmp >= bins.length) throw "random.float() >= 1.0"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 511) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); }); QUnit.test("random.range() uniform distribution", function(assert) { - const N = 10000, expected = N * 2; - let bins = new Uint32Array(50), tmp; - random.init(new Date().getTime()); - for (let i = 0; i < N; ++i) { - tmp = random.range(0, bins.length - 1); - if (tmp >= bins.length) throw "random.range() > upper bound"; - ++bins[tmp]; + const N = 1e4, TRIES = 3, XSQ = 66.34; // quantile of chi-square dist. k=49, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(50); + for (let i = 0; i < N; ++i) { + let tmp = random.range(0, bins.length - 1); + if (tmp >= bins.length) throw "random.range() > upper bound"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 49) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); } - let variance = bins.reduce(function(a, v){ return a + Math.pow(v - N / bins.length, 2); }, 0); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance); + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +QUnit.test("random.range() uniform distribution with offset", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 66.34; // quantile of chi-square dist. k=49, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(50); + for (let i = 0; i < N; ++i) { + let tmp = random.range(10, 10 + bins.length - 1) - 10; + if (tmp < 0) throw "random.range() < lower bound"; + if (tmp >= bins.length) throw "random.range() > upper bound"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 49) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); }); QUnit.test("random.range() PRNG reproducibility", function(assert) { let seed, result1, result2; - seed = new Date().getTime(); + seed = Math.random() * 0x100000000; for (let t = 0; t < 50; ++t) { random.init(seed); result1 = random.range(1, 20); @@ -78,44 +140,192 @@ QUnit.test("random.range() PRNG reproducibility", function(assert) { } }); -QUnit.test("random.choose() with equal distribution", function(assert) { - const N = 10000, expected = N * 3; - let bins = new Uint32Array(3), tmp; - random.init(new Date().getTime()); - for (let i = 0; i < N; ++i) { - tmp = random.choose([[1, 0], [1, 1], [1, 2]]); - if (tmp >= bins.length) throw "random.choose() > upper bound"; - ++bins[tmp]; +QUnit.test("random.ludOneTo() distribution", function(assert) { + const N = 1e5, TRIES = 3, XSQ = 123.22; // quantile of chi-square dist. k=99, p=.05 + let dist = new Uint32Array(100), tries = []; + random.init(Math.random() * 0x100000000); + /* build the ideal distribution for comparison + * I thought this would be the PDF of the log-normal distribution, but I couldn't get mu & sigma figured out? */ + for (let i = 0; i < (100 * dist.length); ++i) { + dist[Math.floor(Math.exp(i / (100*dist.length) * Math.log(dist.length)))] += N / (dist.length * 100); } - let variance = Math.pow(bins[0] - N / 3, 2) + Math.pow(bins[1] - N / 3, 2) + Math.pow(bins[2] - N / 3, 2); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance + " (" + bins + ")"); + assert.equal(dist[0], 0); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(dist.length), xsq = 0; + for (let i = 0; i < N; ++i) { + let tmp = random.ludOneTo(bins.length)>>>0; + if (tmp >= bins.length) throw "random.ludOneTo() > upper bound"; // this could happen.. + ++bins[tmp]; + } + assert.equal(bins[0], 0); + for (let i = 1; i < bins.length; ++i) { + xsq += Math.pow(bins[i] - dist[i], 2) / dist[i]; + } + /* + * XSQ = scipy.stats.chi2.isf(.05, 99) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); }); -QUnit.test("random.choose() with unequal distribution", function(assert) { - const N = 10000, expected = N * 3; - let bins = new Uint32Array(3), tmp; - random.init(new Date().getTime()); - for (let i = 0; i < N; ++i) { - tmp = random.choose([[1, 0], [2, 1], [1, 2]]); - if (tmp >= bins.length) throw "random.choose() > upper bound"; - ++bins[tmp]; +QUnit.test("random.item() exception cases", function(assert) { + assert.throws(random.item, /non array type/); + assert.throws(function(){ return random.item(1); }, /non array type/); + assert.throws(function(){ return random.item("1"); }, /non array type/); + assert.throws(function(){ return random.item({}); }, /non array type/); +}); + +QUnit.test("random.item() distribution with list", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 5.99; // quantile of chi-square dist. k=2, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(3); + for (let i = 0; i < N; ++i) { + let tmp = random.item([99, 100, 101]) - 99; + if (tmp < 0) throw "random.item() < lower bound"; + if (tmp >= bins.length) throw "random.item() > upper bound"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 2) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); } - let variance = Math.pow(bins[0] - N / 4, 2) + Math.pow(bins[1] - N / 2, 2) + Math.pow(bins[2] - N / 4, 2); - assert.ok(variance < expected, "Expecting variance to be under " + expected + ", got " + variance + " (" + bins + ")"); + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +QUnit.test("random.key() distribution", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 5.99; // quantile of chi-square dist. k=2, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(3); + for (let i = 0; i < N; ++i) { + let tmp = random.key({99: 0, 100: 0, 101: 0}) - 99; + if (tmp < 0) throw "random.key() < lower bound"; + if (tmp >= bins.length) throw "random.key() > upper bound"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 2) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +QUnit.test("random.bool() distribution", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 3.84; // quantile of chi-square dist. k=1, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(2); + for (let i = 0; i < N; ++i) { + let tmp = random.bool(); + if (tmp === true) + tmp = 1; + else if (tmp === false) + tmp = 0; + else + assert.ok(false, "unexpected random.bool() result: " + tmp); + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 1) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); }); /* -ludOneTo(limit) -item(list) -key(obj) -bool() +XXX pick(obj) chance(limit) +*/ + +QUnit.test("random.choose() with equal distribution", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 5.99; // quantile of chi-square dist. k=2, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(3); + for (let i = 0; i < N; ++i) { + let tmp = random.choose([[1, 0], [1, 1], [1, 2]]); + if (tmp >= bins.length) throw "random.choose() > upper bound"; + ++bins[tmp]; + } + let xsq = bins.reduce(function(a, v){ let e = N / bins.length; return a + Math.pow(v - e, 2) / e; }, 0); + /* + * XSQ = scipy.stats.chi2.isf(.05, 2) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +QUnit.test("random.choose() with unequal distribution", function(assert) { + const N = 1e4, TRIES = 3, XSQ = 5.99; // quantile of chi-square dist. k=2, p=.05 + let tries = []; + random.init(Math.random() * 0x100000000); + for (let attempt = 0; attempt < TRIES; ++attempt) { + let bins = new Uint32Array(3); + for (let i = 0; i < N; ++i) { + let tmp = random.choose([[1, 0], [2, 1], [1, 2]]); + if (tmp >= bins.length) throw "random.choose() > upper bound"; + ++bins[tmp]; + } + let xsq = Math.pow(bins[0] - N / 4, 2) / (N / 4) + Math.pow(bins[1] - N / 2, 2) / (N / 2) + Math.pow(bins[2] - N / 4, 2) / (N / 4); + /* + * XSQ = scipy.stats.chi2.isf(.05, 2) + * if xsq > XSQ, the result is biased at 95% significance + */ + if (xsq < XSQ) { + assert.ok(true, "Expected x^2 to be < " + XSQ + ", got " + xsq + " on attempt #" + (attempt + 1)); + return; + } + tries.push(xsq); + } + assert.ok(false, "Failed in " + TRIES + " attempts to get xsq lower than " + XSQ + ": "+ tries); +}); + +/* +XXX +choose(list, flat=true) weighted(wa) use(obj) shuffle(arr) shuffled(arr) subset(list, limit) -choose(list, flat=true) pop(arr) */