fix(scenic): SAT-correct AABB clearance for fixture pairs — closes c.1 - #16
Conversation
… pairs
Replaces the radial-diagonal `distance > _footprint_clearance_xy` form used
for object↔fixture and distractor↔fixture clearance with the separating-axis
theorem's exact OR-form (per-axis half-width-sum), mirroring the form
already used for object↔object pairs in the same renderer.
Root cause (campaign caveat c.1):
The diagonal form requires (hypot(wa,la) + hypot(wb,lb)) / 2 of
centre-to-centre separation. For symmetric AABBs this is ~√2× more
conservative than necessary; for elongated fixtures (flat_stove
0.36×0.20, desk_caddy 0.14×0.42) it is worse. Combined with multiple
sampled distractors `in SAFE_REGION` (0.70×0.50 m), the conjunction
becomes infeasible within 25k rejection-sampler iterations on the 10
`libero_goal/*` drawer-family tasks — the cardinality-monotone
signature documented in worst_50_rca.md (0.064% → 0.769% G3 fail
rate from cardinality 2 → 8) and rca/stage4_c1_addendum_10_task_footprint.md.
Why this is principled, not a band-aid:
By the separating-axis theorem, two axis-aligned bounding boxes are
non-overlapping iff their projections are disjoint on x OR y. The
OR form is therefore the *exact* AABB non-overlap condition — strictly
tighter than the radial form for axis-aligned geometry. The original
comment justifying the radial form ("OR form permits diagonal corner
penetration") was mathematically incorrect: disjoint x-projections imply
disjoint AABBs regardless of y, and vice versa. This change does not
loosen any *physically-required* clearance — it removes a spurious √2×
slack that the constraint never needed in the first place.
Smoke evidence (scripts/smoke_c1_drawer_family.py):
10 c.1 tasks × 5 axis subsets × 5 seeds = 250 conditions
Result: 250/250 PASS at maxIterations=25000
Max iterations observed: 2553 (≈10% of cap → ample headroom)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
|
CI audit (#17) — RCA for the red Tier-1 unit job here The two failing tests in
This PR's whole point is to swap the (incorrect, circular) Action required on this branch: update those two assertions (and any sibling assertions in assert "abs(" in code and ".position.x -" in code and ".position.y -" in codeor more strictly: assert any(
f"abs({obj}.position.x - {fix}.position.x) >" in code
for fix in fixtures
)Once the tests are updated to assert the new formulation, this PR's Tier-1 will clear. CI infrastructure is fine — fix belongs here, not in the CI branch.
|
The PR's source change replaced radial `distance from X to Y > c` clearance constraints with the SAT/box form (per-axis half-width-sum OR'd over x/y) for object↔fixture and distractor↔fixture pairs, but the two affected unit tests still asserted the old `distance from` substring. Update assertions to match the new emission so the tests exercise the actual contract the PR introduces. No source-side change. Fixes: - tests/test_scenic.py::TestScenicGenerator ::test_position_mode_adds_fixed_fixture_clearance_constraints - tests/test_scenic.py::TestLiberoCorpusAudit::test_distractor_mode_compiles Local: 67/67 tests in test_scenic.py pass. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
… distractor↔object AABB, per-(variant,surface) z (#24) * fix(scenic): close placement-clearance gaps — robot in require graph, distractor↔object AABB, per-(variant,surface) z Closes three placement-clearance defects that drove the dominant pose_tolerance failures in validation run2 (RCA Findings A and B): /home/batman/.omar/ea/4/validation_run2/rca/stage1_g5_pose_tolerance_object_axis_and_settle_drift.md Fix 1 — robot init pose in the require graph (Finding B): The perturbed robot-axis init pose was never represented in the Scenic require graph, so samples that placed an object inside a perturbed arm link's swept volume were accepted; MuJoCo settle then resolved the penetration by shoving the object 40–260 mm in xy. Each link's perturbed world position is now a linear (Jacobian) function of the sampled joint deltas, and a SAT-correct 3-D AABB require clause keeps every placed object / distractor / fixture out of each link's measured world AABB. Footprints measured by scripts/measure_robot_link_footprints.py into data/robot_link_footprints.json. New module robot_metadata.py. Fix 2 — distractor↔object clearance (radial→SAT AABB): The distractor↔object constraint used a hardcoded radial point distance (`distance from d to obj > 0.13`) that both over-constrains on-axis and under-constrains on the diagonal, and ignored each object's measured footprint. Replaced with the SAT per-axis OR-form (`abs(d.x - obj.x) > dx or abs(d.y - obj.y) > dy`) using measured object dims — the same fix PR #16 applied to fixture pairs. Distractor↔distractor pairs keep a diagonal-radius point clearance (correct for equal extents). Fix 3 — per-(variant, surface) spawn z (Finding A): Settled clearance is NOT class-invariant: an object-axis OOD variant seats at a different height than its canonical class, and the same class settles differently on different supports (stove vs cabinet top). surface_spawn_z now resolves per-(variant, surface) → per-class → median-derived fallback. The renderer emits the object-axis chooser as a Uniform over (asset_class, resolved_spawn_z) PAIRS and reads `_chosen_X[1]` for the spawn z, so the chosen variant carries ITS seating height (Scenic forbids branching on a random value; correlated tuples are the idiomatic substitute). The optional data/spawn_clearances_variants.json table is absent until generated; surface_spawn_z falls back to the canonical class table — an expected, documented absence, not a swallowed error. Tests: 271 passed (tests/test_scenic.py + invariant/placement/clearance/ scenic/spawn selection), 0 failures. Prior agent implemented these changes but died before committing; this commit verifies the diff is coherent and ships it. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(validation/G4): populate spawn_clearances_variants.json (FV SMT D2) FV SMT D2 / MC Prop 2: VARIANT_CLEARANCES was empty so surface_spawn_z ignored its surface argument. Generate the per-(variant,surface) clearance table from real MuJoCo measurement (measure_variants; 14 keys). The surface dimension is now exercised: akita_black_bowl seats 31mm apart on table vs kitchen_table. Also warn at import when the table is empty so the inert state is visible. NOTE: flat_stove/wooden_cabinet keys are absent because no init scene pre-places a movable on a fixture (see .omar RCA fixA_surface_dimension_inert.md). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scenic): emit distractor world settle z in robot clearance (FV SMT G) FV SMT Finding G: the robot<->distractor z-term used distractor_i.position.z (Scenic SAFE_REGION TABLE_Z ~0.82) while MuJoCo instantiates the distractor at its world settle z (~0.92), a phantom ~100mm guard. Emit the canonical-distractor world settle z (surface_spawn_z) as a constant so the constraint variable and the static z-prune band share the simulator's world frame. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scenic): propagate variant xy footprint via max-over-pool (FV MC #6) FV MC Property 6 (CRITICAL): _render_constraints and _render_robot_clearance computed clearance half-extents (thx,thy[,thz]) from the canonical class only, so a wider OOD object-axis variant could overlap a neighbour / fixture / robot link at the perturbed init pose (the simulator then shoves it). Compute the half-extents as the max over {canonical} U substitution-pool, mirroring the max-over-pool the z-prune already did. obj_info now carries the pool-max dims, covering pairwise, fixture, and distractor-object clauses. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(scenic): per-instance surface and chooser keying (FV MC #3) FV MC Property 3: _render_objects keyed variant choosers and surface z by object CLASS (seen_classes + asset_var_map[obj_class]), so two same-class objects on different supports shared the first instance's surface z and variant identity. Key per node.instance_name instead: each instance resolves its own surface_class and gets an independent _chosen_<instance> chooser (independent draws per instance for diversity; comment in code). Adds tests covering Fix B/C/D. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style(tests): format test_pr24_clearance_fixes.py (ruff isort + black) The prior FV-followup PR added tests/test_pr24_clearance_fixes.py with an unsorted import block and unformatted layout, failing the CI Lint & Format job (ruff I001 + ruff format + black --check on src/ tests/). Apply ruff --fix import sorting and black formatting at source. No logic change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(spawn_z): expect per-(variant,surface) bowl z on the workspace table test_renderer_emits_concrete_resolved_z_for_table_objects asserted the legacy class-only spawn z (surface_class=None -> 0.9209). After the PR #24 per-(variant,surface) clearance work (FV Finding A), main_table is a WorkspaceNode whose object class is "table", so the renderer resolves the bowl's support-surface class to "table" and emits the measured (akita_black_bowl|table) settled z (0.9520) via the SAME surface_spawn_z call the simulator uses (lockstep). Update the expectation to resolve the spawn z through surface_class="table", matching the renderer exactly. This asserts the new invariant rather than the superseded class-only value. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(scenic): raise combined-mode maxIterations for tightened require graph test_combined_mode_compiles hit RejectionException at maxIterations=10000. The PR #24 clearance fixes (FV MC #6 max-over-pool variant footprints, Fix 1 robot-link AABB clauses, distractor<->object/fixture clearances) are each CORRECT and must not be loosened -- doing so re-opens the FV MC #6 CRITICAL (the simulator would shove an overlapping wider variant). The cost is a tighter but still feasible region for the fully-perturbed combined scene, so the rejection sampler needs a larger iteration budget. Per the RCA (combined_mode_rejection_feasibility.md Option B) and FV MC Property 5, raise the budget rather than weaken any require clause. No correctness constraint or tolerance is changed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(scenic): match per-instance variant chooser name (FV MC #3) test_variant_pool_emits_per_variant_distinct_z searched for the legacy per-class chooser variable "_chosen_wine_bottle". After the PR #24 per-instance keying (FV MC #3, commit f0bcc6a), the chooser is keyed by object instance, so the emitted variable is "_chosen_wine_bottle_1". Match the per-instance name. The per-variant z values it then checks remain distinct (wine_bottle/ketchup/milk seat at different heights), so the distinct-z invariant is unchanged; only the chooser variable name is updated. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(measure): contact-filter table-resting samples so (*|table) clearance excludes fixture-perched objects A movable sampled at an (x,y) over an elevated fixture (wine_rack, stove, cabinet) settles ON the fixture, ~tens of mm above the table, yet was bucketed under its <class>|table key and counted as a table-resting clearance. This inflated the per-(variant,surface) median (akita_black_bowl|table 0.100 -> 0.132). Add _settled_on_table_surface(): step the live MuJoCo sim and inspect data.contact to require an actual table-geom contact before a sample is bucketed under a workspace-table surface (snapshot/restore qpos/qvel so the check is side-effect-free). Applied in both measure() and measure_variants(). Add _assert_table_rows_match_canonical() to fail loudly before writing if any canonical-class <class>|table row drifts > 3cm from its canonical per-class z. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data: regenerate spawn_clearances{,_variants}.json with contact-filtered table measurement Re-measured against the contact-filtered script. The variants file is the key change: akita_black_bowl|table 0.13203 -> 0.10094 (now matches its canonical per-class z), and every canonical-class <class>|table row now equals its canonical clearance within tolerance. OOD object-axis variants (macaroni_and_cheese, bbq_sauce, orange_juice, ...) seat at their own measured heights, guarded by the per-sample contact check rather than pinned to the canonical median. Canonical file re-measured with the same filter (values stable; akita_black_bowl 0.10094). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(renderer,measure): fixture-aware distractor spawn-z + goal-feas require (generator) Generator-side of the two consolidated distractor fixes (validator-side G4 invariant lands in the next commit). Closes the 0/41 distractor pose_tolerance residual (RCA stage1_g5_residual_distractor_pool_spawn_z) and prevents goal impossibilization (validation plan §1 G4 dual). Fix 2 — fixture-aware distractor spawn-z (option i). Distractors were placed `in SAFE_REGION` (which pins Scenic z to the bare TABLE_Z) and settled wherever their sampled (x,y) landed — often on a fixture top ~130 mm up — so their injected z never matched their settled z. Now the renderer deterministically assigns each distractor slot to a support (the table OR a specific non-goal fixture, round-robin), constrains its (x,y) to the central support footprint, and emits the resolved per-(class,surface) spawn z. The class and its seating height are drawn together as a correlated Uniform of (class, z) pairs; the class string is still exposed as a param for the BDDL patch. The simulator resolves the same z via the same surface_spawn_z (lockstep) → injected z == settled z. - asset_metadata: surface_spawn_z computes on-fixture z analytically (fixture_top_z_above_table + table-resting body-origin offset) for an unmeasured (class|fixture) pair — no hardcoded fixture heights, no chicken-and-egg with the measured table; measured per-pair rows supersede it. New measured fixture-geometry accessors replace the hand-coded _FIXTURE_DIMS under-estimates (the cause of distractors slipping onto fixtures). - measure_spawn_clearances: measure_distractor_fixtures() settles distractors on each fixture, confirms contact with the assigned fixture, records the settled clearance, and loudly asserts the settled bottom face sits on the contacted fixture-geom top — the frame-correct restatement of "fixture_top + half_height" with the half measured at rest (no body-origin-is-centre assumption, guarding the 40/43 frame-confusion class). Writes fixture_geometry.json + (class|fixture) rows. (Data regenerated in a follow-up commit.) - renderer: distractors never assigned to the GOAL fixture; measured fixture footprints used for distractor↔fixture clearance; a fixture-assigned distractor declares the fixture as support_parent_name so the settled-position validator permits the intentional resting contact. Fix 1 (generator half) — goal-feasibility require. ir/goal_regions.py resolves the goal-relevant regions (goal_target edges → target support patch inflated by the goal object's footprint, world frame, incl. table goal regions that fixture clearance does not cover). The renderer emits a SAT-correct AABB require keeping every distractor's footprint outside each goal region, so the goal object placed at the region centre always has a clear spot. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(validation): G4 goal-feasibility invariant — distractor must not block goal (Fix 1) Validator-side of Fix 1 (the generator goal-feasibility require landed in the previous commit). Adds the G4 semantic assertion that NO active distractor occupies a goal-relevant region — the dual of "no accidental trivialization": goal impossibilization (validation plan §1 G4). A distractor placed where the goal object must end up (e.g. on the stove burner for On(bowl, flat_stove_1_cook_region), or in a table goal region such as stove_front_region) makes the task physically unsolvable. - domain.assert_goal_region_admits_object: re-derives the goal regions via the SAME resolver the renderer uses (ir.goal_regions), then asserts every active distractor's footprint stays outside each region inflated by the goal object's footprint, so the goal object placed at the region centre always fits. Added to DOMAIN_ASSERTIONS / assert_domain. - only ACTIVE distractors (index < n_distractors) are scored: inactive slots exist in the Scenic scene with unconstrained positions but are never injected into MuJoCo, so scoring them would raise spurious goal-block failures. - tests: goal-feasibility across generated stove scenes; assert_domain now runs all seven family-B invariants. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data: distractor-on-fixture spawn-z (+ measurement convergence fix) Regenerate spawn_clearances_variants.json with 20 measured (distractor_class|fixture_class) on-fixture clearance rows across 3 fixture classes (flat_stove, microwave, wooden_cabinet), and add the measured data/fixture_geometry.json (footprint/height/rest-top_z per fixture). Measurement convergence fix (validation_run2 RCA): the per-sample frame/stability cross-check in measure_distractor_fixtures is now NON-FATAL — an unstable settle (e.g. an irregular-footprint bowl_drainer/desk_caddy whose AABB extends below its contact feet) is logged loudly and excluded from that pair's median instead of raising AssertionError and aborting the whole run. The precise injected==settled guarantee remains independently enforced by the v4 smoke's 5 mm pose_tolerance. 3 samples excluded this run; all 20 matrix pairs still produced measured rows. Table/object variant rows untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: ruff-format + black on scenic_renderer.py & test_scenic.py Pure formatting (line-wrap/blank-line normalization, no logic change) to green the Lint & Format CI job, which was failing on pre-existing violations from the Fix-1/Fix-2 commits. ruff check, ruff format --check, and black --check all pass on src/ tests/. Goal-feasibility require clause semantically unchanged. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data: distractor table-clearance rows + white_cabinet fixture (v5) Closes the residual distractor pose_tolerance gap (PR #24). An empirical MuJoCo probe showed distractors load as a SINGLE canonical-class asset (not OOD variants), so fixture-assigned distractors already settle within 0.2-2.5mm. The real z-error was TABLE-assigned distractors of distractor-only pool classes (desk_caddy, bowl_drainer, cookies, popcorn, alphabet_soup, macaroni_and_cheese) — never task objects, so never measured — falling back to the DEFAULT_CLEARANCE prior (0.10), catastrophically wrong for tall/irregular ones (desk_caddy injected 0.10 vs settled 0.27). Measurement (scripts/measure_spawn_clearances.py): - measure_distractor_fixtures now ALSO captures clean TABLE-distractor clearances (via the distractor placement path — the only path that covers distractor-only classes), guarded by the physical table-contact check so a distractor that perched on a fixture is excluded, not mis-bucketed. - _merge_distractor_table_rows adds ONLY classes missing from the canonical table; never overwrites a validated task-object measurement. - Added KITCHEN_SCENE5_put_the_black_bowl_on_the_plate to MEASURE_TASKS so white_cabinet appears as a NON-goal fixture (it was only ever the goal fixture, hence excluded from distractor assignment -> no rows). Data regenerated: - spawn_clearances.json: +6 distractor-only table rows (DEFAULT median 0.100->0.101, in-bounds). - spawn_clearances_variants.json: 21 class|fixture rows incl. white_cabinet. - fixture_geometry.json: 4 fixtures (white_cabinet added: footprint/top_z). wine_rack: documented fallback. Its top is a narrow bottle cradle; the central placement half-extent for an 0.08 m distractor is NEGATIVE, so it is correctly filtered from distractor support assignment in every scene and never seats a distractor. Left on the analytic on-fixture z rather than forcing it. No renderer change: the table-distractor slot already resolves surface_spawn_z(class, None) -> SPAWN_CLEARANCES, picking up new rows. No tolerance widening, no hardcoded z, no try/except masking, Fix-1 goal feasibility require untouched. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * rca(finding-b): robot-shove refuted — diagnostics prove arm ≥100mm clear Direct MuJoCo measurement (pre- and post-settle) shows the perturbed arm is ≥100mm clear of every failing distractor; robot-axis distractor xy displacement is median 0.0mm / p90 0.63mm (lower than non-robot). The 2 'xy-dominated' fails are a desk_caddy|wooden_cabinet spawn-z penetration (35mm low → ejected up+side), not a shove; the other 6 robot-subset fails are pure-z and reproduce identically without the robot axis. No robot-clearance change warranted. _diag_robot_shove.py — per-fail pre/post-settle link-AABB gap reproducer _diag_distractor2_z.py — distractor_2 class/fixture + spawn-z vs settle Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data: distractor z-convergence — desk_caddy/bowl_drainer cabinet rows (v6) Corrects the two irregular-distractor on-cabinet clearance rows the Finding-B RCA handed off (validation_run2 rca/distractor_z_convergence.md): desk_caddy|wooden_cabinet 0.37098 -> 0.41148 (+40.5 mm) bowl_drainer|white_cabinet 0.39012 -> 0.41234 (+22.2 mm) Root cause: the per-sample stability gate `abs(AABB_bottom - contact_top) <= 50mm` mis-rejected irregular OPEN-BOTTOM distractors — a desk_caddy's open multi-compartment AABB hangs ~56 mm below its actual contact feet, so the gate EXCLUDED its stable settle and the sparse surviving (atypical) sample under-stated the resting clearance by ~40 mm. The renderer then injected the distractor that far too low -> it penetrated the cabinet top and was ejected up+sideways (the "xy shove" Finding-B refuted). This is NOT orientation multi-modality: a gate-free audit (94 samples) shows every distractor tips a CONSISTENT ~90deg and only the two irregular cabinet rows diverge > pose_tolerance from stored. Fix (scripts/measure_spawn_clearances.py), no hardcoded z / no tolerance widening / no masking: * Retire the AABB-bottom stability gate (a live-stepping quiescence replacement SEGFAULTS — irregular distractor<->cabinet contacts overflow MuJoCo's contact arena, ncon=5000). Per-sample admission keeps the step-free filters that don't assume a footprint: fixture-contact-exists + physical clearance band. * Aggregate each (class|fixture) by the dominant settle MODE (largest cluster median); reduces to the median for the tight unimodal box distributions. * Merge a measured row only when it diverges from the stored row by > 5mm pose_tolerance; within-tolerance rows stay byte-identical. The 19 box rows, 6 table rows, white_cabinet, wine_rack fallback, and fixture_geometry.json are preserved by construction. Fix-1 goal-feasibility require and G4 assert_goal_region_admits_object are untouched. Invariant: injected z (1.23148) == settled z (~1.231) within < 1 mm; verified by the smoke's 5 mm pose_tolerance over the merged data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * wip(distractor-proxy-footprint): per-class yaw-robust footprint threading + pool-fit rejection (RECOVERY CHECKPOINT — footprint data not yet measured; measurement died on MuJoCo contact-arena overflow) Code-complete B/C/F threading (renderer + asset_metadata + measure script + tests), feasibility 40/40 verified by the agent before it crashed. Footprint data file NOT yet produced (the --distractor-footprints-only run hit 'Too many contacts' arena overflow). Committed by EA as a durable recovery checkpoint; finisher to produce data + validate. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(distractor-footprint): isolated static-AABB measurement + per-class threading data Finishes the proxy-footprint workstream (recovery from 27bedf6 WIP). The crashed agent's footprint measurement generated full multi-distractor scenes and settled irregular distractors onto fixtures, overflowing MuJoCo's ncon contact arena ("Too many contacts") — the data file was never produced (RCA ~/.omar/ea/4/validation_run2/rca/proxy_footprint_measure.md). Root cause: a distractor footprint is STATIC asset geometry (the geom-AABB extent of the loaded mesh) — it needs no scene generation or settling. Rewrote measure_distractor_footprints() to load each pool class IN ISOLATION (EmptyArena + single object, mj_forward, read geom world-AABB), which is deterministic, runs in seconds, and structurally cannot overflow the contact arena (no contacts are ever computed). Produces data/distractor_geometry.json for all 10 pool classes. Feasibility fix (renderer): the checkpoint inset the table placement Range by the worst-case pool fit-half, so one oversized class (desk_caddy, 0.46 m) shrank the table to ~0.158x0.058 m for EVERY sample, starving the common small distractors (scenario generation became infeasible — 2 tier-1 tests failed once real data was present). Now inset the static Range by the SMALLEST pool fit-half (the union of all classes' feasible regions) and enforce per-class on-table containment with a per-sample require using the sampled _distractor_i_r. Small distractors place freely; oversized classes are confined/rejected by their real footprint. Confirmed structural win: desk_caddy and bowl_drainer fit NO fixture top and fall back to the table. Tier-1: 78/78 pass. Also fixed 4 pre-existing E501 lines from the checkpoint. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data(distractor-table-z): re-measure butter + cream_cheese TABLE spawn-clearances (v8) The proxy-footprint fix (0660e57) shifted which small box distractors land on the plain table, surfacing a pre-existing TABLE spawn-z inaccuracy: distractor @5mm pose_tolerance dropped 76.6 (v6) -> 62.3 (v7). Audit (scripts/_diag_distractor_table_z.py) maps 13/20 z-dominated distractor fails to exactly two under-measured canonical table rows: butter 0.08738 -> 0.09861 (+11.2mm, n=33) cream_cheese 0.08672 -> 0.10031 (+13.6mm, n=10) Both are also task objects, so their canonical rows were seeded from the object-axis measure() in their NATURAL pose; but a distractor is injected at IDENTITY orientation (preserve_default_z=False), where both rest ~11-14mm higher. The renderer therefore injected them too low on the table -> settle drift > 5mm. (Table-level TASK objects keep LIBERO's default z and never read this table, so TASK is unaffected.) All other table rows are accurate (<2mm) and stay byte-identical. Root of the staleness: _merge_distractor_table_rows was add-missing-only, so it never re-measured a class already present. New surgical path: * measure_distractor_table() / --distractor-table-only: generates the same distractor scenes, collects ONLY table-resting samples (no on-fixture contact/AABB machinery -> cannot perturb validated class|fixture rows and cannot settle an irregular distractor onto an undersized fixture = no contact-arena overflow). Gate-free admission (physical band + real table contact) + dominant settle MODE aggregation, matching the z-convergence work. * _merge_distractor_table_corrective(): rewrites a row only on >5mm divergence; adds missing classes; leaves within-tol rows byte-identical; records provenance (distractor_table_corrections: old->new, n). Touches ONLY data/spawn_clearances.json (distractor_geometry.json, fixture geometry, on-fixture variant rows, renderer footprint threading all untouched). No hardcoded z, no tolerance widening, no masking; Fix-1 require + G4 untouched. RCA: ~/.omar/ea/4/validation_run2/rca/distractor_table_z_recover.md Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * data(distractor-fixture-z): measure box×flat_stove rows; crash-safe measurement (v9 Residual A) The analytic on-fixture fallback (fixture_top_z_above_table + on_table) over- estimated chocolate_pudding/cream_cheese/popcorn on flat_stove by ~77-80mm: flat_stove AABB top_z=0.135 is the highest geom, but a box rests on the burner grate ~0.055 above the table. Injecting a box 65-80mm too high over a 0.30x0.19m stove makes it free-fall and tumble off the edge (the v8 285-299mm "slides"). Measured directly in-scene via scripts/measure_box_fixture_safe.py, which runs each (task,seed) scene in an ISOLATED subprocess so the t=0 contact-arena overflow (ncon=5000) that segfaults the in-process path is contained — the child dies alone, the parent records the rest. Gate-free admission + dominant settle MODE, corrective merge (>5mm only); all 13 pre-existing box×fixture rows re-confirmed within <2mm (byte-identical). Three flat_stove rows added: chocolate_pudding|flat_stove 0.15795 (n=12, direct) popcorn|flat_stove 0.15462 (n=4, direct) cream_cheese|flat_stove 0.15474 (derived: rest_surface + table offset; 0 direct RNG samples — the mis-z'd class is rejected by settle-validation, broken via a bootstrap seed) No hardcoded z (measured), no tolerance widening, no masking. Fix-1 goal require + G4 validator untouched. Tier-1 78/78. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * diag(distractor-robot-residual): RCA scripts for the v8/v9 285-339mm gross fails Reproducibility diagnostics establishing that the "cabinet-top slide" gross fails are a pre-existing ROBOT-PERTURBATION table-distractor settle instability (NOT goal-surface, NOT edge-margin, NOT the Residual-A z gap): _diag_robot_overlap.py — distractor↔robot-geom distance at reset _diag_arm_sag.py — arm transient settle-sag (37-47mm) _diag_t0_contacts.py — t=0 contacts: table-only at inject, airborne after settle Finding: with robot axis OFF max distractor err is 8.1mm (0 gross); with robot ON it reaches 339mm. See ~/.omar/.../rca/distractor_cabinet_goal_residual.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * feat(distractor-clearance): offset-aware distractor<->fixture clearance (validated) The distractor<->fixture clearance was offset-blind (guarded the fixture BODY ORIGIN with a symmetric footprint), but flat_stove's collision geom is offset ~95mm in x, so the +x third of the real stove was unguarded -> a table distractor sampled there was injected penetrating the stove and ejected as a gross 'slide' (85-339mm). Adds a measured per-fixture offset (fixture_geometry.json offset:[dx,dy], default [0,0] for centered fixtures) and guards the offset geom-AABB center at all three clearance sites (distractor/object/robot <->fixture). Emission is byte-identical for centered fixtures (|off|<1e-6 passthrough) -> zero blast radius for them. Validated (offset-fix-validate): - Deterministic repro: pre-fix seed0 distractor_0=alphabet_soup is sampled overlapping flat_stove (68.8mm penetration; the exact RCA bad point) and settles 86.4mm (gross launch); post-fix it is no longer sampled overlapping (0/60 vs 5/60 seeds) and settles 0.0mm. Penetration launch eliminated. - v10 smoke (8 seeds): distractor pose_tolerance 93.24% @5mm, max distractor err 43mm (a minor settle, no stove penetration). Remaining >40mm fails are PRE-EXISTING task-object robot-shove (out of this fix's scope). - Broad scenic sweep G3 at the real 5000-iter runtime budget: 98.49% post vs 98.53% pre (delta = 1 tight seed) >> 97.7 gate. The 2000-iter sweep's 97.66% was a sampler-budget artifact for 2 crowded flat_stove scenes. - Fix-1 goal-feasibility require + G4 untouched; no tolerance/clearance widening. Includes scripts/measure_fixture_offsets.py: static, crash-safe per-fixture geom-AABB offset measurement tool that generated the offset data. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * test(scenic): offset-aware distractor<->fixture clearance assertion test_distractor_mode_compiles pinned the OLD offset-blind clearance string (abs(distractor_0.position.x - flat_stove_1.position.x)). The offset fix (7d266b4) correctly guards the measured geom-AABB center, so flat_stove now emits abs(distractor_0.position.x - (flat_stove_1.position.x + 0.09471)). Update the assertion to verify the cardinality-gated distractor_0<->fixture clearance require exists per fixture on both axes, tolerant of an optional measured offset term (centered fixtures unchanged). Intent preserved; brittle exact-string removed. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Root cause (campaign caveat c.1, drawer-family G3 residual)
The
requireclauses for object↔fixture and distractor↔fixture clearance insrc/libero_infinity/renderer/scenic_renderer.pyused a radial-diagonal form:By the separating-axis theorem, two axis-aligned AABBs (which is what every LIBERO fixture / object is) are non-overlapping iff:
The radial form is ~√2× more conservative than necessary in the symmetric case and worse for elongated fixtures (
flat_stove0.36×0.20,desk_caddy0.14×0.42,wooden_cabinetwith door 0.30×0.30). Forwooden_cabinetvs a 0.16×0.16 bowl the radial threshold is 0.325 m vs the AABB-correct 0.23 m on either axis — a sizeable spurious shrinkage of feasible region.Combined with
SAFE_REGION(0.70 × 0.50 m) and 4 sampled distractors, plus the existing distractor-pair and distractor-object clearance conjunctions, the result is the exact cardinality-monotone rejection signature documented inrca/stage4_c1_addendum_10_task_footprint.md:(All 50/50 worst-conditions in
reports/worst_50_rca.mdare c.1 RejectionException atscenarios.py:437.)The original comment in the code justifying the radial form ("OR form permits diagonal corner penetration") is mathematically incorrect: disjoint x-projections imply disjoint AABBs regardless of y, and vice versa — that's the entire content of the separating-axis theorem.
Fix
distance > _footprint_clearance_xy(...)with the SAT-correct per-axis OR-form for object↔fixture and distractor↔fixture clearance, exactly matching the form already used for object↔object pairs at line 866-867._footprint_clearance_aabb(dims_a, dims_b) -> (dx_min, dy_min)helper._footprint_clearance_xyfor non-renderer callers that need a single scalar clearance (e.g. settle-drift checks); update its docstring to clarify that the renderer uses the SAT-correct form.Why this is principled, not a band-aid
min_clearancelowered, nomaxIterationsbumped, no task-specific bypass added).Smoke evidence (
scripts/smoke_c1_drawer_family.py)10 c.1 tasks × 5 axis-subsets (cardinality 2 → 7) × 5 seeds = 250 conditions.
≈10% of the 25k budget is the worst observed; previously these tasks were budget-exhausting at 25k. The full output is reproducible via:
Test plan
pytest tests/test_renderer.py tests/test_perturbation_audit.py tests/test_settle_correctness.py→ 58 passedpython scripts/smoke_c1_drawer_family.py→ 250/250 pass at 25k iter capReferences
~/.omar/ea/4/validation_run/reports/worst_50_rca.md~/.omar/ea/4/validation_run/rca/stage4_c1_addendum_10_task_footprint.md~/.omar/ea/4/validation_run/rca/stage3_open_drawer_persistent_25k.md🤖 Generated with Claude Code