mirror of
https://github.com/jspsych/jsPsych.git
synced 2025-05-10 19:20:55 +00:00
Merge pull request #2379 from jspsych/feature-seed-rng
Create a seedable random number generator
This commit is contained in:
commit
34c82950d4
5
.changeset/rotten-worms-float.md
Normal file
5
.changeset/rotten-worms-float.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
---
|
||||||
|
"jspsych": minor
|
||||||
|
---
|
||||||
|
|
||||||
|
Added `setSeed()` to `jsPsych.randomization` to allow for seeding the random number generator and generating predictable sequences of random numbers.
|
@ -467,6 +467,51 @@ var sample = jsPsych.randomization.sampleWithoutReplacement(myArray, 2);
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## jsPsych.randomization.setSeed
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
jsPsych.randomization.setSeed(seed)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Parameters
|
||||||
|
|
||||||
|
| Parameter | Type | Description |
|
||||||
|
| --------- | ----- | ------------------------------ |
|
||||||
|
| seed | string | A seed for the random number generator |
|
||||||
|
|
||||||
|
### Return value
|
||||||
|
|
||||||
|
Returns the seed value.
|
||||||
|
|
||||||
|
### Description
|
||||||
|
|
||||||
|
This function will override the behavior of `Math.random()` to produce a seedable pseudo random number generator.
|
||||||
|
It uses the [seedrandom package](https://www.npmjs.com/package/seedrandom).
|
||||||
|
Note that calling `setSeed()` will change how `Math.random()` behaves for the entire document.
|
||||||
|
If you have non-jsPsych components on the page that use `Math.random()` they will be affected.
|
||||||
|
|
||||||
|
Using `setSeed()` without passing in a seed will generate a random 32-bit seed.
|
||||||
|
The seed value will be returned from the function call, allowing you to save it in the data for the experiment if needed.
|
||||||
|
|
||||||
|
### Examples
|
||||||
|
|
||||||
|
#### Use a random 32-bit seed and save to data
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
const seed = jsPsych.setSeed();
|
||||||
|
jsPsych.data.addProperties({
|
||||||
|
rng_seed: seed
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Use your own seed
|
||||||
|
|
||||||
|
```javascript
|
||||||
|
jsPsych.setSeed("jspsych");
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## jsPsych.randomization.shuffle
|
## jsPsych.randomization.shuffle
|
||||||
|
|
||||||
```javascript
|
```javascript
|
||||||
|
32
package-lock.json
generated
32
package-lock.json
generated
@ -3080,6 +3080,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@types/seedrandom": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-YPLqEOo0/X8JU3rdiq+RgUKtQhQtrppE766y7vMTu8dGML7TVtZNiiiaC/hhU9Zqw9UYopXxhuWWENclMVBwKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"node_modules/@types/semver": {
|
"node_modules/@types/semver": {
|
||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz",
|
||||||
@ -12974,6 +12980,11 @@
|
|||||||
"node": ">=10"
|
"node": ">=10"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/seedrandom": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
||||||
|
},
|
||||||
"node_modules/semver": {
|
"node_modules/semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
@ -15915,11 +15926,13 @@
|
|||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"random-words": "^1.1.1"
|
"random-words": "^1.1.1",
|
||||||
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jspsych/config": "^1.1.0",
|
"@jspsych/config": "^1.1.0",
|
||||||
"@types/dom-mediacapture-record": "^1.0.11"
|
"@types/dom-mediacapture-record": "^1.0.11",
|
||||||
|
"@types/seedrandom": "^3.0.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"packages/plugin-animation": {
|
"packages/plugin-animation": {
|
||||||
@ -19123,6 +19136,12 @@
|
|||||||
"@types/node": "*"
|
"@types/node": "*"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"@types/seedrandom": {
|
||||||
|
"version": "3.0.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-3.0.2.tgz",
|
||||||
|
"integrity": "sha512-YPLqEOo0/X8JU3rdiq+RgUKtQhQtrppE766y7vMTu8dGML7TVtZNiiiaC/hhU9Zqw9UYopXxhuWWENclMVBwKQ==",
|
||||||
|
"dev": true
|
||||||
|
},
|
||||||
"@types/semver": {
|
"@types/semver": {
|
||||||
"version": "6.2.3",
|
"version": "6.2.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/semver/-/semver-6.2.3.tgz",
|
||||||
@ -24356,8 +24375,10 @@
|
|||||||
"requires": {
|
"requires": {
|
||||||
"@jspsych/config": "^1.1.0",
|
"@jspsych/config": "^1.1.0",
|
||||||
"@types/dom-mediacapture-record": "^1.0.11",
|
"@types/dom-mediacapture-record": "^1.0.11",
|
||||||
|
"@types/seedrandom": "^3.0.1",
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"random-words": "^1.1.1"
|
"random-words": "^1.1.1",
|
||||||
|
"seedrandom": "^3.0.5"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"just-debounce": {
|
"just-debounce": {
|
||||||
@ -26698,6 +26719,11 @@
|
|||||||
"xmlchars": "^2.2.0"
|
"xmlchars": "^2.2.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"seedrandom": {
|
||||||
|
"version": "3.0.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
|
||||||
|
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
|
||||||
|
},
|
||||||
"semver": {
|
"semver": {
|
||||||
"version": "5.7.1",
|
"version": "5.7.1",
|
||||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz",
|
||||||
|
@ -40,10 +40,12 @@
|
|||||||
"homepage": "https://www.jspsych.org",
|
"homepage": "https://www.jspsych.org",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"auto-bind": "^4.0.0",
|
"auto-bind": "^4.0.0",
|
||||||
"random-words": "^1.1.1"
|
"random-words": "^1.1.1",
|
||||||
|
"seedrandom": "^3.0.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@jspsych/config": "^1.1.0",
|
"@jspsych/config": "^1.1.0",
|
||||||
"@types/dom-mediacapture-record": "^1.0.11"
|
"@types/dom-mediacapture-record": "^1.0.11",
|
||||||
|
"@types/seedrandom": "^3.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,20 @@
|
|||||||
import rw from "random-words";
|
import rw from "random-words";
|
||||||
|
import seedrandom from "seedrandom";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uses the `seedrandom` package to replace Math.random() with a seedable PRNG.
|
||||||
|
*
|
||||||
|
* @param seed An optional seed. If none is given, a random seed will be generated.
|
||||||
|
* @returns The seed value.
|
||||||
|
*/
|
||||||
|
export function setSeed(seed?: string) {
|
||||||
|
if (!seed) {
|
||||||
|
const prng = seedrandom();
|
||||||
|
seed = prng.int32().toString();
|
||||||
|
}
|
||||||
|
seedrandom(seed, { global: true });
|
||||||
|
return seed;
|
||||||
|
}
|
||||||
|
|
||||||
export function repeat(array, repetitions, unpack = false) {
|
export function repeat(array, repetitions, unpack = false) {
|
||||||
const arr_isArray = Array.isArray(array);
|
const arr_isArray = Array.isArray(array);
|
||||||
|
@ -3,6 +3,7 @@ import {
|
|||||||
randomID,
|
randomID,
|
||||||
randomInt,
|
randomInt,
|
||||||
repeat,
|
repeat,
|
||||||
|
setSeed,
|
||||||
shuffle,
|
shuffle,
|
||||||
shuffleAlternateGroups,
|
shuffleAlternateGroups,
|
||||||
shuffleNoRepeats,
|
shuffleNoRepeats,
|
||||||
@ -12,7 +13,7 @@ afterEach(() => {
|
|||||||
jest.restoreAllMocks();
|
jest.restoreAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#shuffle", () => {
|
describe("shuffle", () => {
|
||||||
test("should produce fixed order with mock RNG", () => {
|
test("should produce fixed order with mock RNG", () => {
|
||||||
jest.spyOn(Math, "random").mockReturnValue(0.5);
|
jest.spyOn(Math, "random").mockReturnValue(0.5);
|
||||||
const arr = [1, 2, 3, 4, 5, 6];
|
const arr = [1, 2, 3, 4, 5, 6];
|
||||||
@ -31,7 +32,7 @@ describe("shuffleAlternateGroups", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("#randomID", () => {
|
describe("randomID", () => {
|
||||||
test("should produce ID based on mock RNG", () => {
|
test("should produce ID based on mock RNG", () => {
|
||||||
jest
|
jest
|
||||||
.spyOn(Math, "random")
|
.spyOn(Math, "random")
|
||||||
@ -165,3 +166,20 @@ describe("randomInt", () => {
|
|||||||
}).toThrowError();
|
}).toThrowError();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("setSeed", () => {
|
||||||
|
test("Replaces Math.random() with seedable RNG", () => {
|
||||||
|
setSeed("jspsych");
|
||||||
|
|
||||||
|
const r1_1 = Math.random();
|
||||||
|
const r1_2 = Math.random();
|
||||||
|
|
||||||
|
setSeed("jspsych");
|
||||||
|
|
||||||
|
const r2_1 = Math.random();
|
||||||
|
const r2_2 = Math.random();
|
||||||
|
|
||||||
|
expect(r1_1).toEqual(r2_1);
|
||||||
|
expect(r1_2).toEqual(r2_2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
93
packages/jspsych/tests/randomization/setseed.test.ts
Normal file
93
packages/jspsych/tests/randomization/setseed.test.ts
Normal file
@ -0,0 +1,93 @@
|
|||||||
|
// this file is to test that jsPsych.randomization.setSeed works in context
|
||||||
|
|
||||||
|
import htmlKeyboardResponse from "@jspsych/plugin-html-keyboard-response";
|
||||||
|
import { pressKey, startTimeline } from "@jspsych/test-utils";
|
||||||
|
|
||||||
|
import { initJsPsych } from "../../src";
|
||||||
|
|
||||||
|
describe("setSeed generates predictable randomization", () => {
|
||||||
|
test("timeline variable randomization is preserved", async () => {
|
||||||
|
const jsPsych = initJsPsych();
|
||||||
|
|
||||||
|
const seed = jsPsych.randomization.setSeed();
|
||||||
|
|
||||||
|
const { getData } = await startTimeline(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
type: htmlKeyboardResponse,
|
||||||
|
stimulus: "this is html",
|
||||||
|
data: {
|
||||||
|
i: jsPsych.timelineVariable("i"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeline_variables: [
|
||||||
|
{ i: 0 },
|
||||||
|
{ i: 1 },
|
||||||
|
{ i: 2 },
|
||||||
|
{ i: 3 },
|
||||||
|
{ i: 4 },
|
||||||
|
{ i: 5 },
|
||||||
|
{ i: 6 },
|
||||||
|
{ i: 7 },
|
||||||
|
{ i: 8 },
|
||||||
|
],
|
||||||
|
randomize_order: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
jsPsych
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
pressKey(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data_run_1 = getData().readOnly();
|
||||||
|
|
||||||
|
const jsPsych_run2 = initJsPsych();
|
||||||
|
|
||||||
|
jsPsych_run2.randomization.setSeed(seed);
|
||||||
|
|
||||||
|
const { getData: getData2 } = await startTimeline(
|
||||||
|
[
|
||||||
|
{
|
||||||
|
timeline: [
|
||||||
|
{
|
||||||
|
type: htmlKeyboardResponse,
|
||||||
|
stimulus: "this is html",
|
||||||
|
data: {
|
||||||
|
i: jsPsych_run2.timelineVariable("i"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
timeline_variables: [
|
||||||
|
{ i: 0 },
|
||||||
|
{ i: 1 },
|
||||||
|
{ i: 2 },
|
||||||
|
{ i: 3 },
|
||||||
|
{ i: 4 },
|
||||||
|
{ i: 5 },
|
||||||
|
{ i: 6 },
|
||||||
|
{ i: 7 },
|
||||||
|
{ i: 8 },
|
||||||
|
],
|
||||||
|
randomize_order: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
jsPsych_run2
|
||||||
|
);
|
||||||
|
|
||||||
|
for (let i = 0; i < 9; i++) {
|
||||||
|
pressKey(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const data_run_2 = getData2().readOnly();
|
||||||
|
|
||||||
|
console.log(data_run_1.values());
|
||||||
|
console.log(data_run_2.values());
|
||||||
|
|
||||||
|
expect(data_run_1.select("i").values).toEqual(data_run_2.select("i").values);
|
||||||
|
});
|
||||||
|
});
|
Loading…
Reference in New Issue
Block a user