Can I have multiple independent random number generators with different seeds?

I’m hoping to use multiple random number generators so that I can have different random processes that don’t interfere with each other and can be seeded for some amount of consistency. Is this possible with p5.js?

2 Likes

Normally, we would expect that each instance of p5 would have its own random seed. :seedling:

In such case, we could instantiate a new p5 and call its noCanvas(), just for the sake of using its random() method w/ a specific randomSeed(). :money_mouth_face:

However, the code responsible to generate seeded pseudo-random values in p5js is a singleton object stored in a closure variable named lcg: :neutral_face:

B/c lcg is a closure variable, no matter how many times we instantiate the class p5, there’ll only be 1 unique lcg object shared across all p5 instances! :astonished:

Instead, lcg needs to be changed into a class property, so each instance of that class would have its own lcg, rather than a shared 1. :face_with_monocle:

BtW, the p5js library is littered w/ such closure local variables, which are a dumb, poor, short-sighted attempt to make private, unaccessible states. :male_detective:

And considering the library is supposed to have multiple instances of it at the same time, it shouldn’t have any shared states at all! :no_good_man:

Now, as a workaround, you can simply rewrite the “random.js” file as a class, whose instances will have their own seed state. :bulb:

More discussion about pseudo-random generation: :sunglasses:

6 Likes

Thanks for the incredibly thorough and informative answer, @GoToLoop. If I am running p5 inside of another js environment and have access to math.random, does that change the answer? I’m working with a friend on a web app that uses javascript, p5, react, and probably some other things I’m not aware of. I’m a js noob.

The only sensible solution is creating a class for random + seed. :seedling:

If you prefer not to, the only other feasible solution is using iframes: :framed_picture:

Let’s say you need 10 unique seeds. Then you’re gonna need 10 iframes w/ a p5js in it. :grimacing:

1 Like

Gotcha. Thank you so much! This has been extremely helpful.

If you just want a standalone random class that takes a seed, I implemented one recently as part of a cross-language processing randomness kit called CommonRandom. (The goal was repeatable psuedorandom generators across JavaScript, Java, and python mode sketches). The pseudo-random method is based on XORshift. Create as many RandAnywhere objects as you like an use them individually.

Here is a demo sketch:

var rand;

function setup() {
  createCanvas(400, 200);
  //rand = new RandAnywhere(0);
  //// rand = new RandAnywhere(1536694537142); // example of a specific millisecond timecode

  // print a series of examples values from a specific seed
  // corresponding to epoch milliseconds, as if a seed was
  // requested on 2018-11-11 at 11:11:11.
  rand = new RandAnywhere(1541963471000);
  print(rand.nextLong());        // 1043807117
  print(rand.nextInt());         // 935077768
  print(rand.random());          // 0.7892195325294601
  print(rand.random(10));        // 1.1313271294959482
  print(rand.random(0, width));  // 140.0591152441032
  
  // reset seed
  rand.randomSeed(0);

  // set native seed
  randomSeed(0);
}

/**
 * draw a series of random lines
 */
function draw() {
  if (frameCount>64) {
    noLoop();
  }
  stroke(0, 0, 0);
  line(rand.random(0, width/2), rand.random(0, height), 
    rand.random(0, width/2), rand.random(0, height));
  stroke(255, 0, 0);
  translate(width/2, 0);
  line(random(0, width/2), random(0, height), 
    random(0, width/2), random(0, height));
}

/**
 * a simple cross-language pseudorandom number generator (PRNG)
 * implemented in Processing (Java)
 * inspired by https://stackoverflow.com/a/34659107/7207622
 */
function RandAnywhere(newSeed) {
  // a simple XorShift implementation
  // inspired by https://stackoverflow.com/a/34659107/7207622
  this.a = 1664525;
  this.c = 1013904223;
  this.Integer_MAX_VALUE = 2147483647;
  // Defaults to current system time in milliseconds.
  if (newSeed === undefined) {
    var d = new Date();
    newSeed = d.getMilliseconds()%this.Integer_MAX_VALUE;
  }
  this.randomSeed(newSeed);
}

/**
 * Updates seed based on past value, returns new value.
 * 
 * Returns unsigned 32bit value stored in a long.
 * (named for cross-language compatability with Java nextLong)
 */
RandAnywhere.prototype.nextLong = function() {
  // The intermediate result fits in 52 bits, so no overflow
  this.seed = ((this.seed * 1664525 + 1013904223) | 0) >>> 0;
  return this.seed;
}
/**
 * Returns next long wrapped to the max (Java) integer value.
 * (for cross-language compatability)
 * Implemented so that calls to nextInt or nextLong stay in sync,
 * the next seed internally is always updated to the next long.
 */
RandAnywhere.prototype.nextInt = function() {
  var next = this.nextLong();
  return next%this.Integer_MAX_VALUE;
}
/**
 * Set the current seed.
 */
RandAnywhere.prototype.randomSeed = function(newSeed) {
  this.seed = newSeed%this.Integer_MAX_VALUE;
}
/**
 * Return a random float in the specified range.
 * 
 * 0 arguments: 0-1
 * 1 argument:  0-max
 * 2 arguments: min-max
 */
RandAnywhere.prototype.random = function(a, b) {
  if(!a && !b){
    rmin = 0;
    rmax = 1;
  } else if(!b){
    rmin = 0;
    rmax = a;
  } else {
    rmin = a;
    rmax = b;
  }
  return map(this.nextInt(), 0, this.Integer_MAX_VALUE, rmin, rmax);
}
2 Likes

Thanks, @jeremydouglass! I’ll take a stab at plugging that into my code.

@jeremydouglass Do you find that the first number generated by this is always small? I keep seeding it with random and rerunning the code, and the first result between 0 and 5 is always .47.

Do you mean that the first five values you get back are the same? I’m not seeing that.

RandAnywhere rand;

void setup() {
  rand = new RandAnywhere(1);
  println(rand.random());          // 0.47291106
  println(rand.random());          // 0.73854136
  
  // reset seed
  rand.randomSeed(1000);
  println(rand.random());          // 0.24724080
  println(rand.random());          // 0.96466416
}

I think a property of XorShift (which I am not an expert in) is that the seed and the first value are correlated. So you can’t walk through seeds 0, 1, 2, 3 (or seeds 1000, 1001, 1002, 1003) and take the first value and expect the results to be pseudorandom. That is not the correct way to use it – choose one seed (or don’t) and then walk through the results for psuedorandomness. If you want dramatically initial values from different seeds, separate them by e.g. 100 or 1000 – but still don’t walk through seeds this way.

RandAnywhere rand;

void setup() {
  rand = new RandAnywhere();
  println("\nwrong way");
  for(int i=0; i<5; i++){
    rand.randomSeed(i);
    println(rand.random());
  }
  println("\nstill wrong");
  for(int i=1000; i<1005; i++){
    rand.randomSeed(i);
    println(rand.random());
  }
  println("\nright way");
  rand = new RandAnywhere();
  for(int i=0; i<5; i++){
    println(rand.random());
  }
}

wrong way
0.47213593
0.47291106
0.47368616
0.47446126
0.47523636

still wrong
0.2472408
0.24801591
0.24879101
0.24956611
0.2503412

right way
0.33733737
0.9579861
0.28542346
0.9715376
0.09700281

2 Likes

Thanks for the response, @jeremydouglass. That’s not quite what I meant, and I realized I was including unnecessary and confusing details instead of just posting code.

I’ve been using random() as the seed for my new generators, and the first number they generate always tends to be similar. I’ve been attempting to use random() as the seed so that I use randomSeed() for processing’s built-in random function and get consistent results from each of the RandAnywhere generators that I create.

This code generates very consistent first random values that are approximately 2.36, but the second random value always seems to have a larger distribution.

function setup() {
  noLoop();
}

function draw() {
  for (let i = 0; i < 100; i++) {
   let a = new RandAnywhere(random());
    console.log("first random " + a.random(0,5));
    console.log("second random " + a.random(0,5));
    
  }
}



function RandAnywhere(newSeed) {
  // a simple XorShift implementation
  // inspired by https://stackoverflow.com/a/34659107/7207622
  this.a = 1664525;
  this.c = 1013904223;
  this.Integer_MAX_VALUE = 2147483647;
  // Defaults to current system time in milliseconds.
  if (newSeed === undefined) {
    var d = new Date();
    newSeed = d.getMilliseconds()%this.Integer_MAX_VALUE;
  }
  this.randomSeed(newSeed);
}

/**
 * Updates seed based on past value, returns new value.
 * 
 * Returns unsigned 32bit value stored in a long.
 * (named for cross-language compatability with Java nextLong)
 */
RandAnywhere.prototype.nextLong = function() {
  // The intermediate result fits in 52 bits, so no overflow
  this.seed = ((this.seed * 1664525 + 1013904223) | 0) >>> 0;
  return this.seed;
}
/**
 * Returns next long wrapped to the max (Java) integer value.
 * (for cross-language compatability)
 * Implemented so that calls to nextInt or nextLong stay in sync,
 * the next seed internally is always updated to the next long.
 */
RandAnywhere.prototype.nextInt = function() {
  var next = this.nextLong();
  return next%this.Integer_MAX_VALUE;
}
/**
 * Set the current seed.
 */
RandAnywhere.prototype.randomSeed = function(newSeed) {
  this.seed = newSeed%this.Integer_MAX_VALUE;
}
/**
 * Return a random float in the specified range.
 * 
 * 0 arguments: 0-1
 * 1 argument:  0-max
 * 2 arguments: min-max
 */
RandAnywhere.prototype.random = function(a, b) {
  if(!a && !b){
    rmin = 0;
    rmax = 1;
  } else if(!b){
    rmin = 0;
    rmax = a;
  } else {
    rmin = a;
    rmax = b;
  }
  return map(this.nextInt(), 0, this.Integer_MAX_VALUE, rmin, rmax);
}

The first and second values are shown here…

first random 2.362770860717991 
second random 3.525784948619914 
first random 2.363426078745828 
second random 4.152572226781665 
first random 2.361602986865492 
second random 4.570561526143254 
first random 2.3624849167477735 
second random 2.564898146113799 
first random 2.3632667993024303 
second random 4.027956830816323 
first random 2.3640642186459453 
second random 1.352388975840243 
first random 2.3611599264485577 
second random 2.0854213680538445 
first random 2.361820350569589 
second random 1.3778809161753771 
first random 2.3611923131911046 
second random 0.9939639833727685 
first random 2.3614427574730676 
second random 2.8647322221029237 
first random 2.3619068401688277 
second random 0.3419810232436196 
first random 2.3606833407472276 
second random 3.7966072344205375 
first random 2.361152182501346 
second random 4.195427645088838 
first random 2.362037302163447 
second random 2.4992325145282 
first random 2.3645188041797462 
second random 3.0213742717268754 
first random 2.3635844315232637 
second random 2.734728966716085 
first random 2.3631429171018037 
second random 2.8229369259546218 
first random 2.363937826530979 
second random 0.9695539092503274 
first random 2.3624895454209716 
second random 0.2694403963486852 
first random 2.3635319817641434 
second random 0.4307937041999743 
first random 2.3642933263277137 
second random 2.7078527946527364 
first random 2.3620608390132247 
second random 1.6769073725104833 
first random 2.363296459598139 
second random 3.3982605223535844 
first random 2.3643980349248266 
second random 1.9979303199788232 
first random 2.364150826988812 
second random 0.5141408161791697 
first random 2.3629668040028617 
second random 4.678281049559955 
first random 2.362915171479301 
second random 3.734654806896418 
first random 2.3638024960475987 
second random 0.7085811629465694 
first random 2.364173458127386 
second random 3.18423673658829 
first random 2.363136667927325 
second random 2.421029781653094 

It looks like the issue is reduced if I use random(1000000) as the seed.

I think you might be misunderstanding. The point of a stable PNRG with a seed is for the values to be identical, not very close. The “very close” values are more like a bug – in this case, caused by using that PNRG in the wrong way. There is also a limitation in the code – it generates a default seed from Date milliseconds, so if you create a batch of randomseeds very close together their first values will be correlated.

So, coming back to your original question:

var rand1;
var rand2;
var rand3;
function setup() {
  createCanvas(400, 200);
  rand1 = new RandAnywhere(0);
  rand2 = new RandAnywhere(1000);
  rand3 = new RandAnywhere(2000);
}

Then use rand1.random() as one generator, rand2.random() as another, etc. – no more setting seeds, just call each with no arguments to get the next random number.