Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add testing for Fundamental spaces with full coverage #3048

Merged
merged 5 commits into from
Sep 3, 2022
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
initial commit
  • Loading branch information
pseudo-rnd-thoughts committed Aug 27, 2022
commit b84135a1fa7d47919c7b39b5dbd8b97f0a92ab6b
14 changes: 10 additions & 4 deletions gym/spaces/box.py
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,9 @@ def is_bounded(self, manner: str = "both") -> bool:
elif manner == "above":
return above
else:
raise ValueError("manner is not in {'below', 'above', 'both'}")
raise ValueError(
f"manner is not in {{'below', 'above', 'both'}}, actual value: {manner}"
)

def sample(self, mask: None = None) -> np.ndarray:
r"""Generates a single random sample inside the Box.
Expand Down Expand Up @@ -223,7 +225,10 @@ def contains(self, x) -> bool:
"""Return boolean specifying if x is a valid member of this space."""
if not isinstance(x, np.ndarray):
logger.warn("Casting input x to numpy array.")
x = np.asarray(x, dtype=self.dtype)
try:
x = np.asarray(x, dtype=self.dtype)
except (ValueError, TypeError):
return False

return bool(
np.can_cast(x.dtype, self.dtype)
Expand All @@ -236,7 +241,7 @@ def to_jsonable(self, sample_n):
"""Convert a batch of samples from this space to a JSONable data type."""
return np.array(sample_n).tolist()

def from_jsonable(self, sample_n: Sequence[SupportsFloat]) -> List[np.ndarray]:
def from_jsonable(self, sample_n: Sequence[Union[float, int]]) -> List[np.ndarray]:
"""Convert a JSONable data type to a batch of samples from this space."""
return [np.asarray(sample) for sample in sample_n]

Expand All @@ -252,10 +257,11 @@ def __repr__(self) -> str:
return f"Box({self.low_repr}, {self.high_repr}, {self.shape}, {self.dtype})"

def __eq__(self, other) -> bool:
"""Check whether `other` is equivalent to this instance."""
"""Check whether `other` is equivalent to this instance. Doesn't check dtype equivalence."""
return (
isinstance(other, Box)
and (self.shape == other.shape)
# and (self.dtype == other.dtype)
and np.allclose(self.low, other.low)
and np.allclose(self.high, other.high)
)
Expand Down
12 changes: 5 additions & 7 deletions gym/spaces/discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -85,18 +85,19 @@ def contains(self, x) -> bool:
if isinstance(x, int):
as_int = x
elif isinstance(x, (np.generic, np.ndarray)) and (
x.dtype.char in np.typecodes["AllInteger"] and x.shape == ()
np.issubdtype(x.dtype, np.integer) and x.shape == ()
):
as_int = int(x) # type: ignore
else:
return False

return self.start <= as_int < self.start + self.n

def __repr__(self) -> str:
"""Gives a string representation of this space."""
if self.start != 0:
return "Discrete(%d, start=%d)" % (self.n, self.start)
return "Discrete(%d)" % self.n
return f"Discrete({self.n}, start={self.start})"
return f"Discrete({self.n})"

def __eq__(self, other) -> bool:
"""Check whether ``other`` is equivalent to this instance."""
Expand All @@ -114,8 +115,6 @@ def __setstate__(self, state):
Args:
state: The new state
"""
super().__setstate__(state)

# Don't mutate the original state
state = dict(state)

Expand All @@ -124,5 +123,4 @@ def __setstate__(self, state):
if "start" not in state:
state["start"] = 0

# Update our state
self.__dict__.update(state)
super().__setstate__(state)
6 changes: 3 additions & 3 deletions gym/spaces/graph.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

from gym.spaces.box import Box
from gym.spaces.discrete import Discrete
from gym.spaces.multi_discrete import SAMPLE_MASK_TYPE, MultiDiscrete
from gym.spaces.multi_discrete import MultiDiscrete
from gym.spaces.space import Space


Expand Down Expand Up @@ -97,8 +97,8 @@ def sample(
self,
mask: Optional[
Tuple[
Optional[Union[np.ndarray, SAMPLE_MASK_TYPE]],
Optional[Union[np.ndarray, SAMPLE_MASK_TYPE]],
Optional[Union[np.ndarray, tuple]],
Optional[Union[np.ndarray, tuple]],
]
] = None,
num_nodes: int = 10,
Expand Down
14 changes: 9 additions & 5 deletions gym/spaces/multi_binary.py
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,8 @@ def sample(self, mask: Optional[np.ndarray] = None) -> np.ndarray:

Args:
mask: An optional np.ndarray to mask samples with expected shape of ``space.shape``.
Where mask == 0 then the samples will be 0.
For mask == 0 then the samples will be 0 and mask == 1 then random samples will be generated.
The expected mask shape is the space shape and mask dtype is `np.int8`.

Returns:
Sampled values from space
Expand Down Expand Up @@ -91,17 +92,20 @@ def contains(self, x) -> bool:
"""Return boolean specifying if x is a valid member of this space."""
if isinstance(x, Sequence):
x = np.array(x) # Promote list to array for contains check
if self.shape != x.shape:
return False
return ((x == 0) | (x == 1)).all()

return bool(
isinstance(x, np.ndarray)
and self.shape == x.shape
and np.all((x == 0) | (x == 1))
)

def to_jsonable(self, sample_n) -> list:
"""Convert a batch of samples from this space to a JSONable data type."""
return np.array(sample_n).tolist()

def from_jsonable(self, sample_n) -> list:
"""Convert a JSONable data type to a batch of samples from this space."""
return [np.asarray(sample) for sample in sample_n]
return [np.asarray(sample, self.dtype) for sample in sample_n]

def __repr__(self) -> str:
"""Gives a string representation of this space."""
Expand Down
57 changes: 36 additions & 21 deletions gym/spaces/multi_discrete.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,6 @@
from gym.spaces.discrete import Discrete
from gym.spaces.space import Space

SAMPLE_MASK_TYPE = Tuple[Union["SAMPLE_MASK_TYPE", np.ndarray], ...]


class MultiDiscrete(Space[np.ndarray]):
"""This represents the cartesian product of arbitrary :class:`Discrete` spaces.
Expand Down Expand Up @@ -39,7 +37,7 @@ class MultiDiscrete(Space[np.ndarray]):

def __init__(
self,
nvec: Union[np.ndarray, List[int]],
nvec: Union[np.ndarray, list],
dtype=np.int64,
seed: Optional[Union[int, np.random.Generator]] = None,
):
Expand Down Expand Up @@ -68,7 +66,7 @@ def is_np_flattenable(self):
"""Checks whether this space can be flattened to a :class:`spaces.Box`."""
return True

def sample(self, mask: Optional[SAMPLE_MASK_TYPE] = None) -> np.ndarray:
def sample(self, mask: Optional[tuple] = None) -> np.ndarray:
"""Generates a single random sample this space.

Args:
Expand All @@ -82,15 +80,31 @@ def sample(self, mask: Optional[SAMPLE_MASK_TYPE] = None) -> np.ndarray:
if mask is not None:

def _apply_mask(
sub_mask: SAMPLE_MASK_TYPE, sub_nvec: np.ndarray
sub_mask: Union[np.ndarray, tuple],
sub_nvec: Union[np.ndarray, np.integer],
) -> Union[int, List[int]]:
if isinstance(sub_mask, np.ndarray):
# TODO: consider a special case where the mask can be a single np.ndarray, i.e MD([2, 2])
if isinstance(sub_nvec, np.ndarray):
assert isinstance(
sub_mask, tuple
), f"Expects the mask to be a tuple for sub_nvec ({sub_nvec}), actual type: {type(sub_mask)}"
assert len(sub_mask) == len(
sub_nvec
), f"Expects the mask length to be equal to the number of actions, mask length: {len(sub_mask)}, nvec length: {len(sub_nvec)}"
return [
_apply_mask(new_mask, new_nvec)
for new_mask, new_nvec in zip(sub_mask, sub_nvec)
]
else:
assert np.issubdtype(
type(sub_nvec), np.integer
), f"Expects the mask to be for an action, actual for {sub_nvec}"
), f"Expects the sub_nvec to be an action, actually: {sub_nvec}, {type(sub_nvec)}"
assert isinstance(
sub_mask, np.ndarray
), f"Expects the sub mask to be np.ndarray, actual type: {type(sub_mask)}"
assert (
len(sub_mask) == sub_nvec
), f"Expects the mask length to be equal to the number of actions, mask length: {len(sub_mask)}, nvec length: {sub_nvec}"
), f"Expects the mask length to be equal to the number of actions, mask length: {len(sub_mask)}, action: {sub_nvec}"
assert (
sub_mask.dtype == np.int8
), f"Expects the mask dtype to be np.int8, actual dtype: {sub_mask.dtype}"
Expand All @@ -104,17 +118,6 @@ def _apply_mask(
return self.np_random.choice(np.where(valid_action_mask)[0])
else:
return 0
else:
assert isinstance(
sub_mask, tuple
), f"Expects the mask to be a tuple or np.ndarray, actual type: {type(sub_mask)}"
assert len(sub_mask) == len(
sub_nvec
), f"Expects the mask length to be equal to the number of actions, mask length: {len(sub_mask)}, nvec length: {len(sub_nvec)}"
return [
_apply_mask(new_mask, new_nvec)
for new_mask, new_nvec in zip(sub_mask, sub_nvec)
]

return np.array(_apply_mask(mask, self.nvec), dtype=self.dtype)

Expand All @@ -124,9 +127,16 @@ def contains(self, x) -> bool:
"""Return boolean specifying if x is a valid member of this space."""
if isinstance(x, Sequence):
x = np.array(x) # Promote list to array for contains check

# if nvec is uint32 and space dtype is uint32, then 0 <= x < self.nvec guarantees that x
# is within correct bounds for space dtype (even though x does not have to be unsigned)
return bool(x.shape == self.shape and (0 <= x).all() and (x < self.nvec).all())
return bool(
isinstance(x, np.ndarray)
and x.shape == self.shape
and x.dtype != object
and np.all(0 <= x)
and np.all(x < self.nvec)
)

def to_jsonable(self, sample_n: Iterable[np.ndarray]):
"""Convert a batch of samples from this space to a JSONable data type."""
Expand All @@ -147,13 +157,18 @@ def __getitem__(self, index):
subspace = Discrete(nvec)
else:
subspace = MultiDiscrete(nvec, self.dtype) # type: ignore

# you don't need to deepcopy as np random generator call replaces the state not the data
subspace.np_random.bit_generator.state = self.np_random.bit_generator.state

return subspace

def __len__(self):
"""Gives the ``len`` of samples from this space."""
if self.nvec.ndim >= 2:
logger.warn("Get length of a multi-dimensional MultiDiscrete space.")
logger.warn(
"Getting the length of a multi-dimensional MultiDiscrete space."
)
return len(self.nvec)

def __eq__(self, other):
Expand Down
28 changes: 24 additions & 4 deletions gym/spaces/sequence.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

import numpy as np

import gym
from gym.spaces.space import Space
from gym.utils import seeding

Expand All @@ -25,14 +26,17 @@ class Sequence(Space[Tuple]):
def __init__(
self,
space: Space,
seed: Optional[Union[int, List[int], seeding.RandomNumberGenerator]] = None,
seed: Optional[Union[int, seeding.RandomNumberGenerator]] = None,
):
"""Constructor of the :class:`Sequence` space.

Args:
space: Elements in the sequences this space represent must belong to this space.
seed: Optionally, you can use this argument to seed the RNG that is used to sample from the space.
"""
assert isinstance(
space, gym.Space
), f"Expects the feature space to be instance of a gym Space, actual type: {type(space)}"
self.feature_space = space
super().__init__(
None, None, seed # type: ignore
Expand All @@ -50,7 +54,8 @@ def is_np_flattenable(self):
return False

def sample(
self, mask: Optional[Tuple[Optional[np.ndarray], Any]] = None
self,
mask: Optional[Tuple[Optional[Union[np.ndarray, int]], Optional[Any]]] = None,
) -> Tuple[Any]:
"""Generates a single random sample from this space.

Expand All @@ -68,9 +73,24 @@ def sample(
if mask is not None:
length_mask, feature_mask = mask
else:
length_mask = None
feature_mask = None
length_mask, feature_mask = None, None

if length_mask is not None:
if np.issubdtype(type(length_mask), np.integer):
assert (
0 <= length_mask
), f"Expects the length mask to be greater than zero, actual value: {length_mask}"
elif isinstance(length_mask, np.ndarray):
assert (
len(length_mask.shape) == 1
), f"Expects the shape of the length mask to be 1-dimensional, actual shape: {length_mask.shape}"
assert np.all(
0 <= length_mask
), f"Expects all values in the length_mask to be greater than zero, actual values: {length_mask}"
else:
raise TypeError(
f"Expects the type of length_mask to an integer or a np.ndarray, actual type: {type(length_mask)}"
)
length = self.np_random.choice(length_mask)
else:
length = self.np_random.geometric(0.25)
Expand Down
Loading