Self-Models, Body Schemas, and Affordances › Code Implementation

Michael BrenndoerferJune 18, 202611 min read
World Models HandbookAll chapters
Part 3 of 12Representing Agents and Worlds
Chapter 18Self-Models, Body Schemas, and Affordances › Code Implementation
Reading section 7 of 11 · Code Implementation · 11 minSection 7 of 11 · 11 min

Book contents

Section 7 of 11 · 11 min read

Self-Models, Body Schemas, and Affordances › Code ImplementationOne section · Sections in this chapter7 / 11 ↓
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

The first block defines the true (hidden from the agent) kinematics and the self-generated calibration-data loop. Everything here is small enough to run on a laptop CPU in seconds. The separation between the definition block and the agent's data matters conceptually: the function forward_kinematics plays the role of the unmodeled physical body, and every subsequent step treats it as a black box that maps joint configurations to measured end-effector positions.

In[11]:
Code
import numpy as np
from sklearn.neural_network import MLPRegressor

## True (agent-unknown) geometry
L1, L2 = 1.0, 0.8


def forward_kinematics(q1, q2):
    x = L1 * np.cos(q1) + L2 * np.cos(q1 + q2)
    y = L1 * np.sin(q1) + L2 * np.sin(q1 + q2)
    return np.stack([x, y], axis=-1)

Now the agent runs its self-identification experiment: random motor babbling over joint space, recording the resulting measured end-effector pose. This mirrors the coverage requirement from System Identification: the training inputs must visit the configurations on which the learned schema will be used. Uniform independent sampling over the full joint range provides broad static coverage in expectation, but a finite draw does not by itself certify persistent excitation for a nonlinear model. A real agent with limited energy or safety constraints might choose samples more deliberately; in every case, evaluation should use configurations withheld from fitting.

In[12]:
Code
rng = np.random.default_rng(7)
n_train, n_test = 2000, 500

Q_train = rng.uniform(-np.pi, np.pi, size=(n_train, 2))
Q_test = rng.uniform(-np.pi, np.pi, size=(n_test, 2))
X_train = forward_kinematics(Q_train[:, 0], Q_train[:, 1])
X_test = forward_kinematics(Q_test[:, 0], Q_test[:, 1])
Out[13]:
Console
Training pairs: 2000, held-out pairs: 500

With 2,000 training pairs drawn uniformly over joint space and 500 held-out pairs, the experiment measures interpolation to configurations not used for fitting. A finite uniform sample still leaves uneven local coverage, especially near domain boundaries where neighborhoods are one-sided. The held-out set is therefore necessary but not a certificate of worst-case behavior: a safety-critical evaluation would stratify error by configuration region and deliberately probe joint limits and singular postures.

Fit the body schema. A small multilayer perceptron suffices because the map is smooth and two-dimensional. Notice that the agent never sees L1L_1 or L2L_2; the link lengths are implicit in the learned weights. In a richer problem the network would also have to discover the structure of the map, its periodicity, its symmetry under exchanging certain configurations, but here the smooth two-dimensional regression already carries the essential lesson: the geometry of the body can live entirely in fitted parameters.

In[14]:
Code
schema = MLPRegressor(
    hidden_layer_sizes=(64, 64), max_iter=4000, random_state=0
)
schema.fit(Q_train, X_train)

pred_test = schema.predict(Q_test)
err = np.linalg.norm(pred_test - X_test, axis=1)
Out[15]:
Console
Mean end-effector error: 0.0529
95th-percentile error:   0.1962

The held-out mean error is about 0.0530.053 length units and the 95th-percentile error is about 0.1960.196, for an arm whose maximum reach is 1.81.8 units. The schema captures the overall map, but its tail error is large enough to matter near tight clearances. More important than the point prediction is the derivative, because planners consume Jacobians. We extract it from the learned model by central finite differences and compare with the analytic Jacobian the agent does not know. This step is where a merely predictive model would stop and a control-supporting model continues: the finite-difference probe asks the learned model a question it was never directly trained on, namely how its output responds to infinitesimal input changes, and the answer is only useful if the learned function is smooth in the right way.

In[16]:
Code
def learned_jacobian(q, eps=1e-4):
    J = np.zeros((2, 2))
    for i in range(2):
        qp, qm = q.copy(), q.copy()
        qp[i] += eps
        qm[i] -= eps
        J[:, i] = (
            schema.predict(qp[None]) - schema.predict(qm[None])
        ).ravel() / (2 * eps)
    return J


q0 = np.array([0.4, -1.1])
a1, a2 = q0
J_analytic = np.array(
    [
        [-L1 * np.sin(a1) - L2 * np.sin(a1 + a2), -L2 * np.sin(a1 + a2)],
        [L1 * np.cos(a1) + L2 * np.cos(a1 + a2), L2 * np.cos(a1 + a2)],
    ]
)
J_learned = learned_jacobian(q0)
Out[17]:
Console
Max entrywise Jacobian error: 0.0551

At this configuration, the maximum entrywise difference between the learned and analytic Jacobians is about 0.0550.055. The estimate recovers the local direction only approximately, and this one-posture comparison does not establish suitability for resolved-rate control across the workspace. That requires closed-loop tests, representative postures, feedback, and conditioning checks. Here Δx\Delta x is the desired end-effector displacement and Δq\Delta q is the joint displacement that produces it. Near configurations where the analytic Jacobian loses rank, the linear solve becomes ill-conditioned and small end-effector motions may demand very large joint motions. A learned schema inherits this geometric pathology, so a controller should monitor singular values or use a damped pseudoinverse. The probe exposes the gap between point-prediction accuracy, local derivative agreement, and control-grade validation.

Next, the positional affordance map. The agent samples many joint configurations, records end-effector positions, and rasterizes them into a workspace grid. Cells containing at least one reachable end-effector position are marked reachable. Conceptually, this is the simplest possible estimator for R\mathcal{R}: a Monte Carlo estimate of the forward-kinematic image of the joint space, binned at grid resolution. Its statistical properties follow directly from that description, as we will see in the output.

In[18]:
Code
## Affordance map: rasterize sampled end-effector positions
Q_all = rng.uniform(-np.pi, np.pi, size=(40000, 2))
P_all = forward_kinematics(Q_all[:, 0], Q_all[:, 1])

lim = 2.0
grid_res = 0.05
n_bins = int(2 * lim / grid_res)
ix = np.clip(((P_all[:, 0] + lim) / grid_res).astype(int), 0, n_bins - 1)
iy = np.clip(((P_all[:, 1] + lim) / grid_res).astype(int), 0, n_bins - 1)
reach = np.zeros((n_bins, n_bins), dtype=bool)
reach[iy, ix] = True

## Sanity check against the known annulus geometry
r = np.linalg.norm(P_all, axis=1)
Out[19]:
Console
Min sampled radius: 0.200 (theory |L1-L2| = 0.200)
Max sampled radius: 1.800 (theory L1+L2   = 1.800)
Reachable cells: 4160 of 6400

The sampled radii hug the analytic bounds: with 40,000 configurations the samples densely cover the annular workspace and leave the disc of radius ∣L1−L2∣=0.2|L_1 - L_2| = 0.2 around the shoulder empty. The agent has recovered the workspace pattern through motor babbling, but the raster alone does not prove that every empty cell is unreachable. A hit is constructive positive evidence: at least one sampled configuration reached that cell. A miss is ambiguous, because a reachable cell may remain empty under finite sampling. The analytic kinematics establish the true inner and outer bounds in this toy example; without them, cells near either boundary should be treated as a band of uncertainty rather than a sharp curve.

Now the theory-of-mind experiment. A second agent walks a one-dimensional corridor of positions 00 to 1010 toward one of two hidden goals, g=1g = 1 or g=9g = 9, stepping toward its goal with probability 0.80.8 and away with probability 0.20.2. The observer sees only the position trajectory and knows the behavioral model, and performs exact Bayesian posterior updates over the two goal hypotheses. The setup is intentionally minimal: two hypotheses, a known stochastic behavioral model, and a one-dimensional state. Stripped of all incidental complexity, the experiment isolates the one thing theory of mind requires at level 1, a likelihood that connects behavior to hidden intention.

In[20]:
Code
def step_prob(x, g):
    """Other agent's step distribution from position x toward goal g."""
    if x == g:
        return {0: 1.0}
    toward = 1 if g > x else -1
    return {toward: 0.8, -toward: 0.2}


## Simulate the other agent with hidden goal 9
true_goal = 9
x_traj = [4]
deltas = []
for _ in range(20):
    probs = step_prob(x_traj[-1], true_goal)
    moves = list(probs)
    d = rng.choice(moves, p=np.array([probs[m] for m in moves]))
    deltas.append(d)
    x_traj.append(x_traj[-1] + d)

## Level-1 goal inference: exact Bayes over the two goal hypotheses
goals = [1, 9]
belief = {1: 0.5, 9: 0.5}
beliefs_goal9 = [0.5]
for t in range(len(deltas)):
    x_now, d = x_traj[t], deltas[t]
    post = {
        g: prior * step_prob(x_now, g).get(d, 0.0)
        for g, prior in belief.items()
    }
    total = sum(post.values())
    if total == 0:
        raise ValueError(
            "Observation has zero likelihood under every goal hypothesis"
        )
    belief = {g: p / total for g, p in post.items()}
    beliefs_goal9.append(belief[9])

t_arr = np.arange(len(x_traj))
x_arr = np.array(x_traj, dtype=float)
post_arr = np.array(beliefs_goal9)
Out[21]:
Console
Other agent's final position: 9
Observer's final P(goal = 9): 1.0000

For this seeded trajectory, the observer's final posterior is effectively 1.0 and the other agent finishes at the hidden goal. The inference has moved from a 0.5 prior to near certainty using the observed steps and the assumed behavioral model. The toy update also assumes every realized observation has positive marginal likelihood under at least one hypothesis. A robust implementation would include observation noise or a fallback for out-of-model events instead of allowing a zero normalizer. In richer settings the goal set, behavioral model, and observation model all require justification; the reusable core is likelihood-weighted belief revision over hidden intentions.

All quantities are now precomputed. We visualize the two results: the reachability affordance map with the learned-kinematics error distribution, and the observer's belief trajectory next to the other agent's actual motion.

Out[22]:
Visualization
Scatter of end-effector positions forming an annulus with radius between 0.2 and 1.8.
Empirical affordance map from 40,000 sampled joint configurations. End-effector positions fill an annulus, leaving an unreachable hole around the shoulder.
Out[23]:
Visualization
Histogram of prediction errors concentrated near zero with a short tail.
Distribution of the learned body schema's end-effector prediction error on held-out configurations. Errors concentrate near zero with a short tail, showing the schema generalizes to postures it never visited.

The first figure shows the discovered capability: every sampled end-effector position lies between the two analytic circles, and the interior hole is empty without the agent ever being told about it. The second figure shows the schema's held-out accuracy.

Out[24]:
Visualization
Line chart of corridor position rising from 4 to 9 then flattening.
Observed position of the other agent over time, moving toward the hidden goal at position 9 and stopping on arrival.
Line chart of posterior probability climbing from 0.5 toward 1.0 and ending at exact certainty.
Observer's posterior probability that the goal is 9, updated by exact Bayes after each observed step and reaching exact certainty when the agent stops.

Read the two behaviors together. Before either goal is reached, a step toward position 9 multiplies the posterior odds for goal 9 by four, while a step away divides them by four. The net evidence in this seeded run favors goal 9. When the agent stops at 9, the goal-1 hypothesis assigns zero likelihood to that event and the posterior becomes exactly one. It moves only from about 0.9990.999 to 1.01.0, so "exact" matters more than "dramatic." This is the core level-1 inference loop, but its conclusion is only as sound as the behavioral model. An obstacle, hesitation, or feint can make observed motion depart from the assumed policy and drive the same Bayesian update toward the wrong goal.