Skip to content

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

skyfall-crl run --config experiment.yaml --plugin windy.py
skyfall-crl eval --traces traces.jsonl
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.