Source code for sim.collision.contacts
"""SAP-owned rigid contact buffers for the Newton-compatible collision path.
Source note: the SAP modifications in this module are based on Newton's
contact storage code and adapted so SAP Warp can interoperate with
Newton-authored scenes and runtime objects.
"""
from __future__ import annotations
import warp as wp
from warp import DeviceLike as SapDeviceLike
[docs]
class SapContacts:
"""
Rigid contact storage consumed by SAP-owned collision generation.
The buffer layout mirrors the rigid-contact portion of the source collision
storage: one active contact counter, per-contact shape indices, witness
points, offsets, normal, margins, thread ids, contact force, and optional
per-contact material properties.
"""
EXTENDED_ATTRIBUTES: frozenset[str] = frozenset(("force",))
@classmethod
def validate_extended_attributes(cls, attributes: tuple[str, ...]) -> None:
"""Validate that requested extended contact attributes are supported by the contact buffer
implementation.
"""
if not attributes:
return
invalid = sorted(set(attributes).difference(cls.EXTENDED_ATTRIBUTES))
if invalid:
allowed = ", ".join(sorted(cls.EXTENDED_ATTRIBUTES))
bad = ", ".join(invalid)
raise ValueError(f"Unknown extended contact attribute(s): {bad}. Allowed: {allowed}.")
[docs]
def __init__(
self,
rigid_contact_max: int,
*,
requires_grad: bool = False,
device: SapDeviceLike = None,
per_contact_shape_properties: bool = False,
clear_buffers: bool = False,
requested_attributes: set[str] | None = None,
) -> None:
self.per_contact_shape_properties = per_contact_shape_properties
self.clear_buffers = clear_buffers
requested_attributes = requested_attributes or set()
self.validate_extended_attributes(tuple(requested_attributes))
with wp.ScopedDevice(device):
self._counter_array = wp.zeros(1, dtype=wp.int32)
self.rigid_contact_count = self._counter_array[0:1]
self.rigid_contact_point_id = wp.zeros(rigid_contact_max, dtype=wp.int32)
self.rigid_contact_shape0 = wp.full(rigid_contact_max, -1, dtype=wp.int32)
self.rigid_contact_shape1 = wp.full(rigid_contact_max, -1, dtype=wp.int32)
self.rigid_contact_point0 = wp.zeros(rigid_contact_max, dtype=wp.vec3)
self.rigid_contact_point1 = wp.zeros(rigid_contact_max, dtype=wp.vec3)
self.rigid_contact_offset0 = wp.zeros(rigid_contact_max, dtype=wp.vec3)
self.rigid_contact_offset1 = wp.zeros(rigid_contact_max, dtype=wp.vec3)
self.rigid_contact_normal = wp.zeros(rigid_contact_max, dtype=wp.vec3)
self.rigid_contact_margin0 = wp.zeros(rigid_contact_max, dtype=wp.float32)
self.rigid_contact_margin1 = wp.zeros(rigid_contact_max, dtype=wp.float32)
self.rigid_contact_tids = wp.full(rigid_contact_max, -1, dtype=wp.int32)
self.rigid_contact_force = wp.zeros(rigid_contact_max, dtype=wp.vec3)
if self.per_contact_shape_properties:
self.rigid_contact_stiffness = wp.zeros(rigid_contact_max, dtype=wp.float32)
self.rigid_contact_damping = wp.zeros(rigid_contact_max, dtype=wp.float32)
self.rigid_contact_friction = wp.zeros(rigid_contact_max, dtype=wp.float32)
else:
self.rigid_contact_stiffness = None
self.rigid_contact_damping = None
self.rigid_contact_friction = None
self.force: wp.array | None = None
if "force" in requested_attributes:
self.force = wp.zeros(rigid_contact_max, dtype=wp.spatial_vector, requires_grad=requires_grad)
self.requires_grad = requires_grad
self.rigid_contact_max = rigid_contact_max
def clear(self) -> None:
"""Reset the active contact count and, when configured, clear stale contact payload buffers."""
self._counter_array.zero_()
if self.clear_buffers:
self.rigid_contact_shape0.fill_(-1)
self.rigid_contact_shape1.fill_(-1)
self.rigid_contact_tids.fill_(-1)
self.rigid_contact_force.zero_()
if self.force is not None:
self.force.zero_()
if self.per_contact_shape_properties:
self.rigid_contact_stiffness.zero_()
self.rigid_contact_damping.zero_()
self.rigid_contact_friction.zero_()
@property
def device(self):
"""Return the Warp device that owns the contact counter and payload arrays."""
return self.rigid_contact_count.device
@property
def has_rigid_contacts(self) -> bool:
"""Return True because this buffer always owns rigid-contact storage."""
return self.rigid_contact_count is not None
__all__ = ["SapContacts"]