Skip to content

Commit ed7620b

Browse files
authored
Domino system-ID tasks: reach-limited min-block (friction) + heavy-block obstacle (mass) (#44)
* Add EnvironmentTask.reward_fn: binary task reward generalizing atom-set goals When set, the callable is the environment-side success criterion and BaseEnv.goal_reached evaluates it in place of the plain goal-atom check (which remains the None default). Propagated through the PyBullet task conversion and alt-goal replacement. Deliberately not copied into the agent-facing Task, so approaches cannot peek at the ground-truth criterion. First user: the domino min-block reward (topple AND at most K* movable blues used). * Min-block sysID experiment config: friction mismatch, single-flag ablation arms Settings: domino min-block task mode (reach-limited start/target spans, per-task K* budget via MinBlockReward), domino_true_friction vs domino_planning_friction (planning base sims run at the miscalibrated value), task cache dir under saved_datasets/, and agent_sim_learn_oracle_sim_params extended to also grant planning base sims the TRUE physical params (absorbs the former agent_planner_oracle_base_sim_params; the agent_ prefix keeps it outside the task-cache key on purpose). agents.yaml: baseline (agent_base_sim_no_learning) and oracle (agent_oracle_hybrid_sim) both run agent_sim_learning and differ in exactly that one flag, so the arms are a clean one-bit ablation of base-sim calibration. envs/all.yaml: min-block domino config (true 0.1 vs planning 0.5, span band, 4 staged blues); old chain-task config kept commented for easy revert. * Domino min-block task generation: simulated K*, agent-buildable corner search, per-leg differentiation The min-block pipeline lives in task_generators/min_block_generation.py (quota loop, disk cache keyed by config+seed+source digest) with the layout/probe machinery in task_generators/min_block_utils.py; the env keeps the physics and reward semantics (MinBlockReward, per-instance set_domino_physical_params via changeDynamics, planning-friction wiring for skip_process_dynamics envs). K* is always a simulated minimum with the real Push at the agent-typical params [0.04, 0.05] (a stronger probe push previously masked the miscalibration the tasks exist to expose). Straight tasks: evenly-spaced chains, kept only when the planning-friction recount is strictly cheaper. Turn tasks: K* searches only agent-buildable layouts -- straight-line probes plus ONE natural-yaw corner blue with sim-calibrated configs (derived from the oracle run's own -36deg corner solution); the generator's mirrored 45deg pair is deliberately excluded since no planner would propose it. Because natural corners barely propagate at the planning friction, turn differentiation uses a relaxed per-LEG certificate: straight-chain probes of each leg at both frictions, kept only when the wrong model under-counts the legs (the corner cost cancels). Empirical notes: corner viability at the two frictions is anti-correlated across every domain lever (mass, restitution, spinning/rolling friction, shape), so the knife-edge corner search is structural, not a tuning miss. * Docs: min-block task-generation slide deck and figure scripts Self-contained reveal.js deck (motivation -> pipeline overview -> reward, anatomy, K* search, corner-layout search, turn-physics A/B, differentiation filter, per-task examples). Figures are generated by checked-in scripts that drive the real task-gen code: the corner-candidate grid, the turn yaw A/B (scoped: natural alignment fails only at min-block gaps; the legacy generator's natural corners work at tighter gaps), and the five live test tasks with calibrated vs miscalibrated solutions, every panel sim-verified. * Refine domino task configuration: adjust friction parameters and remove obsolete comments for clarity * Min-block generation hot-spot fixes: memoized leg probes, certificate-first turn filtering, partial-cache flagging Generation took ~20 min per arm (vs ~2 min for the old straight-only generator): 29 of 33 failed turn attempts paid for the full corner layout search (dozens of robot Push rollouts each) before rejection, identical leg spans were re-probed on every attempt, and both arms regenerated in parallel after a cache-key miss. - Memoize straight_span_k_star per (1cm span bucket, budget, physics override), cleared once per generation run. - _make_turn_task: run the cheap memoized leg probes BEFORE the corner layout search — a feasibility bound (legs' straight-chain minima already exceed the staged budget) and the per-leg differentiation certificate now reject most attempts for free. - Cache format now records num_requested; a partial set (quota loop hit its attempt cap) warns loudly on every reload instead of silently shrinking the eval. - Exclude render-only flags (camera width/height, draw_debug) from the cache key so a resolution change no longer orphans a generation cache. * Heavy-block (immovable obstacle) domino task type behind domino_heavy_block_tasks Each task is a dogleg: a gray domino-shaped block with true mass 1000 kg (DominoComponent.heavy_block_true_mass) stands on the start's fall line, yawed ~30 degrees off it, posing as a ready-made bend link toward the target. Planning sims believe it has normal domino mass (new heavy_block_mass physical-param override, set at env init for skip-dynamics envs), so an uncalibrated planner runs its chain into the gray block and dies there; a mass-calibrated planner bends early with its own corner blue and cuts across. Generation (reusing the min-block quota loop / cache / MinBlockReward): - heavy_dogleg_k_star probe (all entry/exit splits, swept exit gaps, memoized at the canonical anchor); - certificate: believed dogleg exists, believed detour is no cheaper (both at planning physics), true dogleg dead, true detour K* within the staged blues (K* may equal them: differentiation is by topple failure, not the over-build cap); budget = detour K*; - shape (legs/bend from probe-sweep bands, lattice-snapped for memo reuse) certified once, placement retried across start poses; - compute_turn_k_star gains an extra-scene arg with spawn-overlap pruning so detour probes include the gray obstacle. Support changes: reset_state moves absent dominoes out of view by identity (heavy tasks use the last slot, breaking the old prefix assumption), gray blocks get true mass at every reset with the generic mass override shielded, staging exempts gray blocks, place_domino grows is_heavy_block. * Heavy-block tasks: natural-alignment variants (straight swerve + gray-at-corner turn) Replaces the dogleg geometry with two variants that keep natural alignments, mixed per domino_min_block_turn_ratio: - straight: start -> gray -> target collinear and co-facing; the believed (normal-gray-mass) plan chains straight through the gray as a free link; the true solution is a half-circle swerve around it (new swerve layout family in min_block_utils: heading profile phi*sin(2*pi*t), aligned at both ends, knocks within the measured ~33-degree propagation tolerance at phi 25-40 degrees); - turn: find the believed-cheapest L corner layout at the family minimum (all candidates of the cheapest toppling k are scanned, not just the first), and stand the gray exactly at that natural corner pose. The lure (gray as free corner) is structurally one blue cheaper than every own-corner alternative; the true solution skips around via the existing corner search with the gray as obstacle. Both certify per task: believed lure exists, lure dead at the true physics, true swerve/detour K* within the staged blues; budget = K*. Lure probes and corner blueprints memoize at the canonical anchor; placement retries reuse the certificate. The turn-variant lure needs believed friction == true friction (corners never propagate at 0.5), so the domino_heavy launch config drops domino_planning_friction: this env isolates the MASS dimension. Also: launch config block (envs/all.yaml domino_heavy + agents.yaml override; min-block block kept with SKIP), heavy task-examples slide + figure script in the deck, slide text updated. * Min-block turn yield: raise attempt cap 3x, lattice-snap turn legs The 2026-07-03 oracle run shipped a 4/5 PARTIAL set: turn attempts survive ~1-in-30 (the per-leg differentiation certificate's dead band dominates — 23 of 33 drops), and the old 12x+20 cap gave only 80 attempts. Rejected attempts are nearly free since the probe memo, so the cap is now 36x+40. Turn legs also snap to the 1 cm probe-memo lattice: repeats are guaranteed memo hits (no bucket-edge double probes) and the drop log becomes a readable coverage map over the finite (entry, exit) cells for any future band retuning. Validated by regenerating the seed-0 friction sets with the exact launch flags: 5/5 test tasks (2 turns K*=3, 3 straights K*=1, 37 turn drops absorbed) in ~10 min; train 1/1; straight-path smoke unchanged. * Optimize heavy-block generation ~5x Per-attempt cost was dominated by three hot spots, all fixed: - placement loops now stage FIRST (pure geometry) and only pay the sim re-verification (real-pose swerve / detour search) on a staged pose — staging failures on the single-row grid were re-running full searches per pose try; - the true-dead certificate scans only the believed cost k_bel (new only_k arg on heavy_dogleg_k_star): a block-minimizing planner only builds believed-cheapest layouts, so other counts cannot leak; - the believed corner-blueprint sweep is capped at the chord's straight-chain minimum + 2 (memoized probe): shapes where no corner propagates used to sweep every candidate at every k (~minutes) before concluding None. Blueprints are also computed for one side and mirrored for the other (physics is mirror-symmetric; the gray-substituted lure rollout re-verifies the mirrored geometry). Smoke: straight task kept in 8s (was 60s), turn attempts ~10-13s flat (was up to 60s+ with multi-minute blueprint misses), full single-flag generation 41s (was 227s); scene/physics/cache assertions unchanged. * Fix probe nondeterminism: pin arm to initial joints before every probe push The same layout alternated topple/no-topple on repeated probe rollouts: _set_state reaches the robot pose by IK FROM THE CURRENT arm configuration, so consecutive probes inherited ~1e-3 rad wrist differences from wherever the previous rollout parked the arm — enough to flip knife-edge (corner/swerve) layouts. Symptoms: the heavy turn pipeline contradicted itself (blueprints blessed by a lucky flicker, then the physically identical gray-substituted lure failing 71% of re-rolls), and figure scripts reported certified solutions as 'not reproducible'. _layout_topples and _chain_topples now set the arm to its exact initial joints after state assembly. Probes are fully deterministic and match episode-start conditions (episodes also begin from the initial joints). Verified: 6x identical repeats now agree, and motion-planning ON vs OFF give identical outcomes once pinned — the flag never changed push physics; the drift did. * Heavy-block reward budget = staged blues; K* certifies solvability only Diagnosis from the regenerated figure: with deterministic probes, one task's recorded detour K*=3 re-derived at k=4 in a fresh process (and another's K*=4 at k=2) — knife-edge corner layouts are sensitive to the simulator's contact-solver history even with pinned arm joints, so an exact-K* budget recorded under generation's sim history can be unreachable under execution's. Heavy tasks differentiate on topple-vs-not (the believing planner dies at the gray regardless of budget), so MinBlockReward's max_blocks is now the STAGED blue count: borderline-K* flips can no longer make a task unsolvable-within-budget, for the agent or for figure re-derivation. The swerve/detour searches remain as solvability certificates (a within-staged-blues solution exists). Docs, figure labels (budget=N), and settings comment updated. * Heavy-block certificates: require 2 independent solution layouts Task 1 of the seed-0 set kept failing figure re-derivation even with the staged-blues budget: its detour family had exactly ONE toppling layout out of 83 candidates, and knife-edge single layouts flip with the simulator's contact-solver history — a task whose only solution toppled once during generation can be unsolvable under a fresh sim. compute_turn_k_star and swerve_k_star gain a min_hits arg (scan continues until that many distinct layouts have toppled across k <= budget); both heavy makers demand min_hits=2 at the real-pose check, so shipped tasks always have at least two independent solutions and survive history variance. * Fix cross-history nondeterminism: zero domino velocities on reset The reset path repositions bodies via resetBasePositionAndOrientation, which does NOT clear velocities — a domino that was mid-fall when the previous rollout ended carried its momentum into the next 'static' scene. This made probe outcomes depend on what the simulator ran beforehand: the same layout toppled or died based on sim history, which broke figure re-derivation of certified solutions and would equally have broken execution-time reproducibility of generation certificates. (deterministicOverlappingPairs was tested and is NOT the cause.) DominoComponent.reset_state now zeroes every domino body's linear and angular velocity. Scramble test: a marginal corner layout re-probed after batches of unrelated rollouts now agrees 4/4 (was flipping). * Heavy turn tasks: verify the lure at the real pose; figures derive in a fresh sim The turn lure (gray-substituted corner layout) was certified only at the canonical anchor and transformed to the task's real pose — but pose transfer is only approximately physics-preserving, and a shipped task's real-pose lure turned out never to topple believedly (0 of 9 corner-matching layouts): the baseline would not even be lured. The placement loop now re-verifies the transformed lure at the real pose (believed-topple + true-dead, 2 rollouts, after free staging) and retries the pose on failure. The examples figure also re-creates the simulator between generation and panel derivation: a certified detour that re-derives cleanly in a fresh process (2 hits at k=2) failed inside the long-lived generation process — residual sim context still shifts knife-edge outcomes, and a fresh simulator is also the faithful (execution-like) context. * Heavy turn placement tries 12 -> 24; figure role colors by equality The real-pose lure re-verification lowered turn placement yield (a certified shape now also needs a pose where the transferred lure physically works) and the seed-0 set came out 4/5. Failed poses are cheap (~2 rollouts), so the pose-retry cap doubles. Figure: object roles are matched by equality instead of identity — the env is re-created between loading and panel derivation, so family-derived layouts are keyed by the fresh env's objects (equal by name to the cached tasks'). * Docs: heavy-block task-examples figure (complete 5-task set) + deck rebuild All 15 panels re-derive and sim-verify in a fresh simulator: two turn tasks (gray at the natural corner; skip-around detours topple, believed gray-corner plans die at the gray) and three straights (half-circle swerves topple, believed through-gray chains die). Deck rebuilt with the heavy-block intro and examples slides. * Launch configs: park the domino_heavy env (commented out) Both the envs/all.yaml block and the agents.yaml excluded_predicates override are commented out together (a FLAGS-only override without its base env breaks the launcher). The min-block domino block stays present with SKIP: True. Uncomment the domino_heavy pair to launch the heavy-block arms; the task cache in saved_datasets is prewarmed at the current code digest. * CI checks: repo-canonical formatting, docs excluded from mypy/pylint, lint fixes - run_autoformat.sh (docformatter reflows) applied to the fig scripts and domino task generators; verified stable under the CI-pinned tool versions (yapf 0.32.0, isort 5.10.1, docformatter 1.4). - docs/ added to the mypy exclude and pylint ignore-paths: it holds one-off slide/figure-generation scripts, not library code (both configs already excluded asset directories). - Lint fixes in the domino code: unused imports, demo-block variable shadowing in env.py's __main__, an over-long docstring line, and the real-pose lure probe now binds its layout via functools.partial (cell-var-from-loop + typing clean). Verified locally: mypy . (native and --platform linux) clean over 610 files; pylint 10.00/10 on every changed file; autoformat idempotent; 55/56 branch-relevant unit tests pass — the one failure (test_push_second_switch_boil_position_mode) fails identically on master locally (known macOS-vs-Linux switch-push divergence; passes on CI Linux).
1 parent 0d22d98 commit ed7620b

27 files changed

Lines changed: 4549 additions & 95 deletions

.predicators_pylintrc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ extension-pkg-whitelist=numpy,pybullet,torch,tensorflow,pyrealsense2
1010
ignore=CVS
1111

1212
# Add paths to the blacklist.
13-
ignore-paths=predicators/envs/assets,predicators/third_party,venv
13+
ignore-paths=predicators/envs/assets,predicators/third_party,venv,docs
1414

1515
# Add files or directories matching the regex patterns to the blacklist. The
1616
# regex matches against base names, not paths.

docs/envs/assets/domino_min_block/build_deck.py

Lines changed: 518 additions & 0 deletions
Large diffs are not rendered by default.
189 KB
Loading
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""Schematic: min-block task anatomy — init, calibrated plan, miscalibrated
2+
plan. Pure drawing (numbers match the measured reaches)."""
3+
import matplotlib
4+
5+
matplotlib.use("Agg")
6+
from pathlib import Path
7+
8+
import matplotlib.pyplot as plt
9+
import numpy as np
10+
from matplotlib.patches import FancyArrowPatch, Rectangle
11+
12+
W, D = 0.07, 0.015 # domino width (perp to chain) x depth (along chain)
13+
SPAN = 0.26
14+
TRUE_REACH, BELIEVED_REACH = 0.11, 0.16
15+
16+
17+
def block(ax, x, y, color, label=None):
18+
ax.add_patch(
19+
Rectangle((x - D / 2, y - W / 2), D, W, facecolor=color,
20+
edgecolor="k", lw=1.2, zorder=3))
21+
if label:
22+
ax.text(x, y - W / 2 - 0.03, label, ha="center", va="top", fontsize=9)
23+
24+
25+
def gap_arrow(ax, x0, x1, y, txt, color):
26+
ax.add_patch(
27+
FancyArrowPatch((x0 + D / 2, y), (x1 - D / 2, y), arrowstyle="<->",
28+
color=color, mutation_scale=10, lw=1.4, zorder=2))
29+
ax.text((x0 + x1) / 2, y + 0.045, txt, ha="center", fontsize=9,
30+
color=color)
31+
32+
33+
fig, axes = plt.subplots(1, 3, figsize=(13, 3.2))
34+
35+
# ── Panel 1: the task ────────────────────────────────────────────
36+
ax = axes[0]
37+
block(ax, 0.0, 0.0, "#7fc97f", "start\n(push me)")
38+
block(ax, SPAN, 0.0, "#c599c5", "target\n(topple me)")
39+
gap_arrow(ax, 0.0, SPAN, 0.0, f"span {SPAN} m", "k")
40+
for i, bx in enumerate((0.04, 0.10, 0.16, 0.22)):
41+
block(ax, bx, -0.16, "#7fb2d9")
42+
ax.text(0.13, -0.235, "4 staged blues (place as few as possible)",
43+
ha="center", fontsize=9, color="#33658a")
44+
ax.set_title("Task: topple target with ≤ K* blues", fontsize=11)
45+
46+
# ── Panel 2: calibrated plan (true reach 0.11 → K*=2) ────────────
47+
ax = axes[1]
48+
block(ax, 0.0, 0.0, "#7fc97f")
49+
g = SPAN / 3
50+
for i in (1, 2):
51+
block(ax, i * g, 0.0, "#7fb2d9")
52+
block(ax, SPAN, 0.0, "#c599c5")
53+
gap_arrow(ax, 0.0, g, 0.0, f"{g:.3f} ≤ 0.11 ✓", "#1a7a1a")
54+
ax.text(SPAN / 2, -0.19, "true reach 0.11 → K* = 2 blues\ntopples ✓ uses 2 ≤ K* ✓ REWARD",
55+
ha="center", fontsize=10, color="#1a7a1a")
56+
ax.set_title("Calibrated model (friction 0.1)", fontsize=11)
57+
58+
# ── Panel 3: miscalibrated plan (believed reach 0.16 → 1 blue) ───
59+
ax = axes[2]
60+
block(ax, 0.0, 0.0, "#7fc97f")
61+
block(ax, SPAN / 2, 0.0, "#7fb2d9")
62+
block(ax, SPAN, 0.0, "#c599c5")
63+
gap_arrow(ax, 0.0, SPAN / 2, 0.0, f"{SPAN/2:.2f} ≤ 0.16 ?", "#b3541e")
64+
ax.text(SPAN * 0.75, 0.09, "✗ chain dies\n(0.13 > 0.11 real reach)",
65+
ha="center", fontsize=10, color="#a01515")
66+
ax.text(SPAN / 2, -0.19,
67+
"believed reach 0.16 → plans 1 blue\nvalidates in ITS sim, fails in real",
68+
ha="center", fontsize=10, color="#a01515")
69+
ax.set_title("Miscalibrated model (believes 0.5)", fontsize=11)
70+
71+
for ax in axes:
72+
ax.set_xlim(-0.09, 0.36)
73+
ax.set_ylim(-0.28, 0.17)
74+
ax.set_aspect("equal")
75+
ax.axis("off")
76+
77+
fig.tight_layout()
78+
out = str(Path(__file__).parent / "task_anatomy.png")
79+
fig.savefig(out, dpi=140, bbox_inches="tight")
80+
print("saved", out)

0 commit comments

Comments
 (0)