diff --git a/dimos/core/module.py b/dimos/core/module.py index f36124116d..48a99a79a3 100644 --- a/dimos/core/module.py +++ b/dimos/core/module.py @@ -149,6 +149,12 @@ def _close_module(self) -> None: if hasattr(self, "_disposables"): self._disposables.dispose() + # Break the In/Out -> owner -> self reference cycle so the instance + # can be freed by refcount instead of waiting for GC. + for attr in list(vars(self).values()): + if isinstance(attr, (In, Out)): + attr.owner = None + def _close_rpc(self) -> None: if self.rpc: self.rpc.stop() # type: ignore[attr-defined] diff --git a/dimos/mapping/voxels.py b/dimos/mapping/voxels.py index df0e730868..124073cf49 100644 --- a/dimos/mapping/voxels.py +++ b/dimos/mapping/voxels.py @@ -105,6 +105,11 @@ def start(self) -> None: @rpc def stop(self) -> None: super().stop() + # Free tensor-tracked objects eagerly so Open3D does not report them as leaks. + self.get_global_pointcloud.invalidate_cache(self) + self.get_global_pointcloud2.invalidate_cache(self) + self.vbg = None + self._voxel_hashmap = None def _on_frame(self, frame: PointCloud2) -> None: self.add_frame(frame) diff --git a/dimos/msgs/sensor_msgs/PointCloud2.py b/dimos/msgs/sensor_msgs/PointCloud2.py index 3f28edc680..22fe731a70 100644 --- a/dimos/msgs/sensor_msgs/PointCloud2.py +++ b/dimos/msgs/sensor_msgs/PointCloud2.py @@ -363,27 +363,27 @@ def as_numpy( colors = np.asarray(self.pointcloud.colors) if self.pointcloud.has_colors() else None return points, colors - @functools.cache - def get_axis_aligned_bounding_box(self) -> o3d.geometry.AxisAlignedBoundingBox: + @functools.cached_property + def axis_aligned_bounding_box(self) -> o3d.geometry.AxisAlignedBoundingBox: """Get axis-aligned bounding box of the point cloud.""" return self.pointcloud.get_axis_aligned_bounding_box() - @functools.cache - def get_oriented_bounding_box(self) -> o3d.geometry.OrientedBoundingBox: + @functools.cached_property + def oriented_bounding_box(self) -> o3d.geometry.OrientedBoundingBox: """Get oriented bounding box of the point cloud.""" return self.pointcloud.get_oriented_bounding_box() - @functools.cache - def get_bounding_box_dimensions(self) -> tuple[float, float, float]: + @functools.cached_property + def bounding_box_dimensions(self) -> tuple[float, float, float]: """Get dimensions (width, height, depth) of axis-aligned bounding box.""" - bbox = self.get_axis_aligned_bounding_box() + bbox = self.axis_aligned_bounding_box extent = bbox.get_extent() return tuple(extent) def bounding_box_intersects(self, other: PointCloud2) -> bool: # Get axis-aligned bounding boxes - bbox1 = self.get_axis_aligned_bounding_box() - bbox2 = other.get_axis_aligned_bounding_box() + bbox1 = self.axis_aligned_bounding_box + bbox2 = other.axis_aligned_bounding_box # Get min and max bounds min1 = bbox1.get_min_bound() diff --git a/dimos/perception/detection/conftest.py b/dimos/perception/detection/conftest.py index 70338bd74f..e81ab2ab4a 100644 --- a/dimos/perception/detection/conftest.py +++ b/dimos/perception/detection/conftest.py @@ -113,7 +113,8 @@ def moment_provider(**kwargs) -> Moment: "tf": tf, } - return moment_provider + yield moment_provider + moment_provider.cache_clear() @pytest.fixture(scope="session") @@ -217,6 +218,7 @@ def moment_provider(**kwargs) -> Moment2D: yield moment_provider + moment_provider.cache_clear() module._close_module() @@ -250,6 +252,7 @@ def moment_provider(**kwargs) -> Moment3D: } yield moment_provider + moment_provider.cache_clear() if module is not None: module._close_module() diff --git a/dimos/perception/detection/type/detection3d/object.py b/dimos/perception/detection/type/detection3d/object.py index ae7a208baa..ec160c4a68 100644 --- a/dimos/perception/detection/type/detection3d/object.py +++ b/dimos/perception/detection/type/detection3d/object.py @@ -95,7 +95,7 @@ def update_object(self, other: Object) -> None: def get_oriented_bounding_box(self) -> Any: """Get oriented bounding box of the pointcloud.""" - return self.pointcloud.get_oriented_bounding_box() + return self.pointcloud.oriented_bounding_box def scene_entity_label(self) -> str: """Get label for scene visualization.""" diff --git a/dimos/perception/detection/type/detection3d/pointcloud.py b/dimos/perception/detection/type/detection3d/pointcloud.py index 5abc03e0c8..741b9c7498 100644 --- a/dimos/perception/detection/type/detection3d/pointcloud.py +++ b/dimos/perception/detection/type/detection3d/pointcloud.py @@ -74,15 +74,15 @@ def pose(self) -> PoseStamped: def get_bounding_box(self): # type: ignore[no-untyped-def] """Get axis-aligned bounding box of the detection's pointcloud.""" - return self.pointcloud.get_axis_aligned_bounding_box() + return self.pointcloud.axis_aligned_bounding_box def get_oriented_bounding_box(self): # type: ignore[no-untyped-def] """Get oriented bounding box of the detection's pointcloud.""" - return self.pointcloud.get_oriented_bounding_box() + return self.pointcloud.oriented_bounding_box def get_bounding_box_dimensions(self) -> tuple[float, float, float]: """Get dimensions (width, height, depth) of the detection's bounding box.""" - return self.pointcloud.get_bounding_box_dimensions() + return self.pointcloud.bounding_box_dimensions def bounding_box_intersects(self, other: Detection3DPC) -> bool: """Check if this detection's bounding box intersects with another's.""" diff --git a/dimos/utils/decorators/decorators.py b/dimos/utils/decorators/decorators.py index 01e9f8b553..ab3ed21e3f 100644 --- a/dimos/utils/decorators/decorators.py +++ b/dimos/utils/decorators/decorators.py @@ -16,7 +16,7 @@ from functools import wraps import threading import time -from typing import Any, Protocol, TypeVar +from typing import Any, Protocol, TypeVar, cast from .accumulators import Accumulator, LatestAccumulator @@ -113,7 +113,7 @@ def wrapper(*args, **kwargs): # type: ignore[no-untyped-def] return decorator -def simple_mcache(method: Callable) -> Callable: # type: ignore[type-arg] +def simple_mcache(method: Callable[..., _CacheReturn]) -> CachedMethod[_CacheReturn]: """ Decorator to cache the result of a method call on the instance. @@ -163,7 +163,7 @@ def invalidate_cache(instance: Any) -> None: getter.invalidate_cache = invalidate_cache # type: ignore[attr-defined] - return getter + return cast("CachedMethod[_CacheReturn]", getter) def retry(max_retries: int = 3, on_exception: type[Exception] = Exception, delay: float = 0.0): # type: ignore[no-untyped-def] diff --git a/dimos/utils/testing/moment.py b/dimos/utils/testing/moment.py index a0aa4219cb..62779995c4 100644 --- a/dimos/utils/testing/moment.py +++ b/dimos/utils/testing/moment.py @@ -44,6 +44,7 @@ def start(self) -> None: pass def stop(self) -> None: + self.value = None self.transport.stop() @@ -65,6 +66,7 @@ def start(self) -> None: pass def stop(self) -> None: + self.value = None self.transport.stop()