Skip to content

Commit

Permalink
Modify Space.seed such that the return can be used as seeding values (
Browse files Browse the repository at this point in the history
  • Loading branch information
pseudo-rnd-thoughts authored Apr 28, 2024
1 parent d196497 commit 8bf2543
Show file tree
Hide file tree
Showing 13 changed files with 271 additions and 146 deletions.
41 changes: 21 additions & 20 deletions gymnasium/spaces/dict.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,44 +107,45 @@ def is_np_flattenable(self):
"""Checks whether this space can be flattened to a :class:`spaces.Box`."""
return all(space.is_np_flattenable for space in self.spaces.values())

def seed(self, seed: dict[str, Any] | int | None = None) -> list[int]:
def seed(self, seed: int | dict[str, Any] | None = None) -> dict[str, int]:
"""Seed the PRNG of this space and all subspaces.
Depending on the type of seed, the subspaces will be seeded differently
* ``None`` - All the subspaces will use a random initial seed
* ``Int`` - The integer is used to seed the :class:`Dict` space that is used to generate seed values for each of the subspaces. Warning, this does not guarantee unique seeds for all of the subspaces.
* ``Dict`` - Using all the keys in the seed dictionary, the values are used to seed the subspaces. This allows the seeding of multiple composite subspaces (``Dict["space": Dict[...], ...]`` with ``{"space": {...}, ...}``).
* ``Int`` - The integer is used to seed the :class:`Dict` space that is used to generate seed values for each of the subspaces. Warning, this does not guarantee unique seeds for all subspaces, though is very unlikely.
* ``Dict`` - A dictionary of seeds for each subspace, requires a seed key for every subspace. This supports seeding of multiple composite subspaces (``Dict["space": Dict[...], ...]`` with ``{"space": {...}, ...}``).
Args:
seed: An optional list of ints or int to seed the (sub-)spaces.
"""
seeds: list[int] = []
seed: An optional int or dictionary of subspace keys to int to seed each PRNG. See above for more details.
if isinstance(seed, dict):
assert (
seed.keys() == self.spaces.keys()
), f"The seed keys: {seed.keys()} are not identical to space keys: {self.spaces.keys()}"
for key in seed.keys():
seeds += self.spaces[key].seed(seed[key])
Returns:
A dictionary for the seed values of the subspaces
"""
if seed is None:
return {key: subspace.seed(None) for (key, subspace) in self.spaces.items()}
elif isinstance(seed, int):
seeds = super().seed(seed)
super().seed(seed)
# Using `np.int32` will mean that the same key occurring is extremely low, even for large subspaces
subseeds = self.np_random.integers(
np.iinfo(np.int32).max, size=len(self.spaces)
)
for subspace, subseed in zip(self.spaces.values(), subseeds):
seeds += subspace.seed(int(subseed))
elif seed is None:
for space in self.spaces.values():
seeds += space.seed(None)
return {
key: subspace.seed(int(subseed))
for (key, subspace), subseed in zip(self.spaces.items(), subseeds)
}
elif isinstance(seed, dict):
if seed.keys() != self.spaces.keys():
raise ValueError(
f"The seed keys: {seed.keys()} are not identical to space keys: {self.spaces.keys()}"
)

return {key: self.spaces[key].seed(seed[key]) for key in seed.keys()}
else:
raise TypeError(
f"Expected seed type: dict, int or None, actual type: {type(seed)}"
)

return seeds

def sample(self, mask: dict[str, Any] | None = None) -> dict[str, Any]:
"""Generates a single random sample from this space.
Expand Down
102 changes: 83 additions & 19 deletions gymnasium/spaces/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,19 @@ class Graph(Space[GraphInstance]):
Example:
>>> from gymnasium.spaces import Graph, Box, Discrete
>>> observation_space = Graph(node_space=Box(low=-100, high=100, shape=(3,)), edge_space=Discrete(3), seed=42)
>>> observation_space.sample()
GraphInstance(nodes=array([[-12.224312 , 71.71958 , 39.473606 ],
[-81.16453 , 95.12447 , 52.22794 ],
[ 57.21286 , -74.37727 , -9.922812 ],
[-25.840395 , 85.353 , 28.773024 ],
[ 64.55232 , -11.317161 , -54.552258 ],
[ 10.916958 , -87.23655 , 65.52624 ],
[ 26.33288 , 51.61755 , -29.094807 ],
[ 94.1396 , 78.62422 , 55.6767 ],
[-61.072258 , -6.6557994, -91.23925 ],
[-69.142105 , 36.60979 , 48.95243 ]], dtype=float32), edges=array([2, 0, 1, 1, 0, 0, 1, 0]), edge_links=array([[7, 5],
[6, 9],
[4, 1],
[8, 6],
[7, 0],
[3, 7],
[8, 4],
[8, 8]], dtype=int32))
>>> observation_space = Graph(node_space=Box(low=-100, high=100, shape=(3,)), edge_space=Discrete(3), seed=123)
>>> observation_space.sample(num_nodes=4, num_edges=8)
GraphInstance(nodes=array([[ 36.47037 , -89.235794, -55.928024],
[-63.125637, -64.81882 , 62.4189 ],
[ 84.669 , -44.68512 , 63.950912],
[ 77.97854 , 2.594091, -51.00708 ]], dtype=float32), edges=array([2, 0, 2, 1, 2, 0, 2, 1]), edge_links=array([[3, 0],
[0, 0],
[0, 1],
[0, 2],
[1, 0],
[1, 0],
[0, 1],
[0, 2]], dtype=int32))
"""

def __init__(
Expand Down Expand Up @@ -110,6 +104,76 @@ def _generate_sample_space(
f"Expects base space to be Box and Discrete, actual space: {type(base_space)}."
)

def seed(
self, seed: int | tuple[int, int] | tuple[int, int, int] | None = None
) -> tuple[int, int] | tuple[int, int, int]:
"""Seeds the PRNG of this space and node / edge subspace.
Depending on the type of seed, the subspaces will be seeded differently
* ``None`` - The root, node and edge spaces PRNG are randomly initialized
* ``Int`` - The integer is used to seed the :class:`Graph` space that is used to generate seed values for the node and edge subspaces.
* ``Tuple[int, int]`` - Seeds the :class:`Graph` and node subspace with a particular value. Only if edge subspace isn't specified
* ``Tuple[int, int, int]`` - Seeds the :class:`Graph`, node and edge subspaces with a particular value.
Args:
seed: An optional int or tuple of ints for this space and the node / edge subspaces. See above for more details.
Returns:
A tuple of two or three ints depending on if the edge subspace is specified.
"""
if seed is None:
if self.edge_space is None:
return super().seed(None), self.node_space.seed(None)
else:
return (
super().seed(None),
self.node_space.seed(None),
self.edge_space.seed(None),
)
elif isinstance(seed, int):
if self.edge_space is None:
super_seed = super().seed(seed)
node_seed = int(self.np_random.integers(np.iinfo(np.int32).max))
# this is necessary such that after int or list/tuple seeding, the Graph PRNG are equivalent
super().seed(seed)
return super_seed, self.node_space.seed(node_seed)
else:
super_seed = super().seed(seed)
node_seed, edge_seed = self.np_random.integers(
np.iinfo(np.int32).max, size=(2,)
)
# this is necessary such that after int or list/tuple seeding, the Graph PRNG are equivalent
super().seed(seed)
return (
super_seed,
self.node_space.seed(int(node_seed)),
self.edge_space.seed(int(edge_seed)),
)
elif isinstance(seed, (list, tuple)):
if self.edge_space is None:
if len(seed) != 2:
raise ValueError(
f"Expects a tuple of two values for Graph and node space, actual length: {len(seed)}"
)

return super().seed(seed[0]), self.node_space.seed(seed[1])
else:
if len(seed) != 3:
raise ValueError(
f"Expects a tuple of three values for Graph, node and edge space, actual length: {len(seed)}"
)

return (
super().seed(seed[0]),
self.node_space.seed(seed[1]),
self.edge_space.seed(seed[2]),
)
else:
raise TypeError(
f"Expects `None`, int or tuple of ints, actual type: {type(seed)}"
)

def sample(
self,
mask: None
Expand Down
53 changes: 29 additions & 24 deletions gymnasium/spaces/oneof.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Implementation of a space that represents the cartesian product of other spaces."""
from __future__ import annotations

import collections.abc
import typing
from typing import Any, Iterable

Expand All @@ -17,11 +16,11 @@ class OneOf(Space[Any]):
Example:
>>> from gymnasium.spaces import OneOf, Box, Discrete
>>> observation_space = OneOf((Discrete(2), Box(-1, 1, shape=(2,))), seed=42)
>>> observation_space = OneOf((Discrete(2), Box(-1, 1, shape=(2,))), seed=123)
>>> observation_space.sample() # the first element is the space index (Box in this case) and the second element is the sample from Box
(1, array([-0.3991573 , 0.21649833], dtype=float32))
>>> observation_space.sample() # this time the Discrete space was sampled as index=0
(0, 0)
>>> observation_space.sample() # this time the Discrete space was sampled as index=0
(1, array([-0.00711833, -0.7257502 ], dtype=float32))
>>> observation_space[0]
Discrete(2)
>>> observation_space[1]
Expand Down Expand Up @@ -57,43 +56,49 @@ def is_np_flattenable(self):
"""Checks whether this space can be flattened to a :class:`spaces.Box`."""
return all(space.is_np_flattenable for space in self.spaces)

def seed(self, seed: int | typing.Sequence[int] | None = None) -> list[int]:
def seed(self, seed: int | tuple[int, ...] | None = None) -> tuple[int, ...]:
"""Seed the PRNG of this space and all subspaces.
Depending on the type of seed, the subspaces will be seeded differently
* ``None`` - All the subspaces will use a random initial seed
* ``Int`` - The integer is used to seed the :class:`Tuple` space that is used to generate seed values for each of the subspaces. Warning, this does not guarantee unique seeds for all the subspaces.
* ``List`` - Values used to seed the subspaces. This allows the seeding of multiple composite subspaces ``[42, 54, ...]``.
* ``Tuple[int, ...]`` - Values used to seed the subspaces, first value seeds the OneOf and subsequent seed the subspaces. This allows the seeding of multiple composite subspaces ``[42, 54, ...]``.
Args:
seed: An optional list of ints or int to seed the (sub-)spaces.
seed: An optional int or tuple of ints to seed the OneOf space and subspaces. See above for more details.
Returns:
A tuple of ints used to seed the OneOf space and subspaces
"""
if isinstance(seed, collections.abc.Sequence):
assert (
len(seed) == len(self.spaces) + 1
), f"Expects that the subspaces of seeds equals the number of subspaces. Actual length of seeds: {len(seed)}, length of subspaces: {len(self.spaces)}"
seeds = super().seed(seed[0])
for subseed, space in zip(seed, self.spaces):
seeds += space.seed(subseed)
if seed is None:
super_seed = super().seed(None)
return (super_seed,) + tuple(space.seed(None) for space in self.spaces)
elif isinstance(seed, int):
seeds = super().seed(seed)
super_seed = super().seed(seed)
subseeds = self.np_random.integers(
np.iinfo(np.int32).max, size=len(self.spaces)
)
for subspace, subseed in zip(self.spaces, subseeds):
seeds += subspace.seed(int(subseed))
elif seed is None:
seeds = super().seed(None)
for space in self.spaces:
seeds += space.seed(None)
# this is necessary such that after int or list/tuple seeding, the OneOf PRNG are equivalent
super().seed(seed)
return (super_seed,) + tuple(
space.seed(int(subseed))
for space, subseed in zip(self.spaces, subseeds)
)
elif isinstance(seed, (tuple, list)):
if len(seed) != len(self.spaces) + 1:
raise ValueError(
f"Expects that the subspaces of seeds equals the number of subspaces + 1. Actual length of seeds: {len(seed)}, length of subspaces: {len(self.spaces)}"
)

return (super().seed(seed[0]),) + tuple(
space.seed(subseed) for space, subseed in zip(self.spaces, seed[1:])
)
else:
raise TypeError(
f"Expected seed type: list, tuple, int or None, actual type: {type(seed)}"
f"Expected None, int, or tuple of ints, actual type: {type(seed)}"
)

return seeds

def sample(self, mask: tuple[Any | None, ...] | None = None) -> tuple[int, Any]:
"""Generates a single random sample inside this space.
Expand Down
52 changes: 43 additions & 9 deletions gymnasium/spaces/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,18 @@ class Sequence(Space[Union[typing.Tuple[Any, ...], Any]]):
Example:
>>> from gymnasium.spaces import Sequence, Box
>>> observation_space = Sequence(Box(0, 1), seed=2)
>>> observation_space.sample()
(array([0.26161215], dtype=float32),)
>>> observation_space = Sequence(Box(0, 1), seed=0)
>>> observation_space.sample()
(array([0.6369617], dtype=float32), array([0.26978672], dtype=float32), array([0.04097353], dtype=float32))
(array([0.6822636], dtype=float32), array([0.18933342], dtype=float32), array([0.19049619], dtype=float32))
>>> observation_space.sample()
(array([0.83506], dtype=float32), array([0.9053838], dtype=float32), array([0.5836242], dtype=float32), array([0.63214064], dtype=float32))
Example with stacked observations
>>> observation_space = Sequence(Box(0, 1), stack=True, seed=0)
>>> observation_space.sample()
array([[0.6822636 ],
[0.18933342],
[0.19049619]], dtype=float32)
"""

def __init__(
Expand Down Expand Up @@ -53,11 +59,39 @@ def __init__(
# None for shape and dtype, since it'll require special handling
super().__init__(None, None, seed)

def seed(self, seed: int | None = None) -> list[int]:
"""Seed the PRNG of this space and the feature space."""
seeds = super().seed(seed)
seeds += self.feature_space.seed(seed)
return seeds
def seed(self, seed: int | tuple[int, int] | None = None) -> tuple[int, int]:
"""Seed the PRNG of the Sequence space and the feature space.
Depending on the type of seed, the subspaces will be seeded differently
* ``None`` - All the subspaces will use a random initial seed
* ``Int`` - The integer is used to seed the :class:`Sequence` space that is used to generate a seed value for the feature space.
* ``Tuple of ints`` - A tuple for the :class:`Sequence` and feature space.
Args:
seed: An optional int or tuple of ints to seed the PRNG. See above for more details
Returns:
A tuple of the seeding values for the Sequence and feature space
"""
if seed is None:
return super().seed(None), self.feature_space.seed(None)
elif isinstance(seed, int):
super_seed = super().seed(seed)
feature_seed = int(self.np_random.integers(np.iinfo(np.int32).max))
# this is necessary such that after int or list/tuple seeding, the Sequence PRNG are equivalent
super().seed(seed)
return super_seed, self.feature_space.seed(feature_seed)
elif isinstance(seed, (tuple, list)):
if len(seed) != 2:
raise ValueError(
f"Expects the seed to have two elements for the Sequence and feature space, actual length: {len(seed)}"
)
return super().seed(seed[0]), self.feature_space.seed(seed[1])
else:
raise TypeError(
f"Expected None, int, tuple of ints, actual type: {type(seed)}"
)

@property
def is_np_flattenable(self):
Expand Down
15 changes: 11 additions & 4 deletions gymnasium/spaces/space.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,20 @@ def sample(self, mask: Any | None = None) -> T_cov:
"""
raise NotImplementedError

def seed(self, seed: int | None = None) -> list[int]:
"""Seed the PRNG of this space and possibly the PRNGs of subspaces."""
def seed(self, seed: int | None = None) -> int | list[int] | dict[str, int]:
"""Seed the pseudorandom number generator (PRNG) of this space and, if applicable, the PRNGs of subspaces.
Args:
seed: The seed value for the space. This is expanded for composite spaces to accept multiple values. For further details, please refer to the space's documentation.
Returns:
The seed values used for all the PRNGs, for composite spaces this can be a tuple or dictionary of values.
"""
self._np_random, np_random_seed = seeding.np_random(seed)
return [np_random_seed]
return np_random_seed

def contains(self, x: Any) -> bool:
"""Return boolean specifying if x is a valid member of this space."""
"""Return boolean specifying if x is a valid member of this space, equivalent to ``sample in space``."""
raise NotImplementedError

def __contains__(self, x: Any) -> bool:
Expand Down
Loading

0 comments on commit 8bf2543

Please sign in to comment.