Addressed parallel work

A mutable stream assigns random values by execution order, which a parallel schedule may change. Immutable keys assign random values to trial numbers, particle indices, data identifiers, or other application addresses.

Derive from the work index

This simulation derives each trial key from its index.

using ImmutableRNGs, Random, Statistics

root = Philox4x32(2026)
trial(key, i) = mean(randn(subrng(key, i), 64))

serial = [trial(root, i) for i in 1:32]
reordered = Dict(i => trial(root, i) for i in reverse(1:32))

serial == [reordered[i] for i in 1:32]
true

Each result depends on its trial index. A different scheduler or worker count does not change the trials.

Use ordinary mutable code inside each address

@keyed derives one wrapper from each loop-variable value. The loop body can use the standard rand* interface.

threaded = zeros(32)
@keyed rng = root Threads.@threads for i in eachindex(threaded)
    threaded[i] = mean(randn(64))
end

reference = [mean(randn(MutableRNG(subrng(root, i)), 64)) for i in 1:32]
threaded == reference
true

The address must identify the work. Use an array position only if it remains stable across ordering, filtering, and retries.

Give each generated element a key

generate permits variable work within an element. Element i receives subrng(root, i - 1), so the outer array remains addressable.

walks = generate(root, Float64, 16) do element_key
    step_key, sign_key = splitrng(element_key)
    step = randn(step_key)
    rand(sign_key, Bool) ? step : -step
end

larger = generate(root, Float64, 32) do element_key
    step_key, sign_key = splitrng(element_key)
    step = randn(step_key)
    rand(sign_key, Bool) ? step : -step
end

walks == larger[1:16]
true

See 03_parallel_mc.jl, 08_generate_block.jl, and 16_distributed_trials.jl.