Skip to content

End-to-End World Model Pipeline

From Sensor Data to Vehicle Control for Airside Autonomous Vehicles


1. Full Pipeline Architecture

1.1 System Overview

                           AIRSIDE WORLD MODEL PIPELINE
  ============================================================================

  SENSORS             BEV ENCODING        TOKENIZATION       WORLD MODEL
  --------            -----------          ------------       -----------
  LiDAR x8  ──┐                          ┌─ VQ-VAE ─────┐
               ├──► Voxelization ──►     │  Encoder     │   Spatio-Temporal
  IMU/GNSS ──┐│     PointPillars        │  Codebook    │──► Transformer
              ││     Backbone            │  Quantize    │   (GPT-style)
  Odom ──────┘│     Swin-T neck          └─────────────┘   ├─ Spatial Attn
               │                                            ├─ Temporal Attn
  [Phase 2+]  │     [Phase 2+]                             └─ Ego Conditioning
  Cameras x6 ─┘     LSS / BEVFormer
                     Feature Fusion


  WORLD MODEL         PLANNING            CONTROL            ACTUATORS
  -----------         --------            -------            ---------
  Future BEV     ┌─► Trajectory     ┌─► MPC (20Hz)    ┌─► Steering
  Tokens t+1..N  │   Scoring        │   Lateral ctrl   │
                 │   Cost volumes   │                   ├─► Throttle
  Occupancy  ────┤   Collision chk  ├─► PID (50Hz)     │
  Prediction     │                  │   Longitudinal    ├─► Brake
                 │   Multi-modal    │                   │
  Ego Trajectory─┘   Selection      └─► Safety gate ───┘
                                        RSS check

1.2 Tensor Shapes at Each Stage

The following documents concrete tensor shapes flowing through the pipeline, using airside-specific configuration (102.4m x 102.4m operating range, 0.2m BEV resolution).

Stage 1: Sensor Input

LiDAR aggregate:     (N, 4)          # N ~ 200K points, [x, y, z, intensity]
                                     # 8x RoboSense at 10Hz, aggregated
IMU:                 (6,)            # [ax, ay, az, wx, wy, wz]
GNSS/Odom:           (7,)            # [x, y, z, qx, qy, qz, qw]
Cameras (Phase 2+):  (6, 3, 900, 1600)  # 6 cameras, RGB, H, W

Stage 2: Voxelization

python
# Airside PointPillars configuration
point_cloud_range = [-51.2, -51.2, -3.0, 51.2, 51.2, 5.0]  # meters
voxel_size = [0.2, 0.2, 8.0]                                 # pillar: 0.2m x 0.2m x full height

# Resulting grid
grid_size = [512, 512, 1]  # (102.4m / 0.2m) = 512 cells per axis
max_pillars = 40000        # typically 6K-12K non-empty on airside apron
max_points_per_pillar = 32

# Pillar tensor (sparse → dense)
pillars:         (P, 32, 9)     # P non-empty pillars, 32 pts max, 9 features
                                 # features: [x, y, z, intensity, xc, yc, zc, xp, yp]
                                 # (xc,yc,zc)=offset from pillar center, (xp,yp)=pillar coords
pillar_coords:   (P, 3)          # [batch_idx, x_idx, y_idx]

Stage 3: BEV Feature Encoding

python
# PointNet per-pillar → scatter to pseudo-image
pillar_features:  (P, 64)           # after PointNet MLP: 9→64→64
pseudo_image:     (1, 64, 512, 512) # (B, C, H, W) — scattered to BEV grid

# Backbone (SECOND-style 2D CNN or Swin-T)
# Multi-scale feature pyramid
bev_feat_1x:     (1, 64, 512, 512)  # stride 1 — full resolution
bev_feat_2x:     (1, 128, 256, 256) # stride 2
bev_feat_4x:     (1, 256, 128, 128) # stride 4

# FPN neck → unified BEV features
bev_features:    (1, 256, 128, 128) # after FPN aggregation
                                     # effective resolution: 0.8m per cell
                                     # covers 102.4m x 102.4m

Stage 4: VQ-VAE Tokenization

python
# Encoder (downsamples BEV features 8x from voxel resolution)
# Input:  bev_features (1, 256, 128, 128)
# Swin-T encoder blocks

encoder_out:     (1, 256, 128, 128) # latent features before quantization

# Vector Quantization
codebook_size = 512                  # 512 discrete codes (OccWorld default)
embedding_dim = 256                  # each code is a 256-dim vector

# Quantized output
z_q:             (1, 256, 128, 128) # quantized latent features
token_indices:   (1, 128, 128)      # codebook indices — 16,384 tokens per frame
                                     # each index ∈ {0, 1, ..., 511}

# Scene token sequence for world model
scene_tokens:    (T, 16384)         # T historical frames, each with 16K tokens

Stage 5: World Model (Spatio-Temporal Transformer)

python
# Architecture: GPT-style transformer with interleaved spatial/temporal attention
# Based on OccWorld / Copilot4D paradigm

# Input preparation
token_embeddings:  (B, T, 128, 128, 256)  # embedded tokens in spatial layout
ego_embedding:     (B, T, 256)             # ego state (pose, velocity) projected

# Transformer blocks (6 layers total, alternating)
# Spatial block: Swin Transformer on each frame independently
#   Attention window: 8x8 tokens
#   Params: 256 dim, 8 heads, MLP ratio 4
# Temporal block: Causal attention across time at same spatial position
#   Attends over T frames
#   Params: 256 dim, 8 heads

# Output
predicted_tokens:  (B, T_future, 128, 128)  # next T_future frames of token indices
                                             # T_future typically 5 (= 1 second at 5Hz)
ego_trajectory:    (B, T_future, 3)          # predicted ego [x, y, yaw] per step

Stage 6: Decoded Predictions

python
# VQ-VAE Decoder: token indices → 3D occupancy / BEV segmentation
# For each predicted future frame:

decoded_bev:        (B, C_sem, 128, 128)     # semantic BEV segmentation
                                              # C_sem classes: free, vehicle, aircraft,
                                              # person, GSE, building, FOD, unknown
decoded_occupancy:  (B, C_sem, 200, 200, 16) # 3D semantic occupancy (if using OccWorld)
                                              # 0.4m x 0.4m x 0.5m voxels

Stage 7: Planning

python
# Multi-modal trajectory generation + scoring

# Candidate trajectories (sampled or learned)
candidates:         (B, K, T_plan, 3)   # K=256 candidate trajectories
                                         # T_plan=10 steps at 20Hz = 0.5s
                                         # each step: [x, y, yaw]

# Cost volume from world model predictions
cost_volume:        (B, 128, 128)        # per-cell traversal cost

# Trajectory scoring
scores:             (B, K)               # score per candidate
                                          # combines: collision cost, progress,
                                          # comfort, lane adherence

# Selected trajectory
best_trajectory:    (B, T_plan, 3)       # highest scoring trajectory
waypoints:          (B, T_plan, 2)       # [x, y] control waypoints

Stage 8: Control Output

python
# MPC → low-level commands
control_cmd:    (3,)    # [steering_angle, throttle, brake]
                        # steering: -1.0 to 1.0 (normalized)
                        # throttle: 0.0 to 1.0
                        # brake: 0.0 to 1.0

# Alternative: Twist command (ROS convention for airside AV)
cmd_twist:              # geometry_msgs/Twist
  linear.x:   float     # forward velocity (m/s), max ~5 m/s airside
  angular.z:  float     # yaw rate (rad/s)

1.3 Model Parameter Counts

ComponentParametersGPU Memory (FP16)Inference Time (Orin)
PointPillars encoder5M~200 MB~5 ms
Swin-T BEV backbone28M~400 MB~15 ms
VQ-VAE tokenizer (enc+dec)13-20M~300 MB~8 ms
World model transformer39-200M~400-1600 MB~30-80 ms
Planning head2-5M~50 MB~3 ms
Safety ensemble (x3)15M~150 MB~5 ms
Total~100-270M~1.5-2.7 GB~66-116 ms

2. Data Flow Timing

2.1 Multi-Rate Architecture

The pipeline operates at four frequency tiers. Each tier has specific responsibilities, and data flows between tiers through ring buffers with timestamp-based interpolation.

 ┌─────────────────────────────────────────────────────────────┐
 │                    TIMING DIAGRAM (1 second)                │
 │                                                             │
 │  50Hz ║████║████║████║████║████║████║████║████║████║████║... │
 │  ctrl ║ 20ms cycle: PID + safety gate + actuator write      │
 │       ║                                                     │
 │  20Hz ║██████████║██████████║██████████║██████████║████...   │
 │  plan ║ 50ms cycle: trajectory scoring + MPC solve           │
 │       ║ consumes latest world model prediction               │
 │       ║                                                     │
 │  10Hz ║████████████████████║████████████████████████║...     │
 │  perc ║ 100ms cycle: point cloud → BEV features             │
 │       ║ PointPillars + backbone + FPN                       │
 │       ║                                                     │
 │   5Hz ║████████████████████████████████████████████║...      │
 │  wm   ║ 200ms budget: VQ-VAE encode + transformer predict   │
 │       ║ + VQ-VAE decode occupancy                           │
 └─────────────────────────────────────────────────────────────┘

2.2 50Hz Control Loop (20ms cycle)

What runs:

  • Read latest trajectory waypoints from planning buffer
  • Interpolate trajectory to current timestamp using cubic spline
  • PID controller: compute steering and throttle from trajectory error
    • Lateral: Stanley or Pure Pursuit controller → steering command
    • Longitudinal: PID on velocity profile → throttle/brake
  • RSS safety check on interpolated trajectory
  • Write actuator commands via CAN/EtherCAT

Timing budget:

PID computation:         ~0.5 ms (CPU)
RSS boundary check:      ~0.2 ms (CPU)
Actuator write:          ~0.3 ms (CAN bus latency)
Margin:                  ~19 ms
Total:                   <1 ms active, 19 ms idle

Buffer interface:

python
# Planning → Control ring buffer
trajectory_buffer = RingBuffer(capacity=5)  # holds last 5 planning outputs

class TrajectoryStamped:
    timestamp: float                # ROS time of planning cycle
    waypoints: np.ndarray           # (T_plan, 3) — [x, y, yaw]
    velocities: np.ndarray          # (T_plan,) — target speed per waypoint
    confidence: float               # world model confidence score

2.3 20Hz Planning Loop (50ms cycle)

What runs:

  • Read latest world model predictions from prediction buffer
  • Read latest BEV features from perception buffer (for cost volume)
  • Generate candidate trajectories (lattice planner or learned sampler)
  • Score candidates against predicted occupancy:
    • Collision cost: check overlap with predicted occupied cells
    • Progress cost: reward forward movement toward goal
    • Comfort cost: penalize high jerk, lateral acceleration
    • Lane cost: penalize deviation from planned route
  • MPC solve on top-k candidates for smoothed trajectory
  • Publish selected trajectory to control buffer

Timing budget:

Candidate generation:    ~2 ms  (GPU, batch parallel)
Cost volume lookup:      ~5 ms  (GPU, scatter-gather on predicted occ)
MPC solve:               ~15 ms (CPU, QP solver — OSQP or acados)
Buffer writes:           ~1 ms
Total:                   ~23 ms (within 50ms budget)

When world model is stale (>400ms old):

  • Fall back to constant-velocity extrapolation of last BEV
  • Reduce max speed to 2 m/s
  • Log staleness warning

2.4 10Hz Perception Loop (100ms cycle)

What runs:

  • Receive aggregated LiDAR point cloud (8 sensors fused by aggregator node)
  • Voxelize into pillars
  • Run PointPillars encoder → BEV pseudo-image
  • Run 2D backbone + FPN → multi-scale BEV features
  • [Phase 2+] Camera images → LSS/BEVFormer → camera BEV → fuse with LiDAR BEV
  • Publish BEV features to perception buffer (consumed by tokenizer and planner)

Timing budget (LiDAR-only, Orin):

Point cloud preprocessing: ~3 ms  (CPU: NaN removal, range filter)
Voxelization:              ~3 ms  (GPU: scatter to pillars)
PointPillars PointNet:     ~2 ms  (GPU: per-pillar MLP)
Scatter to pseudo-image:   ~0.5 ms (GPU)
2D backbone:               ~8 ms  (GPU: Swin-T or ResNet-18)
FPN neck:                  ~3 ms  (GPU)
Buffer write:              ~0.5 ms
Total:                     ~20 ms (within 100ms budget)

[Phase 2+] with cameras (additional):

Image preprocessing:       ~2 ms  (GPU: resize, normalize)
Image backbone:            ~12 ms (GPU: ResNet-50 or Swin-T)
View transform (LSS):     ~8 ms  (GPU: depth distribution + splat)
BEV fusion:                ~3 ms  (GPU: concatenate + 1x1 conv)
Total with cameras:        ~45 ms (still within 100ms budget)

2.5 5Hz World Model Loop (200ms cycle)

What runs:

  • Read latest BEV features from perception buffer
  • VQ-VAE encoder: BEV features → discrete tokens
  • Append to temporal token buffer (sliding window of T=4 past frames)
  • World model transformer: predict T_future=5 future token frames
    • Conditioned on ego action (current velocity + planned trajectory)
    • Discrete diffusion: 10 denoising steps for highest quality
    • Or single-pass autoregressive for lower latency
  • VQ-VAE decoder: predicted tokens → future occupancy / BEV segmentation
  • Uncertainty estimation: entropy of token predictions
  • Publish predictions to world model buffer

Timing budget:

VQ-VAE encode:           ~8 ms   (GPU: Swin-T encoder + quantize)
Token buffer management: ~0.5 ms (CPU: sliding window update)
Transformer forward:     ~60 ms  (GPU: 6 layers, 5 future frames)
  [OR discrete diffusion: ~120 ms with 10 steps]
VQ-VAE decode:           ~10 ms  (GPU: per-frame decode)
Uncertainty compute:     ~2 ms   (GPU: softmax entropy)
Buffer write:            ~0.5 ms
Total (autoregressive):  ~81 ms  (within 200ms budget)
Total (diffusion):       ~141 ms (within 200ms budget, less margin)

2.6 Interpolation Between Tiers

When a faster loop needs data from a slower loop, it interpolates:

python
class TimestampedBuffer:
    """Thread-safe ring buffer with timestamp-based interpolation."""

    def __init__(self, capacity: int, interp_method: str = 'linear'):
        self.buffer = collections.deque(maxlen=capacity)
        self.lock = threading.Lock()
        self.interp_method = interp_method

    def get_at_time(self, query_time: float) -> np.ndarray:
        """Interpolate buffer contents to query_time."""
        with self.lock:
            if len(self.buffer) < 2:
                return self.buffer[-1].data  # hold last value

            # Find bracketing entries
            times = [entry.timestamp for entry in self.buffer]
            idx = bisect.bisect_right(times, query_time) - 1
            idx = max(0, min(idx, len(self.buffer) - 2))

            t0, d0 = times[idx], self.buffer[idx].data
            t1, d1 = times[idx + 1], self.buffer[idx + 1].data

            # Linear interpolation (for trajectories)
            alpha = (query_time - t0) / max(t1 - t0, 1e-6)
            alpha = np.clip(alpha, 0.0, 1.5)  # allow small extrapolation

            return d0 * (1 - alpha) + d1 * alpha

2.7 Latency Chain Analysis

End-to-end latency from sensor input to actuator output:

Worst case (all pipelines just missed their cycle):
  LiDAR capture:          0 ms   (trigger)
  Wait for perception:   100 ms  (missed 10Hz cycle, wait for next)
  Perception compute:     20 ms
  Wait for world model:  200 ms  (missed 5Hz cycle, wait for next)
  World model compute:    80 ms
  Wait for planning:      50 ms  (missed 20Hz cycle)
  Planning compute:       23 ms
  Wait for control:       20 ms  (missed 50Hz cycle)
  Control compute:         1 ms
  ─────────────────────────────
  WORST CASE:            ~494 ms

Best case (all pipelines are synchronized):
  LiDAR capture:           0 ms
  Perception compute:     20 ms
  World model compute:    80 ms  (pipelined with next perception)
  Planning compute:       23 ms
  Control compute:         1 ms
  ─────────────────────────────
  BEST CASE:             ~124 ms

Typical case (pipelined, no stalls):
  Effective latency:     ~200-250 ms
  (dominated by world model cycle time)

At airside speeds (max 5 m/s = 18 km/h), 250ms latency means the vehicle travels ~1.25m before reacting. This is acceptable for the low-speed airside ODD but requires:

  • Minimum following distance: 5m (4x reaction distance)
  • Safety braking distance at 5 m/s: ~2.5m (comfortable decel at 5 m/s^2)
  • Total stopping distance budget: ~3.75m

3. Memory Management

3.1 GPU Memory Budget (Jetson AGX Orin 64GB Unified Memory)

The Orin uses unified memory shared between CPU and GPU. Budget allocation must account for all concurrent models plus working memory.

 ┌─────────────────────────────────────────────────────────────────┐
 │              GPU MEMORY ALLOCATION (Total: 64 GB)              │
 │                                                                 │
 │  ┌──────────────────────────────────────────────────┐           │
 │  │  TensorRT Engines (persistent)         ~3.5 GB  │           │
 │  │  ├── PointPillars encoder:              200 MB   │           │
 │  │  ├── BEV backbone (Swin-T):             400 MB   │           │
 │  │  ├── VQ-VAE encoder:                    150 MB   │           │
 │  │  ├── VQ-VAE decoder:                    150 MB   │           │
 │  │  ├── World model transformer:          1600 MB   │           │
 │  │  ├── Planning scorer:                    50 MB   │           │
 │  │  ├── Safety ensemble (3x small):        150 MB   │           │
 │  │  └── [Phase 2] Camera backbone:         800 MB   │           │
 │  └──────────────────────────────────────────────────┘           │
 │                                                                 │
 │  ┌──────────────────────────────────────────────────┐           │
 │  │  Activation / Working Memory            ~2.5 GB  │           │
 │  │  ├── Point cloud buffer (2 frames):     100 MB   │           │
 │  │  ├── BEV feature maps (current):        256 MB   │           │
 │  │  ├── Token buffer (4 frames):            64 MB   │           │
 │  │  ├── Transformer KV cache:              512 MB   │           │
 │  │  ├── Predicted occupancy (5 frames):    400 MB   │           │
 │  │  ├── Candidate trajectories:             32 MB   │           │
 │  │  ├── Cost volume:                        64 MB   │           │
 │  │  └── Misc intermediate tensors:        1072 MB   │           │
 │  └──────────────────────────────────────────────────┘           │
 │                                                                 │
 │  ┌──────────────────────────────────────────────────┐           │
 │  │  System / OS / ROS                      ~4.0 GB  │           │
 │  │  ├── Linux kernel + drivers:           1000 MB   │           │
 │  │  ├── ROS node overhead:                 500 MB   │           │
 │  │  ├── Sensor drivers:                    500 MB   │           │
 │  │  ├── Visualization (optional):         1000 MB   │           │
 │  │  └── Safety monitor + logging:         1000 MB   │           │
 │  └──────────────────────────────────────────────────┘           │
 │                                                                 │
 │  Total allocated:    ~10 GB                                     │
 │  Available margin:   ~54 GB (unified memory headroom)           │
 │                                                                 │
 │  Note: On Orin 32GB, total comes to ~10 GB — still fits        │
 │  with ~22 GB margin. Constraint is compute, not memory.         │
 └─────────────────────────────────────────────────────────────────┘

3.2 CUDA Stream Architecture

Multiple CUDA streams enable overlapping computation across pipeline stages. The key principle: perception for frame N+1 runs concurrently with world model inference on frame N.

python
import torch

class PipelineStreamManager:
    """Manages CUDA streams for overlapped multi-model inference."""

    def __init__(self):
        # Dedicated streams per pipeline stage
        self.stream_perception = torch.cuda.Stream(priority=-1)  # high priority
        self.stream_tokenizer  = torch.cuda.Stream(priority=0)
        self.stream_worldmodel = torch.cuda.Stream(priority=0)
        self.stream_planning   = torch.cuda.Stream(priority=-1)  # high priority
        self.stream_safety     = torch.cuda.Stream(priority=-1)  # high priority
        self.stream_transfer   = torch.cuda.Stream(priority=0)   # H2D / D2H

        # Events for synchronization between streams
        self.evt_perception_done = torch.cuda.Event()
        self.evt_tokenizer_done  = torch.cuda.Event()
        self.evt_worldmodel_done = torch.cuda.Event()

    def run_perception(self, point_cloud):
        """Run BEV encoding on perception stream."""
        with torch.cuda.stream(self.stream_perception):
            # Upload point cloud (overlaps with previous world model)
            pillars = self.voxelize(point_cloud)
            bev_features = self.bev_encoder(pillars)
            self.evt_perception_done.record(self.stream_perception)
        return bev_features

    def run_world_model(self, bev_features):
        """Run tokenization + prediction on world model stream."""
        # Wait for perception to finish
        self.stream_tokenizer.wait_event(self.evt_perception_done)

        with torch.cuda.stream(self.stream_tokenizer):
            tokens = self.vqvae_encoder(bev_features)
            self.evt_tokenizer_done.record(self.stream_tokenizer)

        self.stream_worldmodel.wait_event(self.evt_tokenizer_done)

        with torch.cuda.stream(self.stream_worldmodel):
            predicted_tokens = self.transformer(tokens)
            predicted_occ = self.vqvae_decoder(predicted_tokens)
            self.evt_worldmodel_done.record(self.stream_worldmodel)

        return predicted_occ

    def run_planning(self, predicted_occ, ego_state):
        """Run planning on planning stream (high priority)."""
        self.stream_planning.wait_event(self.evt_worldmodel_done)

        with torch.cuda.stream(self.stream_planning):
            candidates = self.generate_trajectories(ego_state)
            costs = self.score_trajectories(candidates, predicted_occ)
            best = candidates[costs.argmin()]
        return best

3.3 Memory Optimization Techniques

TensorRT engine optimization:

bash
# Build engines with workspace limit and FP16
trtexec --onnx=bev_encoder.onnx \
        --saveEngine=bev_encoder.engine \
        --fp16 \
        --workspace=2048 \      # 2GB workspace limit
        --minShapes=pillars:1x100x32x9 \
        --optShapes=pillars:1x10000x32x9 \
        --maxShapes=pillars:1x40000x32x9 \
        --memPoolSize=workspace:2048MiB

# For world model transformer with dynamic sequence length
trtexec --onnx=world_model.onnx \
        --saveEngine=world_model.engine \
        --fp16 \
        --workspace=4096 \
        --minShapes=tokens:1x2x128x128 \
        --optShapes=tokens:1x4x128x128 \
        --maxShapes=tokens:1x8x128x128

Tensor memory pooling:

python
class TensorPool:
    """Pre-allocate and reuse GPU tensors to avoid allocation overhead."""

    def __init__(self):
        self.pools = {}

    def get(self, name: str, shape: tuple, dtype=torch.float16) -> torch.Tensor:
        key = (name, shape, dtype)
        if key not in self.pools:
            self.pools[key] = torch.empty(shape, dtype=dtype, device='cuda')
        return self.pools[key]

# Pre-allocate all working buffers at startup
pool = TensorPool()
bev_buffer     = pool.get('bev_feat', (1, 256, 128, 128))
token_buffer   = pool.get('tokens',   (1, 4, 128, 128), dtype=torch.long)
pred_buffer    = pool.get('pred_occ', (1, 5, 128, 128), dtype=torch.long)
traj_buffer    = pool.get('traj',     (1, 256, 10, 3))

3.4 Multi-GPU Considerations

For development workstations with multiple GPUs (training scenario):

ConfigurationBEV EncoderVQ-VAEWorld ModelPlanning
1x GPU (Orin)GPU:0GPU:0GPU:0GPU:0
2x GPU (dev)GPU:0GPU:0GPU:1GPU:1
4x GPU (train)GPU:0GPU:1GPU:2-3 (DDP)GPU:0

4. Training Pipeline End-to-End

4.1 Overview: Raw ROS Bags to Deployed TensorRT Model

 Phase A          Phase B          Phase C          Phase D          Phase E
 DATA PREP        TRAIN BEV        TRAIN VQ-VAE     TRAIN WM         DEPLOY
 ─────────        ─────────        ────────────     ────────         ──────
 ROS bags    ──►  PointPillars ──► VQ-VAE       ──► Transformer ──► ONNX
 → extract        on nuScenes      on BEV feats     on token seqs    → TRT
 → clean          → fine-tune      → codebook       → future pred    → Orin
 → format         airside          learning          → ego pred
 → split                                            → fine-tune
                                                     airside
 ~1-2 weeks       ~2 days          ~1-2 days        ~2-5 days        ~1 day

4.2 Phase A: Data Preparation (from ROS Bags)

Step 1: Index and catalog bags

bash
# Install tooling (no ROS dependency needed)
pip install rosbags rosbags-dataframe mcap mcap-ros1-support

# Index all available bags
python scripts/index_bags.py \
    --bag-dir /data/airside_bags/ \
    --output /data/airside_dataset/bag_index.json

# Expected output: list of bags with topics, durations, sizes
# Typical airside dataset: 50-200 hours of driving

Step 2: Extract synchronized sensor data

python
#!/usr/bin/env python3
"""Extract synchronized LiDAR + odom from ROS bags into training format."""

from pathlib import Path
from rosbags.rosbag1 import Reader
from rosbags.serde import deserialize_cdr, ros1_to_cdr
import numpy as np
import json

def extract_bag(bag_path: Path, output_dir: Path):
    """Extract point clouds and ego poses from a single bag."""
    output_dir.mkdir(parents=True, exist_ok=True)

    with Reader(bag_path) as reader:
        # Collect timestamps for synchronization
        pc_msgs = []
        odom_msgs = []

        for connection, timestamp, rawdata in reader.messages():
            msg = deserialize_cdr(ros1_to_cdr(rawdata, connection.msgtype),
                                  connection.msgtype)

            if connection.topic == '/pointcloud_aggregator/output':
                pc_msgs.append((timestamp, msg))
            elif connection.topic == '/odom/fused':
                odom_msgs.append((timestamp, msg))

        # Synchronize: for each point cloud, find nearest odom
        for i, (pc_ts, pc_msg) in enumerate(pc_msgs):
            # Find closest odom
            odom_idx = np.argmin([abs(o[0] - pc_ts) for o in odom_msgs])
            odom_msg = odom_msgs[odom_idx][1]

            # Extract point cloud as numpy array
            points = pointcloud2_to_numpy(pc_msg)  # (N, 4): x, y, z, intensity

            # Extract ego pose
            ego_pose = np.array([
                odom_msg.pose.pose.position.x,
                odom_msg.pose.pose.position.y,
                odom_msg.pose.pose.position.z,
                odom_msg.pose.pose.orientation.x,
                odom_msg.pose.pose.orientation.y,
                odom_msg.pose.pose.orientation.z,
                odom_msg.pose.pose.orientation.w,
            ])

            # Save
            np.save(output_dir / f'{i:06d}_points.npy', points.astype(np.float32))
            np.save(output_dir / f'{i:06d}_ego.npy', ego_pose.astype(np.float64))

    # Save metadata
    meta = {
        'bag_path': str(bag_path),
        'num_frames': len(pc_msgs),
        'duration_sec': (pc_msgs[-1][0] - pc_msgs[0][0]) / 1e9,
        'hz': len(pc_msgs) / ((pc_msgs[-1][0] - pc_msgs[0][0]) / 1e9),
    }
    with open(output_dir / 'meta.json', 'w') as f:
        json.dump(meta, f, indent=2)

Step 3: Generate ground truth occupancy labels

bash
# Option A: LiDAR accumulation (self-supervised, no manual labels)
python scripts/generate_occupancy_labels.py \
    --data-dir /data/airside_dataset/extracted/ \
    --output-dir /data/airside_dataset/occupancy/ \
    --accumulate-frames 20 \    # accumulate 20 past/future frames
    --voxel-size 0.4 \          # 0.4m voxels
    --grid-range "[-40, -40, -3, 40, 40, 5]" \
    --num-workers 16

# Option B: Use pre-trained segmentation model for semantic labels
python scripts/generate_semantic_occupancy.py \
    --data-dir /data/airside_dataset/extracted/ \
    --model-checkpoint pretrained/cylinder3d_nuscenes.pth \
    --remap-classes configs/nuscenes_to_airside_classes.yaml

Step 4: Create dataset splits

bash
python scripts/create_splits.py \
    --data-dir /data/airside_dataset/ \
    --train-ratio 0.8 \
    --val-ratio 0.1 \
    --test-ratio 0.1 \
    --temporal-split \          # split by recording session, not random frames
    --min-sequence-length 20    # at least 20 consecutive frames per sequence

4.3 Phase B: Train BEV Encoder

Step 1: Pre-train on nuScenes (optional but recommended)

bash
# Using OpenPCDet
git clone https://github.com/open-mmlab/OpenPCDet.git
cd OpenPCDet && pip install -e .

# Download nuScenes and create data infos
python -m pcdet.datasets.nuscenes.nuscenes_dataset --func create_nuscenes_infos \
    --cfg_file tools/cfgs/dataset_configs/nuscenes_dataset.yaml \
    --version v1.0-trainval

# Train PointPillars on nuScenes
python -m torch.distributed.launch --nproc_per_node=4 \
    tools/train.py \
    --cfg_file tools/cfgs/nuscenes_models/cbgs_pillar0075_res2d_centerpoint.yaml \
    --batch_size 16 \
    --epochs 20 \
    --launcher pytorch

Step 2: Fine-tune on airside data

bash
# Modify config for airside classes and geometry
# Key changes:
#   - point_cloud_range: [-51.2, -51.2, -3.0, 51.2, 51.2, 5.0]
#   - voxel_size: [0.2, 0.2, 8.0]
#   - class_names: ['vehicle', 'aircraft', 'person', 'gse', 'fod']
#   - Freeze backbone for first 5 epochs, then unfreeze

python -m torch.distributed.launch --nproc_per_node=4 \
    tools/train.py \
    --cfg_file configs/airside/pointpillars_airside_finetune.yaml \
    --pretrained_model output/nuscenes/cbgs_pillar/checkpoint_epoch_20.pth \
    --batch_size 8 \
    --epochs 30 \
    --launcher pytorch

4.4 Phase C: Train VQ-VAE Tokenizer

Step 1: Extract BEV features from trained encoder

bash
# Run trained BEV encoder on full dataset to generate feature maps
python scripts/extract_bev_features.py \
    --data-dir /data/airside_dataset/ \
    --checkpoint output/airside/pointpillars/best.pth \
    --output-dir /data/airside_dataset/bev_features/ \
    --batch-size 32 \
    --num-workers 8
    # Output: (N_frames, 256, 128, 128) saved as .npy per sequence

Step 2: Train VQ-VAE tokenizer

bash
# Train the scene tokenizer
python train_vqvae.py \
    --config configs/vqvae/vqvae_airside.yaml \
    --data-dir /data/airside_dataset/bev_features/ \
    --codebook-size 512 \
    --embedding-dim 256 \
    --commitment-loss-weight 0.25 \
    --lr 3e-4 \
    --batch-size 32 \
    --epochs 100 \
    --gpus 4 \
    --output-dir output/vqvae/

# Key config (vqvae_airside.yaml):
# encoder:
#   type: SwinTransformer
#   embed_dim: 96
#   depths: [2, 2, 6, 2]
#   num_heads: [3, 6, 12, 24]
#   window_size: 8
#   downsample: 1  (no spatial downsampling — keep 128x128)
# decoder:
#   type: SwinTransformerDecoder
#   upsample_layers: [2, 2, 6, 2]
# quantizer:
#   type: EMAVectorQuantizer
#   codebook_size: 512
#   embedding_dim: 256
#   decay: 0.99
#   commitment_cost: 0.25

Step 3: Validate tokenizer reconstruction

bash
python eval_vqvae.py \
    --checkpoint output/vqvae/best.pth \
    --data-dir /data/airside_dataset/bev_features/ \
    --split val \
    --metrics reconstruction_loss codebook_usage perplexity
    # Target: reconstruction L1 < 0.05, codebook usage > 80%, perplexity > 400

4.5 Phase D: Train World Model

Step 1: Generate token sequences from dataset

bash
# Tokenize entire dataset
python scripts/tokenize_dataset.py \
    --data-dir /data/airside_dataset/bev_features/ \
    --vqvae-checkpoint output/vqvae/best.pth \
    --output-dir /data/airside_dataset/tokens/ \
    --batch-size 64
    # Output per sequence: token_indices (T, 128, 128) int16
    #                      ego_poses (T, 7) float64

Step 2: Train world model transformer

bash
# Pre-train on nuScenes tokens (if available), then fine-tune on airside
python train_world_model.py \
    --config configs/world_model/transformer_airside.yaml \
    --data-dir /data/airside_dataset/tokens/ \
    --context-frames 4 \        # condition on 4 past frames
    --predict-frames 5 \        # predict 5 future frames (1 second at 5Hz)
    --lr 1e-4 \
    --batch-size 8 \
    --epochs 50 \
    --gpus 4 \
    --output-dir output/world_model/

# Key config (transformer_airside.yaml):
# model:
#   type: SpatioTemporalTransformer
#   embed_dim: 256
#   num_layers: 6
#   num_heads: 8
#   spatial_block: SwinTransformer
#   temporal_block: CausalGPT2
#   window_size: 8
#   mlp_ratio: 4.0
#   dropout: 0.1
# training:
#   objective_weights:
#     future_prediction: 0.5   # predict future tokens from past
#     joint_modeling: 0.4      # reconstruct past + future
#     unconditional: 0.1       # no conditioning (for CFG)
#   ego_conditioning: true
#   noise_injection: 0.2       # 20% uniform noise on non-masked tokens

Step 3: Train planning head

bash
python train_planner.py \
    --config configs/planning/cost_planner_airside.yaml \
    --world-model-checkpoint output/world_model/best.pth \
    --data-dir /data/airside_dataset/ \
    --lr 5e-4 \
    --batch-size 16 \
    --epochs 30 \
    --gpus 2

4.6 Phase E: Export and Deploy

Step 1: Export to ONNX

bash
# Export each component separately for independent optimization
python scripts/export_onnx.py \
    --component bev_encoder \
    --checkpoint output/airside/pointpillars/best.pth \
    --output bev_encoder.onnx \
    --dynamic-axes "pillars:0,num_pillars:0" \
    --opset 17

python scripts/export_onnx.py \
    --component vqvae_encoder \
    --checkpoint output/vqvae/best.pth \
    --output vqvae_encoder.onnx \
    --opset 17

python scripts/export_onnx.py \
    --component world_model \
    --checkpoint output/world_model/best.pth \
    --output world_model.onnx \
    --dynamic-axes "context_length:1" \
    --opset 17

python scripts/export_onnx.py \
    --component vqvae_decoder \
    --checkpoint output/vqvae/best.pth \
    --output vqvae_decoder.onnx \
    --opset 17

Step 2: Build TensorRT engines

bash
# On the target Orin device (engines are device-specific)

# BEV encoder — FP16
trtexec --onnx=bev_encoder.onnx \
        --saveEngine=bev_encoder_fp16.engine \
        --fp16 \
        --workspace=2048

# VQ-VAE encoder — FP16
trtexec --onnx=vqvae_encoder.onnx \
        --saveEngine=vqvae_encoder_fp16.engine \
        --fp16 \
        --workspace=1024

# World model — FP16 (largest model, most benefit from optimization)
trtexec --onnx=world_model.onnx \
        --saveEngine=world_model_fp16.engine \
        --fp16 \
        --workspace=4096 \
        --minShapes=tokens:1x2x128x128 \
        --optShapes=tokens:1x4x128x128 \
        --maxShapes=tokens:1x8x128x128

# VQ-VAE decoder — FP16
trtexec --onnx=vqvae_decoder.onnx \
        --saveEngine=vqvae_decoder_fp16.engine \
        --fp16 \
        --workspace=1024

# INT8 quantization (optional, for maximum speed)
# Requires calibration dataset
trtexec --onnx=world_model.onnx \
        --saveEngine=world_model_int8.engine \
        --int8 \
        --fp16 \
        --calib=calibration_cache.bin \
        --workspace=4096

Step 3: Validate on device

bash
# Run inference benchmark
trtexec --loadEngine=world_model_fp16.engine \
        --shapes=tokens:1x4x128x128 \
        --iterations=100 \
        --warmUp=10 \
        --avgRuns=10

# Expected output on Orin AGX 64GB:
# Throughput: ~15-25 qps
# Latency (median): ~50-80 ms
# GPU Compute: ~45-70 ms
# H2D + D2H: ~3-5 ms

Step 4: Integration test

bash
# Run full pipeline on recorded bag (offline replay)
python scripts/run_pipeline_offline.py \
    --bag /data/test_bags/airside_test_001.bag \
    --engines-dir /opt/world_model/engines/ \
    --output-dir /data/eval/pipeline_test/ \
    --visualize \
    --save-predictions

# Run online on vehicle (shadow mode)
roslaunch world_model_stack shadow_mode.launch \
    engine_dir:=/opt/world_model/engines/ \
    mode:=shadow

5. Evaluation Pipeline

5.1 Metrics at Each Stage

The evaluation pipeline validates each component independently, then measures end-to-end performance.

 STAGE              METRIC              TARGET (airside)     MEASUREMENT
 ─────              ──────              ────────────────     ───────────
 BEV Encoder        BEV Seg IoU         > 70% mIoU          Per-class IoU on held-out set
                    Detection mAP       > 50% mAP@0.5       AP for vehicle, aircraft, person
                    Latency             < 25 ms              TensorRT profiling on Orin

 VQ-VAE             Reconstruction L1   < 0.05               L1 between input/output features
                    Codebook Usage      > 80%                % of codes used in epoch
                    Perplexity          > 400 (of 512)       exp(entropy) of code distribution
                    Visual Fidelity     Qualitative          Side-by-side BEV comparison

 World Model        1s Prediction IoU   > 40% mIoU           Compare predicted vs GT occupancy
                    2s Prediction IoU   > 25% mIoU           (IoU degrades with horizon)
                    Chamfer Distance    < 2.0m (1s)          Point cloud reconstruction quality
                    Token Accuracy      > 60% (next frame)   % of tokens correctly predicted
                    FVD                 < 200                 Frechet Video Distance (if video)
                    Ego Traj L2         < 1.0m @ 2s          L2 error of ego predictions

 Planning           Collision Rate      < 0.1%               % of plans intersecting GT objects
                    Progress Rate       > 95%                % of goals reached within time
                    Comfort Score       < 2.0 m/s^3 jerk     Max jerk along trajectory
                    Infeasibility Rate  < 1%                 % of dynamically infeasible plans

 End-to-End         Route Completion    > 98%                % of routes completed successfully
                    Collision-free      > 99.9%              % of runs with zero collisions
                    Intervention Rate   < 1 per 10 km        Human takeover frequency
                    Avg Speed           > 3.5 m/s            Operational efficiency (of 5 max)

5.2 BEV Encoder Evaluation

python
def evaluate_bev_encoder(model, dataloader, classes):
    """Evaluate BEV segmentation and detection quality."""
    iou_calculator = IoUCalculator(num_classes=len(classes))
    det_evaluator = DetectionEvaluator(classes, iou_thresholds=[0.3, 0.5, 0.7])
    latencies = []

    for batch in dataloader:
        points, gt_seg, gt_boxes = batch

        t0 = time.perf_counter()
        pred_seg, pred_boxes = model(points)
        torch.cuda.synchronize()
        latencies.append(time.perf_counter() - t0)

        iou_calculator.update(pred_seg, gt_seg)
        det_evaluator.update(pred_boxes, gt_boxes)

    results = {
        'mIoU': iou_calculator.compute_miou(),
        'per_class_iou': iou_calculator.compute_per_class(),
        'mAP@0.5': det_evaluator.compute_map(0.5),
        'latency_ms': {
            'mean': np.mean(latencies) * 1000,
            'p95': np.percentile(latencies, 95) * 1000,
            'p99': np.percentile(latencies, 99) * 1000,
        }
    }
    return results

5.3 VQ-VAE Tokenizer Evaluation

python
def evaluate_vqvae(model, dataloader):
    """Evaluate tokenizer reconstruction quality and codebook health."""
    total_l1 = 0
    code_counts = torch.zeros(model.codebook_size)

    for bev_features in dataloader:
        # Encode → quantize → decode
        z_e = model.encoder(bev_features)
        z_q, indices, commit_loss = model.quantizer(z_e)
        recon = model.decoder(z_q)

        # Reconstruction loss
        total_l1 += F.l1_loss(recon, bev_features).item()

        # Codebook usage tracking
        for idx in indices.flatten():
            code_counts[idx] += 1

    n_batches = len(dataloader)
    usage = (code_counts > 0).float().mean().item()
    perplexity = torch.exp(-torch.sum(
        (code_counts / code_counts.sum()) *
        torch.log(code_counts / code_counts.sum() + 1e-10)
    )).item()

    return {
        'reconstruction_l1': total_l1 / n_batches,
        'codebook_usage': usage,
        'perplexity': perplexity,
        'dead_codes': (code_counts == 0).sum().item(),
    }

5.4 World Model Prediction Evaluation

python
def evaluate_world_model(model, vqvae, dataloader, horizons=[1, 2, 3, 5]):
    """Evaluate world model prediction quality at multiple horizons."""
    metrics = {h: {'iou': [], 'chamfer': [], 'token_acc': []} for h in horizons}

    for context_tokens, future_tokens, ego_poses in dataloader:
        # Predict future
        predicted = model.predict(context_tokens, ego_poses, steps=max(horizons))

        for h in horizons:
            # Token-level accuracy
            pred_h = predicted[:, h-1]              # (B, 128, 128)
            gt_h = future_tokens[:, h-1]            # (B, 128, 128)
            token_acc = (pred_h == gt_h).float().mean().item()

            # Decode to occupancy for IoU
            pred_occ = vqvae.decode(pred_h)          # (B, C, H, W)
            gt_occ = vqvae.decode(gt_h)

            iou = compute_iou(pred_occ.argmax(1), gt_occ.argmax(1))
            chamfer = compute_chamfer(pred_occ, gt_occ)

            metrics[h]['iou'].append(iou)
            metrics[h]['chamfer'].append(chamfer)
            metrics[h]['token_acc'].append(token_acc)

    return {
        f'{h}s': {
            'mIoU': np.mean(metrics[h]['iou']),
            'chamfer_m': np.mean(metrics[h]['chamfer']),
            'token_accuracy': np.mean(metrics[h]['token_acc']),
        }
        for h in horizons
    }

5.5 Planning and End-to-End Evaluation

python
def evaluate_planning_e2e(pipeline, test_scenarios):
    """End-to-end evaluation on recorded or simulated scenarios."""
    results = {
        'route_completion': [],
        'collisions': 0,
        'interventions': 0,
        'total_distance_m': 0,
        'total_time_s': 0,
        'jerk_values': [],
        'speeds': [],
    }

    for scenario in test_scenarios:
        outcome = pipeline.run_scenario(
            scenario,
            max_time=scenario.time_limit,
            record=True,
        )

        results['route_completion'].append(outcome.progress)
        results['collisions'] += outcome.collision_count
        results['interventions'] += outcome.intervention_count
        results['total_distance_m'] += outcome.distance_traveled
        results['total_time_s'] += outcome.duration
        results['jerk_values'].extend(outcome.jerk_profile)
        results['speeds'].extend(outcome.speed_profile)

    n = len(test_scenarios)
    return {
        'route_completion_rate': np.mean(results['route_completion']),
        'collision_rate': results['collisions'] / n,
        'collision_free_rate': 1.0 - (results['collisions'] / n),
        'interventions_per_km': (
            results['interventions'] /
            max(results['total_distance_m'] / 1000, 0.001)
        ),
        'avg_speed_ms': np.mean(results['speeds']),
        'max_jerk': np.max(results['jerk_values']),
        'p95_jerk': np.percentile(results['jerk_values'], 95),
    }

5.6 Automated Evaluation Script

bash
#!/bin/bash
# run_full_evaluation.sh — runs all evaluation stages

set -e

DATA_DIR=/data/airside_dataset
CHECKPOINTS=/output
RESULTS=/data/eval/$(date +%Y%m%d_%H%M%S)
mkdir -p $RESULTS

echo "=== Stage 1: BEV Encoder ==="
python eval/eval_bev_encoder.py \
    --checkpoint $CHECKPOINTS/bev_encoder/best.pth \
    --data-dir $DATA_DIR --split test \
    --output $RESULTS/bev_encoder.json

echo "=== Stage 2: VQ-VAE Tokenizer ==="
python eval/eval_vqvae.py \
    --checkpoint $CHECKPOINTS/vqvae/best.pth \
    --data-dir $DATA_DIR/bev_features --split test \
    --output $RESULTS/vqvae.json

echo "=== Stage 3: World Model ==="
python eval/eval_world_model.py \
    --wm-checkpoint $CHECKPOINTS/world_model/best.pth \
    --vqvae-checkpoint $CHECKPOINTS/vqvae/best.pth \
    --data-dir $DATA_DIR/tokens --split test \
    --horizons 1 2 3 5 \
    --output $RESULTS/world_model.json

echo "=== Stage 4: Planning ==="
python eval/eval_planner.py \
    --pipeline-config configs/pipeline/full_pipeline.yaml \
    --scenarios $DATA_DIR/test_scenarios/ \
    --output $RESULTS/planning.json

echo "=== Stage 5: End-to-End ==="
python eval/eval_e2e.py \
    --pipeline-config configs/pipeline/full_pipeline.yaml \
    --scenarios $DATA_DIR/e2e_test_scenarios/ \
    --output $RESULTS/e2e.json

echo "=== Generating Report ==="
python eval/generate_report.py \
    --results-dir $RESULTS \
    --output $RESULTS/report.html

echo "Results saved to $RESULTS"

6. Failure Modes

6.1 Failure Taxonomy

 STAGE              FAILURE MODE              SEVERITY   DETECTION             RESPONSE
 ─────              ────────────              ────────   ─────────             ────────

 SENSORS
 ├── LiDAR dropout  One or more LiDARs stop   HIGH       Heartbeat timeout     Reduce speed, use
 │                  publishing                            on /rslidar topics    remaining LiDARs

 ├── Point cloud    Points show systematic     MEDIUM     Statistical test on   Recalibrate or
 │   degradation    bias (rain, fog, dust)                point density/range   reduce confidence

 └── Time sync      LiDAR timestamps drift     HIGH       PPS monitoring,       Stop if drift
     drift          from system clock                     NTP check             > 10ms

 BEV ENCODER
 ├── Empty BEV      No pillars generated       HIGH       Check non-empty       Emergency stop
 │                  (total sensor failure)                 pillar count > 100

 ├── Feature         Backbone produces          MEDIUM     L2 norm monitoring    Fall back to
 │   collapse       near-zero features                    on BEV features       rule-based safety

 └── Latency        Inference exceeds          MEDIUM     Wall-clock timing     Skip frame,
     spike          100ms budget                          per cycle             use stale features

 VQ-VAE
 ├── Codebook       >20% of tokens map to      MEDIUM     Track index           Retrain tokenizer
 │   collapse       same few codes                        histogram per frame   with reset

 ├── Reconstruction Input BEV cannot be         HIGH       Online reconstruction Reduce WM weight
 │   failure        faithfully reconstructed              loss monitoring       in planning

 └── OOD tokens     Scene maps to rarely-       MEDIUM     Entropy of code       Flag uncertainty,
                    used codes (novel scene)              distribution          increase caution

 WORLD MODEL
 ├── Hallucination  Predicts objects that       HIGH       Compare prediction    Discard prediction,
 │                  don't exist (phantom                   entropy to threshold  use constant-vel
 │                  aircraft, vehicles)                    > 2.0 nats            extrapolation

 ├── Missed         Fails to predict real       CRITICAL   Cross-check with      Emergency stop if
 │   prediction     dynamic objects                        current perception    close object missed

 ├── Temporal        Predictions flicker or     MEDIUM     Temporal consistency  Smooth predictions
 │   inconsistency  show impossible motion                 score (IoU between   with EMA filter
 │                                                         consecutive preds)

 ├── Ego drift      Predicted ego trajectory    HIGH       L2 between predicted  Clamp ego
 │                  diverges from physics                  and kinematic model   prediction to
 │                                                                              feasible set

 └── Autoregressive Error compounds across      HIGH       Monitor prediction    Limit prediction
     error          predicted frames                      quality vs horizon    horizon to
     accumulation                                                               confident range

 PLANNING
 ├── No feasible    All trajectories score      HIGH       Check if any          Emergency stop +
 │   trajectory     above collision threshold              candidate passes     request human
 │                                                         all safety checks

 ├── Oscillation    Planner alternates          MEDIUM     Track trajectory      Hysteresis on
 │                  between two plans                      change rate           plan selection

 └── Goal           Cannot reach goal due to    LOW        Progress monitoring   Replan to
     unreachable    predicted permanent                    over 10s window       alternate route
                    blockage

 CONTROL
 ├── Actuator       Steering/braking does       CRITICAL   Compare commanded     Emergency stop
 │   failure        not respond                            vs measured state     via redundant
 │                                                                              brake

 └── Tracking       Large error between         MEDIUM     Cross-track error     Reduce speed,
     error          planned and actual path                monitoring            increase control
                                                                                gains

6.2 Detection Mechanisms

Runtime health monitor:

python
class PipelineHealthMonitor:
    """Monitors all pipeline stages for anomalies."""

    def __init__(self):
        self.checks = {
            'sensor_heartbeat':    HeartbeatCheck(timeout_ms=200),
            'bev_feature_norm':    RangeCheck(min_val=0.01, max_val=100.0),
            'codebook_entropy':    ThresholdCheck(min_val=4.0),  # bits
            'prediction_entropy':  ThresholdCheck(max_val=3.0),  # nats
            'ego_prediction_l2':   ThresholdCheck(max_val=2.0),  # meters
            'planning_feasible':   BooleanCheck(expected=True),
            'control_tracking':    ThresholdCheck(max_val=0.5),  # meters
            'cycle_time':          LatencyCheck(budgets={
                'perception': 100, 'world_model': 200,
                'planning': 50, 'control': 20,
            }),
        }

    def check_all(self) -> HealthStatus:
        failures = []
        for name, check in self.checks.items():
            if not check.passes():
                failures.append(HealthFailure(name, check.severity, check.value))

        if any(f.severity == 'CRITICAL' for f in failures):
            return HealthStatus.EMERGENCY_STOP
        elif any(f.severity == 'HIGH' for f in failures):
            return HealthStatus.DEGRADE_TO_SAFETY
        elif any(f.severity == 'MEDIUM' for f in failures):
            return HealthStatus.REDUCE_CONFIDENCE
        return HealthStatus.NOMINAL

6.3 Graceful Degradation Hierarchy

 LEVEL 0: NOMINAL
   All systems operational, full world model pipeline active
   Max speed: 5 m/s, full prediction horizon (1s)

 LEVEL 1: REDUCED CONFIDENCE
   One or more MEDIUM severity issues detected
   Actions:
   ├── Reduce max speed to 3 m/s
   ├── Increase following distance to 8m
   ├── Shorten prediction horizon to 0.6s
   └── Log warnings, continue operation

 LEVEL 2: SAFETY MODE
   One or more HIGH severity issues detected
   Actions:
   ├── Disable world model predictions
   ├── Fall back to rule-based planner (constant velocity extrapolation)
   ├── Reduce max speed to 2 m/s
   ├── Activate safety ensemble voting
   └── Request human monitoring attention

 LEVEL 3: MINIMAL RISK CONDITION
   CRITICAL failure or multiple HIGH failures
   Actions:
   ├── Execute controlled stop (decelerate at 2 m/s^2)
   ├── Activate hazard lights
   ├── Broadcast position to fleet manager
   ├── Hold position until human intervention
   └── Maintain sensor recording for post-incident analysis

6.4 World Model-Specific Safety Checks

python
class WorldModelSafetyValidator:
    """Validates world model outputs before they reach the planner."""

    def __init__(self):
        self.entropy_threshold = 3.0        # nats — high entropy = uncertain
        self.temporal_iou_threshold = 0.3   # minimum IoU between consecutive frames
        self.ego_kinematic_limit = 5.0      # m/s max speed for ego prediction
        self.max_acceleration = 3.0         # m/s^2 max for predicted ego

    def validate(self, prediction, prev_prediction, ego_state):
        issues = []

        # 1. Entropy check — are predictions confident?
        entropy = self.compute_token_entropy(prediction.token_logits)
        if entropy > self.entropy_threshold:
            issues.append(('high_entropy', entropy, 'MEDIUM'))

        # 2. Temporal consistency — do consecutive predictions agree?
        if prev_prediction is not None:
            temporal_iou = self.compute_temporal_iou(
                prediction.occupancy[0],    # first predicted frame
                prev_prediction.occupancy[1] # second frame from previous pred
            )
            if temporal_iou < self.temporal_iou_threshold:
                issues.append(('temporal_inconsistency', temporal_iou, 'HIGH'))

        # 3. Kinematic feasibility — is the ego prediction physically possible?
        ego_traj = prediction.ego_trajectory
        velocities = np.linalg.norm(np.diff(ego_traj[:, :2], axis=0), axis=1) * 5  # 5Hz
        accelerations = np.abs(np.diff(velocities)) * 5

        if np.any(velocities > self.ego_kinematic_limit):
            issues.append(('ego_speed_violation', velocities.max(), 'HIGH'))
        if np.any(accelerations > self.max_acceleration):
            issues.append(('ego_accel_violation', accelerations.max(), 'MEDIUM'))

        # 4. Phantom object check — new large objects appearing from nowhere
        if prev_prediction is not None:
            new_objects = self.detect_phantom_objects(
                prediction.occupancy, prev_prediction.occupancy
            )
            if len(new_objects) > 0:
                issues.append(('phantom_objects', len(new_objects), 'HIGH'))

        return ValidationResult(
            valid=len([i for i in issues if i[2] in ('HIGH', 'CRITICAL')]) == 0,
            issues=issues,
        )

7. Phased Deployment

7.1 Phase Overview

 PHASE 0          PHASE 1          PHASE 2          PHASE 3
 DATA             SHADOW           CAMERAS          VLA
 ────             ──────           ───────          ───
 Duration:        Duration:        Duration:        Duration:
 2-4 months       3-6 months       4-8 months       6-12 months

 Collect          Run world        Add camera       Vision-Language
 training data    model in         fusion, close    Action model
 from existing    shadow mode      the loop on      with language
 fleet ops        alongside        limited routes   reasoning
                  production
                  stack

7.2 Phase 0: Data Collection and Foundation

Objective: Build a training dataset of 100+ hours of airside driving with sensor data, poses, and annotations.

Activities:

  1. Instrument existing fleet vehicles with data recording (ROS bag recording of all sensor topics)
  2. Set up data pipeline: bags → extracted frames → occupancy labels
  3. Train BEV encoder on nuScenes, validate on airside data
  4. Train VQ-VAE tokenizer on airside BEV features
  5. Train initial world model on airside token sequences
  6. Establish evaluation benchmark with held-out test scenarios

Infrastructure:

  • On-vehicle: 2TB NVMe SSD per vehicle for bag recording
  • Off-vehicle: NAS or cloud storage for bag archive (estimate 10-50TB)
  • Training: 4-8x A100 GPUs (cloud or on-prem)
  • CI/CD: automated training pipeline with experiment tracking (W&B / MLflow)

Milestones:

MilestoneTargetMeasurement
M0.1: Bag pipeline operationalWeek 2Can extract, index, process bags automatically
M0.2: 50 hours collectedWeek 6Bag index shows 50+ hours of driving
M0.3: BEV encoder trainedWeek 8mIoU > 50% on airside val set
M0.4: VQ-VAE trainedWeek 10Reconstruction L1 < 0.05, codebook usage > 80%
M0.5: World model v0.1Week 121s prediction IoU > 20% on airside val
M0.6: 100 hours collectedWeek 14Bag index shows 100+ hours
M0.7: World model v0.2Week 161s prediction IoU > 30% on airside val

Go/No-Go Criteria for Phase 1:

  • [REQUIRED] 100+ hours of clean, synchronized sensor data collected
  • [REQUIRED] BEV encoder achieves > 50% mIoU on airside validation set
  • [REQUIRED] VQ-VAE codebook usage > 70%, reconstruction L1 < 0.1
  • [REQUIRED] World model produces visually plausible 1s predictions on 10+ test sequences
  • [DESIRED] 1s prediction IoU > 25%
  • [DESIRED] Automated training pipeline runs end-to-end without manual intervention

7.3 Phase 1: Shadow Mode

Objective: Run the world model pipeline alongside the production stack, comparing predictions to reality without controlling the vehicle.

Activities:

  1. Deploy world model stack as shadow ROS namespace
  2. Record shadow stack predictions + production stack decisions + actual outcomes
  3. Continuously compare world model predictions against reality (closed-loop validation)
  4. Mine disagreements between shadow and production for hard examples
  5. Iteratively retrain world model on growing dataset (data flywheel)
  6. Implement and validate safety monitor / arbitrator

Architecture:

 Production Stack                Shadow Stack (World Model)
 ┌────────────────┐             ┌────────────────────────┐
 │ Existing        │             │ BEV Encoder             │
 │ Perception      │←─ LiDAR ──→│ VQ-VAE Tokenizer        │
 │ Planning        │   (shared) │ World Model Transformer  │
 │ Control ────────│──► VEHICLE │ Planner (scoring only)   │
 └────────────────┘             └──────────┬─────────────┘

                                     ┌──────▼──────┐
                                     │ Comparator   │
                                     │ - pred vs GT │
                                     │ - shadow vs  │
                                     │   production │
                                     │ - log + mine │
                                     └─────────────┘

Milestones:

MilestoneTargetMeasurement
M1.1: Shadow stack runs on vehicleWeek 4All nodes publish, no crashes for 1 hour
M1.2: Comparator logging worksWeek 6Can replay and visualize pred vs reality
M1.3: Shadow runs 100 hoursWeek 12No OOM, no crashes, <1% frame drops
M1.4: 1s pred IoU > 35%Week 16Measured on real-time vehicle data
M1.5: Planning agreement > 70%Week 20Shadow planner agrees with production 70%+
M1.6: Safety monitor validatedWeek 24All injected faults detected, <5% false positive

Go/No-Go Criteria for Phase 2:

  • [REQUIRED] Shadow stack runs continuously for 500+ hours with no crashes or OOM
  • [REQUIRED] 1s prediction IoU > 35% measured on live vehicle data
  • [REQUIRED] Safety monitor detects 100% of injected critical faults
  • [REQUIRED] Safety monitor false positive rate < 5%
  • [REQUIRED] Shadow planner agrees with production planner on > 70% of decisions
  • [REQUIRED] No cases where shadow planner would have caused collision that production avoided
  • [DESIRED] 1s prediction IoU > 40%
  • [DESIRED] World model correctly predicts > 80% of dynamic object behaviors

7.4 Phase 2: Camera Fusion and Closed Loop

Objective: Add cameras to the perception pipeline, fuse camera + LiDAR BEV features, and begin closed-loop control on limited routes.

Activities:

  1. Install camera rig on test vehicles (6 cameras for 360 coverage)
  2. Train camera-to-BEV encoder (LSS or BEVFormer)
  3. Train fused BEV encoder (LiDAR + camera)
  4. Retrain VQ-VAE and world model on fused features
  5. Validate on shadow mode with cameras
  6. Begin closed-loop control on single, well-mapped test route
  7. Gradually expand to more routes as confidence builds

Camera configuration for airside:

yaml
cameras:
  front:      {fov: 70, resolution: [1920, 1080], fps: 10}
  front_left: {fov: 70, resolution: [1920, 1080], fps: 10}
  front_right:{fov: 70, resolution: [1920, 1080], fps: 10}
  rear:       {fov: 70, resolution: [1920, 1080], fps: 10}
  rear_left:  {fov: 70, resolution: [1920, 1080], fps: 10}
  rear_right: {fov: 70, resolution: [1920, 1080], fps: 10}

Closed-loop deployment rules:

  • Safety driver always present with physical e-stop
  • Maximum speed: 3 m/s initially, increase to 5 m/s after 50 hours incident-free
  • Operating hours: daylight only initially, expand to night after 100 hours
  • Weather: clear conditions initially, expand to rain after 200 hours
  • Route: single fixed route initially, expand after each 100-hour incident-free window

Milestones:

MilestoneTargetMeasurement
M2.1: Camera rig installedWeek 4All 6 cameras publishing, calibrated
M2.2: Camera BEV encoder trainedWeek 8Camera-only mIoU > 40%
M2.3: Fused BEV > LiDAR-onlyWeek 12Fused mIoU > LiDAR-only mIoU
M2.4: Retrained WM on fusedWeek 161s pred IoU > 45% with fusion
M2.5: First closed-loop runWeek 20Complete 1 route, 0 interventions
M2.6: 50 hours closed-loopWeek 28< 2 interventions per 10 km
M2.7: 200 hours closed-loopWeek 32< 0.5 interventions per 10 km

Go/No-Go Criteria for Phase 3:

  • [REQUIRED] 200+ hours of closed-loop operation with < 1 intervention per 10 km
  • [REQUIRED] Zero collisions in closed-loop operation
  • [REQUIRED] World model prediction IoU > 45% at 1s, > 30% at 2s
  • [REQUIRED] System operates in day + night + light rain conditions
  • [REQUIRED] Fused perception outperforms LiDAR-only on all metrics
  • [DESIRED] Route completion rate > 98%
  • [DESIRED] Average operational speed > 4 m/s

7.5 Phase 3: Vision-Language-Action Model

Objective: Integrate language reasoning into the driving pipeline for complex airside scenarios (e.g., understanding marshaller signals, radio instructions, signage).

Activities:

  1. Collect language-annotated driving data (narrated scenarios, instruction-following)
  2. Fine-tune VLA model (based on AutoVLA, VQ-VLA, or similar) on airside data
  3. Implement dual-system architecture: fast planner (world model) + slow reasoner (VLA)
  4. VLA handles edge cases: unusual aircraft types, construction zones, emergency vehicles
  5. World model handles routine navigation at high frequency
  6. Extensive validation in simulation and shadow mode before any closed-loop

VLA Architecture for Airside:

 ┌─────────────────────────────────────────────────────────────┐
 │                    DUAL-SYSTEM VLA                          │
 │                                                             │
 │  SYSTEM 1 (Fast, 20Hz)          SYSTEM 2 (Slow, 2Hz)       │
 │  ┌────────────────────┐         ┌──────────────────────┐    │
 │  │ World Model        │         │ VLA (Vision-Language) │    │
 │  │ ├── BEV Encoder    │         │ ├── Image Encoder     │    │
 │  │ ├── VQ-VAE         │         │ ├── Language Model    │    │
 │  │ ├── Transformer    │         │ ├── Action Decoder    │    │
 │  │ └── Planner        │         │ └── Reasoning Chain   │    │
 │  └────────┬───────────┘         └──────────┬───────────┘    │
 │           │                                │                │
 │           ▼                                ▼                │
 │  ┌────────────────────────────────────────────────────┐     │
 │  │              Arbitration Layer                      │     │
 │  │  - System 2 provides high-level intent/constraints  │     │
 │  │  - System 1 executes within those constraints       │     │
 │  │  - System 2 can override System 1 for edge cases    │     │
 │  └────────────────────────────────────┬───────────────┘     │
 │                                       │                     │
 │                                       ▼                     │
 │                                   CONTROL                   │
 └─────────────────────────────────────────────────────────────┘

Milestones:

MilestoneTargetMeasurement
M3.1: Language dataset collectedWeek 810K+ annotated scenarios
M3.2: VLA fine-tuned on airsideWeek 16Passes 80% of edge-case scenarios
M3.3: Dual-system running shadowWeek 24VLA correctly classifies 90%+ of scenarios
M3.4: VLA handles marshaller signalsWeek 32Detects and follows 95%+ of signals
M3.5: Full VLA closed-loop (limited)Week 40100 hours, < 0.5 interventions/10km

Go/No-Go for production deployment:

  • [REQUIRED] 1000+ hours total closed-loop operation across all conditions
  • [REQUIRED] Zero at-fault collisions
  • [REQUIRED] Intervention rate < 0.1 per 10 km
  • [REQUIRED] Handles all standard airside scenarios (pushback, towing, stand approach, taxiway crossing)
  • [REQUIRED] Passes FAA AGVS safety review (AC 150/5210-XX or equivalent)
  • [REQUIRED] Redundant safety systems validated (hardware e-stop, RSS, watchdog)

8. Quick Start Guide

8.1 Objective

Get a world model predicting airside scenes from LiDAR bags within 1 week.

This guide takes the fastest path: use OccWorld (open-source, ECCV 2024) with nuScenes pre-trained weights, adapt minimally to airside LiDAR data, and get predictions running.

8.2 Day 1-2: Environment and Data

Set up compute (2 hours):

bash
# Option A: Cloud GPU (recommended for speed)
# Provision 1x A100 80GB on Lambda Labs, RunPod, or CoreWeave
# ~$1.10-1.64/hr

# Option B: Local workstation with RTX 4090 (24GB, will be tight)

# Base environment
conda create -n worldmodel python=3.10 -y
conda activate worldmodel

# PyTorch 2.1+ with CUDA 11.8
pip install torch==2.1.0 torchvision==0.16.0 --index-url https://download.pytorch.org/whl/cu118

# OccWorld dependencies
pip install mmcv==2.1.0 -f https://download.openmmlab.com/mmcv/dist/cu118/torch2.1/index.html
pip install mmdet==3.2.0 mmsegmentation==1.2.2
pip install mmdet3d==1.4.0
pip install einops timm open3d wandb

# Clone OccWorld
git clone https://github.com/wzzheng/OccWorld.git
cd OccWorld
pip install -e .

Prepare airside data (4 hours):

bash
# 1. Extract point clouds and poses from ROS bags
pip install rosbags

python scripts/extract_from_bags.py \
    --bag-dir /path/to/airside/bags/ \
    --output-dir /data/airside_quick/ \
    --lidar-topic /pointcloud_aggregator/output \
    --odom-topic /odom/fused \
    --max-bags 10 \
    --target-hz 2  # downsample to 2Hz for training (matches nuScenes)

# 2. Generate self-supervised occupancy labels
# Accumulate 10 future + 10 past frames → dense occupancy ground truth
python scripts/generate_occ_labels.py \
    --data-dir /data/airside_quick/extracted/ \
    --output-dir /data/airside_quick/occupancy/ \
    --accumulate-frames 10 \
    --voxel-size 0.4 \
    --grid-size "200 200 16" \
    --range "[-40, -40, -3, 40, 40, 5]"

# 3. Format into OccWorld-compatible structure
python scripts/format_for_occworld.py \
    --data-dir /data/airside_quick/ \
    --output-dir /data/airside_occworld/ \
    --sequence-length 20

8.3 Day 3-4: Train Models

Train VQ-VAE scene tokenizer (8-12 hours):

bash
# Use pre-trained nuScenes weights as initialization
# Download OccWorld pre-trained weights
wget https://github.com/wzzheng/OccWorld/releases/download/v1.0/occworld_nuscenes.pth

# Train tokenizer on airside occupancy
python tools/train.py \
    configs/occworld/occworld_tokenizer_airside.py \
    --work-dir output/tokenizer_airside/ \
    --load-from occworld_nuscenes.pth \
    --cfg-options \
        data_root=/data/airside_occworld/ \
        train_dataloader.batch_size=8 \
        optim_wrapper.optimizer.lr=1e-4 \
        train_cfg.max_epochs=50

# Monitor training
# Target: reconstruction_loss < 0.1 by epoch 30
# codebook_perplexity > 300 (of 512 codes)

Train world model transformer (12-24 hours):

bash
# Train GPT-style transformer on tokenized sequences
python tools/train.py \
    configs/occworld/occworld_transformer_airside.py \
    --work-dir output/worldmodel_airside/ \
    --load-from output/tokenizer_airside/best.pth \
    --cfg-options \
        data_root=/data/airside_occworld/ \
        train_dataloader.batch_size=4 \
        optim_wrapper.optimizer.lr=5e-5 \
        train_cfg.max_epochs=30 \
        model.world_model.num_layers=6 \
        model.world_model.num_heads=8 \
        model.world_model.embed_dim=256

# Monitor training
# Target: prediction_loss decreasing, token_accuracy > 40% by epoch 20

8.4 Day 5-6: Evaluate and Visualize

Run predictions on test sequences:

bash
# Generate predictions
python tools/test.py \
    configs/occworld/occworld_transformer_airside.py \
    output/worldmodel_airside/best.pth \
    --work-dir output/eval_airside/ \
    --cfg-options data_root=/data/airside_occworld/

# Visualize predictions vs ground truth
python scripts/visualize_predictions.py \
    --pred-dir output/eval_airside/predictions/ \
    --gt-dir /data/airside_occworld/occupancy/ \
    --output-dir output/eval_airside/vis/ \
    --format video \
    --fps 2

Quick evaluation script:

python
#!/usr/bin/env python3
"""Quick evaluation of world model predictions."""

import numpy as np
from pathlib import Path

def quick_eval(pred_dir, gt_dir, horizons=[1, 2, 5]):
    pred_files = sorted(Path(pred_dir).glob('*.npy'))
    gt_files = sorted(Path(gt_dir).glob('*.npy'))

    for h in horizons:
        ious = []
        for pf, gf in zip(pred_files, gt_files):
            pred = np.load(pf)  # (T, 200, 200, 16)
            gt = np.load(gf)

            if h >= pred.shape[0]:
                continue

            # Binary IoU (occupied vs free)
            pred_occ = pred[h] > 0
            gt_occ = gt[h] > 0

            intersection = (pred_occ & gt_occ).sum()
            union = (pred_occ | gt_occ).sum()
            iou = intersection / max(union, 1)
            ious.append(iou)

        print(f"Horizon {h} ({h*0.5:.1f}s): IoU = {np.mean(ious):.3f} "
              f"(+/- {np.std(ious):.3f})")

quick_eval('output/eval_airside/predictions/', '/data/airside_occworld/occupancy/')

8.5 Day 7: Offline Replay on Vehicle Data

Run world model on a recorded bag (no vehicle control):

bash
# Convert trained model to inference-friendly format
python scripts/export_inference.py \
    --config configs/occworld/occworld_transformer_airside.py \
    --checkpoint output/worldmodel_airside/best.pth \
    --output output/inference_model/

# Replay a bag through the world model pipeline
python scripts/replay_bag_with_worldmodel.py \
    --bag /data/airside_bags/test_drive_001.bag \
    --model-dir output/inference_model/ \
    --lidar-topic /pointcloud_aggregator/output \
    --odom-topic /odom/fused \
    --output-dir output/replay_results/ \
    --visualize

What you should see after 1 week:

  • The world model takes a LiDAR point cloud sequence and predicts what the scene will look like 0.5-2.5 seconds in the future
  • Predictions should show: static structure (buildings, fences) persists correctly; moving vehicles/GSE are extrapolated forward; overall scene layout is recognizable
  • Quality will be rough (IoU ~20-30% at 1s) — this is expected for a week-one prototype with limited data
  • Primary value: validates the entire pipeline works end-to-end, identifies data quality issues, gives a concrete baseline to improve

8.6 What to Improve Next

After the quick start, the path to production quality:

PriorityActionExpected Improvement
1Collect more data (target 100+ hours)10-15% IoU improvement
2Add ego velocity conditioning to world modelBetter dynamic predictions
3Tune VQ-VAE codebook size (try 256, 512, 1024)Sharper reconstructions
4Add semantic classes to occupancy labelsClass-specific prediction quality
5Implement Copilot4D-style discrete diffusionHigher quality multi-modal predictions
6Add camera fusion (Phase 2)Better far-field and semantic understanding
7Train planning head on world model outputsClose the perception-action loop
8Export to TensorRT for on-vehicle deploymentReal-time inference on Orin

Sources

Public research notes collected from public sources.