Interpretability and World-Model Debugging › A Deterministic CPU Laboratory

Michael BrenndoerferAugust 5, 202625 min read
World Models HandbookAll chapters
Part 11 of 12Evaluation and Understanding
Chapter 66Interpretability and World-Model Debugging › A Deterministic CPU Laboratory
Reading section 7 of 10 · A Deterministic CPU Laboratory · 25 minSection 7 of 10 · 25 min

Book contents

Section 7 of 10 · 25 min read

Interpretability and World-Model Debugging › A Deterministic CPU LaboratoryOne section · Sections in this chapter7 / 10 ↓
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

We build a reproducible CPU laboratory with NumPy and Matplotlib. Closed-form point-mass dynamics provide a reference state at each step, letting us compare estimates and predictions without fitting another reference model. These states are exact relative to the simulated model and floating-point implementation. On a real system, a simulator or measurement reference introduces its own errors and information limits. The code retains the arrays needed for the demonstrated comparisons; a production diagnostic would also implement the logging protocol above.

The System, the Observation, and the Representation

The true state is st=(pt,vt)s_t = (p_t, v_t): position and velocity. The dynamics are the exact constant-acceleration discretization of a point mass.

In[3]:
Code
import matplotlib.pyplot as plt
import numpy as np

DT = 0.2  # seconds per step
HORIZON = 30  # steps per episode
A_MAX = 1.0  # acceleration limit, m/s^2
SIGMA_O = 0.01  # observation noise standard deviation, m
N_TRAIN, N_VAL, N_TEST = 30, 15, 15
N_EPISODES = N_TRAIN + N_VAL + N_TEST


def rollout_true(state, actions, dt=DT):
    """Exact constant-acceleration discretization of a point mass."""
    p, v = state
    p_hist, v_hist = [p], [v]
    for a in actions:
        p = p + v * dt + 0.5 * a * dt * dt
        v = v + a * dt
        p_hist.append(p)
        v_hist.append(v)
    return np.array(p_hist), np.array(v_hist)

The observation channel exposes position only, with additive Gaussian noise. Velocity is not observed directly; it must be inferred from history. Retaining observation noise lets us examine its contribution to velocity-channel error; valid downstream conclusions still depend on the stated diagnostic protocol.

In[4]:
Code
def make_episode(seed, horizon=HORIZON):
    rng = np.random.default_rng(seed)
    p0 = rng.uniform(-1.0, 1.0)
    v0 = rng.uniform(-0.5, 0.5)
    actions = rng.uniform(-A_MAX, A_MAX, size=horizon)
    p_true, v_true = rollout_true((p0, v0), actions)
    obs = p_true + rng.normal(0.0, SIGMA_O, size=p_true.shape)
    return {"p": p_true, "v": v_true, "a": actions, "obs": obs}


episodes = [make_episode(1000 + i) for i in range(N_EPISODES)]
train_eps = episodes[:N_TRAIN]
val_eps = episodes[N_TRAIN : N_TRAIN + N_VAL]
test_eps = episodes[N_TRAIN + N_VAL :]

Each episode uses a separate fixed pseudorandom seed for reproducibility. The streams are intended to emulate independent draws; unequal seeds alone do not prove statistical independence. Time is in seconds, position in meters, velocity in meters per second, and acceleration in meters per second squared. A step applies constant acceleration, integrates, then observes position. Episode cost sums 30 undiscounted post-transition costs. Truncation omits later costs rather than increasing the coefficients on earlier costs.

These laboratory outputs were reproduced with Python 3.11.14 and NumPy 2.4.2, using the PCG64 generators created by default_rng. Exact sample values describe that execution, not a cross-version or cross-platform guarantee. Record the bit generator, seed schedule, RNG call sequence and arguments, build, and execution environment when reproducing them.

The representation is handcrafted. It estimates position directly from the observation and estimates velocity by finite differencing consecutive observations.

In[5]:
Code
def encode(obs, dt=DT):
    """Handcrafted 2-D representation: (position estimate, finite-difference velocity estimate).

    Without observation noise, the finite difference equals the interval's
    midpoint velocity (v[t-1] + v[t]) / 2 under constant acceleration.
    Observation noise adds (epsilon[t] - epsilon[t-1]) / dt.
    """
    T = len(obs)
    z = np.zeros((T, 2))
    z[:, 0] = obs
    z[1:, 1] = (obs[1:] - obs[:-1]) / dt
    return z

This is a diagnostic toy, not a discovered neural representation. Its velocity channel uses observation history. Without noise, it measures average velocity over the preceding interval, which equals the midpoint velocity under this toy's constant-acceleration update. It differs from the current endpoint velocity when that interval's acceleration is nonzero; the two coincide when acceleration is zero. Keeping this handcrafted representation fixed lets us vary downstream components without also retraining an encoder.

For t≥1t\geq1, the channel equals vt−1+12at−1Δt+(ϵt−ϵt−1)/Δtv_{t-1}+\tfrac12 a_{t-1}\Delta t+(\epsilon_t-\epsilon_{t-1})/\Delta t, where ϵt\epsilon_t is position-observation noise. Independent observation noises with standard deviation σo\sigma_o give differenced-noise standard deviation 2σo/Δt\sqrt{2}\sigma_o/\Delta t. The channel therefore combines interval averaging with amplified observation noise. Inspect it before trusting a downstream readout.

In[6]:
Code
_channel_episode = test_eps[0]
_z_channel = encode(_channel_episode["obs"])
_channel_time = np.arange(len(_z_channel)) * DT
_channel_v_true = _channel_episode["v"]
_channel_v_est = _z_channel[:, 1].copy()
_channel_v_est[0] = np.nan  # undefined at step 0 by construction
Out[7]:
Visualization
Line chart comparing true and estimated velocity from finite differencing over time.
Finite-difference velocity estimate versus true endpoint velocity over one test episode. The noiseless estimate equals the preceding interval's midpoint velocity under constant acceleration; observation noise adds scatter. Dotted references lie plus or minus three times sqrt(2) sigma_o/dt around current true velocity, showing the differenced-noise scale. They are not coverage bounds for the lagged channel because their center is the endpoint velocity rather than the midpoint. A successful probe does not make the raw channel a clean measurement.

Interval averaging follows from the finite-difference estimator. Noise originates in the observation channel and is amplified by differencing. A trained probe can partly compensate for these errors, so predictive recovery and raw-channel measurement accuracy are different quantities.

Probing the Representation

We fit a ridge probe to recover true velocity from the representation. The probe uses episode-grouped splits: no episode appears in more than one split. We group by episode to target unseen-episode generalization. Mixing frames from the same episode across partitions can expose shared history and, for adjacent finite-difference rows, shared observation noise across the fitting boundary.

In[8]:
Code
def build_probe_data(eps, dt=DT):
    Zs, Ys = [], []
    for ep in eps:
        z = encode(ep["obs"], dt=dt)
        Zs.append(z[1:])  # drop step 0, where the velocity channel is undefined
        Ys.append(ep["v"][1:])  # probe target: true velocity
    return np.concatenate(Zs, axis=0), np.concatenate(Ys, axis=0)


Z_tr, Y_tr = build_probe_data(train_eps)
Z_va, Y_va = build_probe_data(val_eps)
Z_te, Y_te = build_probe_data(test_eps)

Preprocessing statistics come from training only, and the ridge penalty is selected on validation.

In[9]:
Code
def standardize_fit(X):
    mu = X.mean(axis=0)
    sd = X.std(axis=0)
    sd = np.where(sd < 1e-8, 1.0, sd)
    return mu, sd


def standardize_apply(X, mu, sd):
    return (X - mu) / sd


mu_tr, sd_tr = standardize_fit(Z_tr)
Ztr_s = standardize_apply(Z_tr, mu_tr, sd_tr)
Zva_s = standardize_apply(Z_va, mu_tr, sd_tr)
Zte_s = standardize_apply(Z_te, mu_tr, sd_tr)


def ridge_fit(X, y, alpha):
    n, d = X.shape
    Xb = np.hstack([X, np.ones((n, 1))])
    A = Xb.T @ Xb + alpha * np.eye(d + 1)
    A[-1, -1] -= alpha  # do not penalize the intercept
    return np.linalg.solve(A, Xb.T @ y)


def ridge_predict(X, w):
    Xb = np.hstack([X, np.ones((X.shape[0], 1))])
    return Xb @ w


def r2_score(y, yhat):
    ss_res = np.sum((y - yhat) ** 2)
    ss_tot = np.sum((y - y.mean()) ** 2)
    return 1.0 - ss_res / ss_tot
In[10]:
Code
alphas = [1e-4, 1e-3, 1e-2, 1e-1, 1.0, 10.0]
val_scores = [
    r2_score(Y_va, ridge_predict(Zva_s, ridge_fit(Ztr_s, Y_tr, a)))
    for a in alphas
]
best_alpha = alphas[int(np.argmax(val_scores))]
w_probe = ridge_fit(Ztr_s, Y_tr, best_alpha)

We evaluate an intercept-only baseline, a training-target shuffled refit, the selected linear probe, and a probe on one orthogonally rotated representation. The baseline is the training-target mean. The shuffle supplies one randomized-label comparison, not a definitive structure-versus-noise classifier. The rotation tests this particular transform, not all invertible transformations.

In[11]:
Code
r2_intercept = r2_score(Y_te, np.full_like(Y_te, Y_tr.mean()))
r2_probe = r2_score(Y_te, ridge_predict(Zte_s, w_probe))

# Capacity-matched control: shuffle the training targets only, then refit the same probe.
shuffle_rng = np.random.default_rng(7)
Y_tr_shuffled = shuffle_rng.permutation(Y_tr)
w_shuffled = ridge_fit(Ztr_s, Y_tr_shuffled, best_alpha)
r2_shuffled = r2_score(Y_te, ridge_predict(Zte_s, w_shuffled))

# One orthogonal rotation; this does not test every invertible transform.
theta = np.pi / 4
R = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
Ztr_rot = Ztr_s @ R.T
Zte_rot = Zte_s @ R.T
w_rot = ridge_fit(Ztr_rot, Y_tr, best_alpha)
r2_rot = r2_score(Y_te, ridge_predict(Zte_rot, w_rot))

probe_results = {
    "Intercept-only": r2_intercept,
    "Shuffled control": r2_shuffled,
    "Linear probe": r2_probe,
    "Rotated probe": r2_rot,
}
Out[12]:
Console
      Intercept-only: held-out R^2 = -0.378
    Shuffled control: held-out R^2 = -0.456
        Linear probe: held-out R^2 =  0.954
       Rotated probe: held-out R^2 =  0.954

Best ridge penalty selected on validation: 10.0
Probe gap over shuffled control: 1.410
Rotation gap (rotated - linear):   0.000
Out[13]:
Visualization
Bar chart of held-out R-squared for intercept-only, shuffled control, linear probe, and rotated probe.
Held-out R-squared for four probe variants on the frozen handcrafted representation. The linear and rotated probes score about 0.954; the shuffled-target control scores about -0.456, below the intercept-only baseline of about -0.378. The shuffled control does not recover velocity on these held-out episodes. The tested invertible rotation preserves recovery; this is evidence of linear accessibility, not sufficiency or downstream causal use.

Read the result carefully. The linear probe recovers velocity well above the intercept-only baseline on these held-out episodes. This does not establish a sufficient Markov state, downstream use of velocity, or absence of other information. The shuffled-target control scores about −0.456-0.456, below the intercept-only score of about −0.378-0.378; the scores are not equal. The linear and rotated probes both score about 0.9540.954 to numerical precision in this experiment. Invertible linear coordinate changes preserve the class of linear readouts, although fitting procedures with regularization need not be invariant to every such transformation. What changes under rotation is the interpretation of individual coordinates: velocity can remain linearly accessible without occupying the same coordinate.

Decodability Without Use

We add a diagnostic coordinate: a deterministic sine function of observed position. It is redundant given the existing position coordinate and can correlate with control-relevant position; it is not information-free. The controller interface explicitly discards it, allowing a designed decodability-without-use comparison.

In[14]:
Code
def encode_ext(obs, dt=DT):
    z = np.zeros((len(obs), 3))
    z[:, :2] = encode(obs, dt=dt)
    # Redundant sine of observed position; discarded at the controller interface.
    z[:, 2] = np.sin(5.0 * obs)
    return z


Zext_tr = np.vstack([encode_ext(ep["obs"]) for ep in train_eps])
Zext_te = np.vstack([encode_ext(ep["obs"]) for ep in test_eps])
nuis_tr = Zext_tr[:, 2]
nuis_te = Zext_te[:, 2]

mu_n, sd_n = standardize_fit(Zext_tr)
w_nuis = ridge_fit(standardize_apply(Zext_tr, mu_n, sd_n), nuis_tr, best_alpha)
r2_nuis = r2_score(
    nuis_te, ridge_predict(standardize_apply(Zext_te, mu_n, sd_n), w_nuis)
)

Now the matched pair: two states with identical meaningful coordinates (p,v)(p, v) but different nuisance values.

In[15]:
Code
def mpc_action(z, transition_fn, horizon, n_samples, a_max, rng, cost_weights):
    q_p, q_v, q_a = cost_weights
    best_cost = np.inf
    best_a = 0.0
    for _ in range(n_samples):
        actions = rng.uniform(-a_max, a_max, size=horizon)
        state = np.array(z, dtype=float)
        cost = 0.0
        for a in actions:
            state = transition_fn(state, a)
            cost += q_p * state[0] ** 2 + q_v * state[1] ** 2 + q_a * a**2
        if cost < best_cost:
            best_cost = cost
            best_a = actions[0]
    return best_a


COST_WEIGHTS = (1.0, 0.1, 0.01)


def transition_true(state, a, dt=DT):
    p, v = state
    return np.array([p + v * dt + 0.5 * a * dt * dt, v + a * dt])


z_pair_a = np.array([0.30, 0.10, 0.0])
z_pair_b = np.array([0.30, 0.10, 1.0])
a_a = mpc_action(
    z_pair_a[:2],
    transition_true,
    10,
    64,
    A_MAX,
    np.random.default_rng(77),
    COST_WEIGHTS,
)
a_b = mpc_action(
    z_pair_b[:2],
    transition_true,
    10,
    64,
    A_MAX,
    np.random.default_rng(77),
    COST_WEIGHTS,
)
Out[16]:
Console
Nuisance probe held-out R^2:       1.000
Action with nuisance value 0.0:   -0.826301
Action with nuisance value 1.0:   -0.826301
Action difference:                 0.000000e+00

The nuisance probe recovers its target essentially perfectly (held-out R^2 ~= 0.9999), because the coordinate is present verbatim in the representation and the probe is fitting the identity map. The controller's action is identical across the matched pair, because the controller reads only the first two coordinates. The pair illustrates the indifference already established by that input slice. A probe that recovers a variable tells you the variable is accessible, not that the pipeline uses it; inspect or test the consumer separately.

The two plotted diagnostics have different definitions: held-out R2R^2 for the readout, and the empirical action standard deviation (normalized by the number of sweep values, ddof=0) divided by the absolute empirical mean plus 10−1210^{-12}. They are not interchangeable or universally bounded between zero and one. In this designed no-op sweep, every controller input and candidate seed is identical, making the action coefficient of variation zero.

In[17]:
Code
nuisance_values = np.linspace(0.0, 1.0, 8)
z_fixed = np.array([0.30, 0.10])  # meaningful coordinates held constant
nuisance_actions = np.array(
    [
        mpc_action(
            z_fixed,
            transition_true,
            10,
            64,
            A_MAX,
            np.random.default_rng(77),
            COST_WEIGHTS,
        )
        for _ in nuisance_values
    ]
)
action_spread = nuisance_actions.std()
action_scale = abs(nuisance_actions.mean()) + 1e-12
action_sensitivity = (
    action_spread / action_scale
)  # zero when the action never moves
Out[18]:
Visualization
Bar chart showing held-out nuisance probe R-squared near one and action coefficient of variation equal to zero. The two bars represent different statistics, not a shared bounded scale.
Two distinct diagnostics for the designed nuisance-coordinate sweep: held-out probe R-squared and the action standard deviation divided by its absolute mean plus a small stabilizer. The probe recovers the supplied coordinate nearly perfectly, while the controller ignores that coordinate and returns the same action throughout the sweep. These statistics have different definitions; the displayed zero-to-one range covers these observed values, not their possible ranges.

High probe recovery and zero action sensitivity coexist without contradiction. The representation carries the coordinate, and the pipeline ignores it.

Localizing Faults with Rollouts

We now inject a dynamics fault: the transition model integrates with the wrong time step. The fault is disclosed and isolated.

In[19]:
Code
def transition_faulty(state, a):
    # Same functional form, wrong integration time step.
    return transition_true(state, a, dt=0.12)


def teacher_forced_errors(eps, transition_fn):
    out = []
    for ep in eps:
        p, v, a = ep["p"], ep["v"], ep["a"]
        errs = []
        for t in range(len(a)):
            pred = transition_fn(np.array([p[t], v[t]]), a[t])
            errs.append(abs(pred[0] - p[t + 1]))
        out.append(errs)
    return np.array(out)


def recursive_errors(eps, transition_fn):
    out = []
    for ep in eps:
        p, v, a = ep["p"], ep["v"], ep["a"]
        pp, vv = p[0], v[0]
        errs = []
        for t in range(len(a)):
            pp, vv = transition_fn(np.array([pp, vv]), a[t])
            errs.append(abs(pp - p[t + 1]))
        out.append(errs)
    return np.array(out)


tf_true = teacher_forced_errors(test_eps, transition_true)
rec_true = recursive_errors(test_eps, transition_true)
tf_fault = teacher_forced_errors(test_eps, transition_faulty)
rec_fault = recursive_errors(test_eps, transition_faulty)
Out[20]:
Visualization
Line chart of position error versus horizon for four rollout conditions.
Mean absolute position error versus horizon for teacher-forced and recursive predictions under correct and faulty dynamics. Both correct-dynamics traces coincide at zero. Under faulty dynamics, the final-step teacher-forced error is about 0.041 m, while recursive error reaches about 1.067 m. This fixed-action experiment distinguishes local mismatch from its accumulated rollout effect. Recursive errors need not grow monotonically in other systems; these traces do not establish asymptotic stability.
Out[21]:
Console
Teacher-forced final-step error, correct:  0.0000 m
Recursive final-step error, correct:       0.0000 m
Teacher-forced final-step error, faulty:   0.0406 m
Recursive final-step error, faulty:        1.0673 m

Both correct-dynamics traces are exactly zero: recursion alone does not create an error in this deterministic, matched-model experiment. With faulty dynamics, final-step teacher-forced position error averages about 0.0410.041 m, while recursive error averages about 1.0671.067 m. Small local mismatch accumulates along the fixed action sequence. These traces do not by themselves classify stability or uniquely identify a fault. The double integrator is not asymptotically stable, and recursive error need not grow monotonically in a different system.

Component Substitution

Now the closed-loop experiment. We run the full pipeline in the true environment from matched held-out initial states. Each condition regenerates its observation and planner streams on demand from the same episode and step seeds; no stream arrays are precomputed. Observation draws match at each episode step. Planner streams also match, but different horizons group their draws into different candidate sequences. These matched cases support paired episode-outcome comparisons under the disclosed component configurations; they do not hold the resulting trajectories or all search properties fixed. Unlike the noisy initial observation in make_episode, the closed-loop function starts its observation history with exact initial position and adds observation noise only after transitions. Every compared closed-loop condition shares this initialization.

In[22]:
Code
def encoder_correct(obs_hist, true_state):
    return encode(obs_hist, dt=DT)


def encoder_faulty(obs_hist, true_state):
    # Perception fault: velocity channel uses the wrong time step.
    return encode(obs_hist, dt=0.12)


def encoder_oracle(obs_hist, true_state):
    # Privileged readout of simulator state (not available to a deployed system).
    z = np.zeros((len(obs_hist), 2))
    z[-1] = true_state[:2]
    return z


def mpc_reference(z, transition_fn, rng):
    return mpc_action(z, transition_fn, 10, 64, A_MAX, rng, COST_WEIGHTS)


def mpc_faulty(z, transition_fn, rng):
    # Short-horizon alternative; whether it underperforms is measured below.
    return mpc_action(z, transition_fn, 3, 64, A_MAX, rng, COST_WEIGHTS)


def run_closed_loop(
    init_state, encoder_fn, transition_fn, mpc_fn, episode_idx, n_steps=HORIZON
):
    state = np.array(init_state, dtype=float)
    obs_hist = [state[0]]
    obs_rng = np.random.default_rng(5000 + episode_idx)
    total_cost = 0.0
    for t in range(n_steps):
        z = encoder_fn(np.array(obs_hist), state)
        z_t = z[-1]
        step_rng = np.random.default_rng(9000 + 100 * episode_idx + t)
        a = float(np.clip(mpc_fn(z_t, transition_fn, step_rng), -A_MAX, A_MAX))
        state = transition_true(state, a)
        obs_hist.append(state[0] + obs_rng.normal(0.0, SIGMA_O))
        total_cost += state[0] ** 2 + 0.1 * state[1] ** 2 + 0.01 * a**2
    return total_cost
In[23]:
Code
init_states = [np.array([ep["p"][0], ep["v"][0]]) for ep in test_eps]

condition_specs = [
    ("baseline", encoder_correct, transition_true, mpc_reference, "Baseline"),
    (
        "perception_fault",
        encoder_faulty,
        transition_true,
        mpc_reference,
        "Perception fault",
    ),
    (
        "perception_fault_oracle",
        encoder_oracle,
        transition_true,
        mpc_reference,
        "Perception fault + oracle perception",
    ),
    (
        "dynamics_fault",
        encoder_correct,
        transition_faulty,
        mpc_reference,
        "Dynamics fault",
    ),
    (
        "dynamics_fault_oracle",
        encoder_correct,
        transition_true,
        mpc_reference,
        "Dynamics fault + oracle dynamics",
    ),
    (
        "planner_fault",
        encoder_correct,
        transition_true,
        mpc_faulty,
        "Short horizon (H=3)",
    ),
    (
        "planner_fault_reference",
        encoder_correct,
        transition_true,
        mpc_reference,
        "H=3 replaced by H=10",
    ),
]

closed_loop_costs = {}
for key, enc, trans, mpc, _label in condition_specs:
    costs = [
        run_closed_loop(init_states[i], enc, trans, mpc, i)
        for i in range(len(test_eps))
    ]
    closed_loop_costs[key] = np.array(costs)
Out[24]:
Console
Condition                                  mean cost   paired diff    diff std
------------------------------------------------------------------------------
Baseline                                        1.45         +0.00        0.00
Perception fault                                1.68         +0.22        0.26
Perception fault + oracle perception            1.48         +0.03        0.30
Dynamics fault                                  1.53         +0.08        0.28
Dynamics fault + oracle dynamics                1.45         +0.00        0.00
Short horizon (H=3)                             1.17         -0.28        0.53
H=3 replaced by H=10                            1.45         +0.00        0.00
Out[25]:
Visualization
Seven cost bars comparing baseline, perception and dynamics faults, a short-horizon alternative, and three substitutions, with across-episode standard-error bars.
Mean closed-loop costs across 15 matched held-out episodes for seven component configurations. Perception and dynamics faults raise the sample mean. Oracle perception reduces perception-fault cost but remains slightly above baseline. Reusing baseline dynamics or planner reproduces baseline exactly. The H=3 alternative has lower cost than the H=10 baseline here, so it is not a demonstrated planner fault. Error bars are marginal across-episode standard errors, s/sqrt(15), not paired-difference confidence intervals.

The sample means differ: baseline costs about 1.4531.453, perception fault 1.6771.677, and oracle perception 1.4781.478. Oracle perception improves the faulty-encoder condition but does not beat baseline here. Replacing faulty dynamics with the exact baseline transition reproduces baseline bit-for-bit; that substitution is identical to the baseline configuration. The H=3 alternative costs about 1.1701.170, lower than the H=10 baseline. Restoring H=10 therefore increases cost in this sample, not repairs a demonstrated planner fault. All seven conditions remain interface-compatible, but compatibility does not guarantee improvement or unique attribution.

Oracle perception has access to simulator state unavailable to the observation-based encoder, so its result is a reference comparison, not an achievable or optimal bound. Both horizon configurations sample 64 candidate sequences, but the longer horizon requires more simulated transitions per decision. This finite search is not a global planning oracle, and greater horizon need not lower executed cost. With 64 candidates and no early termination, H=10 evaluates 640 one-step model transitions per decision, whereas H=3 evaluates 192. These counts do not measure wall-clock time. The comparison changes horizon and compute budget; a matched-compute experiment would answer a different question. These sample means alone do not establish statistically significant advantages or a unique mechanism.

Mixed Faults Can Interact

Now the interaction case: perception fault and dynamics fault together. This is the case that tests whether the localization properties remain well-behaved when two faults are active at once.

In[26]:
Code
mixed_specs = [
    (
        "perception_only",
        encoder_faulty,
        transition_true,
        mpc_reference,
        "Perception only",
    ),
    (
        "dynamics_only",
        encoder_correct,
        transition_faulty,
        mpc_reference,
        "Dynamics only",
    ),
    (
        "both_faults",
        encoder_faulty,
        transition_faulty,
        mpc_reference,
        "Both faults",
    ),
    (
        "both_oracle_dynamics",
        encoder_faulty,
        transition_true,
        mpc_reference,
        "Both + oracle dynamics",
    ),
    (
        "both_oracle_perception",
        encoder_oracle,
        transition_faulty,
        mpc_reference,
        "Both + oracle perception",
    ),
]

mixed_costs = {}
for key, enc, trans, mpc, _label in mixed_specs:
    costs = [
        run_closed_loop(init_states[i], enc, trans, mpc, i)
        for i in range(len(test_eps))
    ]
    mixed_costs[key] = np.array(costs)
Out[27]:
Console
Baseline cost:                      1.45
Perception-only cost:               1.68
Dynamics-only cost:                 1.53
Both-faults cost:                   1.63
Additive prediction:                1.76
Interaction (actual - additive):   -0.13
Out[28]:
Visualization
Bar chart comparing single-fault, combined, additive-prediction, and partial-substitution costs.
Six mixed-fault comparisons on the same episodes. Combined-fault cost is about 1.627, below the additive prediction of about 1.756; the observed interaction is about -0.129. Replacing perception reduces cost to about 1.465, near baseline 1.453. Replacing dynamics leaves cost about 1.677, equal to the perception-only condition and worse than both faults together here. Substitution effects are asymmetric; this example does not prove that every feedback system has nonadditive faults.

Combined-fault mean cost is about 1.6271.627, compared with additive prediction 1.7561.756, giving interaction about −0.129-0.129 on these seeds. Replacing perception gives cost about 1.4651.465, near baseline 1.4531.453. Replacing dynamics gives about 1.6771.677, reproducing the perception-only condition and increasing cost relative to both faults together. This illustrates asymmetric, compensating interactions. Nonadditivity is possible in a feedback loop, not inevitable in every system; the observed comparison does not identify a unique mechanism.

The joint-fault comparison measures each replacement's effect within this pipeline. It does not identify an exclusive responsible component or provide additive attribution. The factorial cost contrast is a descriptive interaction on these episodes; an additive decomposition requires further composition assumptions.

Matched Representation Interventions

Finally, we match a base and source episode at the step where their positions are closest, then sweep a dose interpolating or extrapolating the velocity coordinate. Position matching alone does not verify support of the combined representation. A dose sweep exposes how the response changes across this specified perturbation, without assuming monotonicity.

In[29]:
Code
base_ep = test_eps[0]
source_ep = test_eps[2]
t_match = int(
    np.argmin(np.abs(base_ep["p"] - source_ep["p"]))
)  # on the chapter's seeds this selects t = 15, where z_base[1] = -0.17 and z_source[1] = 0.26

z_base = encode(base_ep["obs"])[t_match]
z_source = encode(source_ep["obs"])[t_match]

doses = np.linspace(0.0, 1.5, 16)
actions_velocity = []
actions_sham = []
for d in doses:
    # Intervention on the velocity coordinate.
    z_int = z_base.copy()
    z_int[1] = z_base[1] + d * (z_source[1] - z_base[1])
    actions_velocity.append(
        mpc_action(
            z_int,
            transition_true,
            10,
            64,
            A_MAX,
            np.random.default_rng(4242),
            COST_WEIGHTS,
        )
    )
    # No-op sham: adding zero leaves the complete controller input unchanged.
    z_sham = z_base.copy()
    z_sham[0] = z_base[0] + d * 0.0
    actions_sham.append(
        mpc_action(
            z_sham,
            transition_true,
            10,
            64,
            A_MAX,
            np.random.default_rng(4242),
            COST_WEIGHTS,
        )
    )
Out[30]:
Visualization
Line chart of planned action versus intervention dose for velocity intervention and sham.
Planned action versus velocity-intervention dose and a no-op sham. Every dose resets the planner generator to seed 4242, using the same candidate bank rather than a fresh stream. Selected actions change non-monotonically as candidate rankings change. The sham leaves the entire input fixed and produces a flat action. Closely matched positions do not certify joint representation support; interpolation and extrapolation may create off-support inputs.

The velocity intervention changes the selected action non-monotonically: at doses 00, 0.10.1, and 0.20.2, actions are approximately −0.470-0.470, 0.3070.307, and −0.104-0.104. Each call resets the same planner seed, so these are candidate-ranking changes under a fixed bank, not fresh random draws at each dose. The no-op sham is flat because it changes no controller input. This establishes response to this specified representation perturbation, not a real-world causal mechanism.

The edited velocity differs from the base-step value, but that does not prove it lies outside the base trajectory's velocity range or the population's joint support. Position matching alone does not establish support of the combined state. Doses from zero to 1.5 include interpolation and extrapolation beyond the source displacement; either needs support checks if normal-operation semantics are claimed. The coordinate's velocity meaning is handcrafted, not discovered from a neural representation.

What the Laboratory Established

Let us be precise about what we showed and what we did not.

Established on these fixed episodes and seeds:

  • The linear probe recovers velocity above the intercept-only baseline.
  • The shuffled refit scores below that baseline; the tested orthogonal rotation preserves recovery.
  • The redundant diagnostic coordinate is decodable, but the controller interface discards it.
  • Correct-dynamics teacher-forced and recursive errors are zero. The faulty final recursive error exceeds the faulty final one-step error.
  • Oracle perception gives a cost near baseline, and exact baseline re-substitutions reproduce baseline. Neither statement is a significance or equivalence test.
  • H=3 gives a lower sample mean cost than H=10 under their disclosed, different transition budgets.
  • The mixed-fault cost is below its additive prediction on these episodes.
  • The fixed-bank velocity dose sweep changes the selected action non-monotonically. The no-op sham leaves it unchanged.

Not established by these comparisons:

  • Markov sufficiency or absence of other representation information.
  • Hewitt-and-Liang selectivity from the frame-target shuffle.
  • Unique fault attribution or an additive decomposition.
  • An achievable optimal bound from a privileged reference.
  • Physical-world causal fidelity of the representation edit.
  • Generalization beyond this protocol and evaluation distribution.

That list is the honest summary, and it is the summary you should write for any diagnostic experiment on a real world model. The discipline of writing the list explicitly is what keeps a diagnostic useful: it tells a reader exactly which claims are supported, and helps identify unsupported conclusions and candidate next experiments.