235 GB
849,915 files
Updated 8 days ago
Name
Size
.pytest_cache
configs
docs
envs
evals
scripts
src
tests
.git56 Bytes
xet
.gitignore1.07 kB
xet
README.md14.5 kB
xet
UPSTREAM.md4.43 kB
xet
environment.yml868 Bytes
xet
pyproject.toml1.5 kB
xet
requirements.txt292 Bytes
xet
README.md

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 §1–§4 and are not reproducible on this tree.

Where to go

you want to read time
understand the idea docs/concepts.md 20 min
set it up from scratch docs/SETUP.md once
look up a term docs/GLOSSARY.md
run it docs/RUNBOOK.md
modify the code docs/technical/, in chapter order 2 h
edit the code docs/code_style.md — the cleanup rule and the numeric invariants
check the numbers 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. 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 §2. Why they are split the way they are: 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
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 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. 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 is done — clones, environments, weights, demonstrations and artifacts all in place.

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

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

Total size
235 GB
Files
849,915
Last updated
Aug 15
Pre-warmed CDN
US EU US EU

Contributors