Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(random/unstable): basic randomization functions #5626

Merged
merged 56 commits into from
Sep 5, 2024
Merged
Show file tree
Hide file tree
Changes from 42 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
19b6ed7
feat(random/unstable): basic randomization functions
lionel-rowe Aug 3, 2024
f5fa351
Better validation, module docs
lionel-rowe Aug 4, 2024
09b2861
Move shuffle and pick/pickWeighted->sample to collections
lionel-rowe Aug 4, 2024
7d3e287
Validate length of weights array
lionel-rowe Aug 4, 2024
1200129
Validate min and max for between functions
lionel-rowe Aug 4, 2024
9208fe1
Update Lint PR title scopes
lionel-rowe Aug 4, 2024
d39753c
Merge branch 'main' into random
kt3k Aug 5, 2024
82a0c00
Change PRNG algorithm from Wichman-Hill to PCG32
lionel-rowe Aug 6, 2024
276cc3c
Merge branch 'random' of https://github.com/lionel-rowe/deno_std into…
lionel-rowe Aug 6, 2024
6dc1b3f
Xor seeds with FNV-1a for better results with low-entropy seeds
lionel-rowe Aug 7, 2024
8bbd63e
Merge branch 'main' into random
lionel-rowe Aug 7, 2024
3f55d14
Change seed + state formats and implement pcg32_srandom_r for seeding
lionel-rowe Aug 8, 2024
b501ced
Naming changes + move sample/shuffle back into random
lionel-rowe Aug 8, 2024
ae7ef2a
Merge branch 'main' into random
lionel-rowe Aug 8, 2024
79a694d
Remove implementation dependencies on @std/assert
lionel-rowe Aug 8, 2024
1196193
Use arrays of numbers 0..255 to represent seed/state/inc
lionel-rowe Aug 8, 2024
c86aadb
Move getUint64s into own function
lionel-rowe Aug 9, 2024
a3cc9df
Merge branch 'main' into random
lionel-rowe Aug 9, 2024
333d07d
Update random/between.ts
lionel-rowe Aug 29, 2024
7e1538f
Update random/between.ts
lionel-rowe Aug 29, 2024
793f090
Update random/shuffle.ts
lionel-rowe Aug 29, 2024
2a96137
Update random/between.ts
lionel-rowe Aug 29, 2024
e472f71
Update random/seeded_random.ts
lionel-rowe Aug 29, 2024
810d026
Update random/integer_between.ts
lionel-rowe Aug 29, 2024
2df294e
Update random/sample.ts
lionel-rowe Aug 29, 2024
b3cda5d
Update random/sample_test.ts
lionel-rowe Aug 29, 2024
bbe382e
Update random/integer_between.ts
lionel-rowe Aug 29, 2024
f81a57b
Update random/sample_test.ts
lionel-rowe Aug 29, 2024
a4cf0ce
Update random/sample_test.ts
lionel-rowe Aug 29, 2024
e8e1b33
Merge branch 'main' into random
lionel-rowe Aug 29, 2024
03c36fb
Fix syntax/reference errors
lionel-rowe Aug 29, 2024
371e73f
Fix lint errors
lionel-rowe Aug 30, 2024
c7d3c53
Document Prng type better
lionel-rowe Aug 30, 2024
7e6504c
Update import_map.json
lionel-rowe Aug 30, 2024
bf9b6d8
Rename `random` option to `prng`
lionel-rowe Aug 30, 2024
03fbfed
Replace seeded random class with HOF taking scalar bigint seed
lionel-rowe Aug 30, 2024
0156a3a
Test coverage
lionel-rowe Aug 30, 2024
21b1a24
Add messages to assertThrows assertions
lionel-rowe Aug 30, 2024
4d95110
Remove unused internal exports
lionel-rowe Aug 30, 2024
0c912c7
Use built-in BigInt.asUintN for uint wrapping
lionel-rowe Aug 30, 2024
be0d89a
Copy edits
lionel-rowe Sep 2, 2024
c2c934a
Simplify + reduce function calls
lionel-rowe Sep 2, 2024
e501043
Merge branch 'main' into random
lionel-rowe Sep 3, 2024
dbb7f94
Allow non-integer min+max for randomIntegerBetween
lionel-rowe Sep 3, 2024
18442d6
Add note about algorithm used to randomSeeded
lionel-rowe Sep 3, 2024
8980fec
Simplify weights example for sample
lionel-rowe Sep 3, 2024
3a80b0e
refactor: add `@experimental` tag to module docs
iuioiua Sep 3, 2024
003d2ac
refactor: minor cleanups
iuioiua Sep 3, 2024
f226f4c
refactor: minor cleanups
iuioiua Sep 3, 2024
9dcb837
Fix randomBetween overflow and rounding issues
lionel-rowe Sep 4, 2024
db2477f
Change sample input types to ArrayLike to allow typed arrays
lionel-rowe Sep 4, 2024
82cc66f
Apply suggestions from code review
lionel-rowe Sep 4, 2024
d790759
Merge branch 'main' into random
lionel-rowe Sep 4, 2024
0088886
deno fmt
lionel-rowe Sep 4, 2024
6b4b79a
refactor: improve error handling
iuioiua Sep 5, 2024
6bb99e4
Merge branch 'main' into random
iuioiua Sep 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions _tools/check_circular_package_dependencies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ type Mod =
| "msgpack"
| "net"
| "path"
| "random"
| "regexp"
| "semver"
| "streams"
Expand Down Expand Up @@ -106,6 +107,7 @@ const ENTRYPOINTS: Record<Mod, string[]> = {
msgpack: ["mod.ts"],
net: ["mod.ts"],
path: ["mod.ts"],
random: ["mod.ts"],
regexp: ["mod.ts"],
semver: ["mod.ts"],
streams: ["mod.ts"],
Expand Down
1 change: 1 addition & 0 deletions _tools/check_docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ const ENTRY_POINTS = [
"../path/mod.ts",
"../path/posix/mod.ts",
"../path/windows/mod.ts",
"../random/mod.ts",
"../regexp/mod.ts",
"../semver/mod.ts",
"../streams/mod.ts",
Expand Down
1 change: 1 addition & 0 deletions browser-compat.tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"./msgpack",
"./net",
"./path",
"./random",
"./regexp",
"./semver",
"./streams",
Expand Down
1 change: 1 addition & 0 deletions deno.json
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@
"./msgpack",
"./net",
"./path",
"./random",
"./regexp",
"./semver",
"./streams",
Expand Down
1 change: 1 addition & 0 deletions import_map.json
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
"@std/net": "jsr:@std/net@^1.0.1",
"@std/path": "jsr:@std/path@^1.0.3",
"@std/regexp": "jsr:@std/regexp@^1.0.0",
"@std/random": "jsr:@std/random@^0.1.0",
"@std/semver": "jsr:@std/semver@^1.0.2",
"@std/streams": "jsr:@std/streams@^1.0.3",
"@std/testing": "jsr:@std/testing@^1.0.1",
Expand Down
100 changes: 100 additions & 0 deletions random/_pcg32.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
// Based on Rust `rand` crate (https://github.com/rust-random/rand). Apache-2.0 + MIT license.

/** Multiplier for the PCG32 algorithm. */
const MUL: bigint = 6364136223846793005n;
/** Initial increment for the PCG32 algorithm. Only used during seeding. */
const INC: bigint = 11634580027462260723n;

// Constants are for 64-bit state, 32-bit output
const ROTATE = 59n; // 64 - 5
const XSHIFT = 18n; // (5 + 32) / 2
const SPARE = 27n; // 64 - 32 - 5

/**
* Internal state for the PCG32 algorithm.
* `state` prop is mutated by each step, whereas `inc` prop remains constant.
*/
type PcgMutableState = {
state: bigint;
inc: bigint;
};

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L129-L135
*/
export function fromSeed(seed: Uint8Array) {
const d = new DataView(seed.buffer);
return fromStateIncr(d.getBigUint64(0, true), d.getBigUint64(8, true) | 1n);
}

/**
* Mutates `pcg` by advancing `pcg.state`.
*/
function step(pgc: PcgMutableState) {
pgc.state = BigInt.asUintN(64, pgc.state * MUL + (pgc.inc | 1n));
}

/**
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L99-L105
*/
function fromStateIncr(state: bigint, inc: bigint): PcgMutableState {
const pcg: PcgMutableState = { state, inc };
// Move away from initial value
pcg.state = BigInt.asUintN(64, state + inc);
step(pcg);
return pcg;
}

/**
* Internal PCG32 implementation, used by both the public seeded random
* function and the seed generation algorithm.
*
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_pcg/src/pcg64.rs#L140-L153
*
* `pcg.state` is internally advanced by the function.
*
* @param pcg The state and increment values to use for the PCG32 algorithm.
* @returns The next pseudo-random 32-bit integer.
*/
export function nextU32(pcg: PcgMutableState): number {
const state = pcg.state;
step(pcg);
// Output function XSH RR: xorshift high (bits), followed by a random rotate
const rot = state >> ROTATE;
const xsh = BigInt.asUintN(32, ((state >> XSHIFT) ^ state) >> SPARE);
return Number(rotateRightU32(xsh, rot));
}

// `n`, `rot`, and return val are all u32
function rotateRightU32(n: bigint, rot: bigint): bigint {
const left = BigInt.asUintN(32, n << (-rot & 31n));
const right = n >> rot;
return (left | right);
}

/**
* Convert a scalar bigint seed to a Uint8Array of the specified length.
* Modified from https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
*/
export function seedFromU64(state: bigint, numBytes: number): Uint8Array {
const seed = new Uint8Array(numBytes);

const pgc: PcgMutableState = { state: BigInt.asUintN(64, state), inc: INC };
// We advance the state first (to get away from the input value,
// in case it has low Hamming Weight).
step(pgc);

for (let i = 0; i < Math.floor(numBytes / 4); ++i) {
new DataView(seed.buffer).setUint32(i * 4, nextU32(pgc), true);
}

const rem = numBytes % 4;
if (rem) {
const bytes = new Uint8Array(4);
new DataView(bytes.buffer).setUint32(0, nextU32(pgc), true);
seed.set(bytes.subarray(0, rem), numBytes - rem);
}

return seed;
}
122 changes: 122 additions & 0 deletions random/_pcg32_test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

import { assertEquals } from "../assert/equals.ts";
import { fromSeed, nextU32, seedFromU64 } from "./_pcg32.ts";

Deno.test("seedFromU64() generates seeds from bigints", async (t) => {
await t.step("first 10 16-bit seeds are same as rand crate", async (t) => {
/**
* Expected results obtained by copying the Rust code from
* https://github.com/rust-random/rand/blob/f7bbccaedf6c63b02855b90b003c9b1a4d1fd1cb/rand_core/src/lib.rs#L359-L388
* but directly returning `seed` instead of `Self::from_seed(seed)`
*/
// deno-fmt-ignore
const expectedResults = [
[236, 242, 115, 249, 129, 181, 205, 69, 135, 240, 70, 115, 6, 173, 108, 173],
[234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46],
[77, 209, 16, 204, 177, 124, 55, 30, 237, 239, 68, 142, 238, 125, 215, 7],
[108, 90, 247, 27, 160, 186, 6, 71, 76, 124, 221, 142, 87, 133, 92, 175],
[197, 166, 196, 87, 44, 68, 69, 62, 55, 32, 34, 218, 130, 107, 171, 170],
[60, 64, 172, 11, 74, 188, 224, 128, 161, 112, 220, 75, 85, 212, 145, 251],
[177, 93, 150, 16, 48, 3, 23, 51, 155, 104, 76, 121, 82, 134, 239, 107],
[200, 12, 64, 59, 208, 32, 108, 9, 55, 166, 59, 111, 242, 79, 37, 30],
[222, 11, 88, 159, 202, 89, 63, 215, 36, 57, 0, 156, 63, 131, 114, 90],
[21, 119, 90, 241, 241, 191, 180, 229, 150, 199, 126, 251, 25, 141, 7, 4],
];

for (const [i, expected] of expectedResults.entries()) {
await t.step(`With seed ${i}n`, () => {
const actual = Array.from(seedFromU64(BigInt(i), 16));
assertEquals(actual, expected);
});
}
});

await t.step(
"generates arbitrary-length seed data from a single bigint",
async (t) => {
// deno-fmt-ignore
const expectedBytes = [234, 216, 29, 114, 93, 38, 16, 78, 137, 156, 59, 248, 66, 206, 120, 46, 186];

for (const i of expectedBytes.keys()) {
const slice = expectedBytes.slice(0, i + 1);

await t.step(`With length ${i + 1}`, () => {
const actual = Array.from(seedFromU64(1n, i + 1));
assertEquals(actual, slice);
});
}
},
);

const U64_CEIL = 2n ** 64n;

await t.step("wraps bigint input to u64", async (t) => {
await t.step("exact multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(BigInt(0n), 16));
const actual = Array.from(seedFromU64(U64_CEIL * 99n, 16));
assertEquals(actual, expected);
});

await t.step("multiple of U64_CEIL + 1", () => {
const expected = Array.from(seedFromU64(1n, 16));
const actual = Array.from(seedFromU64(1n + U64_CEIL * 3n, 16));
assertEquals(actual, expected);
});

await t.step("multiple of U64_CEIL - 1", () => {
const expected = Array.from(seedFromU64(-1n, 16));
const actual = Array.from(seedFromU64(U64_CEIL - 1n, 16));
assertEquals(actual, expected);
});

await t.step("negative multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(0n, 16));
const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16));
assertEquals(actual, expected);
});

await t.step("negative multiple of U64_CEIL", () => {
const expected = Array.from(seedFromU64(0n, 16));
const actual = Array.from(seedFromU64(U64_CEIL * -3n, 16));
assertEquals(actual, expected);
});
});
});

Deno.test("nextU32() generates random 32-bit integers", async (t) => {
/**
* Expected results obtained from the Rust `rand` crate as follows:
* ```rs
* use rand_pcg::rand_core::{RngCore, SeedableRng};
* use rand_pcg::Lcg64Xsh32;
*
* let mut rng = Lcg64Xsh32::seed_from_u64(0);
* for _ in 0..10 {
* println!("{}", rng.next_u32());
* }
* ```
*/
const expectedResults = [
298703107,
4236525527,
336081875,
1056616254,
1060453275,
1616833669,
501767310,
2864049166,
56572352,
2362354238,
];

const pgc = fromSeed(seedFromU64(0n, 16));
const next = () => nextU32(pgc);

for (const [i, expected] of expectedResults.entries()) {
await t.step(`#${i + 1} generated uint32`, () => {
const actual = next();
assertEquals(actual, expected);
});
}
});
22 changes: 22 additions & 0 deletions random/_types.ts
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.

/**
* A pseudo-random number generator implementing the same contract as
* `Math.random`, i.e. taking zero arguments and returning a random number in
* the range `[0, 1)`. The behavior of a function that accepts a `Prng` an
* option may be customized by passing a `Prng` with different behavior from
* `Math.random`, for example it may be seeded.
*/
export type Prng = typeof Math.random;
iuioiua marked this conversation as resolved.
Show resolved Hide resolved

/**
* Options for random number generation.
*/
export type RandomOptions = {
/**
* A pseudo-random number generator returning a random number in the range
* `[0, 1)`, used for randomization.
* @default {Math.random}
*/
prng: Prng;
};
40 changes: 40 additions & 0 deletions random/between.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
import type { RandomOptions } from "./_types.ts";

/**
* Generates a random number between the provided minimum and maximum values.
*
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
* The number is in the range `[min, max)`, i.e. `min` is included but `max` is excluded.
iuioiua marked this conversation as resolved.
Show resolved Hide resolved
*
* @param min The minimum value (inclusive)
* @param max The maximum value (exclusive)
* @param options The options for the random number generator
* @returns A random number between the provided minimum and maximum values
*
* @example Usage
* ```ts no-assert
* import { randomBetween } from "@std/random";
*
* randomBetween(1, 10); // 6.688009464410508
* randomBetween(1, 10); // 3.6267118101712006
* randomBetween(1, 10); // 7.853320239013774
* ```
*/
export function randomBetween(
min: number,
max: number,
options?: RandomOptions,
): number {
if (!Number.isFinite(min)) {
throw new RangeError(`min cannot be ${min}`);
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
}
if (!Number.isFinite(max)) {
throw new RangeError(`max cannot be ${max}`);
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
}
if (max < min) {
throw new RangeError("max must be greater than or equal to min");
lionel-rowe marked this conversation as resolved.
Show resolved Hide resolved
}

const { prng } = { prng: Math.random, ...options };
return prng() * (max - min) + min;
}
Loading