From 5dbe5b7fa6d3de8385a67ace01aebe34fdc7011f Mon Sep 17 00:00:00 2001 From: Jesse Schwartzentruber Date: Tue, 11 Apr 2017 17:43:09 -0400 Subject: [PATCH] More random.js tests and fix an issue in random.float() --- random/mersennetwister.js | 4 +- random/random.js | 25 +++++--- tests/random/mersennetwister.js | 44 +++++++------ tests/random/random.js | 109 ++++++++++++++++++++++++++++---- 4 files changed, 140 insertions(+), 42 deletions(-) diff --git a/random/mersennetwister.js b/random/mersennetwister.js index bbd8611..67e0422 100644 --- a/random/mersennetwister.js +++ b/random/mersennetwister.js @@ -77,10 +77,10 @@ function MersenneTwister() y = y ^ ((y << 15) & 0xefc60000); y = y ^ (y >>> 18); - return y; + return y >>> 0; }; this.real2 = function () { return this.int32() * (1.0 / 4294967296.0); - } + }; } diff --git a/random/random.js b/random/random.js index 623175a..d7d93e4 100644 --- a/random/random.js +++ b/random/random.js @@ -9,18 +9,23 @@ var random = { this.twister.seed(seed); }, number: function (limit) { - // Returns an integer in [0, limit]. Uniform distribution. + // Returns an integer in [0, limit). Uniform distribution. if (limit == 0) { return limit; } if (limit == null || limit === undefined) { limit = 0xffffffff; } - return (this.twister.int32() >>> 0) % limit; + let x = (0x100000000 / limit) >>> 0, + y = (x * limit) >>> 0, r; + do { + r = this.twister.int32(); + } while(y && r >= y); + return (r / x) >>> 0; }, float: function () { - // Returns a float in [0, 1]. Uniform distribution. - return this.twister.real2() >>> 0; + // Returns a float in [0, 1). Uniform distribution. + return this.twister.real2(); }, range: function (start, limit) { // Returns an integer in [start, limit]. Uniform distribution. @@ -28,16 +33,16 @@ var random = { Utils.traceback(); throw new TypeError("random.range() received a non number type: '" + start + "', '" + limit + "')"); } - return random.number(limit - start + 1) + start; + return this.number(limit - start + 1) + start; }, ludOneTo: function(limit) { // Returns a float in [1, limit]. The logarithm has uniform distribution. - return Math.exp(random.float() * Math.log(limit)); + return Math.exp(this.float() * Math.log(limit)); }, item: function (list) { if (!(list instanceof Array || (typeof list != "string" && "length" in list))) { Utils.traceback(); - throw new TypeError("random.item() received a non array type: '" + list + "'"); + throw new TypeError("this.item() received a non array type: '" + list + "'"); } return list[this.number(list.length)]; }, @@ -106,13 +111,13 @@ var random = { return a; }, use: function (obj) { - return random.bool() ? obj : ""; + return this.bool() ? obj : ""; }, shuffle: function (arr) { let len = arr.length; let i = len; while (i--) { - let p = random.number(i + 1); + let p = this.number(i + 1); let t = arr[i]; arr[i] = arr[p]; arr[p] = t; @@ -120,7 +125,7 @@ var random = { }, shuffled: function (arr) { let newArray = arr.slice(); - random.shuffle(newArray); + this.shuffle(newArray); return newArray; }, subset: function (list, limit) { diff --git a/tests/random/mersennetwister.js b/tests/random/mersennetwister.js index 7f5a772..180bc43 100644 --- a/tests/random/mersennetwister.js +++ b/tests/random/mersennetwister.js @@ -1,23 +1,29 @@ -/* XXX: translate some of the dieharder tests here? */ - -QUnit.test("MersenneTwister test distribution", function(assert) { +QUnit.test("MersenneTwister test uniform distribution", function(assert) { + const N = Math.pow(2, 17), expected = N * 1.35; let mt = new MersenneTwister(); mt.seed(new Date().getTime()); - for (let i = 0; i < 100; ++i) { - let a = [], again = false; - for (let j = 0; j < 10; ++j) { - a[j] = mt.int32(); - } - a.sort(); - for (let j = 0; j < (a.length - 1); ++j) { - if (a[j] === a[j+1]) { - again = true; - } - } - if (!again) { - assert.ok(true, "no dupes in 10 entries"); - return; - } + 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]; + } + 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, "could not get unique entries"); +}); + +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]; + } + 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); }); diff --git a/tests/random/random.js b/tests/random/random.js index 96f3491..d8d9a9c 100644 --- a/tests/random/random.js +++ b/tests/random/random.js @@ -11,23 +11,110 @@ QUnit.test("random.init() with provided seed", function(assert) { }); */ +QUnit.test("random.init() is required", function(assert) { + assert.throws(random.number, /undefined/, "twister is uninitialized"); + random.init(1); + random.number(); +}); + +QUnit.test("random.number() corner cases", function(assert) { + random.init(new Date().getTime()); + let sum = 0; + for (let i = 0; i < 100; ++i) + sum += random.number(0); + assert.equal(sum, 0); + for (let i = 0; i < 100; ++i) + sum += random.number(1); + assert.equal(sum, 0); + let bins = new Uint32Array(2); + for (let i = 0; i < 100; ++i) + ++bins[random.number(2)]; + assert.equal(bins[0] + bins[1], 100); + assert.ok(bins[0] > 20); + sum = 0; + for (let i = 0; i < 12; ++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]; + } + 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); +}); + +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]; + } + 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); +}); + QUnit.test("random.range() PRNG reproducibility", function(assert) { let seed, result1, result2; seed = new Date().getTime(); - random.init(seed); - result1 = random.range(1, 20); - random.init(seed); - result2 = random.range(1, 20); - assert.equal(result1, result2, "both results are the same") + for (let t = 0; t < 50; ++t) { + random.init(seed); + result1 = random.range(1, 20); + for (let i = 0; i < 5; ++i) { + random.init(seed); + result2 = random.range(1, 20); + assert.equal(result1, result2, "both results are the same") + } + seed = random.number(); + } }); QUnit.test("random.choose() with equal distribution", function(assert) { - let foo = 0, bar = 0; + const N = 10000, expected = N * 3; + let bins = new Uint32Array(3), tmp; random.init(new Date().getTime()); - for (let i = 0; i < 100; ++i) { - let tmp = random.choose([[1, 'foo'], [1, 'bar']]); - if (tmp == "foo") { foo += 1; } - if (tmp == "bar") { bar += 1; } + 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]; } - assert.ok(bar > 0 && foo > 0, "both objects were chosen") + 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 + ")"); }); + +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]; + } + 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 + ")"); +}); + +/* +ludOneTo(limit) +item(list) +key(obj) +bool() +pick(obj) +chance(limit) +weighted(wa) +use(obj) +shuffle(arr) +shuffled(arr) +subset(list, limit) +choose(list, flat=true) +*/