gap-graph-as-policy / atomrobot.html
trretretret's picture
Four-suite evaluation: baseline vs pre-ranking across Object/Spatial/Goal/Long
884af91
Raw History Blame Contribute Delete
30.5 kB
<!-- ATOMROBOT — agentic skill graphs for robot execution. Standalone, no affiliations. -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>AtomRobot — every skill earns its next step</title>
<style>
:root{
--bg:#0f1117; --panel:#171a23; --panel2:#1d212c; --ink:#e8eaf0; --muted:#9aa3b2;
--line:#2a2f3b; --accent:#76b900; --accent2:#3b82f6; --warn:#f59e0b;
--bad:#ef4444; --good:#22c55e;
--mono:ui-monospace,SFMono-Regular,"SF Mono",Menlo,monospace;
}
*{box-sizing:border-box}
body{margin:0; background:var(--bg); color:var(--ink);
font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",sans-serif;
line-height:1.65; -webkit-font-smoothing:antialiased}
.wrap{max-width:1080px; margin:0 auto; padding:44px 24px 100px}
header.hero{text-align:center; padding:34px 0 30px; border-bottom:1px solid var(--line); margin-bottom:38px}
.kicker{color:var(--accent); font:700 12.5px/1.5 var(--mono); letter-spacing:.18em; text-transform:uppercase}
h1{font-size:44px; margin:16px 0 10px; letter-spacing:-1.2px; text-wrap:balance}
h1 .hl{color:var(--accent)}
header.hero p{color:var(--muted); max-width:760px; margin:0 auto; font-size:16px}
h2{font-size:23px; margin:52px 0 14px; letter-spacing:-.3px; display:flex; align-items:center; gap:12px}
h2 .num{font:700 12px/1 var(--mono); color:var(--accent); border:1px solid var(--accent);
border-radius:5px; padding:5px 7px; letter-spacing:.06em}
h3{font-size:17px; margin:30px 0 10px; color:var(--ink)}
p{margin:12px 0}
p.lead{color:#c3c9d6}
.legend{color:var(--muted); font-size:13.5px}
b{color:#fff; font-weight:650}
code{font-family:var(--mono); font-size:12.6px; background:var(--panel2);
border:1px solid var(--line); border-radius:5px; padding:1.5px 5px}
table{width:100%; border-collapse:collapse; margin:18px 0; font-size:14px}
th,td{border-bottom:1px solid var(--line); padding:9px 10px; text-align:left}
th{color:var(--muted); font-weight:600; font-size:12.5px; text-transform:uppercase; letter-spacing:.05em}
td.n{text-align:right; font-family:var(--mono)}
.good{color:var(--good)} .bad{color:var(--bad)}
.callout{background:var(--panel); border:1px solid var(--line); border-left:3px solid var(--accent);
border-radius:10px; padding:16px 18px; margin:22px 0; font-size:14.4px}
.callout .t{display:block; font-weight:700; margin-bottom:6px; color:#fff}
.callout.warn{border-left-color:var(--warn)}
/* ---------- animated skill graph ---------- */
.gwrap{background:linear-gradient(180deg,#141824,#10131b); border:1px solid var(--line);
border-radius:14px; padding:18px 16px 14px; margin:24px 0 10px; overflow-x:auto}
#skillGraph{width:100%; height:auto; min-width:760px; display:block}
.edge{fill:none; stroke:var(--line); stroke-width:2.4; marker-end:url(#ah)}
.edge.fail{stroke:#2b2028; stroke-dasharray:7 6}
.lit{fill:none; stroke:var(--accent); stroke-width:3.4; marker-end:url(#ahg);
filter:drop-shadow(0 0 6px rgba(118,185,0,.55))}
.lit.badpath{stroke:var(--bad); marker-end:url(#ahb); filter:drop-shadow(0 0 6px rgba(239,68,68,.55))}
.gnode rect{fill:var(--panel2); stroke:var(--line); stroke-width:2; transition:all .32s ease}
.gnode .nlab{fill:var(--ink); font:600 14.5px var(--mono); text-anchor:middle; transition:fill .3s}
.gnode .nsub{fill:var(--muted); font:400 11px -apple-system,sans-serif; text-anchor:middle}
.gnode.ours rect{stroke:#3a4a22}
.gnode.active rect{stroke:var(--accent); fill:#1a2413; filter:drop-shadow(0 0 14px rgba(118,185,0,.5))}
.gnode.active .nlab{fill:#c7f06a}
.gnode.settled rect{stroke:#3f5a2a; fill:#161d13}
.gnode.term.active rect{stroke:var(--good); fill:#10241a; filter:drop-shadow(0 0 16px rgba(34,197,94,.55))}
.gnode.abort.active rect{stroke:var(--bad); fill:#241213; filter:drop-shadow(0 0 16px rgba(239,68,68,.55))}
.gate polygon{fill:var(--panel2); stroke:#3c4356; stroke-width:2; transition:all .3s ease}
.gate .glab{fill:var(--muted); font:600 12.5px var(--mono); text-anchor:middle}
.gate .gres{fill:transparent; font:700 15px var(--mono); text-anchor:middle; transition:fill .25s}
.gate.checking polygon{stroke:var(--warn); fill:#241d10; filter:drop-shadow(0 0 12px rgba(245,158,11,.45))}
.gate.pass polygon{stroke:var(--good); fill:#10241a}
.gate.pass .glab{fill:#9fe8b6} .gate.pass .gres{fill:var(--good)}
.gate.fail polygon{stroke:var(--bad); fill:#241213; filter:drop-shadow(0 0 12px rgba(239,68,68,.5))}
.gate.fail .glab{fill:#f3a5a5} .gate.fail .gres{fill:var(--bad)}
.railcap{fill:#6b3b42; font:600 11px var(--mono); text-anchor:middle}
.gbar{display:flex; gap:10px; align-items:center; flex-wrap:wrap; margin-top:12px}
.gbtn{background:var(--accent); color:#0b0d12; border:0; border-radius:8px;
padding:9px 16px; font:700 13px var(--mono); cursor:pointer; transition:filter .2s}
.gbtn:hover{filter:brightness(1.12)} .gbtn:disabled{opacity:.45; cursor:default}
.gbtn.alt{background:#3a2226; color:#ffb4b4; border:1px solid #5e3037}
.gstatus{font:600 12.8px var(--mono); color:var(--muted); margin-left:auto; text-align:right}
.gstatus .ok{color:var(--good)} .gstatus .no{color:var(--bad)} .gstatus .go{color:var(--accent)}
.score{background:linear-gradient(180deg,var(--panel),#141821); border:1px solid var(--line); border-radius:14px; padding:26px 24px; margin:20px 0}
.score .row{display:flex; align-items:center; gap:14px; margin:16px 0}
.score .tag{width:160px; flex:0 0 160px; font-size:13.5px}
.score .tag b{display:block; font-size:14px} .score .tag span{color:var(--muted); font-size:12px}
.track{flex:1; height:34px; background:var(--panel2); border-radius:7px; overflow:hidden; position:relative}
.fill{height:100%; width:0; border-radius:7px; transition:width 1.4s cubic-bezier(.2,.8,.2,1)}
.fill.base{background:linear-gradient(90deg,#7a2020,var(--bad))}
.fill.mut{background:linear-gradient(90deg,#3a3f4d,#6b7280)}
.fill.ours{background:linear-gradient(90deg,#3d5c00,var(--accent))}
.pct{position:absolute; right:10px; top:50%; transform:translateY(-50%); font:700 15px/1 var(--mono); color:#fff}
.delta{text-align:center; margin-top:10px; font:700 14px/1.5 var(--mono); color:var(--accent); opacity:0; transition:opacity .6s .9s}
.delta.show{opacity:1}
.cards{display:grid; grid-template-columns:repeat(auto-fit,minmax(170px,1fr)); gap:12px; margin:18px 0}
.card{background:var(--panel); border:1px solid var(--line); border-radius:10px; padding:15px 16px}
.card .lab{color:var(--muted); font-size:11.5px; text-transform:uppercase; letter-spacing:.06em}
.card .val{font:700 27px/1.15 var(--mono); margin-top:6px}
.vids{display:grid; grid-template-columns:repeat(auto-fit,minmax(280px,1fr)); gap:14px; margin:20px 0}
.vid{background:var(--panel); border:1px solid var(--line); border-radius:12px; padding:10px}
.vid video{width:100%; border-radius:8px; display:block; background:#000}
.vid .cap{font:600 12.5px var(--mono); margin-top:8px}
@media(max-width:640px){ h1{font-size:32px} .wrap{padding:28px 16px 70px} }
</style>
<div class="wrap">
<header class="hero">
<div class="kicker">Agentic skill graphs · robot execution</div>
<h1>Atom<span class="hl">Robot</span></h1>
<p>A language instruction is compiled into a <b>typed graph of atomic robot skills</b>. Each skill runs only when the
gate in front of it <b>verifies</b>. Nothing is a black box: when the robot fails, the graph says which atom failed
and why.</p>
</header>
<h2><span class="num">01</span> Watch it execute</h2>
<p class="lead">Boxes are <b>atomic skills</b>. Diamonds are <b>verification gates</b>. The rule is the whole idea:
<b>a skill fires only after the preceding gate passes.</b> Press play — then run the failing branch and watch execution
leave the pipeline the moment a gate says no.</p>
<div class="gwrap">
<svg id="skillGraph" viewBox="0 0 1120 620" role="img" aria-label="Animated atomic skill graph: observe, perceive target, perceive container, found gate, compute grasp, goto and close, held gate, transport, release, placed gate, done, plus an abort rail">
<defs>
<marker id="ah" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#2a2f3b"/></marker>
<marker id="ahg" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#76b900"/></marker>
<marker id="ahb" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse"><path d="M0,0 L10,5 L0,10 z" fill="#ef4444"/></marker>
</defs>
<g class="edges">
<path id="e1" class="edge" d="M210,103 H248"/>
<path id="e2" class="edge" d="M420,103 H458"/>
<path id="e3" class="edge" d="M630,103 H701"/>
<path id="e4" class="edge" d="M760,158 V238"/>
<path id="e5" class="edge" d="M620,267 H554"/>
<path id="e6" class="edge" d="M380,267 H309"/>
<path id="e7" class="edge" d="M250,294 V402"/>
<path id="e8" class="edge" d="M350,431 H416"/>
<path id="e9" class="edge" d="M590,431 H645"/>
<path id="e10" class="edge" d="M757,431 H786"/>
<path id="f1" class="edge fail" d="M817,103 H1015 V496"/>
<path id="f2" class="edge fail" d="M193,267 H150 V340 H1015 V496"/>
<path id="f3" class="edge fail" d="M700,486 V527 H936"/>
</g>
<g class="gnode" id="n1"><rect x="40" y="76" width="170" height="54" rx="10"/><text class="nlab" x="125" y="100">observe</text><text class="nsub" x="125" y="118">RGB-D capture</text></g>
<g class="gnode ours" id="n2"><rect x="250" y="76" width="170" height="54" rx="10"/><text class="nlab" x="335" y="100">perceive·target</text><text class="nsub" x="335" y="118">detect → pre-rank → VLM</text></g>
<g class="gnode ours" id="n3"><rect x="460" y="76" width="170" height="54" rx="10"/><text class="nlab" x="545" y="100">perceive·box</text><text class="nsub" x="545" y="118">detect → pre-rank → VLM</text></g>
<g class="gnode" id="n4"><rect x="620" y="240" width="170" height="54" rx="10"/><text class="nlab" x="705" y="264">compute_grasp</text><text class="nsub" x="705" y="282">6-DoF pose</text></g>
<g class="gnode" id="n5"><rect x="380" y="240" width="170" height="54" rx="10"/><text class="nlab" x="465" y="264">goto + close</text><text class="nsub" x="465" y="282">planned motion</text></g>
<g class="gnode" id="n6"><rect x="180" y="404" width="170" height="54" rx="10"/><text class="nlab" x="265" y="428">transport</text><text class="nsub" x="265" y="446">collision-free</text></g>
<g class="gnode" id="n7"><rect x="420" y="404" width="170" height="54" rx="10"/><text class="nlab" x="505" y="428">release</text><text class="nsub" x="505" y="446">open gripper</text></g>
<g class="gnode term" id="n8"><rect x="786" y="404" width="152" height="54" rx="10"/><text class="nlab" x="862" y="428">done</text><text class="nsub" x="862" y="446">reward 1.0</text></g>
<g class="gnode abort" id="nab"><rect x="936" y="496" width="152" height="54" rx="10"/><text class="nlab" x="1012" y="520">abort</text><text class="nsub" x="1012" y="538">arm → home</text></g>
<g class="gate" id="g1"><polygon points="760,48 817,103 760,158 703,103"/><text class="glab" x="760" y="100">found?</text><text class="gres" x="760" y="124"></text></g>
<g class="gate" id="g2"><polygon points="251,212 309,267 251,322 193,267"/><text class="glab" x="251" y="264">held?</text><text class="gres" x="251" y="288"></text></g>
<g class="gate" id="g3"><polygon points="701,376 757,431 701,486 645,431"/><text class="glab" x="701" y="428">placed?</text><text class="gres" x="701" y="452"></text></g>
<text class="railcap" x="1015" y="212">abort rail</text>
</svg>
<div class="gbar">
<button class="gbtn" id="gRun">▶ run · gates pass</button>
<button class="gbtn alt" id="gFail">⚠ run · perception fails</button>
<div class="gstatus" id="gStatus">ready</div>
</div>
</div>
<p class="legend">The two green nodes are the perception atoms — the most expensive part of the pipeline, and the one
this project rebuilt (§03).</p>
<h2><span class="num">02</span> Why gate every atom</h2>
<p class="lead">An end-to-end policy that fails gives you one bit of information: it failed. A gated graph gives you the
node, the inputs it saw, and the branch it took. That turns debugging a robot into reading a trace.</p>
<p>The gates are what make the failure legible. A run that dies at <code>found?</code> stops after about <b>51 control
steps</b> — the arm has not moved, because the graph refused to hand an unverified object pose to the motion planner. A
run that clears every gate executes roughly <b>750 steps</b>. You can classify a trial by its step count alone.</p>
<div class="vids">
<div class="vid"><video src="media/demo/gap_fail_abort.mp4" autoplay loop muted playsinline></video>
<div class="cap"><span class="bad">gate failed</span> · perception could not verify → abort, arm home</div></div>
<div class="vid"><video src="media/demo/gap_success_reward1.mp4" autoplay loop muted playsinline></video>
<div class="cap"><span class="good">all gates passed</span> · grasp → transport → release, reward 1.0</div></div>
</div>
<h2><span class="num">03</span> Making the perception atom cheap</h2>
<p class="lead">The perception atom dominated cost. It proposed ~8 candidate boxes and ran a single-elimination
tournament of vision-language comparisons — one model call per elimination, several times per task.</p>
<p>The fix is to rank candidates against the target <b>name</b> first, keep the top few, and skip the tournament
entirely when the leader is unambiguous. Same interface, same gates, far less compute:</p>
<table>
<thead><tr><th>Task family</th><th class="n">calls / trial · before</th><th class="n">calls / trial · after</th><th class="n">reduction</th></tr></thead>
<tbody>
<tr><td>Tabletop objects</td><td class="n">14.4</td><td class="n"><b class="good">2.6</b></td><td class="n">5.5×</td></tr>
<tr><td>Spatial referring</td><td class="n">16.3</td><td class="n"><b class="good">1.9</b></td><td class="n">8.6×</td></tr>
<tr><td>Goal-directed</td><td class="n">14.0</td><td class="n"><b class="good">1.3</b></td><td class="n">10.8×</td></tr>
<tr style="background:rgba(118,185,0,.08)"><td><b>Average</b></td><td class="n"><b>14.9</b></td><td class="n"><b class="good">1.9</b></td><td class="n"><b>≈8×</b></td></tr>
</tbody>
</table>
<div class="callout">
<span class="t">The honest headline: same accuracy, a fraction of the compute</span>
Across <b>90 matched trials</b> the two configurations score <b>identically — 36/90 each (40.0%)</b>. Pre-ranking is
not an accuracy trick; it is an <b>efficiency</b> result: the same behaviour at roughly <b>8× fewer</b> vision-language
calls and ~2.3× faster wall-clock. Where perception is the <i>binding constraint</i>, it becomes an accuracy result
too — on a deliberately weak perception backend the same change lifted one task from <b>6.7% → 80.0%</b>.
</div>
<h2><span class="num">04</span> Where the pipeline works — and where it stops</h2>
<table>
<thead><tr><th>Task family (10 tasks × 3 trials each)</th><th class="n">baseline</th><th class="n">+ pre-ranking</th></tr></thead>
<tbody>
<tr><td>Tabletop pick-and-place</td><td class="n"><b>28/30 = 93.3%</b></td><td class="n"><b class="good">29/30 = 96.7%</b></td></tr>
<tr><td>Spatial referring expressions</td><td class="n">3/30 = 10.0%</td><td class="n">1/30 = 3.3%</td></tr>
<tr><td>Goal-directed manipulation</td><td class="n">5/30 = 16.7%</td><td class="n">6/30 = 20.0%</td></tr>
<tr style="background:rgba(118,185,0,.08)"><td><b>All 90 trials</b></td><td class="n"><b>36/90 = 40.0%</b></td><td class="n"><b>36/90 = 40.0%</b></td></tr>
</tbody>
</table>
<p class="legend">Read the collapse outside pick-and-place carefully — the traces show <b>three different causes</b>, and
only one is a real ceiling.</p>
<h3>① A real limit: the skill library</h3>
<p>Opening a drawer, turning a knob, pushing a plate — these are not pick-and-place. Those tasks ran their full trials
with <b>zero perception aborts</b> and the <i>longest</i> executions measured: <b>2680–3297 control steps</b> against
~720 for a success. The graph was built, perception verified, and the arm genuinely tried. The manipulation atoms
simply do not exist. That is a library boundary, and it is visible precisely because the gates all passed.</p>
<h3>② A fixable bug: the qualifier gets dropped</h3>
<p>For "the bowl <i>between the plate and the ramekin</i>", graph generation emits the target as just
<code>"black bowl"</code> — and the scene contains two identical bowls. No downstream atom can recover the missing
words. Worse, confident pre-ranking then locks onto the <i>salient</i> bowl rather than the <i>referred</i> one, which
is why that column moves the wrong way. The fix under test scores candidates by <b>relation geometry</b> against
detected anchor objects, at zero extra model calls.</p>
<h3>③ Sampling, not capability</h3>
<p>One graph is sampled per task. A single malformed sample zeroes all three trials — one observed failure was a graph
whose error branch routed to a label its own edge map never declared, so it died before the arm moved at all. That is
graph-generation variance, not robot capability, and it is reported separately rather than folded into the score.</p>
<div class="callout warn">
<span class="t">Scope, stated plainly</span>
3 trials per task, one graph sample per task, a free-tier model behind the perception atom, and a fourth task family
(long-horizon, multi-step) still running. Small numbers are labelled as such. Every trial counted here actually ran:
runs killed by quota, connectivity, or out-of-memory are discarded and re-run, never scored as failures.
</div>
<h2><span class="num">05</span> The reproduction — matching the published baseline, for free</h2>
<p class="lead">The reference implementation reports LIBERO grocery success around <b>~0.95</b>. Earlier reproduction
attempts fell short only because they ran an <b>open perception backend</b>. Point the <code>perceive</code> atom at
the framework's <b>default</b> perception model — <b>Gemini-3.1-flash-lite</b>, on Google's own endpoint — and the
<b>unmodified baseline</b> reproduces that level. We ran the entire thing on a <b>free</b> API key.</p>
<div class="score" id="repScore">
<div class="row">
<div class="tag"><b style="color:var(--muted)">Our free reproduction</b><span>unmodified baseline · all 10 tasks</span></div>
<div class="track"><div class="fill mut" data-w="93.3"></div><div class="pct" id="pRep">0.0%</div></div>
</div>
<div class="row">
<div class="tag"><b style="color:var(--accent)">+ our pre-ranking</b><span>same tasks, same backend</span></div>
<div class="track"><div class="fill ours" data-w="96.7"></div><div class="pct" id="pOurs">0.0%</div></div>
</div>
<div class="delta" id="dRep">baseline 28/30 = 93.3% → ours 29/30 = 96.7%&nbsp;&nbsp;·&nbsp;&nbsp;~5.5× fewer perception calls (14.4 → 2.6 per trial, measured)&nbsp;&nbsp;·&nbsp;&nbsp;free API key</div>
</div>
<table>
<thead><tr><th>Grocery task · 3 trials each</th><th class="n">baseline</th><th class="n">+ pre-ranking</th><th class="n">ours calls/trial</th></tr></thead>
<tbody>
<tr><td>0 · alphabet soup can</td><td class="n">2/3</td><td class="n">2/3</td><td class="n">3.3</td></tr>
<tr><td>1 · cream cheese box</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">4.0</td></tr>
<tr><td>2 · dark salad-dressing bottle</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.7</td></tr>
<tr><td>3 · red bbq sauce bottle</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.0</td></tr>
<tr><td>4 · ketchup bottle</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.0</td></tr>
<tr style="background:rgba(118,185,0,.06)"><td>5 · tomato sauce can</td><td class="n"><span class="bad">2/3</span></td><td class="n"><span class="good"><b>3/3</b></span></td><td class="n">1.3</td></tr>
<tr><td>6 · butter box</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.0</td></tr>
<tr><td>7 · milk carton</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.7</td></tr>
<tr><td>8 · chocolate pudding box</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">3.3</td></tr>
<tr><td>9 · orange juice</td><td class="n">3/3</td><td class="n">3/3</td><td class="n">2.7</td></tr>
<tr style="background:rgba(118,185,0,.10)"><td><b>Aggregate · all 10 tasks</b></td><td class="n"><b>28/30 = 93.3%</b></td><td class="n"><b class="good">29/30 = 96.7%</b></td><td class="n"><b>2.6</b> <span class="legend">vs 14.4 base</span></td></tr>
</tbody>
</table>
<p class="legend">Same-scene A/B across the whole gate. On this strong backend the baseline already sits near the
ceiling, so accuracy headroom is small — only task 5 moves. The win here is <b>parity-or-better accuracy at ~5.5×
fewer perception calls</b>: it prunes ~8 detector candidates to 3 before the tournament, and skips the container
comparison outright when the leader is unambiguous.</p>
<div class="callout warn">
<span class="t">The honest scope</span>
No budget for paid API, so everything runs on a <b>free key with a tight daily quota</b>, spread over several days as
it refilled. That means <b>3 trials/task</b> against the reference's ~30–50 — a <b>sanity-scale reproduction, not a
full-scale eval</b>. What it is not is fabricated: every trial is counted, and runs killed by quota, connectivity, or
memory are discarded and re-run, never scored as failures.
</div>
<h2><span class="num">06</span> The severe case — when the abort <i>is</i> the failure</h2>
<p class="lead">On a strong perception backend the gates rarely trip, so pre-ranking buys efficiency. Put the same
graph on a <b>deliberately constrained</b> backend, where the perception atom genuinely cannot resolve the object, and
the same change stops being an optimisation and starts being the difference between moving and not moving.</p>
<div class="score" id="sevScore">
<div class="row">
<div class="tag"><b style="color:var(--bad)">Baseline</b><span>perception aborts</span></div>
<div class="track"><div class="fill base" data-w="6.7"></div><div class="pct" id="pSevB">0.0%</div></div>
</div>
<div class="row">
<div class="tag"><b style="color:var(--accent)">+ our pre-ranking</b><span>perception resolves</span></div>
<div class="track"><div class="fill ours" data-w="80"></div><div class="pct" id="pSevO">0.0%</div></div>
</div>
<div class="delta" id="dSev">▲ +73.3 points&nbsp;&nbsp;·&nbsp;&nbsp;2/30 → 24/30 trials&nbsp;&nbsp;·&nbsp;&nbsp;30-trial ablation, p ≪ 0.001</div>
</div>
<div class="cards">
<div class="card"><div class="lab">Success</div><div class="val"><span style="color:var(--bad)">6.7%</span> <span style="color:var(--muted);font-size:18px">→</span> <span style="color:var(--accent)">80.0%</span></div></div>
<div class="card"><div class="lab">Pre-ranking fired</div><div class="val" style="color:var(--accent)">90×</div></div>
<div class="card"><div class="lab">Baseline failures = perception</div><div class="val" style="color:var(--bad)">100%</div></div>
<div class="card"><div class="lab">Avg control steps</div><div class="val">51 <span style="color:var(--muted);font-size:18px">→</span> 746</div></div>
</div>
<p class="legend"><b>Why the step count is the proof.</b> A baseline trial dies after ~<b>51</b> steps: the graph
reached <code>perceive</code>, the <code>found?</code> gate refused it, and the arm never moved. With pre-ranking the
same task runs ~<b>746</b> steps — the gate passes, so the arm actually grasps and places. Independent failure
attribution agrees: <b>100% of baseline failures (28/28) are perception</b>, exactly the atom this targets.</p>
<div class="callout warn">
<span class="t">Read §06 as an ablation, not a headline</span>
These numbers come from a <b>controlled module-on / module-off ablation</b> on a deliberately weak perception
backend, which is why the baseline here is 6.7% rather than §05's 93.3%. Everything else is identical, so the gap
between the two rows isolates the module's effect. Read <b>the difference</b>, not the absolute values — and read
§04's dead heat as the fair verdict on strong backends.
</div>
<p class="legend" style="margin-top:34px; border-top:1px solid var(--line); padding-top:16px">
Built on an open-source typed skill-graph framework; the perception pre-ranking, the measurements, and the failure
attribution are our own. All numbers come from real benchmark executions on 2×RTX 4090 — none are estimated.
</p>
</div>
<script>
(function(){
var svg=document.getElementById('skillGraph'); if(!svg) return;
var status=document.getElementById('gStatus'),
bRun=document.getElementById('gRun'), bFail=document.getElementById('gFail');
var overlays={}, timers=[], running=false;
// build an animated overlay for every static edge
svg.querySelectorAll('.edges path').forEach(function(p){
var o=p.cloneNode(); o.id=''; o.setAttribute('class','lit');
var L=p.getTotalLength();
o.style.strokeDasharray=L; o.style.strokeDashoffset=L; o.style.opacity=0;
p.parentNode.appendChild(o); overlays[p.id]={el:o,len:L};
});
function reset(){
timers.forEach(clearTimeout); timers=[];
Object.keys(overlays).forEach(function(k){
var o=overlays[k]; o.el.style.transition='none'; o.el.style.strokeDashoffset=o.len;
o.el.style.opacity=0; o.el.classList.remove('badpath');
});
svg.querySelectorAll('.gnode').forEach(function(n){n.classList.remove('active','settled');});
svg.querySelectorAll('.gate').forEach(function(n){
n.classList.remove('checking','pass','fail'); n.querySelector('.gres').textContent='';
});
void svg.getBoundingClientRect();
}
function at(ms,fn){ timers.push(setTimeout(fn,ms)); }
function say(html){ status.innerHTML=html; }
var NODE_MS=560, EDGE_MS=460, GATE_MS=780;
function play(fail){
if(running) return; running=true; reset();
bRun.disabled=true; bFail.disabled=true;
var t=260;
// step list: n=node, e=edge, g=gate
var steps=[{n:'n1',s:'observe · capturing scene'},
{e:'e1'},{n:'n2',s:'perceive · ranking candidates'},
{e:'e2'},{n:'n3',s:'perceive · locating container'},
{e:'e3'},{g:'g1',pass:!fail,s:'verify found?'}];
if(fail){
steps=steps.concat([{e:'f1',bad:1},{n:'nab',bad:1,s:'gate failed → abort, arm home'}]);
}else{
steps=steps.concat([{e:'e4'},{n:'n4',s:'compute_grasp · 6-DoF pose'},
{e:'e5'},{n:'n5',s:'goto + close gripper'},
{e:'e6'},{g:'g2',pass:true,s:'verify held?'},
{e:'e7'},{n:'n6',s:'transport · collision-free'},
{e:'e8'},{n:'n7',s:'release'},
{e:'e9'},{g:'g3',pass:true,s:'verify placed?'},
{e:'e10'},{n:'n8',s:'done · reward 1.0'}]);
}
steps.forEach(function(st){
if(st.e){
at(t,function(){
var o=overlays[st.e]; if(!o) return;
if(st.bad) o.el.classList.add('badpath');
o.el.style.opacity=1;
o.el.style.transition='stroke-dashoffset '+EDGE_MS+'ms linear';
o.el.style.strokeDashoffset=0;
});
t+=EDGE_MS+40;
} else if(st.n){
at(t,function(){
var el=document.getElementById(st.n);
svg.querySelectorAll('.gnode.active').forEach(function(p){
p.classList.remove('active'); p.classList.add('settled');
});
el.classList.add('active');
say('<span class="'+(st.bad?'no':'go')+'">'+st.s+'</span>');
});
t+=NODE_MS;
} else if(st.g){
at(t,function(){
var el=document.getElementById(st.g);
el.classList.add('checking'); say('<span style="color:var(--warn)">'+st.s+'</span>');
});
at(t+GATE_MS*0.55,function(){
var el=document.getElementById(st.g);
el.classList.remove('checking');
el.classList.add(st.pass?'pass':'fail');
el.querySelector('.gres').textContent = st.pass?'✓ pass':'✗ fail';
say(st.pass ? '<span class="ok">'+st.s+' → ✓ pass</span> · next atom may execute'
: '<span class="no">'+st.s+' → ✗ fail</span> · leaving pipeline');
});
t+=GATE_MS;
}
});
at(t+420,function(){
running=false; bRun.disabled=false; bFail.disabled=false;
say(fail?'<span class="no">aborted — failure localised to the perceive atom</span>'
:'<span class="ok">complete — every gate verified before its skill ran</span>');
});
}
bRun.addEventListener('click',function(){play(false);});
bFail.addEventListener('click',function(){play(true);});
var seen=false;
new IntersectionObserver(function(es){
es.forEach(function(e){ if(e.isIntersecting && !seen){ seen=true; setTimeout(function(){play(false);},350); } });
},{threshold:.3}).observe(svg);
})();
/* animated score panels (§05, §06) */
(function(){
function countTo(el,target,dur){
if(!el) return; var start=null;
function step(ts){ if(!start) start=ts; var p=Math.min((ts-start)/dur,1);
el.textContent=(target*(p<1?(1-Math.pow(1-p,3)):1)).toFixed(1)+'%';
if(p<1) requestAnimationFrame(step); }
requestAnimationFrame(step);
}
function panel(id,pairs,deltaId){
var el=document.getElementById(id); if(!el) return; var fired=false;
new IntersectionObserver(function(es){
es.forEach(function(e){
if(!e.isIntersecting||fired) return; fired=true;
el.querySelectorAll('.fill').forEach(function(f){ f.style.width=f.dataset.w+'%'; });
pairs.forEach(function(p){ countTo(document.getElementById(p[0]),p[1],1400); });
setTimeout(function(){ var d=document.getElementById(deltaId); if(d) d.classList.add('show'); },900);
});
},{threshold:.4}).observe(el);
}
panel('repScore',[['pRep',93.3],['pOurs',96.7]],'dRep');
panel('sevScore',[['pSevB',6.7],['pSevO',80.0]],'dSev');
})();
</script>