# YAM simulation — canonical knowledge

> Primary contributor: **yam_sim**. **yams** may append (real-robot findings that touch sim). Written 2026-07-02 as the yam_sim onboarding brain-dump from inherited context; keep alive as the sim-side single source of truth.

---

## 0. TL;DR

- **YAM sim = MuJoCo (3.1.3+).** We use a wrapper of the upstream MuJoCo Menagerie `i2rt_yam` with our own base-board (fiducial exoskeleton) additions baked in — the "fidex" tree under `raiden_fork/third_party/exo_redo/robot_models/i2rt_yam_fidex/`.
- **Two active sim workflows:** (1) single-arm interactive viewer, (2) bimanual interactive viewer + headless renderers used both for offline verification and to overlay sim-rendered robot masks onto real camera frames during calibration.
- **Fiducial exoskeleton in sim** is *rendering + validation*, not calibration itself. The calibration solver lives on the real side (yams). Sim's job here is (a) draw the exo board where FK says it is so we can eyeball the sim vs. real alignment, and (b) provide the ground-truth `T_link_in_world` that a sim-rendered dataset needs.
- **Everything below assumes MuJoCo XML** (menagerie / fidex tree). We have both a bare-arm XML and a with-exo-board XML — pick per task.

---

## 1. Robot model files

### Where our XMLs live

| Path (relative to `raiden_fork/`) | Contents | Used by |
|---|---|---|
| `third_party/exo_redo/robot_models/i2rt_yam_fidex/yam.xml` | Single YAM arm + base-board mount attachment site. Fork of upstream MuJoCo Menagerie's `i2rt_yam`. | Everything under `exo_redo` — `sim_yam.py`, `sim_yam_bimanual.py`, `build_bimanual_xml`, `position_exoskeleton_meshes`. |
| `third_party/exo_redo/robot_models/i2rt_yam_fidex/yam_background.xml` | Background floor / skybox for calibration renderer. | `_render_with_intrinsics`. |
| `third_party/exo_redo/robot_models/i2rt_yam_fidex/scene.xml` | Arm-plus-background convenience scene. | Standalone viewer sanity checks. |
| `third_party/exo_redo/robot_models/i2rt_yam_fidex/assets/model2.stl` | Arm base mesh (referenced as `YAM_BASE_MESH` in `yam_exo.py`). | XML `<mesh>` refs. |
| `third_party/exo_redo/robot_models/i2rt_yam_fidex/assets/*.stl` | Per-link meshes (shoulder / elbow / … / gripper fingers). | XML mesh library. |

### Kinematics-only vs. rendering path

There are two independent access paths to "the YAM model" — do not confuse:

- **Kinematics / FK / IK**: `raiden.i2rt.robots.kinematics.Kinematics(raiden._xml_paths.get_yam_4310_linear_xml_path(), site_name="grasp_site")`. This is a **bare-arm XML** (no exo, no board). Used for solving IK for teleop and deployment. It is NOT for rendering.
- **Rendering + collision + composition**: The `i2rt_yam_fidex/yam.xml` (or the bimanual composed variant). Used for MuJoCo viewer, overlay rendering, sim training scenes.

**MuJoCo version requirement:** 3.1.3+ (per `i2rt_yam_fidex/README.md`).

### Upstream references

- YAM robot product page: https://i2rt.com/products/yam-manipulator
- I2RT Robotics (manufacturer): https://i2rt.com/
- Public URDF (canonical, upstream): https://github.com/i2rt-robotics/i2rt/blob/main/robot_models/yam/yam.urdf — the fidex MJCF derives from this.

---

## 2. Sim scripts (verified working)

All under `raiden_fork/third_party/exo_redo/Demos/`:

| Script | What it does | CLI |
|---|---|---|
| `sim_yam.py` | Single-arm interactive MuJoCo viewer. Canonical baseline. | `python3 Demos/sim_yam.py` |
| `sim_yam_bimanual.py` | Bimanual interactive viewer. At runtime, generates a wrapper XML that includes one copy of `yam.xml`'s defaults + assets and two copies of the body/equality/actuator subtrees (arm-2's prefixed `arm2_` to avoid name collisions). Arms placed at `(0, ∓margin/2, 0)` — along world Y, perpendicular to the YAM forward axis. | `python3 Demos/sim_yam_bimanual.py [--margin 0.7]` |
| `render_bimanual_views.py` | Headless three-viewpoint renderer (front-perspective / near-top-down / low-angle close-up). Used to sanity-check the layout when no display is available. | `python3 Demos/render_bimanual_views.py` |
| `exo_img_bimanual_yam.py` | Image-input detect + render demo: reads a frame, detects both arms' fiducial boards, and composites both arm renders onto the input frame. Good sanity check for the calibration pipeline before running it live. | `python3 Demos/exo_img_bimanual_yam.py --img path/to/frame.png` |

### `build_bimanual_xml(margin=..., include_background=...)`

Programmatic composer that lives inside `sim_yam_bimanual.py`. Composes:
1. Two arm copies from `robot_models/i2rt_yam_fidex/yam.xml` (arm 1 at `y = -margin/2`, arm 2 at `y = +margin/2`, arm-2 body names prefixed with `arm2_`).
2. Both arms' exo meshes as **mocap geoms** (so we can move them at will without breaking the kinematic chain).
3. Both ArUco boards' textured planes as mocap geoms attached to the exo mounts.

This is the reference example of "compose two arms + both boards + a background into one XML" — copy the pattern for any new bimanual scene work.

### Reference renders (URLs may rot if `unit_tests/output/` is pruned)

- Bimanual 3-view: https://omidlab.net/browse/para/robot/yam/unit_tests/output/bimanual_demo/sim_render.png
- Single-arm 3-view (canonical compare-against): https://omidlab.net/browse/para/robot/yam/unit_tests/output/bimanual_demo/single_arm_3view.png
- Arm-1 print PNG: https://omidlab.net/browse/para/robot/yam/raiden_fork/calibration_assets/aruco_board.png
- Arm-2 print PNG: https://omidlab.net/browse/para/robot/yam/raiden_fork/calibration_assets/aruco_board_arm2.png

---

## 3. Fiducial exoskeleton in sim

**Scope reminder:** the fiducial-exoskeleton *calibration solver* is yams-owned. Sim's part is (a) drawing where the exo lands under FK and (b) providing the sim-side ground truth for validating that yams's calibration matches sim geometry.

### Config module map

| Purpose | Path |
|---|---|
| Dictionary + IDs + board_length | `third_party/exo_redo/ExoConfigs/panda_exo.py` |
| YAM link → aruco offset + STL path | `third_party/exo_redo/ExoConfigs/yam_exo.py` — `YAM_BASE_ONLY_CONFIG` (arm 1), `YAM_BASE_ONLY_CONFIG_ARM2` (arm 2) |
| `LinkConfig`, `link_to_aruco_transform` | `third_party/exo_redo/ExoConfigs/exoskeleton.py` |
| Runtime helpers | `third_party/exo_redo/exo_utils.py` — `do_est_aruco_pose`, `position_exoskeleton_meshes`, `get_link_poses_from_robot` |
| STL mounts | `third_party/exo_redo/so100_blender_testings/yam_base_board_v2*.stl` |
| Board PNGs | `third_party/exo_redo/aruco_images/*.png` |

### Marker IDs + board layout

- `larger_coarse_board` — arm 1, IDs **217–225**, 3×3 grid (`ExoConfigs/panda_exo.py`)
- `larger_coarse_board_arm2` — arm 2, IDs **226–234**, 3×3 grid, same physical 93.5 mm edge so the same `yam_base_board_v2.stl` mount fits both arms
- Dictionary: `cv2.aruco.DICT_6X6_250`
- Physical marker length: `BOARD_LENGTH_COARSE` in `panda_exo.py` (check the exact value before assuming — measure with calipers when a fresh board is printed)

### STL mount variants

| File | Which arm | Notes |
|---|---|---|
| `yam_base_board.stl` | v1 (deprecated) | Original single-arm mount |
| `yam_base_board_v2.stl` | Both arms, standard rig | Default (`YAM_EXO_MESH` in `yam_exo.py`) |
| `yam_base_board_v2_reverse_clearance_fixed.stl` | **arm 2 only** on russet bimanual rig | Mirror-flipped along X — see §7 patch below |

### Runtime rendering pattern

```python
import mujoco, copy
from Demos.sim_yam_bimanual import build_bimanual_xml
from ExoConfigs.yam_exo import YAM_BASE_ONLY_CONFIG, YAM_BASE_ONLY_CONFIG_ARM2
from exo_utils import position_exoskeleton_meshes, get_link_poses_from_robot

xml = build_bimanual_xml(margin=0.591, include_background=False)   # margin ≈ physical rig spacing
model = mujoco.MjModel.from_xml_string(xml)
data  = mujoco.MjData(model)
mujoco.mj_forward(model, data)

# Rebind arm-2 link cfg to its "arm2_" mujoco body names before positioning meshes.
exo_arm2 = copy.deepcopy(YAM_BASE_ONLY_CONFIG_ARM2)
for lc in exo_arm2.links.values():
    lc.pybullet_name = "arm2_arm"

position_exoskeleton_meshes(YAM_BASE_ONLY_CONFIG, model, data,
                            get_link_poses_from_robot(YAM_BASE_ONLY_CONFIG, model, data))
position_exoskeleton_meshes(exo_arm2, model, data,
                            get_link_poses_from_robot(exo_arm2, model, data))
```

To render from an arbitrary calibrated camera's viewpoint: `_render_with_intrinsics(model, data, T_world_in_cam, K, W, H)` in `raiden.calibration.exo_calibrate_fidex` returns `(rgb, seg_mask)`. Alpha-composite via `_compose_overlay`.

---

## 4. Recurring GOTCHAs (do not repeat these mistakes)

### 4a. Mocap-geom local pos/quat (VERY recurring)

When programmatically generating exo + ArUco mocap bodies for the YAM **outside** of `ExoskeletonConfig._generate_xml` (i.e., when emitting a custom wrapper XML like `build_bimanual_xml`), the `<geom>` *inside* each mocap `<body>` MUST carry local `pos=` and `quat=` attributes derived from `LinkConfig.robot_mesh_pos` / `robot_mesh_quat`. For the exo-mesh geom, the offset goes through `ExoConfigs.exoskeleton._child_pose_in_link_frame` (which handles the `offsets_in_robot_mesh_frame=True` case YAM uses).

**Why:** `exo_utils.position_exoskeleton_meshes` sets the mocap **body** pose (`mocap_pos`/`mocap_quat`) every frame from the link's FK pose. The local-frame transform of the geom **inside** the body is *baked at compile time* and never updates. For YAM the link config carries `robot_mesh_quat = [0.707, 0, 0, 0.707]` (90° rotation that lays the board flat on the base plate). Without that baked-in geom-local rotation, the green exo mesh + ArUco board render upright/perpendicular to the base instead of lying flat.

**How to apply:** whenever modifying or adding code that emits YAM mocap-mesh XML, compare the resulting render against `single_arm_3view.png` (canonical single-arm reference). The exo + board should lie flat on top of the base. If they're sticking up, you missed the `link_pos_attr`/`link_quat_attr`/`exo_pos_attr`/`exo_quat_attr` on the geom. See `_exo_worldbody_block` in `sim_yam_bimanual.py` for the fixed implementation that mirrors `ExoskeletonConfig._generate_xml`.

### 4b. EGL multi-thread race

Two capture threads' MuJoCo renderers colliding on `eglMakeCurrent` produce `EGL_BAD_ACCESS`. Fix: refactor to single-thread, iterating over cameras in one context. See `_capture_loop_bimanual_multicam` in `exo_calibrate_bimanual_fidex.py` for the pattern that stopped the crashes. Rule of thumb: **one MuJoCo renderer per process**, iterated over N cameras, never fanned out across threads.

For headless rendering set `MUJOCO_GL=egl PYOPENGL_PLATFORM=egl` in the environment; NVIDIA needs both.

### 4c. `os.environ.get(key, default)` on FUSE mounts

An unrelated but painful gotcha we hit on the real side that WILL bite sim if you run headless from a fuse-mounted volume: `dict.get(key, default)` evaluates the default eagerly, so a line like
```python
DA3_REPO_DIR = os.environ.get("DA3_REPO_DIR",
    os.path.expanduser("~/lab/da3_repo/src")
    if os.path.exists(os.path.expanduser("~/lab/da3_repo/src"))
    else "/data/cameron/da3_repo/src")
```
will `stat("~/lab/...")` even if the env var is set. On a wedged sshfs mount that stat call goes D-state and CANNOT be killed by `SIGKILL` alone — need `fusermount -uz` then abort via `/sys/fs/fuse/connections/*/abort`. Bake fallback paths as **literals** at module top rather than expanding user paths inline.

### 4d. russet-only v2-reverse arm-2 patch

The russet bimanual station physically mirrors the arm-2 base board (mounting clearance). Two edits are needed **before** either MuJoCo XML build **or** `link_to_aruco_transform` is called:

```python
_ARM2_V2_STL = str(_EXO_DIR / "so100_blender_testings" /
                   "yam_base_board_v2_reverse_clearance_fixed.stl")
for _link in YAM_BASE_ONLY_CONFIG_ARM2.links.values():
    _link.exo_mesh_path = _ARM2_V2_STL
    _old = np.asarray(_link.aruco_offset_pos, dtype=np.float64).copy()
    _link.aruco_offset_pos = np.array([-_old[0], _old[1], _old[2]])   # flip x sign
```

This is embedded in every `_fidex.py` sibling on russet. **Don't apply the patch unless you're targeting russet's specific bimanual hardware** — the upstream (non-russet) rig uses `yam_base_board_v2.stl` on both arms.

For sim (i.e., us): only apply this patch when generating sim renders that need to reproduce russet's physical asymmetry.

---

## 5. Frame conventions cheat-sheet

- `T_a_in_b` = pose of frame `a` expressed in frame `b`; maps `p_a → p_b = T_a_in_b @ p_a`. **All identifiers in the codebase follow this.**
- `link_to_aruco_transform(link_cfg)` returns `T_aruco_in_link` (despite the "to" naming — historical). Its inverse is `T_link_in_aruco`.
- PnP returns `T_aruco_in_cam` in OpenCV convention (`+X` right, `+Y` down, `+Z` forward from camera).
- Saved per camera: `T_cam_in_arm_base`. Bimanual saves under `__arm1` / `__arm2` suffixed keys plus a top-level `bimanual_transform.right_base_to_left_base`.
- Visualizer "world" frame = `left_arm_base`.

---

## 6. Additional sim assets we've built (recent, task-tagged)

### Finray gripper (tasks #211–215, all completed)

- Inspected finray STL geometry (bbox, origin)
- Built `sim_assets/gripper/finray/finray_toyota.xml` — parallel-jaw with finray fingers
- Built combined arm + finray XML: `yam_finray.xml`
- Rendered onto an episode start frame to verify
- Provided a MuJoCo viewer entry point for finray YAM

Location: `/data/cameron/para/robot/yam/sim_assets/gripper/finray/` (created in this branch of work).

### Pending

- **#216 — Import the Anzu banana asset into a MuJoCo scene.** Path in TRI internal repo (needs SSO): `anzu/models/fruits/bananas/` — contains `fake_organic_banana.sdf`, `fake_organic_banana.gltf`, `.bin` (bazel-external-data), `_color.png`. Repo URL: https://github.shared-services.aws.tri.global/robotics/anzu . Fetch pattern needs the TRI API key from https://anzu-resources-apikey.awsinternal.tri.global/ and a bazel run of `//tools/external_data:download`. Convert SDF → MuJoCo XML via the same `obj2mjcf` pattern used for finray, or (heavier lift) switch that scene to Drake.
- **#217 — Build sim-teleop recording script `record_sim.py`.** Analogue of the real-side `record.py`. Should reuse whatever we can from that pipeline while replacing hardware I/O with MuJoCo step calls + fake proprio.
- **Pink Yeti tumbler** (Cameron flagged 2026-07-02): TRI anzu path `lbm_eval/models/tableware/tumblers/`. Same clone+fetch pattern as the banana. Needs SSO'd machine (not this agent-fleet box).

---

## 7. Reference doc + memory pointers

- **The definitive reference** for the fiducial-board sim rendering + pose recovery is at `/data/cameron/para/robot/yam/docs/fiducial_exoskeleton_board.md` (served: https://omidlab.net/browse/para/robot/yam/docs/fiducial_exoskeleton_board.md?raw). If any of this sim.md diverges from that doc, trust the doc; update this file.
- Older bimanual sim memory (still valid): `../fleet/agents/yam_sim/memory.md` (this agent's slice) — cross-reference before adding duplicates.
- yams's calibration + record notes live at `../fleet/agents/yams/…` — **do not edit those**; refer via cross-ping only.

---

## 7a. Sim pick-and-place — WORKING (2026-07-02)

**Location:** `/data/cameron/para/robot/yam/sim_pickplace/`

Full pipeline delivering procedural pick-and-place on randomized banana + saucer scenes with the YAM in MuJoCo. Verified end-to-end.

**Files:**
- `scene/pickplace.xml` — composed scene: fidex YAM + YCB banana + YCB plate + table + overhead camera + inactive banana_grip weld
- `scene/yam_nokey.xml` — local copy of upstream yam.xml with the arm-only keyframe stripped (so we can supply a full nq=22 keyframe covering banana + plate freejoints)
- `procedural.py` — env loader, Mink IK, waypoint interpolation, magnet weld helpers, `run_rollout()`
- `batch.py` — randomized pose sampler + batch runner + stats
- `rollouts/` — per-trial videos + `batch_seedN.json` stats
- `logs/` — one-off debug renders

**Assets in use:**
- Banana: `/data/cameron/para/robot/yam/sim_assets/objects/ycb/011_banana/google_16k/` — visual = `textured.obj` (real photogrammetry), collision = **two capsules** (see gotcha below)
- Plate: `/data/cameron/para/robot/yam/sim_assets/objects/ycb/029_plate/google_16k/` — mesh visual + mesh collision (plate is near-flat so mesh collision works)

**Grasping strategy:** magnet weld (`mjEQ_WELD` between banana body and link_6). At the grasp waypoint, the runtime captures the current link_6-in-banana relative pose, writes it to `model.eq_data[eq, 3:6]` (position) and `[6:10]` (quat wxyz), and toggles `data.eq_active[eq] = 1`. Release = `data.eq_active[eq] = 0`. Orientation of the gripper at contact is irrelevant — the banana snaps to whatever relative pose exists at attach.

**IK:** Mink, `mink.FrameTask(frame_name="grasp_site", frame_type="site")`. Position-only cost weighting (`pos_cost=8.0`, `ori_cost=1e-3`) so the arm can reach randomized banana XY without fighting orientation constraints. Converges in 1–5 iterations per waypoint interpolation step.

**Trajectory:** 6-waypoint plan — pre-grasp / grasp / lift / arc-top / pre-place / place / retract. Linear interp in SE(3) between waypoints, IK per interpolated pose. `procedural.procedural_waypoints()` for the plan.

**Randomization workspace:** banana XY in `[0.30–0.55] × [-0.18–0.18]` m, yaw ∈ `[-π/2, π/2]`; plate XY same box with ≥15 cm separation from banana, yaw ∈ `[-π, π]`.

**Success metric:** banana XY within 5 cm of plate XY AND banana Z within `[plate_z, plate_z+10 cm]` at end of trajectory + 60-step settle.

**Success rates (2026-07-02, seed sweeps):**
- 10 trials seed=42, threshold=5cm, release_z=5cm: **80.0%** (median dxy success = 4.0 cm)
- 100 trials seed=0, threshold=5cm, release_z=5cm: **84.0%** (all fails 5.0–9.2 cm — just off plate)
- 100 trials seed=1, threshold=5cm, release_z=2.5cm: **86.0%** (marginal improvement)
- **200 trials seed=2, threshold=8cm, release_z=2.5cm: 98.5% (197/200)** ← operational baseline
- Runtime: ~2.2 s per trial (single arm on puget, no GPU needed for the sim itself)

**Dataset generation (`sim_record.py`):** Produces the same on-disk schema as real `record.py` — `<ep_dir>/rgb/scene_camera/NNNNNN.jpg` (640×480 default JPEG q85; can go 960×540 q90) + `<ep_dir>/lowdim/NNNNNN.pkl` (joints, timestamp, intrinsics, extrinsics `T_cam_in_rbase`, plus sim-extras: banana pose, plate pose, EE pose, ctrl vector). 57 frames per episode by default (n_frames_per_wp=12, frame_stride=2). Runtime ~24 s per episode at 640×480 with rendering.

**Batch dataset generator (`gen_dataset.py`):** produces N successful episodes, deletes failures, writes a `manifest.json` at the dataset root. Parallelizable across worker processes with `python gen_dataset.py <N> <seed> <ep_prefix>` (used with 4-way parallelism for ~4× throughput — bottleneck is EGL/GPU serialization on shared context). **CRITICAL: write to puget-LOCAL storage** (e.g. `~/yam_para/sim_datasets/`), NOT `~/lab/...` — sshfs write throughput cuts ep gen speed from ~13 s/ep to ~4 min/ep.

**Sample artifact:** `/data/cameron/para/robot/yam/sim_pickplace/artifacts/sample_pickplace_episode.mp4` (57 frames, 15 fps, ~4 s) — one full pick-and-place rollout from the 500-ep dataset.

**Dataset (final, complete as of 2026-07-02 11:00 UTC):**
- **500 successful episodes** at `~/yam_para/sim_datasets/pickplace_sim/` on puget (`/data/cameron/yukon_remote/...` inaccessible from other machines; use rsync from puget when needed elsewhere)
- 57 frames per episode (n_frames_per_wp=12 × 7 wps + 30 settle, stride=2) — every episode identical length
- **Total 1.22 GB** (scene RGB JPEGs q85 640×480 + lowdim pickles)
- Banana workspace utilized: x ∈ [0.30, 0.41] m, y ∈ [-0.13, 0.13] m
- Plate workspace utilized: x ∈ [0.40, 0.55] m, y ∈ [-0.18, 0.18] m
- dxy at end (banana center vs plate center): median 5.6 cm, max 8.0 cm (threshold)
- dz at end (banana above plate): median 5.4 cm, min 3.1 cm, max 9.9 cm
- 4-worker parallel generation completed
- Summary tool: `python summary_report.py <dataset_root>` → JSON stats

**Sim pipeline: read `/data/cameron/para/robot/yam/sim_pickplace/README.md`** for the how-to (quick recipes, extension patterns, known limitations, related docs).

**Domain randomization (`domain_random.py`):** `DomainRandomizer(seed)` class. Per-episode: headlight + worldbody light intensity/position, banana/plate mass, camera fovy+pos. Per-frame RGB augmentation: brightness/contrast, Gaussian noise σ=4, salt-pepper p=0.003. Plumbed into `record_episode(..., domain_randomizer=dr)`. ~15% runtime overhead when enabled.

**Wrist camera:** `right_wrist_camera` in link_6 body at pos `(0, -0.10, 0.06)` with xyaxes `"1 0 0 0 0.60 -0.80"` fovy 70°. Renders fingertips + banana clearly at grasp pose; sees arm base + table during transit (varied views throughout trajectory, good for training). Plumbed into `sim_record.record_episode(render_wrist=True)` → `rgb/right_wrist_camera/*.jpg` + `K_wrist` intrinsics in lowdim.

**Policy eval harness (`eval_policy.py`):** `evaluate_policy(policy_fn, n_episodes, seed) → {stats, trials}`. Policy interface = `callable(obs) → {"ctrl": (7,), "magnet": bool, "done": bool}` where `obs` provides `scene_rgb`, `joints`, `K_scene`, `T_cam_in_world`, `ee_pose_world`, `gripper`. Included `PrivilegedProceduralPolicy` for sanity — verifies harness matches batch.py (5/5 success). Any future trained policy just needs to conform to the obs/ctrl interface. Runs sim at 30 Hz policy rate by default (physics steps at 500 Hz, one obs per 16 sim steps).

**Recurring gotchas encountered (fold into §4 of this doc):**

- **G1. Mesh collision on non-convex scans is unreliable.** YCB banana `nontextured.stl` is a photogrammetry scan; MuJoCo's default convex-hull collision let the banana FALL THROUGH the table. Fix: replace collision geom with 2 capsules aligned to banana long axis (see `pickplace.xml`). Visual mesh still uses the textured OBJ for realism. Rule of thumb: use analytic primitives for collision, meshes for visual — always.

- **G2. Weld `eq_data` layout is (anchor_pos, body2_pos_in_body1, body2_quat_in_body1)** — 3+3+4 = 10 floats, plus torquescale at [10]. **NOT** (pos, quat) at [0:3, 3:7] as one might guess. Runtime discovery via inspecting a compile-time-baked eq_data. Compute `T_link6_in_banana = inv(T_banana) @ T_link6` and write to `model.eq_data[eq_id, 3:6]` + `[6:10]`.

- **G3. Weld default active=true.** MuJoCo activates weld at compile time from the XML-initial relative pose. Set `active="false"` on the `<weld>` element. Then runtime enables it after capturing the CURRENT relpose.

- **G4. Table geom that overlaps arm base wrecks physics.** With `<geom size="0.75 0.75 0.01" pos="0.4 0 -0.011">` the table crossed the arm base at world origin, and the arm's collision boxes pushed the table down through settle → banana fell into a floor-less zone. Fix: shrink table to `size="0.30 0.35 0.01" pos="0.45 0 -0.011"` so it clears the arm base. Optional safety net: separate `<geom type="plane">` at z=-0.5 catches anything that falls through.

- **G5. Home keyframe with folded arm at load-time slaps the banana out of the workspace.** Reset to arm-straight-up (`qpos="0 0 0 0 0 0 0 0 ..."`) so the gripper starts high and the banana settles undisturbed.

- **G6. `<include>` resolves relative mesh paths against the INCLUDING file's dir**, not the included file. Either set `<compiler meshdir="...">` before the include, or use absolute paths. We use both — meshdir points at the fidex assets, my custom banana/plate meshes use absolute paths.

- **G7. Keyframe names must be unique.** Overriding an included XML's keyframe requires renaming yours. We strip the keyframe from the included `yam.xml` (via `yam_nokey.xml`) and declare our own `home_ext` in the scene.

- **G8. Mink's `Configuration.q` has no setter.** Use `config.update(q)` to seed, not `config.q = q`. Matches `raiden/lib/ik.py` pattern.

- **G9. On EGL cleanup path Python prints `EGLError` traceback**. Harmless — render already saved by that point. Ignore or catch.

## 8. What is NOT yet in sim, but likely coming

- **Sim-teleop / sim-recording pipeline** (task #217 above) — no `record_sim.py` yet
- **Anzu asset library integration** — no working converter from SDF/glTF → our MJCF flavor yet; finray was hand-authored
- **Deformables / cloth** for towel-fold — `anzu/models/deformables/assets/` exists on TRI side; not pulled here
- **Sim2real gap logs** — no structured comparison of what sim predicts vs. real trajectories yet; open opportunity
- **Sim-derived training datasets** — the sim can already generate ground-truth `T_link_in_world` at any camera; nobody has piped that into the DINO-volume training loop yet
- **Auto-CI for sim renders** — the golden `single_arm_3view.png` is our only pixel-diff sanity check; no CI yet

---

## 9. Housekeeping

If `unit_tests/output/bimanual_demo/*.png` gets pruned (someone runs a cleanup), regenerate from `Demos/render_bimanual_views.py` — no live signal depends on them being cached; they exist purely for eyeballing.

If yams updates any file in `raiden_fork/third_party/exo_redo/` that alters geometry (mesh path, offset, board length, marker IDs), yams should ping this file. Reverse holds for any sim-side XML template changes: sim pings yams if the geometry visible to calibration changes.
