Buckets:
| Name | Size | Uploaded | Xet hash |
|---|---|---|---|
| .pytest_cache | 5 items | ||
| configs | 3 items | ||
| docs | 11 items | ||
| envs | 3 items | ||
| evals | 29 items | ||
| scripts | 17 items | ||
| src | 105 items | ||
| tests | 36 items | ||
| .git | 56 Bytes xet | 0b78c67e | |
| .gitignore | 1.07 kB xet | 241af755 | |
| README.md | 14.6 kB xet | 7ca8b992 | |
| UPSTREAM.md | 4.43 kB xet | 0d7e3dd1 | |
| environment.yml | 868 Bytes xet | c415a42e | |
| pyproject.toml | 1.5 kB xet | cca453fa | |
| requirements.txt | 292 Bytes xet | 12bbae78 |
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. Readonfas 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
entryrecipe scores 378/393 = 96.2% on that cell againstblend_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_scaled015at 311/382 = 81.4% (p = 0.21); againstblend_fullit 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_a05still 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_opon 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 onobject,spatialorgoal, and none on GR00T-N1.7. - No same-session
basearm 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
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
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(), notapprox) againsttests/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