| # 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 |
|
|
| ```xml |
| <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: |
| ```xml |
| <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: |
|
|
| ```python |
| 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.** |
|
|
| ```python |
| 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 |
|
|
| ```python |
| 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 |
|
|
| ```python |
| 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. |
|
|