diff --git a/.changeset/wet-carpets-sell.md b/.changeset/wet-carpets-sell.md new file mode 100644 index 00000000..8332d154 --- /dev/null +++ b/.changeset/wet-carpets-sell.md @@ -0,0 +1,5 @@ +--- +"@tsplus/stdlib": patch +--- + +update Random implementation diff --git a/packages/stdlib/_src/global.ts b/packages/stdlib/_src/global.ts index ebe4699e..27da4288 100644 --- a/packages/stdlib/_src/global.ts +++ b/packages/stdlib/_src/global.ts @@ -149,6 +149,18 @@ import { Exception } from "@tsplus/stdlib/exceptions/Exception" * @tsplus global */ import { Eval } from "@tsplus/stdlib/io/Eval/definition" +/** + * @tsplus global + */ +import { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +/** + * @tsplus global + */ +import { MutableRandom } from "@tsplus/stdlib/io/Random/MutableRandom" +/** + * @tsplus global + */ +import { PCGRandom } from "@tsplus/stdlib/io/Random/PCGRandom" /** * @tsplus global */ @@ -435,10 +447,6 @@ import { * @tsplus global */ import { lazy } from "@tsplus/stdlib/utilities/Lazy" -/** - * @tsplus global - */ -import { RandomPCG } from "@tsplus/stdlib/utilities/RandomPCG" /** * @tsplus global */ diff --git a/packages/stdlib/_src/io.ts b/packages/stdlib/_src/io.ts index cdee5aa0..d510f790 100644 --- a/packages/stdlib/_src/io.ts +++ b/packages/stdlib/_src/io.ts @@ -1 +1,2 @@ export * as eval from "@tsplus/stdlib/io/Eval" +export * as random from "@tsplus/stdlib/io/Random" diff --git a/packages/stdlib/_src/io/Random.ts b/packages/stdlib/_src/io/Random.ts new file mode 100644 index 00000000..39bab830 --- /dev/null +++ b/packages/stdlib/_src/io/Random.ts @@ -0,0 +1,6 @@ +// codegen:start {preset: barrel, include: ./Random/*.ts, prefix: "@tsplus/stdlib/io"} +export * from "@tsplus/stdlib/io/Random/Distribution" +export * from "@tsplus/stdlib/io/Random/Generator" +export * from "@tsplus/stdlib/io/Random/MutableRandom" +export * from "@tsplus/stdlib/io/Random/PCGRandom" +// codegen:end diff --git a/packages/stdlib/_src/io/Random/Distribution.ts b/packages/stdlib/_src/io/Random/Distribution.ts new file mode 100644 index 00000000..e4178005 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./Distribution/*.ts, prefix: "@tsplus/stdlib/io/Random"} +export * from "@tsplus/stdlib/io/Random/Distribution/definition" +export * from "@tsplus/stdlib/io/Random/Distribution/operations" +// codegen:end diff --git a/packages/stdlib/_src/io/Random/Distribution/_internal/ArrayInt.ts b/packages/stdlib/_src/io/Random/Distribution/_internal/ArrayInt.ts new file mode 100644 index 00000000..b9ba1867 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/_internal/ArrayInt.ts @@ -0,0 +1,201 @@ +/** + * An ArrayInt represents an integer larger than what can be represented in + * classical JavaScript. + * + * The values stored in data must be in the range `[0, 0xffffffff]`. + * + * ```ts + * { sign: 1, data: [ 42 ] } // = 42 + * { sign: -1, data: [ 42 ] } // = -42 + * { sign: -1, data: [ 5, 42 ] } // = -1 * (5 * 2**32 + 42) + * { sign: -1, data: [ 1, 5, 42 ] } // = -1 * (1 * 2**64 + 5 * 2**32 + 42) + * ``` + */ +export interface ArrayInt { + /** + * Sign of the represented number + */ + sign: -1 | 1 + /** + * Value of the number, must only contain numbers in the range [0, 0xffffffff] + */ + data: number[] +} + +/** + * Add two ArrayInt + * @internal + */ +export function addArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt { + if (arrayIntA.sign !== arrayIntB.sign) { + return substractArrayIntToNew(arrayIntA, { + sign: -arrayIntB.sign as -1 | 1, + data: arrayIntB.data + }) + } + const data = [] + let reminder = 0 + const dataA = arrayIntA.data + const dataB = arrayIntB.data + for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) { + const vA = indexA >= 0 ? dataA[indexA]! : 0 + const vB = indexB >= 0 ? dataB[indexB]! : 0 + const current = vA + vB + reminder + data.push(current >>> 0) + reminder = ~~(current / 0x100000000) + } + if (reminder !== 0) { + data.push(reminder) + } + return { sign: arrayIntA.sign, data: data.reverse() } +} + +/** + * Add one to a given positive ArrayInt + * @internal + */ +export function addOneToPositiveArrayInt(arrayInt: ArrayInt): ArrayInt { + arrayInt.sign = 1 // handling case { sign: -1, data: [0,...,0] } + const data = arrayInt.data + for (let index = data.length - 1; index >= 0; --index) { + if (data[index] === 0xffffffff) { + data[index] = 0 + } else { + data[index] += 1 + return arrayInt + } + } + data.unshift(1) + return arrayInt +} + +/** @internal */ +function isStrictlySmaller(dataA: number[], dataB: number[]): boolean { + const maxLength = Math.max(dataA.length, dataB.length) + for (let index = 0; index < maxLength; ++index) { + const indexA = index + dataA.length - maxLength + const indexB = index + dataB.length - maxLength + const vA = indexA >= 0 ? dataA[indexA]! : 0 + const vB = indexB >= 0 ? dataB[indexB]! : 0 + if (vA < vB) return true + if (vA > vB) return false + } + return false +} + +/** + * Substract two ArrayInt + * @internal + */ +export function substractArrayIntToNew(arrayIntA: ArrayInt, arrayIntB: ArrayInt): ArrayInt { + if (arrayIntA.sign !== arrayIntB.sign) { + return addArrayIntToNew(arrayIntA, { sign: -arrayIntB.sign as -1 | 1, data: arrayIntB.data }) + } + const dataA = arrayIntA.data + const dataB = arrayIntB.data + if (isStrictlySmaller(dataA, dataB)) { + const out = substractArrayIntToNew(arrayIntB, arrayIntA) + out.sign = -out.sign as -1 | 1 + return out + } + const data = [] + let reminder = 0 + for (let indexA = dataA.length - 1, indexB = dataB.length - 1; indexA >= 0 || indexB >= 0; --indexA, --indexB) { + const vA = indexA >= 0 ? dataA[indexA]! : 0 + const vB = indexB >= 0 ? dataB[indexB]! : 0 + const current = vA - vB - reminder + data.push(current >>> 0) + reminder = current < 0 ? 1 : 0 + } + return { sign: arrayIntA.sign, data: data.reverse() } +} + +/** + * Trim uneeded zeros in ArrayInt + * and uniform notation for zero: {sign: 1, data: [0]} + */ +export function trimArrayIntInplace(arrayInt: ArrayInt) { + const data = arrayInt.data + let firstNonZero = 0 + // eslint-disable-next-line no-empty + for (; firstNonZero !== data.length && data[firstNonZero] === 0; ++firstNonZero) {} + if (firstNonZero === data.length) { + // only zeros + arrayInt.sign = 1 + arrayInt.data = [0] + return arrayInt + } + data.splice(0, firstNonZero) + return arrayInt +} + +// Helpers specific to 64 bits versions + +/** @internal */ +export type ArrayInt64 = Required & { data: [number, number] } + +/** + * We only accept safe integers here + * @internal + */ +export function fromNumberToArrayInt64(out: ArrayInt64, n: number): ArrayInt64 { + if (n < 0) { + const posN = -n + out.sign = -1 + out.data[0] = ~~(posN / 0x100000000) + out.data[1] = posN >>> 0 + } else { + out.sign = 1 + out.data[0] = ~~(n / 0x100000000) + out.data[1] = n >>> 0 + } + return out +} + +/** + * Substract two ArrayInt of 64 bits on 64 bits. + * With arrayIntA - arrayIntB >= 0 + * @internal + */ +export function substractArrayInt64(out: ArrayInt64, arrayIntA: ArrayInt64, arrayIntB: ArrayInt64): ArrayInt64 { + const lowA = arrayIntA.data[1] + const highA = arrayIntA.data[0] + const signA = arrayIntA.sign + const lowB = arrayIntB.data[1] + const highB = arrayIntB.data[0] + const signB = arrayIntB.sign + + // Requirement: arrayIntA - arrayIntB >= 0 + out.sign = 1 + + if (signA === 1 && signB === -1) { + // Operation is a simple sum of arrayIntA + abs(arrayIntB) + const low = lowA + lowB + const high = highA + highB + (low > 0xffffffff ? 1 : 0) + out.data[0] = high >>> 0 + out.data[1] = low >>> 0 + return out + } + // signA === -1 with signB === 1 is impossible given: arrayIntA - arrayIntB >= 0 + + // Operation is a substraction + let lowFirst = lowA + let highFirst = highA + let lowSecond = lowB + let highSecond = highB + if (signA === -1) { + lowFirst = lowB + highFirst = highB + lowSecond = lowA + highSecond = highA + } + let reminderLow = 0 + let low = lowFirst - lowSecond + if (low < 0) { + reminderLow = 1 + low = low >>> 0 + } + out.data[0] = highFirst - highSecond - reminderLow + out.data[1] = low + return out +} diff --git a/packages/stdlib/_src/io/Random/Distribution/_internal/uniformArrayIntDistributionInternal.ts b/packages/stdlib/_src/io/Random/Distribution/_internal/uniformArrayIntDistributionInternal.ts new file mode 100644 index 00000000..11ec2525 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/_internal/uniformArrayIntDistributionInternal.ts @@ -0,0 +1,43 @@ +import type { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +import { uniformIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformIntDistributionInternal" +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +/** + * Uniformly generate an `ArrayInt` in the range [0, rangeSize]. + * + * @remarks + * In the worst case scenario it may discard half of the randomly generated + * values. Worst case being: most significant number is `1` and remaining part + * evaluates to `0`. + * + * @internal + */ +export function uniformArrayIntDistributionInternal( + out: ArrayInt["data"], + rangeSize: ArrayInt["data"], + rng: RandomGenerator +): ArrayInt["data"] { + const rangeLength = rangeSize.length + + // We iterate until we find a valid value for arrayInt + // eslint-disable-next-line no-constant-condition + while (true) { + // We compute a new value for arrayInt + for (let index = 0; index !== rangeLength; ++index) { + const indexRangeSize = index === 0 ? rangeSize[0]! + 1 : 0x100000000 + out[index] = uniformIntDistributionInternal(indexRangeSize, rng) + } + + // If in the correct range we can return it + for (let index = 0; index !== rangeLength; ++index) { + const current = out[index]! + const currentInRange = rangeSize[index]! + if (current < currentInRange) { + return out // arrayInt < rangeSize + } else if (current > currentInRange) { + break // arrayInt > rangeSize + } + } + // Otherwise we need to try another one + } +} diff --git a/packages/stdlib/_src/io/Random/Distribution/_internal/uniformIntDistributionInternal.ts b/packages/stdlib/_src/io/Random/Distribution/_internal/uniformIntDistributionInternal.ts new file mode 100644 index 00000000..498ba3e9 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/_internal/uniformIntDistributionInternal.ts @@ -0,0 +1,48 @@ +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +/** + * Uniformly generate an integer in the range [0, rangeSize]. + * + * @internal + */ +export function uniformIntDistributionInternal(rangeSize: number, rng: RandomGenerator): number { + const MinRng = rng.min() + const NumValues = rng.max() - rng.min() + 1 + + // Range provided by the RandomGenerator is large enough + if (rangeSize <= NumValues) { + const MaxAllowed = NumValues - (NumValues % rangeSize) + // eslint-disable-next-line no-constant-condition + while (true) { + const out = rng.next() + const deltaV = out - MinRng + if (deltaV < MaxAllowed) { + return deltaV % rangeSize // Warning: we expect NumValues <= 2**32, so diff too + } + } + } + + // Compute number of iterations required to have enough random + // to build uniform entries in the asked range + let FinalNumValues = NumValues * NumValues + let NumIterations = 2 // At least 2 (at this point in the code) + while (FinalNumValues < rangeSize) { + FinalNumValues *= NumValues + ;++NumIterations + } + const MaxAcceptedRandom = rangeSize * Math.floor((1 * FinalNumValues) / rangeSize) + + // eslint-disable-next-line no-constant-condition + while (true) { + // Aggregate mutiple calls to next() into a single random value + let value = 0 + for (let num = 0; num !== NumIterations; ++num) { + const out = rng.next() + value = NumValues * value + (out - MinRng) // Warning: we overflow may when diff > max_safe (eg: =max_safe-min_safe+1) + } + if (value < MaxAcceptedRandom) { + const inDiff = value - rangeSize * Math.floor((1 * value) / rangeSize) + return inDiff + } + } +} diff --git a/packages/stdlib/_src/io/Random/Distribution/definition.ts b/packages/stdlib/_src/io/Random/Distribution/definition.ts new file mode 100644 index 00000000..23c031d3 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/definition.ts @@ -0,0 +1,15 @@ +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +/** + * Generate random value based on a given RandomGenerator. Return the generated + * value and an offsetted version of the RandomGenerator. + * + * @tsplus type Random.Distribution + */ +export type Distribution = (rng: RandomGenerator) => T + +/** + * @tsplus type Random.Distribution.Ops + */ +export interface DistributionOps {} +export const Distribution: DistributionOps = {} diff --git a/packages/stdlib/_src/io/Random/Distribution/operations.ts b/packages/stdlib/_src/io/Random/Distribution/operations.ts new file mode 100644 index 00000000..98053507 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/operations.ts @@ -0,0 +1,5 @@ +// codegen:start {preset: barrel, include: ./operations/*.ts, prefix: "@tsplus/stdlib/io/Random/Distribution"} +export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformArrayIntDistribution" +export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformBigIntDistribution" +export * from "@tsplus/stdlib/io/Random/Distribution/operations/uniformIntDistribution" +// codegen:end diff --git a/packages/stdlib/_src/io/Random/Distribution/operations/uniformArrayIntDistribution.ts b/packages/stdlib/_src/io/Random/Distribution/operations/uniformArrayIntDistribution.ts new file mode 100644 index 00000000..b83c9138 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/operations/uniformArrayIntDistribution.ts @@ -0,0 +1,46 @@ +import type { ArrayInt } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +import { + addArrayIntToNew, + addOneToPositiveArrayInt, + substractArrayIntToNew, + trimArrayIntInplace +} from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +import { uniformArrayIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformArrayIntDistributionInternal" +import type { Distribution } from "@tsplus/stdlib/io/Random/Distribution/definition" +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +/** @internal */ +function uniformArrayIntInternal(from: ArrayInt, to: ArrayInt, rng: RandomGenerator): ArrayInt { + const rangeSize = trimArrayIntInplace(addOneToPositiveArrayInt(substractArrayIntToNew(to, from))) + const emptyArrayIntData = rangeSize.data.slice(0) + const g = uniformArrayIntDistributionInternal(emptyArrayIntData, rangeSize.data, rng) + return trimArrayIntInplace(addArrayIntToNew({ sign: 1, data: g }, from)) +} + +/** + * Uniformly generate random ArrayInt values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * + * @tsplus static Random.Distribution.Ops uniformArrayIntDistribution + */ +export function uniformArrayIntDistribution(from: ArrayInt, to: ArrayInt): Distribution +/** + * Uniformly generate random ArrayInt values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * @param rng - Instance of RandomGenerator to extract random values from + */ +export function uniformArrayIntDistribution(from: ArrayInt, to: ArrayInt, rng: RandomGenerator): ArrayInt +export function uniformArrayIntDistribution(from: ArrayInt, to: ArrayInt, rng?: RandomGenerator) { + if (rng != null) { + return uniformArrayIntInternal(from, to, rng) + } + return function(rng: RandomGenerator) { + return uniformArrayIntInternal(from, to, rng) + } +} diff --git a/packages/stdlib/_src/io/Random/Distribution/operations/uniformBigIntDistribution.ts b/packages/stdlib/_src/io/Random/Distribution/operations/uniformBigIntDistribution.ts new file mode 100644 index 00000000..08080b72 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/operations/uniformBigIntDistribution.ts @@ -0,0 +1,58 @@ +import type { Distribution } from "@tsplus/stdlib/io/Random/Distribution/definition" +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +function uniformBigIntInternal(from: bigint, diff: bigint, rng: RandomGenerator): bigint { + const MinRng = BigInt(rng.min()) + const NumValues = BigInt(rng.max() - rng.min() + 1) + + // Number of iterations required to have enough random + // to build uniform entries in the asked range + let FinalNumValues = NumValues + let NumIterations = BigInt(1) + while (FinalNumValues < diff) { + FinalNumValues *= NumValues + ;++NumIterations + } + const MaxAcceptedRandom = FinalNumValues - (FinalNumValues % diff) + + // eslint-disable-next-line no-constant-condition + while (true) { + // Aggregate mutiple calls to next() into a single random value + let value = BigInt(0) + for (let num = BigInt(0); num !== NumIterations; ++num) { + const out = rng.next() + value = NumValues * value + (BigInt(out) - MinRng) + } + if (value < MaxAcceptedRandom) { + const inDiff = value % diff + return inDiff + from + } + } +} + +/** + * Uniformly generate random bigint values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * + * @tsplus static Random.Distribution.Ops uniformBigIntDistribution + */ +export function uniformBigIntDistribution(from: bigint, to: bigint): Distribution +/** + * Uniformly generate random bigint values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * @param rng - Instance of RandomGenerator to extract random values from + */ +export function uniformBigIntDistribution(from: bigint, to: bigint, rng: RandomGenerator): bigint +export function uniformBigIntDistribution(from: bigint, to: bigint, rng?: RandomGenerator) { + const diff = to - from + BigInt(1) + if (rng != null) { + return uniformBigIntInternal(from, diff, rng) + } + return (rng: RandomGenerator) => uniformBigIntInternal(from, diff, rng) +} diff --git a/packages/stdlib/_src/io/Random/Distribution/operations/uniformIntDistribution.ts b/packages/stdlib/_src/io/Random/Distribution/operations/uniformIntDistribution.ts new file mode 100644 index 00000000..2d7e837a --- /dev/null +++ b/packages/stdlib/_src/io/Random/Distribution/operations/uniformIntDistribution.ts @@ -0,0 +1,78 @@ +import type { ArrayInt64 } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +import { fromNumberToArrayInt64, substractArrayInt64 } from "@tsplus/stdlib/io/Random/Distribution/_internal/ArrayInt" +import { uniformArrayIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformArrayIntDistributionInternal" +import { uniformIntDistributionInternal } from "@tsplus/stdlib/io/Random/Distribution/_internal/uniformIntDistributionInternal" +import type { Distribution } from "@tsplus/stdlib/io/Random/Distribution/definition" +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +const sharedA: ArrayInt64 = { sign: 1, data: [0, 0] } +const sharedB: ArrayInt64 = { sign: 1, data: [0, 0] } +const sharedC: ArrayInt64 = { sign: 1, data: [0, 0] } +const sharedData = [0, 0] + +function uniformLargeIntInternal( + from: number, + to: number, + rangeSize: number, + rng: RandomGenerator +): number { + const rangeSizeArrayIntValue = rangeSize <= Number.MAX_SAFE_INTEGER + ? fromNumberToArrayInt64(sharedC, rangeSize) // no possible overflow given rangeSize is in a safe range + : substractArrayInt64( + sharedC, + fromNumberToArrayInt64(sharedA, to), + fromNumberToArrayInt64(sharedB, from) + ) // rangeSize might be incorrect, we compute a safer range + + // Adding 1 to the range + if (rangeSizeArrayIntValue.data[1] === 0xffffffff) { + // rangeSizeArrayIntValue.length === 2 by construct + // rangeSize >= 0x00000001_00000000 and rangeSize <= 0x003fffff_fffffffe + // with Number.MAX_SAFE_INTEGER - Number.MIN_SAFE_INTEGER = 0x003fffff_fffffffe + rangeSizeArrayIntValue.data[0] += 1 + rangeSizeArrayIntValue.data[1] = 0 + } else { + rangeSizeArrayIntValue.data[1] += 1 + } + + uniformArrayIntDistributionInternal(sharedData, rangeSizeArrayIntValue.data, rng) + return sharedData[0]! * 0x100000000 + sharedData[1]! + from +} + +function uniformIntInternal(from: number, to: number, rng: RandomGenerator): number { + const rangeSize = to - from + if (rangeSize <= 0xffffffff) { + // Calling uniformIntDistributionInternal can be considered safe + // up-to 2**32 values. Above this range it may miss values. + let g = uniformIntDistributionInternal(rangeSize + 1, rng) + g += from + return g + } + return uniformLargeIntInternal(from, to, rangeSize, rng) +} + +/** + * Uniformly generate random integer values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * + * @tsplus static Random.Distribution.Ops uniformIntDistribution + */ +export function uniformIntDistribution(from: number, to: number): Distribution +/** + * Uniformly generate random integer values between `from` (inclusive) and `to` + * (inclusive). + * + * @param from - Lower bound of the range (inclusive) + * @param to - Upper bound of the range (inclusive) + * @param rng - Instance of RandomGenerator to extract random values from + */ +export function uniformIntDistribution(from: number, to: number, rng: RandomGenerator): number +export function uniformIntDistribution(from: number, to: number, rng?: RandomGenerator) { + if (rng != null) { + return uniformIntInternal(from, to, rng) + } + return (rng: RandomGenerator) => uniformIntInternal(from, to, rng) +} diff --git a/packages/stdlib/_src/io/Random/Generator.ts b/packages/stdlib/_src/io/Random/Generator.ts new file mode 100644 index 00000000..a015a46b --- /dev/null +++ b/packages/stdlib/_src/io/Random/Generator.ts @@ -0,0 +1,4 @@ +// codegen:start {preset: barrel, include: ./Generator/*.ts, prefix: "@tsplus/stdlib/io/Random"} +export * from "@tsplus/stdlib/io/Random/Generator/MersenneTwister" +export * from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" +// codegen:end diff --git a/packages/stdlib/_src/io/Random/Generator/MersenneTwister.ts b/packages/stdlib/_src/io/Random/Generator/MersenneTwister.ts new file mode 100644 index 00000000..0424b0d7 --- /dev/null +++ b/packages/stdlib/_src/io/Random/Generator/MersenneTwister.ts @@ -0,0 +1,158 @@ +/* + Original MT19937 license: + + A C-program for MT19937, with initialization improved 2002/1/26. + Coded by Takuji Nishimura and Makoto Matsumoto. + + Before using, initialize the state by using init_genrand(seed) + or init_by_array(init_key, key_length). + + Copyright (C) 1997 - 2002, Makoto Matsumoto and Takuji Nishimura, + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions + are met: + + 1. Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + + 3. The names of its contributors may not be used to endorse or promote + products derived from this software without specific prior written + permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR + CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, + EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, + PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR + PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF + LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING + NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS + SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + Any feedback is very welcome. + http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html + email: m-mat @ math.sci.hiroshima-u.ac.jp (remove space) +*/ +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +const N = 624 +const M = 397 +const UPPER_MASK = 0x80000000 +const LOWER_MASK = 0x7fffffff +const MATRIX_A = 0x9908b0df + +export class MersenneTwister implements RandomGenerator { + private mt: Array + private mti: number + + constructor(seed?: number) { + this.mt = new Array(N) + this.mti = N + 1 + this.setSeed(seed ?? Date.now()) + } + + setSeed(seed: number) { + let s: number + + this.mt[0] = seed >>> 0 + + for (this.mti = 1; this.mti < N; this.mti++) { + s = this.mt[this.mti - 1]! ^ (this.mt[this.mti - 1]! >>> 30) + this.mt[this.mti] = ((((s & 0xffff0000) >>> 16) * 1812433253) << 16) + (s & 0x0000ffff) * 1812433253 + this.mti + this.mt[this.mti] >>>= 0 + } + } + + seedArray(vector: ReadonlyArray) { + let i = 1, + j = 0, + k = N > vector.length ? N : vector.length, + s: number + + this.setSeed(19650218) + + for (; k > 0; k--) { + s = this.mt[i - 1]! ^ (this.mt[i - 1]! >>> 30) + + this.mt[i] = (this.mt[i]! ^ (((((s & 0xffff0000) >>> 16) * 1664525) << 16) + (s & 0x0000ffff) * 1664525)) + + vector[j]! + j + this.mt[i] >>>= 0 + i++ + j++ + if (i >= N) { + this.mt[0] = this.mt[N - 1]! + i = 1 + } + if (j >= vector.length) { + j = 0 + } + } + + for (k = N - 1; k; k--) { + s = this.mt[i - 1]! ^ (this.mt[i - 1]! >>> 30) + this.mt[i] = (this.mt[i]! ^ (((((s & 0xffff0000) >>> 16) * 1566083941) << 16) + (s & 0x0000ffff) * 1566083941)) - + i + this.mt[i] >>>= 0 + i++ + if (i >= N) { + this.mt[0] = this.mt[N - 1]! + i = 1 + } + } + + this.mt[0] = 0x80000000 + } + + next() { + let y: number, + kk: number, + // eslint-disable-next-line prefer-const + mag01: [number, number] = [0, MATRIX_A] + + if (this.mti >= N) { + if (this.mti === N + 1) { + this.setSeed(5489) + } + + for (kk = 0; kk < N - M; kk++) { + y = (this.mt[kk]! & UPPER_MASK) | (this.mt[kk + 1]! & LOWER_MASK) + this.mt[kk] = this.mt[kk + M]! ^ (y >>> 1) ^ mag01[y & 1]! + } + + for (; kk < N - 1; kk++) { + y = (this.mt[kk]! & UPPER_MASK) | (this.mt[kk + 1]! & LOWER_MASK) + this.mt[kk] = this.mt[kk + (M - N)]! ^ (y >>> 1) ^ mag01[y & 1]! + } + + y = (this.mt[N - 1]! & UPPER_MASK) | (this.mt[0]! & LOWER_MASK) + this.mt[N - 1] = this.mt[M - 1]! ^ (y >>> 1) ^ mag01[y & 1]! + this.mti = 0 + } + + y = this.mt[this.mti++]! + + y ^= y >>> 11 + y ^= (y << 7) & 0x9d2c5680 + y ^= (y << 15) & 0xefc60000 + y ^= y >>> 18 + + return y >>> 0 + } + + min(): number { + return 0 + } + + max(): number { + return 0xffffffff + } +} diff --git a/packages/stdlib/_src/io/Random/Generator/RandomGenerator.ts b/packages/stdlib/_src/io/Random/Generator/RandomGenerator.ts new file mode 100644 index 00000000..2ec285ab --- /dev/null +++ b/packages/stdlib/_src/io/Random/Generator/RandomGenerator.ts @@ -0,0 +1,20 @@ +export interface RandomGenerator { + setSeed(seed: number): void + next(): number + jump?(): RandomGenerator + min(): number // inclusive + max(): number // inclusive +} + +export function generateN(rng: RandomGenerator, num: number): ReadonlyArray { + const out: Array = [] + for (let idx = 0; idx != num; ++idx) { + const nextOut = rng.next() + out.push(nextOut) + } + return out +} + +export function skipN(rng: RandomGenerator, num: number): void { + generateN(rng, num) +} diff --git a/packages/stdlib/_src/io/Random/MutableRandom.ts b/packages/stdlib/_src/io/Random/MutableRandom.ts new file mode 100644 index 00000000..99e37237 --- /dev/null +++ b/packages/stdlib/_src/io/Random/MutableRandom.ts @@ -0,0 +1,105 @@ +import { + uniformArrayIntDistribution, + uniformBigIntDistribution, + uniformIntDistribution +} from "@tsplus/stdlib/io/Random/Distribution" +import type { RandomGenerator } from "@tsplus/stdlib/io/Random/Generator/RandomGenerator" + +/** + * Wrapper around an instance of a `RandomGenerator` random number generator + * offering a simpler interface to deal with random with impure patterns. + */ +export class MutableRandom { + private static MIN_INT: number = 0x80000000 | 0 + private static MAX_INT: number = 0x7fffffff | 0 + private static DBL_FACTOR: number = Math.pow(2, 27) + private static DBL_DIVISOR: number = Math.pow(2, -53) + + /** + * Create a mutable random number generator. + * + * @param internalRng - Immutable random number generator + */ + constructor(private internalRng: RandomGenerator) {} + + /** + * Clone the random number generator. + */ + clone(): MutableRandom { + return new MutableRandom(this.internalRng) + } + + /** + * Generate an integer having `bits` random bits. + * + * @param bits - Number of bits to generate + */ + next(bits: number): number { + return this.uniformIn(0, (1 << bits) - 1) + } + + /** + * Generate a random boolean. + */ + + nextBoolean(): boolean { + return this.uniformIn(0, 1) === 1 + } + + /** + * Generate a random integer (32 bits). + */ + nextInt(): number + + /** + * Generate a random integer between min (inclusive) and max (inclusive). + * + * @param min - Minimal integer value + * @param max - Maximal integer value + */ + nextInt(min: number, max: number): number + nextInt(min?: number, max?: number): number { + return this.uniformIn(min == null ? MutableRandom.MIN_INT : min, max == null ? MutableRandom.MAX_INT : max) + } + + /** + * Generate a random `bigint` between min (inclusive) and max (inclusive). + * + * @param min - Minimal `bigint` value + * @param max - Maximal `bigint` value + */ + nextBigInt(min: bigint, max: bigint): bigint { + return uniformBigIntDistribution(min, max, this.internalRng) + } + + /** + * Generate a random `ArrayInt` between min (inclusive) and max (inclusive). + * + * @param min - Minimal `ArrayInt` value + * @param max - Maximal `ArrayInt` value + */ + nextArrayInt( + min: { sign: 1 | -1; data: number[] }, + max: { sign: 1 | -1; data: number[] } + ): { sign: 1 | -1; data: number[] } { + return uniformArrayIntDistribution(min, max, this.internalRng) + } + + /** + * Generate a random floating point number between 0.0 (inclusive) and 1.0 + * (exclusive). + */ + nextDouble(): number { + const a = this.next(26) + const b = this.next(27) + return (a * MutableRandom.DBL_FACTOR + b) * MutableRandom.DBL_DIVISOR + } + + setSeed(seed: number): void { + this.internalRng.setSeed(seed) + } + + private uniformIn(rangeMin: number, rangeMax: number): number { + return uniformIntDistribution(rangeMin, rangeMax, this.internalRng) + } +} diff --git a/packages/stdlib/_src/utilities/RandomPCG.ts b/packages/stdlib/_src/io/Random/PCGRandom.ts similarity index 55% rename from packages/stdlib/_src/utilities/RandomPCG.ts rename to packages/stdlib/_src/io/Random/PCGRandom.ts index 7d38a3ad..90b282df 100644 --- a/packages/stdlib/_src/utilities/RandomPCG.ts +++ b/packages/stdlib/_src/io/Random/PCGRandom.ts @@ -1,11 +1,13 @@ -// forked from https://github.com/frptools - -// Copyright 2014 Thom Chiovoloni, released under the MIT license. - -/// A random number generator based on the basic implementation of the PCG algorithm, -/// as described here: http://www.pcg-random.org/ - -// Adapted for TypeScript from Thom's original code at https://github.com/thomcc/pcg-random +/* + * Copyright 2014 Thom Chiovoloni, released under the MIT license. + * + * A random number generator based on the basic implementation of the PCG algorithm, + * as described here: http://www.pcg-random.org/ + * + * Adapted for TypeScript from Thom's original code at https://github.com/thomcc/pcg-random + * + * forked from https://github.com/frptools + */ export function isNothing(value: T | null | undefined) { return value === void 0 || value === null @@ -21,22 +23,44 @@ const BIT_27 = 134217728.0 export type PCGRandomState = [number, number, number, number] export type OptionalNumber = number | null | undefined -export class RandomPCG { +/** + * PCG is a family of simple fast space-efficient statistically good algorithms + * for random number generation. Unlike many general-purpose RNGs, they are also + * hard to predict. + */ +export class PCGRandom { private _state: Int32Array + + /** + * Creates an instance of PCGRandom. + * + * @param seed - The low 32 bits of the seed (0 is used for high 32 bits). + * + * @memberOf PCGRandom + */ constructor(seed?: OptionalNumber) + /** + * Creates an instance of PCGRandom. + * + * @param seedHi - The high 32 bits of the seed. + * @param seedLo - The how 32 bits of the seed. + * @param inc - The low 32 bits of the incrementer (0 is used for high 32 bits). + * + * @memberOf PCGRandom + */ constructor(seedHi: OptionalNumber, seedLo: OptionalNumber, inc?: OptionalNumber) - constructor( - seedHi: OptionalNumber, - seedLo: OptionalNumber, - incHi: OptionalNumber, - incLo: OptionalNumber - ) - constructor( - seedHi?: OptionalNumber, - seedLo?: OptionalNumber, - incHi?: OptionalNumber, - incLo?: OptionalNumber - ) { + /** + * Creates an instance of PCGRandom. + * + * @param seedHi - The high 32 bits of the seed. + * @param seedLo - The how 32 bits of the seed. + * @param incHi - The high 32 bits of the incrementer. + * @param incLo - The how 32 bits of the incrementer. + * + * @memberOf PCGRandom + */ + constructor(seedHi: OptionalNumber, seedLo: OptionalNumber, incHi: OptionalNumber, incLo: OptionalNumber) + constructor(seedHi?: OptionalNumber, seedLo?: OptionalNumber, incHi?: OptionalNumber, incLo?: OptionalNumber) { if (isNothing(seedLo) && isNothing(seedHi)) { seedLo = (Math.random() * 0xffffffff) >>> 0 seedHi = 0 @@ -54,28 +78,23 @@ export class RandomPCG { incHi = 0 } - this._state = new Int32Array([ - 0, - 0, - ( incHi) >>> 0, - ((incLo || 0) | 1) >>> 0 - ]) + this._state = new Int32Array([0, 0, ( incHi) >>> 0, ((incLo || 0) | 1) >>> 0]) this._next() - add64( - this._state, - this._state[0]!, - this._state[1]!, - ( seedHi) >>> 0, - ( seedLo) >>> 0 - ) + add64(this._state, this._state[0]!, this._state[1]!, ( seedHi) >>> 0, ( seedLo) >>> 0) this._next() return this } + /** + * @returns A copy of the internal state of this random number generator as a JavaScript Array + */ getState(): PCGRandomState { return [this._state[0]!, this._state[1]!, this._state[2]!, this._state[3]!] } + /** + * Restore state previously retrieved using getState() + */ setState(state: PCGRandomState) { this._state[0] = state[0] this._state[1] = state[1] @@ -83,51 +102,54 @@ export class RandomPCG { this._state[3] = state[3] | 1 } - private _next() { - const oldHi = this._state[0]! >>> 0 - const oldLo = this._state[1]! >>> 0 - - mul64(this._state, oldHi, oldLo, MUL_HI, MUL_LO) - add64( - this._state, - this._state[0]!, - this._state[1]!, - this._state[2]!, - this._state[3]! - ) - - let xsHi = oldHi >>> 18 - let xsLo = ((oldLo >>> 18) | (oldHi << 14)) >>> 0 - xsHi = (xsHi ^ oldHi) >>> 0 - xsLo = (xsLo ^ oldLo) >>> 0 - const xorshifted = ((xsLo >>> 27) | (xsHi << 5)) >>> 0 - const rot = oldHi >>> 27 - const rot2 = ((-rot >>> 0) & 31) >>> 0 - return ((xorshifted >>> rot) | (xorshifted << rot2)) >>> 0 - } - + /// Get a uniformly distributed 32 bit integer between [0, max). integer(max: number) { if (!max) { return this._next() } max = max >>> 0 if ((max & (max - 1)) === 0) { - return this._next() & (max - 1) + return this._next() & (max - 1) // fast path for power of 2 } let num = 0 const skew = (-max >>> 0) % max >>> 0 for (num = this._next(); num < skew; num = this._next()) { - // + // this loop will rarely execute more than twice, + // and is intentionally empty } return num % max } + /// Get a uniformly distributed IEEE-754 double between 0.0 and 1.0, with + /// 53 bits of precision (every bit of the mantissa is randomized). number() { const hi = (this._next() & 0x03ffffff) * 1.0 const lo = (this._next() & 0x07ffffff) * 1.0 return (hi * BIT_27 + lo) / BIT_53 } + + private _next() { + // save current state (what we'll use for this number) + const oldHi = this._state[0]! >>> 0 + const oldLo = this._state[1]! >>> 0 + + // churn LCG. + mul64(this._state, oldHi, oldLo, MUL_HI, MUL_LO) + add64(this._state, this._state[0]!, this._state[1]!, this._state[2]!, this._state[3]!) + + // get least sig. 32 bits of ((oldstate >> 18) ^ oldstate) >> 27 + let xsHi = oldHi >>> 18 + let xsLo = ((oldLo >>> 18) | (oldHi << 14)) >>> 0 + xsHi = (xsHi ^ oldHi) >>> 0 + xsLo = (xsLo ^ oldLo) >>> 0 + const xorshifted = ((xsLo >>> 27) | (xsHi << 5)) >>> 0 + // rotate xorshifted right a random amount, based on the most sig. 5 bits + // bits of the old state. + const rot = oldHi >>> 27 + const rot2 = ((-rot >>> 0) & 31) >>> 0 + return ((xorshifted >>> rot) | (xorshifted << rot2)) >>> 0 + } } function mul64( @@ -162,6 +184,7 @@ function mul64( out[1] = lo } +// add two 64 bit numbers (given in parts), and store the result in `out`. function add64( out: Int32Array, aHi: number, diff --git a/packages/stdlib/_src/structure/Hash.ts b/packages/stdlib/_src/structure/Hash.ts index 49fb772e..f6ae5fdf 100644 --- a/packages/stdlib/_src/structure/Hash.ts +++ b/packages/stdlib/_src/structure/Hash.ts @@ -146,7 +146,7 @@ function isZero(value: any): boolean { return value === null || value === void 0 || value === false } -const RANDOM = new RandomPCG((Math.random() * 4294967296) >>> 0) +const RANDOM = new PCGRandom((Math.random() * 4294967296) >>> 0) const CACHE = new WeakMap() function randomInt() { diff --git a/packages/stdlib/_src/utilities.ts b/packages/stdlib/_src/utilities.ts index 20fa346d..9ce53b56 100644 --- a/packages/stdlib/_src/utilities.ts +++ b/packages/stdlib/_src/utilities.ts @@ -1,4 +1,3 @@ export * as guard from "@tsplus/stdlib/utilities/Guards" export * as lazy from "@tsplus/stdlib/utilities/Lazy" -export * as randomPCG from "@tsplus/stdlib/utilities/RandomPCG" export * as types from "@tsplus/stdlib/utilities/Types"