Source code for embodichain.lab.sim.motion.workspace.metrics.reachability_metric
# ----------------------------------------------------------------------------
# Copyright (c) 2021-2026 DexForce Technology Co., Ltd.
#
# 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.
# ----------------------------------------------------------------------------
from typing import Dict, Any
import numpy as np
from embodichain.lab.sim.motion.workspace.metrics.base_metric import (
BaseMetric,
)
from embodichain.lab.sim.motion.workspace.configs.metric_config import (
ReachabilityConfig,
)
[docs]
class ReachabilityMetric(BaseMetric):
"""Reachability metric for workspace analysis.
Computes reachable workspace volume and coverage statistics.
"""
[docs]
def __init__(self, config: ReachabilityConfig | None = None):
"""Initialize reachability metric.
Args:
config: Reachability configuration.
"""
super().__init__(config or ReachabilityConfig())
[docs]
def compute(
self,
workspace_points: np.ndarray,
joint_configurations: np.ndarray | None = None,
**kwargs,
) -> Dict[str, Any]:
"""Compute reachability metrics.
Args:
workspace_points: Workspace points in Cartesian space, shape (N, 3).
joint_configurations: Joint configurations, shape (N, num_joints).
**kwargs: Additional arguments.
Returns:
Dictionary containing:
- volume: Estimated reachable volume (m³)
- num_voxels: Number of occupied voxels
- coverage: Coverage percentage relative to bounding box
- bounding_box: Min and max bounds
- centroid: Center of workspace
"""
points = self._to_numpy(workspace_points)
if len(points) == 0:
return {
"volume": 0.0,
"num_voxels": 0,
"coverage": 0.0,
"bounding_box": {"min": None, "max": None},
"centroid": None,
}
# Compute bounding box
min_bounds = points.min(axis=0)
max_bounds = points.max(axis=0)
# Compute centroid
centroid = points.mean(axis=0)
# Voxelize the workspace
voxel_size = self.config.voxel_size
voxel_grid = self._voxelize_points(points, voxel_size)
# Count occupied voxels
num_voxels = len(voxel_grid)
# Estimate volume
volume = num_voxels * (voxel_size**3)
# Compute coverage if requested
coverage = 0.0
if self.config.compute_coverage:
# Bounding box volume
bbox_volume = np.prod(max_bounds - min_bounds)
if bbox_volume > 0:
coverage = (volume / bbox_volume) * 100.0
self.results = {
"volume": float(volume),
"num_voxels": int(num_voxels),
"coverage": float(coverage),
"bounding_box": {
"min": min_bounds.tolist(),
"max": max_bounds.tolist(),
},
"centroid": centroid.tolist(),
"voxel_size": voxel_size,
}
return self.results
def _voxelize_points(
self, points: np.ndarray, voxel_size: float
) -> Dict[tuple, int]:
"""Convert points to voxel grid using vectorized operations.
Args:
points: Point cloud, shape (N, 3).
voxel_size: Size of each voxel.
Returns:
Dictionary mapping voxel coordinates to point count.
"""
# Convert points to voxel indices
voxel_indices = np.floor(points / voxel_size).astype(int)
# Use np.unique for vectorized counting
unique_indices, counts = np.unique(voxel_indices, axis=0, return_counts=True)
# Filter by minimum points threshold and build dict
min_points = self.config.min_points_per_voxel
voxel_grid = {}
for idx, count in zip(unique_indices, counts):
if count >= min_points:
voxel_grid[tuple(idx)] = int(count)
return voxel_grid