Repository: josdejong/mathjs
Pull request #799 (2017-02-21 21:56:24)

Added Seeded Random Number Generation
I know you are not 100% committed to how you are doing distributions but this works with your current setup. I wanted seeded random number generation for testing. All of your usage is preserved. The additional functionality works as follows:

```
math.seedrandom('abc');
math.random() // Always predicts same thing
```

This works with all of your current random functions so it makes the entire system deterministic with a single call. Calling:

```
math.seedrandom();
```

Seeds the random number generator with a random seed. 

I also updated the unit tests for distribution to use this seed paradigm rather than the way you did it by replacing the random number generator manually at test time. 


Patch:
diff --git a/docs/core/configuration.md b/docs/core/configuration.md
index 9366acf0b..5e208f204 100644
--- a/docs/core/configuration.md
+++ b/docs/core/configuration.md
@@ -1,6 +1,6 @@
 # Configuration
 
-Math.js contains a number of configuration options. There are two ways to 
+Math.js contains a number of configuration options. There are two ways to
 configure math.js:
 
 - Configure an existing instance of math.js using `math.config(options)`,
@@ -37,14 +37,16 @@ The following configuration options are available:
   This setting only applies to BigNumbers, not to numbers.
   Default value is `64`.
 
-- `predictable`. Predictable output type of functions. When true, output type 
-  depends only on the input types. When false (default), output type can vary 
+- `predictable`. Predictable output type of functions. When true, output type
+  depends only on the input types. When false (default), output type can vary
   depending on input values. For example `math.sqrt(-4)` returns `complex('2i')` when
   predictable is false, and returns `NaN` when true.
-  Predictable output can be needed when programmatically handling the result of 
-  a calculation, but can be inconvenient for users when evaluating dynamic 
+  Predictable output can be needed when programmatically handling the result of
+  a calculation, but can be inconvenient for users when evaluating dynamic
   equations.
 
+- `randomSeed`. Set this option to seed pseudo random number generation, making it deterministic. The pseudo random number generator is reset with the seed provided each time this option is set. For example, setting it to `'a'` will cause `math.random()` to return `0.43449421599986604` upon the first call after setting the option every time. Set to `null` to seed the pseudo random number generator with a random seed. Default value is `null`.
+
 
 ## Examples
 
@@ -65,7 +67,7 @@ var math2 = math.create({
   matrix: 'Array'       // Choose 'Matrix' (default) or 'Array'
 });
 
-// range will output an Array 
+// range will output an Array
 math2.range(0, 4);      // Array [0, 1, 2, 3]
 
 // change the configuration of math2 from Arrays to Matrices
@@ -99,24 +101,24 @@ bigmath.eval('1 / 3');  // BigNumber, 0.33333333333333333333333333333333
 <body>
   <script type="text/javascript">
     // the default instance of math.js is available as 'math'
-  
+
     // range will output a Matrix
     math.range(0, 4);       // Matrix [0, 1, 2, 3]
-    
+
     // change the configuration of math from Matrices to Arrays
     math.config({
       matrix: 'Array'      // Choose 'Matrix' (default) or 'Array'
     });
-    
-    // range will output an Array 
+
+    // range will output an Array
     math.range(0, 4);       // Array [0, 1, 2, 3]
-    
+
     // create a new instance of math.js with bignumber configuration
     var bigmath = math.create({
       number: 'BigNumber',  // Choose 'number' (default), 'BigNumber', or 'Fraction'
       precision: 32         // 64 by default, only applicable for BigNumbers
     });
-    
+
     // parser will parse numbers as BigNumber now:
     bigmath.eval('1 / 3');  // BigNumber, 0.33333333333333333333333333333333
   </script>
diff --git a/lib/core/core.js b/lib/core/core.js
index dcf6af95b..c56383737 100644
--- a/lib/core/core.js
+++ b/lib/core/core.js
@@ -26,6 +26,9 @@ var configFactory = require('./function/config');
  *                              on input values. For example `math.sqrt(-4)`
  *                              returns `complex('2i')` when predictable is false, and
  *                              returns `NaN` when true.
+ *                            {string} randomSeed
+ *                              Random seed for seeded pseudo random number generator.
+ *                              Set to null to randomly seed.
  * @returns {Object} Returns a bare-bone math.js instance containing
  *                   functions:
  *                   - `import` to add new functions
@@ -73,7 +76,11 @@ exports.create = function create (options) {
     // on the input types. When false (default), output type can vary depending
     // on input values. For example `math.sqrt(-4)` returns `complex('2i')` when
     // predictable is false, and returns `NaN` when true.
-    predictable: false
+    predictable: false,
+
+    // random seed for seeded pseudo random number generation
+    // null = randomly seed
+    randomSeed: null
   };
 
   /**
diff --git a/lib/core/function/config.js b/lib/core/function/config.js
index b3fc04847..76151f9f3 100644
--- a/lib/core/function/config.js
+++ b/lib/core/function/config.js
@@ -8,7 +8,7 @@ function factory (type, config, load, typed, math) {
 
   /**
    * Set configuration options for math.js, and get current options.
-   * Will emit a 'config' event, with arguments (curr, prev).
+   * Will emit a 'config' event, with arguments (curr, prev, changes).
    *
    * Syntax:
    *
@@ -35,6 +35,9 @@ function factory (type, config, load, typed, math) {
    *                            {string} parenthesis
    *                              How to display parentheses in LaTeX and string
    *                              output.
+   *                            {string} randomSeed
+   *                              Random seed for seeded pseudo random number generator.
+   *                              Set to null to randomly seed.
    * @return {Object} Returns the current configuration
    */
   function _config(options) {
@@ -50,8 +53,10 @@ function factory (type, config, load, typed, math) {
 
       var curr = object.clone(config);
 
+      var changes = object.clone(options);
+
       // emit 'config' event
-      math.emit('config', curr, prev);
+      math.emit('config', curr, prev, changes);
 
       return curr;
     }
diff --git a/lib/function/probability/distribution.js b/lib/function/probability/distribution.js
index 0c57155a5..9da5f5c36 100644
--- a/lib/function/probability/distribution.js
+++ b/lib/function/probability/distribution.js
@@ -6,10 +6,13 @@ var isNumber = require('../../utils/number').isNumber;
 
 // TODO: rethink math.distribution
 // TODO: rework to a typed function
-function factory (type, config, load, typed) {
+function factory (type, config, load, typed, math) {
   var matrix = load(require('../../type/matrix/function/matrix'));
   var array = require('../../utils/array');
 
+  // seeded pseudo random number generator
+  var rng = load(require('./seededRNG'));
+
   /**
    * Create a distribution object with a set of random functions for given
    * random distribution.
@@ -204,9 +207,9 @@ function factory (type, config, load, typed) {
 
         while (result.length < number) {
           if (typeof weights === 'undefined') {
-            pick = possibles[Math.floor(Math.random() * length)];
+            pick = possibles[Math.floor(rng() * length)];
           } else {
-            var randKey = Math.random() * totalWeights;
+            var randKey = rng() * totalWeights;
 
             for (var i = 0, len = possibles.length; i < len; i++) {
               randKey -= weights[i];
@@ -264,7 +267,7 @@ function factory (type, config, load, typed) {
   var distributions = {
 
     uniform: function() {
-      return Math.random;
+      return rng;
     },
 
     // Implementation of normal distribution using Box-Muller transform
@@ -278,8 +281,8 @@ function factory (type, config, load, typed) {
         // We reject values outside of the interval [0, 1]
         // TODO: check if it is ok to do that?
         while (picked < 0 || picked > 1) {
-          u1 = Math.random();
-          u2 = Math.random();
+          u1 = rng();
+          u2 = rng();
           picked = 1/6 * Math.pow(-2 * Math.log(u1), 0.5) * Math.cos(2 * Math.PI * u2) + 0.5;
         }
         return picked;
diff --git a/lib/function/probability/seededRNG.js b/lib/function/probability/seededRNG.js
new file mode 100644
index 000000000..2485f2410
--- /dev/null
+++ b/lib/function/probability/seededRNG.js
@@ -0,0 +1,27 @@
+'use strict';
+var seedrandom = require('seed-random');
+
+function factory (type, config, load, typed, math) {
+
+  // initialize a seeded pseudo random number generator with config's random seed
+  var generator = config.randomSeed === null ? seedrandom() : seedrandom(config.randomSeed.toString());
+
+  // wrapper function so the rng can be updated via generator
+  function rng() {
+      return generator();
+  }
+
+  // updates generator with a new instance of a seeded pseudo random number generator
+  math.on('config', function (curr, prev, changes) {
+    // if the user specified a randomSeed
+    if(changes.randomSeed !== undefined) {
+      // update generator with a new instance of a seeded pseudo random number generator
+      generator = curr.randomSeed === null ? seedrandom() : seedrandom(curr.randomSeed.toString());
+    }
+  });
+
+  return rng;
+}
+
+exports.factory = factory;
+exports.math = true;
diff --git a/package.json b/package.json
index 435a76948..0eaf77b15 100644
--- a/package.json
+++ b/package.json
@@ -76,10 +76,11 @@
     "unit"
   ],
   "dependencies": {
+    "seed-random": "2.2.0",
+    "tiny-emitter": "1.0.2",
     "decimal.js": "7.1.1",
     "fraction.js": "4.0.0",
     "complex.js": "2.0.1",
-    "tiny-emitter": "1.1.0",
     "typed-function": "0.10.5"
   },
   "devDependencies": {


Test patch:
diff --git a/test/function/probability/distribution.test.js b/test/function/probability/distribution.test.js
index 130904624..aba7305ba 100644
--- a/test/function/probability/distribution.test.js
+++ b/test/function/probability/distribution.test.js
@@ -1,6 +1,5 @@
 var assert = require('assert');
 var error = require('../../../lib/error/index');
-var seed = require('seed-random');
 var _ = require('underscore');
 var math = require('../../../index');
 math.import(require('../../../lib/function/probability/distribution'));
@@ -47,14 +46,13 @@ describe('distribution', function () {
   var originalRandom, uniformDistrib;
 
   before(function () {
-    // replace the original Math.random with a reproducible one
-    originalRandom = Math.random;
-    Math.random = seed('key');
+    // Seed Random Number Generator for Reproducibility
+    math.config({randomSeed: 'test'});
   });
 
   after(function () {
-    // restore the original random function
-    Math.random = originalRandom;
+    // Randomly seed random number generator
+    math.config({randomSeed: null});
   });
 
   beforeEach(function() {
diff --git a/test/function/probability/seededrandom.test.js b/test/function/probability/seededrandom.test.js
new file mode 100644
index 000000000..9213a8551
--- /dev/null
+++ b/test/function/probability/seededrandom.test.js
@@ -0,0 +1,101 @@
+var assert = require('assert'),
+    math = require('../../../index');
+
+describe('seed', function () {
+
+  after(function () {
+    // Randomly seed random number generator
+    math.config({randomSeed: null});
+  });
+
+  it('should generate same number with seed', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.random();
+    math.config({randomSeed: 'a'});
+    var second = math.random();
+    assert.equal(first, second);
+  });
+
+  it('should generate different number subsequent calls to seeded random', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.random();
+    var second = math.random();
+    assert.notEqual(first, second);
+  });
+
+  it('calling with no parameters should unseed rng', function () {
+    math.config({randomSeed: 'a'});
+    var firstA = math.random();
+    var secondA = math.random();
+    math.config({randomSeed: 'a'});
+    var firstB = math.random();
+    math.config({randomSeed: null});
+    var secondB = math.random();
+    assert.equal(firstA, firstB);
+    assert.notEqual(secondA, secondB);
+  });
+
+  it('should generate same matrix with seed', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.random([5, 5]);
+    math.config({randomSeed: 'a'});
+    var second = math.random([5, 5]);
+    assert.equal(math.deepEqual(first, second), true);
+  });
+
+  it('should generate different matrices subsequent calls to seeded random', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.random([5, 5]);
+    var second = math.random([5, 5]);
+    assert.equal(math.deepEqual(first, second), false);
+  });
+
+  it('should pick same number with seed', function () {
+    var range = math.range(1,1000);
+    math.config({randomSeed: 'a'});
+    var first = math.pickRandom(range);
+    math.config({randomSeed: 'a'});
+    var second = math.pickRandom(range);
+    assert.equal(first, second);
+  });
+
+  it('should pick different number subsequent calls to seeded random', function () {
+    // In theory these might be the same but with 'a' as seed they are different and always will be
+    var range = math.range(1, 1000);
+    math.config({randomSeed: 'a'});
+    var first = math.pickRandom(range);
+    var second = math.pickRandom(range);
+    assert.notEqual(first, second);
+  });
+
+  it('should pick same int with seed', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.randomInt(1, 100);
+    math.config({randomSeed: 'a'});
+    var second = math.randomInt(1, 100);
+    assert.equal(first, second);
+  });
+
+  it('should pick different int subsequent calls to seeded random', function () {
+    math.config({randomSeed: 'a'});
+    var first = math.randomInt(1, 100);
+    var second = math.randomInt(1, 100);
+    assert.notEqual(first, second);
+  });
+
+  it('should work for number seeds', function () {
+    math.config({randomSeed: 1});
+    var first = math.random();
+    math.config({randomSeed: 1});
+    var second = math.random();
+    assert.equal(first, second);
+  });
+
+  it('should work for object seeds', function () {
+    math.config({randomSeed: {a: 1}});
+    var first = math.random();
+    math.config({randomSeed: {a: 1}});
+    var second = math.random();
+    assert.equal(first, second);
+  });
+});
diff --git a/test/index.test.js b/test/index.test.js
index 477949c9e..eab9e210a 100644
--- a/test/index.test.js
+++ b/test/index.test.js
@@ -11,7 +11,8 @@ describe('factory', function() {
       number: 'number',
       precision: 64,
       predictable: false,
-      epsilon: 1e-12
+      epsilon: 1e-12,
+      randomSeed: null
     });
   });
 
@@ -27,7 +28,8 @@ describe('factory', function() {
       number: 'BigNumber',
       precision: 64,
       predictable: false,
-      epsilon: 1e-12
+      epsilon: 1e-12,
+      randomSeed: null
     });
   });
 
@@ -59,7 +61,8 @@ describe('factory', function() {
       number: 'number',
       precision: 64,
       predictable: false,
-      epsilon: 1e-12
+      epsilon: 1e-12,
+      randomSeed: null
     });
 
     // restore the original config