Skip to content

Commit

Permalink
Random: allow negative seeds (#51416)
Browse files Browse the repository at this point in the history
Alternative to #46190, see that PR for background. There isn't a strong
use-case for accepting negative seeds, but probably many people tried
something like `seed = rand(Int); seed!(rng, seed)` and saw it failing.
As it's easy to support, let's do it.

This might "break" some random streams, those for which the upper bit of
`make_seed(seed)[end]` was set, so it's rare.
  • Loading branch information
rfourquet authored Sep 29, 2023
1 parent b74daf5 commit 8ab635d
Show file tree
Hide file tree
Showing 5 changed files with 93 additions and 13 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
52 changes: 40 additions & 12 deletions stdlib/Random/src/RNGs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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!()
Expand Down
2 changes: 2 additions & 0 deletions stdlib/Random/src/Random.jl
Original file line number Diff line number Diff line change
Expand Up @@ -392,6 +392,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.
Expand Down
14 changes: 13 additions & 1 deletion stdlib/Random/src/Xoshiro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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()
Expand Down
36 changes: 36 additions & 0 deletions stdlib/Random/test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -908,6 +908,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
Expand Down Expand Up @@ -1130,3 +1136,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

0 comments on commit 8ab635d

Please sign in to comment.