Skip to content

danvi/dim1486/generalize dan perception stack - #3723

Open
bogwi wants to merge 16 commits into
mainfrom
danvi/dim1486/generalize-dan-perception-stack
Open

danvi/dim1486/generalize dan perception stack#3723
bogwi wants to merge 16 commits into
mainfrom
danvi/dim1486/generalize-dan-perception-stack

Conversation

@bogwi

@bogwi bogwi commented Aug 27, 2026

Copy link
Copy Markdown
Member

Generealize perception stack

This PR builds, improves, and adds new functionality based on the foundation of these previous PRs:
#3496
#3422

How to test and what to expect

refference machine:
Lenovo Legion laptop, AMD Ruyzen AI 7, 32GB RAM, NVIDIA RTX 5070 Laptop edition (8 GB)

xArm

This uses the deault location for xArm dataset, already on main, xarm6_worldbelief_20260729_203624_161992.db.
You do not need to specify the --dataset <location_to_recording_bag_.db>

time uv run python -m dimos.perception.memory.tool_localize "roll of black tape" "book" "pen" "red marker" "yellow sticky notes" pc_xArm_all_multi.rrd --from 427 --duration 76 --multi && uv run dimos-viewer pc_xArm_all_multi.rrd

real	0m34.002s
user	1m4.301s
sys	    0m5.702s
xArm_projections

G1

You will need a g1 recording for this, check this PR, it's on main already, #3527

time uv run python -m dimos.perception.memory.tool_localize "paper box" "plant in a pot" "shoes" "wooden crate" "office chair" "bag" g1_sf_120_180_localize.rrd --dataset data/g1_sf_office.db --from 120 --duration 60 --multi && uv run dimos-viewer g1_sf_120_180_localize.rrd

real	1m11.971s
user	4m44.586s
sys	    0m11.864s
pr1486_g1_sf

Go2

Run on the default go2_short.db

time uv run python -m dimos.perception.memory.tool_localize "table" "white robot" "potted plants" "wooden doors" "coke bottle" "fanta bottle" "sprite bottle" "lays chips" "pillow" "green plant" "plant pot" go2_short_00_60.rrd --dataset data/go2_short.db --from 0 --duration 60 --multi && uv run dimos-viewer go2_short_00_60.rrd

real    1m4.001s
user    1m45.383s
sys     0m24.279s
pr1486_go2_short

Important

  1. Performance is affected:
  • by a cold run, weights need to be loaded, around ~11 seconds on my machine
  • saving to *.rrd format
  1. tool_inventory.py has been not touched by this commit, will come later.
    Use localize() API for testing and building
  2. Re-id and spatial memory are underway too
  3. Improving objects 2D into 3D is also underway
  4. supporting mid360 lidar is also undeerway
  5. Any questions - contact me directly, @bogwi

@codecov

codecov Bot commented Aug 27, 2026

Copy link
Copy Markdown

❌ 3 Tests Failed:

Tests completed Failed Passed Skipped
4810 3 4807 87
View the top 2 failed test(s) by shortest run time
dimos.perception.detection.type.detection3d.test_pointcloud::test_detection3dpc
Stack Traces | 2s run time
detection3dpc = Detection3DPC(bbox=(503.43731689453125, 249.89385986328125, 655.9505004882812, 469.82879638671875), track_id=1, class_...-0.223766, -0.681714, 0.653357, -0.241493)), pointcloud=PointCloud(points=79, frame_id='world', ts=1757960670.4755309))

    @pytest.mark.skipif_macos_bug
    def test_detection3dpc(detection3dpc) -> None:
        # def test_oriented_bounding_box(detection3dpc):
        """Test oriented bounding box calculation and values."""
        obb = detection3dpc.get_oriented_bounding_box()
        assert obb is not None, "Oriented bounding box should not be None"
    
        # Verify OBB center values
        assert obb.center[0] == pytest.approx(-3.36002, abs=0.1)
>       assert obb.center[1] == pytest.approx(-0.196446, abs=0.1)
E       assert np.float64(-0...7547408540046) == -0.196446 ± 1.0e-01
E         
E         comparison failed
E         Obtained: -0.30017547408540046
E         Expected: -0.196446 ± 1.0e-01

detection3dpc = Detection3DPC(bbox=(503.43731689453125, 249.89385986328125, 655.9505004882812, 469.82879638671875), track_id=1, class_...-0.223766, -0.681714, 0.653357, -0.241493)), pointcloud=PointCloud(points=79, frame_id='world', ts=1757960670.4755309))
obb        = OrientedBoundingBox: center: (-3.31621, -0.300175, 0.240114), extent: 0.593476, 0.470315, 0.164996)

.../type/detection3d/test_pointcloud.py:30: AssertionError
dimos.e2e_tests.test_manipulation_planning_groups::test_dual_arm_plans_and_dispatches_both_arms_through_control_coordinator
Stack Traces | 120s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f402097cb0>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e840>

    def test_dual_arm_plans_and_dispatches_both_arms_through_control_coordinator(
        lcm_spy: LcmSpy,
        start_blueprint: Callable[..., DimosCliCall],
    ) -> None:
        """Plan one generated plan over both arms and dispatch through one trajectory task."""
>       _start_openarm_mock_planner(start_blueprint, lcm_spy)

lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f402097cb0>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e840>

dimos/e2e_tests/test_manipulation_planning_groups.py:190: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/test_manipulation_planning_groups.py:151: in _start_openarm_mock_planner
    lcm_spy.wait_for_saved_topic(JOINT_STATE_TOPIC, timeout=120.0)
        lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f402097cb0>
        start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e840>
dimos/e2e_tests/lcm_spy.py:97: in wait_for_saved_topic
    wait_until(
        condition  = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999f1a0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f402097cb0>
        timeout    = 120.0
        topic      = '/coordinator_joint_state#sensor_msgs.JointState'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999f1a0>

    def wait_until(
        predicate: Callable[[], bool],
        *,
        timeout: float,
        interval: float = 0.1,
        message: str | None = None,
    ) -> None:
        """Poll ``predicate`` until it returns truthy or ``timeout`` elapses."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if predicate():
                return
            time.sleep(interval)
>       raise TimeoutError(message or f"Timed out after {timeout}s waiting for condition")
E       TimeoutError: Timeout waiting for topic /coordinator_joint_state#sensor_msgs.JointState

deadline   = 4300261.705239764
interval   = 0.1
message    = 'Timeout waiting for topic /coordinator_joint_state#sensor_msgs.JointState'
predicate  = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999f1a0>
timeout    = 120.0

.../utils/testing/waiting.py:35: TimeoutError
View the full list of 1 ❄️ flaky test(s)
dimos.e2e_tests.test_manipulation_planning_groups::test_single_arm_plans_and_executes_through_control_coordinator

Flake rate in main: 7.69% (Passed 12 times, Failed 1 times)

Stack Traces | 120s run time
lcm_spy = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f2f9ce9340>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e2a0>

    def test_single_arm_plans_and_executes_through_control_coordinator(
        lcm_spy: LcmSpy,
        start_blueprint: Callable[..., DimosCliCall],
    ) -> None:
        """Plan with one arm and execute through its trajectory task."""
>       _start_openarm_mock_planner(start_blueprint, lcm_spy)

lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f2f9ce9340>
start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e2a0>

dimos/e2e_tests/test_manipulation_planning_groups.py:159: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 
dimos/e2e_tests/test_manipulation_planning_groups.py:151: in _start_openarm_mock_planner
    lcm_spy.wait_for_saved_topic(JOINT_STATE_TOPIC, timeout=120.0)
        lcm_spy    = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f2f9ce9340>
        start_blueprint = <function start_blueprint.<locals>.set_name_and_start at 0x72f2f999e2a0>
dimos/e2e_tests/lcm_spy.py:97: in wait_for_saved_topic
    wait_until(
        condition  = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999c9a0>
        self       = <dimos.e2e_tests.lcm_spy.LcmSpy object at 0x72f2f9ce9340>
        timeout    = 120.0
        topic      = '/coordinator_joint_state#sensor_msgs.JointState'
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ 

predicate = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999c9a0>

    def wait_until(
        predicate: Callable[[], bool],
        *,
        timeout: float,
        interval: float = 0.1,
        message: str | None = None,
    ) -> None:
        """Poll ``predicate`` until it returns truthy or ``timeout`` elapses."""
        deadline = time.monotonic() + timeout
        while time.monotonic() < deadline:
            if predicate():
                return
            time.sleep(interval)
>       raise TimeoutError(message or f"Timed out after {timeout}s waiting for condition")
E       TimeoutError: Timeout waiting for topic /coordinator_joint_state#sensor_msgs.JointState

deadline   = 4300141.528142378
interval   = 0.1
message    = 'Timeout waiting for topic /coordinator_joint_state#sensor_msgs.JointState'
predicate  = <function LcmSpy.wait_for_saved_topic.<locals>.condition at 0x72f2f999c9a0>
timeout    = 120.0

.../utils/testing/waiting.py:35: 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.

@greptile-apps

greptile-apps Bot commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This change broadens perception-memory localization across depth-camera and registered point-cloud rigs, adds persistent object identities, batches detector scoring, and expands recording CLI configuration.

Two localization reliability problems remain. Short embedded recordings can yield no semantic frame to inspect, so objects in one- or two-frame windows are never sent to detection. Separately, a frame that temporarily lacks usable 3D geometry is permanently skipped when the same identity store is reused, even after geometry becomes available.

T-Rex validation blocked

The short-recording check could not run to completion because the Python runtime was missing repository dependencies. The final import failure was for the plum package, so the harness did not reach the localization path.

Confidence Score: 3/5

Not safe to merge without addressing the two localization failure paths, because valid objects can be omitted from results.

One failure was reproduced by executing two localization calls against a reused identity store and demonstrating recovery after delaying ingestion. The short-recording failure is directly supported by the selection path, but its runtime harness was blocked by missing Python packages.

Files Needing Attention: dimos/perception/memory/localize.py needs fallback candidate selection for short semantic indexes and delayed persistence of ingestion state; dimos/memory/transform.py may also need a short-stream behavior that yields a usable result.

T-Rex T-Rex Logs

What T-Rex did

  • T-Rex produced a proof for a posted P1 finding using a focused two-call harness, and the activity is supported by the harness source and runtime outputs.
  • Validation was blocked due to a missing Python package (plum) in the authored short-index harness, preventing execution and producing a ModuleNotFoundError.
  • T-Rex produced another finding-proof for a posted P1 finding.
  • Execution was blocked: the localize-short-index-repro.py script could not run, with baseline failing on numpy and environment issues, and subsequent imports failing for reactivex, pydantic_settings, dimos_lcm, and plum, leaving no executable observation of the localization behavior.

View all artifacts

T-Rex Ran code and verified through T-Rex

Reviews (1): Last reviewed commit: "ship go2_short.db.rig.json inside the go..." | Re-trigger Greptile

Comment on lines +269 to +273
.transform(peaks(key=_similarity, distance=1.0))
.materialize()
)
logger.info(
f"localize {q!r}: {label_peaks.count()} semantic peaks of {index.count()} embedded"

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.

P1 Short indexes lose candidates

When an embedded index contains only one or two observations, routing the complete search result through peaks() produces no peaks, so detection never runs and localization returns an empty result even when an available frame contains the requested object.

T-Rex Ran code and verified through T-Rex

key = (obs.ts, queries[j], floor)
cache.move_to_end(key)
rows_per_label.append((j, cache[key]))
ingested[j].add(obs.ts)

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.

P1 Failed lifts become permanent

A frame is added to the persistent ingested set before segmentation and 3D lifting have succeeded. If pose or geometry is temporarily unavailable, a later call using the same IdentityStore skips the frame entirely, so the object cannot be recovered once geometry becomes available. Mark the timestamp ingested only after the frame has produced durable identity evidence, or retain failed frames for retry.

Artifacts

Focused two-call harness source

  • Authored narrow harness that executes localize() twice with one reused IdentityStore, toggling geometry availability between calls and comparing the current source with delayed ingestion.

Current-source two-call output

  • Executed output (exit 0): first=[]; ingested_after_first=[7.0]; second=[]; score_calls=1; segment_calls=1; lift_geometry_states=[False].

Delayed-ingestion comparison output

  • Executed output (exit 0): first=[]; ingested_after_first=[]; second=['recovered']; score_calls=1; segment_calls=2; lift_geometry_states=[False, True].

View artifacts

T-Rex Ran code and verified through T-Rex

@leshy leshy left a comment

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.

basically escaped mem2 immediately, first comments first view to push back on this, might have more stuff

index.search(query_embedding)
.order_by("ts")
.transform(peaks(key=_similarity, distance=1.0))
.materialize()

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 are we ordering by ts? we can order by peak height (this is a default ordering) - why are you materializing the full stream, this is a very expensive operation, that loads all data in memory. we should be able to have a seeker head that loads observation by observation until peak becomes irrelevant. very low mem footprint

logger.info(
f"localize {q!r}: {label_peaks.count()} semantic peaks of {index.count()} embedded"
)
peaks_per_label.append(label_peaks)

@leshy leshy Aug 27, 2026

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.

you escaped memory system and streams immediately and are building lists. why isn't this a mem stream? you should always operate on streams of observations, not on raw lists containing all data, we are angling towards parsing an hour, a day of data etc

expanded.add(peak.ts)
nearby: Stream[Any, Any] = index.near(
peak.pose_stamped, radius=policy.verify_radius_m
).transform(QualityWindow(lambda img: img.sharpness, window=0.5))

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.

embeddings themselves should already be filtered by the qualitywindow, all these matches will be clean, you don't need to filter downstream

).transform(QualityWindow(lambda img: img.sharpness, window=0.5))
for obs in nearby:
frames.setdefault(obs.ts, obs)
ordered = sorted(frames.values(), key=lambda obs: obs.ts)

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.

ordering is a db operation

logger.info(f"detection: {len(ordered)} candidate frames for {len(queries)} labels")

if ordered:
from dimos.perception.memory.support_plane import fit_support_plane

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 are not these following lines a nice mem2 transform?

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