sekai.check_env(env) exercises an environment through the whole API and returns a list of
problems. It is a testing utility: call it from your test suite (or sekai check on the
command line), not in training code. Nothing is wrapped around your env, so it costs nothing
at training time; gymnasium, by contrast, wraps every make()d env in a PassiveEnvChecker.
import numpy as np
import sekai
print(sekai.check_env(sekai.make("Portfolio-v0"), check_determinism=True)) # []
Each issue is a string prefixed ERROR: (the env breaks the API) or WARNING: (it works,
but something is likely wrong). An empty list means every check passed.
| Argument | Default | Effect |
|---|---|---|
n_steps |
100 |
random-action steps to take (resetting whenever an episode ends); actions respect the env’s action mask, if it has one |
warn_on_anomaly |
True |
report WARNINGs; ERRORs are always reported |
check_obs_space |
True |
check observations against observation_space |
check_action_space |
True |
check action_space.sample() is in action_space |
check_reset_seed |
True |
check reset(seed=0) twice gives the same observation |
check_determinism |
False |
check two seeded rollouts with the same actions match |
check_async |
True |
exercise async_reset() / async_step() (skipped inside a running event loop) |
The env is reset and stepped, so pass a fresh instance.
| Area | Check | Level |
|---|---|---|
| Spaces | observation_space and action_space exist and are sekai.Space instances |
ERROR (stops) |
action_space.sample() is contained in action_space |
ERROR | |
observation_space.sample() is contained in observation_space |
ERROR | |
| Action masks | if the env or a wrapper defines action_mask(): action_space.sample(mask=env.action_mask()) does not raise after reset() |
ERROR (stops) |
and returns an action contained in action_space |
ERROR | |
| and does not raise in any state the random-action rollout reaches (e.g. every action masked) | ERROR (ends the rollout) | |
reset() |
reset(seed=42) does not raise |
ERROR (stops) |
it returns an object with .observation and .info |
ERROR (stops) | |
info is a dict |
ERROR | |
| Observations | every observation (from reset, step and the async methods) is in observation_space |
WARNING |
for a Box space, the observation’s dtype equals the space’s |
WARNING | |
step() |
step() does not raise |
ERROR (stops stepping) |
it returns an object with all five StepResult fields |
ERROR (stops stepping) | |
| a previously returned observation was not mutated in place (reported once) | WARNING | |
reward is a real number (not a bool), terminated / truncated are bool, info is a dict |
WARNING | |
| no NaN rewards; a count of infinite rewards | WARNING | |
| Seeding | reset(seed=0) twice returns equal observations |
WARNING |
| Determinism | two rollouts from reset(seed=123) with the same (up to 50) actions match exactly |
WARNING |
| Async | async_reset(seed=7) and one async_step() return valid results and do not raise |
ERROR / WARNING |
Per-step problems are reported once each: the first occurrence, plus how many steps showed
it, e.g. ... has dtype float64, space has float32. (seen at 500 steps; first shown).
A sloppy environment: it ignores the seed and mutates the array it returned on the previous step.
class SloppyEnv(sekai.Env):
def __init__(self):
self.observation_space = sekai.Box(-1.0, 1.0, shape=(3,), dtype=np.float32)
self.action_space = sekai.Discrete(2)
def reset(self, *, seed=None, options=None):
self.state = np.random.uniform(-0.1, 0.1, size=3).astype(np.float32) # ignores seed
return sekai.ResetResult(self.state, {})
def step(self, action):
self.state += 0.01 if action else -0.01 # mutates the array returned last step
return sekai.StepResult(self.state, 1.0, False, False, {})
for issue in sekai.check_env(SloppyEnv(), n_steps=50):
print(issue)
WARNING: step() mutated a previously returned observation in place. Return a copy (e.g. self._state.copy()) so stored transitions are not corrupted.
WARNING: reset(seed=0) returned different observations on two calls. Call self._set_rng(seed) at the start of reset() and draw all randomness from self.rng.
A broken one: its action space promises an action it cannot handle. check_env samples
actions from action_space, so seed the space to make the report reproducible.
class BrokenEnv(SloppyEnv):
PUSH = {0: -0.01, 1: 0.01}
def __init__(self):
super().__init__()
self.action_space = sekai.Discrete(3) # but only 0 and 1 are handled
def reset(self, *, seed=None, options=None):
self._set_rng(seed)
self.state = self.rng.uniform(-0.1, 0.1, size=3).astype(np.float32)
return sekai.ResetResult(self.state.copy(), {})
def step(self, action):
self.state = self.state + self.PUSH[int(action)]
return sekai.StepResult(self.state.copy(), 1.0, False, False, {})
env = BrokenEnv()
env.action_space.seed(0)
print(sekai.check_env(env)) # ['ERROR: step() raised KeyError at step 8: 2']
check_env respects action masks. If the env (or any wrapper around it) has an
action_mask() method, every random action, in the rollouts and in the async check, is
sampled with the current mask, so actions the env declares invalid are never tried. Declaring
that action 2 is not available makes the same env pass:
class MaskedEnv(BrokenEnv):
def action_mask(self):
return np.array([1, 1, 0], dtype=np.int8) # action 2 is never valid here
env = MaskedEnv()
env.action_space.seed(0)
print(sekai.check_env(env)) # []
A mask in the wrong format (wrong shape, every action masked), after reset() or in any state
the rollout reaches, is reported as an ERROR naming the step, and ends the rollout.
import pytest
@pytest.mark.parametrize("env_id", ["CartPole-v1", "Trading-v0", "Portfolio-v0"])
def test_env_api(env_id):
issues = sekai.check_env(sekai.make(env_id), n_steps=500, check_determinism=True)
assert not issues, "\n".join(issues)
test_env_api("Portfolio-v0")
For envs that are stochastic beyond their seed (a live feed), leave check_determinism off
and register them with nondeterministic=True to document it.
sekai check ID runs the same checks on a registered environment (500 steps by default) and
exits non-zero on errors, or on any issue with --strict:
$ sekai check CartPole-v1 --determinism
CartPole-v1: all checks passed.
See Command line.