Migrating from gymnasium¶
sekai keeps gymnasium's mental model (environments, spaces, wrappers, a registry, vector envs), so most code ports line by line. This page maps the API, shows how to use sekai environments with gymnasium-based libraries (and the reverse), and lists the differences that change behaviour.
API mapping¶
| gymnasium | sekai |
|---|---|
gym.make("CartPole-v1") |
sekai.make("CartPole-v1") |
gym.make(id, render_mode="rgb_array"), env.render() |
renderer = CartPoleRenderer(), renderer.render(env) (Rendering) |
obs, info = env.reset(seed=0) |
result = env.reset(seed=0) → result.observation, result.info |
obs, reward, terminated, truncated, info = env.step(a) |
result = env.step(a) → result.observation, .reward, .terminated, .truncated, .info |
terminated or truncated |
result.done |
env.np_random |
env.rng |
super().reset(seed=seed) inside reset() |
self._set_rng(seed) |
self.np_random inside the env |
self.rng |
env.unwrapped |
env.unwrapped |
env.get_wrapper_attr(name) |
env.get_attr(name) |
gym.spaces.Box, Discrete, MultiDiscrete, MultiBinary, Dict, Tuple |
sekai.Box, sekai.Discrete, ... (same arguments) |
gym.spaces.Sequence(space, stack=False) |
sekai.Sequence(space, max_length=None, stack=False) |
| (none) | sekai.MaskedBox: a padded set with a validity mask (Variable-size observations) |
gym.spaces.utils.flatten(space, x) |
sekai.spaces.flatten(space, x) |
gym.register(...) / gym.spec(id) / gym.pprint_registry() |
sekai.register(...) / sekai.spec(id) / sekai.pprint_registry() |
gym.make_vec(id, n, vectorization_mode="sync") |
sekai.make_vec(id, n, vectorization_mode="sync") |
gym.make_vec(id, n, vectorization_mode="async") (subprocesses) |
sekai.make_vec(id, n, vectorization_mode="proc") |
gym.make_vec(id, n, vectorization_mode="vector_entry_point") |
sekai.make_vec(id, n, vectorization_mode="native") ("vector_entry_point" is accepted too); both are the default when the env registers one |
gym.register(id, ..., vector_entry_point=...), a gym.vector.VectorEnv subclass |
sekai.register(id, ..., vector_entry_point=...), a sekai.NativeVecEnv subclass |
gym.vector.SyncVectorEnv / AsyncVectorEnv |
sekai.SyncVecEnv / sekai.ProcVecEnv |
| (none) | sekai.AsyncVecEnv: asyncio, for I/O-bound envs |
envs.single_observation_space / envs.observation_space |
vec.observation_space / vec.batch_observation_space |
infos["episode"]["r"][i] (vector) |
infos[i]["episode"].episode_return |
gymnasium.utils.env_checker.check_env(env) (raises) |
sekai.check_env(env) (returns a list of issues) |
gym.wrappers.RecordVideo |
sekai.rendering.RecordFrames + save() |
an action_masks() method for sb3-contrib's MaskablePPO |
an action_mask() method, forwarded as action_masks() by to_gymnasium() |
PettingZoo ParallelEnv |
sekai.MultiAgentEnv, bridged with to_pettingzoo() / from_pettingzoo() |
Wrappers¶
Most wrappers exist under the same name. Argument names that differ:
| gymnasium | sekai |
|---|---|
Autoreset(env) |
AutoReset(env); also sets info["final_observation"] |
RecordEpisodeStatistics(env, buffer_length=100, stats_key="episode") |
RecordEpisodeStatistics(env, tracker=None, deque_size=100); info["episode"] is an EpisodeStats |
FrameStackObservation(env, stack_size, padding_type="reset") |
FrameStackObservation(env, num_stack, padding="reset"); padding="zeros" for gymnasium's "zero" (no custom padding value); any batchable space, Dict included |
TransformObservation(env, func, observation_space) |
TransformObservation(env, func, new_observation_space=None) |
ClipReward(env, min_reward=None, max_reward=None) |
ClipReward(env, min_reward=-inf, max_reward=inf); None also leaves a side unbounded, and the bounds are floats (no arrays) |
OrderEnforcing(env, disable_render_order_enforcing=False) |
OrderEnforcing(env) (sekai envs do not render) |
FilterObservation(env, filter_keys) |
FilterObservation(env, keys) |
TimeAwareObservation(env, flatten=True, normalize_time=False, dict_time_key="time") |
TimeAwareObservation(env, max_episode_steps=None, normalize_time=True, dict_key="time") |
DelayObservation(env, delay) |
DelayObservation(env, delay, padding="zeros") (or "reset") |
StickyAction(env, repeat_action_probability, repeat_action_duration=1) |
StickyAction(env, repeat_action_probability) |
MaxAndSkipObservation(env, skip=4) |
ActionRepeat(env, repeat) (sums rewards; no frame max-pooling) |
TimeLimit, ClipAction, RescaleAction, FlattenObservation, NormalizeObservation,
NormalizeReward, TransformReward(env, func), RescaleObservation and DtypeObservation
take the same arguments. (sekai 0.1 called the callable of TransformObservation and
TransformReward fn; fn= still works until sekai 0.3, with a DeprecationWarning.) sekai's
NormalizeObservation also accepts keyword-only stats=, halflife= and clip=, and
normalises Dict observations part by part. DelayAction, ActionMaskObservation and
PadSequenceObservation have no gymnasium counterpart; see Wrappers.
Side by side¶
The dynamics, seeding and spaces are identical, so both print the same return.
A custom environment:
import numpy as np
import sekai
class Drift(sekai.Env[np.ndarray, np.intp]):
def __init__(self):
self.observation_space = sekai.Box(-10.0, 10.0, shape=(1,))
self.action_space = sekai.Discrete(2)
def reset(self, *, seed=None, options=None):
self._set_rng(seed)
self.x = self.rng.normal()
return sekai.ResetResult(np.array([self.x], np.float32), {})
def step(self, action):
self.x += (1 if action else -1) * 0.5 + self.rng.normal(0.0, 0.1)
done = abs(self.x) > 5
obs = np.array([np.clip(self.x, -10, 10)], np.float32)
return sekai.StepResult(obs, -abs(self.x), done, False, {})
print(sekai.check_env(Drift())) # []
import gymnasium as gym
import numpy as np
class Drift(gym.Env):
def __init__(self):
self.observation_space = gym.spaces.Box(-10.0, 10.0, shape=(1,))
self.action_space = gym.spaces.Discrete(2)
def reset(self, *, seed=None, options=None):
super().reset(seed=seed)
self.x = self.np_random.normal()
return np.array([self.x], np.float32), {}
def step(self, action):
self.x += (1 if action else -1) * 0.5 + self.np_random.normal(0.0, 0.1)
done = abs(self.x) > 5
obs = np.array([np.clip(self.x, -10, 10)], np.float32)
return obs, -abs(self.x), done, False, {}
from gymnasium.utils.env_checker import check_env
check_env(Drift()) # raises on failure
A vectorised loop:
import numpy as np
import sekai
vec = sekai.VecRecordEpisodeStatistics(
sekai.make_vec("CartPole-v1", num_envs=4, vectorization_mode="sync")
)
actions = vec.batch_action_space
actions.seed(0)
result = vec.reset(seed=0)
for _ in range(100):
result = vec.step(actions.sample())
for i in np.flatnonzero(result.dones):
episode_return = result.infos[i]["episode"].episode_return
last_obs = result.infos[i]["final_observation"]
# result.observations[i] is already the first obs of the next episode
vec.close()
import gymnasium as gym
import numpy as np
envs = gym.wrappers.vector.RecordEpisodeStatistics(
gym.make_vec("CartPole-v1", num_envs=4, vectorization_mode="sync")
)
envs.action_space.seed(0)
obs, infos = envs.reset(seed=0)
for _ in range(100):
obs, rewards, terminations, truncations, infos = envs.step(envs.action_space.sample())
if "episode" in infos:
for i in np.flatnonzero(infos["_episode"]):
episode_return = infos["episode"]["r"][i]
# next-step autoreset (the default): on the following step, a finished sub-env
# returns its reset observation with reward 0 and ignores its action
envs.close()
Using sekai envs with gymnasium libraries¶
sekai.compat.to_gymnasium(env) wraps any sekai env (including a wrapper stack) as a
gymnasium.Env: spaces are converted, results become tuples, seeds pass through.
import sekai
from sekai.compat import to_gymnasium
gym_env = to_gymnasium(sekai.make("Portfolio-v0"))
print(type(gym_env).__name__, gym_env.action_space) # SekaiToGymnasium Box(0.0, 1.0, (6,), float32)
obs, info = gym_env.reset(seed=0)
print(sorted(obs)) # ['returns', 'weights']
from gymnasium.utils.env_checker import check_env as gym_check_env
gym_check_env(gym_env) # passes
Training with Stable-Baselines3¶
PPO on CartPole, through the bridge. 50k steps take about 30 seconds on a laptop CPU and reach a mean return near the maximum of 500:
from stable_baselines3 import PPO
from stable_baselines3.common.env_util import make_vec_env
import sekai
from sekai.compat import to_gymnasium
vec_env = make_vec_env(lambda: to_gymnasium(sekai.make("CartPole-v1")), n_envs=4, seed=0)
model = PPO("MlpPolicy", vec_env, verbose=0, seed=0)
model.learn(total_timesteps=50_000)
env = to_gymnasium(sekai.make("CartPole-v1"))
returns = []
for seed in range(10):
obs, _ = env.reset(seed=seed)
done, total = False, 0.0
while not done:
action, _ = model.predict(obs, deterministic=True)
obs, reward, terminated, truncated, _ = env.step(action)
total += float(reward)
done = terminated or truncated
returns.append(total)
print(f"PPO mean return over 10 episodes: {sum(returns) / len(returns):.1f}") # ~ PPO mean return over 10 episodes: 493.5
Dict observations, like Portfolio-v0's, need SB3's MultiInputPolicy. SB3's own
check_env accepts every finance environment. For most of them it only adds advice, such as
using a symmetric [-1, 1] action space, which RescaleAction provides. Market-v0 and
LimitOrderBook-v0 are the exception: their MaskedBox entry becomes a Dict
inside the observation Dict, and SB3 does not support nested observation spaces
(check_env warns about it, and MultiInputPolicy raises NotImplementedError). Flatten
those two first, as shown below.
import numpy as np
from stable_baselines3.common.env_checker import check_env as sb3_check_env
portfolio = sekai.make("Portfolio-v0", n_assets=3, episode_length=63)
env = to_gymnasium(sekai.RescaleAction(portfolio, -1.0, 1.0))
sb3_check_env(env) # passes (SB3 still suggests flattening the 2-D "returns" entry)
model = PPO("MultiInputPolicy", env, n_steps=512, batch_size=64, verbose=0, seed=0)
model.learn(total_timesteps=20_000)
def evaluate(policy, episodes=50):
totals = []
for seed in range(1000, 1000 + episodes):
obs, _ = env.reset(seed=seed)
done, total = False, 0.0
while not done:
obs, reward, terminated, truncated, _ = env.step(policy(obs))
total += reward
done = terminated or truncated
totals.append(total)
return float(np.mean(totals))
ppo = evaluate(lambda obs: model.predict(obs, deterministic=True)[0])
equal_weight = evaluate(lambda obs: np.ones(4, dtype=np.float32))
print(f"PPO {ppo:+.4f}, equal weight {equal_weight:+.4f}") # ~ PPO +0.0075, equal weight +0.0086
A 20k-step run is nowhere near converged; the closed-form and heuristic baselines on the finance page are the bar a learned policy has to clear.
Nested observations: flatten first¶
For Market-v0 and LimitOrderBook-v0, wrap the sekai env in sekai.FlattenObservation
before converting it, and train with MlpPolicy:
market = to_gymnasium(sekai.FlattenObservation(sekai.make("Market-v0")))
print(type(market.observation_space).__name__) # Box
model = PPO("MlpPolicy", market, verbose=0, seed=0)
model.learn(total_timesteps=2_048)
The flattened vector is the same one gymnasium's FlattenObservation builds from the
converted env, because to_gymnasium() keeps the Dict key order.
Action masks¶
When the sekai env has an action_mask() method, the gymnasium
env gets an action_masks() method returning the mask flattened to one boolean array, the
layout sb3-contrib's MaskablePPO expects. Wrappers forward action_mask(), so this still
works through FlattenObservation. LimitOrderBook-v0 masks orders that would breach its
inventory limit (pip install sb3-contrib):
from sb3_contrib import MaskablePPO
lob = to_gymnasium(sekai.FlattenObservation(sekai.make("LimitOrderBook-v0")))
lob.reset(seed=0)
print(lob.action_space, lob.action_masks().shape) # MultiDiscrete([6 6 3]) (15,)
model = MaskablePPO("MlpPolicy", lob, verbose=0, seed=0)
model.learn(total_timesteps=2_048) # every action it samples is allowed by the mask
Rendering¶
Pass a renderer to get gymnasium's render_mode="rgb_array", so tools like
gymnasium.wrappers.RecordVideo work:
from sekai.envs.classic_control import CartPoleRenderer
gym_env = to_gymnasium(sekai.make("CartPole-v1"), renderer=CartPoleRenderer())
gym_env.reset(seed=0)
print(gym_env.render_mode, gym_env.render().shape) # rgb_array (400, 600, 3)
print(gym_env.metadata) # {'render_modes': ['rgb_array'], 'render_fps': 50}
For a text renderer pass render_mode="ansi" as well. Other modes raise ValueError,
"human" included: sekai renderers return frames instead of opening a window. To watch the
frames, wrap the converted env in gymnasium.wrappers.HumanRendering (needs pygame).
Using gymnasium envs in sekai¶
sekai.compat.from_gymnasium(env) goes the other way, so any gymnasium environment (Atari,
MuJoCo, Box2D, your own) can use sekai's wrappers, vector envs, statistics and checker:
import gymnasium as gym
import sekai
from sekai.compat import from_gymnasium
env = from_gymnasium(gym.make("Acrobot-v1"))
print(env.observation_space, env.action_space) # Box([-1.0, -1.0, -1.0, -1.0, -12.566371, -28.274334], [1.0, 1.0, 1.0, 1.0, 12.566371, 28.274334], (6,), float32) Discrete(3)
print(sekai.check_env(env)) # []
vec = sekai.SyncVecEnv([lambda: from_gymnasium(gym.make("Acrobot-v1")) for _ in range(4)])
print(vec.reset(seed=0).observations.shape) # (4, 6)
vec.close()
The gymnasium env keeps its own generator: seeds given to reset() go straight to it and
env.rng is its np_random. gymnasium only takes int seeds, so a NumPy integer seed is
passed on as an int, and reset(seed=generator) keeps sekai's meaning by making that
Generator the gymnasium env's np_random. To render such an env, create it with gymnasium's
render_mode and use sekai.compat.GymnasiumRenderer, which calls the gymnasium env's own
render():
gymnasium's classic-control environments draw with pygame, which sekai[gymnasium] does not
install; pip install "gymnasium[classic-control]" adds it for this example.
from sekai.compat import GymnasiumRenderer
env = from_gymnasium(gym.make("CartPole-v1", render_mode="rgb_array"))
env.reset(seed=0)
print(GymnasiumRenderer().render(env).shape) # (400, 600, 3)
space_to_gymnasium() and space_from_gymnasium() convert individual spaces. A Dict
keeps its key order in both directions (a gymnasium Dict built from a plain dict sorts
its keys), so flattened observations line up on both sides. A Sequence becomes gymnasium's
Sequence, which has no length bound, so max_length is dropped; a MaskedBox becomes a
gymnasium Dict of values and mask, and comes back as a plain Dict:
from sekai.compat import space_to_gymnasium
print(space_to_gymnasium(sekai.Sequence(sekai.Box(0.0, 1.0, shape=(2,)), max_length=8))) # Sequence(Box(0.0, 1.0, (2,), float32), stack=False)
print(space_to_gymnasium(sekai.MaskedBox(0.0, 1.0, shape=(2,), max_size=8))) # Dict('values': Box(0.0, 1.0, (8, 2), float32), 'mask': MultiBinary((8,)))
Multi-agent: PettingZoo¶
The same bridge exists for multi-agent environments: sekai.compat.to_pettingzoo(env) exposes
a sekai.MultiAgentEnv as a PettingZoo ParallelEnv, and from_pettingzoo(parallel_env)
wraps a PettingZoo parallel environment as a MultiAgentEnv. Install it with
pip install "sekai[pettingzoo]"; see Multi-agent.
Differences that change behaviour¶
Vector autoreset is same-step
gymnasium ≥ 1.0 vector envs default to next-step autoreset. sekai resets a finished
sub-env inside the same step(): observations[i] already belongs to the new episode and
the terminal observation is in infos[i]["final_observation"]. Remove any code that skips
the dummy step after an episode ends, and bootstrap truncated episodes from
final_observation. See Autoreset.
vectorization_mode=\"async\" means asyncio
In gymnasium, "async" starts subprocesses. In sekai, "async" is AsyncVecEnv
(asyncio, one process) and "proc" is the subprocess backend.
Results are dataclasses
obs, reward, terminated, truncated, info = env.step(a) raises TypeError with sekai,
because StepResult is not iterable. Use the fields, or go through to_gymnasium()
when a library needs tuples.
No render mode on the env
sekai.make() takes no render_mode; any keyword argument goes to the environment's
constructor. Create a renderer instead.
Seeding details
Seeding an env works as in gymnasium (reset(seed=s) then unseeded reset()s continue the
stream). Vector envs differ: an int seed is expanded into independent child seeds rather
than seed + i. Pass a list for explicit per-env seeds.
Vector infos are lists
infos is a list with one dict per sub-env, not a dict of arrays with _key masks.
sekai.stack_infos(infos) gives a dict of arrays when you need one.