Skip to content

Spaces

Space classes are exported from sekai and sekai.spaces; the utility functions from sekai.spaces (also available as sekai.space_utils). See the Spaces guide.

Base class

Space

Space(shape: tuple[int, ...] | None = None, dtype: dtype | None = None, seed: SeedType = None)

Bases: ABC, Generic[T]

Abstract base class for all observation and action spaces.

Spaces define the valid range of values for observations and actions. Unlike gymnasium, T is invariant so contains() is correctly typed.

Equality: the built-in spaces compare by value (and, being mutable, are not hashable). A subclass that does not define __eq__ compares by identity, so two instances are equal only if they are the same object; define __eq__ (and set __hash__ = None, or define it consistently) to compare by value.

is_flattenable abstractmethod property

is_flattenable: bool

Whether this space can be flattened to a Box.

seed

seed(seed: SeedType = None) -> int

Seed the space's RNG. Returns the actual seed used.

sample abstractmethod

sample(mask: Any = None) -> T

Sample a random element from this space.

contains abstractmethod

contains(x: Any) -> bool

Return True if x is a valid element of this space.

Fundamental spaces

Box

Box(low: ArrayLike | float, high: ArrayLike | float, shape: tuple[int, ...] | None = None, dtype: DTypeLike = float32, seed: SeedType = None)

Bases: Space[NDArray[Any]]

Continuous n-dimensional box space.

Supports bounded, semi-bounded, and unbounded dimensions. Unbounded dimensions are sampled from a shifted/scaled normal distribution.

Bounds must not be NaN. For integer dtypes, infinite bounds (and any others outside the dtype's range) become the dtype's limits, e.g. Box(-np.inf, np.inf, dtype=np.int64) spans np.iinfo(np.int64).

is_bounded property

is_bounded: tuple[bool, bool]

Returns (lower_bounded, upper_bounded) for the entire space.

zero

zero() -> NDArray[Any]

The zero element, clipped into the bounds (the neutral value for padding).

sample

sample(mask: NDArray[Any] | None = None) -> NDArray[Any]

Sample uniformly (bounded), exponentially (half-bounded) or normally (unbounded).

mask: optional array broadcastable to shape; entries where it is 0 are not sampled but set to zero clipped into the bounds (zero()), e.g. the weight of an asset that cannot be traded.

Discrete

Discrete(n: int | integer[Any], seed: SeedType = None, start: int | integer[Any] = 0)

Bases: Space[intp]

Discrete space with n values: {start, start+1, ..., start+n-1}.

Equivalent to gymnasium's Discrete, with an optional start offset.

sample

sample(mask: ArrayLike | None = None) -> intp

Sample uniformly; mask (shape (n,), e.g. int8 or a list) marks allowed values with 1.

MultiDiscrete

MultiDiscrete(nvec: ArrayLike, dtype: type = int64, seed: SeedType = None, start: ArrayLike | None = None)

Bases: Space[NDArray[intp]]

Space for multi-dimensional discrete actions.

Each dimension i takes values in {start[i], ..., start[i] + nvec[i] - 1}.

sample

sample(mask: NDArray[int8] | tuple[Any, ...] | None = None) -> NDArray[intp]

Sample uniformly, optionally restricted by a mask.

mask: per entry, an int8 array of length nvec[i] with 1 marking allowed values. Give either a tuple of such arrays (nested to match nvec's shape, as in gymnasium) or one array whose last axis is padded to max(nvec). A mask of any other shape raises ValueError.

MultiBinary

MultiBinary(n: int | integer[Any] | tuple[int, ...], seed: SeedType = None)

Bases: Space[NDArray[int8]]

Space of n-dimensional binary vectors with values in {0, 1}.

sample

sample(mask: ArrayLike | None = None) -> NDArray[int8]

Sample uniformly; mask (shape n): 1 forces a 1, 0 forces a 0, -1 samples freely.

Composite spaces

Dict

Dict(spaces: Mapping[str, Space[Any]] | None = None, seed: SeedType | dict[str, SeedType] | None = None, **spaces_kwargs: Space[Any])

Bases: Space[dict[str, Any]]

Space of ordered dicts of sub-spaces.

Useful for heterogeneous observations, e.g. combining price arrays, portfolio state, and order book snapshots into a single observation.

seed

seed(seed: SeedType | Mapping[str, SeedType] = None) -> int

Seed this space and, deterministically, every sub-space.

A dict of seeds ({"prices": 1, "cash": 2}) seeds the named sub-spaces individually instead, leaving the others as they are, and returns -1 (there is no single seed). A key that is not a sub-space raises ValueError.

Tuple

Tuple(spaces: tuple[Space[Any], ...] | list[Space[Any]], seed: SeedType | list[SeedType] | None = None)

Bases: Space[tuple[Any, ...]]

Space of ordered tuples of sub-spaces.

Unlike Dict, Tuple sub-spaces are accessed by index rather than key. Useful when the number and type of components are fixed and heterogeneous.

seed

seed(seed: SeedType | list[SeedType] | tuple[SeedType, ...] = None) -> int

Seed this space and, deterministically, every sub-space.

A list of seeds ([1, None, 3]) seeds the sub-spaces individually instead, in order (None leaves one as it is), and returns -1 (there is no single seed). More seeds than sub-spaces raise ValueError.

sample

sample(mask: tuple[Any, ...] | None = None) -> tuple[Any, ...]

Sample a tuple; mask, if given, has one sub-mask (or None) per sub-space.

Variable-size spaces

See Variable-size observations.

Sequence

Sequence(feature_space: Space[Any], *, max_length: int | None = None, stack: bool = False, seed: SeedType = None)

Bases: Space[Any]

Variable-length sequences of elements from a feature space.

For observations whose size changes from step to step: the trades printed since the last decision, the news items in a window, the orders resting in a book. With stack=False (the default) a sample is a tuple of elements; with stack=True the elements are stacked into one batched sample (an array of shape (length, *feature_shape) for a Box, a dict of arrays for a Dict, and so on), which is what most models consume.

Sequences cannot be flattened or stacked across vector sub-envs into one array; convert them to a fixed-size padded form with sekai.PadSequenceObservation (which yields a MaskedBox) when a policy needs fixed shapes.

Parameters:

Name Type Description Default
feature_space Space[Any]

The space of each element.

required
max_length int | None

Optional upper bound on the length. Samples are uniform in [0, max_length]; without a bound the length is geometric (mean 3).

None
stack bool

Represent samples as one stacked batch instead of a tuple.

False
seed SeedType

Seed for sampling.

None

seed

seed(seed: SeedType = None) -> int

Seed this space and, deterministically, its feature space.

sample

sample(mask: tuple[int | None, Any] | None = None) -> Any

Sample a sequence.

mask: optional (length, feature_mask); a given length fixes the sequence length, and feature_mask is passed to every element's sample().

pack

pack(items: list[Any] | tuple[Any, ...]) -> Any

Build a sample of this space from a list of elements.

elements

elements(x: Any) -> list[Any]

The elements of a sample, as a list (inverse of pack()).

MaskedBox

MaskedBox(low: ArrayLike | float, high: ArrayLike | float, shape: tuple[int, ...] | None = None, *, max_size: int, dtype: DTypeLike = float32, fill_value: ArrayLike | None = None, seed: SeedType = None)

Bases: Dict

A variable-size set of up to max_size Box elements, padded to a fixed shape.

Samples are dicts with two arrays:

  • "values": shape (max_size, *shape), one row per slot;
  • "mask": int8 of shape (max_size,), 1 where the slot holds an element.

Empty slots hold fill_value (by default zero, clipped into the bounds). This is the standard way to give a policy an observation whose size changes, such as the assets in a trading universe that gains and loses listings: shapes stay fixed, so samples batch across vector sub-envs, flatten, and convert to gymnasium, while the mask tells a set encoder, transformer or masked mean which rows are real.

MaskedBox is a Dict subclass, so every Dict utility (flatten, batch_space, concatenate, FilterObservation, gymnasium conversion) applies unchanged.

Example
space = sekai.MaskedBox(-np.inf, np.inf, shape=(3,), max_size=100)
obs = space.pack(features, slots=[0, 4, 7])  # 3 assets in slots 0, 4 and 7
values, slots = space.unpack(obs)

Parameters:

Name Type Description Default
low ArrayLike | float

Lower bound of each element (scalar or array of shape).

required
high ArrayLike | float

Upper bound of each element.

required
shape tuple[int, ...] | None

Shape of one element; inferred from low/high when omitted.

None
max_size int

Number of slots.

required
dtype DTypeLike

Element dtype.

float32
fill_value ArrayLike | None

Value of empty slots; must lie within the bounds.

None
seed SeedType

Seed for sampling.

None

element_shape property

element_shape: tuple[int, ...]

Shape of one element.

value_dtype property

value_dtype: dtype[Any]

dtype of the values array.

empty

empty() -> dict[str, NDArray[Any]]

A sample with every slot empty.

pack

pack(values: ArrayLike, slots: ArrayLike | None = None) -> dict[str, NDArray[Any]]

Place k elements into slots (default: the first k) and pad the rest.

Parameters:

Name Type Description Default
values ArrayLike

Array of shape (k, *shape).

required
slots ArrayLike | None

k distinct slot indices in [0, max_size).

None

unpack

unpack(x: dict[str, Any]) -> tuple[NDArray[Any], NDArray[intp]]

The occupied rows and their slot indices: (values[slots], slots).

sample

sample(mask: Any = None) -> dict[str, NDArray[Any]]

Sample a padded set.

mask: optional int8/bool array of shape (max_size,) fixing which slots are occupied; by default each slot is occupied with probability 1/2.

Utilities

Space utilities: flattening, batching, and (de)serialisation.

Every function here is a functools.singledispatch generic, so custom spaces can opt in by registering an implementation:

@sekai.spaces.flatdim.register(MySpace)
def _(space: MySpace) -> int: ...

flatdim

flatdim(space: Space[Any]) -> int

Return the number of dimensions when this space is flattened.

flatten

flatten(space: Space[Any], x: Any) -> NDArray[float32]

Flatten a sample from space into a 1D float32 array.

float32 holds integers exactly only up to 2**24 in magnitude, and rounds float64 values, so such samples do not survive a flatten/unflatten round trip exactly.

unflatten

unflatten(space: Space[Any], x: NDArray[Any]) -> Any

Unflatten a 1D array back into a valid sample from space.

The result is always contained in space: Box values are cast to its dtype (integers rounded) and clipped into its bounds, MultiBinary values are thresholded at 0.5, and (Multi)Discrete one-hots are read with argmax.

flatten_space

flatten_space(space: Space[Any]) -> Box

Return the float32 Box that flatten() maps samples of space into.

batch_space

batch_space(space: Space[Any], n: int) -> Space[Any]

Return the space of n stacked samples from space (leading axis of size n).

concatenate

concatenate(space: Space[Any], items: Sequence[Any]) -> Any

Stack per-env samples of space into one batched sample.

Array spaces stack along a new leading axis; Dict and Tuple recurse into their sub-spaces. Spaces without a registered rule return a plain list.

iterate

iterate(space: Space[Any], batch: Any) -> Iterator[Any]

Split a batched sample of space back into per-env samples (inverse of concatenate).

A list of per-env samples is also accepted and iterated as-is.

zero_sample

zero_sample(space: Space[Any]) -> Any

The neutral element of space: zeros clipped into bounds, the lowest discrete value, empty sequences and empty MaskedBox slots. Used for padding (frame stacks, delays).

space_from_jsonable

space_from_jsonable(data: Mapping[str, Any]) -> Space[Any]

Rebuild a space from the dict produced by its to_jsonable().

register_space_type

register_space_type(name: str, cls: type[Space[Any]]) -> None

Make a custom space reconstructable by space_from_jsonable().