AgentCanvas / Pages / Developer Guide / Nodesets / Env / ObjectNav
2026-07-21 14:18

The ObjectNav NodeSet (EnvObjnavNodeSet) wraps habitat-lab 0.2.4 for object-goal navigation: an episode names a category and the agent must reach any instance of it and call STOP within 1 m. Two episode corpora are selectable from the env panel β€” HM3D (6 categories: chair, bed, plant, toilet, tv_monitor, sofa) and MP3D (21 categories, the Habitat-2020-challenge generation). Unlike the other nav envs here, the goal arrives as a bare noun, not a route description, so the whole task is search under semantic priors.

Sibling of Habitat, not an extension. Both wrap habitat, but they cannot share a process or an interpreter: Habitat pins habitat-sim 0.1.7 + Python 3.8 for VLN-CE, while ObjectNav pins habitat-sim 0.2.4 + habitat-lab 0.2.420230405 + Python 3.9 in its own ac-objnav env. Different simulator generation, different config schema (habitat.* omegaconf vs VLN-CE's TASK_CONFIG.* yacs).


1. Overview

Benchmark configuration is the paper standard, not the challenge

ObjectNav has two live configurations, and they are not comparable. The Habitat Challenge 2023 pins challenge-2023 habitat-lab and a Stretch robot embodiment; the published literature β€” VLFM (23.12), SG-Nav, and the RSS-2026 pair MVP-Nav / OpenFrontier β€” instead uses habitat 0.2.4 with the classic cylindrical agent inherited from Challenge 2022. This nodeset implements the latter, because that is what every number in every comparison table was produced under:

KnobValueSource
habitat-sim / habitat-lab0.2.4 / 0.2.420230405VLFM pyproject.toml; OpenFrontier states "Habitat 0.2.4"
Forward step0.25 mChallenge-2022 agent
Turn angle30Β°Challenge-2022 agent
Sensors640Γ—480 RGB-D + GPS + compassChallenge-2022 agent
Step budget500Challenge-2022 agent
Success radius1.0 m from any goal instanceObjectNav Revisited (20.06)
Episodes (HM3D)objectnav_hm3d_v1 val β€” 2000 episodes, 20 scenes, 6 categoriesVLFM, MVP-Nav (v2 also selectable β€” OpenFrontier's choice)
Episodes (MP3D)objectnav_mp3d_v1 val β€” 2195 episodes, 11 scenes, 21 categoriesHabitat 2020 challenge generation, the ObjectNav Revisited standard; VLFM's MP3D table uses exactly this dataset

One nodeset serves both corpora because habitat-lab 0.2.4's objectnav_mp3d.yaml and objectnav_hm3d.yaml benchmark configs are identical except for data_path (verified by a resolved-config field diff): same agent, sensors, action space, success criterion and budget. Selecting a dataset in the env panel is therefore an episode-corpus choice, not a task change β€” method graphs never see the difference.

Architecture

The three-layer shape shared by every env nodeset:

  1. ObjnavEnvManager (singleton) β€” owns one non-vectorized habitat.Env, a lock, and a single-thread executor pinning all simulator work to one OS thread (GL affinity). Blocking methods only; the nodes hop onto its executor.
  2. Canvas nodes β€” the four gym verbs, thin adapters over the manager.
  3. EnvObjnavNodeSet β€” server_python = conda_env_python("ac-objnav", "OBJNAV_PYTHON"), so the framework auto-hosts the nodeset in its own subprocess under ?mode=server.

Server-mode only in practice: habitat_sim is not installed in the default agentcanvas env. Override the interpreter with $OBJNAV_PYTHON if your env lives elsewhere.


2. Canvas nodes

Seven nodes on the gym contract: reset carries metadata only, step_* returns control signals only, perception is pulled via observe_*, and evaluate is a thin sink for the after-loop band.

Node typeDisplay nameInput portsOutput portsPurpose
env_objnav__reset ObjNav: Reset trigger (ANY, optional) object_category, episode_id, scene_id Ensure a live episode β€” re-arm in place if done, read untouched if live. Never chooses an episode (that is env-panel-owned). No observation ports.
env_objnav__step_discrete ObjNav: Step (discrete) action (ACTION, 0–5) reward, terminated, truncated, info Advance one tick. reward is always 0.0 β€” this is an eval task, habitat computes no shaped reward here.
env_objnav__step_pose ObjNav: Step (pose) target (POSE) reward, terminated, truncated, info Walk toward a world-frame target via ShortestPathFollower with real discrete primitives β€” every step counts toward SPL and the budget. Stops at 0.36 m radius, 50 nav-steps, or episode end; never dispatches STOP (committing is the reasoning side's decision). The frontier-method workhorse: the method picks a point, the env walks there.
env_objnav__observe_egocentric ObjNav: Observe (egocentric) trigger (ANY, optional) rgb, depth, pose, intrinsics, gps, compass Idempotent read of the current frame; never advances the env, never auto-resets. gps/compass are the ObjectNav task sensors (position and heading relative to episode start) that VLFM-style methods integrate into a map.
env_objnav__observe_camera_pose ObjNav: Camera Pose trigger (ANY, optional) position, rotation World-frame position [x,y,z] + orientation quaternion [x,y,z,w] as bare lists β€” for projection nodes that want raw values rather than the POSE bundle.
env_objnav__observe_panorama ObjNav: Observe (panorama) trigger (ANY, optional) views, directions, n_views, composite N-heading panorama rendered off-pose via get_observations_at (pure read, never steps). Two representations on a config switch: views_rgbd β€” aligned RGB + depth (8-bit viewer PNG + 16-bit mm raw) per heading; composite β€” one stitched labelled grid image for single-image VLM prompts. 4–24 views, default 12 (30Β° bins). Expensive: one render per view, pull on demand.
env_objnav__evaluate ObjNav: Evaluate trigger (ANY, optional) metrics, success, spl Pull task metrics on demand (success, spl, soft_spl, distance_to_goal). Independent of done, so it also works mid-episode.

Action space

habitat 0.2.4's ObjectNav defaults, in index order. The order matters: a textParse node configured with these six choices emits an index that is the action, so no mapper node is needed between reasoning and env.

IndexActionEffect
0STOPEnd the episode β€” the only way to score a success
1MOVE_FORWARD0.25 m
2TURN_LEFT30Β°
3TURN_RIGHT30Β°
4LOOK_UPTilt camera up 30Β°
5LOOK_DOWNTilt camera down 30Β°

terminated vs truncated

habitat exposes a single episode_over flag; the manager splits it into the two gym signals by looking at what caused it. An explicit STOP (action 0) is terminated β€” the agent committed, and the metrics reflect its judgment. Running out of the step budget without ever calling STOP is truncated. Terminal metrics ride info["metrics"] in both cases, so a graph that ends on budget still harvests a verdict.


3. Env panel

ObjnavEnvPanel owns episode placement β€” there are no split/episode canvas nodes. Its cascade is three deep:

Field changes emit the episode_reset signal, clearing every lifetime="episode" state container. play re-seats the selected episode before starting the run.


4. Data staging

Scenes come from the official Matterport distribution (login-gated; an API token works headlessly via curl --user token-id:secret). Episodes are public direct downloads from dl.fbaipublicfiles.com β€” with one trap: the MP3D episode zip's official URL spells the corpus m3d (…/objectnav/m3d/v1/objectnav_mp3d_v1.zip); the expected mp3d path returns 403.

PathContents
data/scene_datasets/hm3d/val/100 HM3D v0.2 val scenes (.basis.glb + .basis.navmesh), plus .semantic.glb/.semantic.txt for the 36 annotated ones
data/datasets/objectnav/hm3d/v1/objectnav_hm3d_v1 β€” val (2000 eps / 20 scenes), val_mini (30), train
data/datasets/objectnav/hm3d/v2/objectnav_hm3d_v2 β€” same split shape
data/scene_datasets/mp3dcompat symlink β†’ ../habitat/scene_datasets/mp3d: all 90 habitat-format MP3D scans (.glb + .house + .navmesh + _semantic.ply), long staged for VLN-CE; episodes bake mp3d/{scan}/{scan}.glb
data/datasets/objectnav/mp3d/v1/objectnav_mp3d_v1 β€” val (2195 eps / 11 scenes / 21 categories; sharded per-scene under content/, the outer val.json.gz is an empty shell), val_mini (30), train
v1 and v2 disagree about where scenes live. Every episode bakes a scene_id path, and the two generations use different prefixes: v1 says hm3d/val/…, v2 says hm3d_v0.2/val/…, and v2's val_mini points at hm3d_v0.2/minival/… (its two scenes, 00800 and 00802, are a subset of the val download). Both v2 prefixes are served by compat symlinks into the single real scene tree β€” data/scene_datasets/hm3d_v0.2/val β†’ ../hm3d/val and …/minival β†’ ../hm3d/val. Without them habitat-sim aborts with "Missing (at least) one of scene dataset attributes … Likely an invalid scene name".

Semantic meshes are not required for scoring

Success is judged against goal viewpoints embedded in the episode JSON, not against a runtime semantic lookup β€” verified by driving ShortestPathFollower to the nearest viewpoint and getting SR 1.00 with no semantic files present. The .semantic.glb files are staged anyway because habitat's top_down_map measure reads the semantic scene, and reference implementations (VLFM among them) enable it.


5. Probe graph: objnav_hm3d

workspace/graphs/vln/unverified/objnav_hm3d.json β€” the ObjectNav analog of simple_navigator, and the graph that exercises this nodeset end to end. Every reasoning node ships with the platform; there is no method-specific code anywhere in it.

The loop is: observe β†’ caption the frame β†’ format an observation string β†’ LLM picks one of the six actions β†’ parse β†’ step. The parse node's choice list is written in action-index order (STOP,FORWARD,LEFT,RIGHT,LOOK_UP,LOOK_DOWN), so its index output wires straight into step_discrete.action. step_discrete.terminated drives iter_out.stop; evaluate hangs off iter_out.final_stop in the after-loop band, which is what keeps the terminal STOP step inside the harvested metrics. The graph is also corpus-agnostic β€” nothing in it names a dataset, so the same JSON runs MP3D by submitting with dataset=mp3d_v1.

Scope. This graph exists to prove the wiring, not to compete. Its success rate is not meaningful β€” the reference point on this benchmark is VLFM at SR 52.5 / SPL 30.4 on full val. Treat it the way you would a unit test with a printed number.

6. Verification evidence

HM3D rows 2026-07-20 (slot-b backend); MP3D rows 2026-07-21 (slot-c backend). Single RTX 3090, ac-objnav env.

CheckEvidenceResult
Bare-sim render (pre-nodeset)habitat-sim opens 00800-TEEsavR23oF headlessnavmesh loaded, RGB 480Γ—640 (99% non-zero), depth 0–4.9 m
Dataset shapeobjectnav_hm3d_v1 val via habitat-labexactly 2000 episodes / 20 scenes / 6 categories, goals carry viewpoints
Scoring sanityoracle ShortestPathFollower vs random agent, 5+3 episodesoracle SR 1.00 (SPL 0.90–0.98, stop 0.02–0.08 m from goal); random SR 0.00 β€” metric is not vacuous
Manager surfaceinit / observe / step / STOP / ensure_live / set_episode / episode listall pass; STOP β‡’ terminated=True, truncated=False with metrics attached
Budget exhaustionmax_steps=5, forward-only, never STOPterminated=False, truncated=True, terminal metrics still attached
LOOK_UP / LOOK_DOWNactions 4 and 5 dispatched directlyaccepted, step counter advances
v2 dataset switchswitch_dataset_split("v2", "val_mini")30 episodes load (after the symlink fix in Β§4)
Consecutive-episode sequencing4 placements walking a scene change and a same-scene pair, stepping away between eachevery episode starts at its own start_position (drift 0.0000 m) with fresh counters; a revisited episode reproduces its start pose exactly β€” the 0.1.7 stale-pose bug does not reproduce on 0.2.4
Server modeload?mode=server on the slot backendauto_host subprocess up, 4 tools registered, env panel bridged via RemoteEnvPanelProxy, split change rebuilds val β†’ val_mini
Graph wiringPOST /api/graphs/validate24/24 edges checked, zero wire errors
Graph runrun 20260720_173306 (/experiment:run objnav-hm3d objnav_hm3d, 1 ep, budget 8)completed β€” resetΓ—1 / observeΓ—9 / stepΓ—8 / evaluateΓ—1; metrics harvested into aggregate
Multi-worker batchrun 20260720_205631 β€” val_mini all 30 episodes, worker_count=4, budget 830/30 completed in 931 s (~4Γ— serial); balanced 8/8/7/7 across workers; 30 unique (scene, episode_id) pairs with 30 distinct distance_to_goal values and no same-scene identical-metric pairs β€” no cross-worker state bleed, no stale-pose signature. Measured VRAM ~535 MB/worker (peak 2897 MB incl. 755 MB baseline); host RAM ~3.4 GB/worker
MP3D config parityresolved objectnav_mp3d.yaml vs objectnav_hm3d.yaml field diffidentical except data_path β€” same agent, sensors, actions, success criterion, budget
MP3D dataset shapeobjectnav_mp3d_v1 val via habitat-labexactly 2195 episodes / 11 scenes / 21 categories; val_mini 30; sharded content/ load works
MP3D oracle install checkmetric-blind ShortestPathFollower to episode viewpoints, STOP on planner terminationSR 30/30 val_mini + 33/33 (3 eps Γ— all 11 val scenes), mean SPL 0.92–0.93
MP3D server mode + dataset cascadeload?mode=server on the slot backend, panel field push dataset=mp3d_v1 then split=val_mini7 tools registered; rebuild β†’ 2195 then 30 episodes; options labelled; reset action re-seats and emits episode_reset
HM3D alias regressionswitch_dataset_split("v1", "val_mini")legacy value resolves to hm3d_v1, 30 episodes load

The evaluate count is the load-bearing one. It confirms the after-loop band fires and this graph does not reproduce the harvest miss that affects several gym-migrated graphs (roadmap TODO #64), where a loop-body evaluate never sees the terminal STEP and records success=0.

A sizing trap worth inheriting

The first probe run died at step 10 of 30 with "timeout after 150.0s". ADR-eval-002 caps each episode at step_budget Γ— default_per_step_budget_sec, and the nodeset originally declared 5.0 β€” correct for a bare habitat step, wildly wrong for a graph making two vision-LLM calls per step (~15 s). It is now 30.0, matching Habitat's value and for the same reason. Any env destined for LLM-in-the-loop graphs needs the larger number, and the symptom of getting it wrong looks like an env bug rather than a budget one.

Oracle stop precision on MP3D

MP3D's 2020-era goal viewpoints are sparser than HM3D's, which breaks naive oracle harnesses twice. A follower goal_radius of 0.25 m stops outside the 0.1 m success criterion β€” the first attempt scored SR 0.567 with every failure sitting at distance_to_goal 0.11–0.25. Tightening the radius to 0.1 makes the follower oscillate around it on 0.25 m steps until the budget burns, standing at dtg β‰ˆ 0.04 with no STOP ever dispatched. The working form keeps the stop decision metric-blind: goal_radius = success_distance plus a no-progress rule (net displacement under 0.05 m across a 50-step nav call means the discretization is at its limit β€” commit STOP where standing). HM3D's dense viewpoints mask both traps: the same loose harness scores 1.0 there.


7. Not yet verified

Stated plainly so nobody reads the table above as broader than it is:

AgentCanvas docs