Skip to content

Learned-policy results

Every environment ships a tuned PID and an MPC upper bound so that a learned policy has something real to beat. This page is the other half of that comparison, and the honest state of it is: the harness is in place and no results are published yet.

Why the numbers live in a file

Training costs GPU-hours, and the answer only moves when the environment or the agent moves, so learned results are recorded once and checked, like the baselines.

Results go in src/target_gym/data/rl_results.json, written through target_gym.rl_results.record_result, and the suite checks three things about each entry: that it is well formed, that it is scored over the environment's own episode so it sits in the same column as the PID and MPC numbers, and that it still describes the environment it claims to.

Nothing asserts that RL beats a PID. Whether it does is the question this library exists to ask, and a test that presumed the answer would be worthless.

What invalidates a result, and what does not

The guard is environment_fingerprint, which is deliberately narrower than the one the shipped baselines use. An agent never calls a PID, so re-tuning a controller must not throw away a training run that cost GPU-hours. A change to the dynamics, the reward, the parameters, the integrator or the shared reward helper must.

The agent's own configuration -- learning rate, network width, timesteps -- is stored verbatim and is deliberately not fingerprinted: two runs of the same agent at different hyperparameters are different results, not stale ones, and both are worth keeping. tag separates them.

The experimental design -- which agents, which hyperparameters, how many seeds, what the expert-based arm actually is -- is fixed in advance in the measurement protocol. It is written to be one a reader who wanted the opposite conclusion would accept.

Running the training

Training happens outside this package. The dependency runs one way -- from the RL library to here -- so installing TargetGym never drags in an RL framework, and there is no cycle to untangle.

Ajax is the intended runner. It is JAX-native, already depends on this package, and already handles the two things that would otherwise bias every number on these tasks: termination against truncation, and preserving the terminal observation at a time-limit truncation so the value bootstrap is correct. These episodes truncate at max_steps constantly, so getting that wrong would quietly distort every result.

# doc: skip -- the call shape; it needs a trained agent's returns to run
from target_gym.rl_results import record_result

record_result(
    env="plane",
    algorithm="SAC",
    library="ajax",
    library_version=ajax.__version__,
    returns=per_seed_episode_returns,   # undiscounted, one per seed
    episode_steps=280,                  # plane's scored episode
    total_timesteps=1_000_000,
    config={"n_envs": 64, "lr": 3e-4},
)

returns must be undiscounted episode returns measured the way the shipped baselines are. A normalised score or a training curve is not on the same axis as the numbers it would sit beside, and the suite rejects an episode longer than the environment's own.

Reading the result, when there is one

Before trusting a win or a loss, run the checks in How to read the result: cross-check a loss against a standard implementation, and check that a win was not against a defective baseline or a simulator exploit.