class Adjacency(TensorizedRelativeState):
"""
Pairwise adjacency state.
S = number of scenes
N = number of objects with Adjacency state
VALUES has shape (S, N, N, 18) bool: +Z, -Z, then the positive and negative
directions of the eight unique legacy horizontal axes. VALUES[s, a, b, k] is True iff
object b is adjacent to object a in direction k (from a's AABB center).
Diagonal and cross-scene cells are always False.
Cloth is excluded via is_compatible (no collision mesh to ray-cast against).
"""
# Wp kernel inputs (single source of truth).
_aabb_obj_idxs = None # wp.array (N_adj,) int32
_link_to_obj_idx = None # wp.array (L_total,) int32
_link_to_scene_idx = None # wp.array (L_total,) int32
_directions = None # wp.array2d (18, 3) float32
_max_distances = None # wp.array (18,) float32
_output = None # wp.array4d (S, N_adj, N_adj, 18) int32 — atomic_max target
@classproperty
def value_shape(cls):
return (_ADJ_AXIS_COUNT,)
@classproperty
def value_type(cls):
return th.bool
@classproperty
def value_name(cls):
return "adjacency"
@classmethod
def get_dependencies(cls):
deps = super().get_dependencies()
deps.add(AABB)
return deps
@classmethod
def is_compatible(cls, obj, **kwargs):
compatible, reason = super().is_compatible(obj, **kwargs)
if not compatible:
return compatible, reason
# Cloth has no collision_mesh_cpu_data — exclude as both origin and target.
# TODO: revisit when cloth gains a queryable mesh proxy.
if obj.prim_type == PrimType.CLOTH:
return False, "Adjacency does not support cloth objects"
return True, None
@classmethod
def global_initialize(cls):
super().global_initialize()
# Build the constant axis tables (directions + per-axis max distance).
directions_cpu, max_distances_cpu = _build_adjacency_axis_tables()
cls._directions = lazy.isaacsim.core.utils.warp.tensor.create_tensor_from_list(
directions_cpu, "float32", device="cuda"
)
cls._max_distances = lazy.isaacsim.core.utils.warp.tensor.create_tensor_from_list(
max_distances_cpu, "float32", device="cuda"
)
@classmethod
def initialize_view(cls):
super().initialize_view()
S = len(cls.IDX_OBJS)
N = len(cls.OBJ_IDXS)
# Build aabb_obj_idxs: maps Adjacency-N → AABB-N (or -1 if unknown to AABB).
if N > 0 and AABB.OBJ_IDXS is not None:
aabb_obj_idxs_cpu = th.full((N,), -1, dtype=th.int32)
for rel_path, adj_idx in cls.OBJ_IDXS.items():
aabb_obj_idxs_cpu[adj_idx] = AABB.OBJ_IDXS.get(rel_path, -1)
cls._aabb_obj_idxs = lazy.isaacsim.core.utils.warp.tensor.create_tensor_from_list(
aabb_obj_idxs_cpu, "int32", device="cuda"
)
else:
cls._aabb_obj_idxs = None
# Build link_to_obj_idx / link_to_scene_idx tables, length = N_links_total in
# RigidBodyViewAPI (whether or not those links belong to Adjacency-tracked objects).
# Untracked link slots stay at -1; the kernel skips them.
if RigidBodyViewAPI._PATH_TO_IDX:
L_total = len(RigidBodyViewAPI._PATH_TO_IDX)
link_to_obj_cpu = th.full((L_total,), -1, dtype=th.int32)
link_to_scene_cpu = th.full((L_total,), -1, dtype=th.int32)
for s_idx, scene_row in enumerate(cls.IDX_OBJS):
for adj_idx, obj in enumerate(scene_row):
if obj is None:
continue
for link in obj.links.values():
flat_idx = RigidBodyViewAPI.get_flat_idx(link.prim_path)
if flat_idx is None:
continue
link_to_obj_cpu[flat_idx] = adj_idx
link_to_scene_cpu[flat_idx] = s_idx
cls._link_to_obj_idx = lazy.isaacsim.core.utils.warp.tensor.create_tensor_from_list(
link_to_obj_cpu, "int32", device="cuda"
)
cls._link_to_scene_idx = lazy.isaacsim.core.utils.warp.tensor.create_tensor_from_list(
link_to_scene_cpu, "int32", device="cuda"
)
else:
cls._link_to_obj_idx = None
cls._link_to_scene_idx = None
# Allocate the int32 scratch (S, N, N, K) used as the atomic_max target.
if S > 0 and N > 0:
cls._output = wp.zeros((S, N, N, _ADJ_AXIS_COUNT), dtype=wp.int32, device="cuda")
else:
cls._output = None
@classmethod
def _update_values(cls, values):
# All required handles must be live; otherwise nothing to do this step.
if (
cls._output is None
or cls.VALUES_WP is None
or AABB.VALUES_WP is None
or cls._aabb_obj_idxs is None
or RigidBodyViewAPI.LINK_MESH_IDS is None
or RigidBodyViewAPI.POSE_MATRICES is None
or cls._link_to_obj_idx is None
or cls._link_to_scene_idx is None
):
return
S = values.shape[0]
N = values.shape[1]
K = _ADJ_AXIS_COUNT
L = RigidBodyViewAPI.LINK_MESH_IDS.shape[0]
if S == 0 or N == 0 or L == 0:
return
# 1. Zero the int32 scratch via CUDA memset (graph-capturable for contiguous wp.array).
cls._output.zero_()
# 2. Ray-cast against each link's mesh; atomic_max into scratch on hit.
wp.launch(
kernel=_adjacency_ray_cast_kernel,
dim=(S, N, L, K),
inputs=[
AABB.VALUES_WP,
cls._aabb_obj_idxs,
cls._directions,
cls._max_distances,
RigidBodyViewAPI.LINK_MESH_IDS,
RigidBodyViewAPI.POSE_MATRICES,
cls._link_to_obj_idx,
cls._link_to_scene_idx,
cls._output,
],
device="cuda",
)
# 3. Convert int32 scratch → uint8 bool VALUES.
wp.launch(
kernel=_adjacency_finalize_kernel,
dim=(S, N, N, K),
inputs=[cls._output, cls.VALUES_WP],
device="cuda",
)