diff --git a/.chplcheckignore b/.chplcheckignore index 8653e1056d7..3929ed8da8e 100644 --- a/.chplcheckignore +++ b/.chplcheckignore @@ -83,6 +83,7 @@ SipHash.chpl SortMsg.chpl SparseMatrix.chpl SparseMatrixMsg.chpl +SplitMix64RNG.chpl Stats.chpl StatsMsg.chpl StatusMsg.chpl diff --git a/.configs/MultiDimTestServerModules.cfg b/.configs/MultiDimTestServerModules.cfg index ea7b35a349b..2775c76f2d7 100644 --- a/.configs/MultiDimTestServerModules.cfg +++ b/.configs/MultiDimTestServerModules.cfg @@ -38,6 +38,7 @@ SequenceMsg SetMsg SortMsg #SparseMatrixMsg +SplitMix64RNG StatsMsg #TimeClassMsg TransferMsg diff --git a/ServerModules.cfg b/ServerModules.cfg index 3ba2a8ac64e..794609c38ea 100644 --- a/ServerModules.cfg +++ b/ServerModules.cfg @@ -38,6 +38,7 @@ SequenceMsg SetMsg SortMsg SparseMatrixMsg +SplitMix64RNG StatsMsg TimeClassMsg TransferMsg diff --git a/arkouda/numpy/random/generator.py b/arkouda/numpy/random/generator.py index 3b207af2f94..ebf906e275a 100644 --- a/arkouda/numpy/random/generator.py +++ b/arkouda/numpy/random/generator.py @@ -1,6 +1,8 @@ import numpy as np import numpy.random as np_random +from typeguard import typechecked + from arkouda.client import get_registration_config from arkouda.numpy.dtypes import ( _val_isinstance_of_union, @@ -8,6 +10,7 @@ float_scalars, int_scalars, numeric_scalars, + uint64, ) from arkouda.numpy.dtypes import dtype as akdtype from arkouda.numpy.dtypes import dtype as to_numpy_dtype @@ -1058,6 +1061,196 @@ def uniform(self, low=0.0, high=1.0, size=None): self._state += full_size return create_pdarray(rep_msg) + @typechecked + def _fill_with_stateless_u64_from_index( + self, x: pdarray, seed: int_scalars, stream: int_scalars, start_idx: int_scalars = 0 + ) -> None: + """ + Fill an existing uint64 pdarray with stateless, index-based random values. + + Random values are generated as a deterministic function of + ``(seed, stream, global_index)`` and written in-place to ``x``. + No mutable RNG state is used, and results are invariant to + parallelism, chunking, and execution order. + + Parameters + ---------- + x : pdarray + Destination array of dtype uint64 to be filled in-place. + Currently only 1-dimensional arrays are supported. + seed : int_scalars + Seed selecting the deterministic randomness universe. Converted + internally to uint64. + stream : int_scalars + Stream identifier used to generate independent random streams + from the same seed. Converted internally to uint64. + start_idx : int_scalars, optional + Global index offset corresponding to ``x[0]``. Converted internally + to uint64. Default is 0. + + Notes + ----- + This method is a low-level primitive intended for internal use. + It dispatches to a server-side kernel that generates values + independently for each global index. + """ + from arkouda.client import generic_msg + + if x.ndim != 1: + raise ValueError("Only 1 dimensional arrays supported.") + + # Normalize all scalar parameters to uint64 at the API boundary + seed_u64 = uint64(seed) + stream_u64 = uint64(stream) + start_idx_u64 = uint64(start_idx) + + generic_msg( + cmd="fillRandU64", + args={ + "A": x, + "seed": seed_u64, + "stream": stream_u64, + "startIdx": start_idx_u64, + }, + ) + + @typechecked + def _stateless_u64_from_index( + self, n: int, seed: int_scalars, stream: int_scalars, start_idx: int_scalars = 0 + ) -> pdarray: + """ + Create a uint64 pdarray of stateless, index-based random values. + + The returned array satisfies:: + + out[i] = f(seed, stream, start_idx + i) + + where ``f`` is a deterministic mixing function. Results are + reproducible across runs and invariant to distributed execution. + + Parameters + ---------- + n : int + Number of elements to generate. + seed : int_scalars + Seed selecting the deterministic randomness universe. Converted + internally to uint64. + stream : int_scalars + Stream identifier used to generate independent random streams + from the same seed. Converted internally to uint64. + start_idx : int_scalars, optional + Global index offset corresponding to the first element. + Converted internally to uint64. Default is 0. + + Returns + ------- + pdarray + A uint64 pdarray of length ``n`` containing stateless random values. + + Notes + ----- + This is a fundamental building block for sampling, shuffling, + sketching, and other randomized algorithms. + """ + from arkouda.numpy.pdarraycreation import zeros as ak_zeros + + x = ak_zeros(n, dtype=uint64) + self._fill_with_stateless_u64_from_index(x, seed, stream, start_idx) + return x + + @typechecked + def _fill_stateless_uniform_01_from_index( + self, + x: pdarray, + seed: int_scalars, + stream: int_scalars, + start_idx: int_scalars = 0, + ) -> None: + """ + Fill an existing float64 pdarray with stateless uniform random values in [0, 1). + + Each element is generated deterministically from + ``(seed, stream, global_index)`` and mapped to a floating-point + value uniformly distributed on [0, 1). + + Parameters + ---------- + x : pdarray + Destination array of dtype float64 to be filled in-place. + Currently only 1-dimensional arrays are supported. + seed : int_scalars + Seed selecting the deterministic randomness universe. Converted + internally to uint64. + stream : int_scalars + Stream identifier used to generate independent random streams + from the same seed. Converted internally to uint64. + start_idx : int_scalars, optional + Global index offset corresponding to ``x[0]``. + Converted internally to uint64. Default is 0. + + Notes + ----- + This method performs all computation server-side and guarantees + reproducibility independent of locale count or execution order. + """ + from arkouda.client import generic_msg + + if x.ndim != 1: + raise ValueError("Only 1 dimensional arrays supported.") + + seed_u64 = uint64(seed) + stream_u64 = uint64(stream) + start_idx_u64 = uint64(start_idx) + + generic_msg( + cmd="fillUniform01", # <-- MUST be the float64-uniform kernel + args={"A": x, "seed": seed_u64, "stream": stream_u64, "startIdx": start_idx_u64}, + ) + + @typechecked + def _stateless_uniform_01_from_index( + self, n: int, seed: int_scalars, stream: int_scalars, start_idx: int_scalars = 0 + ) -> pdarray: + """ + Create a float64 pdarray of stateless uniform random values in [0, 1). + + Random values are a deterministic function of + ``(seed, stream, start_idx + i)`` for each element ``i``. + Results are reproducible across runs and invariant to + distributed execution details. + + Parameters + ---------- + n : int + Number of elements to generate. + seed : int_scalars + Seed selecting the deterministic randomness universe. Converted + internally to uint64. + stream : int_scalars + Stream identifier used to generate independent random streams + from the same seed. Converted internally to uint64. + start_idx : int_scalars, optional + Global index offset corresponding to the first element. + Converted internally to uint64. Default is 0. + + Returns + ------- + pdarray + A float64 pdarray of length ``n`` with values in the half-open + interval [0, 1). + + Notes + ----- + This method is equivalent to generating a stateless uint64 sequence + and mapping it to floating-point values, but performs the operation + efficiently in a single server-side pass. + """ + from arkouda.numpy.pdarraycreation import zeros as ak_zeros + + x = ak_zeros(n, dtype=akfloat64) + self._fill_stateless_uniform_01_from_index(x, seed, stream, start_idx) + return x + _supported_chapel_types = frozenset(("int", "int(64)", "uint", "uint(64)", "real", "real(64)", "bool")) diff --git a/pytest.ini b/pytest.ini index c84c8191e1c..8403eb1a9c1 100644 --- a/pytest.ini +++ b/pytest.ini @@ -59,6 +59,7 @@ testpaths = tests/numpy/pdarray_creation_test.py tests/numpy/pdarraymanipulation_tests.py tests/numpy/random_test.py + tests/numpy/random_stateless_rng_test.py tests/numpy/segarray_test.py tests/numpy/setops_test.py tests/numpy/sort_test.py diff --git a/src/SplitMix64RNG.chpl b/src/SplitMix64RNG.chpl new file mode 100644 index 00000000000..5e334f1a09c --- /dev/null +++ b/src/SplitMix64RNG.chpl @@ -0,0 +1,63 @@ +// SplitMix64: fast 64-bit mixer suitable for stateless RNG / hashing. +// Works by design with overflow modulo 2^64, so we use uint(64) everywhere. + +module SplitMix64RNG { + // Constants from the canonical SplitMix64 reference. + private param GAMMA: uint(64) = 0x9E3779B97F4A7C15:uint(64); + private param C1: uint(64) = 0xBF58476D1CE4E5B9:uint(64); + private param C2: uint(64) = 0x94D049BB133111EB:uint(64); + + // A single SplitMix64 mix step. + // Given an input x, returns a well-scrambled 64-bit value. + inline proc splitmix64(xIn: uint(64)): uint(64) { + var x = xIn + GAMMA; + x = (x ^ (x >> 30)) * C1; + x = (x ^ (x >> 27)) * C2; + x = x ^ (x >> 31); + return x; + } + + // Derive a per-stream key from seed and stream id. + // This makes streams "independent enough" for analytics/sampling/shuffle keys. + inline proc streamKey(seed: uint(64), stream: uint(64)): uint(64) { + // Any deterministic combination is fine; we mix it to avoid structure. + return splitmix64(seed ^ (stream * 0xD2B74407B1CE6E93:uint(64))); + } + + // Stateless uint64 RNG at a global index i: + // r[i] = splitmix64(i ^ key(seed, stream)) + inline proc randU64At(i: uint(64), seed: uint(64), stream: uint(64) = 0:uint(64)): uint(64) { + const k = streamKey(seed, stream); + return splitmix64(i ^ k); + } + + // Convert uint64 to a uniform float64 in [0,1) using top 53 bits (exact grid). + inline proc u64ToUniform01(r: uint(64)): real(64) { + const top53 = r >> 11; // keep 53 MSBs + return top53:real(64) * 0x1.0p-53; // 2^-53 + } + + // Fill an array with uint64 randomness based on *global* indices. + // startIdx is the global index of A.domain.low (for Arkouda you'd pass the locale's global offset). + @arkouda.registerCommand + proc fillRandU64(ref A: [?d] ?t, seed: uint, stream: uint = 0: uint, startIdx: uint = 0:uint) + where (t == uint(64)) && (d.rank == 1) { + const k = streamKey(seed, stream); + forall idx in d with (ref A){ + // idx is the domain index type; cast to uint(64) for a stable global counter + const i = startIdx + idx:uint(64); + A[idx] = splitmix64(i ^ k); + } + } + + // Fill an array with uniform float64 in [0,1). + @arkouda.registerCommand + proc fillUniform01(ref A: [?d] ?t, seed: uint, stream: uint = 0:uint, startIdx: uint = 0:uint) throws + where (t == real) && (d.rank == 1){ + const k = streamKey(seed, stream); + forall idx in d with (ref A){ + const i = startIdx + idx:uint(64); + A[idx] = u64ToUniform01(splitmix64(i ^ k)); + } + } +} diff --git a/tests/numpy/random_stateless_rng_test.py b/tests/numpy/random_stateless_rng_test.py new file mode 100644 index 00000000000..c611f29a851 --- /dev/null +++ b/tests/numpy/random_stateless_rng_test.py @@ -0,0 +1,302 @@ +import math + +import numpy as np +import pytest + +from scipy import stats as sp_stats + +import arkouda as ak + +from arkouda.numpy.dtypes import float64, uint64 +from arkouda.numpy.pdarraycreation import zeros as ak_zeros + + +@pytest.mark.requires_chapel_module("SplitMix64RNG") +class TestStatelessRNG: + def test_stateless_u64_same_params_reproducible(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 123 + stream = 0 + start_idx = 0 + + a = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + b = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert a.dtype == uint64 + assert b.dtype == uint64 + assert ak.all(a == b) + + def test_stateless_u64_stream_changes_sequence(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 123 + start_idx = 0 + + a = rng._stateless_u64_from_index(n, seed=seed, stream=0, start_idx=start_idx) + b = rng._stateless_u64_from_index(n, seed=seed, stream=1, start_idx=start_idx) + + # They should not be identical elementwise + assert ak.any(a != b) + + def test_stateless_u64_start_idx_matches_slice(self): + rng = ak.random.default_rng() + n_total = 20_000 + seed = 999 + stream = 7 + + full = rng._stateless_u64_from_index(n_total, seed=seed, stream=stream, start_idx=0) + + start_idx = 5_000 + n = 3_000 + sub = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert ak.all(sub == full[start_idx : start_idx + n]) + + def test_fill_with_stateless_u64_inplace_matches_allocate(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 42 + stream = 3 + start_idx = 1234 + + x = ak_zeros(n, dtype=uint64) + rng._fill_with_stateless_u64_from_index(x, seed=seed, stream=stream, start_idx=start_idx) + + y = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert x.dtype == uint64 + assert y.dtype == uint64 + assert ak.all(x == y) + + def test_stateless_uniform01_dtype_and_range(self): + rng = ak.random.default_rng() + n = 50_000 + seed = 1 + stream = 0 + start_idx = 0 + + u = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert u.dtype == float64 + assert ak.all(u >= 0.0) + assert ak.all(u < 1.0) + + def test_stateless_uniform01_same_params_reproducible(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 321 + stream = 2 + start_idx = 99 + + a = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + b = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert a.dtype == float64 + assert ak.all(a == b) + + def test_stateless_uniform01_start_idx_matches_slice(self): + rng = ak.random.default_rng() + n_total = 30_000 + seed = 2026 + stream = 11 + + full = rng._stateless_uniform_01_from_index(n_total, seed=seed, stream=stream, start_idx=0) + + start_idx = 7_000 + n = 4_000 + sub = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert ak.all(sub == full[start_idx : start_idx + n]) + + def test_fill_uniform01_inplace_matches_allocate(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 8 + stream = 9 + start_idx = 10 + + x = ak_zeros(n, dtype=float64) + rng._fill_stateless_uniform_01_from_index(x, seed=seed, stream=stream, start_idx=start_idx) + + y = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + + assert x.dtype == float64 + assert ak.all(x == y) + + def test_stateless_uniform01_stream_changes_sequence(self): + rng = ak.random.default_rng() + n = 10_000 + seed = 1234 + start_idx = 0 + + a = rng._stateless_uniform_01_from_index(n, seed=seed, stream=0, start_idx=start_idx) + b = rng._stateless_uniform_01_from_index(n, seed=seed, stream=1, start_idx=start_idx) + + assert ak.any(a != b) + + # ----------------------------- + # Helpers (non-flaky thresholds) + # ----------------------------- + @staticmethod + def _six_sigma_half(n: int) -> float: + """6-sigma bound for deviation from 0.5 for a Bernoulli(0.5) mean.""" + return 6.0 * math.sqrt(0.25 / n) + + @staticmethod + def _corr_bound(n: int) -> float: + """Loose ~6/sqrt(n) correlation bound.""" + return 6.0 / math.sqrt(n) + + # ----------------------------- + # Uniform[0,1) statistical tests + # ----------------------------- + def test_stateless_uniform01_ks_test(self): + rng = ak.random.default_rng() + n = 80_000 + seed, stream, start_idx = 123, 0, 0 + + u = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + u_np = u.to_ndarray() + + # KS test against continuous U[0, 1) + stat = sp_stats.kstest(u_np, "uniform") + # Keep alpha conservative to avoid flakes; fixed seed makes this stable. + assert stat.pvalue > 1e-4, stat + + def test_stateless_uniform01_chisquare_binned(self): + rng = ak.random.default_rng() + n = 200_000 + k = 256 + seed, stream, start_idx = 456, 2, 10 + + u = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + u_np = u.to_ndarray() + + # Bin into k equal-width buckets on [0,1) + bins = np.floor(u_np * k).astype(np.int64) + # Safety: u in [0,1), so bins in [0,k-1] + assert bins.min() >= 0 + assert bins.max() < k + + counts = np.bincount(bins, minlength=k) + chisq = sp_stats.chisquare(counts) + + assert chisq.pvalue > 1e-4, chisq + + def test_stateless_uniform01_no_pathological_endpoints(self): + rng = ak.random.default_rng() + n = 200_000 + seed, stream, start_idx = 999, 7, 0 + + u = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + u_np = u.to_ndarray() + + # Should never hit 1.0 due to top-53-bit mapping + assert not np.any(u_np == 1.0) + # Exact 0.0 is possible but astronomically rare; with this n it should be 0. + assert np.sum(u_np == 0.0) == 0 + + # ----------------------------- + # uint64 bit-level sanity tests + # ----------------------------- + def test_stateless_u64_monobit_selected_bits(self): + rng = ak.random.default_rng() + n = 250_000 + seed, stream, start_idx = 42, 0, 0 + + r = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + r_np = r.to_ndarray().astype(np.uint64, copy=False) + + # Check a few low and high bits + bits_to_check = list(range(0, 8)) + list(range(56, 64)) + tol = self._six_sigma_half(n) + + for b in bits_to_check: + ones = ((r_np >> np.uint64(b)) & np.uint64(1)).mean() + assert abs(ones - 0.5) < tol, (b, ones, tol) + + def test_stateless_u64_low_byte_histogram_chisquare(self): + rng = ak.random.default_rng() + n = 300_000 + seed, stream, start_idx = 123, 3, 1000 + + r = rng._stateless_u64_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + r_np = r.to_ndarray().astype(np.uint64, copy=False) + + low_byte = (r_np & np.uint64(0xFF)).astype(np.int64, copy=False) + counts = np.bincount(low_byte, minlength=256) + + chisq = sp_stats.chisquare(counts) + assert chisq.pvalue > 1e-4, chisq + + # ----------------------------- + # Independence / correlation + # ----------------------------- + def test_stateless_uniform01_lag1_autocorrelation_small(self): + rng = ak.random.default_rng() + n = 120_000 + seed, stream, start_idx = 2026, 0, 0 + + u = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=start_idx) + u_np = u.to_ndarray() + + # Lag-1 correlation + x = u_np[:-1] + y = u_np[1:] + corr = np.corrcoef(x, y)[0, 1] + + assert abs(corr) < self._corr_bound(len(x)), corr + + def test_stateless_uniform01_cross_stream_correlation_small(self): + rng = ak.random.default_rng() + n = 120_000 + seed, start_idx = 777, 0 + + u0 = rng._stateless_uniform_01_from_index( + n, seed=seed, stream=0, start_idx=start_idx + ).to_ndarray() + u1 = rng._stateless_uniform_01_from_index( + n, seed=seed, stream=1, start_idx=start_idx + ).to_ndarray() + + corr = np.corrcoef(u0, u1)[0, 1] + assert abs(corr) < self._corr_bound(n), corr + + def test_stateless_uniform01_startidx_two_sample_ks(self): + rng = ak.random.default_rng() + n = 80_000 + seed, stream = 31415, 9 + + u0 = rng._stateless_uniform_01_from_index(n, seed=seed, stream=stream, start_idx=0).to_ndarray() + u_l = rng._stateless_uniform_01_from_index( + n, seed=seed, stream=stream, start_idx=10_000_000 + ).to_ndarray() + + # Same distribution despite different index offsets + stat = sp_stats.ks_2samp(u0, u_l) + assert stat.pvalue > 1e-4, stat + + # ----------------------------- + # Practical "shuffle key" sanity + # ----------------------------- + def test_stateless_u64_shuffle_key_not_index_biased(self): + rng = ak.random.default_rng() + n = 200_000 + seed, stream, start_idx = 9999, 0, 0 + + keys = rng._stateless_u64_from_index( + n, seed=seed, stream=stream, start_idx=start_idx + ).to_ndarray() + # Shuffle-by-keys => take indices of smallest fraction of keys + frac = 0.01 + m = int(n * frac) + + idx = np.argpartition(keys, m)[:m] + + # If keys are “random enough”, selected indices should be ~uniform over 10 deciles + dec = (idx * 10) // n + counts = np.bincount(dec, minlength=10) + + chisq = sp_stats.chisquare(counts) + assert chisq.pvalue > 1e-4, (chisq, counts) diff --git a/tests/numpy/random_test.py b/tests/numpy/random_test.py index 9d042f73e9b..3efdc87e021 100644 --- a/tests/numpy/random_test.py +++ b/tests/numpy/random_test.py @@ -624,22 +624,22 @@ def test_legacy_rand3D(self, size): assert_almost_equivalent(known, given) def test_legacy_randint(self): - testArray = ak.random.randint(0, 10, 5) - assert isinstance(testArray, ak.pdarray) - assert 5 == len(testArray) - assert ak.int64 == testArray.dtype + test_array = ak.random.randint(0, 10, 5) + assert isinstance(test_array, ak.pdarray) + assert 5 == len(test_array) + assert ak.int64 == test_array.dtype - testArray = ak.random.randint(np.int64(0), np.int64(10), np.int64(5)) - assert isinstance(testArray, ak.pdarray) - assert 5 == len(testArray) - assert ak.int64 == testArray.dtype + test_array = ak.random.randint(np.int64(0), np.int64(10), np.int64(5)) + assert isinstance(test_array, ak.pdarray) + assert 5 == len(test_array) + assert ak.int64 == test_array.dtype - testArray = ak.random.randint(np.float64(0), np.float64(10), np.int64(5)) - assert isinstance(testArray, ak.pdarray) - assert 5 == len(testArray) - assert ak.int64 == testArray.dtype + test_array = ak.random.randint(np.float64(0), np.float64(10), np.int64(5)) + assert isinstance(test_array, ak.pdarray) + assert 5 == len(test_array) + assert ak.int64 == test_array.dtype - test_ndarray = testArray.to_ndarray() + test_ndarray = test_array.to_ndarray() for value in test_ndarray: assert 0 <= value <= 10 @@ -752,28 +752,30 @@ def test_legacy_randint_with_seed(self): ak.random.randint(np.uint8(1), np.uint32(5), np.uint16(10), seed=np.uint8(2)) def test_legacy_uniform(self): - testArray = ak.random.uniform(3) - assert isinstance(testArray, ak.pdarray) - assert 3 == len(testArray) - assert ak.float64 == testArray.dtype + test_array = ak.random.uniform(3) + assert isinstance(test_array, ak.pdarray) + assert 3 == len(test_array) + assert ak.float64 == test_array.dtype - testArray = ak.random.uniform(np.int64(3)) - assert isinstance(testArray, ak.pdarray) - assert 3 == len(testArray) - assert ak.float64 == testArray.dtype + test_array = ak.random.uniform(np.int64(3)) + assert isinstance(test_array, ak.pdarray) + assert 3 == len(test_array) + assert ak.float64 == test_array.dtype # The next two tests also retain the non pytest.seed, because they assert specific values. - uArray = ak.random.uniform(size=3, low=0, high=5, seed=0) + u_array = ak.random.uniform(size=3, low=0, high=5, seed=0) assert np.allclose( [0.30013431967121934, 0.47383036230759112, 1.0441791878997098], - uArray.tolist(), + u_array.tolist(), ) - uArray = ak.random.uniform(size=np.int64(3), low=np.int64(0), high=np.int64(5), seed=np.int64(0)) + u_array = ak.random.uniform( + size=np.int64(3), low=np.int64(0), high=np.int64(5), seed=np.int64(0) + ) assert np.allclose( [0.30013431967121934, 0.47383036230759112, 1.0441791878997098], - uArray.tolist(), + u_array.tolist(), ) with pytest.raises(TypeError):