Genie and Interactive Generated Environments › Evaluation of Control and Consistency
World Models HandbookAll chapters Genie and Interactive Generated Environments › Evaluation of Control and ConsistencyOne section · Sections in this chapter5 / 9 ↓
Reading tools Definitions & inline links
Choose your expertise level to adjust how many terms are explained. Beginners see more tooltips, experts see fewer to maintain reading flow. Hover over underlined terms for instant definitions.
Article links
Make inline references clickable
Evaluating an open-ended generated environment is harder when no reference simulator covers its possible action branches. Where a real environment and fixed action sequence exist, as in our toy, we can measure MSE against the matching reference rollout. We also use behavioral proxies because pixel similarity alone misses control and persistence. This chapter computes six measures:
- Action adherence. Given a requested action, does the model's prediction reflect that action in a way consistent with the environment's mechanics? In our grid world, we can check whether the agent in the predicted frame ends up in the position that the true environment would produce.
- One-step transition error. How close is a single model prediction to the true next frame, when the input is a real frame and a real action? This is the easiest metric to compute and the least informative about interactive quality.
- Multi-step rollout error. How close is a sequence of model-generated frames to the true sequence under the same action sequence? This captures compounding drift.
- Branch separation. How different are rollouts from the same start state under different actions? This captures action responsiveness.
- Revisit consistency. If the agent leaves a state and returns to it, does the model predict the same observation? This captures the model's ability to maintain persistent structure.
- Downstream control. Does a specified planner reach a goal when it selects actions using the model's predictions? This tests the predictor and planner together, not the model in isolation.
No single one of these proxies is sufficient. Action adherence can be inflated by an action-agnostic predictor of a globally common outcome, especially when no-ops dominate. One-step error can be low for a model that ignores actions entirely for the same reason. Multi-step rollout error can be dominated by drift. Branch separation can be produced by noise. Revisit consistency can be satisfied by a model that outputs a constant. The proxies are useful only together, and even together they do not fully characterize playability. They are a practical compromise: cheap to compute, interpretable, and correlated with the properties that matter.
We already computed one-step error, multi-step rollout error, and branch separation. Let us add the other three.
def predicted_position(frame):
r, c = np.unravel_index(np.argmax(frame), frame.shape)
return (r, c)
def action_adherence_rate(model, obs_seq, act_seq, use_perm):
hits = 0
total = 0
for t in range(len(act_seq)):
obs = obs_seq[t]
a = act_seq[t]
expected_next = obs_seq[t + 1]
a_idx = action_index[a]
if use_perm:
a_idx = action_perm[a_idx]
x = np.concatenate(
[obs.reshape(1, -1), one_hot(np.array([a_idx]))], axis=1
)
pred = model.predict(x).reshape(obs.shape)
if predicted_position(pred) == predicted_position(expected_next):
hits += 1
total += 1
return hits / total
adherence_true = np.mean(
[
action_adherence_rate(model_true, obs_seq, act_seq, False)
for obs_seq, act_seq in zip(test_obs_seq, test_act_seq)
]
)
adherence_latent = np.mean(
[
action_adherence_rate(model_latent, obs_seq, act_seq, True)
for obs_seq, act_seq in zip(test_obs_seq, test_act_seq)
]
)Revisit consistency asks a slightly different question. Suppose the agent moves away from a cell and comes back. Does the model put the agent back in the same cell? We test this with a short sequence that leaves the start position and returns to it. Formally, if the sequence of actions maps the true state back to the starting position , then a revisiting-consistent model should predict an observation close to after feeding its own outputs back k times. The MSE we compute next measures how far the final prediction drifts from the original observation.
Revisit consistency is a minimal test of persistence, not proof of scene memory. Our selected action sequences return the true agent to its start; we compare each model's final recursive prediction with the initial frame. A constant-output model could score well on this comparison while failing action adherence and branch tests, so the measures belong together. The persistence baseline is exactly zero by construction and is not the best interactive model.
revisit_starts = [(1, 5), (3, 2), (6, 5), (0, 3), (0, 0)]
seq_forward = ["right", "right", "left", "left"]
revisit_true = []
revisit_latent = []
revisit_persist = []
for start_pos in revisit_starts:
initial = render(start_pos)
pred_true = rollout_model(model_true, initial, seq_forward, False)[-1]
pred_latent = rollout_model(model_latent, initial, seq_forward, True)[-1]
revisit_true.append(np.mean((pred_true - initial) ** 2))
revisit_latent.append(np.mean((pred_latent - initial) ** 2))
revisit_persist.append(np.mean((initial - initial) ** 2))
revisit_true = float(np.mean(revisit_true))
revisit_latent = float(np.mean(revisit_latent))
revisit_persist = float(np.mean(revisit_persist))Finally, we test downstream control. Give the model a target cell, use it to plan a sequence of actions greedily (each step, pick the action whose predicted next frame is closest to the target), execute those actions in the true environment, and check whether the agent actually reaches the target. Formally, for a start state and target , the planner selects at each step the action that minimizes the Manhattan distance between the predicted position and . This is a crude but honest test of whether the model is useful for control, not just for prediction.
The planner is greedy: at each real environment state it rerenders the true observation, scores four one-step model predictions, executes the selected action, and repeats. Thus it does replan after each step, but has no multi-step lookahead and is given privileged access to the true current state. A poor result may reflect model error, the greedy rule, target geometry, or the small trial set; it does not prove a stronger planner would fail. The test asks only whether this particular predictor-plus-controller reaches a target within 20 steps.
def plan_and_execute(model, start_pos, target_pos, use_perm):
pos = start_pos
for _ in range(20):
if pos == target_pos:
return True
obs = render(pos)
best_action, best_dist = None, np.inf
for a in ACTION_NAMES:
if model is None: # oracle dynamics, same greedy rule
pred = render(step(pos, a))
else:
a_idx = action_index[a]
if use_perm:
a_idx = action_perm[a_idx]
x = np.concatenate(
[obs.reshape(1, -1), one_hot(np.array([a_idx]))], axis=1
)
pred = model.predict(x).reshape(obs.shape)
r, c = predicted_position(pred)
d = abs(r - target_pos[0]) + abs(c - target_pos[1])
if d < best_dist:
best_dist = d
best_action = a
pos = step(pos, best_action)
return pos == target_pos
# The control-success values below are computed once and reused by the
# downstream figure cell.
rng_control = np.random.default_rng(2)
n_trials = 15
control_true = 0
control_latent = 0
control_oracle = 0
for _ in range(n_trials):
start = (int(rng_control.integers(GRID)), int(rng_control.integers(GRID)))
while start in WALLS:
start = (
int(rng_control.integers(GRID)),
int(rng_control.integers(GRID)),
)
target = (int(rng_control.integers(GRID)), int(rng_control.integers(GRID)))
while target in WALLS or target == start:
target = (
int(rng_control.integers(GRID)),
int(rng_control.integers(GRID)),
)
if plan_and_execute(model_true, start, target, False):
control_true += 1
if plan_and_execute(model_latent, start, target, True):
control_latent += 1
if plan_and_execute(None, start, target, False):
control_oracle += 1
control_true /= n_trials
control_latent /= n_trials
control_oracle /= n_trialsThe grouped bars compare action adherence and downstream control. In this seeded run, adherence is about 0.57 for the true-action predictor and 0.52 for the calibrated latent-code predictor. Each learned-model controller reaches one of 15 targets. The same greedy rule with exact one-step simulator dynamics reaches 10 of 15. Greedy search and walls still limit that oracle. On these trials, the learned predictors add a substantial failure source beyond the planner.

The corner exposes a local identifiability limit. At , both "up" and "left" produce no movement. A frame-pair-only inverse model cannot determine which was pressed from that transition. The labeled predictor still receives different action inputs and may output slightly different frames, so the next diagnostic measures its learned prediction gap rather than a proof of perfect ambiguity.
corner_obs = render((0, 0))
noop_up = rollout_model(model_true, corner_obs, ["up"], False)[-1]
noop_left = rollout_model(model_true, corner_obs, ["left"], False)[-1]
noop_gap = float(np.mean((noop_up - noop_left) ** 2))Action adherence rate (predicted next cell matches true next cell): True-action model: 0.569 Latent-action model: 0.516 Revisit consistency MSE (lower is better): Persistence: 0.0000 True-action model: 0.0150 Latent-action model: 0.0148 Downstream control success rate: True-action model: 0.07 Latent-action model: 0.07 Oracle dynamics: 0.67 Corner no-op gap (MSE between 'up' and 'left' predictions): 0.0008
At , both "up" and "left" are blocked and have the same true next frame. No frame-pair-only inverse model can recover which of those buttons was pressed from that single transition. The supervised true-action predictor can still receive distinct button labels, and a finite fitted model may give slightly different outputs for them; the measured gap is therefore not guaranteed to be zero. The ambiguity is about inferring the cause from the observed pair, not about whether a labeled predictor can distinguish its inputs.
This is a limit on recovering the pressed button from that frame pair, not a claim that two buttons are globally indistinguishable. They can have different effects elsewhere, and a supervised predictor can receive their labels. More subtle data-coverage problems arise when rare state–action interactions are underrepresented. Latent-action discovery cannot infer a distinction that never appears in its observations; evaluation should separate local no-op ambiguity from global action responsiveness.
We can quantify the identifiability gap by comparing the corner with a free cell where the two actions have different effects.
free_obs = render((1, 1))
free_up = rollout_model(model_true, free_obs, ["up"], False)[-1]
free_down = rollout_model(model_true, free_obs, ["down"], False)[-1]
free_gap = float(np.mean((free_up - free_down) ** 2))

This heatmap makes two distinct ambiguities visible. The numeric code labels are arbitrary and could be permuted without changing the clustering. More importantly, the mixed row is not an arbitrary relabeling of one button: blocked actions from several buttons produce the same zero displacement, so their intent cannot be recovered from the observed frame pair. The clustering uses displacements computed from the two frames, not the button labels. Evaluation must distinguish arbitrary code identity from the information the observation actually contains.
The heatmap also makes the no-op absorption visible. If a row has counts spread across several true actions, that latent code is not cleanly aligned with a single direction. That can happen because the code is absorbing no-op transitions, or because the clustering has merged two similar directions. In a real latent-action model, this kind of analysis is harder because the codes are not directly interpretable as directions, but the same principle applies. You want to know which codes are well used, which are collapsed, and which correspond to mixtures of changes. The confusion matrix is a simple tool for asking those questions in a setting where you happen to have the true labels.