WorldRepresentation / docs /06_mujoco_scenes_and_code.md
Nirav Madhani
Import World Representation Space
5af572e
|
Raw
History Blame Contribute Delete
15 kB

MuJoCo Scene Designs and Code Architecture


Scene 1: The Indistinguishable Pair (Chapter 1)

Two visually identical cubes with different physics. This is the single most important demo because it makes the argument in 5 seconds.

MuJoCo XML Sketch

<mujoco model="indistinguishable_pair">
  <worldbody>
    <body name="table">
      <geom type="box" size="0.5 0.5 0.02" pos="0 0 0.5" rgba="0.6 0.6 0.6 1"/>
    </body>
    
    <!-- Heavy cube: steel, 2kg -->
    <body name="cube_heavy" pos="-0.15 0 0.55">
      <joint type="free"/>
      <geom type="box" size="0.03 0.03 0.03" mass="2.0" rgba="0.8 0.2 0.2 1"
            friction="0.5 0.005 0.001"/>
    </body>
    
    <!-- Light cube: foam, 0.02kg -->
    <body name="cube_light" pos="0.15 0 0.55">
      <joint type="free"/>
      <geom type="box" size="0.03 0.03 0.03" mass="0.02" rgba="0.8 0.2 0.2 1"
            friction="0.3 0.005 0.001"/>
    </body>
    
    <!-- Pusher (robot finger proxy) -->
    <body name="pusher" pos="0 -0.15 0.55">
      <joint type="slide" axis="0 1 0" range="-0.3 0.3"/>
      <joint type="slide" axis="1 0 0" range="-0.3 0.3"/>
      <geom type="sphere" size="0.015" rgba="0.3 0.3 0.8 1"/>
    </body>
  </worldbody>
  
  <actuator>
    <velocity joint="pusher_slide_y" kv="50"/>
    <velocity joint="pusher_slide_x" kv="50"/>
  </actuator>
</mujoco>

What You Measure

Push both cubes with identical force. Record:

  • Displacement over time
  • Final position
  • Contact forces

The demo shows: identical visual input β†’ completely different physical outcomes β†’ video prediction fails, field + material estimation succeeds.


Scene 2: The Tabletop (Chapters 2, 3, 4)

The workhorse scene. 5 objects with varied materials on a table. Used for SDF reconstruction, material estimation, object tracking, and mixture-of-experts.

Objects

Object Shape Material Properties Purpose
Wooden block Box E=10GPa, ρ=600, μ=0.4, e=0.3 Baseline rigid object
Rubber ball Sphere E=0.01GPa, ρ=1100, μ=0.8, e=0.7 High friction + bounce
Metal cylinder Cylinder E=200GPa, ρ=7800, μ=0.2, e=0.5 Heavy, low friction
Plastic cup Cylinder (hollow) E=3GPa, ρ=1200, μ=0.35, e=0.4 Container (for future pouring)
Sponge Box E=0.001GPa, ρ=30, μ=0.6, e=0.1 Deformable (soft body in MuJoCo)

Camera Setup

4 cameras to support viewpoint experiments:

<visual>
  <!-- Top-down -->
  <camera name="top" pos="0 0 1.5" quat="1 0 0 0"/>
  <!-- Front -->
  <camera name="front" pos="0 -0.8 0.8" xyaxes="1 0 0 0 0.707 0.707"/>
  <!-- Side -->
  <camera name="side" pos="0.8 0 0.8" xyaxes="0 1 0 -0.707 0 0.707"/>
  <!-- Ego (wrist-like) -->
  <camera name="ego" pos="0 -0.3 0.65" xyaxes="1 0 0 0 0.5 0.866"/>
</visual>

Scene 3: The Occlusion Test (Chapter 3)

Two identical objects. An occluder wall in the middle. One object is pushed behind the wall and comes back.

Time 0:   [A]  |wall|  [B]     ← A and B visible, distinct
Time 1:   [Aβ†’] |wall|  [B]     ← A moving toward wall
Time 2:        |wall|  [B]     ← A fully occluded
Time 3:   [←A] |wall|  [B]     ← A re-emerges. Is it still A?

For the field-only model: it has no persistent identity, so after occlusion it might assign A's latent to B or vice versa. The graph model maintains the node for A with existence probability decaying slightly but not zeroing out, and recovers identity on re-emergence via appearance + position matching.


Scene 4: The Stack (Chapters 3, 6)

Three blocks stacked. Tests support relation inference and prediction when a block is removed.

[C]  ← top
[B]  ← middle  
[A]  ← bottom (on table)

Remove B β†’ C should fall. The graph knows (A supports B) and (B supports C). Removing B breaks the chain. The field predicts where C lands based on geometry + dynamics.


Code Architecture

hybrid_world_model/
β”‚
β”œβ”€β”€ envs/                          # MuJoCo environments
β”‚   β”œβ”€β”€ base_env.py                # Common: step, render, get_obs, get_ground_truth
β”‚   β”œβ”€β”€ indistinguishable.py       # Scene 1
β”‚   β”œβ”€β”€ tabletop.py                # Scene 2
β”‚   β”œβ”€β”€ occlusion.py               # Scene 3
β”‚   └── stack.py                   # Scene 4
β”‚
β”œβ”€β”€ model/
β”‚   β”œβ”€β”€ field.py                   # Neural field MLP
β”‚   β”‚   class ObjectField(nn.Module):
β”‚   β”‚       """Maps (x_local, z_i) β†’ (sdf_mu, sdf_sigma, material_params)"""
β”‚   β”‚       def __init__(self, latent_dim=128, hidden=256, layers=4):
β”‚   β”‚       def forward(self, x_local, z_i):
β”‚   β”‚           β†’ returns dict: {
β”‚   β”‚               'sdf_mu': ..., 'sdf_sigma': ...,
β”‚   β”‚               'friction_mu': ..., 'friction_sigma': ...,
β”‚   β”‚               'density_mu': ..., 'density_sigma': ...,
β”‚   β”‚               'stiffness_mu': ..., 'stiffness_sigma': ...
β”‚   β”‚           }
β”‚   β”‚
β”‚   β”œβ”€β”€ mixture.py                 # Mixture of experts
β”‚   β”‚   class MixtureField(nn.Module):
β”‚   β”‚       """Global field = Ξ£ w_i * ObjectField_i + w_0 * BackgroundField"""
β”‚   β”‚       def __init__(self, object_field, gating_network, n_objects):
β”‚   β”‚       def query(self, x_world, object_poses, object_latents):
β”‚   β”‚           # 1. Transform x_world to each object frame
β”‚   β”‚           # 2. Query each local field
β”‚   β”‚           # 3. Compute gating weights
β”‚   β”‚           # 4. Return mixture distribution
β”‚   β”‚           β†’ returns dict with mixture predictions
β”‚   β”‚
β”‚   β”œβ”€β”€ gating.py                  # Gating network
β”‚   β”‚   class GatingNetwork(nn.Module):
β”‚   β”‚       """SDF-based prior + learned residual β†’ ownership weights"""
β”‚   β”‚       def forward(self, sdf_values, x_world, object_latents):
β”‚   β”‚           β†’ returns weights [w_0, w_1, ..., w_N], sum to 1
β”‚   β”‚
β”‚   β”œβ”€β”€ graph.py                   # Object graph
β”‚   β”‚   class ObjectGraph:
β”‚   β”‚       """Nodes: (pose, latent, existence). Edges: (contact_prob, type)"""
β”‚   β”‚       def __init__(self, max_objects=10):
β”‚   β”‚       def update(self, observations):     # Update poses, latents, contacts
β”‚   β”‚       def get_contacts(self):             # Return active contact pairs
β”‚   β”‚       def get_support_chain(self, obj_i): # Trace support relations
β”‚   β”‚
β”‚   β”œβ”€β”€ encoder.py                 # Observation β†’ state
β”‚   β”‚   class WorldModelEncoder(nn.Module):
β”‚   β”‚       """RGB + depth β†’ updated (field, graph) state"""
β”‚   β”‚       def __init__(self, vision_backbone='resnet18'):
β”‚   β”‚       def forward(self, rgb, depth, prev_state):
β”‚   β”‚           # 1. Extract visual features
β”‚   β”‚           # 2. Segment objects (or use GT masks in sim)
β”‚   β”‚           # 3. Update per-object latents z_i
β”‚   β”‚           # 4. Update poses T_i
β”‚   β”‚           # 5. Update contact probabilities
β”‚   β”‚           β†’ returns WorldState(field, graph)
β”‚   β”‚
β”‚   └── dynamics.py                # State prediction
β”‚       class DynamicsModel(nn.Module):
β”‚           """Predict S_{t+1} from S_t and action a_t"""
β”‚           def __init__(self):
β”‚           def forward(self, state, action):
β”‚               # 1. GNN message passing on graph
β”‚               # 2. Update object latents and poses
β”‚               # 3. Update contact probabilities
β”‚               β†’ returns predicted WorldState
β”‚
β”œβ”€β”€ training/
β”‚   β”œβ”€β”€ data_collector.py          # Run MuJoCo, collect trajectories
β”‚   β”œβ”€β”€ train_field.py             # Train SDF field on GT data
β”‚   β”œβ”€β”€ train_encoder.py           # Train encoder on observations
β”‚   β”œβ”€β”€ train_dynamics.py          # Train dynamics on trajectory data
β”‚   └── train_full.py              # End-to-end training
β”‚
β”œβ”€β”€ evaluation/
β”‚   β”œβ”€β”€ sdf_metrics.py             # SDF MAE, IoU, Chamfer, calibration
β”‚   β”œβ”€β”€ material_metrics.py        # Material param estimation error
β”‚   β”œβ”€β”€ tracking_metrics.py        # Identity tracking accuracy
β”‚   └── planning_metrics.py        # Grasp/push success rate
β”‚
β”œβ”€β”€ planning/
β”‚   β”œβ”€β”€ cem.py                     # Cross-entropy method planner
β”‚   β”œβ”€β”€ grasp_planner.py           # Grasp force selection from field queries
β”‚   └── push_planner.py            # Push planning using dynamics model
β”‚
β”œβ”€β”€ visualization/
β”‚   β”œβ”€β”€ field_vis.py               # SDF slices, isosurfaces, uncertainty heatmaps
β”‚   β”œβ”€β”€ graph_vis.py               # Graph topology visualization
β”‚   β”œβ”€β”€ ownership_vis.py           # Gating weight heatmaps
β”‚   └── material_convergence.py    # Distribution evolution plots
β”‚
β”œβ”€β”€ demos/                         # Gradio demo apps (one per chapter)
β”‚   β”œβ”€β”€ demo_ch1_why_not_video.py
β”‚   β”œβ”€β”€ demo_ch2_belief_field.py
β”‚   β”œβ”€β”€ demo_ch3_object_graph.py
β”‚   β”œβ”€β”€ demo_ch4_mixture.py
β”‚   β”œβ”€β”€ demo_ch5_viewpoint.py
β”‚   └── demo_ch6_control.py
β”‚
β”œβ”€β”€ app.py                         # Main Gradio app (combines all demos)
└── requirements.txt

Key Implementation Decisions

Simplification 1: Use ground truth segmentation masks from MuJoCo.

MuJoCo gives you per-object segmentation for free (via mj_render with segmentation flag). In the minimal version, don't build a learned segmentor β€” just use GT masks. This lets you focus on the field + graph + dynamics, which is the novel part. State explicitly: "We use oracle segmentation; replacing this with learned segmentation (e.g., SAM2) is future work."

Simplification 2: Train SDF field with direct supervision.

MuJoCo gives you exact object meshes β†’ exact SDFs. Compute GT SDF on a grid, train the field MLP to match. No need for differentiable rendering in the minimal version. The field learns to represent geometry + material under object-conditioned querying.

Simplification 3: Simple material estimation loop.

Don't do full Bayesian inference. Start with a point estimate + learned uncertainty:

class MaterialEstimator(nn.Module):
    def forward(self, interaction_history):
        """
        interaction_history: list of (action, observed_outcome) pairs
        Returns: material parameter estimates with uncertainty
        """
        # Encode history with a small transformer or RNN
        h = self.history_encoder(interaction_history)
        mu = self.mu_head(h)       # point estimate
        sigma = self.sigma_head(h)  # uncertainty (softplus to keep positive)
        return mu, sigma

This is simpler than the full variational approach but demonstrates the same concept: uncertainty decreases with more interactions, estimates converge to ground truth.

Simplification 4: Language prior as lookup table.

MATERIAL_PRIORS = {
    "rubber":  {"friction": (0.8, 0.1), "stiffness": (0.01, 0.005), "density": (1100, 200)},
    "steel":   {"friction": (0.2, 0.05), "stiffness": (200, 20), "density": (7800, 500)},
    "wood":    {"friction": (0.4, 0.1), "stiffness": (10, 3), "density": (600, 100)},
    "glass":   {"friction": (0.15, 0.05), "stiffness": (70, 10), "density": (2500, 300)},
    "plastic": {"friction": (0.35, 0.08), "stiffness": (3, 1), "density": (1200, 200)},
}

def get_language_prior(text):
    """Returns (mean, std) for each material parameter."""
    text = text.lower()
    for material, params in MATERIAL_PRIORS.items():
        if material in text:
            return params
    return DEFAULT_PRIOR  # uninformative

No language model needed. Demonstrates the interface. You state: "In the full system, this lookup would be replaced by a language encoder mapping arbitrary text to material prior distributions."


Data Collection Protocol

Per-Scene Rollout Collection

def collect_rollouts(env, n_rollouts=1000, max_steps=100):
    """Collect training data from MuJoCo environment."""
    dataset = []
    for i in range(n_rollouts):
        obs = env.reset(randomize=True)  # Random object positions, orientations
        trajectory = []
        for t in range(max_steps):
            # Random exploration action (or scripted interactions)
            action = sample_action(env)  # push, poke, drop, slide
            
            # Record everything
            step_data = {
                'rgb': env.render(camera='front', mode='rgb'),
                'depth': env.render(camera='front', mode='depth'),
                'rgb_top': env.render(camera='top', mode='rgb'),
                'segmentation': env.render(camera='front', mode='segmentation'),
                # Ground truth from simulator
                'gt_poses': env.get_object_poses(),           # SE(3) per object
                'gt_velocities': env.get_object_velocities(), # 6D per object
                'gt_contacts': env.get_contacts(),            # contact pairs + forces
                'gt_sdf_samples': env.sample_sdf(n=1000),     # random 3D points + SDF values
                'gt_materials': env.get_material_params(),     # per-object material vectors
                'action': action,
            }
            trajectory.append(step_data)
            obs = env.step(action)
        
        dataset.append(trajectory)
    return dataset

Interaction Types for Material Estimation

INTERACTION_SCRIPTS = {
    'push': lambda env, obj: env.apply_force(obj, direction='x', magnitude=5.0),
    'poke': lambda env, obj: env.apply_impulse(obj, direction='z', magnitude=2.0),
    'slide': lambda env, obj: env.apply_force(obj, direction='x', magnitude=1.0, duration=0.5),
    'drop': lambda env, obj: env.set_position(obj, height=0.3),  # drop from 30cm
    'tap': lambda env, obj: env.apply_impulse(obj, direction='random', magnitude=0.5),
}

Each interaction type provides different material information:

  • Push/slide β†’ friction (how far it goes)
  • Drop/tap β†’ restitution (how high it bounces) + density (acceleration under gravity)
  • Poke β†’ stiffness (deformation response; mainly for soft objects)

Training Schedule (Single GPU)

Stage What Training Time Data
1 SDF field (geometry only) 2-4 hours 1000 static scenes, GT SDF samples
2 Material estimator 4-8 hours 1000 interaction rollouts with GT materials
3 Gating network 2-4 hours Same static scenes, multi-object
4 Encoder (vision β†’ state) 8-16 hours 1000 rollouts, RGB+depth β†’ state
5 Dynamics model 8-16 hours 1000 rollouts, state β†’ next state
6 End-to-end fine-tuning 16-24 hours All data, all losses

Total: ~2-4 days on a single A100 or equivalent.

For V1 / MVP, you can skip stages 4-6 and just show the field + material estimation with GT encoder (use MuJoCo's GT poses/segmentation as the encoder). This is ~1 day of training and still demonstrates the core ideas.