Feat/ivan/pure modules#2469
Draft
leshy wants to merge 125 commits into
Draft
Conversation
…e + health A PureModule declares when it runs (one tick() input), how every other input is sampled at that moment (latest/interpolate/window), and one pure step() bound to inputs by parameter name. The same class runs live on pubsub ports (NullStore-bridged, recording = store choice) or offline over stored memory2 streams via Module.over() — lazy, exact, chainable into other modules. Live ticks flow through a selectable BackpressureBuffer (KeepLast default); every queue in the path is bounded, incl. a max_pending cap so a dead interpolate() input can't accumulate ticks. Health follows "drops are metrics, not errors": per-reason drop counters, a 1 Hz _health stream in the module store, one warmup line comparing observed vs expected input rates, transition-logged DEGRADED/STALLED contract messages with throttled reminders, and strict mode for offline replay determinism.
Executable walkthrough (md-babel) in docs/usage/pure_modules.md: synthetic recording, first module with interpolation proof, sampler language, missing-data policy, explicit state, module chaining, live deployment notes, and a fake-clock health contract demo. Linked from the usage TOC and the puremodule.md reference.
❌ 20 Tests Failed:
View the top 3 failed test(s) by shortest run time
To view more test analytics, go to the Test Analytics Dashboard |
…module MarkerDetectionPureModule sits next to MarkerDetectionStreamModule (untouched): the per-frame TF lookup becomes a declared interpolate(tolerance=0.5) camera_pose input, QualityWindow/SpeedLimit config knobs become upstream stream composition, and the emit_empty_frames sentinel plumbing reduces to step() returning the (possibly empty) Detection3DArray. smoothing_window is deliberately out of scope — it is recurrent state and maps to an explicit Mealy state parameter. Also: PureModule.offline() now returns Self.
Runnable Navigator example in the gentle intro (two Out ports, partial emission, slicing one output back out of the offline dict rows) and a proper Outputs section in the reference covering single/multi/None/ partial semantics, where outputs land per mode, and the dict-row vs per-port asymmetry as a known open point. Also fixes a map_data misuse hiding in a skip block — the callable receives the observation, not data.
docs/usage/pure_modules.md is now the single user-facing document: runnable tutorial followed by a Reference part with the exact rules (declaration/binding, alignment semantics, outputs, over(), deployment knobs, health). dimos/memory2/puremodule.md shrinks to design notes — the why behind ticks/backpressure/health, replay fidelity under drops, the state persistence journal plan, the run-handle plan, and the deferred list. No more duplicated tables; architecture.md links both.
The detached HealthMonitor in the contracts section looked like it was monitoring something; say explicitly that modules construct their own from config at start() (module.health_monitor), and the demo hand-feeds one to show the messages a deployed Follower would produce.
…iter Health contracts gain scale-free forms: max_drop_ratio (the step keeps up, independent of deployment rates), max_missing_ratio (the hardcoded >50% staleness rule made configurable), and max_tick_latency_s over a new end-to-end latency metric — trigger arrival to outputs published, covering queue wait + alignment + buffer + step. Latency is the felt consequence of queue growth under any backpressure policy, so queue depth stays a diagnostic gauge rather than a contract. Ratio contracts evaluate only above ratio_min_samples: tiny windows are noise and zero traffic passes vacuously, so the absolute contracts remain the liveness floor. Multi-output ergonomics: the reserved `out` writer parameter on step — assignment emits, skipping a port stays quiet, unknown ports raise at the assignment line, last write wins. With `out`, stateless steps return None and stateful steps return just new_state (no more (state, dict) tuple). The dict return stays as the low-level equivalent; single-output bare return is unchanged. Outputs is exported for annotation and is the seam for typed Out bundles later — design notes record the agreed bundle/structural-wiring direction, the flat-syntax-as-anonymous-bundle compatibility rule, and the contracts-on-inputs-vs-outputs rationale. Docs: health/contracts section rewritten from the deployment perspective around a captured log transcript; contracts table in the reference; Navigator example switched to the writer form.
Usage doc gains the second captured transcript — a heavy step that keeps the absolute 10 Hz contract while drop-ratio and tick-latency catch it — plus ratio/latency examples in the deployment block and recompute semantics for projected multi-output streams. Design notes: the agreed nested In/Out bundle design (annotation injection, nested-XOR-flat rule, binding by annotation, tick-row synergy, stated trade-offs).
# Conflicts: # docs/usage/index.md
- mypy-exclude puremodule_api_sketch.py and ignore it in blueprint scanning (incl. the name-closure, which its Captioner collision was poisoning with every VL model); exclude PureModule as a framework base - commit pure_modules_dag.svg referenced by docs/usage/pure_modules.md - drop __all__, mark re-exports with the 'as' form, delete dead logging.getLogger in tick.py - regenerate all_blueprints.py (adds marker-detection-pure-module) - scrub stale deployment-override docstrings and doc drift
…ency - stop() drains and joins the tick threads before super().stop() disposes the store; store disposable registered after the input subscriptions so a recording store outlives ingest and the final flush (new test_stop_records_the_tail proves the tail survives a mid-flow stop) - HealthMonitor report path and state/latest properties now hold the lock; sink runs outside it - tick latency measured from trigger arrival (_arrival_ts stamp), so latency contracts stay meaningful when input_sources feed a recording - MarkerDetectionPureModule.camera_info is required; width mismatch warns once instead of per-frame debug; NDArray annotations - drop dead TickMachine.end_of_stream, warn on unresolvable step() hints, complete the spec re-export (Outputs, contract), scrub stale health.py docstrings and change-history narration, test Bounded/DropNew.clone()
# Conflicts: # dimos/robot/all_blueprints.py
Contributor
|
This pull request has been automatically marked as stale because it has not had recent activity. It will be closed in 7 days if no further activity occurs. |
Sketch 3 (+rpc variant) as source of truth for the pure-modules layer, dimos/pure/tasks/index.md with per-task boundaries + status pipeline; mypy exclude widened to cover all design-sketch files.
Sketch 3 (+rpc) is the source of truth; older iterations only confuse planning agents.
Protocols (Stateless/AsyncStateless/Mealy/Fold, positional-only data params), EngineSurface with the over() overload set, m.i/m.o port accessor descriptors, and a mypy-under-pytest harness: 15 marker-driven fixtures + 4 guard tests, one batched mypy run, gate-invisible by layout. Spec in dimos/pure/tasks/t4-typing.md.
Implement PureGraph.blueprint() (§7.2): lower a built GraphPlan onto the existing coordinator via the T8 legacy bridge. Each member deploys through legacy_blueprint (one process); the graph's explicit edges GENERATE the wiring — interior producers path-qualify their topic and consumers join it, name-crossing edges become .remappings entries, rim In/Out stay exposed (bare) to link by the autoconnect (name,type) convention. tf edges ride the shared tf rail. Composes with autoconnect/.namespace/.remappings, no fork. Add TwistUnstamp (§0.4): TwistStamped -> Twist translator so a stamped pure command drives a legacy In[Twist] sink without covariant matching. Tests: NavStack §7.3 structural lowering, combined GO2+NavStack blueprint (name+type link, no conflicts), translator row-level + bridgeability, a scaled-down live graph-beside-legacy coordinator deploy (test_legacy), and a data-gated/CI-skipped GO2 --replay grounding deploy. Regenerate all_blueprints for TwistUnstamp. mypy --strict clean; 538 pure tests pass.
_lower_blueprint generates coordinator wiring from the plan's explicit edges: each member via legacy_blueprint (one worker), rim In/Out exposed bare, interior edges path-qualified, name-crossing edges -> remappings, tf edges ride the tf rail — reusing autoconnect().remappings().namespace(expose=). TwistUnstamp translator (TwistStamped->Twist, §0.4). Grounding: live cross-worker deploy proven (echo graph + legacy pinger, data both directions); full GO2 --replay deploys+wires (5 workers) but end-to-end costmap emit deferred (rim/replay timing, not the lowering). 539 passed, mypy clean.
Planner/NavStack take a bare point goal (goal_point: PointStamped, e.g. a rerun click) instead of a PoseStamped goal; step converts to Vector3 for A*. New dimos/pure/modules/go2_nav.py: GO2Connection + go2 rerun renderer + click server + NavStack graph, autoconnected, runnable live or --replay (clicked_point remaps onto goal_point). example_hk_nav/tests updated. 539 passed, mypy clean.
… bug) The go2_hongkong_office recording carries a Go2-firmware-corrupt lidar scan stamped ~75 days in the future. The aligner accepted it (it passed the monotonic gate as a forward jump), latching the tick port's frontier to the garbage ts; every subsequent correctly-timed scan then failed `ts <= last_ts` and was dropped forever, starving the fold — the pure NavStack /map died mid-run in the multiprocess deployment while delivery kept flowing. Legacy replay was immune (no monotonic gate). Add a forward-outlier gate in Aligner.__next__: a sample jumping more than _FUTURE_TS_OUTLIER_S (3600s) past a finite port frontier is dropped WITHOUT advancing the frontier, with a warning naming the firmware bug. Marked TEMPORARY — remove once recordings/firmware carry sane stamps.
Adds run_pure(): mem2 db -> NavStack.over(...) -> NavRerunSink via .save(), so the recorded lidar/odom drive the graph in-process with no wall clock in the data path — the DAG computes once and map/costmap/path fan into a single pure rerun consumer at native rates (verified ~23x realtime on go2_hongkong_office). --pure needs --replay; --sink chooses grpc/spawn. Deploy mode (coordinator) unchanged.
Switch the graph's mapper to the VoxelMapper2 respelling; trim a device comment.
…criptions Two invisibility bugs that between them hid a dead costmap for hours. drivers/resources used bare logging.getLogger(), which in a forkserver worker resolves to an unconfigured root logger (WARNING, no handler) and drops every line. Switch to setup_logger() — the pattern align.py and pubsubrpc.py already use — so driver errors and resource warmup/teardown failures actually surface, in the CLI and the worker's own jsonl. _connect_streams now warns once, at build, listing consumer streams with no producer in the blueprint. Autoconnect matches by a runtime (name, type) string convention, so a renamed producer silently leaves its consumer subscribed to a dead topic and mypy cannot see it. Non-fatal: viewers legitimately subscribe to optional topics.
…ontrol=True) Two live-path bugs that together killed /costmap and goal clicks in the coordinator deploy, while --pure (in-process) worked fine. 1. A live OPTIONAL secondary parked the whole session. rim's _LiveFeed blocks in __next__ until data or stop, so a port wired to a real-but-silent topic never returned — the tick never fired and the module emitted nothing, with no error. PureCostMapper.merged_map is exactly that: optional, subscribed, never published. Add _LiveFeed.pull_nowait() and use it for optional secondaries: they resolve from newest/default instead of holding. The tick still blocks (that IS the pacing) and REQUIRED secondaries still hold (spec §5). Plain iterators have no pull_nowait, so offline over() keeps its deterministic blocking merge. 2. latest(control=True) for out-of-band control input. A click is stamped with the browser's wall clock; under replay that is ~75 days past the data clock, so it is never the merge minimum, never promoted to newest, and resolves to its default forever — silently. Control ports are re-stamped to the tick frontier on arrival (payload untouched, only the merge key). Planner.goal_point uses it. Measurements are unaffected: the flag is opt-in. Also remap the go2 renderer's global_costmap onto NavStack's costmap export — same type, drifted name, so the viewer was subscribed to a dead topic.
…pure modules A bridged pure module with a REQUIRED pm.tf() port drops every tick silently: legacy.py binds only In bundle fields, and tf is a synthetic stream port, so m.i.tf is never bound, the TfBuffer is never fed, and _fire drops each tick as unresolved. That is why Planner emits no path in the coordinator (its position port is required) while VoxelMapper2 maps fine (its pose port is optional) and --pure works throughout (over() wires odom_tf.tf in-graph). Spec records the diagnosis with line pointers and Ivan's direction: consume TFMessage as an ordinary subscribed stream rather than via the PubSubTF service.
…2_nav --pgo
The map IS the keyframes: body clouds registered at ISAM2-optimized poses,
full grid rebuild + immediate emit per accepted loop closure. Reuses the
offline _PGOState core (new public accessors: n_keyframes/n_loops/
correction/keyframe_world_clouds) and a new VoxelGrid.reset(). Exports
correction (world_corrected <- world_raw) + GraphNodes3D/LineSegments3D
debug viz. NavStackPGO subclass (graph outputs must be wired ports, so a
subclass, not a flag); NavRerunSink moves example_hk_nav.py -> rerunsink.py
(sink only, demo runners deleted). Also: silent-CPU device fix
("GPU:0" -> "CUDA:0"), VoxelMapper2 startup log ungated.
hk office full run: 40 loop closures, 14 scans/s vs ~8 Hz live, final map
0.77/0.91 within 1/2 voxels of the offline two-pass reference.
T15 (debugrec.py): per-module flight recorder + deterministic replay. Decisions/rows/state layers into one mem2 debug.db per run (module-path- dotted streams; validate_identifier relaxed to allow dots, all SQL sites audited); bounded ring on the tick path, drained inline per tick-attempt batch under over() and by the pacer live; reader with summary()/drops()/ replay()/fixture(); pm.Debug/debug_load/debug_latest; `dimos pure debug` CLI. Silent zero-rows runs now warn unconditionally at teardown. T9 (health.py): live-liveness-only Health — wall-stamped row carrying the data-time frontier_ts watermark (lag = ts - frontier_ts is the stall signature; joins on frontier only). HealthCounters is the single aggregation shared with ModuleDebug.summary() (post-hoc == live by construction). Rim-owned HealthPacer absorbs T15's ring drain (interim pacer retired); flat wire row + full-fidelity HealthRecord storage split; legacy HealthPlaceholder swapped for the real type; health_hz on PureModuleConfig. Off under over(): eval perf is a T15 query (step_ms distribution + data-time contract cadence). Aligner robustness (found building PGO): the required-tf hold is bounded at tick_ts + horizon (one pre-tf-stream tick used to drain the ENTIRE tf stream into eviction and silently starve every later tick); tf ingestion gets the ports' >1h future-ts guard (one corrupt stamp latched the frontier forever and expired the edge history). Both now warn. Task docs: t15 (done, incl. field-found drain-cadence + test-isolation fixes), t9 (done), t16 run-dirs design (GO, unimplemented), pure retrospective + scalar-output design notes.
Optional Mealy-only `def finish(self, s: State) -> Out | None`, called exactly once on graceful exhaustion (never on early close, error unwind, or an empty stream), stamped from the last consumed tick's ts — the tail emission fold always had. stepspec classifies + validates it ([finish-not-mealy]/[finish-params]/[finish-state]/[finish-return]...); drive_mealy owns the call, so over(), graph terminal drives, and the live rim all deliver the tail through the normal out-row path (T15 records it in the OUT layer under the last tick's seq; no TickDecision; replay does not re-fire it). VoxelMapper2.finish flushes the trailing partial batch — fold vs Mealy is now FULLY parity (hk slice: both emit the 298 tail, voxel-identical), which also un-sticks test_over_matches_hand_chaining. PGOVoxelMapper .finish emits the final corrected map + viz unless the last cadence tick already did.
…symlink mint_run_dir(label): global counter claimed race-safely via mkdir(exist_ok=False) retry, slugged label, set_run_log_dir routing (main.jsonl + DIMOS_RUN_LOG_DIR for forkserver workers), and an atomically-repointed logs/latest symlink. GlobalConfig.run_dir overrides (absolute / relative-to-LOG_DIR / append-into-existing). Minting from `dimos run` (dir gets the counter name; DIMOS_RUN_ID watchdog tag keeps its own format — never parsed, only environ-matched) and go2_nav main() in both --pure and deploy modes. debugrec's last-resort fallback now mints a run dir instead of a shared debug.db — cross-run stream mixing (same replay ts twice into one stream) is structurally gone; pm.debug_latest() resolves latest -> highest-NNN -> legacy mtime.
… graphs Graph edges deliver a member's nested field payload, and downstream aligners align on the payload's OWN ts — but only the Out row is engine- stamped from the tick. A payload with a wall-clock ts (Planner's Path via min_cost_astar's time.time() default) pinned an off-scale future head and starved the planner to 1 output over a full recording; PGOVoxelMapper's global_map carried the grid's cached snapshot ts, which runs BACKWARDS on a loop-closure rebuild, so the cost mapper dropped exactly the immediate closure refreshes as nonmonotonic. The tee/round-robin distribution was verified faithful — the currency was mis-stamped, not the delivery. _carry_row_ts in _project (one choke point for _RunInstance exports and _TerminalRun.save) stamps payload.ts = row.ts when they differ; rim sources keep true acquisition ts. T13 §0.6 amendment (BINDING). Four regression tests (frozen payload ts, wall-clock latest, burst chain, store recording); hongkong eval: cost mapper 243/243 fired 0 dropped (was 168 fired / 73 nonmonotonic), planner 243 attempts (was 1). Follow-up noted in t13 §0.6: stamp Path/global_map correctly at the producing modules too (belt and suspenders).
"tick dropped: required tf unresolvable" and the future-ts guards now carry bundle= (the In-bundle path, e.g. ...PGOVoxelMapper.In) / owner= (the tf claim owner) — a drop log line was previously unattributable when two modules share a field name like 'pose'.
Contributor
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
Contributor
|
Preview deployment for your docs. Learn more about Mintlify Previews.
💡 Tip: Enable Workflows to automatically generate PRs for you. |
…cone A graph member that ticks without emitting now yields Progress(ts) frontier markers on its interior edges (run_over(progress=True), MEALY/STATELESS). The aligner refill treats a marker as frontier-advance-without-sample and stops pulling a covered latest port, so a silent producer (planner with no reachable goal) no longer drives its upstream cone to exhaustion on the first sink tick — the rerun burst-at-end incident. Markers never cross the rim; save-to-store drains skip them; the module sink is now debug-recorded.
The T4 os.devnull rationale (<1 s cold) went stale once the typing fixtures grew to import the real production graph: the cold batched mypy hit 34.8 s, 54% of the pure suite's wall time. A dedicated cache dir (isolated from the gate's .mypy_cache) brings the harness to ~3 s warm; assertions unchanged.
…save() save()/record= now resolve rim input names (the source stream tees a record branch), so NavRerunSink drives on the raw scan stream: lidar-rate frames, map/costmap/path demoted to change-detected latest logging (each distinct sample logged once — re-presenting the newest map 30x per emission would bloat the recording). village3: 701 scan frames rendered vs 43 before.
…mory2 The v1 PureModule (memory2/puremodule.py + tick.py + health.py) was the earlier sketch of this architecture; dimos/pure supersedes it end to end, including deployment (rim + the legacy_actor bridge). Nothing in dimos/pure ever imported it — only memory2's store/stream, which are untouched. Removed with its dependents: the spec re-export shim, the marker-detection experiment that ran parallel to the stream module, and docs/usage/pure_modules.md (it documents the v1 API — class-attr inputs, Outputs, window() — none of which survive on the pm surface). New-architecture docs land separately. Moved puremodule_api_sketch3_rpc.py to dimos/pure — it is forward design for the un-built RPC wave, so it belongs beside the other pure sketches. memory2 keeps exactly one change, the one the new engine needs: dot-separated stream names, so debugrec can namespace by module path (<path>.raw.<port>). buffer.clone() goes too — only the v1 tick buffer ever called it. all_blueprints loses every dimos.pure.modules entry: those were an artifact of v1's `class PureModule(Module)` seeding the generator's AST closure. The pure modules are not dimos.core Modules (mro: PureModule -> EngineSurface), so they were never deployable through the registry; graph blueprint() lowering is Phase B. Regenerated, so the file now matches main.
The pure engine was opted out of six repo-wide checks. Conform instead of exempting. The pm surface moves from dimos/pure/__init__.py to dimos/pure/pm.py: a plain module, so importing a submodule no longer drags the whole surface in, and the package becomes a namespace package. Callers say `from dimos.pure import pm`; every `pm.X` reference is untouched. Re-exports take the `X as X` form (two renamed ones carry noqa: F401), which is also what lets other modules import names from it. dimos/pure/modules/__init__.py is deleted outright — its lone re-export had no importers; the two consumers already import the submodules directly. __all__ goes from 25 files. Two tests read it as their surface lock: pm's now computes the public namespace (equivalent, since every binding is a re-export), and T4's spells its ten names out. test_costmapper built the legacy CostMapper via __new__ to dodge its ctor. The ctor takes flat config kwargs and works fine — it just spins up transport threads, so the parity helper now disposes it. test_graph's Phase-C stub test used object.__new__(PureGraph) with a "no subclass is definable pre-impl" comment that stale-dated itself; _Fan is right there in the file. `_ = expr` side-effect probes become bare accesses with noqa: B018. Also unstales the NavStack lowering tests: they still expected the member name voxel_mapper, but the graph derives names from the class and NavStack wires VoxelMapper2 — so nav_stack/voxel_mapper2 is what lowering correctly produces. mypy clean; suite 3741 passed. The one remaining failure, test_no_sections, is 290 markers inside the untracked dimos-viewer/ checkout — no tracked file.
11.4k lines of internal working specs across 16 files. They're the drafting
notes the engine was built from, not a deliverable, so they stay out of the PR
and live on untracked in the working tree.
Nothing reads them at runtime — every remaining reference is docstring prose
("Spec: dimos/pure/tasks/t8-rim.md §11"), and no markdown links point in, so
doclinks stays clean. Those pointers now name files the branch doesn't ship;
the text still identifies which spec section a module implements.
puregraph_api_sketch.py and puremodule_api_sketch3_rpc.py are non-functional design drafts — the graph one is superseded by the landed dimos/pure/graph.py, the RPC one is forward design for a wave that isn't built. Both stay untracked in the working tree; nothing imports either. With the RPC sketch gone the blueprint generator needs no special-casing, so both accommodations revert to main: its IGNORED_FILES entry and the IGNORED_FILES filter in _build_module_class_set. Regeneration is unchanged either way — all_blueprints.py does not churn. That leaves one real change in the generator: test-named directories are not production module files, so dimos/pure/test_typing_fixtures/ stops registering its fixture classes.
api_improvements.md (field notes from the PGOVoxelMapper session) and number_problem.md (scalars-to-rerun, implementation not started) are working notes like the task specs and API sketches before them. Untracked, still on disk. The three code references are docstring prose — health.py and stepspec.py cite them for why a design is shaped the way it is — and nothing links to them, so doclinks stays clean.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.