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?
Normally, we would expect that each instance of p5 would have its own random seed.
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().
However, the code responsible to generate seeded pseudo-random values in p5js is a singleton object stored in a closure variable named lcg:
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!
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.
BtW, the p5js library is littered w/ such closure local variables, which are a dumb, poor, short-sighted attempt to make private, unaccessible states.
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!
Now, as a workaround, you can simply rewrite the “random.js” file as a class, whose instances will have their own seed state.
More discussion about pseudo-random generation:
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.
If you prefer not to, the only other feasible solution is using iframes:
Let’s say you need 10 unique seeds. Then you’re gonna need 10 iframes w/ a p5js in it.
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);
}
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.47523636still wrong
0.2472408
0.24801591
0.24879101
0.24956611
0.2503412right way
0.33733737
0.9579861
0.28542346
0.9715376
0.09700281
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.