exoskeleton/code/experiments/rng.py

54 lines
1.7 KiB
Python
Raw Permalink Normal View History

"""Order-independent named NumPy ``SeedSequence`` substreams."""
from __future__ import annotations
import hashlib
from typing import Iterable, Mapping
import numpy as np
from .hashing import canonical_json_bytes
DEFAULT_STREAMS = ("trajectory", "sensor", "model", "network")
def _label_words(*labels: object) -> list[int]:
digest = hashlib.sha256(canonical_json_bytes(labels)).digest()
return [
int.from_bytes(digest[offset : offset + 4], "little")
for offset in range(0, 16, 4)
]
def named_seed_record(
root_seed: int,
namespace: object,
stream_names: Iterable[str] = DEFAULT_STREAMS,
) -> dict[str, list[int]]:
"""Create stable named seed states independent of request order."""
root_seed = int(root_seed)
if root_seed < 0:
raise ValueError("root_seed must be non-negative")
record: dict[str, list[int]] = {}
for name in sorted(set(stream_names)):
if not name:
raise ValueError("stream names must be non-empty")
entropy = [root_seed & 0xFFFFFFFF, (root_seed >> 32) & 0xFFFFFFFF]
entropy.extend(_label_words(namespace, name))
state = np.random.SeedSequence(entropy).generate_state(4, dtype=np.uint32)
record[name] = [int(word) for word in state]
return record
def generator_from_record(
record: Mapping[str, list[int]],
stream_name: str,
) -> np.random.Generator:
if stream_name not in record:
raise KeyError(f"Unknown random stream {stream_name!r}")
words = [int(word) for word in record[stream_name]]
if len(words) != 4 or any(word < 0 or word > 0xFFFFFFFF for word in words):
raise ValueError(f"Invalid seed state for stream {stream_name!r}")
return np.random.default_rng(np.random.SeedSequence(words))