Your own environment¶
The first run's table was constants because nothing changed the world. This rung fixes that with
one small file: an environment of your own whose conditions genuinely shift while the policy
learns. The pattern — a factory in a file, named by the configuration, loaded with --plugin —
is exactly how any environment of yours plugs in, whether it wraps CartPole or your own
simulator.
One file: a world that shifts¶
# file: windy.py
"""CartPole whose gravity shifts on a fixed period, with a penalty for dropping the pole."""
import gymnasium as gym
class Windy(gym.Wrapper):
def __init__(self, period: int = 200, calm: float = 9.8, windy: float = 18.0):
super().__init__(gym.make("CartPole-v1"))
self._period, self._calm, self._windy = int(period), float(calm), float(windy)
self._count = 0
def _apply(self) -> None:
phase = (self._count // self._period) % 3 # calm, windy, calm again
self.unwrapped.gravity = self._windy if phase == 1 else self._calm
def reset(self, *, seed=None, options=None):
self._apply()
return super().reset(seed=seed, options=options)
def step(self, action):
self._apply()
self._count += 1
observation, reward, terminated, truncated, info = super().step(action)
if terminated:
reward -= 5.0
return observation, reward, terminated, truncated, info
def make(**kwargs):
return Windy(**kwargs)
Two changes to stock CartPole, each there for a reason. Gravity nearly doubles every period
steps and returns — the parameter drift form of non-stationarity, applied by the environment
itself. And a fall now costs 5, so the reward distinguishes holding the pole from dropping it;
without that, CartPole pays 1 per step no matter what, which is what flattened the last rung's
table.
The configuration names it¶
# file: experiment.yaml
env: {id: "windy:make", kwargs: {period: 200}}
algorithm: {name: discrete_hill_climbing}
rollout: {window_steps: 100, total_steps: 600, auto_reset: true}
regime:
provider: scheduled
params:
schedule:
regimes:
- {regime_id: calm, duration_steps: 200}
- {regime_id: windy, duration_steps: 200}
- {regime_id: calm_again, alias_of: calm, duration_steps: 200}
run: {name: windy-run, seed: 0, trace_path: traces.jsonl}
env.id is a dotted module:factory; --plugin windy.py below imports the file so the name
resolves. One duplication to notice honestly: the environment shifts every 200 steps and the
schedule declares 200-step intervals — the same number, stated twice, and keeping them aligned
is your job at this tier. The next rung removes the duplication by letting the environment label
its own steps.
Run and score¶
1 trace(s), 3 configuration(s)
Metric segment
----------------------------------
Per-configuration reward 0.791667
Adaptation speed 1
Forgetting -0.175
Recovery time 1
Stability (variance) 0.993928
Performance gap --
Zero-shot reward 0.75
Regret --
Effective rank --
Settled reward 0.75
Return (discounted) 83.3021
Return (undiscounted) 475
Invalid-action rate 0
The constants are gone. Falls under the doubled gravity cost real reward (0.79 per step overall, variance 0.99), and forgetting reports −0.175: the return to calm conditions went better than the first visit. The per-regime sections tell it interval by interval:
Configuration 'windy'
Metric segment
---------------------------------
Per-configuration reward 0.7
Adaptation speed 1
Forgetting --
Recovery time 1
Stability (variance) 1.41709
...
windy is the hardest interval — the lowest reward and the highest variance in the run — and it
reports no forgetting because its conditions never recur. Note what still says 1: adaptation
and recovery. This reward is still nearly binary (upright pays 1, a fall pays −4 once), so at
step granularity the running mean recovers almost immediately after a shift and there is nothing
for those two metrics to resolve. A world that genuinely changes was half the fix; a reward that
can measure quality is the other half, and that is the next rung.
What Tier 1 is¶
What you just built is a Tier-1 environment in the
contract's terms: any
gymnasium.Env, supplying its own reward. Tier 1 already gets the harness, the scheduling, the
tracing, and every metric a self-scored reward can support. What it cannot get is the composable
reward system or a derived ceiling — which is why the performance gap stayed -- — because both
need the environment to describe its steps rather than just score them.