Source code for mink_warp.limits.linear_inequality
"""Arbitrary constant linear inequality limit ``G dq <= h``.
The general-inequality escape hatch: any half-space (or stack of them) on the
tangent step that a per-dof box cannot express — an oriented plane in dof space,
a hand-written collision half-space, a coupled-joint bound. ``G`` and ``h`` are
constant (configuration-independent) and broadcast to every world, so this is
inequality-only (``box_capable = False``); it is enforced by the constrained
solver's general OSQP-ADMM path.
For configuration-dependent rows (e.g. collision avoidance, whose normals move
with ``q``) subclass :class:`~mink_warp.limits.Limit` and implement
``scatter_inequalities`` directly.
"""
from __future__ import annotations
import numpy as np
import numpy.typing as npt
import warp as wp
from ..configuration import Configuration
from ..kernels.constrained import linear_ineq_scatter
from .limit import Limit
[docs]
class LinearInequalityLimit(Limit):
"""Constant dense inequality ``G dq <= h`` applied to every world."""
box_capable = False
def __init__(self, G: npt.ArrayLike, h: npt.ArrayLike):
G_np = np.atleast_2d(np.asarray(G, dtype=np.float32))
h_np = np.atleast_1d(np.asarray(h, dtype=np.float32))
if G_np.ndim != 2:
raise ValueError(f"G must be 2-D (m, nv); got shape {G_np.shape}")
if G_np.shape[0] == 0:
raise ValueError(
"LinearInequalityLimit needs at least one row; an inequality "
"limit with no inequalities constrains nothing."
)
if h_np.shape != (G_np.shape[0],):
raise ValueError(
f"h must have shape ({G_np.shape[0]},) to match G's rows; "
f"got {h_np.shape}"
)
self._G_np = G_np
self._h_np = h_np
self.nv = int(G_np.shape[1])
self.n_inequalities = int(G_np.shape[0])
self._dev: dict[str, tuple[wp.array, wp.array]] = {}
def _ensure_dev(self, device: str):
cached = self._dev.get(device)
if cached is not None:
return cached
with wp.ScopedDevice(device):
arrs = (
wp.array(self._G_np, dtype=float),
wp.array(self._h_np, dtype=float),
)
self._dev[device] = arrs
return arrs
[docs]
def scatter_inequalities(
self,
configuration: Configuration,
dt: float,
row_offset: int,
G: wp.array,
h: wp.array,
) -> None:
del dt # Constant rows are timestep-independent.
if self.n_inequalities == 0:
return
if self.nv != configuration.nv:
raise ValueError(
f"LinearInequalityLimit G has nv={self.nv} but the configuration "
f"has nv={configuration.nv}."
)
device = configuration.device
Gc, hc = self._ensure_dev(device)
with wp.ScopedDevice(device):
wp.launch(
linear_ineq_scatter,
dim=(configuration.nworld, self.n_inequalities),
inputs=[Gc, hc, int(row_offset)],
outputs=[G, h],
)