Autonomous Driving › A worked example: a two-agent intersection

Michael BrenndoerferJuly 28, 202629 min read
World Models HandbookAll chapters
Part 10 of 12Application Domains
Chapter 58Autonomous Driving › A worked example: a two-agent intersection
Reading section 6 of 10 · A worked example: a two-agent intersection · 29 minSection 6 of 10 · 29 min

Book contents

Section 6 of 10 · 29 min read

Autonomous Driving › A worked example: a two-agent intersectionOne section · Sections in this chapter6 / 10 ↓

We now build a small, CPU-sized experiment that isolates the behavior-uncertainty part of the opening intersection. Unlike the fully occluded opening, the toy gives the ego the other agent's current position and speed but not its intent. The ego travels east along the xx-axis toward a shared box centered at the origin. The other agent travels north along the yy-axis. At t=0t=0, a predictor proposes futures and a threshold rule decides whether the ego brakes. This is a one-decision action-outcome test, not a reactive multi-agent simulator or repeated closed-loop replanning.

We make the scenario minimal so that every step is auditable. The other agent has two possible policies. In continue mode it keeps a constant velocity. In yield mode it brakes to a stop before the intersection. Both policies share the same current-state input. This toy does not simulate an observation history. A classical constant-velocity baseline extrapolates that input without learning. The single-trajectory predictor approaches the mean of the two futures. The label-supervised predictor produces one trajectory per mode. We compare prediction metrics and, after the rollouts, report sampled toy-footprint overlaps and totals of the initial braking decisions.

For the overlap test, the ego is a 3 m long by 2 m wide axis-aligned rectangle traveling east. The other vehicle is a 3 m long by 2 m wide rectangle traveling north. With centers at (xego,0)(x_{\text{ego}},0) and (0,yother)(0,y_{\text{other}}), these rectangles have positive-area overlap exactly when ∣xego∣<2.5|x_{\text{ego}}|<2.5 m and ∣yother∣<2.5|y_{\text{other}}|<2.5 m. Boundary-only touching is excluded. The shaded 5 m by 5 m box in the figures is this center-overlap region, not a claim about real intersection or vehicle dimensions. The rectangles are a toy geometry. The experiment does not model turning. It does not model shape uncertainty. It does not model contact dynamics. During rollout, overlap is checked at each 0.1-second sampled state, not continuously between steps.

Setting up the simulation

We begin with imports and physical constants. The chapter uses fixed seeds and runs on CPU; runtime depends on the machine.

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

## Simulation constants
dt = 0.1  # timestep in seconds
T_pred = 30  # prediction horizon (3.0 s)
EGO_LENGTH = 3.0  # along x, meters
EGO_WIDTH = 2.0  # along y, meters
OTHER_LENGTH = 3.0  # along y, meters
OTHER_WIDTH = 2.0  # along x, meters
INTERSECTION_HALF = 2.5  # half-size of the center-overlap box in meters
assert INTERSECTION_HALF == (EGO_LENGTH + OTHER_WIDTH) / 2
assert INTERSECTION_HALF == (EGO_WIDTH + OTHER_LENGTH) / 2
EGO_START_X = -25.0  # ego initial position along x-axis
EGO_SPEED = 12.0  # ego initial speed in m/s
BRAKE_DECEL = 5.0  # other agent's yield deceleration in m/s^2

The ego is 25 meters west of the crossing center, moving east at 12 m/s. Its center will reach the near edge of the overlap box at t=(25−2.5)/12≈1.88t = (25 - 2.5)/12 \approx 1.88 s and the far edge at t≈2.29t \approx 2.29 s if it does not brake. The ego's unbraked crossing times are fixed; the other agent's arrival timing varies with its sampled initial position and speed.

Next we define the other agent's two behavior modes and a function to simulate a single rollout of either mode.

In[4]:
Code
def simulate_other(y0, v0, mode, t_total=T_pred, dt=dt):
    """Simulate the other agent along the y-axis.
    mode = 1: continue at constant velocity.
    mode = 0: yield by braking until stopped.
    Returns (positions, velocities) of length t_total + 1.
    """
    y = np.zeros(t_total + 1)
    v = np.zeros(t_total + 1)
    y[0], v[0] = y0, v0
    for t in range(t_total):
        if mode == 0:
            v[t + 1] = max(0.0, v[t] - BRAKE_DECEL * dt)
        else:
            v[t + 1] = v[t]
        y[t + 1] = y[t] + v[t] * dt
    return y, v

We sample initial states uniformly: y0∈[−26,−22]y_0 \in [-26, -22] m south of the intersection and v0∈[9,11]v_0 \in [9, 11] m/s northward. The mode is drawn uniformly from {0,1}\{0, 1\}. With the code's 0.1-second explicit position update, a yield-mode agent travels at most 12.65 m before stopping, so even one starting at y0=−22y_0=-22 m stops at y=−9.35y=-9.35 m, south of the box edge at −2.5-2.5 m. The continuous-time stopping-distance formula gives 12.1 m at 11 m/s; it is not the upper bound for this discrete rollout. Some continue-mode trajectories reach the box during the ego's crossing window, while others do not. The predictor receives only the current state (y0,v0)(y_0, v_0). That input does not reveal the sampled mode. The target contains 31 positions from the present at t=0t=0 through t=3.0t=3.0 s.

In[5]:
Code
def make_dataset(n, rng):
    X = np.zeros((n, 2))
    Y = np.zeros((n, T_pred + 1))
    modes = np.zeros(n, dtype=int)
    for i in range(n):
        mode = int(rng.integers(0, 2))
        y0 = rng.uniform(-26, -22)
        v0 = rng.uniform(9, 11)
        y, _ = simulate_other(y0, v0, mode)
        X[i] = [y0, v0]
        Y[i] = y
        modes[i] = mode
    return X, Y, modes


rng = np.random.default_rng(0)
X_train, Y_train, modes_train = make_dataset(4000, rng)
X_test, Y_test, modes_test = make_dataset(1000, rng)

## Prepare fixed trajectories outside the theme-rerunnable plotting cell.
bev_rng = np.random.default_rng(7)
bev_examples = {0: [], 1: []}
for mode in (0, 1):
    for sample_idx in range(6):
        y0 = bev_rng.uniform(-26, -22)
        v0 = bev_rng.uniform(9, 11)
        y, velocity = simulate_other(y0, v0, mode)
        bev_examples[mode].append(y)
Out[6]:
Console
Training set: (4000, 2), test set: (1000, 2)
Fraction of 'continue' mode in training: 0.50

The training set is approximately balanced between the two modes. Each sample contains a 2D input. Its target is a 31-dimensional trajectory.

Visualizing the scenario

Before training anything, we see what the situation looks like from above. We plot the center-overlap box, the ego's nominal unbraked centerline, and a few other-agent trajectories in both modes.

Out[7]:
Visualization
<matplotlib.legend.Legend at 0x112897390>
Top-down view of an intersection with an ego path from the west and separate continue and yield trajectories moving north.
Bird's-eye view of the two-agent crossing. The shaded box marks center positions at which the toy rectangular footprints can overlap. The ego approaches from the west; continue-mode paths move north through the box while yield-mode paths stop south of it. Other-agent paths have small horizontal display offsets so both clusters remain visible; all are simulated on the same centerline.

The plot shows the two clusters of trajectories clearly. Continue-mode agents move north toward or through the box; yield-mode agents stop south of it. The ego's center crosses the box at x∈[−2.5,2.5]x \in [-2.5, 2.5]. Under the stated rectangular toy footprints, simultaneous occupancy of the box's interior by the two centers is exactly the positive-area overlap condition.

Empirical center-position grids

The other agent's center position over time traces a curve in the (t,y)(t, y) plane. We discretize the lateral coordinate and estimate the probability that its center lies in each yy cell at each time step. Each time row sums to one: this is a per-time center-position distribution, not a distribution over all space-time cells. Physical footprint occupancy is different: one vehicle can cover several cells at once, so those cell-occupancy probabilities need not sum to one. This simplified display is small enough to inspect by eye.

We compute smoothed empirical center-position grids from ground-truth held-out trajectories grouped by their true modes, then combine the grids with equal weights. These dataset-averaged displays illustrate a possible output format; neither trained predictor produced them for the current scene. The Gaussian kernel only makes the sample positions visible as cell probabilities and is not a calibrated uncertainty model.

Formally, the mode-marginal position probability at time tt and lateral cell yy is

Pmix(y∣t)=∑k∈{0,1}πk Pk(y∣t)P_{\text{mix}}(y \mid t) = \sum_{k \in \{0,1\}} \pi_k \, P_k(y \mid t)

where:

  • Pk(y∣t)P_k(y \mid t): the smoothed empirical probability mass for the agent's center in lateral cell yy at time tt among held-out examples with true mode kk; its cells sum to one separately at each tt
  • πk\pi_k: the prior probability of mode kk, here π0=π1=0.5\pi_0 = \pi_1 = 0.5 since modes are drawn uniformly
  • ∑kπk Pk(y∣t)\sum_{k} \pi_k \, P_k(y \mid t): the per-time position marginal under the two-mode mixture, averaged here across the test scenes for display

This uses the same equal weights as the data-generating mode prior, but the time-indexed marginals discard temporal dependence. The grid shows where empirical center-position mass lies at each time; it cannot by itself give the probability of an overlap at any time across an interval. The later planner checks complete scene-specific predicted trajectories and their weights instead of consuming this dataset-averaged display grid.

In[8]:
Code
## Discretize the (t, y) plane
H, W = T_pred + 1, 84
t_range = (0.0, T_pred * dt)
y_range = (-30.0, 12.0)

y_centers = y_range[0] + (np.arange(W) + 0.5) * (y_range[1] - y_range[0]) / W


def build_occupancy(Y_traj, modes, mode_val, sigma=0.8):
    """Estimate per-time-step cell probabilities for one behavior mode."""
    trajectories = Y_traj[modes == mode_val]
    grid = np.zeros((H, W))
    for traj in trajectories:
        kernel = np.exp(
            -0.5 * ((y_centers[None, :] - traj[:, None]) / sigma) ** 2
        )
        kernel /= kernel.sum(axis=1, keepdims=True)
        grid += kernel
    return grid / len(trajectories)


occ_continue = build_occupancy(Y_test, modes_test, 1)
occ_yield = build_occupancy(Y_test, modes_test, 0)
occ_mix = 0.5 * occ_continue + 0.5 * occ_yield
np.testing.assert_allclose(occ_mix.sum(axis=1), np.ones(H), atol=1e-12)
assert np.max(Y_test) < y_range[1]
assert np.max(Y_test[modes_test == 0]) < -INTERSECTION_HALF
Out[9]:
Visualization
Text(0.0, 1.0, 'Empirical Center Position: Yield')
Smoothed empirical per-time center-position probabilities for held-out continue-mode trajectories. Dashed vertical lines mark the overlap-box bounds at y=±2.5 m; this is not a scene-conditioned model prediction.
Heatmap of empirical center-position probability by time for held-out continue-mode trajectories moving north.
Smoothed empirical per-time center-position probabilities for held-out yield-mode trajectories. Dashed vertical lines mark the overlap-box bounds at y=±2.5 m; this is not a scene-conditioned model prediction.
Heatmap of empirical center-position probability by time for held-out yield-mode trajectories stopping south.

The two empirical grids look different. Continue-mode center mass moves north toward and sometimes through the intersection; yield-mode center mass settles south of it. Averaging trajectories into one path would lose this distinction. Mixing mode-conditioned center-position probabilities retains mass in both regions, although it no longer identifies which behavior mode produced a given cell probability. This illustration is not itself the prediction used by the planner.

The non-oracle planner does not observe which mode is realized. The oracle baseline below receives the true mode only for comparison. For a fixed time, summing the center-position probabilities over the box gives the probability that its center is there under this empirical toy distribution. Summing those probabilities across times would double-count trajectories; an any-time footprint-overlap event needs temporal dependence and the ego's motion, which this grid discards. The later action rule therefore uses mode-specific trajectory predictions.

The time-indexed position marginal is shown below, paired with the mode-specific grids above.

Out[10]:
Visualization
Heatmap of empirical center-position probability over time and lateral position for the two-mode sample mixture.
Smoothed empirical center-position marginal at each time, formed by equally weighting held-out continue and yield groups. Dashed vertical lines mark y=±2.5 m. The grid omits trajectory identity and temporal correlation and is not a scene-conditioned predictor output.

A single-trajectory predictor

Now we train a small MLP that outputs a single trajectory. The training runs in the cell immediately below. It minimizes mean squared error, whose unrestricted population optimum is the conditional mean of the future given the input. A finite network trained for a finite number of steps only approximates that target. The architecture is deliberately simple, since the point of the experiment is the loss function and the output structure, not the network's capacity.

Out[12]:
Console
Single-trajectory training MSE: 21.9506

The training MSE is about 22 m222\,\mathrm{m}^2 per trajectory position for this seed. Under squared error, the population-optimal point prediction is the conditional mean E[τ∣o1:t,m]=∑kwkτk\mathbb{E}[\tau \mid o_{1:t},m] = \sum_k w_k\tau^k. A finite-capacity network trained for a finite number of steps need not equal that mean exactly, but here it approaches a path between the two labeled behaviors. That path is a poor summary for a threshold collision check even when its average squared error is reasonable.

A multimodal predictor

The multimodal predictor has a shared trunk and two heads. One head outputs mode logits, which softmax converts to probabilities; the other outputs one trajectory per mode. Because this synthetic dataset gives us the true behavior label for every training trajectory, we use that label to train the corresponding trajectory head and a cross-entropy objective for the mode probabilities. This makes the two output modes identifiable in this controlled example; it does not establish that an unlabeled mixture model would learn them as reliably.

Out[14]:
Console
Assigned-mode trajectory MSE: 0.9175
Mode cross-entropy:            0.6941

The training objective is

L(θ)=1N∑n=1N[1Tpred+1∑t=0Tpred(τt(n)−τtzn,(n))2(1 m)2−log⁡wzn(n)]\mathcal{L}(\theta) = \frac{1}{N} \sum_{n=1}^{N}\left[\frac{1}{T_{\rm pred}+1}\sum_{t=0}^{T_{\rm pred}}\frac{\bigl(\tau_t^{(n)}-\tau_t^{z_n,(n)}\bigr)^2}{(1\,\mathrm{m})^2} - \log w_{z_n}^{(n)}\right]

where:

  • NN: number of training samples
  • TpredT_{\rm pred}: number of prediction steps after the stored position at t=0t=0; each trajectory has Tpred+1T_{\rm pred}+1 positions
  • znz_n: the observed continue-or-yield label for sample nn
  • τ(n)\tau^{(n)}: the observed future for sample nn
  • τzn,(n)\tau^{z_n,(n)}: the predicted trajectory assigned to the labeled mode
  • wzn(n)w_{z_n}^{(n)}: the predicted probability of the labeled mode

The first term fits the trajectory head associated with the known label; the second trains the predicted mode probability. Dividing by a one-meter squared scale makes the position term dimensionless and leaves the numerical loss computed from meter-valued coordinates unchanged. It also fixes the relative weighting of position error and mode cross-entropy when interpreting the equation in physical units. Since the current input (y0,v0)(y_0,v_0) does not reveal intent, a calibrated classifier should assign approximately equal probabilities to the two modes. This labeled construction lets us examine the decisions of a two-mode predictor in this controlled example; learning modes without labels is a harder problem.

Comparing predictions

Now we evaluate the two learned predictors and a constant-velocity baseline on the test set, in a computation cell that runs once and prepares the metric values that figure cells consume. We compute seven metrics:

  • Mean squared position error (MSE) of constant-velocity extrapolation overall and separately for the true continue and yield subsets.
  • Mean squared position error (MSE) of the single predictor.
  • Mean squared position error of the mixture mean, i.e., the probability-weighted average of the two modes.
  • Min-of-K squared error for the two-mode predictor: the error of the closest candidate trajectory.
  • Mode coverage: the fraction of test trajectories for which at least one mode is within a tolerance.

These metrics compare point-prediction error with candidate-set accuracy. The constant-velocity errors expose how much the extrapolation depends on the true behavior. The single and mixture-mean errors measure average squared error; the last two metrics describe best-candidate time-averaged error and coverage at a stated threshold. None directly measures action safety or unnecessary braking, and the candidate-set metrics ignore mode probabilities.

Out[16]:
Console
Constant velocity MSE (all):            46.085
Constant velocity MSE (continue):       0.000
Constant velocity MSE (yield):          87.948
Single predictor MSE (mean trajectory): 21.951
Mixture mean MSE (expected trajectory): 22.081
Mixture min-of-K MSE (best mode):       0.887
Mixture mode coverage (within 1 m^2):   0.691

The min-of-K error is defined as

min-of-K=1N∑n=1Nmin⁡k∈{1,…,K}1Tpred+1∑t=0Tpred(τt(n)−τtk,(n))2\mathrm{min\text{-}of\text{-}K} = \frac{1}{N} \sum_{n=1}^{N} \min_{k \in \{1, \dots, K\}} \frac{1}{T_{\rm pred}+1} \sum_{t=0}^{T_{\rm pred}} \left( \tau_t^{(n)} - \tau_t^{k,(n)} \right)^2

where:

  • NN: the number of test samples
  • TpredT_{\rm pred}: the number of prediction steps after t=0t=0, giving Tpred+1T_{\rm pred}+1 stored positions
  • τt(n)\tau_t^{(n)}: the true position at time step tt for test sample nn
  • τtk,(n)\tau_t^{k,(n)}: the predicted position at time step tt from mode kk for test sample nn
  • min⁡k\min_{k}: the operation that selects the single mode closest to the truth for each sample

In words, the metric first computes the time-averaged squared error of each complete candidate trajectory, selects the best candidate per sample, then averages over samples. It measures best-candidate closeness, not pointwise coverage: the separate coverage percentage applies a 1 m21\,\mathrm{m}^2 threshold to that time-averaged error and does not require every predicted position to be within one meter.

The numbers tell a more qualified story. Constant velocity reproduces the continue path in this construction but extrapolates through the intersection when the other agent yields; its mode-specific errors show the failure hidden by one pooled score. The single predictor and mixture mean have similar MSE, as expected when both approximate the conditional mean. The mixture's min-of-K MSE is much lower, but its coverage at the stated 1 m21\,\mathrm{m}^2 tolerance is below one: it has not learned every trajectory to that precision. A planner can check each predicted mode for risk, provided the modes include the dangerous futures and their probabilities are meaningful.

Let's visualize the predictions for a specific test scenario where the true mode is "continue" and the input is ambiguous.

Out[17]:
Visualization
<matplotlib.legend.Legend at 0x1179eca50>
Line plot comparing true future trajectory with single-predictor mean and two mixture modes.
Held-out continue-mode prediction comparing a single-trajectory predictor against a two-mode, label-supervised predictor. The single predictor regresses toward the conditional mean; the two-mode predictor shows separate continue and yield paths with approximately equal weights. The later action-outcome test checks whether this representation changes the ego's decision.

The plot shows the tension. The true future moves north toward the intersection. The single predictor, forced to choose one trajectory, produces a slower mean path. The two-mode predictor shows a continue path near the truth and a separate yield path.

One-decision action-outcome evaluation

Prediction metrics are useful diagnostics but are not sufficient to evaluate action safety. What matters for safety is whether the predictor enables a useful ego decision. In this one-decision scenario, the ego observes (y0,v0)(y_0, v_0), asks the predictor for a future, checks which predicted modes overlap its planned crossing window, sums their weights, and brakes if that risk score exceeds 0.1. The choice is Boolean: if it brakes, the ego follows a fixed 5 m/s25\,\mathrm{m/s^2} deceleration profile. We then roll out both agents for up to six seconds, stopping at the first detected sampled footprint overlap. There is no repeated observation or replanning, so this is not a reactive closed-loop driving benchmark.

We now define four predictors to compare: an oracle that knows the true mode, constant-velocity extrapolation, the single-trajectory MLP, and the mixture MLP.

We now run many scenarios. For each, we draw (y0,v0)(y_0, v_0) and a true mode, and we run each predictor.

Out[21]:
Console
Scenarios: 300  (continue: 160, yield: 140)

            Oracle:  toy-footprint overlaps =   0  (continue-mode:   0, yield-mode:   0)  brakes = 105  brakes when oracle proceeds =   0
 Constant velocity:  toy-footprint overlaps =   0  (continue-mode:   0, yield-mode:   0)  brakes = 185  brakes when oracle proceeds =  80
            Single:  toy-footprint overlaps = 105  (continue-mode: 105, yield-mode:   0)  brakes =   0  brakes when oracle proceeds =   0
           Mixture:  toy-footprint overlaps =   0  (continue-mode:   0, yield-mode:   0)  brakes = 274  brakes when oracle proceeds = 169

In this seeded experiment, the oracle has zero toy-footprint overlaps and brakes in the 105 scenarios where the true continue path threatens the box. Constant-velocity extrapolation also has zero overlaps, but brakes in 185 scenarios, including 80 where the oracle proceeds: it assumes even a yielding agent continues at its initial speed. The two-mode predictor has zero overlaps but brakes in 274 of 300 scenarios, including 169 where the oracle proceeds. Thus the classical baseline is less intervention-heavy than this learned mixture under the same toy risk rule. The single predictor never brakes and has 105 overlaps in continue-mode scenarios. The risk check adds weight ∑kwk⋅1 ⁣[∃j: tj=jΔt∈[tin,tout], ∣τtjk∣<INTERSECTION_HALF]\sum_k w_k \cdot \mathbb{1}\!\left[\exists j:\, t_j=j\Delta t\in[t_{\text{in}},t_{\text{out}}],\ |\tau_{t_j}^{k}|<\texttt{INTERSECTION\_HALF}\right] over the sampled crossing window and brakes when this exceeds 0.1. A separate rule that always brakes would brake in all 300 scenarios and have no sampled overlaps here; that conservative toy rule is not a road-driving policy. These results illustrate a safety-efficiency tradeoff under the toy geometry, not a deployed performance claim.

Before inspecting a single scenario, it helps to read the aggregate. Because footprint overlap occurs only in the continue mode, the rates should be broken out by the true mode rather than pooled. The evaluation set contains the 300300 scenarios drawn above with seed 4242; each predictor receives one Boolean overlap outcome per scenario.

In[22]:
Code
predictor_names = ["Oracle", "Constant velocity", "Mixture", "Single"]
continue_collision_rate = [
    results[name]["collisions_continue"] / max(results[name]["n_continue"], 1)
    for name in predictor_names
]
yield_collision_rate = [
    results[name]["collisions_yield"] / max(results[name]["n_yield"], 1)
    for name in predictor_names
]
Out[23]:
Visualization
Grouped bar chart of toy-footprint overlap rates for oracle, constant-velocity, mixture, and single predictors.
Toy-footprint overlap rate by true behavior mode in the seeded one-decision test. Oracle, constant-velocity, and two-mode predictors have no overlaps; the mean-trajectory predictor overlaps in continue-mode cases. Overlap rate alone hides conservatism: constant velocity brakes in 185 of 300 scenarios and the two-mode predictor in 274.
Out[24]:
Visualization
<matplotlib.legend.Legend at 0x11afcfd10>
One-decision outcome for a held-out continue-mode scenario. Under the single predictor, the ego proceeds and the two shaded toy vehicle footprints overlap. The rectangles mark the simulated vehicles' positions at the first detected sampled overlap, not a separately invented collision point.
Top-down path plot with the ego and other vehicle's overlapping rectangular footprints at the first detected sampled overlap.
The same initial conditions and braking rule under the two-mode predictor cause the ego to stop before the overlap region while the other agent continues north.
Top-down path plot with the ego stopped west of the crossing and the other vehicle moving north without footprint overlap.

In the single-predictor panel, the ego crosses while the other agent is in the shared box. In the two-mode panel, the ego brakes and the other agent passes. The initial conditions and decision rule are the same. Only the predictions differ. This selected case does not show the cost of the additional braking in other cases.

What this experiment shows, and what it doesn't

The experiment is deliberately small, but it isolates a useful failure mode: an MSE-trained single trajectory can conceal a hazardous mode. Representing separate modes lets the simple planner avoid toy-footprint overlaps in this dataset, at the cost of many extra brakes compared with the oracle. Yet the constant-velocity baseline also avoids overlaps and brakes less often than the learned mixture in this particular toy. Richer prediction alone does not guarantee a better decision. The experiment therefore motivates measuring both modeled safety and progress or intervention cost, rather than prediction error alone.

Three caveats are important. The example uses two labeled behaviors even though real actions vary continuously. The other agent follows a fixed policy and never reacts to the ego; its predictions are action-independent. The ego makes one threshold decision at t=0t=0 rather than repeatedly observing and replanning. The example is therefore an action-outcome check, not an action-conditioned response model or a realistic closed-loop driving simulation.