Buckets:
| # Graph-grounded support tracking for frozen VLA policies | |
| A vision-language-action policy that is never retrained **drifts** off the demonstrated manifold when | |
| the world is perturbed. | |
| This repo tracks that drift with one mechanism: a graph built from the training demonstrations, plus | |
| a small GNN that maps a window of joint states to a distribution over graph nodes, filtered over time | |
| into a belief. The policy is frozen — no finetuning, no LoRA, no adapter. The same artifacts drive | |
| StableVLA and GR00T-N1.7 without touching either. | |
| > **Naming.** The Python package is `onf` ("Oriented Neural Field"). That name is historical. | |
| > Nothing in the current method is a neural field. Read `onf` as the package name and nothing else. | |
| > The one component that is still a field — `onf_head.npz`, a 136k-parameter MLP scoring | |
| > distance-to-manifold — is called the **cleanliness field** everywhere in these docs. | |
| ``` | |
| demo HDF5 ──► g_nodes.npz ──► g_head.npz ──► g_track.npz | |
| (raw) + g_edges.npz (the GNN) (transition kernel + basin geometry) | |
| │ │ │ | |
| └────────┬──────────┘────────────────────┘ | |
| ▼ | |
| SENTINEL (belief filter) | |
| ``` | |
| Once per policy action chunk (8 env steps) the sentinel steps its belief over the graph and reads a | |
| target off it: the projection of the current configuration onto the certified basin around the | |
| tracked node, published as a `SEG_K`-step reference segment. | |
| **The reading is applied by mixing it into the policy's own actions.** The reference segment becomes | |
| an end-effector tracking chunk in the policy's action units, and that chunk is blended into the | |
| `[8, 7]` chunk the policy just produced, at exactly one point — the refill line in | |
| `evals/libero_plus/harness/model2libero_interface.py`. There is no second controller: the policy | |
| keeps executing its own chunk. The gripper column is never blended. Modes in | |
| `evals/common/modes.sh`: `base`, `blend` (alpha 0, the plumbing check), the fixed-alpha probes | |
| (`blend_a05`, `blend_a015_bounded`, `blend_bounded`, …) and the learned-weight arms | |
| (`blend_scaled015`, `blend_full`). `blend_full` is the current recipe. | |
| The joint-space PD servo, the WHEN e-process fire rule and the conformal monitors that preceded this | |
| were **deleted in commit `e942260`** — they existed only to support a second controller. Their | |
| results are kept as history in [`docs/results.md`](docs/results.md) §1–§4 and are not reproducible | |
| on this tree. | |
| ## Where to go | |
| | you want to | read | time | | |
| |---|---|---| | |
| | understand the idea | [`docs/concepts.md`](docs/concepts.md) | 20 min | | |
| | set it up from scratch | [`docs/SETUP.md`](docs/SETUP.md) | once | | |
| | look up a term | [`docs/GLOSSARY.md`](docs/GLOSSARY.md) | — | | |
| | run it | [`docs/RUNBOOK.md`](docs/RUNBOOK.md) | — | | |
| | modify the code | [`docs/technical/`](docs/technical/), in chapter order | 2 h | | |
| | edit the code | [`docs/code_style.md`](docs/code_style.md) — the cleanup rule and the numeric invariants | — | | |
| | check the numbers | [`docs/results.md`](docs/results.md) — every measured cell; the table below is the headline excerpt | — | | |
| ## Results | |
| Full tables, per-axis breakdown, run-to-run spread and McNemar tests: | |
| [`docs/results.md`](docs/results.md). The current mechanism in full, and two rows of the deleted one: | |
| **Current — action-chunk blend, full stack (`blend_full`), StableVLA, `long`, 2026-08-12** | |
| (`results.md` §6). Learned per-row per-block weight rescaled by `SN_BLEND_SCALE=0.27`, row-0 | |
| saturation bound, behavioural task lane, base-frame tracking chunk. Paired McNemar against the | |
| `lp_libero_10_*` base on every cell: | |
| | axis | n | base | `blend_full` | Δ | p | | |
| |---|---|---|---|---|---| | |
| | Robot_Initial_States | 393 | 170 = 43.3% | 246 = 62.6% | **+19.3** | 4.3e−15 | | |
| | Camera_Viewpoints | 419 | 230 = 54.9% | 278 = 66.3% | **+11.5** | 9.7e−07 | | |
| | Objects_Layout | 312 | 247 = 79.2% | 275 = 88.1% | **+9.0** | 2.3e−04 | | |
| | Sensor_Noise | 449 | 336 = 74.8% | 375 = 83.5% | **+8.7** | 2.6e−06 | | |
| | Language_Instructions | 382 | 299 = 78.3% | 304 = 79.6% | +1.3 | 0.72 ns | | |
| | Background_Textures | 289 | 243 = 84.1% | 245 = 84.8% | +0.7 | 0.90 ns | | |
| | Light_Conditions | 273 | 243 = 89.0% | 244 = 89.4% | +0.4 | 1.00 ns | | |
| | **total** | **2517** | **1768 = 70.2%** | **1967 = 78.1%** | **+7.9** | | | |
| The same mechanism measured **−12.6 pp** on the same seven axes twelve hours earlier, at a fixed | |
| alpha with an unbounded row 0, a task lane that resolved to nothing on the axis it was for, and a | |
| world-frame chunk (`results.md` §5, **superseded**). Four fixes, each measured separately, are the | |
| whole difference; `results.md` §6a names them. | |
| **Historical — the deleted entry/sentinel arms, 2026-08-06/07** (`results.md` §1–§4), kept as the | |
| record of what the graph reading was worth under a joint-space controller: | |
| | cell | StableVLA | GR00T-N1.7 | | |
| |---|---|---| | |
| | t=0 `entry`, `long` × Robot_Initial_States (n=393) | 378/393 = 96.2% | 359/393 = 91.3% | | |
| | t>0 `sentinel`, `long` × Objects_Layout (n=312) | 249/312 = 79.8% | 198/312 = 63.5% | | |
| Read with these caveats. They are not boilerplate; each one changes how the tables should be | |
| interpreted. | |
| - **Four axes gain and three do not move.** Language_Instructions, Background_Textures and | |
| Light_Conditions are all non-significant. The +7.9 pp total is carried entirely by the four | |
| significant cells; rounding the other three up would misattribute it. | |
| - **On Robot_Initial_States the blend is still 33.6 pp behind what it replaced.** The deleted `entry` | |
| recipe scores 378/393 = 96.2% on that cell against `blend_full`'s 62.6% — paired, 141 episodes | |
| broken against 9 fixed, p = 1.2e−31. A t=0 joint-space prefix is still the best answer to a t=0 | |
| joint-space perturbation, and this tree cannot run one. | |
| - **Language_Instructions is not a win, and no Language arm is.** Ten arms have been run on that | |
| axis and none differs significantly from base. The best is `blend_scaled015` at 311/382 = 81.4% | |
| (p = 0.21); against `blend_full` it is −1.9 pp, CI [−5.6, +2.1], p = 0.42. Any mechanism story | |
| about the difference between those two recipes is unsupported. | |
| - **No single arm is best everywhere.** The superseded fixed-alpha `blend_a05` still leads on | |
| Camera_Viewpoints (293/419 = 69.9% vs 278). Oracle unions over arms that already exist reach 98.7% | |
| on Robot_Initial_States and 88.0% on Language — the headroom is in a per-episode selector, not in | |
| another weight (`results.md` §6h). | |
| - **The blend's basin gate never fired** (`no_op` on 0% of checks) in the only sweep that measured | |
| it, so it ran on all 42–57 checks an episode, where the servo it replaced was capped at 2 | |
| interventions. | |
| - **Only `long`, only StableVLA.** No blend arm has been run on `object`, `spatial` or `goal`, and | |
| none on GR00T-N1.7. | |
| - **No same-session `base` arm was run for any GR00T cell.** There is no baseline to compare against | |
| in the same session, and cross-session GR00T comparison is invalid (next bullet). | |
| - **GR00T-N1.7 is flow-matching and unseeded.** Measured run-to-run spread on the sentinel cell is | |
| 62.8–66.7% over seven runs of the identical config (`results.md` §2c). Any GR00T delta smaller than | |
| ~4 pp is noise. StableVLA is deterministic (0 episode churn on the entry cell across 393 episodes), | |
| so its arms compare episode-by-episode. | |
| - **On GR00T at Robot_Initial_States, a fixed-home baseline (94.7%) beats this method (91.3%).** The | |
| graph-derived entry target wins on StableVLA and loses on GR00T. | |
| ## Install | |
| Three environments; they cannot be merged (conflicting pins). Install commands and the constraints | |
| this repo adds on top of each upstream README: [`docs/SETUP.md`](docs/SETUP.md) §2. Why they are | |
| split the way they are: [`envs/README.md`](envs/README.md). | |
| | runtime | env file | used for | | |
| |---|---|---| | |
| | `onf-core` | `envs/onf-core.yml` | the `onf` package: graph build/train, artifacts, tests. CPU is enough. | | |
| | `stablevla` | `envs/stablevla.yml` | StableVLA policy server **and** the LIBERO-Plus sim client | | |
| | gr00t | Isaac-GR00T's own `.venv`, path in `$GR00T_PY` | GR00T-N1.7 policy server | | |
| ```bash | |
| conda env create -f envs/onf-core.yml && conda activate onf-core | |
| pip install -e . | |
| export ONF_DATA=... ONF_OUTPUTS=... ONF_RESULTS=... | |
| ``` | |
| `onf.config.Paths` resolves every path from those three variables. Nothing is hardcoded. | |
| **Checkpoints, demonstrations and trained artifacts do not ship with this repo** — the first two are | |
| downloads (~10 GB a suite, ~17 GB with the GR00T arm), the third is trained from the second. | |
| [`docs/SETUP.md`](docs/SETUP.md) is six steps of copy-paste: what to clone, what to download, which | |
| directory each piece lands in, and the three places StableVLA's and LIBERO's own instructions have to | |
| be overridden. | |
| On the author's box `data/`, `outputs/`, `results/` and `external/` are symlinks into a shared | |
| working tree. | |
| ## Hardware and wall-clock | |
| | step | hardware | time (`long`, 500 demos) | | |
| |---|---|---| | |
| | cleanliness field | CPU | ~10 s | | |
| | graph build | CPU | minutes | | |
| | head training | 1 GPU | ~1 h | | |
| | tracker fit | 1 GPU | ~30 min | | |
| | smoke benchmark (`--rung R1`, 35 episodes) | 2 GPUs | ~15 min | | |
| | full suite × axis benchmark | 2 GPUs | ~1 h per 400-episode cell | | |
| Estimates, not a benchmark. The build figures come from the timed end-to-end run recorded in | |
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md). The per-cell figure is one measured `long` × | |
| Robot_Initial_States run on a single GPU (393 episodes, 8 clients, ~60 min). | |
| `OMP_NUM_THREADS=4` is required for every command. Without it the training loop oversubscribes | |
| threads and runs an order of magnitude slower. | |
| ## Quickstart | |
| Assumes [`docs/SETUP.md`](docs/SETUP.md) is done — clones, environments, weights, demonstrations and | |
| artifacts all in place. | |
| ```bash | |
| export PY=python | |
| export OMP_NUM_THREADS=4 PYTHONPATH=src | |
| export SUITE=long # object | spatial | goal | long | |
| export GPU=0 # adjust to a free device | |
| # are this suite's artifacts built? | |
| $PY -c "from onf.config import default_paths as d; import os; \ | |
| print({s: sorted(os.listdir(d().graph(s))) for s in ('object','spatial','goal','long')})" | |
| # benchmark. Dry-run first. | |
| $PY scripts/run_sr.py --check-env | |
| $PY scripts/run_sr.py --rung R1 --gpus $GPU --dry-run | |
| $PY scripts/run_sr.py --rung R1 --gpus $GPU | |
| # score. The tag prefix names the policy: run_sr.py writes stablevla_*, run_gr00t.py writes gr00t_*. | |
| $PY evals/libero_plus/score.py --suites $SUITE --methods base:stablevla_base | |
| ``` | |
| [`docs/RUNBOOK.md`](docs/RUNBOOK.md) is the same list with outputs, timings and failure modes. | |
| ## Repo layout | |
| ``` | |
| src/onf/ | |
| device.py the single torch.device resolver | |
| config.py Paths + GraphConfig / SentinelConfig, all env-driven | |
| fk.py forward kinematics helpers | |
| graph/ | |
| core/ the graph itself. Imports nothing else in onf.graph. Mostly numpy-only. | |
| schema.py every constant and the on-disk artifact layout | |
| nodes.py NodeTable: raw frames -> nodes | |
| edges.py EdgeSet: the 12 relations | |
| binning.py phase -> bin index; the one bucketing everything agrees on | |
| geometry.py distances, graph_hash, cleanliness | |
| corpus.py graph measurement (no torch) | |
| net/ pure nn.Module. Takes tensors, knows no paths, knows no |V|. | |
| run/ executes inside a rollout. Imports neither train/ nor build/. | |
| retrieve.py memoryless inference | |
| track.py belief-filter inference | |
| kernel.py params.py readout.py | |
| train/ build-time: loop.py + data / loss / metrics / types / drift | |
| build/ build-time artifact producers, top of the dependency graph | |
| cli.py python -m onf.graph build | train | |
| sentinel/ the runtime observer: per-check belief step + basin readout | |
| blend/ the correction: reference segment -> action-unit chunk -> mix into the policy's | |
| ee_track.py (SO(3), ActionScale, EeTrack, blend), plan.py, alpha.py (the learned | |
| weight), target.py + kinematics.py (build-time only) | |
| field/ the cleanliness field: build.py -> train.py -> field.py | |
| evals/ | |
| common/modes.sh the ONLY definition of the run modes | |
| libero_plus/ vendored sim client, scorer, McNemar | |
| gr00t/run_gr00t.py GR00T-N1.7 benchmark driver | |
| stablevla/ StableVLA policy server | |
| scripts/ | |
| run_sr.py StableVLA benchmark driver (ladder, scheduling, ledger) | |
| build_sentinel_artifacts.py fits g_track.npz | |
| fit_action_scale.py fits action_scale.json — metres/radians per action unit | |
| cache_policy_actions.py caches the frozen policy's own chunks (a_pi_raw.npz) for alpha training | |
| trace_query.py walks ONE query through the whole pipeline, printing shapes | |
| capture_golden.py regenerates the bit-parity golden file | |
| ``` | |
| The five `graph/` layers import downwards only: `core ← net ← run, train`, with `build` on top. | |
| `tests/test_layering.py` asserts that and the absence of import cycles. `tests/test_imports.py` | |
| asserts the numpy-only modules stay numpy-only. | |
| ## Reproducibility | |
| ```bash | |
| OMP_NUM_THREADS=4 PYTHONPATH=src pytest tests/ -q | |
| ``` | |
| Three tests are contracts, not coverage: | |
| - `tests/test_parity.py` — replays 64 recorded t=0 retrievals and 64 t>0 tracker steps against the | |
| real trained artifacts and asserts exact float equality (`float.hex()`, not `approx`) against | |
| `tests/golden/parity.json`. The golden file was captured with **no cleanliness field loaded**, so | |
| the parity net covers the graph head and the belief filter only, not the field path. | |
| - `tests/test_layering.py` — the import rules above. | |
| - `tests/test_imports.py` — which modules must stay torch-free. | |
| Every artifact carries a `graph_hash` stamp and is refused on mismatch. Every result directory carries | |
| a `run.json` tying its number to a git commit and a specific artifact set. See | |
| [`docs/technical/05-artifacts.md`](docs/technical/05-artifacts.md) for why the hash check is the most | |
| important check in the subsystem. | |
| ## License and provenance | |
| *TODO: no LICENSE file exists. Two vendored trees ship without an upstream license | |
| (`UPSTREAM.md` flags them). Resolve before any distribution.* | |
| Third-party code, commits and licenses: [`UPSTREAM.md`](UPSTREAM.md). | |
Xet Storage Details
- Size:
- 14.6 kB
- Xet hash:
- 7ca8b9924f3422c75c94302b59f2a025c545ade20ba6b20ee2ebcacb2615ffb4
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.