Source code for embodichain.lab.sim.motion.workspace.samplers.iniform_sampler

# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------

import numpy as np
import torch
from typing import Union, TYPE_CHECKING

from embodichain.lab.sim.motion.workspace.configs.sampling_config import (
    SamplingStrategy,
)
from embodichain.lab.sim.motion.workspace.samplers.base_sampler import (
    BaseSampler,
)


from embodichain.utils import logger

# Note: Geometric constraint imports temporarily disabled

__all__ = ["UniformSampler"]


[docs] class UniformSampler(BaseSampler): """Uniform grid sampler. This sampler generates samples on a regular grid within the specified bounds. It ensures even coverage of the entire space, but suffers from the curse of dimensionality - the number of samples grows exponentially with the number of dimensions. Note: Geometric constraint sampling is temporarily disabled. Attributes: samples_per_dim: Number of samples to generate per dimension. When specified, this controls the grid density and takes precedence over num_samples. Total grid points = samples_per_dim^n_dims. """
[docs] def __init__( self, seed: int = 42, samples_per_dim: int | None = None, device: torch.device | None = None, ): """Initialize the uniform sampler. Args: seed: Random seed for reproducibility. Defaults to 42. samples_per_dim: Fixed number of samples per dimension. If None, will be calculated automatically from num_samples. Defaults to None. device: PyTorch device for tensor operations. """ super().__init__(seed, device) self.samples_per_dim = samples_per_dim
def _sample_from_bounds( self, bounds: torch.Tensor | np.ndarray, num_samples: int ) -> torch.Tensor: """Generate uniform grid samples within the given bounds. Args: bounds: Tensor/Array of shape (n_dims, 2) containing [lower, upper] bounds for each dimension. num_samples: Total number of samples to generate. This is used to calculate samples_per_dim if not explicitly provided during initialization. Note: The actual number of samples (samples_per_dim^n_dims) will not exceed this value, but may be less to maintain a uniform grid. Returns: Tensor of shape (actual_num_samples, n_dims) containing the sampled points. The actual number of samples will be samples_per_dim^n_dims. Raises: ValueError: If bounds are invalid. Examples: >>> sampler = UniformSampler(samples_per_dim=3) >>> bounds = torch.tensor([[-1, 1], [-1, 1]], dtype=torch.float32) >>> samples = sampler.sample(bounds, num_samples=10) >>> samples.shape torch.Size([9, 2]) # 3^2 = 9 samples """ bounds = self._validate_bounds(bounds) n_dims = bounds.shape[0] # Calculate samples per dimension if not provided if self.samples_per_dim is None: # Compute samples_per_dim to approximate the desired num_samples # Use floor to ensure actual grid size never exceeds num_samples samples_per_dim = max(2, int(num_samples ** (1.0 / n_dims))) else: samples_per_dim = self.samples_per_dim actual_num_samples = samples_per_dim**n_dims if actual_num_samples != num_samples and self.samples_per_dim is None: logger.log_info( f"Uniform grid: requested {num_samples} samples, " f"generating {actual_num_samples} samples " f"({samples_per_dim}^{n_dims}) for uniform coverage." ) # Create uniform grid for each dimension samples = self._create_grid(bounds, samples_per_dim) # Validate samples self._validate_samples(samples, bounds) return samples def _create_grid(self, bounds: torch.Tensor, samples_per_dim: int) -> torch.Tensor: """Create a uniform grid of samples. Args: bounds: Tensor of shape (n_dims, 2) containing [lower, upper] bounds. samples_per_dim: Number of samples per dimension. Returns: Tensor of shape (samples_per_dim^n_dims, n_dims) containing grid points. """ n_dims = bounds.shape[0] # Create linspace for each dimension grids = [] for i in range(n_dims): grid = torch.linspace( bounds[i, 0].item(), bounds[i, 1].item(), samples_per_dim, device=self.device, ) grids.append(grid) # Create meshgrid and flatten mesh = torch.meshgrid(*grids, indexing="ij") samples = torch.stack([m.flatten() for m in mesh], dim=-1) return samples # Note: Constraint-based sampling methods temporarily disabled
[docs] def get_strategy_name(self) -> str: """Get the name of the sampling strategy. Returns: String identifier for the sampling strategy. """ return SamplingStrategy.UNIFORM.value
def __repr__(self) -> str: """String representation of the sampler.""" return ( f"{self.__class__.__name__}(" f"strategy={self.get_strategy_name()}, " f"samples_per_dim={self.samples_per_dim}, " f"seed={self.seed})" )