Skip to content

Fan-io#2364

Open
bogwi wants to merge 22 commits into
mainfrom
danvi/feat/fan-io
Open

Fan-io#2364
bogwi wants to merge 22 commits into
mainfrom
danvi/feat/fan-io

Conversation

@bogwi
Copy link
Copy Markdown
Collaborator

@bogwi bogwi commented Jun 5, 2026

Fan-io

Problem

StreamModule only supports 1 input and 1 output.

Solution

  • We want to support multiple inputs (which can be aligned on time).
  • And multiple outputs emitted at once.
  • The first declared In regulates all emission times. The pipeline must construct a Bundle which is used to publish messages to multiple outputs.

Proof the platform works

  • memory2/test_module.py fusion tests (literal N:M)
  • MarkerModule migration (clearest before/after)
  • Detection3DModule migration (Rx -> pipeline, compute-once multi-out)

Out of scope

module-as-transform in arbitrary outer chains (will be new mem2-native-modules PR).

What this branch is (the core deliverable)

Piece File What it adds
Generalized StreamModule memory2/module.py N in / M out; ingest() seam; self.streams.<port>; no 1:1 gate
Fan-out primitives memory2/fanio.py Bundle, scatter_to_ports, normalize_to_bundle
Cross-stream pairing memory2/stream.py Stream.align, optional interpolator=
Stock interpolators memory2/interpolators.py lerp_pose, interp_odom
Contract tests memory2/test_module.py TwoInputFusion, ChainedFusion, multi-out compute-once, interpolation

Why the design

Before fan-I/O:

  • MarkerModule copied 24 lines of start() for one ingestion change
  • Detection3DModule was not a mem2 pipeline at all - Rx alignment in start()
  • Multi-out meant manual subscribe/publish or duplicate compute
  • Every new fusion module picked its own private wiring idiom

After fan-I/O:

  • Port count is data: declare N in, M out
  • One authoring model: align -> transform -> Bundle ingest() for ingress enrichment without copying start()
  • One subscribe, one compute, scatter to all outs
  • Migrated modules share the same mental model as offline/replay streams

Summary

Fan-I/O is the wrapper layer: mem2 fusion inside deployed StreamModules. That works today - and an important step toward modules that are just mem2 transforms: the same pipeline() you deploy on a robot should run in any mem2 chain, on a recorded SqliteStore, faster than realtime.

What survives from fan-I/O into that follow-up will be the core - every input as a mem2 stream, .align() / interpolator=, Bundle scatter, ingest() - but not the legacy port/transport wrapper around it.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Jun 5, 2026

Greptile Summary

This PR introduces a fan-I/O platform layer for StreamModule that lifts the previous 1:1 port constraint to N inputs / M outputs, backed by Bundle/scatter_to_ports in fanio.py, a two-pointer Stream.align with an optional interpolator= hook for synthesis at the exact primary timestamp, and stock slerp-based interpolators for PoseStamped and Odometry. The change ships with two concrete migrations: MarkerModule (replaces a copied start() with the ingest() seam and emits dual 2D/3D outputs in one pass) and Detection3DModule (replaces Rx align_timestamped with Stream.align and scatters eight ports from a single Bundle).

  • Platform: fanio.py adds Bundle, normalize_to_bundle, and scatter_to_ports; stream.py extends .align() with interpolator= and refactors the two-pointer inner loop into two named helpers; module.py generalises StreamModule.start() to wire every In port and feed all Out ports through one subscribe.
  • Migrations: MarkerModule overrides ingest() to anchor each frame with TF before the pipeline and emits detections_3d/detections_2d from a single MarkersToBundle transform; Detection3DModule uses self.streams.pointcloud.align(...) to replace the old Rx coupling, emitting up to eight ports from one FusedDetections bundle per tick.
  • Collateral: MarkerTfModule.detections port is renamed to detections_3d, moduleDB.py/ObjectDBModule are removed, and Detection2DArray.to_rerun() is added for image-plane overlays in Rerun.

Confidence Score: 5/5

Safe to merge; the fan-I/O platform and both module migrations are correctly implemented with no functional regressions found.

The two-pointer alignment loop, Bundle scatter, normalize_to_bundle bridge, and ingest() seam all behave correctly. The _fuse field-name access in Detection3DModule aligns with stream names derived from In port names. The _nearest_secondary and _interpolated_secondary helpers preserve the old tolerance boundary and tie-breaking. The MarkerTfModule port rename is updated in every consumer and integration test. moduleDB.py is fully removed with no dangling references.

No files require special attention.

Important Files Changed

Filename Overview
dimos/memory2/fanio.py New file: Bundle dataclass + normalize_to_bundle + scatter_to_ports implement the M-agnostic fan-out contract; logic is correct and well-documented.
dimos/memory2/module.py StreamModule.start() generalised to N-in / M-out; _LiveInputs, ingest() seam, and _apply_pipeline are well-structured; order of _in_streams assignment before _apply_pipeline call is correct.
dimos/memory2/stream.py align() extended with optional interpolator=; two-pointer loop refactored into _nearest_secondary / _interpolated_secondary helpers; tolerance boundary and tie-breaking are equivalent to the previous implementation.
dimos/memory2/interpolators.py New file: lerp_pose and interp_odom; _slerp handles hemisphere alignment and near-parallel fallback to nlerp correctly.
dimos/perception/detection/module3D.py Migrated from Rx align_timestamped to Stream.align; _fuse accesses pair fields correctly given stream names; _to_bundle scatters up to 8 ports from one FusedDetections per tick.
dimos/perception/fiducial/marker_module.py Replaces copied start() with ingest() override for TF pose anchoring; dual 2D/3D output via MarkersToBundle in one pipeline pass.
dimos/perception/fiducial/marker_transformer.py New MarkersToBundle transformer collapses per-marker fan-out into one Bundle per frame; early-flush and end-of-stream flush are handled correctly.
dimos/perception/fiducial/marker_tf_module.py In port renamed from detections to detections_3d; all consumers and integration tests updated.
dimos/perception/detection/moduleDB.py Deleted; ObjectDBModule removed from all_blueprints.py; no remaining references in the codebase.
dimos/msgs/vision_msgs/Detection2DArray.py to_rerun() added converting detections to rr.Boxes2D in XYWH format; empty array clears stale overlays.
dimos/memory2/test_module.py Comprehensive grid tests covering all three pipeline styles plus TwoInputFusion, ChainedFusion, multi-out compute-once, and interpolation cases.

Sequence Diagram

sequenceDiagram
    participant In_primary as In[primary]
    participant In_secondary as In[secondary]
    participant NullStore as NullStore
    participant ingest as ingest() seam
    participant pipeline as pipeline()
    participant align as Stream.align()
    participant normalize as normalize_to_bundle()
    participant scatter as scatter_to_ports()
    participant Out_A as Out[port_A]
    participant Out_B as Out[port_B]

    Note over In_primary,In_secondary: StreamModule.start()

    In_primary->>ingest: msg arrives
    ingest->>NullStore: "stream.append(msg, ts=...)"
    In_secondary->>ingest: msg arrives
    ingest->>NullStore: "stream.append(msg, ts=...)"

    NullStore->>pipeline: primary.live() stream
    pipeline->>align: "primary.transform(...).align(self.streams.secondary, tolerance=T)"
    align-->>pipeline: Stream[AlignedPair]
    pipeline-->>normalize: Stream[Bundle] (or raw T for 1:1)

    normalize-->>scatter: Stream[Bundle] (always bundle-keyed)

    scatter->>Out_A: bundle[port_A] publish
    scatter->>Out_B: bundle[port_B] publish
Loading

Reviews (5): Last reviewed commit: "rename duplicate test class" | Re-trigger Greptile

Comment thread dimos/memory2/module.py
Comment thread dimos/memory2/fanio.py
Comment thread dimos/robot/unitree/go2/blueprints/smart/unitree_go2_detection.py Outdated
Comment thread dimos/perception/detection/moduleDB.py Outdated
Comment thread dimos/perception/detection/module3D.py
@bogwi bogwi added the PlzReview label Jun 5, 2026
Comment thread dimos/perception/detection/module3D.py
Comment thread dimos/perception/detection/module3D.py
Comment thread dimos/memory2/fanio.py
Comment thread dimos/memory2/fanio.py
Comment thread dimos/perception/detection/test_module3D.py
Comment thread dimos/perception/detection/type/detection2d/bbox.py
Comment thread dimos/perception/fiducial/marker_transformer.py
Comment thread dimos/perception/detection/module3D.py
pointcloud_topic.publish(detection.pointcloud)


def deploy( # type: ignore[no-untyped-def]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@leshy Are these functions still used?

Comment thread dimos/perception/fiducial/marker_module.py
Comment thread dimos/perception/fiducial/marker_transformer.py
Comment thread dimos/memory2/test_module.py
Comment thread dimos/memory2/test_module.py
Comment thread dimos/memory2/test_module.py
Comment thread dimos/memory2/test_stream.py
@bogwi
Copy link
Copy Markdown
Collaborator Author

bogwi commented Jun 6, 2026

@paul-nechifor , when I mark the conversation resolved, it means I am resolving/resolved them in the same very moment I mark them. Once stable, a commit(s) with fixes will land on top.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants