Skip to content

Commit

Permalink
Random: allow negative seeds
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 committed Sep 21, 2023
1 parent 5fc5556 commit 5288b37
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 12 deletions.
2 changes: 2 additions & 0 deletions NEWS.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,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
55 changes: 43 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,51 @@ 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)
n = abs(n) # n can still be negative, e.g. n == typemin(Int)
if n < 0
# we assume that `unsigned` can be called on integers `n` for which `abs(n)` is
# negative; `unsigned` is necessary for `n & 0xffffffff` below, which would
# otherwise propagate the sign bit of `n` for types smaller than UInt32
n = unsigned(n)
end
seed = UInt32[]
while true
# we directly encode the bit pattern of `abs(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 @@ -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.
Expand Down
13 changes: 13 additions & 0 deletions stdlib/Random/src/Xoshiro.jl
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ multiple interleaved xoshiro instances).
The virtual PRNGs are discarded once the bulk request has been serviced (and should cause
no heap allocations).
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 `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 @@ -89,6 +96,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 an integer or a vector of `UInt32` integers.
!!! 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 @@ -930,6 +930,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 @@ -1088,3 +1094,33 @@ end
@test TaskLocalRNG() == rng3
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 5288b37

Please sign in to comment.