Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 8 additions & 1 deletion .github/workflows/message-codegen.yml
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ jobs:
if: runner.os == 'Linux'
run: |
sudo apt-get update
sudo apt-get install -y liblcm-dev
sudo apt-get install -y liblcm-dev libportaudio2
mkdir -p build/sdk-deps
curl --fail --location --retry 3 https://github.com/eclipse-zenoh/zenoh-c/releases/download/1.10.1/zenoh-c-1.10.1-x86_64-unknown-linux-gnu-standalone.zip -o build/sdk-deps/zenoh-c.zip
echo '9ee0f2d732b0f3042a7e1cd3076042a2bc3ac0415587c40bc3ed7b8b62fbde11 build/sdk-deps/zenoh-c.zip' | sha256sum --check
Expand All @@ -153,6 +153,13 @@ jobs:
.venv/bin/python -m pytest --noconftest --import-mode=importlib -o addopts='' -o 'markers=native_e2e: compiled native consumer acceptance' -q dimos/message_codegen/test_project_native.py -k actual_coordinator
.native-review/bin/python -I -c 'from dimos.memory import module; from dimos.memory.store import mcap; import pytest; import sys; assert all("site-packages" in str(value.__file__) for value in (module,mcap)); raise SystemExit(pytest.main(sys.argv[1:]))' --noconftest --import-mode=importlib -o addopts='' -q "$PWD/dimos/memory/test_module.py" "$PWD/dimos/memory/store/test_cdr_recording.py" "$PWD/dimos/memory/store/test_mcap.py"
cargo test --manifest-path native/rust/dimos-module/Cargo.toml --locked --features cdr --lib --jobs 2
- name: Verify Go2 CDR boundaries with stubbed drivers and installed code
if: runner.os == 'Linux'
run: |
# Import-only SDK subset, pinned to uv.lock. Audio/hardware APIs are never called.
uv pip install --python .native-review/bin/python 'aiortc==1.14.0'
uv pip install --python .native-review/bin/python --no-deps 'unitree-webrtc-connect==2.1.2' 'wasmtime==41.0.0' 'pycryptodome==3.23.0' 'curl-cffi==0.15.0' 'sounddevice==0.5.5'
LCM_DEFAULT_URL=memq:// .native-review/bin/python -I -c 'from dimos.robot.unitree.go2 import connection; from dimos.robot.unitree.go2.blueprints.basic import unitree_go2_basic; import pytest; import sys; assert all("site-packages" in str(value.__file__) for value in (connection,unitree_go2_basic)); raise SystemExit(pytest.main(sys.argv[1:]))' --noconftest --import-mode=importlib -o addopts='' -q "$PWD/dimos/robot/unitree/go2/test_connection.py" "$PWD/dimos/robot/unitree/test_connection.py" "$PWD/dimos/robot/unitree/go2/blueprints/basic/test_unitree_go2_basic.py"
- name: Verify generated browser schemas, relay encoding and web consumers
if: runner.os == 'Linux'
run: |
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/embedding/clip.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from functools import cached_property
from typing import overload

from dimos.messages.sensor_msgs.msg import Image
from PIL import Image as PILImage
import torch
import torch.nn.functional as functional
from transformers import CLIPModel as HFCLIPModel, CLIPProcessor

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import HuggingFaceModel
from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig
from dimos.msgs.image import image_to_rgb
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/embedding/dino.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from functools import cached_property
from typing import Any, overload

from dimos.messages.sensor_msgs.msg import Image
from PIL import Image as PILImage
import torch
import torch.nn.functional as functional
from transformers import AutoImageProcessor, AutoModel

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import HuggingFaceModel
from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig
from dimos.msgs.image import image_to_rgb
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/embedding/siglip.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from functools import cached_property
from typing import overload

from dimos.messages.sensor_msgs.msg import Image
from PIL import Image as PILImage
import torch
import torch.nn.functional as functional
from transformers import SiglipModel as HFSiglipModel, SiglipProcessor

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import HuggingFaceModel
from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig
from dimos.msgs.image import image_to_rgb
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/embedding/siglip2.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
from functools import cached_property
from typing import overload

from dimos.messages.sensor_msgs.msg import Image
from PIL import Image as PILImage
import torch
import torch.nn.functional as functional
from transformers import SiglipModel as HFSiglipModel, SiglipProcessor

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import HuggingFaceModel
from dimos.models.embedding.base import Embedding, EmbeddingModel, HuggingFaceEmbeddingModelConfig
from dimos.msgs.image import image_to_rgb
Expand Down
37 changes: 21 additions & 16 deletions dimos/models/embedding/test_generated_images.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,20 +29,25 @@
from dimos.msgs.image import image_from_array, image_view


@pytest.mark.parametrize("model_type", [CLIPModel,DINOModel,SigLIPModel,SigLIP2Model])
def test_generated_embeddings_keep_zero_source_time_and_convert_rgb(mocker,model_type):
processor,network=mocker.Mock(),mocker.Mock()
features=torch.tensor([[3.0,4.0]])
processor.return_value=BatchFeature(data={"pixel_values":torch.zeros(1,3,4,8)})
network.get_image_features.return_value=features
network.return_value=BaseModelOutputWithPooling(last_hidden_state=features[:,None,:],pooler_output=features)
mocker.patch.object(model_type,"_processor",new_callable=PropertyMock,return_value=processor)
mocker.patch.object(model_type,"_model",new_callable=PropertyMock,return_value=network)
image=image_from_array(np.full((4,8,3),[0,0,255],dtype=np.uint8),encoding="bgr8",
header=Header(frame_id="camera",stamp=Time(sec=0,nanosec=0)))
model=model_type(device="cpu")
result=model.embed(image)
@pytest.mark.parametrize("model_type", [CLIPModel, DINOModel, SigLIPModel, SigLIP2Model])
def test_generated_embeddings_keep_zero_source_time_and_convert_rgb(mocker, model_type):
processor, network = mocker.Mock(), mocker.Mock()
features = torch.tensor([[3.0, 4.0]])
processor.return_value = BatchFeature(data={"pixel_values": torch.zeros(1, 3, 4, 8)})
network.get_image_features.return_value = features
network.return_value = BaseModelOutputWithPooling(
last_hidden_state=features[:, None, :], pooler_output=features
)
mocker.patch.object(model_type, "_processor", new_callable=PropertyMock, return_value=processor)
mocker.patch.object(model_type, "_model", new_callable=PropertyMock, return_value=network)
image = image_from_array(
np.full((4, 8, 3), [0, 0, 255], dtype=np.uint8),
encoding="bgr8",
header=Header(frame_id="camera", stamp=Time(sec=0, nanosec=0)),
)
model = model_type(device="cpu")
result = model.embed(image)
assert result.timestamp == 0.0
torch.testing.assert_close(result.vector,torch.tensor([0.6,0.8]))
assert processor.call_args.kwargs["images"][0].getpixel((0,0)) == (255,0,0)
np.testing.assert_array_equal(image_view(image)[0,0],[0,0,255])
torch.testing.assert_close(result.vector, torch.tensor([0.6, 0.8]))
assert processor.call_args.kwargs["images"][0].getpixel((0, 0)) == (255, 0, 0)
np.testing.assert_array_equal(image_view(image)[0, 0], [0, 0, 255])
2 changes: 1 addition & 1 deletion dimos/models/embedding/treid.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@

from functools import cached_property

from dimos.messages.sensor_msgs.msg import Image
import torch
import torch.nn.functional as functional
from torchreid import utils as torchreid_utils

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import LocalModel
from dimos.models.embedding.base import Embedding, EmbeddingModel, EmbeddingModelConfig
from dimos.msgs.image import image_to_bgr
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/segmentation/edge_tam.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,14 +20,14 @@
import tempfile
from typing import TYPE_CHECKING, Any, Protocol, TypedDict

from dimos.messages.sensor_msgs.msg import Image
from hydra.utils import instantiate
import numpy as np
from numpy.typing import NDArray
from omegaconf import OmegaConf
from PIL import Image as PILImage
import torch

from dimos.messages.sensor_msgs.msg import Image
from dimos.models.base import default_torch_device
from dimos.msgs.image import image_to_bgr, image_to_jpeg
from dimos.perception.detection.detectors.base import Detector
Expand Down
4 changes: 2 additions & 2 deletions dimos/models/segmentation/test_yoloe.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@
from typing import Any
from unittest.mock import MagicMock

from dimos.messages.sensor_msgs.msg import Image
from dimos.messages.std_msgs.msg import Header
import numpy as np
from ultralytics.models.yolo.yoloe import YOLOEVPSegPredictor

from dimos.messages.sensor_msgs.msg import Image
from dimos.messages.std_msgs.msg import Header
from dimos.models.segmentation.yoloe import YoloeBoxSegmenter
from dimos.msgs.image import image_from_array, image_to_bgr
from dimos.msgs.time import time_from_seconds, to_seconds
Expand Down
2 changes: 1 addition & 1 deletion dimos/models/segmentation/yoloe.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,11 +19,11 @@
from dataclasses import replace
from typing import Protocol

from dimos.messages.sensor_msgs.msg import Image
import numpy as np
from numpy.typing import NDArray
from ultralytics.models.yolo.yoloe import YOLOEVPSegPredictor

from dimos.messages.sensor_msgs.msg import Image
from dimos.msgs.image import image_to_bgr
from dimos.perception.detection.detectors.yoloe import Yoloe2DDetector, YoloePromptMode
from dimos.perception.detection.type.detection2d.bbox import Bbox, Detection2DBBox
Expand Down
61 changes: 61 additions & 0 deletions dimos/robot/unitree/go2/blueprints/basic/test_unitree_go2_basic.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Copyright 2025-2026 Dimensional Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Go2 blueprint rendering consumes the same generated values as live ports."""

import numpy as np

from dimos.messages.builtin_interfaces.msg import Time
from dimos.messages.dimos_msgs.msg import LineSegment3D, LineSegments3D
from dimos.messages.geometry_msgs.msg import Point, Pose, PoseArray, PoseStamped, Quaternion
from dimos.messages.std_msgs.msg import Header
from dimos.msgs.occupancy import occupancy_from_array
from dimos.msgs.pointcloud import pointcloud_from_xyz
from dimos.robot.unitree.go2.blueprints.basic import unitree_go2_basic as view
from dimos.robot.unitree.go2.connection import GO2Connection


def test_camera_and_pose_overrides_accept_generated_port_values():
camera = view._convert_camera_info(GO2Connection.camera_info_static)
assert camera[0][0] == "/world/color_image"
assert camera[0][1].resolution.as_arrow_array().to_pylist() == [[1280.0, 720.0]]
pose = Pose(Point(1.0, 2.0, 3.0), Quaternion(0.0, 0.0, 0.0, 1.0))
entries = dict(view._plot_odom(PoseStamped(Header(Time(0, 0), "world"), pose)))
assert "world/odom/message" in entries
assert "world/odom" in entries
assert entries["plots/odom/x"].scalars.as_arrow_array().to_pylist() == [1.0]
assert entries["plots/odom/y"].scalars.as_arrow_array().to_pylist() == [2.0]


def test_map_and_pgo_overrides_preserve_rendered_geometry():
header = Header(Time(0, 0), "world")
cloud = pointcloud_from_xyz(np.array([[1.0, 2.0, 0.2], [9.0, 8.0, -0.1]]), header=header)
points = view._convert_global_map(cloud).positions.as_arrow_array().to_pylist()
np.testing.assert_allclose(points, [[1.0, 2.0, 0.2]])
pose = Pose(Point(1.0, 2.0, 3.0), Quaternion(0.0, 0.0, 0.0, 1.0))
keyframes = dict(view._convert_pgo_keyframes(PoseArray(header, [pose])))
assert keyframes["world/pgo/keyframes"].positions.as_arrow_array().to_pylist() == [
[1.0, 2.0, 3.0]
]
edges = LineSegments3D(header, [LineSegment3D(Point(1.0, 2.0, 3.0), Point(4.0, 5.0, 6.0), 0.9)])
loops = dict(view._convert_pgo_loops(edges))
assert loops["world/pgo/loops"].strips.as_arrow_array().to_pylist() == [
[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]
]
grid = occupancy_from_array(np.array([[0, 100]], dtype=np.int8), header=header, resolution=0.5)
mesh = view._convert_navigation_costmap(grid)
np.testing.assert_allclose(
mesh.vertex_positions.as_arrow_array().to_pylist(),
[[0.0, 0.0, 0.015], [1.0, 0.0, 0.015], [1.0, 0.5, 0.015], [0.0, 0.5, 0.015]],
)
55 changes: 36 additions & 19 deletions dimos/robot/unitree/go2/blueprints/basic/unitree_go2_basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,11 @@

from dimos.core.coordination.blueprints import autoconnect
from dimos.core.global_config import global_config
from dimos.msgs.geometry_msgs.Pose import Pose as NumericalPose
from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid as NumericalGrid
from dimos.msgs.occupancy import occupancy_view
from dimos.robot.unitree.go2.connection import GO2Connection
from dimos.visualization.rerun.cdr import camera_pinhole, cloud_archetype, generated_archetypes
from dimos.visualization.vis_module import vis_module

if TYPE_CHECKING:
Expand All @@ -32,30 +36,32 @@
from rerun._baseclasses import Archetype
from rerun.blueprint import Blueprint

from dimos.msgs.geometry_msgs.PoseArray import PoseArray
from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped
from dimos.msgs.geometry_msgs.Twist import Twist
from dimos.msgs.nav_msgs.LineSegments3D import LineSegments3D
from dimos.msgs.nav_msgs.OccupancyGrid import OccupancyGrid
from dimos.msgs.sensor_msgs.CameraInfo import CameraInfo
from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2
from dimos.msgs.std_msgs.Float32 import Float32
from dimos.messages.dimos_msgs.msg import LineSegments3D
from dimos.messages.geometry_msgs.msg import PoseArray, PoseStamped, Twist
from dimos.messages.nav_msgs.msg import OccupancyGrid
from dimos.messages.sensor_msgs.msg import CameraInfo, PointCloud2
from dimos.messages.std_msgs.msg import Float32
from dimos.visualization.rerun.bridge import RerunData, RerunMulti


def _convert_camera_info(camera_info: CameraInfo) -> RerunData:
return camera_info.to_rerun(
image_topic="/world/color_image",
optical_frame="camera_optical",
)
return [("/world/color_image", camera_pinhole(camera_info))]


def _convert_global_map(grid: PointCloud2) -> Archetype:
return grid.to_rerun(bottom_cutoff=0)
return cloud_archetype(grid, bottom_cutoff=0)


def _convert_navigation_costmap(grid: OccupancyGrid) -> Archetype:
return grid.to_rerun(
# Reuse the existing numerical texture renderer behind an explicit value adapter.
p, q = grid.info.origin.position, grid.info.origin.orientation
numerical = NumericalGrid(
grid=occupancy_view(grid),
resolution=grid.info.resolution,
origin=NumericalPose(p.x, p.y, p.z, q.x, q.y, q.z, q.w),
frame_id=grid.header.frame_id,
)
return numerical.to_rerun(
colormap="Accent",
z_offset=0.015,
opacity=0.2,
Expand All @@ -66,7 +72,10 @@ def _convert_navigation_costmap(grid: OccupancyGrid) -> Archetype:
def _convert_pgo_keyframes(keyframes: PoseArray) -> RerunMulti:
import rerun as rr

positions = keyframes.positions()
positions = np.asarray(
[[pose.position.x, pose.position.y, pose.position.z] for pose in keyframes.poses],
dtype=np.float32,
).reshape(-1, 3)
return [
("world/pgo/keyframes", rr.Points3D(positions, colors=[[255, 0, 0]], radii=[0.025])),
("world/pgo/path", rr.LineStrips3D([positions], colors=[[255, 255, 255]], radii=[0.01])),
Expand All @@ -76,7 +85,13 @@ def _convert_pgo_keyframes(keyframes: PoseArray) -> RerunMulti:
def _convert_pgo_loops(edges: LineSegments3D) -> RerunMulti:
import rerun as rr

segments = edges.segments.astype(np.float32)
segments = np.asarray(
[
[[edge.start.x, edge.start.y, edge.start.z], [edge.end.x, edge.end.y, edge.end.z]]
for edge in edges.segments
],
dtype=np.float32,
).reshape(-1, 2, 3)
strips = rr.LineStrips3D(segments, colors=[[231, 76, 60]], radii=[0.008])
return [("world/pgo/loops", strips)]

Expand All @@ -90,10 +105,12 @@ def _plot_telemetry(name: str, msg: Float32) -> RerunMulti:
def _plot_odom(odom: PoseStamped) -> RerunMulti:
import rerun as rr

entries = generated_archetypes(odom, "world/odom")
assert entries is not None
return [
("world/odom", odom.to_rerun()),
("plots/odom/x", rr.Scalars(odom.x)),
("plots/odom/y", rr.Scalars(odom.y)),
*entries,
("plots/odom/x", rr.Scalars(odom.pose.position.x)),
("plots/odom/y", rr.Scalars(odom.pose.position.y)),
]


Expand Down
Loading
Loading