Skip to content

fix(scenic): SAT-correct AABB clearance for fixture pairs — closes c.1 - #16

Merged
KE7 merged 2 commits into
mainfrom
fix/scenic-require-drawer-family
May 21, 2026
Merged

fix(scenic): SAT-correct AABB clearance for fixture pairs — closes c.1#16
KE7 merged 2 commits into
mainfrom
fix/scenic-require-drawer-family

Conversation

@KE7

@KE7 KE7 commented May 19, 2026

Copy link
Copy Markdown
Owner

Root cause (campaign caveat c.1, drawer-family G3 residual)

The require clauses for object↔fixture and distractor↔fixture clearance in src/libero_infinity/renderer/scenic_renderer.py used a radial-diagonal form:

require (distance from obj to fixture) > (hypot(w_f, l_f) + hypot(w_o, l_o)) / 2

By the separating-axis theorem, two axis-aligned AABBs (which is what every LIBERO fixture / object is) are non-overlapping iff:

|dx| > (w_a + w_b)/2  OR  |dy| > (l_a + l_b)/2

The radial form is ~√2× more conservative than necessary in the symmetric case and worse for elongated fixtures (flat_stove 0.36×0.20, desk_caddy 0.14×0.42, wooden_cabinet with door 0.30×0.30). For wooden_cabinet vs 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 in rca/stage4_c1_addendum_10_task_footprint.md:

cardinality G3 fail rate
2 0.064%
5 0.242%
8 0.769%

(All 50/50 worst-conditions in reports/worst_50_rca.md are c.1 RejectionException at scenarios.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

  • Replace 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.
  • Add _footprint_clearance_aabb(dims_a, dims_b) -> (dx_min, dy_min) helper.
  • Retain _footprint_clearance_xy for 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

  • This is not loosening a physically-required clearance — the radial form was over-conservative for AABB geometry. The OR form is the tightest possible non-overlap condition. No real overlap becomes admissible.
  • No magic numbers were tuned (no min_clearance lowered, no maxIterations bumped, no task-specific bypass added).
  • The fix is uniform: same form everywhere in the renderer (object↔object already used it).

Smoke evidence (scripts/smoke_c1_drawer_family.py)

10 c.1 tasks × 5 axis-subsets (cardinality 2 → 7) × 5 seeds = 250 conditions.

==============================================================================
Total conditions:   250
Passed:             250  (100.000%)
Failed:             0
Max iters observed: 2553 (cap=25000)
==============================================================================

≈10% of the 25k budget is the worst observed; previously these tasks were budget-exhausting at 25k. The full output is reproducible via:

.venv/bin/python scripts/smoke_c1_drawer_family.py

Test plan

  • pytest tests/test_renderer.py tests/test_perturbation_audit.py tests/test_settle_correctness.py → 58 passed
  • python scripts/smoke_c1_drawer_family.py → 250/250 pass at 25k iter cap

References

  • ~/.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

… 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>
@KE7

KE7 commented May 19, 2026

Copy link
Copy Markdown
Owner Author

CI audit (#17) — RCA for the red Tier-1 unit job here

The two failing tests in tests/test_scenic.py:

  • test_position_mode_adds_fixed_fixture_clearance_constraints asserts "require (distance from " in the emitted code.
  • test_distractor_mode_compiles asserts "(_n_distractors <= 0) or ((distance from distractor_0 to wooden_cabinet_1)" in the emitted code.

This PR's whole point is to swap the (incorrect, circular) distance from X to Y > r formulation for the SAT-correct AABB clearance abs(X.x - Y.x) > rx or abs(X.y - Y.y) > ry. The generator is now emitting exactly that — you can see it in the run-log diff. The tests, however, still match the old circle form.

Action required on this branch: update those two assertions (and any sibling assertions in test_scenic.py) to match the new AABB form, e.g.

assert "abs(" in code and ".position.x -" in code and ".position.y -" in code

or 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.

ci/audit-and-stabilize (PR #17) does not attempt to patch around this — per the no-band-aid rule.

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>
@KE7
KE7 merged commit 656c7aa into main May 21, 2026
2 checks passed
@KE7
KE7 deleted the fix/scenic-require-drawer-family branch May 21, 2026 02:10
KE7 added a commit that referenced this pull request Jun 3, 2026
… 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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant