|
1 | 1 | # SPDX-FileCopyrightText: Copyright (c) 2026 The Newton Developers |
2 | 2 | # SPDX-License-Identifier: Apache-2.0 |
3 | 3 |
|
4 | | -"""Shared Warp kernels for :class:`~newton.controllers.ControllerJointImpedance`.""" |
| 4 | +"""Shared Warp kernels for the joint impedance controllers. |
5 | 5 |
|
6 | | -import numpy as np |
7 | | -import warp as wp |
| 6 | +Every 1-D buffer here is compact — one entry per controlled DOF, robot 0's DOFs |
| 7 | +first, then robot 1's — so every kernel is a flat 1-D launch with no padding to |
| 8 | +skip. The exception is the mass matrix, which :func:`~newton.eval_mass_matrix` |
| 9 | +produces as one square block per articulation: the multiply kernel stays flat |
| 10 | +and indexes into those blocks, while the gather kernel launches over them. |
| 11 | +""" |
| 12 | + |
| 13 | +from __future__ import annotations |
8 | 14 |
|
| 15 | +import warp as wp |
9 | 16 |
|
10 | | -def _idx_max(idx: wp.array[wp.uint32]) -> int: |
11 | | - """Return the minimum flat-array size needed to hold all indices.""" |
12 | | - return int(np.max(idx.numpy())) + 1 |
| 17 | +from ....core.types import Devicelike |
13 | 18 |
|
14 | 19 |
|
15 | 20 | @wp.kernel |
16 | 21 | def _pd_term_kernel( |
17 | | - joint_q: wp.array2d[wp.float32], # (robot_count, max_dofs) |
18 | | - joint_qd: wp.array2d[wp.float32], # (robot_count, max_dofs) |
19 | | - joint_q_des: wp.array2d[wp.float32], # (robot_count, max_dofs) |
20 | | - joint_qd_des: wp.array2d[wp.float32], # (robot_count, max_dofs) |
21 | | - stiffness: wp.array2d[wp.float32], # (robot_count, max_dofs) |
22 | | - damping: wp.array2d[wp.float32], # (robot_count, max_dofs) |
23 | | - dofs_per_robot: wp.array[wp.int32], # (robot_count,) |
24 | | - out: wp.array2d[wp.float32], # (robot_count, max_dofs) |
| 22 | + joint_q: wp.array[wp.float32], # (total_controlled_dofs,) |
| 23 | + joint_qd: wp.array[wp.float32], # (total_controlled_dofs,) |
| 24 | + joint_q_des: wp.array[wp.float32], # (total_controlled_dofs,) |
| 25 | + joint_qd_des: wp.array[wp.float32], # (total_controlled_dofs,) |
| 26 | + stiffness: wp.array[wp.float32], # (total_controlled_dofs,) |
| 27 | + damping: wp.array[wp.float32], # (total_controlled_dofs,) |
| 28 | + out: wp.array[wp.float32], # (total_controlled_dofs,) |
25 | 29 | ): |
26 | | - robot, dof = wp.tid() |
27 | | - if dof >= dofs_per_robot[robot]: |
28 | | - return |
29 | | - out[robot, dof] = stiffness[robot, dof] * (joint_q_des[robot, dof] - joint_q[robot, dof]) + damping[robot, dof] * ( |
30 | | - joint_qd_des[robot, dof] - joint_qd[robot, dof] |
31 | | - ) |
| 30 | + dof = wp.tid() |
| 31 | + out[dof] = stiffness[dof] * (joint_q_des[dof] - joint_q[dof]) + damping[dof] * (joint_qd_des[dof] - joint_qd[dof]) |
32 | 32 |
|
33 | 33 |
|
34 | 34 | @wp.kernel |
35 | 35 | def _add_term_kernel( |
36 | | - term: wp.array2d[wp.float32], # (robot_count, max_dofs) |
37 | | - dofs_per_robot: wp.array[wp.int32], # (robot_count,) |
38 | | - tau: wp.array2d[wp.float32], # (robot_count, max_dofs) |
| 36 | + term: wp.array[wp.float32], # (total_controlled_dofs,) |
| 37 | + tau: wp.array[wp.float32], # (total_controlled_dofs,) |
39 | 38 | ): |
40 | | - robot, dof = wp.tid() |
41 | | - if dof >= dofs_per_robot[robot]: |
42 | | - return |
43 | | - tau[robot, dof] = tau[robot, dof] + term[robot, dof] |
| 39 | + dof = wp.tid() |
| 40 | + tau[dof] = tau[dof] + term[dof] |
44 | 41 |
|
45 | 42 |
|
46 | 43 | @wp.kernel |
47 | 44 | def _mass_matrix_multiply_kernel( |
48 | | - M: wp.array3d[wp.float32], # (robot_count, max_dofs, max_dofs) |
49 | | - vec: wp.array2d[wp.float32], # (robot_count, max_dofs) |
50 | | - dofs_per_robot: wp.array[wp.int32], # (robot_count,) |
51 | | - out: wp.array2d[wp.float32], # (robot_count, max_dofs) |
| 45 | + mass_matrix: wp.array3d[wp.float32], # (controlled_robot_count, max_controlled_dofs, max_controlled_dofs) |
| 46 | + vec: wp.array[wp.float32], # (total_controlled_dofs,) |
| 47 | + robot_of_dof: wp.array[wp.int32], # (total_controlled_dofs,) -> owning robot |
| 48 | + slot_of_dof: wp.array[wp.int32], # (total_controlled_dofs,) -> row within that robot's block |
| 49 | + dof_offsets: wp.array[wp.int32], # (controlled_robot_count,) -> first flat DOF of each robot |
| 50 | + controlled_dofs_per_robot: wp.array[wp.int32], # (controlled_robot_count,) |
| 51 | + out: wp.array[wp.float32], # (total_controlled_dofs,) |
52 | 52 | ): |
53 | | - robot, dof = wp.tid() |
54 | | - if dof >= dofs_per_robot[robot]: |
55 | | - return |
| 53 | + dof = wp.tid() |
| 54 | + robot = robot_of_dof[dof] |
| 55 | + row = slot_of_dof[dof] |
| 56 | + row_base = dof_offsets[robot] |
56 | 57 | acc = float(0.0) |
57 | | - for col in range(dofs_per_robot[robot]): |
58 | | - acc = acc + M[robot, dof, col] * vec[robot, col] |
59 | | - out[robot, dof] = acc |
| 58 | + for col in range(controlled_dofs_per_robot[robot]): |
| 59 | + acc = acc + mass_matrix[robot, row, col] * vec[row_base + col] |
| 60 | + out[dof] = acc |
60 | 61 |
|
61 | 62 |
|
62 | 63 | @wp.kernel |
63 | | -def _gather_dof_flat_kernel( |
64 | | - src: wp.array[wp.float32], # flat sim array |
65 | | - indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot, no padding |
66 | | - dst: wp.array[wp.float32], # flat output (total_dofs,) |
| 64 | +def _gather_mass_matrix_blocks_kernel( |
| 65 | + model_mass_matrix: wp.array3d[wp.float32], # (model_robot_count, model_max_dofs, model_max_dofs) |
| 66 | + model_robot_index: wp.array[wp.int32], # (controlled_robot_count,) -> that robot's index in the model |
| 67 | + local_dof_idx: wp.array2d[wp.int32], # (controlled_robot_count, max_controlled_dofs) -> DOF index within its robot |
| 68 | + controlled_dofs_per_robot: wp.array[wp.int32], # (controlled_robot_count,) |
| 69 | + out: wp.array3d[wp.float32], # (controlled_robot_count, max_controlled_dofs, max_controlled_dofs) |
67 | 70 | ): |
68 | | - flat = wp.tid() |
69 | | - dst[flat] = src[indices[flat]] |
| 71 | + robot, row, col = wp.tid() |
| 72 | + if row >= controlled_dofs_per_robot[robot] or col >= controlled_dofs_per_robot[robot]: |
| 73 | + return |
| 74 | + model_robot = model_robot_index[robot] |
| 75 | + out[robot, row, col] = model_mass_matrix[model_robot, local_dof_idx[robot, row], local_dof_idx[robot, col]] |
| 76 | + |
| 77 | + |
| 78 | +# wp.copy is not recordable under APIC graph capture when either side is |
| 79 | +# non-contiguous, which every indexed-view port is. These two kernels do the |
| 80 | +# same work in a form that captures and serialises. Both controllers launch them |
| 81 | +# at their own port length: one entry per controlled DOF for a compact port, one |
| 82 | +# per model coordinate or DOF for the model-based controller's whole-model ports. |
70 | 83 |
|
71 | 84 |
|
72 | 85 | @wp.kernel |
73 | | -def _gather_dof_kernel( |
74 | | - src: wp.array[wp.float32], # flat sim array |
75 | | - dof_indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot indices |
76 | | - dof_offsets: wp.array[wp.int32], # (robot_count,) — start of each robot in dof_indices |
77 | | - dofs_per_robot: wp.array[wp.int32], # (robot_count,) |
78 | | - dst: wp.array2d[wp.float32], # (robot_count, max_dofs) |
| 86 | +def _gather_port_kernel( |
| 87 | + port: wp.indexedarray[wp.float32], # view of a simulation-sized array |
| 88 | + out: wp.array[wp.float32], # one entry per element the view addresses |
79 | 89 | ): |
80 | | - robot, dof = wp.tid() |
81 | | - if dof >= dofs_per_robot[robot]: |
82 | | - return |
83 | | - dst[robot, dof] = src[dof_indices[dof_offsets[robot] + dof]] |
| 90 | + dof = wp.tid() |
| 91 | + out[dof] = port[dof] |
84 | 92 |
|
85 | 93 |
|
86 | 94 | @wp.kernel |
87 | | -def _scatter_dof_kernel( |
88 | | - src: wp.array2d[wp.float32], # (robot_count, max_dofs) |
89 | | - dof_indices: wp.array[wp.uint32], # (total_dofs,) — concatenated per-robot indices |
90 | | - dof_offsets: wp.array[wp.int32], # (robot_count,) — start of each robot in dof_indices |
91 | | - dofs_per_robot: wp.array[wp.int32], # (robot_count,) |
92 | | - dst: wp.array[wp.float32], # flat sim output |
| 95 | +def _gather_mass_matrix_port_kernel( |
| 96 | + port: wp.indexedarray(dtype=wp.float32, ndim=3), # view selecting robots from a larger set of blocks |
| 97 | + out: wp.array3d[wp.float32], # (controlled_robot_count, max_controlled_dofs, max_controlled_dofs) |
93 | 98 | ): |
94 | | - robot, dof = wp.tid() |
95 | | - if dof >= dofs_per_robot[robot]: |
| 99 | + robot, row, col = wp.tid() |
| 100 | + out[robot, row, col] = port[robot, row, col] |
| 101 | + |
| 102 | + |
| 103 | +@wp.kernel |
| 104 | +def _scatter_port_kernel( |
| 105 | + values: wp.array[wp.float32], # one entry per element the view addresses |
| 106 | + port: wp.indexedarray[wp.float32], # view of a simulation-sized array |
| 107 | +): |
| 108 | + dof = wp.tid() |
| 109 | + port[dof] = values[dof] |
| 110 | + |
| 111 | + |
| 112 | +def _read_port( |
| 113 | + port: wp.array[wp.float32] | wp.array3d[wp.float32] | wp.indexedarray[wp.float32], |
| 114 | + buffer: wp.array[wp.float32] | wp.array3d[wp.float32], |
| 115 | + shape: int | tuple[int, ...], |
| 116 | + device: Devicelike, |
| 117 | +) -> None: |
| 118 | + """Copy a bound port into an internal buffer, whatever it is bound to. |
| 119 | +
|
| 120 | + A view has to go through a kernel: :func:`warp.copy` is not recordable under |
| 121 | + APIC graph capture when either side is non-contiguous, so using it here would |
| 122 | + make a controller that reports ``is_graphable()`` fail to export. |
| 123 | +
|
| 124 | + Args: |
| 125 | + port: The caller-bound port, a :class:`warp.array` or a view of one. |
| 126 | + 1-D for a compact or whole-model port, 3-D for a mass matrix; a 3-D |
| 127 | + view has no bracket spelling and is |
| 128 | + ``wp.indexedarray(dtype=wp.float32, ndim=3)``. |
| 129 | + buffer: Destination, matching ``port`` in shape and dtype. |
| 130 | + shape: Launch shape — the length for a 1-D port, ``(robots, rows, cols)`` |
| 131 | + for a mass matrix. |
| 132 | + device: Device to launch on. |
| 133 | + """ |
| 134 | + if not isinstance(port, wp.indexedarray): |
| 135 | + wp.copy(buffer, port) |
96 | 136 | return |
97 | | - dst[dof_indices[dof_offsets[robot] + dof]] = src[robot, dof] |
| 137 | + |
| 138 | + # A kernel parameter's dimensionality is part of its type, so a view needs |
| 139 | + # the kernel that matches its rank. |
| 140 | + kernel = _gather_port_kernel if port.ndim == 1 else _gather_mass_matrix_port_kernel |
| 141 | + wp.launch(kernel, dim=shape, inputs=[port], outputs=[buffer], device=device) |
0 commit comments