Source code for embodichain.lab.sim.objects.deformable.data
# ----------------------------------------------------------------------------
# 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.
# ----------------------------------------------------------------------------
"""Shared nodal data for Newton volume and surface particle sets."""
from __future__ import annotations
from typing import TYPE_CHECKING, Sequence
import torch
if TYPE_CHECKING:
from dexsim.scene import Scene, SpawnedParticleSet
__all__ = ["DeformableObjectData"]
[docs]
class DeformableObjectData:
"""Common nodal-state view for volume and surface deformables.
Positions and velocities use the simulation world frame. Consumers can
rely on a stable ``(num_instances, n_nodes, 3)`` contract. State properties
return independent snapshots; the default state is captured at Spawn binding.
Args:
entities: Replicated particle sets with equal node counts.
scene: Finalized DexSim scene that owns the particle sets.
device: Device used for state tensors and batch transfers.
"""
[docs]
def __init__(
self,
entities: Sequence[SpawnedParticleSet],
scene: Scene,
device: torch.device,
) -> None:
self.entities = list(entities)
if not self.entities:
raise ValueError("A deformable particle-set batch cannot be empty.")
self.scene = scene
self.device = device
self.num_instances = len(self.entities)
particle_counts = tuple(int(entity.particle_count) for entity in self.entities)
if any(count <= 0 for count in particle_counts):
raise ValueError("Deformable particle sets must contain particles.")
if len(set(particle_counts)) != 1:
raise ValueError(
"All instances of one deformable asset must have the same "
f"particle count, got {particle_counts}."
)
self.n_nodes = particle_counts[0]
self.batch = scene.create_particle_set_batch(self.entities)
self._position_buffer = torch.empty(
(self.num_instances, self.n_nodes, 3),
dtype=torch.float32,
device=self.device,
)
self._velocity_buffer = torch.empty_like(self._position_buffer)
default_positions = self.nodal_pos_w
default_velocities = self.nodal_vel_w
self._default_nodal_state_w = torch.cat(
(default_positions, default_velocities),
dim=-1,
)
@staticmethod
def _check_batch_status(status: int | None, operation: str) -> None:
if status is not None and int(status) < 0:
raise RuntimeError(
f"DexSim particle batch failed to {operation}: status {status}."
)
@property
def nodal_pos_w(self) -> torch.Tensor:
"""Return current Newton particle positions in world frame."""
status = self.batch.fetch_particle_positions(
self._position_buffer.reshape(-1, 3)
)
self._check_batch_status(status, "fetch positions")
return self._position_buffer.clone()
@property
def nodal_vel_w(self) -> torch.Tensor:
"""Return current Newton particle velocities in world frame."""
status = self.batch.fetch_particle_velocities(
self._velocity_buffer.reshape(-1, 3)
)
self._check_batch_status(status, "fetch velocities")
return self._velocity_buffer.clone()
@property
def default_nodal_state_w(self) -> torch.Tensor:
"""Return the particle state captured when Spawn was bound."""
return self._default_nodal_state_w.clone()
@property
def nodal_state_w(self) -> torch.Tensor:
"""Return current nodal state ``[position, velocity]`` in world frame."""
return torch.cat((self.nodal_pos_w, self.nodal_vel_w), dim=-1)
@property
def root_pos_w(self) -> torch.Tensor:
"""Return the mean nodal position for each deformable instance."""
return self.nodal_pos_w.mean(dim=1)
@property
def root_vel_w(self) -> torch.Tensor:
"""Return the mean nodal velocity for each deformable instance."""
return self.nodal_vel_w.mean(dim=1)
def _apply_nodal_state(
self,
positions: torch.Tensor,
velocities: torch.Tensor,
env_ids: Sequence[int],
) -> None:
"""Apply packed state to selected particle-set instances."""
env_ids = [int(env_id) for env_id in env_ids]
if not env_ids:
return
if len(set(env_ids)) != len(env_ids):
raise ValueError(f"env_ids must not contain duplicates, got {env_ids}.")
expected_shape = (len(env_ids), self.n_nodes, 3)
if tuple(positions.shape) != expected_shape:
raise ValueError(
f"positions must have shape {expected_shape}, got "
f"{tuple(positions.shape)}."
)
if tuple(velocities.shape) != expected_shape:
raise ValueError(
f"velocities must have shape {expected_shape}, got "
f"{tuple(velocities.shape)}."
)
if env_ids == list(range(self.num_instances)):
batch = self.batch
else:
batch = self.scene.create_particle_set_batch(
[self.entities[env_id] for env_id in env_ids]
)
packed_positions = (
positions.to(
device=self.device,
dtype=torch.float32,
)
.contiguous()
.reshape(-1, 3)
)
packed_velocities = (
velocities.to(
device=self.device,
dtype=torch.float32,
)
.contiguous()
.reshape(-1, 3)
)
position_status = batch.apply_particle_positions(packed_positions)
self._check_batch_status(position_status, "apply positions")
velocity_status = batch.apply_particle_velocities(packed_velocities)
self._check_batch_status(velocity_status, "apply velocities")