From df05b9c97e9a06ccb9a166da0d1265e541a74ec6 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Mon, 29 Apr 2024 15:11:09 +0900 Subject: [PATCH 1/2] pcg pseudo-number generator --- examples/gno.land/p/demo/pcg/gno.mod | 1 + examples/gno.land/p/demo/pcg/pcg32.gno | 184 ++++++++++++++++ examples/gno.land/p/demo/pcg/pcg32_test.gno | 226 ++++++++++++++++++++ examples/gno.land/p/demo/pcg/pcg64.gno | 205 ++++++++++++++++++ examples/gno.land/p/demo/pcg/pcg64_test.gno | 146 +++++++++++++ 5 files changed, 762 insertions(+) create mode 100644 examples/gno.land/p/demo/pcg/gno.mod create mode 100644 examples/gno.land/p/demo/pcg/pcg32.gno create mode 100644 examples/gno.land/p/demo/pcg/pcg32_test.gno create mode 100644 examples/gno.land/p/demo/pcg/pcg64.gno create mode 100644 examples/gno.land/p/demo/pcg/pcg64_test.gno diff --git a/examples/gno.land/p/demo/pcg/gno.mod b/examples/gno.land/p/demo/pcg/gno.mod new file mode 100644 index 00000000000..8b92c6e2e1b --- /dev/null +++ b/examples/gno.land/p/demo/pcg/gno.mod @@ -0,0 +1 @@ +module gno.land/p/demo/pcg \ No newline at end of file diff --git a/examples/gno.land/p/demo/pcg/pcg32.gno b/examples/gno.land/p/demo/pcg/pcg32.gno new file mode 100644 index 00000000000..80da2036a19 --- /dev/null +++ b/examples/gno.land/p/demo/pcg/pcg32.gno @@ -0,0 +1,184 @@ +package pcg + +// ref: https://gist.github.com/ivan-pi/060e38d5f9a86c57923a61fbf18d095c +const ( + defaultState = 0x853c49e6748fea9b // 9600629759793949339 + multiplier = 0x5851f42d4c957f2d // 6364136223846793005 + incrementStep = 0xda3e39cb94b95bdb // 15726070495360670683 +) + +// PCG32 is a 32-bit pseudorandom number generator based on the PCG family of algorithms. +type PCG32 struct { + state, increment uint64 +} + +// NewPCG32 creates a new PCG32 generator with the default state and sequence values. +func NewPCG32() *PCG32 { + return &PCG32{ + state: defaultState, + increment: incrementStep, + } +} + +// Seed initializes the PCG32 generator with the given state and sequence values. +func (p *PCG32) Seed(state, sequence uint64) *PCG32 { + p.increment = (sequence << 1) | 1 + p.state = (state+p.increment)*multiplier + incrementStep + return p +} + +// neg_mask is a mask to extract the lower 5 bits of a number. +const neg_mask = 31 + +// Uint32 generates a pseudorandom 32-bit unsigned integer using the PCG32 algorithm. +// It updates the internal state of the generator using the PCG32 formula: +// +// state = state * multiplier + increment +// +// It then applies a series of bitwise operations to the old state to produce the random number: +// 1. XOR the old state with (old state >> 18). +// 2. Shift the result right by 27 bits to obtain `xorshifted`. +// 3. Calculate the rotation amount `rot` by shifting the old state right by 59 bits. +// 4. Rotate `xorshifted` right by `rot` bits and OR it with `xorshifted` rotated left by `((-rot) & 31)` bits. +// +// The resulting value is returned as the random number. +func (p *PCG32) Uint32() uint32 { + old := p.state + p.state = old*multiplier + p.increment + + xorshifted := uint32(((old >> 18) ^ old) >> 27) + rot := uint32(old >> 59) + + return (xorshifted >> rot) | (xorshifted << (neg_mask - rot)) +} + +// Uintn32 generates a pseudorandom number in the range [0, bound) using the PCG32 algorithm. +func (p *PCG32) Uintn32(bound uint32) uint32 { + if bound == 0 { + return 0 + } + + threshold := -bound % bound + for { + r := p.Uint32() + if r >= threshold { + return r % bound + } + } +} + +// Uint63 generates a pseudorandom 63-bit integer using two 32-bit numbers. +// The function ensures that the returned number is within the range of 0 to 2^63-1. +func (p *PCG32) Uint63() int64 { + upper := int64(p.Uint32()) & 0x7FFFFFFF // use only the lower 31 bits of the upper half + lower := int64(p.Uint32()) // use all 32 bits of the lower half + return (upper << 32) | lower // combine the two halves to form a 63-bit integer +} + +// advancedLCG64 is an implementation of a 64-bit linear congruential generator (LCG). +// It takes the following parameters: +// - state: The current state of the LCG. +// - delta: The number of steps to advance the LCG. +// - mul: The multiplier of the LCG. +// - add: The increment of the LCG. +// +// The function advances the LCG by `delta` steps and returns the new state. +// +// The LCG algorithm is defined by the following recurrence relation: +// +// state(n+1) = (state(n) * mul + add) mod 2^64 +// +// The function uses an efficient algorithm to advance the LCG by `delta` steps +// without iterating `delta` times. It exploits the properties of the LCG and +// uses binary exponentiation to calculate the result in logarithmic time. +// +// The algorithm works as follows: +// 1. Initialize `accMul` to 1 and `accAdd` to 0. +// 2. Iterate while `delta` is greater than 0: +// - If the least significant bit of `delta` is 1: +// - Multiply `accMul` by `mul`. +// - Set `accAdd` to `accAdd * mul + add`. +// - Update `add` to `(mul + 1) * add`. +// - Update `mul` to `mul * mul`. +// - Right-shift `delta` by 1 (divide by 2). +// 3. Return `accMul * state + accAdd`. +// +// The time complexity of this function is O(log(delta)), as it iterates logarithmically +// with respect to `delta`. The space complexity is O(1), as it uses only a constant +// amount of additional memory. +func (p *PCG32) advancedLCG64(state, delta, mul, add uint64) uint64 { + accMul := uint64(1) + accAdd := uint64(0) + + for delta > 0 { + if delta&1 != 0 { + accMul *= mul + accAdd = accAdd*mul + add + } + add = (mul + 1) * add + mul *= mul + delta /= 2 + } + return accMul*state + accAdd +} + +// Advance moves the PCG32 generator forward by `delta` steps. +// It updates the internal state of the generator using the `lcg64` function +// and returns the updated PCG32 instance. +func (p *PCG32) Advance(delta uint64) *PCG32 { + p.state = p.advancedLCG64(p.state, delta, multiplier, incrementStep) + return p +} + +// Retreat moves the PCG32 generator backward by `delta` steps. +// It calculates the equivalent forward delta using the two's complement of `delta` +// and calls the `Advance` function with the calculated delta. +// It returns the updated PCG32 instance. +func (p *PCG32) Retreat(delta uint64) *PCG32 { + safeDelta := ^delta + 1 + return p.Advance(safeDelta) +} + +// Shuffle shuffles the elements of a slice in place using the Fisher-Yates shuffle algorithm. +// It takes the following parameters: +// - n: The number of elements to shuffle. +// - swap: A function that swaps the elements at indices i and j. +// +// The function shuffles the elements by iterating from n-1 down to 1. In each iteration: +// 1. Generate a random index j between 0 and the current index i (inclusive). +// 2. Call the `swap` function to swap the elements at indices i and j. +// +// This process ensures that each element has an equal probability of ending up at any position +// in the shuffled slice. +// +// The time complexity of this function is O(n), where n is the number of elements to shuffle. +// The space complexity is O(1), as it shuffles the elements in place and uses only a constant +// amount of additional memory. +// +// Note: The function panics if n is negative. +func (p *PCG32) Shuffle(n int, swap func(i, j int)) { + if n < 0 { + panic("invalid argument to shuffle") + } + if n < 2 { + return + } + // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + for i := n - 1; i > 0; i-- { + j := int(p.Uintn32(uint32(i + 1))) + swap(i, j) + } +} + +// Perm returns a slice of n integers. The slice is a random permutation of the integers [0, n). +func (p *PCG32) Perm(n int) []int { + res := make([]int, n) + for i := 0; i < n; i++ { + res[i] = i + } + for i := 1; i < n; i++ { + j := int(p.Uintn32(uint32(i + 1))) + res[i], res[j] = res[j], res[i] + } + return res +} diff --git a/examples/gno.land/p/demo/pcg/pcg32_test.gno b/examples/gno.land/p/demo/pcg/pcg32_test.gno new file mode 100644 index 00000000000..885735f404b --- /dev/null +++ b/examples/gno.land/p/demo/pcg/pcg32_test.gno @@ -0,0 +1,226 @@ +package pcg + +import ( + "math" + "testing" +) + +func TestPCG32_Uintn32(t *testing.T) { + pcg := NewPCG32().Seed(12345, 67890) + + testCases := []struct { + bound uint32 + }{ + {0}, + {1}, + {10}, + {100}, + {1000}, + {10000}, + } + + for _, tc := range testCases { + result := pcg.Uintn32(tc.bound) + if tc.bound != 0 && result >= tc.bound { + t.Errorf("Bounded(%d) = %d; expected a value between 0 and %d", tc.bound, result, tc.bound) + } + if tc.bound == 0 && result != 0 { + t.Errorf("Bounded(%d) = %d; expected 0", tc.bound, result) + } + } +} + +func TestUint63(t *testing.T) { + pcg := NewPCG64(42, 54) + pcg.Seed(42, 54, 18, 27) + for i := 0; i < 100; i++ { + val := pcg.Uint63() + if val < 0 || val > math.MaxInt64 { + t.Errorf("Value out of bounds: %d", val) + } + } +} + +func TestPCG32_UniformDistribution(t *testing.T) { + pcg := NewPCG32().Seed(12345, 67890) + numBins := 10 + numSamples := 1000000 + toleranceRatio := 10 // 10% tolerance + bins := make([]int, numBins) + + for i := 0; i < numSamples; i++ { + r := pcg.Uint32() + binIndex := int(uint64(r) * uint64(numBins) >> 32) + bins[binIndex]++ + } + + expected := numSamples / numBins + tolerance := expected / toleranceRatio + + for _, count := range bins { + if abs(count-expected) > tolerance { + t.Errorf("bin count %d is outside the expected range [%d, %d]", count, expected-tolerance, expected+tolerance) + } + } +} + +func TestPCG32_lcg64(t *testing.T) { + pcg := NewPCG32() + + testCases := []struct { + state uint64 + delta uint64 + expected uint64 + }{ + {1, 1, 3643462645497912072}, + {1, 10, 15256603694110904427}, + {1, 100, 5234694153321213237}, + {1, 1000, 2323235076269450313}, + {1, 10000, 6143568259046921169}, + } + + for _, tc := range testCases { + result := pcg.advancedLCG64(tc.state, tc.delta, multiplier, incrementStep) + if result != tc.expected { + t.Errorf("lcg64(%d, %d) = %d; expected %d", tc.state, tc.delta, result, tc.expected) + } + } +} + +func TestPCG32_Advance(t *testing.T) { + pcg := NewPCG32() + + testCases := []struct { + initialState uint64 + delta uint64 + expectedState uint64 + }{ + {1, 1, 3643462645497912072}, + {1, 10, 15256603694110904427}, + {1, 100, 5234694153321213237}, + {1, 1000, 2323235076269450313}, + {1, 10000, 6143568259046921169}, + } + + for _, tc := range testCases { + pcg.state = tc.initialState + pcg.Advance(tc.delta) + if pcg.state != tc.expectedState { + t.Errorf("Advance(%d) = %d; expected %d", tc.delta, pcg.state, tc.expectedState) + } + } +} + +func TestPCG32_Retreat(t *testing.T) { + pcg := NewPCG32() + + testCases := []struct { + initialState uint64 + delta uint64 + expectedState uint64 + }{ + {3643462645497912072, 1, 1}, + {15256603694110904427, 10, 1}, + {5234694153321213237, 100, 1}, + {2323235076269450313, 1000, 1}, + {6143568259046921169, 10000, 1}, + } + + for _, tc := range testCases { + pcg.state = tc.initialState + pcg.Retreat(tc.delta) + if pcg.state != tc.expectedState { + t.Errorf("Retreat(%d) = %d; expected %d", tc.delta, pcg.state, tc.expectedState) + } + } +} + +func abs(x int) int { + if x < 0 { + return -x + } + return x +} + +func TestPCG32_Shuffle(t *testing.T) { + pcg := NewPCG32().Seed(12345, 67890) + + testCases := []struct { + n int + expected []int + }{ + {0, []int{}}, + {1, []int{0}}, + {2, []int{0, 1}}, + {3, []int{0, 1, 2}}, + {4, []int{0, 1, 2, 3}}, + {5, []int{0, 1, 2, 3, 4}}, + } + + for _, tc := range testCases { + arr := make([]int, tc.n) + for i := range arr { + arr[i] = i + } + + pcg.Shuffle(tc.n, func(i, j int) { + arr[i], arr[j] = arr[j], arr[i] + }) + + if tc.n > 1 && isArrayEqual(arr, tc.expected) { + t.Errorf("Shuffle(%d) = %v; expected a shuffled version of %v", tc.n, arr, tc.expected) + } + } +} + +func isArrayEqual(a, b []int) bool { + if len(a) != len(b) { + return false + } + for i := range a { + if a[i] != b[i] { + return false + } + } + return true +} + +func TestPCG32_Perm(t *testing.T) { + pcg := NewPCG32().Seed(12345, 67890) + + tests := []struct { + n int + expected []int + }{ + {0, []int{}}, + {1, []int{0}}, + {2, []int{0, 1}}, + {3, []int{0, 1, 2}}, + {4, []int{0, 1, 2, 3}}, + {5, []int{0, 1, 2, 3, 4}}, + } + + for _, tt := range tests { + result := pcg.Perm(tt.n) + if !isPermutation(result, tt.expected) { + t.Errorf("Perm(%d) = %v; expected a permutation of %v", tt.n, result, tt.expected) + } + } +} + +func isPermutation(a, b []int) bool { + if len(a) != len(b) { + return false + } + counts := make(map[int]int) + for _, num := range a { + counts[num]++ + } + for _, num := range b { + if counts[num] == 0 { + return false + } + counts[num]-- + } + return true +} diff --git a/examples/gno.land/p/demo/pcg/pcg64.gno b/examples/gno.land/p/demo/pcg/pcg64.gno new file mode 100644 index 00000000000..314a28b8520 --- /dev/null +++ b/examples/gno.land/p/demo/pcg/pcg64.gno @@ -0,0 +1,205 @@ +package pcg + +import ( + "errors" + "math" + "math/bits" +) + +// A PCG64 is a PCG64 generator with 128 bits of internal state. +// A zero PCG64 is equivalent to one seeded with 0. +type PCG64 struct { + hi, lo *PCG32 +} + +// NewPCG64 returns a new PCG64 generator seeded with the given values. +// seed1 and seed2 are the initial state values for the generator. +func NewPCG64(seed1, seed2 uint64) *PCG64 { + return &PCG64{ + hi: NewPCG32().Seed(seed1, 0), + lo: NewPCG32().Seed(seed2, 0), + } +} + +// Seed initializes the PCG64 generator with the given state and sequence values. +// seed1 and seed2 are the initial state values, and seq1 and seq2 are the sequence values. +func (p *PCG64) Seed(seed1, seed2, seq1, seq2 uint64) *PCG64 { + mask := ^uint64(0) >> 1 + if seq1&mask == seq2&mask { + seq2 = ^seq2 + } + p.lo.Seed(seed1, seq1) + p.hi.Seed(seed2, seq2) + + return p +} + +// Uint64 generates a pseudorandom 64-bit unsigned integer using the PCG64 algorithm. +func (p *PCG64) Uint64() uint64 { + return uint64(p.hi.Uint32())<<32 | uint64(p.lo.Uint32()) +} + +// Uint63 generates a pseudorandom 63-bit integer using the PCG64 algorithm. +// It masks the highest bit to ensure the value is within the 63-bit integer range. +func (p *PCG64) Uint63() int64 { + return int64(p.Uint64() & 0x7FFFFFFFFFFFFFFF) // Mask the highest bit to stay within the 63-bit range +} + +// Uint64n generates a pseudorandom number in the range [0, bound) using the PCG64 algorithm. +func (p *PCG64) Uint64n(bound uint64) uint64 { + threshold := -bound % bound + for { + r := p.Uint64() + if r >= threshold { + return r % bound + } + } +} + +// Float64 returns a random float64 value in the range [0.0, 1.0). +// +// The function generates a 63-bit unsigned integer using Uint63() and then: +// 1. Shifts the generated number 11 bits to the right to obtain a 52-bit mantissa. +// 2. Multiplies the mantissa by 2^(-52) to scale it to the range [0.0, 1.0). +// +// This method produces a double-precision floating-point number with 52 bits of precision, +// which is the standard precision for float64 values. +// +// The time complexity of this function is O(1), as it performs a constant number of operations. +// The space complexity is O(1), as it uses only a constant amount of additional memory. +// +// Note: This method does not use all 64 bits of the generated number, which may result in +// a slight loss of precision compared to Float64Full(). However, it is faster and sufficient +// for most use cases. +func (p *PCG64) Float64() float64 { + return float64(p.Uint63()>>11) * (1.0 / (1 << 52)) +} + +// Float64Full returns a random float64 value in the range [0.0, 1.0). +// +// The function generates a 64-bit unsigned integer using Uint64() and then: +// 1. Masks the generated number to obtain the lower 64 bits. +// 2. Divides the masked number by the maximum 64-bit unsigned integer value (math.MaxUint64) +// to scale it to the range [0.0, 1.0). +// +// This method utilizes the full 64 bits of the generated number, providing slightly higher +// precision compared to Float64(). However, it is slower due to the additional masking and +// division operations. +// +// The time complexity of this function is O(1), as it performs a constant number of operations. +// The space complexity is O(1), as it uses only a constant amount of additional memory. +// +// Note: While this method provides higher precision, it may not be necessary for most use cases. +// Float64() is generally faster and sufficient for generating random float64 values. +func (p *PCG64) Float64Full() float64 { + return float64(p.Uint64()&0xFFFFFFFFFFFFFF) / math.MaxUint64 +} + +// Advance moves the PCG64 generator forward by `delta` steps. +// It updates the initial state of the generator. +func (p *PCG64) Advance(delta uint64) *PCG64 { + p.hi.Advance(delta) + p.lo.Advance(delta) + return p +} + +// Retreat moves the PCG64 generator backward by `delta` steps. +// It updates the initial state of the generator. +func (p *PCG64) Retreat(delta uint64) *PCG64 { + safeDelta := ^uint64(0) - 1 + p.Advance(safeDelta) + return p +} + +// Shuffle shuffles the elements of a slice in place using the Fisher-Yates shuffle algorithm. +// It takes the following parameters: +// - n: The number of elements to shuffle. +// - swap: A function that swaps the elements at indices i and j. +// +// The function shuffles the elements by iterating from n-1 down to 1. In each iteration: +// 1. Generate a random index j between 0 and the current index i (inclusive) using Uint64n. +// 2. Call the `swap` function to swap the elements at indices i and j. +// +// This process ensures that each element has an equal probability of ending up at any position +// in the shuffled slice. +// +// The time complexity of this function is O(n), where n is the number of elements to shuffle. +// The space complexity is O(1), as it shuffles the elements in place and uses only a constant +// amount of additional memory. +func (p *PCG64) Shuffle(n int, swap func(i, j int)) { + // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + for i := n - 1; i > 0; i-- { + j := int(p.Uint64n(uint64(i + 1))) + swap(i, j) + } +} + +// Perm returns a random permutation of the integers in the range [0, n). +func (p *PCG64) Perm(n int) []int { + res := make([]int, n) + for i := range res { + res[i] = i + } + p.Shuffle(n, func(i, j int) { + res[i], res[j] = res[j], res[i] + }) + return res +} + +func (p *PCG64) next() (uint64, uint64) { + const ( + mulHi = 2549297995355413924 + mulLo = 4865540595714422341 + incHi = 6364136223846793005 + incLo = 1442695040888963407 + ) + + // state = state * mul + inc + hi, lo := bits.Mul64(p.lo.state, mulLo) + hi += p.hi.state*mulLo + p.lo.state*mulHi + + lo, c := bits.Add64(lo, incLo, 0) + hi, _ = bits.Add64(hi, incHi, c) + + p.lo.state = lo + p.hi.state = hi + + return hi, lo +} + +// Uint64nWithMCG generates a pseudorandom 64-bit unsigned integer using the PCG64 algorithm +// with a Multiplicative Congruential Generator (MCG) as the output function. +// +// The function updates the internal state of the generator using the PCG64 transition function: +// state = state * multiplier + increment +// +// It then applies a series of bitwise operations to the updated state to produce the random number: +// 1. XOR the high 64 bits of the state with the high 64 bits right-shifted by 22 bits. +// 2. Multiply the result by a cheap multiplier (0xda942042e4dd58b5). +// 3. XOR the result with the result right-shifted by 48 bits. +// 4. Multiply the result by (low 64 bits of the state | 1). +// +// The resulting value is returned as the random 64-bit unsigned integer. +// +// The MCG output function used in this implementation is based on the one described in the PCG +// random number generator family, specifically the 128-bit MCG with 64-bit output. +// It provides good statistical properties and passes various randomness tests, including PractRand. +// +// The time complexity of this function is O(1), as it performs a constant number of bitwise operations. +// The space complexity is O(1), as it uses only a constant amount of additional memory. +// +// Note: The Uint64nWithMCG function is deterministic for a given state of the PCG64 generator. +// If you need different random numbers across multiple calls, make sure to advance the state of the +// generator using Advance() or reseed it using Seed() between calls. +func (p *PCG64) Uint64nWithMCG() uint64 { + hi, lo := p.next() + + // ref: https://www.pcg-random.org/posts/128-bit-mcg-passes-practrand.html (#64-bit Multiplier) + const cheapMul = 0xda942042e4dd58b5 // 15750249268501108917 + hi ^= hi >> 22 + hi *= cheapMul + hi ^= hi >> 48 + hi *= (lo | 1) + + return hi +} diff --git a/examples/gno.land/p/demo/pcg/pcg64_test.gno b/examples/gno.land/p/demo/pcg/pcg64_test.gno new file mode 100644 index 00000000000..5bf57f4f95c --- /dev/null +++ b/examples/gno.land/p/demo/pcg/pcg64_test.gno @@ -0,0 +1,146 @@ +package pcg + +import ( + "fmt" + "math" + "math/rand" + "testing" + "time" +) + +func TestPCG_Uint63(t *testing.T) { + pcg := NewPCG64(12345, 67890) + + for i := 0; i < 100000; i++ { + val := pcg.Uint63() + if val < 0 { + t.Errorf("Uint63() = %d; want a non-negative number", val) + } + if val > 0x7FFFFFFFFFFFFFFF { + t.Errorf("Uint63() = %d; want a 63-bit integer", val) + } + } +} + +func TestPCG_Advance(t *testing.T) { + pcg := NewPCG64(12345, 67890) + + testCases := []struct { + delta uint64 + expectedStateHi uint64 + expectedStateLo uint64 + }{ + {1, 16443432798917770532, 1294492316257287365}, + {10, 9073714748428748454, 9095006751169262415}, + {100, 1498360792142116778, 11040029025224029795}, + {1000, 7761321322648589714, 770061004744980459}, + {10000, 8930526547519973282, 18106490617456118331}, + } + + for _, tc := range testCases { + pcg.Advance(tc.delta) + if pcg.hi.state != tc.expectedStateHi { + t.Errorf("Advance(%d) hi state = %d; expected %d", tc.delta, pcg.hi.state, tc.expectedStateHi) + } + if pcg.lo.state != tc.expectedStateLo { + t.Errorf("Advance(%d) lo state = %d; expected %d", tc.delta, pcg.lo.state, tc.expectedStateLo) + } + } +} + +func TestPCG(t *testing.T) { + p := NewPCG64(1, 2) + want := []uint64{ + 0x52addb9b0d4aa107, + 0xc5d5c81b8c97ff8f, + 0xcfa82191c9a86caa, + 0x76b48e618586fdfe, + 0x765ac4ba3e566855, + 0x1d6058a5dd7ab27, + 0x2b913f2f76e81329, + 0x74873f4e5348d32e, + 0xc4c940eb70248174, + 0xb5a1651a6627a924, + 0xc34174eb7f136d0a, + 0xe612b37df73df71c, + 0x884a2539ea7aa198, + 0x2976010a57986e59, + 0x1d0d522531d62a7d, + 0xa7da1ad05db25a75, + 0xdbee2df7bd6428be, + 0x598c54d1eb4abdd7, + 0x559ca964532a3777, + 0x6e64af73ece533b0, + } + + for i, x := range want { + u := p.Uint64nWithMCG() + if u != x { + t.Errorf("PCG #%d = %#x, want %#x", i, u, x) + } + } +} + +func TestPCG_Retreat(t *testing.T) { + pcg := NewPCG64(12345, 67890) + + testCases := []struct { + delta uint64 + expectedStateHi uint64 + expectedStateLo uint64 + }{ + {1, 7265056988599925051, 16912344864586758584}, + {10, 15578097273240930873, 13711579158205810606}, + {100, 3761525201756208775, 6157393363865312820}, + {1000, 15336446625969592741, 13630190462364618442}, + {10000, 10106684222517973779, 4620269966716251888}, + } + + for _, tc := range testCases { + pcg.Retreat(tc.delta) + if pcg.hi.state != tc.expectedStateHi { + t.Errorf("Retreat(%d) hi state = %d; expected %d", tc.delta, pcg.hi.state, tc.expectedStateHi) + } + if pcg.lo.state != tc.expectedStateLo { + t.Errorf("Retreat(%d) lo state = %d; expected %d", tc.delta, pcg.lo.state, tc.expectedStateLo) + } + } +} + +func TestPCG64_Shuffle(t *testing.T) { + pcg := NewPCG64(42, 54) + pcg.Seed(42, 54, 18, 27) + + array := []int{1, 2, 3, 4, 5} + pcg.Shuffle(len(array), func(i, j int) { + array[i], array[j] = array[j], array[i] + }) + + if len(array) != 5 { + t.Errorf("Shuffle() len(array) = %d; want 5", len(array)) + } + + if isArrayEqual(array, []int{1, 2, 3, 4, 5}) { + t.Errorf("Shuffle() array = %v; want shuffled", array) + } +} + +func TestFloat64(t *testing.T) { + pcg := NewPCG64(42, 54) + for i := 0; i < 1000; i++ { + val := pcg.Float64() + if val < 0.0 || val > 1.0 { + t.Error("Float64() returned a value out of bounds") + } + } +} + +func TestFloat64Full(t *testing.T) { + pcg := NewPCG64(42, 54) + for i := 0; i < 1000; i++ { + val := pcg.Float64Full() + if val < 0.0 || val >= 1.0 { + t.Error("Float64Full() returned a value out of bounds") + } + } +} From c616ec5afbfb1434a039cefe9eba8281679fb4b3 Mon Sep 17 00:00:00 2001 From: Lee ByeongJun Date: Tue, 30 Apr 2024 18:30:00 +0900 Subject: [PATCH 2/2] support Read --- examples/gno.land/p/demo/pcg/gno.mod | 2 +- examples/gno.land/p/demo/pcg/pcg32.gno | 98 ++++++++--- examples/gno.land/p/demo/pcg/pcg32_test.gno | 175 ++++++++++++++++---- examples/gno.land/p/demo/pcg/pcg64.gno | 164 +++++++++--------- examples/gno.land/p/demo/pcg/pcg64_test.gno | 140 +++++++++++----- 5 files changed, 402 insertions(+), 177 deletions(-) diff --git a/examples/gno.land/p/demo/pcg/gno.mod b/examples/gno.land/p/demo/pcg/gno.mod index 8b92c6e2e1b..805bdf036ab 100644 --- a/examples/gno.land/p/demo/pcg/gno.mod +++ b/examples/gno.land/p/demo/pcg/gno.mod @@ -1 +1 @@ -module gno.land/p/demo/pcg \ No newline at end of file +module gno.land/p/demo/pcg diff --git a/examples/gno.land/p/demo/pcg/pcg32.gno b/examples/gno.land/p/demo/pcg/pcg32.gno index 80da2036a19..7b9ba8149dd 100644 --- a/examples/gno.land/p/demo/pcg/pcg32.gno +++ b/examples/gno.land/p/demo/pcg/pcg32.gno @@ -1,10 +1,12 @@ package pcg +import "encoding/binary" + // ref: https://gist.github.com/ivan-pi/060e38d5f9a86c57923a61fbf18d095c const ( defaultState = 0x853c49e6748fea9b // 9600629759793949339 multiplier = 0x5851f42d4c957f2d // 6364136223846793005 - incrementStep = 0xda3e39cb94b95bdb // 15726070495360670683 + incrementStep = 0x9e3779b97f4a7c15 // https://www.pcg-random.org/posts/bugs-in-splitmix.html ) // PCG32 is a 32-bit pseudorandom number generator based on the PCG family of algorithms. @@ -27,7 +29,7 @@ func (p *PCG32) Seed(state, sequence uint64) *PCG32 { return p } -// neg_mask is a mask to extract the lower 5 bits of a number. +// neg_mask is a constant used in the rotation operation to calculate the left rotation amount. const neg_mask = 31 // Uint32 generates a pseudorandom 32-bit unsigned integer using the PCG32 algorithm. @@ -53,6 +55,16 @@ func (p *PCG32) Uint32() uint32 { } // Uintn32 generates a pseudorandom number in the range [0, bound) using the PCG32 algorithm. +// If bound is 0, it returns 0. +// +// Parameters: +// - bound: The upper bound (exclusive) of the desired range. +// +// Return value: +// - A pseudorandom number in the range [0, bound). +// +// The function calculates a threshold value to ensure a uniform distribution of the generated numbers. +// It repeatedly generates random numbers using the Uint32 function until a number within the desired range is obtained. func (p *PCG32) Uintn32(bound uint32) uint32 { if bound == 0 { return 0 @@ -70,9 +82,9 @@ func (p *PCG32) Uintn32(bound uint32) uint32 { // Uint63 generates a pseudorandom 63-bit integer using two 32-bit numbers. // The function ensures that the returned number is within the range of 0 to 2^63-1. func (p *PCG32) Uint63() int64 { - upper := int64(p.Uint32()) & 0x7FFFFFFF // use only the lower 31 bits of the upper half - lower := int64(p.Uint32()) // use all 32 bits of the lower half - return (upper << 32) | lower // combine the two halves to form a 63-bit integer + upper := int64(p.Uint32()) & 0x7FFFFFFF // Use only the lower 31 bits of the upper half + lower := int64(p.Uint32()) // Use all 32 bits of the lower half + return (upper << 32) | lower // Combine the two halves to form a 63-bit integer } // advancedLCG64 is an implementation of a 64-bit linear congruential generator (LCG). @@ -139,23 +151,18 @@ func (p *PCG32) Retreat(delta uint64) *PCG32 { return p.Advance(safeDelta) } -// Shuffle shuffles the elements of a slice in place using the Fisher-Yates shuffle algorithm. -// It takes the following parameters: -// - n: The number of elements to shuffle. -// - swap: A function that swaps the elements at indices i and j. +// Shuffle shuffles the elements of a slice in-place using the Fisher-Yates shuffle algorithm. // -// The function shuffles the elements by iterating from n-1 down to 1. In each iteration: -// 1. Generate a random index j between 0 and the current index i (inclusive). -// 2. Call the `swap` function to swap the elements at indices i and j. -// -// This process ensures that each element has an equal probability of ending up at any position -// in the shuffled slice. +// Parameters: +// - n: The number of elements to shuffle. +// - swap: A function that swaps the elements at indices i and j in the slice. // -// The time complexity of this function is O(n), where n is the number of elements to shuffle. -// The space complexity is O(1), as it shuffles the elements in place and uses only a constant -// amount of additional memory. +// Panics: +// - If n is negative. // -// Note: The function panics if n is negative. +// The function uses the Fisher-Yates shuffle algorithm to randomly permute the elements of the slice. +// It starts from the last element and iteratively swaps it with a randomly selected element from the remaining unshuffled portion of the slice. +// The random selection is performed using the Uintn32 function to generate a random index within the range [0, i+1). func (p *PCG32) Shuffle(n int, swap func(i, j int)) { if n < 0 { panic("invalid argument to shuffle") @@ -170,7 +177,17 @@ func (p *PCG32) Shuffle(n int, swap func(i, j int)) { } } -// Perm returns a slice of n integers. The slice is a random permutation of the integers [0, n). +// Perm returns a slice of n integers representing a random permutation of the integers in the range [0, n). +// +// Parameters: +// - n: The number of integers to include in the permutation. +// +// Return value: +// - A slice of n integers representing a random permutation of the integers [0, n). +// +// The function initializes a slice with the integers [0, n) in ascending order. +// It then applies the Fisher-Yates shuffle algorithm to the slice, swapping elements at random positions. +// The resulting shuffled slice represents a random permutation of the integers [0, n). func (p *PCG32) Perm(n int) []int { res := make([]int, n) for i := 0; i < n; i++ { @@ -182,3 +199,44 @@ func (p *PCG32) Perm(n int) []int { } return res } + +// Read generates and fills the given byte slice with random bytes using the PCG32 random number generator. +// +// This function repeatedly generates 32-bit unsigned integers using the Uint32 function, +// and uses binary.LittleEndian.PutUint32 to split these integers into bytes and store them in the slice. +// This approach efficiently copies memory in accordance with the CPU's endian configuration, thereby enhancing performance. +// +// Parameters: +// - buf: The byte slice to be filled with random bytes. +// +// Return values: +// - n: The number of bytes generated and stored in the byte slice. It is always equal to len(buf). +// - err: Always returns nil, indicating no error occurred. +func (p *PCG32) Read(buf []byte) (int, error) { + n := len(buf) + i := 0 + + // loop unrolling: process 8 bytes in each iteration + for ; i <= n-8; i += 8 { + val1 := p.Uint32() + val2 := p.Uint32() + binary.LittleEndian.PutUint32(buf[i:], val1) + binary.LittleEndian.PutUint32(buf[i+4:], val2) + } + + // handle remaining bytes (less than 8 bytes) + if i < n { + remaining := buf[i:] + for j := 0; j < len(remaining); j += 4 { + if i+j < n { + val := p.Uint32() + // handle remaining bytes (less than 4 bytes) + for k := 0; k < 4 && (j+k) < len(remaining); k++ { + remaining[j+k] = byte(val >> (8 * k)) + } + } + } + } + + return n, nil +} diff --git a/examples/gno.land/p/demo/pcg/pcg32_test.gno b/examples/gno.land/p/demo/pcg/pcg32_test.gno index 885735f404b..ce17acf5417 100644 --- a/examples/gno.land/p/demo/pcg/pcg32_test.gno +++ b/examples/gno.land/p/demo/pcg/pcg32_test.gno @@ -30,7 +30,7 @@ func TestPCG32_Uintn32(t *testing.T) { } } -func TestUint63(t *testing.T) { +func TestUint63PCG64(t *testing.T) { pcg := NewPCG64(42, 54) pcg.Seed(42, 54, 18, 27) for i := 0; i < 100; i++ { @@ -67,22 +67,22 @@ func TestPCG32_UniformDistribution(t *testing.T) { func TestPCG32_lcg64(t *testing.T) { pcg := NewPCG32() - testCases := []struct { + tests := []struct { state uint64 delta uint64 expected uint64 }{ - {1, 1, 3643462645497912072}, - {1, 10, 15256603694110904427}, - {1, 100, 5234694153321213237}, - {1, 1000, 2323235076269450313}, - {1, 10000, 6143568259046921169}, + {1, 1, 17764851043169991490}, + {1, 10, 13321747199226635079}, + {1, 100, 7812368804252231469}, + {1, 1000, 7287210203119977849}, + {1, 10000, 13461019320290411953}, } - for _, tc := range testCases { - result := pcg.advancedLCG64(tc.state, tc.delta, multiplier, incrementStep) - if result != tc.expected { - t.Errorf("lcg64(%d, %d) = %d; expected %d", tc.state, tc.delta, result, tc.expected) + for _, tt := range tests { + result := pcg.advancedLCG64(tt.state, tt.delta, multiplier, incrementStep) + if result != tt.expected { + t.Errorf("lcg64(%d, %d) = %d; expected %d", tt.state, tt.delta, result, tt.expected) } } } @@ -90,23 +90,23 @@ func TestPCG32_lcg64(t *testing.T) { func TestPCG32_Advance(t *testing.T) { pcg := NewPCG32() - testCases := []struct { - initialState uint64 - delta uint64 - expectedState uint64 + tests := []struct { + state uint64 + delta uint64 + expected uint64 }{ - {1, 1, 3643462645497912072}, - {1, 10, 15256603694110904427}, - {1, 100, 5234694153321213237}, - {1, 1000, 2323235076269450313}, - {1, 10000, 6143568259046921169}, + {1, 1, 17764851043169991490}, + {1, 10, 13321747199226635079}, + {1, 100, 7812368804252231469}, + {1, 1000, 7287210203119977849}, + {1, 10000, 13461019320290411953}, } - for _, tc := range testCases { - pcg.state = tc.initialState - pcg.Advance(tc.delta) - if pcg.state != tc.expectedState { - t.Errorf("Advance(%d) = %d; expected %d", tc.delta, pcg.state, tc.expectedState) + for _, tt := range tests { + pcg.state = tt.state + pcg.Advance(tt.delta) + if pcg.state != tt.expected { + t.Errorf("Advance(%d) = %d; expected %d", tt.delta, pcg.state, tt.expected) } } } @@ -114,23 +114,23 @@ func TestPCG32_Advance(t *testing.T) { func TestPCG32_Retreat(t *testing.T) { pcg := NewPCG32() - testCases := []struct { + tests := []struct { initialState uint64 delta uint64 expectedState uint64 }{ - {3643462645497912072, 1, 1}, - {15256603694110904427, 10, 1}, - {5234694153321213237, 100, 1}, - {2323235076269450313, 1000, 1}, - {6143568259046921169, 10000, 1}, + {3643462645497912072, 1, 2297219049549015711}, + {15256603694110904427, 10, 9089490691196273925}, + {5234694153321213237, 100, 9614261562837832073}, + {2323235076269450313, 1000, 1018981208295873745}, + {6143568259046921169, 10000, 9666238883299984929}, } - for _, tc := range testCases { - pcg.state = tc.initialState - pcg.Retreat(tc.delta) - if pcg.state != tc.expectedState { - t.Errorf("Retreat(%d) = %d; expected %d", tc.delta, pcg.state, tc.expectedState) + for _, tt := range tests { + pcg.state = tt.initialState + pcg.Retreat(tt.delta) + if pcg.state != tt.expectedState { + t.Errorf("Retreat(%d) = %d; expected %d", tt.delta, pcg.state, tt.expectedState) } } } @@ -224,3 +224,106 @@ func isPermutation(a, b []int) bool { } return true } + +func TestPCG32_Read(t *testing.T) { + tests := []struct { + name string + seed uint64 + bufSize int + expected []byte + }{ + { + name: "Read 0 bytes", + seed: 9876543210, + bufSize: 0, + expected: []byte{}, + }, + { + name: "Read 1 byte", + seed: 42, + bufSize: 1, + expected: []byte{0xb0}, + }, + { + name: "Read 7 bytes", + seed: 42, + bufSize: 7, + expected: []byte{0xb0, 0xfd, 0xe1, 0x6a, 0xe8, 0x9, 0xe4}, + }, + { + name: "Read 16 bytes", + seed: 42, + bufSize: 16, + expected: []byte{ + 0xb0, 0xfd, 0xe1, 0x6a, + 0xe8, 0x9, 0xe4, 0x3d, + 0x50, 0x24, 0x2c, 0x70, + 0xf0, 0xae, 0x88, 0x77, + }, + }, + { + name: "Read 32 bytes", + seed: 1234567890, + bufSize: 32, + expected: []byte{ + 0x64, 0x44, 0xd9, 0x30, + 0x8, 0x8, 0x49, 0x6e, + 0x3f, 0xbd, 0xfe, 0xbf, + 0x30, 0x76, 0xe8, 0x56, + 0x45, 0xcd, 0xa1, 0xba, + 0xd4, 0xda, 0x3e, 0x62, + 0xa7, 0xa9, 0xf0, 0x9c, + 0x1b, 0x75, 0x2, 0xad, + }, + }, + { + name: "Read 64 bytes", + seed: 9876543210, + bufSize: 64, + expected: []byte{ + 0xf5, 0xde, 0x18, 0xa3, + 0x19, 0x9b, 0xbb, 0xd6, + 0xc7, 0x7c, 0x69, 0xb3, + 0xaa, 0x21, 0x15, 0x69, + 0xbc, 0xea, 0x78, 0x1d, + 0xce, 0x78, 0xf8, 0x46, + 0xae, 0x98, 0xe4, 0x20, + 0xe2, 0x16, 0x6, 0x6d, + 0x3e, 0x99, 0x96, 0x66, + 0xd, 0xfc, 0x14, 0x79, + 0x4e, 0x59, 0x42, 0x25, + 0x9, 0x39, 0x41, 0x95, + 0x11, 0x3c, 0xa1, 0xa9, + 0xd2, 0x2, 0x10, 0x51, + 0x6e, 0xaa, 0xe4, 0x25, + 0xa8, 0x45, 0xe0, 0x76, + }, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + p := NewPCG32().Seed(tc.seed, 0) + + buf := make([]byte, tc.bufSize) + + n, err := p.Read(buf) + if err != nil { + t.Fatalf("Read() error = %v; want nil", err) + } + + // assert.Equal(t, tc.bufSize, n) + // assert.Equal(t, tc.expected, buf) + if tc.bufSize != n { + t.Errorf("Read() = %d; expected %d", n, tc.bufSize) + } + + for i := 0; i < tc.bufSize; i++ { + if buf[i] != tc.expected[i] { + t.Errorf("Read() = %v; expected %v", buf, tc.expected) + break + } + } + }) + } +} diff --git a/examples/gno.land/p/demo/pcg/pcg64.gno b/examples/gno.land/p/demo/pcg/pcg64.gno index 314a28b8520..8b1c5a26d3d 100644 --- a/examples/gno.land/p/demo/pcg/pcg64.gno +++ b/examples/gno.land/p/demo/pcg/pcg64.gno @@ -1,18 +1,24 @@ package pcg import ( + "encoding/binary" "errors" "math" "math/bits" ) +const ( + inv52 = 1.0 / (1 << 52) + // inv64 = 1.0 / float64(math.MaxUint64) +) + // A PCG64 is a PCG64 generator with 128 bits of internal state. // A zero PCG64 is equivalent to one seeded with 0. type PCG64 struct { hi, lo *PCG32 } -// NewPCG64 returns a new PCG64 generator seeded with the given values. +// NewPCG64 returns a new PCG64 generator seeded with thr given values. // seed1 and seed2 are the initial state values for the generator. func NewPCG64(seed1, seed2 uint64) *PCG64 { return &PCG64{ @@ -56,41 +62,13 @@ func (p *PCG64) Uint64n(bound uint64) uint64 { } } -// Float64 returns a random float64 value in the range [0.0, 1.0). -// -// The function generates a 63-bit unsigned integer using Uint63() and then: -// 1. Shifts the generated number 11 bits to the right to obtain a 52-bit mantissa. -// 2. Multiplies the mantissa by 2^(-52) to scale it to the range [0.0, 1.0). -// -// This method produces a double-precision floating-point number with 52 bits of precision, -// which is the standard precision for float64 values. -// -// The time complexity of this function is O(1), as it performs a constant number of operations. -// The space complexity is O(1), as it uses only a constant amount of additional memory. -// -// Note: This method does not use all 64 bits of the generated number, which may result in -// a slight loss of precision compared to Float64Full(). However, it is faster and sufficient -// for most use cases. +// Float64 returns a random float64 in the range [0.0, 1.0). func (p *PCG64) Float64() float64 { - return float64(p.Uint63()>>11) * (1.0 / (1 << 52)) -} - -// Float64Full returns a random float64 value in the range [0.0, 1.0). -// -// The function generates a 64-bit unsigned integer using Uint64() and then: -// 1. Masks the generated number to obtain the lower 64 bits. -// 2. Divides the masked number by the maximum 64-bit unsigned integer value (math.MaxUint64) -// to scale it to the range [0.0, 1.0). -// -// This method utilizes the full 64 bits of the generated number, providing slightly higher -// precision compared to Float64(). However, it is slower due to the additional masking and -// division operations. -// -// The time complexity of this function is O(1), as it performs a constant number of operations. -// The space complexity is O(1), as it uses only a constant amount of additional memory. -// -// Note: While this method provides higher precision, it may not be necessary for most use cases. -// Float64() is generally faster and sufficient for generating random float64 values. + return float64(p.Uint63()>>11) * inv52 +} + +// Float64Full uses the full 64 bits of the generated number to produce a random float64. +// slightly more precise than Float64() but slower. func (p *PCG64) Float64Full() float64 { return float64(p.Uint64()&0xFFFFFFFFFFFFFF) / math.MaxUint64 } @@ -104,28 +82,13 @@ func (p *PCG64) Advance(delta uint64) *PCG64 { } // Retreat moves the PCG64 generator backward by `delta` steps. -// It updates the initial state of the generator. +// it updates the initial state of the generator. func (p *PCG64) Retreat(delta uint64) *PCG64 { safeDelta := ^uint64(0) - 1 p.Advance(safeDelta) return p } -// Shuffle shuffles the elements of a slice in place using the Fisher-Yates shuffle algorithm. -// It takes the following parameters: -// - n: The number of elements to shuffle. -// - swap: A function that swaps the elements at indices i and j. -// -// The function shuffles the elements by iterating from n-1 down to 1. In each iteration: -// 1. Generate a random index j between 0 and the current index i (inclusive) using Uint64n. -// 2. Call the `swap` function to swap the elements at indices i and j. -// -// This process ensures that each element has an equal probability of ending up at any position -// in the shuffled slice. -// -// The time complexity of this function is O(n), where n is the number of elements to shuffle. -// The space complexity is O(1), as it shuffles the elements in place and uses only a constant -// amount of additional memory. func (p *PCG64) Shuffle(n int, swap func(i, j int)) { // Fisher-Yates shuffle: https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle for i := n - 1; i > 0; i-- { @@ -134,7 +97,6 @@ func (p *PCG64) Shuffle(n int, swap func(i, j int)) { } } -// Perm returns a random permutation of the integers in the range [0, n). func (p *PCG64) Perm(n int) []int { res := make([]int, n) for i := range res { @@ -146,6 +108,78 @@ func (p *PCG64) Perm(n int) []int { return res } +// Read generates random bytes in the provided byte slice using the PCG64 random number generator. +// It employs loop unrolling to process 16 bytes at a time for performance enhancement. +func (p *PCG64) Read(buf []byte) (int, error) { + n := len(buf) + i := 0 + + // Loop unrolling: processing 16 bytes per iteration + for ; i <= n-16; i += 16 { + val1 := p.Uint64() + val2 := p.Uint64() + binary.LittleEndian.PutUint64(buf[i:], val1) + binary.LittleEndian.PutUint64(buf[i+8:], val2) + } + + // Handle any remaining bytes that were not processed in the main loop + if i < n { + remaining := buf[i:] + for j := 0; j < len(remaining); j += 8 { + if i+j < n { + val := p.Uint64() + // Only write the necessary bytes + for k := 0; k < 8 && (j+k) < len(remaining); k++ { + remaining[j+k] = byte(val >> (8 * k)) + } + } + } + } + + return n, nil +} + +func beUint64(b []byte) uint64 { + _ = b[7] + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +func bePutUint64(b []byte, v uint64) { + _ = b[7] + b[0] = byte(v >> 56) + b[1] = byte(v >> 48) + b[2] = byte(v >> 40) + b[3] = byte(v >> 32) + b[4] = byte(v >> 24) + b[5] = byte(v >> 16) + b[6] = byte(v >> 8) + b[7] = byte(v) +} + +// MarshalBinaryPCG64 serializes the state of the PCG64 generator to a binary format. +// It returns the serialized state as a byte slice. +func (p *PCG64) MarshalBinaryPCG64() ([]byte, error) { + b := make([]byte, 20) + copy(b, "pcg:") + bePutUint64(b[4:], p.hi.state) + bePutUint64(b[4+8:], p.lo.state) + return b, nil +} + +var errUnmarshalPCG = errors.New("invalid PCG encoding") + +// UnmarshalBinaryPCG64 deserializes the state of the PCG64 generator from a binary format. +// It takes the serialized state as a byte slice and updates the generator's state. +func (p *PCG64) UnmarshalBinary(b []byte) error { + if len(b) != 20 || string(b[:4]) != "pcg:" { + return errUnmarshalPCG + } + p.hi.state = beUint64(b[4:]) + p.lo.state = beUint64(b[4+8:]) + return nil +} + func (p *PCG64) next() (uint64, uint64) { const ( mulHi = 2549297995355413924 @@ -167,30 +201,8 @@ func (p *PCG64) next() (uint64, uint64) { return hi, lo } -// Uint64nWithMCG generates a pseudorandom 64-bit unsigned integer using the PCG64 algorithm -// with a Multiplicative Congruential Generator (MCG) as the output function. -// -// The function updates the internal state of the generator using the PCG64 transition function: -// state = state * multiplier + increment -// -// It then applies a series of bitwise operations to the updated state to produce the random number: -// 1. XOR the high 64 bits of the state with the high 64 bits right-shifted by 22 bits. -// 2. Multiply the result by a cheap multiplier (0xda942042e4dd58b5). -// 3. XOR the result with the result right-shifted by 48 bits. -// 4. Multiply the result by (low 64 bits of the state | 1). -// -// The resulting value is returned as the random 64-bit unsigned integer. -// -// The MCG output function used in this implementation is based on the one described in the PCG -// random number generator family, specifically the 128-bit MCG with 64-bit output. -// It provides good statistical properties and passes various randomness tests, including PractRand. -// -// The time complexity of this function is O(1), as it performs a constant number of bitwise operations. -// The space complexity is O(1), as it uses only a constant amount of additional memory. -// -// Note: The Uint64nWithMCG function is deterministic for a given state of the PCG64 generator. -// If you need different random numbers across multiple calls, make sure to advance the state of the -// generator using Advance() or reseed it using Seed() between calls. +// NextUInt64WithMCG generates a pseudorandom 64-bit unsigned integer using the PCG64 algorithm with Multiplier Congruential Generator (MCG). +// It updates the internal state of the generator and returns the generated value. func (p *PCG64) Uint64nWithMCG() uint64 { hi, lo := p.next() diff --git a/examples/gno.land/p/demo/pcg/pcg64_test.gno b/examples/gno.land/p/demo/pcg/pcg64_test.gno index 5bf57f4f95c..1b18985201c 100644 --- a/examples/gno.land/p/demo/pcg/pcg64_test.gno +++ b/examples/gno.land/p/demo/pcg/pcg64_test.gno @@ -1,9 +1,7 @@ package pcg import ( - "fmt" "math" - "math/rand" "testing" "time" ) @@ -25,19 +23,19 @@ func TestPCG_Uint63(t *testing.T) { func TestPCG_Advance(t *testing.T) { pcg := NewPCG64(12345, 67890) - testCases := []struct { + tests := []struct { delta uint64 expectedStateHi uint64 expectedStateLo uint64 }{ - {1, 16443432798917770532, 1294492316257287365}, - {10, 9073714748428748454, 9095006751169262415}, - {100, 1498360792142116778, 11040029025224029795}, - {1000, 7761321322648589714, 770061004744980459}, - {10000, 8930526547519973282, 18106490617456118331}, + {1, 5288318170876267920, 8586121761925336369}, + {10, 658778831176772942, 680070833917286903}, + {100, 11328633206433140426, 2423557365805501827}, + {1000, 14055077221971198434, 7063816904067589179}, + {10000, 3283529072340023762, 12459493142276168811}, } - for _, tc := range testCases { + for _, tc := range tests { pcg.Advance(tc.delta) if pcg.hi.state != tc.expectedStateHi { t.Errorf("Advance(%d) hi state = %d; expected %d", tc.delta, pcg.hi.state, tc.expectedStateHi) @@ -51,26 +49,26 @@ func TestPCG_Advance(t *testing.T) { func TestPCG(t *testing.T) { p := NewPCG64(1, 2) want := []uint64{ - 0x52addb9b0d4aa107, - 0xc5d5c81b8c97ff8f, - 0xcfa82191c9a86caa, - 0x76b48e618586fdfe, - 0x765ac4ba3e566855, - 0x1d6058a5dd7ab27, - 0x2b913f2f76e81329, - 0x74873f4e5348d32e, - 0xc4c940eb70248174, - 0xb5a1651a6627a924, - 0xc34174eb7f136d0a, - 0xe612b37df73df71c, - 0x884a2539ea7aa198, - 0x2976010a57986e59, - 0x1d0d522531d62a7d, - 0xa7da1ad05db25a75, - 0xdbee2df7bd6428be, - 0x598c54d1eb4abdd7, - 0x559ca964532a3777, - 0x6e64af73ece533b0, + 0x6a3b45f887fad67c, + 0x1b0a91e2d7d75723, + 0x905a1518e26e8445, + 0x8cb6b7c0ea9f200c, + 0x59afa674b44b2509, + 0x86ab5e04d104bd4c, + 0x13e180669e2d07ea, + 0x5a6a0bb349dd26ae, + 0x1cd5f134a8581b57, + 0xc807c2686fe5baff, + 0x846b5fc66eb343cb, + 0x169adcefdd042f97, + 0x9b58ba4c9ef301fa, + 0xd54fc77c467d80bf, + 0xb776b6e2508ebab3, + 0xfd002241e862137b, + 0xc6a0fa2cfa1f95e9, + 0x67469082d8377e0a, + 0x7f30faccba8029d4, + 0xf200f5696d060925, } for i, x := range want { @@ -84,30 +82,30 @@ func TestPCG(t *testing.T) { func TestPCG_Retreat(t *testing.T) { pcg := NewPCG64(12345, 67890) - testCases := []struct { + tests := []struct { delta uint64 expectedStateHi uint64 expectedStateLo uint64 }{ - {1, 7265056988599925051, 16912344864586758584}, - {10, 15578097273240930873, 13711579158205810606}, - {100, 3761525201756208775, 6157393363865312820}, - {1000, 15336446625969592741, 13630190462364618442}, - {10000, 10106684222517973779, 4620269966716251888}, + {1, 9562276038148940761, 762819840426222678}, + {10, 6073181909808593307, 4206663794773473040}, + {100, 13320423970107457037, 15716292132216561082}, + {1000, 2035794469121007023, 329538305516032724}, + {10000, 8361027235883110657, 2874612980081388766}, } - for _, tc := range testCases { - pcg.Retreat(tc.delta) - if pcg.hi.state != tc.expectedStateHi { - t.Errorf("Retreat(%d) hi state = %d; expected %d", tc.delta, pcg.hi.state, tc.expectedStateHi) + for _, tt := range tests { + pcg.Retreat(tt.delta) + if pcg.hi.state != tt.expectedStateHi { + t.Errorf("Retreat(%d) hi state = %d; expected %d", tt.delta, pcg.hi.state, tt.expectedStateHi) } - if pcg.lo.state != tc.expectedStateLo { - t.Errorf("Retreat(%d) lo state = %d; expected %d", tc.delta, pcg.lo.state, tc.expectedStateLo) + if pcg.lo.state != tt.expectedStateLo { + t.Errorf("Retreat(%d) lo state = %d; expected %d", tt.delta, pcg.lo.state, tt.expectedStateLo) } } } -func TestPCG64_Shuffle(t *testing.T) { +func Test_ExamplePCG64_Shuffle(t *testing.T) { pcg := NewPCG64(42, 54) pcg.Seed(42, 54, 18, 27) @@ -130,7 +128,7 @@ func TestFloat64(t *testing.T) { for i := 0; i < 1000; i++ { val := pcg.Float64() if val < 0.0 || val > 1.0 { - t.Error("Float64() returned a value out of bounds") + t.Errorf("Float64() returned a value out of bounds: %f", val) } } } @@ -140,7 +138,61 @@ func TestFloat64Full(t *testing.T) { for i := 0; i < 1000; i++ { val := pcg.Float64Full() if val < 0.0 || val >= 1.0 { - t.Error("Float64Full() returned a value out of bounds") + t.Errorf("Float64Full() returned a value out of bounds: %f", val) + } + } +} + +func TestPCG64Read(t *testing.T) { + now := uint64(time.Now().UnixNano()) + testSizes := []int{16, 32, 48, 64, 100, 1023, 2048} + for _, size := range testSizes { + buf := make([]byte, size) + pcg := NewPCG64(12345, now) + n, err := pcg.Read(buf) + if err != nil { + t.Errorf("Read returned an error: %v", err) + } + if n != size { + t.Errorf("Read returned wrong number of bytes: got %v, want %v", n, size) + } + // Check if bytes are not all zero; this is a simplistic randomness check + allZero := true + for _, b := range buf { + if b != 0 { + allZero = false + break + } + } + if allZero { + t.Errorf("Buffer of size %d is filled with zeros, which is highly improbable", size) + } + } +} + +func TestPCG64ReadEdgeCases(t *testing.T) { + now := uint64(time.Now().UnixNano()) + edgeSizes := []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 15, 17} + for _, size := range edgeSizes { + buf := make([]byte, size) + pcg := NewPCG64(12345, now) + n, err := pcg.Read(buf) + if err != nil { + t.Errorf("Read returned an error: %v", err) + } + if n != size { + t.Errorf("Read returned wrong number of bytes: got %v, want %v", n, size) + } + + nonZeroFound := false + for _, b := range buf { + if b != 0 { + nonZeroFound = true + break + } + } + if !nonZeroFound && size != 0 { + t.Errorf("Buffer of size %d has no non-zero bytes, which is highly improbable", size) } } }