Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions src/orng/backends/cupy.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ def choice(
)
return result

def state(self) -> Any:
"""Return the current backend-native generator state."""
return self._state

def set_state(self, state: Any) -> None:
"""Replace the current backend-native generator state."""
self._state = self._impl.init_state(seed=None, generator=state)

def state_dict(self) -> dict[str, Any]:
generator = self._state
return {
Expand Down
8 changes: 8 additions & 0 deletions src/orng/backends/jax.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ def choice(
)
return result

def state(self) -> Any:
"""Return the current JAX random key."""
return self._state

def set_state(self, state: Any) -> None:
"""Replace the current JAX random key."""
self._state = self._impl.init_state(seed=None, generator=state)

def state_dict(self) -> dict[str, Any]:
return {
"key_data": self._impl._jnp.array(
Expand Down
8 changes: 8 additions & 0 deletions src/orng/backends/numpy.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,14 @@ def choice(
)
return result

def state(self) -> Any:
"""Return the current backend-native generator state."""
return self._state

def set_state(self, state: Any) -> None:
"""Replace the current backend-native generator state."""
self._state = self._impl.init_state(seed=None, generator=state)

def state_dict(self) -> dict[str, Any]:
generator = self._state
return {
Expand Down
9 changes: 9 additions & 0 deletions src/orng/backends/torch.py
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ def choice(
)
return result

def state(self) -> Any:
"""Return the current backend-native generator state."""
return self._state

def set_state(self, state: Any) -> None:
"""Replace the current backend-native generator state."""
self._state = self._impl.init_state(seed=None, generator=state)
self._impl._device = getattr(state, "device", self._impl._device)

def state_dict(self) -> dict[str, Any]:
generator = self._state
return {
Expand Down
21 changes: 17 additions & 4 deletions src/orng/orng.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,15 @@

from ._utils import SizeLike
from .backends import create_backend, infer_backend_name_from_xp
from .functional import FunctionalState


class RNGBackend(Protocol):
"""Protocol representing the shim each backend must implement."""

_state: Any
def state(self) -> FunctionalState: ...

def set_state(self, state: FunctionalState) -> None: ...

def state_dict(self) -> dict[str, Any]: ...

Expand Down Expand Up @@ -119,8 +122,6 @@ def __post_init__(self) -> None:
device=self.device,
)

# Public API -----------------------------------------------------------------

def random(
self,
size: SizeLike = None,
Expand Down Expand Up @@ -207,7 +208,19 @@ def to_functional(
self.backend,
pure=pure,
)
return backend, self._impl._state
return backend, self.state()

def state(self) -> FunctionalState:
"""Return the current backend-native random state.

This may be a mutable generator object. Use :meth:`state_dict` when a
detached snapshot is required.
"""
return self._impl.state()

def set_state(self, state: FunctionalState) -> None:
"""Replace the current backend-native random state."""
self._impl.set_state(state)

def state_dict(self) -> dict[str, Any]:
"""Return a detached, versioned snapshot of the generator state."""
Expand Down
14 changes: 14 additions & 0 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -281,6 +281,20 @@ def test_random_generator_load_state_dict(rng, backend_case):
backend_case["assert_close"](actual, expected)


def test_random_generator_set_state(rng, backend_case):
backend_name = backend_case["name"]
source = RandomGenerator(backend_name, seed=987)
expected_source = RandomGenerator(backend_name, seed=987)
state = source.state()

rng.set_state(state)
assert rng.state() is state
actual = rng.normal(size=(4, 3))
expected = expected_source.normal(size=(4, 3))

backend_case["assert_close"](actual, expected)


def test_array_rng_gamma(rng):
x = rng.gamma(shape=2.0, scale=2.0, size=(6, 2))
assert x.shape == (6, 2)
Expand Down
2 changes: 1 addition & 1 deletion tests/test_orng.py
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ def choice(self, state, population, *, size, replace, probabilities):
functional_backend, state = rng.to_functional()

assert functional_backend is backend
assert state is rng._impl._state
assert state is rng.state()


def test_array_rng_to_functional_forwards_pure(monkeypatch):
Expand Down
Loading