Files
Gymnasium/gymnasium/spaces/dict.py

239 lines
10 KiB
Python
Raw Normal View History

"""Implementation of a space that represents the cartesian product of other spaces as a dictionary."""
from __future__ import annotations
import collections.abc
import typing
2017-09-05 08:49:43 -07:00
from collections import OrderedDict
from typing import Any, KeysView, Sequence
import numpy as np
2022-09-08 10:10:07 +01:00
from gymnasium.spaces.space import Space
2017-09-05 08:49:43 -07:00
class Dict(Space[typing.Dict[str, Any]], typing.Mapping[str, Space[Any]]):
"""A dictionary of :class:`Space` instances.
Elements of this space are (ordered) dictionaries of elements from the constituent spaces.
2017-09-05 08:49:43 -07:00
Example:
>>> from gymnasium.spaces import Dict, Box, Discrete
>>> observation_space = Dict({"position": Box(-1, 1, shape=(2,)), "color": Discrete(3)}, seed=42)
>>> observation_space.sample()
OrderedDict([('color', 0), ('position', array([-0.3991573 , 0.21649833], dtype=float32))])
With a nested dict:
2022-09-08 10:10:07 +01:00
>>> from gymnasium.spaces import Box, Dict, Discrete, MultiBinary, MultiDiscrete
>>> Dict( # doctest: +SKIP
... {
Pydocstyle utils vector docstring (#2788) * Added pydocstyle to pre-commit * Added docstrings for tests and updated the tests for autoreset * Add pydocstyle exclude folder to allow slowly adding new docstrings * Add docstrings for setup.py and gym/__init__.py, core.py, error.py and logger.py * Check that all unwrapped environment are of a particular wrapper type * Reverted back to import gym.spaces.Space to gym.spaces * Fixed the __init__.py docstring * Fixed autoreset autoreset test * Updated gym __init__.py top docstring * Fix examples in docstrings * Add docstrings and type hints where known to all functions and classes in gym/utils and gym/vector * Remove unnecessary import * Removed "unused error" and make APIerror deprecated at gym 1.0 * Add pydocstyle description to CONTRIBUTING.md * Added docstrings section to CONTRIBUTING.md * Added :meth: and :attr: keywords to docstrings * Added :meth: and :attr: keywords to docstrings * Imported annotations from __future__ to fix python 3.7 * Add __future__ import annotations for python 3.7 * isort * Remove utils and vectors for this PR and spaces for previous PR * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/spaces/dict.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit * Updated docstrings with :meth: * Updated docstrings with :meth: * Update gym/utils/play.py * Update gym/utils/play.py * Update gym/utils/play.py * Apply suggestions from code review Co-authored-by: Markus Krimmel <montcyril@gmail.com> * pre-commit * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Updated fps and zoom parameter docstring * Update play docstring * Apply suggestions from code review Added suggested corrections from @markus28 Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit magic * Update the `gym.make` docstring with a warning for `env_checker` * Updated and fixed vector docstrings * Update test names for reflect the project filename style Co-authored-by: Markus Krimmel <montcyril@gmail.com>
2022-05-20 14:49:30 +01:00
... "ext_controller": MultiDiscrete([5, 2, 2]),
... "inner_state": Dict(
... {
Pydocstyle utils vector docstring (#2788) * Added pydocstyle to pre-commit * Added docstrings for tests and updated the tests for autoreset * Add pydocstyle exclude folder to allow slowly adding new docstrings * Add docstrings for setup.py and gym/__init__.py, core.py, error.py and logger.py * Check that all unwrapped environment are of a particular wrapper type * Reverted back to import gym.spaces.Space to gym.spaces * Fixed the __init__.py docstring * Fixed autoreset autoreset test * Updated gym __init__.py top docstring * Fix examples in docstrings * Add docstrings and type hints where known to all functions and classes in gym/utils and gym/vector * Remove unnecessary import * Removed "unused error" and make APIerror deprecated at gym 1.0 * Add pydocstyle description to CONTRIBUTING.md * Added docstrings section to CONTRIBUTING.md * Added :meth: and :attr: keywords to docstrings * Added :meth: and :attr: keywords to docstrings * Imported annotations from __future__ to fix python 3.7 * Add __future__ import annotations for python 3.7 * isort * Remove utils and vectors for this PR and spaces for previous PR * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/spaces/dict.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit * Updated docstrings with :meth: * Updated docstrings with :meth: * Update gym/utils/play.py * Update gym/utils/play.py * Update gym/utils/play.py * Apply suggestions from code review Co-authored-by: Markus Krimmel <montcyril@gmail.com> * pre-commit * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Updated fps and zoom parameter docstring * Update play docstring * Apply suggestions from code review Added suggested corrections from @markus28 Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit magic * Update the `gym.make` docstring with a warning for `env_checker` * Updated and fixed vector docstrings * Update test names for reflect the project filename style Co-authored-by: Markus Krimmel <montcyril@gmail.com>
2022-05-20 14:49:30 +01:00
... "charge": Discrete(100),
... "system_checks": MultiBinary(10),
... "job_status": Dict(
... {
Pydocstyle utils vector docstring (#2788) * Added pydocstyle to pre-commit * Added docstrings for tests and updated the tests for autoreset * Add pydocstyle exclude folder to allow slowly adding new docstrings * Add docstrings for setup.py and gym/__init__.py, core.py, error.py and logger.py * Check that all unwrapped environment are of a particular wrapper type * Reverted back to import gym.spaces.Space to gym.spaces * Fixed the __init__.py docstring * Fixed autoreset autoreset test * Updated gym __init__.py top docstring * Fix examples in docstrings * Add docstrings and type hints where known to all functions and classes in gym/utils and gym/vector * Remove unnecessary import * Removed "unused error" and make APIerror deprecated at gym 1.0 * Add pydocstyle description to CONTRIBUTING.md * Added docstrings section to CONTRIBUTING.md * Added :meth: and :attr: keywords to docstrings * Added :meth: and :attr: keywords to docstrings * Imported annotations from __future__ to fix python 3.7 * Add __future__ import annotations for python 3.7 * isort * Remove utils and vectors for this PR and spaces for previous PR * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/envs/classic_control/acrobot.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/spaces/dict.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/env_checker.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/ezpickle.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit * Updated docstrings with :meth: * Updated docstrings with :meth: * Update gym/utils/play.py * Update gym/utils/play.py * Update gym/utils/play.py * Apply suggestions from code review Co-authored-by: Markus Krimmel <montcyril@gmail.com> * pre-commit * Update gym/utils/play.py Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Updated fps and zoom parameter docstring * Update play docstring * Apply suggestions from code review Added suggested corrections from @markus28 Co-authored-by: Markus Krimmel <montcyril@gmail.com> * Pre-commit magic * Update the `gym.make` docstring with a warning for `env_checker` * Updated and fixed vector docstrings * Update test names for reflect the project filename style Co-authored-by: Markus Krimmel <montcyril@gmail.com>
2022-05-20 14:49:30 +01:00
... "task": Discrete(5),
... "progress": Box(low=0, high=100, shape=()),
... }
... ),
... }
... ),
... }
... )
It can be convenient to use :class:`Dict` spaces if you want to make complex observations or actions more human-readable.
2022-07-21 21:00:57 +08:00
Usually, it will not be possible to use elements of this space directly in learning code. However, you can easily
convert :class:`Dict` observations to flat arrays by using a :class:`gymnasium.wrappers.FlattenObservation` wrapper.
2022-12-03 12:02:09 +01:00
Similar wrappers can be implemented to deal with :class:`Dict` actions.
2017-09-05 08:49:43 -07:00
"""
2021-07-29 02:26:34 +02:00
def __init__(
self,
spaces: None | dict[str, Space] | Sequence[tuple[str, Space]] = None,
seed: dict | int | np.random.Generator | None = None,
Reduces warnings produced by pytest from ~1500 to 13 (#2660) * Updated cartpole-v0 to v1 to prevent warning and added pytest.mark.filterwarnings for tests where warnings are unavoidable * Change np.bool to bool as numpy raises a warning and bool is the suggested solution * Seeding randint is deprecated in the future, integers is new solution * Fixed errors thrown when the video recorder is deleted but not closed * spaces.Box expects a floating array, updated all cases where this was not true and modified float32 to float64 as float array default to float64. Otherwise space.Box raises warning that dtype precision (float32) is lower than array precision (float64). * Added pytest.mark.filterwarnings to preventing the raising of an intended warning * Added comment to explain why a warning is raised that can't be prevented without version update to the environment * Added comment to explain why warning is raised * Changed values to float as expected by the box which default to float64 * Removed --forked from pytest as the pytest-forked project is no being maintained and was not raising warnings as expected * When AsyncVectorEnv has shared_memory=True then a ValueError is raised before _state is initialised. Therefore, on the destruction on the env an error is thrown in .close_extra as _state does not exist * Possible fix that was causing an error in test_call_async_vector_env by ensuring that pygame resources are released * Pygame throws an error with ALSA when closed, using a fix from PettingZoo (https://github.com/Farama-Foundation/PettingZoo/blob/master/pettingzoo/__init__.py). We use the dsp audiodriver to prevent this issue * Modification due to running pre-commit locally * Updated cartpole-v0 to v1 to prevent warning and added pytest.mark.filterwarnings for tests where warnings are unavoidable * Change np.bool to bool as numpy raises a warning and bool is the suggested solution * Seeding randint is deprecated in the future, integers is new solution * Fixed errors thrown when the video recorder is deleted but not closed * spaces.Box expects a floating array, updated all cases where this was not true and modified float32 to float64 as float array default to float64. Otherwise space.Box raises warning that dtype precision (float32) is lower than array precision (float64). * Added pytest.mark.filterwarnings to preventing the raising of an intended warning * Added comment to explain why a warning is raised that can't be prevented without version update to the environment * Added comment to explain why warning is raised * Changed values to float as expected by the box which default to float64 * Removed --forked from pytest as the pytest-forked project is no being maintained and was not raising warnings as expected * When AsyncVectorEnv has shared_memory=True then a ValueError is raised before _state is initialised. Therefore, on the destruction on the env an error is thrown in .close_extra as _state does not exist * Possible fix that was causing an error in test_call_async_vector_env by ensuring that pygame resources are released * Pygame throws an error with ALSA when closed, using a fix from PettingZoo (https://github.com/Farama-Foundation/PettingZoo/blob/master/pettingzoo/__init__.py). We use the dsp audiodriver to prevent this issue * Modification due to running pre-commit locally
2022-03-14 14:27:03 +00:00
**spaces_kwargs: Space,
):
"""Constructor of :class:`Dict` space.
This space can be instantiated in one of two ways: Either you pass a dictionary
of spaces to :meth:`__init__` via the ``spaces`` argument, or you pass the spaces as separate
keyword arguments (where you will need to avoid the keys ``spaces`` and ``seed``)
Args:
spaces: A dictionary of spaces. This specifies the structure of the :class:`Dict` space
seed: Optionally, you can use this argument to seed the RNGs of the spaces that make up the :class:`Dict` space.
**spaces_kwargs: If ``spaces`` is ``None``, you need to pass the constituent spaces as keyword arguments, as described above.
"""
# Convert the spaces into an OrderedDict
if isinstance(spaces, collections.abc.Mapping) and not isinstance(
spaces, OrderedDict
):
try:
spaces = OrderedDict(sorted(spaces.items()))
except TypeError:
# Incomparable types (e.g. `int` vs. `str`, or user-defined types) found.
# The keys remain in the insertion order.
spaces = OrderedDict(spaces.items())
elif isinstance(spaces, Sequence):
spaces = OrderedDict(spaces)
elif spaces is None:
spaces = OrderedDict()
else:
assert isinstance(
spaces, OrderedDict
), f"Unexpected Dict space input, expecting dict, OrderedDict or Sequence, actual type: {type(spaces)}"
# Add kwargs to spaces to allow both dictionary and keywords to be used
for key, space in spaces_kwargs.items():
if key not in spaces:
spaces[key] = space
else:
raise ValueError(
f"Dict space keyword '{key}' already exists in the spaces dictionary."
)
self.spaces: dict[str, Space[Any]] = spaces
for key, space in self.spaces.items():
2021-07-29 15:39:42 -04:00
assert isinstance(
space, Space
), f"Dict space element is not an instance of Space: key='{key}', space={space}"
# None for shape and dtype, since it'll require special handling
super().__init__(None, None, seed) # type: ignore
@property
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]:
"""Seed the PRNG of this space and all subspaces.
Depending on the type of seed, the subspaces will be seeded differently
2022-12-03 12:02:09 +01:00
* ``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": {...}, ...}``).
Args:
seed: An optional list of ints or int to seed the (sub-)spaces.
"""
seeds: list[int] = []
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])
elif isinstance(seed, int):
seeds = 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)
else:
raise TypeError(
f"Expected seed type: dict, int or None, actual type: {type(seed)}"
)
return seeds
2017-09-05 08:49:43 -07:00
def sample(self, mask: dict[str, Any] | None = None) -> dict[str, Any]:
"""Generates a single random sample from this space.
The sample is an ordered dictionary of independent samples from the constituent spaces.
Args:
mask: An optional mask for each of the subspaces, expects the same keys as the space
Returns:
A dictionary with the same key and sampled values from :attr:`self.spaces`
"""
if mask is not None:
assert isinstance(
mask, dict
), f"Expects mask to be a dict, actual type: {type(mask)}"
assert (
mask.keys() == self.spaces.keys()
), f"Expect mask keys to be same as space keys, mask keys: {mask.keys()}, space keys: {self.spaces.keys()}"
return OrderedDict(
[(k, space.sample(mask[k])) for k, space in self.spaces.items()]
)
2017-09-05 08:49:43 -07:00
return OrderedDict([(k, space.sample()) for k, space in self.spaces.items()])
def contains(self, x: Any) -> bool:
"""Return boolean specifying if x is a valid member of this space."""
if isinstance(x, dict) and x.keys() == self.spaces.keys():
return all(x[key] in self.spaces[key] for key in self.spaces.keys())
return False
2017-09-05 08:49:43 -07:00
def __getitem__(self, key: str) -> Space[Any]:
"""Get the space that is associated to `key`."""
2019-04-19 23:19:07 +02:00
return self.spaces[key]
2021-07-29 02:26:34 +02:00
def keys(self) -> KeysView:
"""Returns the keys of the Dict."""
return KeysView(self.spaces)
def __setitem__(self, key: str, value: Space[Any]):
"""Set the space that is associated to `key`."""
assert isinstance(
value, Space
2022-09-08 10:10:07 +01:00
), f"Trying to set {key} to Dict space with value that is not a gymnasium space, actual type: {type(value)}"
self.spaces[key] = value
def __iter__(self):
"""Iterator through the keys of the subspaces."""
yield from self.spaces
2019-04-19 23:19:07 +02:00
def __len__(self) -> int:
"""Gives the number of simpler spaces that make up the `Dict` space."""
2021-07-29 22:29:10 +02:00
return len(self.spaces)
def __repr__(self) -> str:
"""Gives a string representation of this space."""
return (
"Dict(" + ", ".join([f"{k!r}: {s}" for k, s in self.spaces.items()]) + ")"
)
def __eq__(self, other: Any) -> bool:
"""Check whether `other` is equivalent to this instance."""
return (
isinstance(other, Dict)
# Comparison of `OrderedDict`s is order-sensitive
and self.spaces == other.spaces # OrderedDict.__eq__
)
2017-09-05 08:49:43 -07:00
def to_jsonable(self, sample_n: Sequence[dict[str, Any]]) -> dict[str, list[Any]]:
"""Convert a batch of samples from this space to a JSONable data type."""
2017-09-05 08:49:43 -07:00
# serialize as dict-repr of vectors
2021-07-29 15:39:42 -04:00
return {
key: space.to_jsonable([sample[key] for sample in sample_n])
for key, space in self.spaces.items()
}
2017-09-05 08:49:43 -07:00
def from_jsonable(
self, sample_n: dict[str, list[Any]]
) -> list[OrderedDict[str, Any]]:
"""Convert a JSONable data type to a batch of samples from this space."""
dict_of_list: dict[str, list[Any]] = {
key: space.from_jsonable(sample_n[key])
for key, space in self.spaces.items()
}
n_elements = len(next(iter(dict_of_list.values())))
result = [
OrderedDict({key: value[n] for key, value in dict_of_list.items()})
for n in range(n_elements)
]
return result