| """PyTorch and USD/Gf utility functions for IsaacGym-based RL. |
| |
| Provides quaternion arithmetic (multiply, apply, rotate, conjugate, inverse |
| transform, combine), Euler-angle conversions, tensor helpers (clamp, scale, |
| unscale, random float/direction), and a USD ``Gf.Matrix4d`` construction |
| helper. Most functions are compiled with ``@torch.jit.script`` for |
| performance. |
| |
| SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. |
| SPDX-License-Identifier: Apache-2.0 |
| """ |
|
|
| import numpy as np |
| from pxr import Gf |
| import torch |
|
|
|
|
| def set_env_attr(self, attr_name, attr_val, env_ids): |
| """Set a per-environment attribute on an env object for the given env ids. |
| |
| If the attribute already exists it is indexed by ``env_ids``; otherwise |
| the value is set as a plain attribute (useful for first-time initialisation |
| before the buffer exists). |
| |
| Args: |
| self: Environment object that owns the attribute. |
| attr_name: Name of the attribute to set. |
| attr_val: Value(s) to assign. |
| env_ids: Integer indices of the environments to update. |
| """ |
| if hasattr(self, attr_name): |
| getattr(self, attr_name)[env_ids] = attr_val |
| else: |
| setattr(self, attr_name, attr_val) |
|
|
|
|
| def to_torch( |
| x: np.ndarray | torch.Tensor | list, device: torch.device | str, dtype=None, requires_grad=False |
| ): |
| """Convert a list, NumPy array, or Tensor to a ``torch.Tensor`` on ``device``. |
| |
| Args: |
| x: Input data. Lists are first converted to ``np.ndarray``. |
| device: Target device (e.g. ``"cuda:0"`` or ``torch.device("cpu")``). |
| dtype: Desired dtype. Defaults to ``torch.float`` for non-Tensor |
| inputs; for existing Tensors the current dtype is preserved when |
| ``dtype`` is ``None``. |
| requires_grad: Whether the result should track gradients. |
| |
| Returns: |
| A ``torch.Tensor`` on ``device`` with the requested dtype and |
| ``requires_grad`` setting. |
| """ |
| |
| if isinstance(x, list): |
| x = np.array(x) |
|
|
| if not isinstance(x, torch.Tensor): |
| if dtype is None: |
| dtype = torch.float |
| x = torch.tensor(x, device=device, dtype=dtype, requires_grad=requires_grad) |
| else: |
| |
| if dtype is None: |
| x = x.to(device=device) |
| if x.requires_grad != requires_grad: |
| x = x.detach().requires_grad_(requires_grad) |
| else: |
| x = x.to(dtype=dtype, device=device) |
| if x.requires_grad != requires_grad: |
| x = x.detach().requires_grad_(requires_grad) |
| return x |
|
|
|
|
| @torch.jit.script |
| def quat_mul(a, b): |
| """Multiply two batches of quaternions (xyzw convention). |
| |
| Args: |
| a: Quaternion tensor of shape ``(..., 4)`` in xyzw order. |
| b: Quaternion tensor of the same shape as ``a``. |
| |
| Returns: |
| Product quaternion tensor of the same shape as ``a``. |
| """ |
| assert a.shape == b.shape |
| shape = a.shape |
| a = a.reshape(-1, 4) |
| b = b.reshape(-1, 4) |
|
|
| x1, y1, z1, w1 = a[:, 0], a[:, 1], a[:, 2], a[:, 3] |
| x2, y2, z2, w2 = b[:, 0], b[:, 1], b[:, 2], b[:, 3] |
| ww = (z1 + x1) * (x2 + y2) |
| yy = (w1 - y1) * (w2 + z2) |
| zz = (w1 + y1) * (w2 - z2) |
| xx = ww + yy + zz |
| qq = 0.5 * (xx + (z1 - x1) * (x2 - y2)) |
| w = qq - ww + (z1 - y1) * (y2 - z2) |
| x = qq - xx + (x1 + w1) * (x2 + w2) |
| y = qq - yy + (w1 - x1) * (y2 + z2) |
| z = qq - zz + (z1 + y1) * (w2 - x2) |
|
|
| quat = torch.stack([x, y, z, w], dim=-1).view(shape) |
|
|
| return quat |
|
|
|
|
| @torch.jit.script |
| def normalize(x, eps: float = 1e-9): |
| """L2-normalize a tensor along its last dimension. |
| |
| Args: |
| x: Input tensor of any shape. |
| eps: Minimum norm value to clamp to, preventing division by zero. |
| |
| Returns: |
| Tensor of the same shape as ``x`` with unit L2 norm along the last dim. |
| """ |
| return x / x.norm(p=2, dim=-1).clamp(min=eps, max=None).unsqueeze(-1) |
|
|
|
|
| @torch.jit.script |
| def quat_apply(a, b): |
| """Rotate a 3-D vector by a quaternion (xyzw convention). |
| |
| Args: |
| a: Quaternion tensor of shape ``(..., 4)`` in xyzw order. |
| b: Vector tensor of shape ``(..., 3)``. |
| |
| Returns: |
| Rotated vector tensor of the same shape as ``b``. |
| """ |
| shape = b.shape |
| a = a.reshape(-1, 4) |
| b = b.reshape(-1, 3) |
| xyz = a[:, :3] |
| t = xyz.cross(b, dim=-1) * 2 |
| return (b + a[:, 3:] * t + xyz.cross(t, dim=-1)).view(shape) |
|
|
|
|
| @torch.jit.script |
| def quat_rotate(q, v): |
| """Rotate batched 3-D vectors by batched quaternions (xyzw convention). |
| |
| Uses the expanded form ``v' = 2(q_w^2 - 0.5)v + 2(q·v)q + 2q_w(q×v)``. |
| |
| Args: |
| q: Quaternion tensor of shape ``(N, 4)`` in xyzw order. |
| v: Vector tensor of shape ``(N, 3)``. |
| |
| Returns: |
| Rotated vector tensor of shape ``(N, 3)``. |
| """ |
| shape = q.shape |
| q_w = q[:, -1] |
| q_vec = q[:, :3] |
| a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1) |
| b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0 |
| c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0 |
| return a + b + c |
|
|
|
|
| |
| def quat_rotate_inverse(q, v): |
| """Rotate batched 3-D vectors by the *inverse* of batched quaternions. |
| |
| Equivalent to rotating by the conjugate quaternion (i.e. the transpose of |
| the rotation matrix). |
| |
| Args: |
| q: Quaternion tensor of shape ``(N, 4)`` in xyzw order. |
| v: Vector tensor of shape ``(N, 3)``. |
| |
| Returns: |
| Inversely-rotated vector tensor of shape ``(N, 3)``. |
| """ |
| shape = q.shape |
| q_w = q[:, -1] |
| q_vec = q[:, :3] |
| a = v * (2.0 * q_w**2 - 1.0).unsqueeze(-1) |
| b = torch.cross(q_vec, v, dim=-1) * q_w.unsqueeze(-1) * 2.0 |
| c = q_vec * torch.bmm(q_vec.view(shape[0], 1, 3), v.view(shape[0], 3, 1)).squeeze(-1) * 2.0 |
| return a - b + c |
|
|
|
|
| @torch.jit.script |
| def quat_conjugate(a): |
| """Return the conjugate of a batch of quaternions (xyzw convention). |
| |
| The conjugate negates the imaginary (xyz) part while keeping the real (w) |
| part, yielding the inverse rotation for unit quaternions. |
| |
| Args: |
| a: Quaternion tensor of shape ``(..., 4)`` in xyzw order. |
| |
| Returns: |
| Conjugate quaternion tensor of the same shape. |
| """ |
| shape = a.shape |
| a = a.reshape(-1, 4) |
| return torch.cat((-a[:, :3], a[:, -1:]), dim=-1).view(shape) |
|
|
|
|
| @torch.jit.script |
| def quat_unit(a): |
| """Normalize a batch of quaternions to unit length. |
| |
| Args: |
| a: Quaternion tensor of shape ``(..., 4)``. |
| |
| Returns: |
| Unit-length quaternion tensor of the same shape. |
| """ |
| return normalize(a) |
|
|
|
|
| @torch.jit.script |
| def quat_from_angle_axis(angle, axis): |
| """Construct a unit quaternion from an angle-axis representation. |
| |
| Args: |
| angle: Rotation angle in radians, shape ``(N,)``. |
| axis: Rotation axes, shape ``(N, 3)``. Need not be unit vectors. |
| |
| Returns: |
| Unit quaternion tensor of shape ``(N, 4)`` in xyzw order. |
| """ |
| theta = (angle / 2).unsqueeze(-1) |
| xyz = normalize(axis) * theta.sin() |
| w = theta.cos() |
| return quat_unit(torch.cat([xyz, w], dim=-1)) |
|
|
|
|
| @torch.jit.script |
| def normalize_angle(x): |
| """Wrap angles into the range ``(-pi, pi]``. |
| |
| Args: |
| x: Angle tensor (radians), any shape. |
| |
| Returns: |
| Wrapped angle tensor of the same shape. |
| """ |
| return torch.atan2(torch.sin(x), torch.cos(x)) |
|
|
|
|
| @torch.jit.script |
| def tf_inverse(q, t): |
| """Compute the inverse of a rigid transform (q, t). |
| |
| Args: |
| q: Rotation quaternion, shape ``(N, 4)`` in xyzw order. |
| t: Translation vector, shape ``(N, 3)``. |
| |
| Returns: |
| Tuple ``(q_inv, t_inv)`` representing the inverse transform. |
| """ |
| q_inv = quat_conjugate(q) |
| return q_inv, -quat_apply(q_inv, t) |
|
|
|
|
| @torch.jit.script |
| def tf_apply(q, t, v): |
| """Apply a rigid transform (q, t) to a batch of points v. |
| |
| Computes ``R(q) * v + t``. |
| |
| Args: |
| q: Rotation quaternion, shape ``(N, 4)`` in xyzw order. |
| t: Translation vector, shape ``(N, 3)``. |
| v: Points to transform, shape ``(N, 3)``. |
| |
| Returns: |
| Transformed points of shape ``(N, 3)``. |
| """ |
| return quat_apply(q, v) + t |
|
|
|
|
| @torch.jit.script |
| def tf_vector(q, v): |
| """Rotate a vector by a quaternion (no translation). |
| |
| Args: |
| q: Quaternion, shape ``(..., 4)`` in xyzw order. |
| v: Vector to rotate, shape ``(..., 3)``. |
| |
| Returns: |
| Rotated vector of the same shape as ``v``. |
| """ |
| return quat_apply(q, v) |
|
|
|
|
| @torch.jit.script |
| def tf_combine(q1, t1, q2, t2): |
| """Compose two rigid transforms T1 followed by T2. |
| |
| Computes the combined rotation ``q1 * q2`` and the combined translation |
| ``R(q1) * t2 + t1``. |
| |
| Args: |
| q1: First rotation quaternion, shape ``(N, 4)`` in xyzw order. |
| t1: First translation, shape ``(N, 3)``. |
| q2: Second rotation quaternion, shape ``(N, 4)`` in xyzw order. |
| t2: Second translation, shape ``(N, 3)``. |
| |
| Returns: |
| Tuple ``(q_combined, t_combined)`` of the composed transform. |
| """ |
| return quat_mul(q1, q2), quat_apply(q1, t2) + t1 |
|
|
|
|
| @torch.jit.script |
| def get_basis_vector(q, v): |
| """Rotate a basis vector ``v`` by quaternion ``q``. |
| |
| Args: |
| q: Quaternion tensor, shape ``(N, 4)`` in xyzw order. |
| v: Basis vector, shape ``(N, 3)``. |
| |
| Returns: |
| Rotated vector of shape ``(N, 3)``. |
| """ |
| return quat_rotate(q, v) |
|
|
|
|
| def get_axis_params(value, axis_idx, x_value=0.0, dtype=np.float64, n_dims=3): |
| """Construct a parameter list for a USD ``Vec`` along a specific axis. |
| |
| Creates an n-dimensional vector that is ``value`` along ``axis_idx`` and |
| zero everywhere else, then overrides index 0 with ``x_value``. |
| |
| Args: |
| value: Scalar value to place at position ``axis_idx``. |
| axis_idx: Index of the axis to set to ``value``. |
| x_value: Value to assign to index 0 after the axis fill. |
| dtype: NumPy dtype of the output array. |
| n_dims: Total number of dimensions in the vector. |
| |
| Returns: |
| List of ``n_dims`` floats suitable for passing to a USD ``Vec`` |
| constructor. |
| """ |
| zs = np.zeros((n_dims,)) |
| assert axis_idx < n_dims, "the axis dim should be within the vector dimensions" |
| zs[axis_idx] = 1.0 |
| params = np.where(zs == 1.0, value, zs) |
| params[0] = x_value |
| return list(params.astype(dtype)) |
|
|
|
|
| @torch.jit.script |
| def copysign(a, b): |
| |
| """Copy the sign of tensor ``b`` onto scalar ``a``. |
| |
| Returns a tensor of the same shape as ``b`` with magnitude ``|a|`` and |
| sign matching each element of ``b``. |
| |
| Args: |
| a: Scalar magnitude. |
| b: Tensor whose signs are copied, shape ``(N,)``. |
| |
| Returns: |
| Tensor of shape ``(N,)`` equal to ``|a| * sign(b)``. |
| """ |
| a = torch.tensor(a, device=b.device, dtype=torch.float).repeat(b.shape[0]) |
| return torch.abs(a) * torch.sign(b) |
|
|
|
|
| @torch.jit.script |
| def get_euler_xyz(q): |
| """Extract Euler XYZ angles (roll, pitch, yaw) from a batch of quaternions. |
| |
| Uses the standard ZYX intrinsic decomposition. Handles the gimbal-lock |
| singularity at |sinp| >= 1 via ``copysign``. |
| |
| Args: |
| q: Quaternion tensor of shape ``(N, 4)`` in xyzw order. |
| |
| Returns: |
| Tuple ``(roll, pitch, yaw)`` each of shape ``(N,)`` in radians, |
| mapped to ``[0, 2*pi)``. |
| """ |
| qx, qy, qz, qw = 0, 1, 2, 3 |
| |
| sinr_cosp = 2.0 * (q[:, qw] * q[:, qx] + q[:, qy] * q[:, qz]) |
| cosr_cosp = ( |
| q[:, qw] * q[:, qw] - q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] + q[:, qz] * q[:, qz] |
| ) |
| roll = torch.atan2(sinr_cosp, cosr_cosp) |
|
|
| |
| sinp = 2.0 * (q[:, qw] * q[:, qy] - q[:, qz] * q[:, qx]) |
| pitch = torch.where(torch.abs(sinp) >= 1, copysign(np.pi / 2.0, sinp), torch.asin(sinp)) |
|
|
| |
| siny_cosp = 2.0 * (q[:, qw] * q[:, qz] + q[:, qx] * q[:, qy]) |
| cosy_cosp = ( |
| q[:, qw] * q[:, qw] + q[:, qx] * q[:, qx] - q[:, qy] * q[:, qy] - q[:, qz] * q[:, qz] |
| ) |
| yaw = torch.atan2(siny_cosp, cosy_cosp) |
|
|
| return roll % (2 * np.pi), pitch % (2 * np.pi), yaw % (2 * np.pi) |
|
|
|
|
| @torch.jit.script |
| def quat_from_euler_xyz(roll, pitch, yaw): |
| """Construct unit quaternions from intrinsic XYZ Euler angles. |
| |
| Args: |
| roll: Rotation around X axis (radians), shape ``(N,)``. |
| pitch: Rotation around Y axis (radians), shape ``(N,)``. |
| yaw: Rotation around Z axis (radians), shape ``(N,)``. |
| |
| Returns: |
| Unit quaternion tensor of shape ``(N, 4)`` in xyzw order. |
| """ |
| cy = torch.cos(yaw * 0.5) |
| sy = torch.sin(yaw * 0.5) |
| cr = torch.cos(roll * 0.5) |
| sr = torch.sin(roll * 0.5) |
| cp = torch.cos(pitch * 0.5) |
| sp = torch.sin(pitch * 0.5) |
|
|
| qw = cy * cr * cp + sy * sr * sp |
| qx = cy * sr * cp - sy * cr * sp |
| qy = cy * cr * sp + sy * sr * cp |
| qz = sy * cr * cp - cy * sr * sp |
|
|
| return torch.stack([qx, qy, qz, qw], dim=-1) |
|
|
|
|
| @torch.jit.script |
| def torch_rand_float(lower, upper, shape, device): |
| |
| """Sample uniform random floats in ``[lower, upper)``. |
| |
| Args: |
| lower: Lower bound of the uniform distribution. |
| upper: Upper bound of the uniform distribution. |
| shape: Output shape as a 2-tuple ``(rows, cols)``. |
| device: Target device string (e.g. ``"cuda:0"``). |
| |
| Returns: |
| Float tensor of the given shape on ``device``. |
| """ |
| return (upper - lower) * torch.rand(*shape, device=device) + lower |
|
|
|
|
| @torch.jit.script |
| def torch_random_dir_2(shape, device): |
| |
| """Sample uniformly random unit vectors in 2-D. |
| |
| Args: |
| shape: Shape of the angle samples as a 2-tuple ``(rows, 1)``. |
| device: Target device string. |
| |
| Returns: |
| Float tensor of shape ``(rows, 2)`` containing ``(cos θ, sin θ)`` |
| with ``θ`` drawn uniformly from ``[-π, π)``. |
| """ |
| angle = torch_rand_float(-np.pi, np.pi, shape, device).squeeze(-1) |
| return torch.stack([torch.cos(angle), torch.sin(angle)], dim=-1) |
|
|
|
|
| @torch.jit.script |
| def tensor_clamp(t, min_t, max_t): |
| """Element-wise clamp of tensor ``t`` to the range ``[min_t, max_t]``. |
| |
| Unlike ``torch.clamp`` this version accepts tensors for the bounds so that |
| per-element limits are supported. |
| |
| Args: |
| t: Input tensor. |
| min_t: Lower bound tensor, same shape or broadcastable to ``t``. |
| max_t: Upper bound tensor, same shape or broadcastable to ``t``. |
| |
| Returns: |
| Clamped tensor of the same shape as ``t``. |
| """ |
| return torch.max(torch.min(t, max_t), min_t) |
|
|
|
|
| @torch.jit.script |
| def scale(x, lower, upper): |
| """Map values from ``[-1, 1]`` to ``[lower, upper]``. |
| |
| Args: |
| x: Input tensor in the normalised range ``[-1, 1]``. |
| lower: Target range lower bound. |
| upper: Target range upper bound. |
| |
| Returns: |
| Tensor scaled to ``[lower, upper]``. |
| """ |
| return 0.5 * (x + 1.0) * (upper - lower) + lower |
|
|
|
|
| @torch.jit.script |
| def unscale(x, lower, upper): |
| """Map values from ``[lower, upper]`` to ``[-1, 1]``. |
| |
| Inverse of :func:`scale`. |
| |
| Args: |
| x: Input tensor in the range ``[lower, upper]``. |
| lower: Source range lower bound. |
| upper: Source range upper bound. |
| |
| Returns: |
| Tensor normalised to ``[-1, 1]``. |
| """ |
| return (2.0 * x - upper - lower) / (upper - lower) |
|
|
|
|
| def unscale_np(x, lower, upper): |
| """NumPy equivalent of :func:`unscale`. |
| |
| Args: |
| x: Input array in the range ``[lower, upper]``. |
| lower: Source range lower bound. |
| upper: Source range upper bound. |
| |
| Returns: |
| Array normalised to ``[-1, 1]``. |
| """ |
| return (2.0 * x - upper - lower) / (upper - lower) |
|
|
|
|
| def euler_xyz_to_gf_matrix(angles): |
| """Convert Euler XYZ angles (in radians) to a USD Gf.Matrix4d. |
| |
| Args: |
| angles: [roll, pitch, yaw] in radians. Can be list, numpy array, or torch.Tensor. |
| |
| Returns: |
| Gf.Matrix4d with the rotation applied. |
| """ |
| if isinstance(angles, list | np.ndarray): |
| angles = torch.tensor(angles, dtype=torch.float32) |
| if angles.dim() == 1: |
| angles = angles.unsqueeze(0) |
|
|
| roll, pitch, yaw = angles[:, 0], angles[:, 1], angles[:, 2] |
| quat = quat_from_euler_xyz(roll, pitch, yaw) |
|
|
| |
| qx, qy, qz, qw = quat[0].tolist() |
| gf_quat = Gf.Quatd(qw, qx, qy, qz) |
|
|
| m = Gf.Matrix4d() |
| m.SetRotate(Gf.Rotation(gf_quat)) |
| return m |
|
|