diff --git a/NEWS.md b/NEWS.md index 824d730ed3c3c..39ddd762bd054 100644 --- a/NEWS.md +++ b/NEWS.md @@ -51,6 +51,8 @@ Standard library changes #### Random +* When seeding RNGs provided by `Random`, negative integer seeds can now be used ([#51416]). + #### REPL * Tab complete hints now show in lighter text while typing in the repl. To disable diff --git a/stdlib/Random/src/RNGs.jl b/stdlib/Random/src/RNGs.jl index 272b46f9774d9..843386d5450f3 100644 --- a/stdlib/Random/src/RNGs.jl +++ b/stdlib/Random/src/RNGs.jl @@ -83,12 +83,12 @@ MersenneTwister(seed::Vector{UInt32}, state::DSFMT_state) = Create a `MersenneTwister` RNG object. Different RNG objects can have their own seeds, which may be useful for generating different streams of random numbers. -The `seed` may be a non-negative integer or a vector of -`UInt32` integers. If no seed is provided, a randomly generated one -is created (using entropy from the system). -See the [`seed!`](@ref) function for reseeding an already existing -`MersenneTwister` object. +The `seed` may be an integer or a vector of `UInt32` integers. +If no seed is provided, a randomly generated one is created (using entropy from the system). +See the [`seed!`](@ref) function for reseeding an already existing `MersenneTwister` object. +!!! compat "Julia 1.11" + Passing a negative integer seed requires at least Julia 1.11. # Examples ```jldoctest @@ -290,20 +290,48 @@ function make_seed() end end +""" + make_seed(n::Integer) -> Vector{UInt32} + +Transform `n` into a bit pattern encoded as a `Vector{UInt32}`, suitable for +RNG seeding routines. + +`make_seed` is "injective" : if `n != m`, then `make_seed(n) != `make_seed(m)`. +Moreover, if `n == m`, then `make_seed(n) == make_seed(m)`. + +This is an internal function, subject to change. +""" function make_seed(n::Integer) - n < 0 && throw(DomainError(n, "`n` must be non-negative.")) + neg = signbit(n) + if neg + n = ~n + end + @assert n >= 0 seed = UInt32[] - while true + # we directly encode the bit pattern of `n` into the resulting vector `seed`; + # to greatly limit breaking the streams of random numbers, we encode the sign bit + # as the upper bit of `seed[end]` (i.e. for most positive seeds, `make_seed` returns + # the same vector as when we didn't encode the sign bit) + while !iszero(n) push!(seed, n & 0xffffffff) - n >>= 32 - if n == 0 - return seed - end + n >>>= 32 + end + if isempty(seed) || !iszero(seed[end] & 0x80000000) + push!(seed, zero(UInt32)) end + if neg + seed[end] |= 0x80000000 + end + seed end # inverse of make_seed(::Integer) -from_seed(a::Vector{UInt32})::BigInt = sum(a[i] * big(2)^(32*(i-1)) for i in 1:length(a)) +function from_seed(a::Vector{UInt32})::BigInt + neg = !iszero(a[end] & 0x80000000) + seed = sum((i == length(a) ? a[i] & 0x7fffffff : a[i]) * big(2)^(32*(i-1)) + for i in 1:length(a)) + neg ? ~seed : seed +end #### seed!() diff --git a/stdlib/Random/src/Random.jl b/stdlib/Random/src/Random.jl index 78d4f15e2beac..15165f5380945 100644 --- a/stdlib/Random/src/Random.jl +++ b/stdlib/Random/src/Random.jl @@ -394,6 +394,8 @@ sequence of numbers if and only if a `seed` is provided. Some RNGs don't accept a seed, like `RandomDevice`. After the call to `seed!`, `rng` is equivalent to a newly created object initialized with the same seed. +The types of accepted seeds depend on the type of `rng`, but in general, +integer seeds should work. If `rng` is not specified, it defaults to seeding the state of the shared task-local generator. diff --git a/stdlib/Random/src/Xoshiro.jl b/stdlib/Random/src/Xoshiro.jl index 5a03e2b84536d..feb26da3101ac 100644 --- a/stdlib/Random/src/Xoshiro.jl +++ b/stdlib/Random/src/Xoshiro.jl @@ -4,7 +4,7 @@ # Lots of implementation is shared with TaskLocalRNG """ - Xoshiro(seed) + Xoshiro(seed::Integer) Xoshiro() Xoshiro256++ is a fast pseudorandom number generator described by David Blackman and @@ -21,6 +21,12 @@ multiple interleaved xoshiro instances). The virtual PRNGs are discarded once the bulk request has been serviced (and should cause no heap allocations). +If no seed is provided, a randomly generated one is created (using entropy from the system). +See the [`seed!`](@ref) function for reseeding an already existing `Xoshiro` object. + +!!! compat "Julia 1.11" + Passing a negative integer seed requires at least Julia 1.11. + # Examples ```jldoctest julia> using Random @@ -191,6 +197,12 @@ endianness and possibly word size. Using or seeding the RNG of any other task than the one returned by `current_task()` is undefined behavior: it will work most of the time, and may sometimes fail silently. + +When seeding `TaskLocalRNG()` with [`seed!`](@ref), the passed seed, if any, +may be any integer. + +!!! compat "Julia 1.11" + Seeding `TaskLocalRNG()` with a negative integer seed requires at least Julia 1.11. """ struct TaskLocalRNG <: AbstractRNG end TaskLocalRNG(::Nothing) = TaskLocalRNG() diff --git a/stdlib/Random/test/runtests.jl b/stdlib/Random/test/runtests.jl index 462a9f86cfecf..836d7aaa68ab9 100644 --- a/stdlib/Random/test/runtests.jl +++ b/stdlib/Random/test/runtests.jl @@ -926,6 +926,12 @@ end m = MersenneTwister(0); rand(m, Int64); rand(m) @test string(m) == "MersenneTwister(0, (0, 2256, 1254, 1, 0, 1))" @test m == MersenneTwister(0, (0, 2256, 1254, 1, 0, 1)) + + # negative seeds + Random.seed!(m, -3) + @test string(m) == "MersenneTwister(-3)" + Random.seed!(m, typemin(Int8)) + @test string(m) == "MersenneTwister(-128)" end @testset "RandomDevice" begin @@ -1148,3 +1154,33 @@ end end end end + +@testset "seed! and make_seed" begin + # Test that: + # 1) if n == m, then make_seed(n) == make_seed(m) + # 2) if n != m, then make_seed(n) != make_seed(m) + rngs = (Xoshiro(0), TaskLocalRNG(), MersenneTwister(0)) + seeds = Any[] + for T = Base.BitInteger_types + append!(seeds, rand(T, 8)) + push!(seeds, typemin(T), typemin(T) + T(1), typemin(T) + T(2), + typemax(T), typemax(T) - T(1), typemax(T) - T(2)) + T <: Signed && push!(seeds, T(0), T(1), T(2), T(-1), T(-2)) + end + + vseeds = Dict{Vector{UInt32}, BigInt}() + for seed = seeds + bigseed = big(seed) + vseed = Random.make_seed(bigseed) + # test property 1) above + @test Random.make_seed(seed) == vseed + # test property 2) above + @test bigseed == get!(vseeds, vseed, bigseed) + # test that the property 1) is actually inherited by `seed!` + for rng = rngs + rng2 = copy(Random.seed!(rng, seed)) + Random.seed!(rng, bigseed) + @test rng == rng2 + end + end +end