Skip to content

fix: dimos map global and dimos map replay to use tf#2690

Draft
jeff-hykin wants to merge 22 commits into
mainfrom
jeff/fix/map
Draft

fix: dimos map global and dimos map replay to use tf#2690
jeff-hykin wants to merge 22 commits into
mainfrom
jeff/fix/map

Conversation

@jeff-hykin

@jeff-hykin jeff-hykin commented Jul 1, 2026

Copy link
Copy Markdown
Member

Vibed extraction of some of my existing map/db changes. Will check soon.

Examples

Rebuild the global map from a recording (registers clouds into world via the recording's tf tree, renders april tags if an april_tags stream is present):

dimos map global data/china_office.db --lidar pointlio_lidar --voxel 0.05

Plan on the reconstructed map and write a planning .rrd:

uv run python -m dimos.navigation.nav_3d.mls_planner.utils.plan_rrd data/china_office.db --robot-height 0.4 --lidar-stream pointlio_lidar --odom-stream pointlio_odometry

Drive PointCloud/Odometry world-registration off each message's frame_id
instead of a baked-in per-observation pose. World-frame messages render
directly; non-world frames are looked up in the recording's `tf` stream via
a read-only DbTf (no loop closure) and skipped with a warning if unplaceable.

DbTf uses an unbounded time tolerance so once-published static transforms
(mounts, world<-map) latch across the whole recording, while densely sampled
dynamic transforms still resolve to their nearest sample.
@codecov

codecov Bot commented Jul 1, 2026

Copy link
Copy Markdown

❌ 2 Tests Failed:

Tests completed Failed Passed Skipped
2510 2 2508 70
View the full list of 2 ❄️ flaky test(s)
dimos.e2e_tests.test_dimsim_spatial_memory::test_go_to_the_bed

Flake rate in main: 22.22% (Passed 63 times, Failed 18 times)

Stack Traces | 548s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7e16e21a8f40>
human_input = <function human_input.<locals>.send_human_input at 0x7e16e21a8fe0>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7e16e35725d0>
explore_house = <function explore_house.<locals>.explore at 0x7e16e21a94e0>

    @pytest.mark.self_hosted_large
    def test_go_to_the_bed(lcm_spy, start_blueprint, human_input, dim_sim, explore_house) -> None:
        start_blueprint(
            "run",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        lcm_spy.save_topic(".../McpClient/on_system_modules/res")
        lcm_spy.wait_for_saved_topic(".../McpClient/on_system_modules/res", timeout=1200.0)
    
        explore_house()
    
        human_input("go to the bed")
    
>       lcm_spy.wait_until_odom_position(-3.567, -1.332, threshold=2, timeout=180)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7e16e35725d0>
explore_house = <function explore_house.<locals>.explore at 0x7e16e21a94e0>
human_input = <function human_input.<locals>.send_human_input at 0x7e16e21a8fe0>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7e16e21a8f40>

dimos/e2e_tests/test_dimsim_spatial_memory.py:32: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:182: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7e16e21a96c0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>
        threshold  = 2
        timeout    = 180
        x          = -3.567
        y          = -1.332
dimos/e2e_tests/lcm_spy.py:168: in wait_for_message_result
    self.wait_until(
        event      = <threading.Event at 0x7e16e3573bf0: unset>
        fail_message = 'Failed to get to position x=-3.567, y=-1.332'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x7e16e21a9580>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7e16e21a96c0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>
        timeout    = 180
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>

    def wait_until(
        self,
        *,
        condition: Callable[[], bool],
        timeout: float,
        error_message: str,
        poll_interval: float = 0.1,
    ) -> None:
        start_time = time.time()
        while time.time() - start_time < timeout:
            if condition():
                return
            time.sleep(poll_interval)
>       raise TimeoutError(error_message)
E       TimeoutError: Failed to get to position x=-3.567, y=-1.332

condition  = <bound method Event.is_set of <threading.Event at 0x7e16e3573bf0: unset>>
error_message = 'Failed to get to position x=-3.567, y=-1.332'
poll_interval = 0.1
self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e430ee10>
start_time = 1783029667.6155794
timeout    = 180

dimos/e2e_tests/lcm_spy.py:105: TimeoutError
dimos.e2e_tests.test_dimsim_walk_forward::test_walk_forward

Flake rate in main: 27.27% (Passed 56 times, Failed 21 times)

Stack Traces | 205s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7e16e21a9260>
human_input = <function human_input.<locals>.send_human_input at 0x7e16e21a9c60>
dim_sim = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7e16e357cce0>

    @pytest.mark.self_hosted_large
    def test_walk_forward(lcm_spy, start_blueprint, human_input, dim_sim) -> None:
        start_blueprint(
            "run",
            "--disable",
            "spatial-memory",
            "--disable",
            "security-module",
            "unitree-go2-agentic",
            simulator="dimsim",
        )
        lcm_spy.save_topic(".../McpClient/on_system_modules/res")
        lcm_spy.wait_for_saved_topic(".../McpClient/on_system_modules/res", timeout=1200.0)
    
        origin_x, origin_y = 1, 2
        dim_sim.set_agent_position(origin_x, origin_y)
    
        human_input("move forward 3 meter")
    
>       lcm_spy.wait_until_odom_position(origin_x + 3, origin_y, threshold=0.4, timeout=120)

dim_sim    = <dimos.e2e_tests.dim_sim_client.DimSimClient object at 0x7e16e357cce0>
human_input = <function human_input.<locals>.send_human_input at 0x7e16e21a9c60>
lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>
origin_x   = 1
origin_y   = 2
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x7e16e21a9260>

dimos/e2e_tests/test_dimsim_walk_forward.py:37: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/lcm_spy.py:182: in wait_until_odom_position
    self.wait_for_message_result(
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7e16e21a91c0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>
        threshold  = 0.4
        timeout    = 120
        x          = 4
        y          = 2
dimos/e2e_tests/lcm_spy.py:168: in wait_for_message_result
    self.wait_until(
        event      = <threading.Event at 0x7e16e357d460: unset>
        fail_message = 'Failed to get to position x=4, y=2'
        listener   = <function LcmSpy.wait_for_message_result.<locals>.listener at 0x7e16e21aa0c0>
        predicate  = <function LcmSpy.wait_until_odom_position.<locals>.predicate at 0x7e16e21a91c0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>
        timeout    = 120
        topic      = '/odom#geometry_msgs.PoseStamped'
        type       = <class 'dimos.msgs.geometry_msgs.PoseStamped.PoseStamped'>
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

self = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>

    def wait_until(
        self,
        *,
        condition: Callable[[], bool],
        timeout: float,
        error_message: str,
        poll_interval: float = 0.1,
    ) -> None:
        start_time = time.time()
        while time.time() - start_time < timeout:
            if condition():
                return
            time.sleep(poll_interval)
>       raise TimeoutError(error_message)
E       TimeoutError: Failed to get to position x=4, y=2

condition  = <bound method Event.is_set of <threading.Event at 0x7e16e357d460: unset>>
error_message = 'Failed to get to position x=4, y=2'
poll_interval = 0.1
self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x7e16e357c980>
start_time = 1783029933.2490127
timeout    = 120

dimos/e2e_tests/lcm_spy.py:105: TimeoutError

To view more test analytics, go to the Test Analytics Dashboard
📋 Got 3 mins? Take this short survey to help us improve Test Analytics.

…er loop)

Regenerated pointlio_lidar + pointlio_odometry via pcap_to_db --rate 2.0 on
china_office.pcap, timestamps re-anchored to the recording epoch and tf tree
rebuilt (static mounts + odom->base_link). Reproduces the known-good __rate2
trajectory; 2x packs denser scans per Point-LIO wall-clock accumulation window
than 1x, yielding a tighter loop closure.
Recordings can reference Python classes not present in the current
checkout. summary/replay now warn and skip those streams instead of
crashing; requesting an unresolvable stream by name still raises.
Remove the 4 ground-truth pointlio streams (all worse than the 1.5x
replay), copy the six surf*/map* config runs' odometry from the grid
sweep, and align every pointlio_odometry* stream (including the __rate*
variants) to the canonical recording anchor so they overlay the tf tree
and lidar.
pointlio_odometry, tf, and pointlio_lidar are pulled from the 1x
regen (unfrozen odom, correctly-recorded 5-edge tf tree) and shifted
onto the June-12 data clock so they align with the kept color_image /
gtsam_odom / raw_april_tags streams. Intermediate checkpoint before PGO.
jnav_pgo added gt_pointlio_odometry, gt_pointlio_lidar, and the exotic-payload
streams (gt_tf_deformation_nodes, pose_graph, tf_graph). The map/replay CLI
registers clouds into the `world` frame, but the regen tf tree was rooted at
`map`, so every lidar frame was dropped (empty global map). Inject a static
`world <- map` identity transform so the full chain resolves. Validated:
map summary/global/replay all succeed, global reconstructs 94/94 frames.
test_sqlite_summary_skips_unresolvable_types opens a SqliteStore, which loads
the sqlite_vec extension. sqlite_vec has no working wheel on macOS or Linux ARM
(the aarch64 runner loads a wrong-arch vec0.so → ELFCLASS32), so mark it with
the same skipif_macos/skipif_aarch64 markers the other sqlite_vec tests use.
try:
stream: Stream[Any] = self.stream(name)
except (ImportError, AttributeError, ValueError) as error:
logger.warning(f"skipping stream {name!r}: unresolved payload type ({error})")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

why do we have unreslovable type in the database now?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

If people save custom types (ex: Graph3D on my gsc_pgo branch) it would be nice for the map to still load the db and ignore those unknown types

@jeff-hykin jeff-hykin changed the title fix: register map/replay clouds via frame_id + TF tree fix: dimos map global and dimos map replay to use tf Jul 2, 2026
Point-LIO runs with extrinsic_r=identity, so its odom->body pose (and the
mid360_link-stamped clouds) are in the sensor's own frame. The recorded
body->base_link identity edge made the tf chain apply the 44 degree mount
pitch a second time, smearing `dimos map global` whenever the robot rotated.
Rewrote body->base_link to (base_link->mid360_link)^-1 so the chain cancels:
world<-mid360_link resolves to the sensor pose and base_link becomes the
true trunk pose.
Point-LIO runs with identity extrinsics, so its odometry child IS the
mid360 sensor frame. Name it honestly (odom->mid360_link) and hang the
robot off it via a derived inverse-mount static
(mid360_link->base_link), giving world->map->odom->mid360_link->
base_link->front_camera->camera_optical. Relabel china_office.db to
match (pure renames, no pose changes).
SqliteStore.stream() silently registers a typo'd name as a new empty
stream, so a wrong --lidar-stream/--odom-stream replayed zero frames
with no error (only the static goal point rendered).
Drop the frame_id="world" override so recordings carry the same moving
edge (odom -> mid360_link) as existing datasets; world/map identity
edges are a localization concern, not the recorder's.
Publish identity world -> map -> odom edges from the static tf so a
fresh recording matches existing datasets (world-rooted tree with
pointlio's odom -> mid360_link as the moving edge).
The puck is mounted sideways: pitched down 44 deg AND rotated -90 deg
about its own z. With pitch-only compensation base_link sat 61.7 deg
off level (measured against gravity in the china_office recording);
with the composed rotation it is level to ~4 deg.
- delete pose_graph, gtsam_odom, and the empty lidar stream
- rename gt_tf_deformation_nodes -> tf_deformation_nodes
- rename raw_april_tags -> april_tags_raw
- replace april_tags with the denser april_tags_dense detections
- stamp april_tags / april_tags_raw with frame_id=camera_optical so
  they resolve through the tf tree into world
Tag poses are placed into world via the recording's tf tree (frame_id
lookup through WorldRegistrar), colored per marker_id, and drawn with a
short pin and visible label.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants