From 2343a2705c35d21f92c004d99cdd751cdfad6a2a Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:36:12 +0100 Subject: [PATCH 01/12] Alias numpy.random.random --- src/util/Util.js | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/util/Util.js b/src/util/Util.js index 2b01f964..f8e84ecc 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -307,6 +307,36 @@ export function IsPointInsidePolygon(point, vertices) return isInside; } +/** + * Return random floats a-la NumPy's in the half-open interval [0.0, 1.0). In other words, from 0 inclusive to 1 exclusive. + * + * @param {number} [size = 1] - number of values to return + * @returns {number} single random float from uniform distribution, if size === 1 + * @returns {Object[]} array of uniformly distributed random floats, if size > 1 + */ +export function random(size = 1) { + if (!Number.isInteger(size) | size < 1) { + // raise error if given an invalid size + throw { + origin: "util.random", + context: "when generating a random float", + error: "size must be a positive integer above 0", + }; + } + + if (size === 1) { + // if size is 1, return a single value + return Math.random(); + } else { + // if size is >1, return an array + let values = [] + for (let i = 0; i < size; i++) { + values.push(Math.random()); + } + return values + } +} + /** * Shuffle an array in place using the Fisher-Yastes's modern algorithm *

See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

From 7f7b791837a3f711f2d14ed480bb505729f8373d Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:49:37 +0100 Subject: [PATCH 02/12] Fully alias numpy.random.randint --- src/util/Util.js | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index f8e84ecc..344a3c28 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -960,12 +960,31 @@ export function turnSquareBracketsIntoArrays(input, max = 1) * @param {number} max - one above the largest integer to be drawn * @returns {number} a random integer in the requested range (signed) */ -export function randint(min = 0, max) +export function randint(min, max = null, size = 1) { + if (!Number.isInteger(size) | size < 1) { + // raise error if given an invalid size + throw { + origin: "util.random", + context: "when generating a random float", + error: "size must be a positive integer above 0", + }; + } + + if (size > 1) { + // if size > 1, call function multiple times with size = 1 and return an array + let values = [] + for (let i = 0; i < size; i++) { + values.push(randint(min, max, 1)); + } + return values + } + let lo = min; let hi = max; - if (typeof max === "undefined") + // if no max given, go from 0 to min + if (max === null) { hi = lo; lo = 0; From 945ef6b7f5077d8d5491a5336073a907acf250c7 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:50:29 +0100 Subject: [PATCH 03/12] Move random functions together --- src/util/Util.js | 101 ++++++++++++++++++++++++----------------------- 1 file changed, 51 insertions(+), 50 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 344a3c28..d6093fe2 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -331,12 +331,62 @@ export function random(size = 1) { // if size is >1, return an array let values = [] for (let i = 0; i < size; i++) { - values.push(Math.random()); + values.push(random(1)); } return values } } +/** + * Generates random integers a-la NumPy's in the "half-open" interval [min, max). In other words, from min inclusive to max exclusive. When max is undefined, as is the case by default, results are chosen from [0, min). An error is thrown if max is less than min. + * + * @param {number} [min = 0] - lowest integer to be drawn, or highest plus one if max is undefined (default) + * @param {number} max - one above the largest integer to be drawn + * @returns {number} a random integer in the requested range (signed) + */ +export function randint(min, max = null, size = 1) +{ + if (!Number.isInteger(size) | size < 1) { + // raise error if given an invalid size + throw { + origin: "util.random", + context: "when generating a random float", + error: "size must be a positive integer above 0", + }; + } + + if (size > 1) { + // if size > 1, call function multiple times with size = 1 and return an array + let values = [] + for (let i = 0; i < size; i++) { + values.push(randint(min, max, 1)); + } + return values + } + + let lo = min; + let hi = max; + + // if no max given, go from 0 to min + if (max === null) + { + hi = lo; + lo = 0; + } + + if (hi < lo) + { + throw { + origin: "util.randint", + context: "when generating a random integer", + error: "min should be <= max", + }; + } + + return Math.floor(Math.random() * (hi - lo)) + lo; +} + + /** * Shuffle an array in place using the Fisher-Yastes's modern algorithm *

See details here: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle#The_modern_algorithm

@@ -953,55 +1003,6 @@ export function turnSquareBracketsIntoArrays(input, max = 1) return matches; } -/** - * Generates random integers a-la NumPy's in the "half-open" interval [min, max). In other words, from min inclusive to max exclusive. When max is undefined, as is the case by default, results are chosen from [0, min). An error is thrown if max is less than min. - * - * @param {number} [min = 0] - lowest integer to be drawn, or highest plus one if max is undefined (default) - * @param {number} max - one above the largest integer to be drawn - * @returns {number} a random integer in the requested range (signed) - */ -export function randint(min, max = null, size = 1) -{ - if (!Number.isInteger(size) | size < 1) { - // raise error if given an invalid size - throw { - origin: "util.random", - context: "when generating a random float", - error: "size must be a positive integer above 0", - }; - } - - if (size > 1) { - // if size > 1, call function multiple times with size = 1 and return an array - let values = [] - for (let i = 0; i < size; i++) { - values.push(randint(min, max, 1)); - } - return values - } - - let lo = min; - let hi = max; - - // if no max given, go from 0 to min - if (max === null) - { - hi = lo; - lo = 0; - } - - if (hi < lo) - { - throw { - origin: "util.randint", - context: "when generating a random integer", - error: "min should be <= max", - }; - } - - return Math.floor(Math.random() * (hi - lo)) + lo; -} - /** * Round to a certain number of decimal places. * From 5e25a00189ca307b3c9c0613b4ad29573fb495ca Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:52:22 +0100 Subject: [PATCH 04/12] Placeholder alias for numpy.random.normal --- src/util/Util.js | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/src/util/Util.js b/src/util/Util.js index d6093fe2..9cde934d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -386,6 +386,20 @@ export function randint(min, max = null, size = 1) return Math.floor(Math.random() * (hi - lo)) + lo; } +/** + * Generate normally distributed random values. + * + * Not yet implemented in PsychoJS. + * + */ +export function normal(loc = 0.0, scale = 1.0, size = null) +{ + throw { + origin: "util.normal", + context: "when generating a random normally distributed value", + error: "function `normal` is not yet implemented in PsychoJS" + } +} /** * Shuffle an array in place using the Fisher-Yastes's modern algorithm From 449ac750a783d44becd21a1194f7d705817cf34b Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:56:24 +0100 Subject: [PATCH 05/12] Random consistent with randint for handling size --- src/util/Util.js | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 9cde934d..55359019 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -323,18 +323,17 @@ export function random(size = 1) { error: "size must be a positive integer above 0", }; } - - if (size === 1) { - // if size is 1, return a single value - return Math.random(); - } else { - // if size is >1, return an array + + if (size > 1) { + // if size > 1, call function multiple times with size = 1 and return an array let values = [] for (let i = 0; i < size; i++) { values.push(random(1)); } return values } + + return Math.random(); } /** From 03170471f0a1aeb14f99a079f448acea1ea95b2f Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 11:59:47 +0100 Subject: [PATCH 06/12] Document size param --- src/util/Util.js | 1 + 1 file changed, 1 insertion(+) diff --git a/src/util/Util.js b/src/util/Util.js index 55359019..e1862a18 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -341,6 +341,7 @@ export function random(size = 1) { * * @param {number} [min = 0] - lowest integer to be drawn, or highest plus one if max is undefined (default) * @param {number} max - one above the largest integer to be drawn + * @param {number} [size = 1] - number of values to return * @returns {number} a random integer in the requested range (signed) */ export function randint(min, max = null, size = 1) From 5d7528264e73f539b776ef3c1cd93c567627e412 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:00:38 +0100 Subject: [PATCH 07/12] Add size to randchoice --- src/util/Util.js | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index e1862a18..d79e2579 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -427,11 +427,30 @@ export function shuffle(array, randomNumberGenerator = undefined) * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * * @param {Object[]} array - the input 1-D array + * @param {number} [size = 1] - number of values to return * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random * @return {Object[]} a chosen value from the array */ -export function randchoice(array, randomNumberGenerator = undefined) +export function randchoice(array, size = 1, randomNumberGenerator = undefined) { + if (!Number.isInteger(size) | size < 1) { + // raise error if given an invalid size + throw { + origin: "util.random", + context: "when generating a random float", + error: "size must be a positive integer above 0", + }; + } + + if (size > 1) { + // if size > 1, call function multiple times with size = 1 and return an array + let values = [] + for (let i = 0; i < size; i++) { + values.push(randchoice(array, 1, randomNumberGenerator)); + } + return values + } + if (randomNumberGenerator === undefined) { randomNumberGenerator = Math.random; From cd8e06c6ff0f38ee44f6e142f952041b1e4bdd7c Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:09:14 +0100 Subject: [PATCH 08/12] Typo --- src/util/Util.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/util/Util.js b/src/util/Util.js index d79e2579..fe41b0d0 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -350,7 +350,7 @@ export function randint(min, max = null, size = 1) // raise error if given an invalid size throw { origin: "util.random", - context: "when generating a random float", + context: "when generating a random integer", error: "size must be a positive integer above 0", }; } From c0cab86f3e3531cb6bf44cffd204533ab6a734e8 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:10:18 +0100 Subject: [PATCH 09/12] Add replace to randchoice --- src/util/Util.js | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index fe41b0d0..8870fb9f 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -431,22 +431,37 @@ export function shuffle(array, randomNumberGenerator = undefined) * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random * @return {Object[]} a chosen value from the array */ -export function randchoice(array, size = 1, randomNumberGenerator = undefined) +export function randchoice(array, size = 1, replace = true, randomNumberGenerator = undefined) { if (!Number.isInteger(size) | size < 1) { // raise error if given an invalid size throw { origin: "util.random", - context: "when generating a random float", + context: "when choosing a random value from array", error: "size must be a positive integer above 0", }; } + if (!replace & size > array.length) { + // if replace is fase, then size can't exceed size of array as each value can only be used once + throw { + origin: "util.random", + context: "when choosing a random value from array", + error: "size cannot exceed length of array when replace is false", + }; + } if (size > 1) { // if size > 1, call function multiple times with size = 1 and return an array let values = [] + let tempArray = array for (let i = 0; i < size; i++) { - values.push(randchoice(array, 1, randomNumberGenerator)); + // add value taken from copy of array + let val = randchoice(tempArray, 1, replace, randomNumberGenerator) + values.push(val) + // if replace is false, remove value from copy of array + if (!replace) { + tempArray.pop(val) + } } return values } From bfbcf72c5618bc0fe89782e402091f296d97668e Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:32:08 +0100 Subject: [PATCH 10/12] Fully alias numpy.random.choice --- src/util/Util.js | 42 +++++++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 9 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 8870fb9f..7065d91b 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -426,12 +426,13 @@ export function shuffle(array, randomNumberGenerator = undefined) /** * Pick a random value from an array, uses `util.shuffle` to shuffle the array and returns the last value. * - * @param {Object[]} array - the input 1-D array + * @param {Object[]} a - the input 1-D array * @param {number} [size = 1] - number of values to return - * @param {Function} [randomNumberGenerator = undefined] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random + * @param {boolean} [replace = true] - Whether the sample is with or without replacement. Default is True, meaning that a value of a can be selected multiple times. + * @param {Function} [randomNumberGenerator = null] - A function used to generated random numbers in the interal [0, 1). Defaults to Math.random * @return {Object[]} a chosen value from the array */ -export function randchoice(array, size = 1, replace = true, randomNumberGenerator = undefined) +export function randchoice(a, size = 1, replace = true, p = null, randomNumberGenerator = null) { if (!Number.isInteger(size) | size < 1) { // raise error if given an invalid size @@ -441,7 +442,7 @@ export function randchoice(array, size = 1, replace = true, randomNumberGenerato error: "size must be a positive integer above 0", }; } - if (!replace & size > array.length) { + if (!replace & size > a.length) { // if replace is fase, then size can't exceed size of array as each value can only be used once throw { origin: "util.random", @@ -453,10 +454,10 @@ export function randchoice(array, size = 1, replace = true, randomNumberGenerato if (size > 1) { // if size > 1, call function multiple times with size = 1 and return an array let values = [] - let tempArray = array + let tempArray = a for (let i = 0; i < size; i++) { // add value taken from copy of array - let val = randchoice(tempArray, 1, replace, randomNumberGenerator) + let val = randchoice(tempArray, 1, replace, p, randomNumberGenerator) values.push(val) // if replace is false, remove value from copy of array if (!replace) { @@ -466,12 +467,35 @@ export function randchoice(array, size = 1, replace = true, randomNumberGenerato return values } - if (randomNumberGenerator === undefined) + if (randomNumberGenerator === null) { + // use Math.random if no generator given randomNumberGenerator = Math.random; } - const j = Math.floor(randomNumberGenerator() * array.length); - return array[j] + + let weights = p + if (weights === null) { + // if no weights given, use uniform + weights = Array.from({length: a.length}, () => 1/a.length) + } + + // normalize and accumulate weights + let total = weights.reduce((x, y) => x + y, 0) + let accum = 0 + for (let i = 0; i < weights.length; i++) { + accum += weights[i] / total + weights[i] = accum + } + + // calculate float index + i = randomNumberGenerator() + + // get integer index from weights array + for (let w of weights) { + if (i < w) { + return a[weights.indexOf(w)] + } + } } /** From 4cacf0c71d89830ddd7d222d7dd9fd755dd36dd8 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:43:28 +0100 Subject: [PATCH 11/12] Remove value from weights too if replace is false --- src/util/Util.js | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index 7065d91b..a8948c7d 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -434,6 +434,12 @@ export function shuffle(array, randomNumberGenerator = undefined) */ export function randchoice(a, size = 1, replace = true, p = null, randomNumberGenerator = null) { + let weights = p + if (weights === null) { + // if no weights given, use uniform + weights = Array.from({length: a.length}, () => 1/a.length) + } + if (!Number.isInteger(size) | size < 1) { // raise error if given an invalid size throw { @@ -461,7 +467,10 @@ export function randchoice(a, size = 1, replace = true, p = null, randomNumberGe values.push(val) // if replace is false, remove value from copy of array if (!replace) { - tempArray.pop(val) + let j = tempArray.indexOf(val) + tempArray.pop(tempArray[j]) + weights.pop(weights[j]) + } } return values @@ -473,12 +482,6 @@ export function randchoice(a, size = 1, replace = true, p = null, randomNumberGe randomNumberGenerator = Math.random; } - let weights = p - if (weights === null) { - // if no weights given, use uniform - weights = Array.from({length: a.length}, () => 1/a.length) - } - // normalize and accumulate weights let total = weights.reduce((x, y) => x + y, 0) let accum = 0 From f6afa658f035219b6d5364ac5d5b275e6314f269 Mon Sep 17 00:00:00 2001 From: Todd Parsons Date: Thu, 31 Aug 2023 12:50:01 +0100 Subject: [PATCH 12/12] Splice rather than pop to remove values when replace is false --- src/util/Util.js | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/util/Util.js b/src/util/Util.js index a8948c7d..9fc15aec 100644 --- a/src/util/Util.js +++ b/src/util/Util.js @@ -468,9 +468,8 @@ export function randchoice(a, size = 1, replace = true, p = null, randomNumberGe // if replace is false, remove value from copy of array if (!replace) { let j = tempArray.indexOf(val) - tempArray.pop(tempArray[j]) - weights.pop(weights[j]) - + tempArray.splice(j, 1) + weights.splice(j, 1) } } return values