Skip to content
Draft
Show file tree
Hide file tree
Changes from 55 commits
Commits
Show all changes
146 commits
Select commit Hold shift + click to select a range
a7a9be9
pgo: publish pose graph (nodes + odom/loop edges) over LCM + top-down…
jeff-hykin May 12, 2026
ba0e17e
pgo: lift pgo_graph_{nodes,edges} via agentic_debug visual override
jeff-hykin May 12, 2026
1801759
pgo: drop second rerun pane; revert to single 3D view
jeff-hykin May 12, 2026
82d7799
pgo: publish per-keyframe delta on every loop-closure event + slow test
jeff-hykin May 12, 2026
3c90a7f
pgo: scan-context-based loop-closure search
jeff-hykin May 12, 2026
3e33003
pgo: synthetic drift test — proves Scan Context catches loops positio…
jeff-hykin May 13, 2026
5f64131
pgo tests: replace _log() print wrappers with the dimos logger
jeff-hykin May 14, 2026
6880ab6
pgo: expand short variable names introduced on this branch
jeff-hykin May 14, 2026
c7fd631
pgo: KITTI-360 benchmark scaffolding (loader + groundtruth + runner)
jeff-hykin May 15, 2026
e45b228
merge main into jeff/feat/better_pgo
jeff-hykin May 15, 2026
1cf0045
make model_path optional
jeff-hykin May 15, 2026
c9a426a
add flowbase robot
jeff-hykin May 15, 2026
272833c
add alfred
jeff-hykin May 15, 2026
0863ad1
add blueprint
jeff-hykin May 15, 2026
9020415
naming
jeff-hykin May 15, 2026
6ea0251
name fix
jeff-hykin May 15, 2026
8e9c445
mypy
jeff-hykin May 15, 2026
277cd29
mypy
jeff-hykin May 15, 2026
f6010b0
-
jeff-hykin May 15, 2026
4f43322
-
jeff-hykin May 15, 2026
c0f6bb9
fixup vis_throttle
jeff-hykin May 16, 2026
45f57a5
-
jeff-hykin May 16, 2026
d1d484f
Merge branch 'jeff/feat/better_pgo' of github.com:dimensionalOS/dimos…
jeff-hykin May 16, 2026
726808e
add recording topics
jeff-hykin May 16, 2026
84bfe56
-
jeff-hykin May 16, 2026
36a7454
-
jeff-hykin May 16, 2026
fe3c43d
Merge branch 'jeff/clean/nav2' into jeff/clean/nav3
jeff-hykin May 16, 2026
8fe1f54
-
jeff-hykin May 16, 2026
d780dac
cleanup
jeff-hykin May 16, 2026
559a0c1
Merge branch 'jeff/clean/nav2' into jeff/clean/nav3
jeff-hykin May 16, 2026
4480966
Merge branch 'main' into jeff/feat/better_pgo
jeff-hykin May 16, 2026
5b828e2
fix: mypy errors in pgo benchmark and demo viz
jeff-hykin May 16, 2026
59b751c
fix: drop dead positive-yaw wrap in yaw_from_shift
jeff-hykin May 16, 2026
4329aac
fix: drop empty pgo benchmark __init__.py
jeff-hykin May 16, 2026
2d77b76
Merge branch 'main' into jeff/feat/flowbase
jeff-hykin May 16, 2026
ac17686
-
jeff-hykin May 16, 2026
1d76b35
Merge branch 'jeff/feat/flowbase' of github.com:dimensionalOS/dimos i…
jeff-hykin May 16, 2026
98a79a5
-
jeff-hykin May 16, 2026
b0fb1e1
Merge branch 'jeff/feat/flowbase' into jeff/clean/nav3
jeff-hykin May 16, 2026
a88f712
Merge branch 'main' into jeff/feat/better_pgo
jeff-hykin May 16, 2026
67f2345
transform frames fix
jeff-hykin May 16, 2026
3eba77b
add missing encoding
jeff-hykin May 16, 2026
b28cd00
important PGO optimizations
jeff-hykin May 16, 2026
a0fafbb
fix: annotate pgo smoke_test lcm callback to satisfy mypy
jeff-hykin May 16, 2026
4479d8d
fix bug in cmu stack
jeff-hykin May 16, 2026
4c412e7
transform frame fixups
jeff-hykin May 16, 2026
0ffa39a
add additional check
jeff-hykin May 16, 2026
4a27dd8
Merge branch 'jeff/feat/better_pgo' of github.com:dimensionalOS/dimos…
jeff-hykin May 16, 2026
6901d71
fix: ignore sklearn import for mypy (no stubs available)
jeff-hykin May 16, 2026
037f0c7
-
jeff-hykin May 16, 2026
ca977fa
-
jeff-hykin May 16, 2026
1c34ab9
nits + greptile fixes on PGO benchmark + reverse-loop test
jeff-hykin May 16, 2026
80adfb6
Merge branch 'main' into jeff/feat/better_pgo
jeff-hykin May 16, 2026
4a0de4d
nits: rename msg/msg_cls/msg_type → message/message_class/message_type
jeff-hykin May 16, 2026
6de903a
nits: lift scipy import to module top in run_kitti360_benchmark
jeff-hykin May 16, 2026
fbd00dc
nits: C++ rename mod→native_module, cp→cloud_with_pose, fix tresh typo
jeff-hykin May 16, 2026
e9f8356
generic-ize PGO output topic names
jeff-hykin May 16, 2026
5cc294b
-
jeff-hykin May 16, 2026
5fe55c8
WIP: move KITTI-360 benchmark out of pgo, refactor into Modules + Blu…
jeff-hykin May 16, 2026
44f41f9
fix: annotate runner.get_results local to satisfy mypy
jeff-hykin May 16, 2026
3015232
flatten pgo/benchmark/ — drop the dir, files live at pgo/
jeff-hykin May 16, 2026
c1308e2
fix: regenerate all_blueprints.py for new pose_graph_kitti360 modules
jeff-hykin May 16, 2026
ff2ccb1
rename
jeff-hykin May 16, 2026
8afcf97
nits: expand fid/src/dst/tp/fp/fn/p/r in loop_groundtruth.py
jeff-hykin May 16, 2026
91cbdca
benchmark_kitti_smoke_test: rewrite on top of Modules + Blueprint
jeff-hykin May 16, 2026
b07f9ac
test_pgo_synthetic_drift: rewrite via Modules + Blueprint
jeff-hykin May 16, 2026
7f12682
cleaning
jeff-hykin May 16, 2026
73669bc
benchmark_place_recognition: humanise top-of-file docstring
jeff-hykin May 16, 2026
999a961
-
jeff-hykin May 16, 2026
d1d3356
fix: simulation arg (#2103)
paul-nechifor May 16, 2026
1b045e9
fix: ignore sklearn import for mypy (no stubs available)
jeff-hykin May 16, 2026
caaad9e
fix: drop ASCII section markers from test_pgo_synthetic_drift
jeff-hykin May 16, 2026
5bcf724
test_pgo_loop_closure: rewrite via Modules + Blueprint
jeff-hykin May 16, 2026
410b7c9
fix: drop ASCII section markers from test_pgo_loop_closure
jeff-hykin May 16, 2026
2bdb6e1
test_pgo_rosbag + rosbag_fixtures: drop hardcoded LCM topics, use Mod…
jeff-hykin May 16, 2026
0a9b92d
remove empty configs + inline _message_to_dict + pydantic Field
jeff-hykin May 16, 2026
101fa7b
Merge origin/jeff/feat/better_pgo (PGO improvements + KITTI360 benchm…
jeff-hykin May 16, 2026
3ce2811
-
jeff-hykin May 16, 2026
2ae7f18
move HACK comment from PGO.start() to _STARTUP_SETTLE_SEC
jeff-hykin May 16, 2026
0227c1a
Merge remote-tracking branch 'origin/main' into jeff/clean/nav3
jeff-hykin May 16, 2026
17a0d67
Merge branch 'jeff/clean/nav0' of github.com:dimensionalOS/dimos into…
jeff-hykin May 16, 2026
f4512c2
-
jeff-hykin May 16, 2026
74e2c43
fix: cover playback-vs-PGO-startup race + section markers + regen all…
jeff-hykin May 16, 2026
936c33d
fix: drop duplicate `bool debug` in pgo/main.cpp + section markers in…
jeff-hykin May 16, 2026
f4a8e2a
fix: stop the host-side RPC client even when proxy.stop() raises
jeff-hykin May 16, 2026
bcc422d
test: mark PGO synthetic_drift + loop_closure as self_hosted
jeff-hykin May 16, 2026
3eb80b8
test: add skipif_no_nix and apply it to PGO C++-binary tests
jeff-hykin May 16, 2026
ed64f25
fix: query-level loop scoring + surface playback errors
jeff-hykin May 16, 2026
158f3eb
fix: RosbagScanOdomPlaybackModule surfaces playback errors
jeff-hykin May 16, 2026
6dccb2d
fix: SyntheticDriftPlaybackModule surfaces playback errors
jeff-hykin May 16, 2026
c01756d
fix: query-level FP in scoring (TP + FP now dimensionally consistent)
jeff-hykin May 16, 2026
8d3d768
Update dimos/navigation/nav_stack/main.py
jeff-hykin May 16, 2026
064af1e
fix: move _maybe_build to NativeModule.build() — fixes PGO startup race
jeff-hykin May 16, 2026
abc78e4
fix: address greptile review on nav_stack/main.py max_hz block
jeff-hykin May 16, 2026
6e236e7
feat: subprocess-ready handshake — native modules signal when they're…
jeff-hykin May 16, 2026
ae6b59a
feat: ready handshake for all C++ native modules
jeff-hykin May 16, 2026
e376930
Merge remote-tracking branch 'origin/main' into jeff/feat/better_pgo
jeff-hykin May 16, 2026
eff5254
test: rename benchmark_kitti_smoke_test.py -> demo_benchmark_kitti_sm…
jeff-hykin May 16, 2026
9e42096
fix: regenerate all_blueprints.py for TopicCounterModule
jeff-hykin May 16, 2026
9af58da
revert
jeff-hykin May 16, 2026
c7dc341
simplify
jeff-hykin May 16, 2026
d45adb2
-
jeff-hykin May 16, 2026
5d95b34
nav: add LoopClosure spec and use it in run_benchmark
jeff-hykin May 16, 2026
026adba
docs
jeff-hykin May 16, 2026
beba4dd
-
jeff-hykin May 16, 2026
e0fecd7
pgo: ready handshake — block start() until C++ binary is subscribed
jeff-hykin May 17, 2026
9128a70
Merge branch 'jeff/clean/nav0' into jeff/clean/nav3
jeff-hykin May 17, 2026
be7412b
Revert "pgo: ready handshake — block start() until C++ binary is subs…
jeff-hykin May 17, 2026
90e062a
make test reliable
jeff-hykin May 17, 2026
5e1e225
fixup start
jeff-hykin May 17, 2026
1584fe6
-
jeff-hykin May 17, 2026
a7af8ac
remove flakey test
jeff-hykin May 17, 2026
5697835
fixup on macos
jeff-hykin May 17, 2026
547205b
fixup LoopClosure spec
jeff-hykin May 17, 2026
ab133a0
-
jeff-hykin May 17, 2026
df16d46
fix timeout
jeff-hykin May 17, 2026
c138f66
Merge remote-tracking branch 'origin/main' into jeff/feat/better_pgo
jeff-hykin May 17, 2026
b9df4fa
Merge remote-tracking branch 'origin' into jeff/clean/nav3
jeff-hykin May 17, 2026
29dfafe
-
jeff-hykin May 17, 2026
cf6772e
Merge remote-tracking branch 'origin/main' into HEAD
jeff-hykin May 17, 2026
5769d48
Merge remote-tracking branch 'origin/main' into HEAD
jeff-hykin May 17, 2026
124c4e3
build native modules in the build step
jeff-hykin May 17, 2026
c152325
feat(msgs): GraphNodes3D Kaitai Struct schema
jeff-hykin May 17, 2026
04d73a3
feat(msgs): expose LineSegments3D public attrs + per-segment timestamps
jeff-hykin May 17, 2026
3564a77
feat(msgs): C++ LineSegments3D typed publisher header
jeff-hykin May 17, 2026
75b4e8d
feat(msgs): Graph3D pose-graph message type
jeff-hykin May 17, 2026
9ead549
refactor(far_planner): switch graph_nodes+graph_edges to graph: Out[G…
jeff-hykin May 17, 2026
6185f62
refactor(pgo): pose_graph_nodes+edges → pose_graph: Out[Graph3D]; dro…
jeff-hykin May 17, 2026
f97e1bd
chore(pgo): rename demo_benchmark_kitti_smoke → benchmark_kitti360_smoke
jeff-hykin May 17, 2026
be9f2ca
remove .ksy
jeff-hykin May 17, 2026
edb0584
test(pgo): restore filename test_pgo_loop_closure.py
jeff-hykin May 17, 2026
7311034
Merge remote-tracking branch 'origin/main' into jeff/feat/better_pgo
jeff-hykin May 17, 2026
bf3c570
Merge branch 'jeff/feat/better_pgo' of github.com:dimensionalOS/dimos…
jeff-hykin May 17, 2026
1c59f23
chore(blueprints): regenerate all_blueprints.py for smoke-test rename
jeff-hykin May 17, 2026
6c87dcc
misc
jeff-hykin May 17, 2026
e4dd71a
feat(msgs): GraphDelta3D + wire PGO's loop_closure_event to it
jeff-hykin May 17, 2026
446fa16
clean comments
jeff-hykin May 17, 2026
4c6fd58
Merge branch 'jeff/feat/better_pgo' into jeff/clean/nav3
jeff-hykin May 17, 2026
444b0ad
Merge branch 'jeff/feat/better_pgo' into jeff/clean/nav3
jeff-hykin May 17, 2026
5a0def2
fix nits: drop self-evident comments
jeff-hykin May 17, 2026
29564c6
fix(rerun): auto-dispatch via to_rerun_multi when message exposes it
jeff-hykin May 17, 2026
e0002e4
clean
jeff-hykin May 17, 2026
8c2fe3d
comments
jeff-hykin May 21, 2026
02cb6a4
Revert "fix(rerun): auto-dispatch via to_rerun_multi when message exp…
jeff-hykin May 21, 2026
d2b67f2
add todo
jeff-hykin May 21, 2026
bbf481d
merge main
jeff-hykin May 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions bin/ci-check
7 changes: 5 additions & 2 deletions dimos/core/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,11 +247,14 @@ def __setstate__(self, state) -> None: # type: ignore[no-untyped-def]
self._tools = {}
self._tools_lock = threading.Lock()

_tf_lock: threading.Lock = threading.Lock()

@property
def tf(self): # type: ignore[no-untyped-def]
if self._tf is None:
# self._tf = self.config.tf_transport()
self._tf = LCMTF()
with self._tf_lock:
if self._tf is None:
self._tf = LCMTF()
return self._tf
Comment on lines +262 to 270
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.

P2 _tf_lock is a class-level attribute shared by every ModuleBase instance

Because _tf_lock is declared directly in the class body, all instances share the exact same threading.Lock object. When dozens of modules initialize concurrently at startup, they all serialize on this single lock even though their _tf fields are entirely independent. Moving initialization to __init__ (e.g., self._tf_lock = threading.Lock()) gives each instance its own lock and eliminates the cross-instance contention.


@tf.setter
Expand Down
32 changes: 23 additions & 9 deletions dimos/hardware/sensors/lidar/fastlio2/module.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,6 @@
from dimos.msgs.geometry_msgs.Vector3 import Vector3
from dimos.msgs.nav_msgs.Odometry import Odometry
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
from dimos.navigation.nav_stack.frames import FRAME_BODY, FRAME_ODOM
from dimos.spec import mapping, perception
from dimos.utils.generic import get_local_ips
from dimos.utils.logging_config import setup_logger
Expand All @@ -83,11 +82,9 @@ class FastLio2Config(NativeModuleConfig):
# Converted to init_pose CLI arg [x, y, z, qx, qy, qz, qw] in model_post_init.
mount: Pose = Pose()

# Frame IDs for output messages. "odom" reflects that FastLio2 provides
# locally-smooth, continuous odometry (no loop-closure jumps). PGO
# publishes the map→odom correction via TF.
frame_id: str = FRAME_ODOM
child_frame_id: str = FRAME_BODY
frame_id: str = "start_point"
child_frame_id: str = "current_point"
sensor_frame: str = "mid360_link"

# FAST-LIO internal processing rates
msr_freq: float = 50.0
Expand Down Expand Up @@ -169,10 +166,11 @@ def start(self) -> None:
)

def _on_odom_for_tf(self, msg: Odometry) -> None:
ts = msg.ts or time.time()
self.tf.publish(
Transform(
frame_id=FRAME_ODOM,
child_frame_id=FRAME_BODY,
frame_id=self.config.frame_id,
child_frame_id=self.config.child_frame_id,
translation=Vector3(
msg.pose.position.x,
msg.pose.position.y,
Expand All @@ -184,7 +182,23 @@ def _on_odom_for_tf(self, msg: Odometry) -> None:
msg.pose.orientation.z,
msg.pose.orientation.w,
),
ts=msg.ts or time.time(),
ts=ts,
)
)
# Static sensor mount
mount = self.config.mount
self.tf.publish(
Transform(
frame_id=self.config.child_frame_id,
child_frame_id=self.config.sensor_frame,
translation=Vector3(mount.x, mount.y, mount.z),
rotation=Quaternion(
mount.orientation.x,
mount.orientation.y,
mount.orientation.z,
mount.orientation.w,
),
ts=ts,
)
)

Expand Down
41 changes: 40 additions & 1 deletion dimos/msgs/nav_msgs/LineSegments3D.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,23 @@
import time
from typing import TYPE_CHECKING, BinaryIO

from dimos_lcm.geometry_msgs import (
Point as LCMPoint,
Pose as LCMPose,
PoseStamped as LCMPoseStamped,
Quaternion as LCMQuaternion,
)
from dimos_lcm.nav_msgs import Path as LCMPath
from dimos_lcm.std_msgs import Header as LCMHeader, Time as LCMTime

from dimos.types.timestamped import Timestamped


def _sec_nsec(ts: float) -> list[int]:
s = int(ts)
return [s, int((ts - s) * 1_000_000_000)]


if TYPE_CHECKING:
from rerun._baseclasses import Archetype

Expand Down Expand Up @@ -57,7 +70,33 @@ def __init__(
self._traversability = traversability or [1.0] * len(self._segments)

def lcm_encode(self) -> bytes:
raise NotImplementedError("Encoded on C++ side")
lcm_msg = LCMPath()
lcm_msg.poses = []

header_sec_nsec = _sec_nsec(self.ts)
for idx, (p1, p2) in enumerate(self._segments):
trav = self._traversability[idx] if idx < len(self._traversability) else 1.0
for endpoint, w_field in ((p1, trav), (p2, 0.0)):
pose = LCMPoseStamped()
pose.header = LCMHeader()
pose.header.stamp = LCMTime()
[pose.header.stamp.sec, pose.header.stamp.nsec] = header_sec_nsec
pose.header.frame_id = self.frame_id
pose.pose = LCMPose()
pose.pose.position = LCMPoint()
pose.pose.position.x = endpoint[0]
pose.pose.position.y = endpoint[1]
pose.pose.position.z = endpoint[2]
pose.pose.orientation = LCMQuaternion()
pose.pose.orientation.w = float(w_field)
lcm_msg.poses.append(pose)

lcm_msg.poses_length = len(lcm_msg.poses)
lcm_msg.header = LCMHeader()
lcm_msg.header.stamp = LCMTime()
[lcm_msg.header.stamp.sec, lcm_msg.header.stamp.nsec] = header_sec_nsec
lcm_msg.header.frame_id = self.frame_id
return lcm_msg.lcm_encode() # type: ignore[no-any-return]

@classmethod
def lcm_decode(cls, data: bytes | BinaryIO) -> LineSegments3D:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class WavefrontConfig(ModuleConfig):
info_gain_threshold: float = 0.03
num_no_gain_attempts: int = 2
goal_timeout: float = 15.0
frame_id: str = "world"


class WavefrontFrontierExplorer(Module):
Expand Down Expand Up @@ -751,7 +752,7 @@ def stop_exploration(self) -> bool:
goal = PoseStamped(
position=self.latest_odometry.position,
orientation=self.latest_odometry.orientation,
frame_id="world",
frame_id=self.config.frame_id,
ts=self.latest_odometry.ts,
)
self.goal_request.publish(goal)
Expand Down Expand Up @@ -792,7 +793,7 @@ def _exploration_loop(self) -> None:
goal_msg.position.y = goal.y
goal_msg.position.z = 0.0
goal_msg.orientation.w = 1.0 # No rotation
goal_msg.frame_id = "world"
goal_msg.frame_id = self.config.frame_id
goal_msg.ts = self.latest_costmap.ts

self.goal_request.publish(goal_msg)
Expand Down
115 changes: 115 additions & 0 deletions dimos/navigation/nav_stack/benchmarks/pose_graph_kitti360/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Pose-graph SLAM benchmark on KITTI-360

Generic loop-closure benchmark. Drop in any pose-graph SLAM module that
exposes the standard interface and the runner will replay a KITTI-360
sequence at it, watch its loop-closure output, and score precision /
recall / F1 against KITTI's ground-truth pose trajectory.

The module under test never sees KITTI — it only sees streams. The
runner provides two helper modules:

| Module | Role |
|------------------------------|-----------------------------------------------------------|
| `Kitti360PlaybackModule` | Publishes `registered_scan` + `odometry` from disk |
| Your pose-graph SLAM module | Consumes those, publishes `pose_graph_edges` + `loop_closure` |
| `PoseGraphScoringModule` | Subscribes to the outputs, accumulates metrics |

`autoconnect` wires the three together by stream name.

## Required interface for the module under test

```python
class YourPoseGraphModule(Module):
registered_scan: In[PointCloud2]
odometry: In[Odometry]

pose_graph_edges: Out[NavPath] # loop edges tagged orientation.w == 0.4
loop_closure: Out[NavPath] # one message per loop-closure update
```

Edge convention on `pose_graph_edges`: poses are paired
`(start, end, start, end, …)`. Odometry edges use `orientation.w = 1.0`,
loop-closure edges use `orientation.w = 0.4`. The timestamp on each
endpoint's `PoseStamped` header is the keyframe's *creation* timestamp,
which the scorer uses to map the endpoint back to its originating
KITTI frame_id.

## Dataset

Download from <https://www.cvlibs.net/datasets/kitti360> (Test SLAM 3D
split is enough). Expected layout:

```
<kitti360-root>/
├── calibration/
├── data_3d_raw/
│ └── 2013_05_28_drive_<NNNN>_sync/velodyne_points/data/*.bin
└── data_poses/
└── 2013_05_28_drive_<NNNN>_sync/cam0_to_world.txt
```

Sequence IDs map onto the drive numbers: `2 → drive_0002`, `4 → drive_0004`,
`8 → drive_0008`, etc.

## Quickstart

```python
from pathlib import Path
from dimos.navigation.nav_stack.benchmarks.pose_graph_kitti360.runner import (
run_benchmark,
)
from dimos.navigation.nav_stack.modules.pgo.pgo_module import PgoModule # your module

results = run_benchmark(
module_under_test=PgoModule.blueprint(),
kitti360_root=Path("~/datasets/kitti360").expanduser(),
sequence_id=2,
max_scans=None, # None = full sequence (~3k frames for seq 2)
publish_interval_sec=0.02,
)

print(results)
# {
# "true_positive": ..., "false_positive": ..., "false_negative": ...,
# "precision": ..., "recall": ..., "f1": ...,
# "detected_loop_edges": ..., "loop_closure_events": ...,
# "wallclock_seconds": ..., "sequence_id": 2,
# }
```

## Ground-truth definition

A loop pair `(i, j)` counts as ground truth if:

* frame gap `|i − j| ≥ DEFAULT_MIN_FRAME_GAP` (default 50), AND
* lidar-position distance ≤ `DEFAULT_MAX_LOOP_DISTANCE_M` (default 4.0 m).

Tune via `min_frame_gap=` and `max_loop_distance_m=` on `run_benchmark`.

A detected edge `(i, j)` is a **true positive** if `j` is in the
ground-truth valid-loop set for `i` (or vice-versa). Otherwise it's a
false positive. Ground-truth queries with no detection in their valid
set become false negatives.

## Files

| File | What it does |
|------|--------------|
| `runner.py` | `run_benchmark()` — builds the blueprint, polls playback, returns scores |
| `playback.py` | `Kitti360PlaybackModule` — streams scan + odom messages from disk |
| `scoring.py` | `PoseGraphScoringModule`, `LoopMetrics` — accumulates TP/FP/FN |
| `kitti360_loader.py` | `load_kitti360_sequence()` — reads velodyne `.bin` + `cam0_to_world.txt` |
| `loop_groundtruth.py` | `compute_loop_groundtruth()` + thresholds |

## Tips

- Start with `max_scans=200` for a smoke test; you should see playback
hit ~95% and a couple of GT pairs before paying for the full 3000-scan
run (~2.5 min wall on a Mac).
- Recall is bounded by your module's loop-search radius. KITTI ground
truth uses 4 m; if your module searches a 1 m radius, recall floors
near zero by construction even on a perfect descriptor.
- The scorer maps edge endpoints back to frame_ids via timestamps. If
your module rewrites pose timestamps after iSAM2 optimization, keep
the **creation** timestamp on the `PoseStamped` header so the lookup
still works.
Loading
Loading