Some checks failed
nightly / Test against latest dependencies (py3.10) (push) Has been cancelled
nightly / Test against latest dependencies (py3.13) (push) Has been cancelled
tests / tests (3.13, locked) (push) Has been cancelled
tests / tests (3.13, unlocked) (push) Has been cancelled
tests / pyright (3.10) (push) Has been cancelled
tests / lint-format (push) Has been cancelled
tests / tests (3.10, locked) (push) Has been cancelled
tests / tests (3.11, locked) (push) Has been cancelled
tests / tests (3.12, locked) (push) Has been cancelled
tests / pyright (3.11) (push) Has been cancelled
tests / pyright (3.12) (push) Has been cancelled
tests / pyright (3.13) (push) Has been cancelled
tests / ty-check (3.10) (push) Has been cancelled
tests / ty-check (3.11) (push) Has been cancelled
tests / ty-check (3.12) (push) Has been cancelled
tests / ty-check (3.13) (push) Has been cancelled
tests / stubs (push) Has been cancelled
tests / smoke-test (push) Has been cancelled
Docker / check_paths (push) Has been cancelled
docs / build (push) Has been cancelled
Docker / build (push) Has been cancelled
Upstream: https://github.com/michaelgillett/mjlab Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047 Upstream-Branch: main
456 lines
17 KiB
ReStructuredText
456 lines
17 KiB
ReStructuredText
.. _sensors:
|
|
|
|
Sensors
|
|
=======
|
|
|
|
As described in :ref:`entity`, sensors sit between ``EntityData`` and
|
|
raw simulation arrays in mjlab's data access hierarchy. At their
|
|
simplest, they wrap MuJoCo sensor primitives with a clean interface
|
|
that maps to real robot hardware. Beyond wrapping, they are a general
|
|
abstraction for transforming simulation data into structured outputs:
|
|
``ContactSensor`` aggregates contact pairs with reduction and air time
|
|
tracking, ``RayCastSensor`` performs GPU-accelerated terrain scanning,
|
|
``CameraSensor`` renders RGB and depth images on the GPU, and the base
|
|
``Sensor`` class can be subclassed for custom measurement logic.
|
|
|
|
Sensors are configured at the **scene level**, not on individual entities. A
|
|
sensor can reference an entity element (a contact sensor on the robot's
|
|
feet, an accelerometer attached to a body site), but it can also be
|
|
independent of any entity entirely. This is why sensors live in
|
|
``SceneCfg`` rather than ``EntityCfg``.
|
|
|
|
.. code-block:: python
|
|
|
|
from mjlab.sensor import (
|
|
BuiltinSensorCfg, ContactSensorCfg, ContactMatch, ObjRef,
|
|
)
|
|
|
|
# A robot with an IMU accelerometer and foot contact detection.
|
|
scene_cfg = SceneCfg(
|
|
entities={"robot": robot_cfg},
|
|
sensors=(
|
|
BuiltinSensorCfg(
|
|
name="imu_acc",
|
|
sensor_type="accelerometer",
|
|
obj=ObjRef(type="site", name="imu_site", entity="robot"),
|
|
),
|
|
ContactSensorCfg(
|
|
name="feet_contact",
|
|
primary=ContactMatch(
|
|
mode="geom", pattern=r".*_foot$", entity="robot",
|
|
),
|
|
secondary=ContactMatch(mode="body", pattern="terrain"),
|
|
fields=("found", "force"),
|
|
),
|
|
),
|
|
)
|
|
|
|
# Access at runtime.
|
|
imu = env.scene["robot/imu_acc"].data # [B, 3] acceleration
|
|
feet = env.scene["feet_contact"].data # ContactData
|
|
feet.found # [B, P] contact count per foot
|
|
feet.force # [B, P, 3] contact force per foot
|
|
|
|
mjlab provides four sensor types: ``BuiltinSensor`` for native MuJoCo
|
|
measurements, ``ContactSensor`` for structured contact detection,
|
|
``RayCastSensor`` for GPU-accelerated raycasting, and ``CameraSensor``
|
|
for RGB-D rendering. The base ``Sensor`` class can be subclassed for
|
|
custom measurement logic; see `Extending: custom sensors`_ below.
|
|
|
|
|
|
BuiltinSensor
|
|
-------------
|
|
|
|
``BuiltinSensor`` wraps MuJoCo's native sensor types. Each sensor is
|
|
attached to a MuJoCo element (site, joint, body, etc.) via ``ObjRef``
|
|
and returns a ``torch.Tensor`` with shape ``[num_envs, dim]`` where
|
|
``dim`` depends on the sensor type (3 for vectors, 4 for quaternions,
|
|
1 for scalars).
|
|
|
|
+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
| Category | Available Sensors |
|
|
+===========+====================================================================================================================================================+
|
|
| **Site** | ``accelerometer``, ``velocimeter``, ``gyro``, ``force``, ``torque``, ``magnetometer``, ``rangefinder`` |
|
|
+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
| **Joint** | ``jointpos``, ``jointvel``, ``jointlimitpos``, ``jointlimitvel``, ``jointlimitfrc``, ``jointactuatorfrc`` |
|
|
+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
| **Frame** | ``framepos``, ``framequat``, ``framexaxis``, ``frameyaxis``, ``framezaxis``, ``framelinvel``, ``frameangvel``, ``framelinacc``, ``frameangacc`` |
|
|
+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
| **Other** | ``actuatorpos``, ``actuatorvel``, ``actuatorfrc``, ``subtreecom``, ``subtreelinvel``, ``subtreeangmom``, ``clock``, ``e_potential``, ``e_kinetic`` |
|
|
+-----------+----------------------------------------------------------------------------------------------------------------------------------------------------+
|
|
|
|
``ObjRef`` identifies which MuJoCo element the sensor attaches to. The
|
|
``entity`` field scopes the lookup to a specific entity's namespace, and
|
|
the sensor name is auto-prefixed accordingly (e.g., ``"imu_acc"`` on
|
|
entity ``"robot"`` becomes ``"robot/imu_acc"``).
|
|
|
|
.. code-block:: python
|
|
|
|
from mjlab.sensor import BuiltinSensorCfg, ObjRef
|
|
|
|
# Accelerometer attached to a site.
|
|
BuiltinSensorCfg(
|
|
name="imu_acc",
|
|
sensor_type="accelerometer",
|
|
obj=ObjRef(type="site", name="imu_site", entity="robot"),
|
|
)
|
|
|
|
# Joint limit sensor with output clamping.
|
|
BuiltinSensorCfg(
|
|
name="knee_limit",
|
|
sensor_type="jointlimitpos",
|
|
obj=ObjRef(type="joint", name="knee_joint", entity="robot"),
|
|
cutoff=0.1,
|
|
)
|
|
|
|
# Relative frame position (end-effector w.r.t. base).
|
|
BuiltinSensorCfg(
|
|
name="ee_pos",
|
|
sensor_type="framepos",
|
|
obj=ObjRef(type="body", name="end_effector", entity="robot"),
|
|
ref=ObjRef(type="body", name="base", entity="robot"),
|
|
)
|
|
|
|
|
|
Auto-discovery
|
|
^^^^^^^^^^^^^^
|
|
|
|
Sensors already defined in an entity's XML are automatically discovered
|
|
during scene composition and prefixed with the entity name. There is no
|
|
need to create a ``BuiltinSensorCfg`` for these.
|
|
|
|
.. code-block:: xml
|
|
|
|
<!-- In robot.xml -->
|
|
<sensor>
|
|
<accelerometer name="trunk_imu" site="imu_site"/>
|
|
<jointpos name="hip_sensor" joint="hip_joint"/>
|
|
</sensor>
|
|
|
|
.. code-block:: python
|
|
|
|
# Access by prefixed name.
|
|
imu = env.scene["robot/trunk_imu"]
|
|
hip = env.scene["robot/hip_sensor"]
|
|
|
|
|
|
ContactSensor
|
|
-------------
|
|
|
|
Each physics step, MuJoCo produces a flat, unstructured list of contact
|
|
pairs across the entire scene. A single foot geom might generate several
|
|
simultaneous contacts with the ground, interleaved with contacts from
|
|
other entities. ``ContactSensor`` filters this raw list to the pairs you
|
|
care about, reduces multiple contacts per element down to a fixed count,
|
|
and packages the result into clean, batched tensors your policy can
|
|
consume directly. It builds on MuJoCo's native
|
|
`contact sensor <https://mujoco.readthedocs.io/en/stable/XMLreference.html#sensor-contact>`_.
|
|
|
|
Primary and secondary
|
|
^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
Contacts are pairwise: you typically want to know "did the robot's feet
|
|
touch the terrain?", not just "did something touch something."
|
|
``primary`` defines the elements you are measuring (the feet).
|
|
``secondary`` optionally restricts what they are contacting (the
|
|
terrain). When ``secondary`` is ``None``, any contact with a primary
|
|
element counts.
|
|
|
|
Each side is specified with a ``ContactMatch``. The ``mode`` selects the
|
|
MuJoCo element type (``"geom"``, ``"body"``, or ``"subtree"``) and the
|
|
``pattern`` accepts a regex or tuple of regexes matched against element
|
|
names within the entity.
|
|
|
|
.. code-block:: python
|
|
|
|
from mjlab.sensor import ContactSensorCfg, ContactMatch
|
|
|
|
# Foot geoms contacting the terrain body.
|
|
ContactSensorCfg(
|
|
name="feet_ground",
|
|
primary=ContactMatch(
|
|
mode="geom", pattern=r".*_foot$", entity="robot",
|
|
),
|
|
secondary=ContactMatch(mode="body", pattern="terrain"),
|
|
fields=("found", "force"),
|
|
)
|
|
|
|
# Self-collision: pelvis subtree against itself.
|
|
ContactSensorCfg(
|
|
name="self_collision",
|
|
primary=ContactMatch(
|
|
mode="subtree", pattern="pelvis", entity="robot",
|
|
),
|
|
secondary=ContactMatch(
|
|
mode="subtree", pattern="pelvis", entity="robot",
|
|
),
|
|
fields=("found",),
|
|
)
|
|
|
|
Output shape
|
|
^^^^^^^^^^^^
|
|
|
|
A pattern like ``r".*_foot$"`` resolves to ``P`` primary elements (e.g.
|
|
four feet on a quadruped). Each primary becomes one column on the
|
|
per-contact axis of the output tensors:
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 35 25 40
|
|
|
|
* - Field group
|
|
- Shape
|
|
- Notes
|
|
* - Per-contact
|
|
(``found``, ``force``, ``torque``, ``dist``, ``pos``, ``normal``,
|
|
``tangent``)
|
|
- ``[B, P * num_slots, ...]``
|
|
- Primary-major: indices
|
|
``[i * num_slots : (i + 1) * num_slots]`` belong to primary ``i``.
|
|
* - Per-primary
|
|
(``current_air_time``, ``last_air_time``,
|
|
``current_contact_time``, ``last_contact_time``)
|
|
- ``[B, P]``
|
|
- Air-time fields are accumulated per primary, reducing across
|
|
slots (any slot in contact counts as the primary in contact).
|
|
|
|
With the default ``num_slots=1`` the two shape families coincide
|
|
(``N == P``), which is why most code can treat both as ``[B, P, ...]``.
|
|
|
|
Use :attr:`mjlab.sensor.contact_sensor.ContactSensor.primary_names` to
|
|
recover the index-to-name mapping after pattern expansion:
|
|
|
|
.. code-block:: python
|
|
|
|
sensor = env.scene["feet_contact"]
|
|
sensor.primary_names # ["FR_foot", "FL_foot", "RR_foot", "RL_foot"]
|
|
sensor.data.current_air_time[:, 0] # air time for FR_foot
|
|
|
|
Reduction
|
|
^^^^^^^^^
|
|
|
|
A single primary element can have many simultaneous contacts with the
|
|
secondary (e.g. a flat foot resting on rough terrain has multiple
|
|
contact points). The ``reduce`` mode collapses those raw contacts down
|
|
to ``num_slots`` representative contacts:
|
|
|
|
.. list-table::
|
|
:header-rows: 1
|
|
:widths: 20 80
|
|
|
|
* - Mode
|
|
- Behavior
|
|
* - ``"none"``
|
|
- Fast, non-deterministic selection of up to ``num_slots`` contacts.
|
|
* - ``"mindist"``
|
|
- Keep the deepest ``num_slots`` contacts.
|
|
* - ``"maxforce"``
|
|
- Keep the strongest ``num_slots`` contacts by force magnitude.
|
|
* - ``"netforce"``
|
|
- Sum all contacts into a single net wrench at the force-weighted
|
|
centroid. Always emits one slot per primary regardless of
|
|
``num_slots``.
|
|
|
|
When to set ``num_slots > 1``
|
|
"""""""""""""""""""""""""""""
|
|
|
|
Almost all configurations leave ``num_slots`` at its default of ``1``,
|
|
because pattern expansion already produces one column per element of
|
|
interest (one per foot, one per finger, one per body link). Increase
|
|
``num_slots`` only when a single primary may have several physically
|
|
distinct contact points that you want to inspect separately, for
|
|
example:
|
|
|
|
- Computing a center of pressure on a flat foot from its corner
|
|
contacts.
|
|
- Reasoning about grasp quality from multiple fingertip-to-object
|
|
contact points.
|
|
- Detecting tipping by watching whether one corner of a contact patch
|
|
loses contact.
|
|
|
|
In those cases pair ``num_slots`` with ``reduce`` in
|
|
``{"mindist", "maxforce", "none"}``. With ``reduce="netforce"`` it has
|
|
no effect.
|
|
|
|
.. note::
|
|
|
|
``num_slots`` is a ceiling on what the sensor will store, not a
|
|
guarantee that MuJoCo will produce that many contacts. The collision
|
|
detector caps the number of contact points generated for each
|
|
geom-pair according to the geom types involved. For example a
|
|
sphere-vs-plane pair produces at most one contact, and a box-vs-box
|
|
pair produces at most four. Setting ``num_slots=8`` on a sphere
|
|
primary against a plane secondary therefore leaves seven slots
|
|
permanently zero. Check
|
|
`MuJoCo's collision documentation <https://mujoco.readthedocs.io/en/stable/computation/index.html#collision-detection>`_
|
|
for the per-pair limits.
|
|
|
|
Fields
|
|
^^^^^^
|
|
|
|
The ``fields`` tuple selects which contact quantities to extract. Only
|
|
requested fields are allocated; the rest are ``None`` on the output
|
|
dataclass. Available fields are ``"found"``, ``"force"``,
|
|
``"torque"``, ``"dist"``, ``"pos"``, ``"normal"``, and ``"tangent"``.
|
|
|
|
.. note::
|
|
|
|
``torque`` and the friction-tangent component of ``force`` are zero
|
|
unless the contact pair has friction enabled, which requires
|
|
``condim >= 3`` on at least one geom in the pair. With ``condim=1``
|
|
(frictionless), the contact only produces a normal force. This is a
|
|
physics property of the contact, not a sensor limitation.
|
|
|
|
Air time tracking
|
|
^^^^^^^^^^^^^^^^^
|
|
|
|
Locomotion tasks often need to know when feet land and take off for
|
|
gait rewards. Setting ``track_air_time=True`` enables per-primary
|
|
timing. The sensor maintains four additional tensors on
|
|
``ContactData``, each shaped ``[B, P]``: ``current_air_time``,
|
|
``last_air_time``, ``current_contact_time``, and
|
|
``last_contact_time``. Two helper methods provide edge detection for
|
|
transition events:
|
|
|
|
.. code-block:: python
|
|
|
|
sensor = env.scene["feet_air"]
|
|
first_contact = sensor.compute_first_contact(dt) # [B, P], True for primaries that just landed
|
|
first_air = sensor.compute_first_air(dt) # [B, P], True for primaries that just took off
|
|
|
|
Air time is per primary even when ``num_slots > 1``: the sensor reduces
|
|
``found`` across slots so that any slot in contact counts as the
|
|
primary being in contact.
|
|
|
|
.. _contact-sensor-history:
|
|
|
|
History (decimation safe contacts)
|
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
|
|
When using decimation (multiple physics substeps per policy step), a
|
|
brief collision can occur and resolve entirely within the substep loop.
|
|
By the time the policy reads the sensor, the contact is gone and
|
|
``found`` reports zero. Setting ``history_length`` on the sensor config
|
|
tells the sensor to keep a rolling buffer of the last *N* substeps for
|
|
force, torque, and distance fields. The policy can then inspect the
|
|
full history and decide whether a real contact occurred.
|
|
|
|
Set ``history_length`` equal to your decimation value so the buffer
|
|
covers exactly one policy step:
|
|
|
|
.. code-block:: python
|
|
|
|
ContactSensorCfg(
|
|
name="self_collision",
|
|
primary=ContactMatch(mode="subtree", pattern="pelvis", entity="robot"),
|
|
secondary=ContactMatch(mode="subtree", pattern="pelvis", entity="robot"),
|
|
fields=("found", "force"),
|
|
history_length=4, # matches decimation=4
|
|
)
|
|
|
|
The history tensors live on ``ContactData`` alongside the regular
|
|
fields:
|
|
|
|
.. code-block:: python
|
|
|
|
data = sensor.data
|
|
data.force_history # [B, N, H, 3] (H = history_length)
|
|
data.torque_history # [B, N, H, 3]
|
|
data.dist_history # [B, N, H]
|
|
|
|
Index 0 is the most recent substep. To check whether any substep had
|
|
a contact force above a threshold:
|
|
|
|
.. code-block:: python
|
|
|
|
force_mag = torch.norm(data.force_history, dim=-1) # [B, N, H]
|
|
had_contact = (force_mag > 10.0).any(dim=1).any(dim=-1) # [B]
|
|
|
|
.. note::
|
|
|
|
``track_air_time=True`` already accumulates contact state across
|
|
substeps for gait rewards, so feet ground sensors typically do not
|
|
need ``history_length``. Use history for sensors where you need to
|
|
detect brief collisions that would otherwise be missed (self
|
|
collisions, illegal contact terminations).
|
|
|
|
|
|
Output
|
|
^^^^^^
|
|
|
|
``ContactData`` is a dataclass whose fields correspond to the
|
|
``fields`` tuple on the config. Unrequested fields are ``None``.
|
|
|
|
.. code-block:: python
|
|
|
|
@dataclass
|
|
class ContactData:
|
|
found: Tensor | None # [B, N] contact count
|
|
force: Tensor | None # [B, N, 3]
|
|
torque: Tensor | None # [B, N, 3]
|
|
dist: Tensor | None # [B, N] penetration depth
|
|
pos: Tensor | None # [B, N, 3] contact position
|
|
normal: Tensor | None # [B, N, 3] surface normal
|
|
tangent: Tensor | None # [B, N, 3]
|
|
|
|
# With track_air_time=True.
|
|
current_air_time: Tensor | None
|
|
last_air_time: Tensor | None
|
|
current_contact_time: Tensor | None
|
|
last_contact_time: Tensor | None
|
|
|
|
|
|
RayCastSensor
|
|
-------------
|
|
|
|
``RayCastSensor`` provides GPU-accelerated raycasting for terrain
|
|
scanning and depth sensing. It supports grid and pinhole camera ray
|
|
patterns with configurable alignment modes. See :ref:`raycast_sensor`
|
|
for full documentation.
|
|
|
|
|
|
RGB-D Camera
|
|
------------
|
|
|
|
``CameraSensor`` renders RGB and depth images from MuJoCo cameras. See
|
|
:ref:`rgbd_camera` for full documentation.
|
|
|
|
|
|
Extending: custom sensors
|
|
-------------------------
|
|
|
|
All sensors inherit from ``Sensor[T]``, a generic base class where
|
|
``T`` is the data type returned by the ``data`` property (e.g.,
|
|
``torch.Tensor`` for ``BuiltinSensor``, ``ContactData`` for
|
|
``ContactSensor``).
|
|
|
|
The base class provides automatic per-step caching. The ``data``
|
|
property calls ``_compute_data()`` on first access each step and
|
|
caches the result. The cache is invalidated automatically when
|
|
``update()`` or ``reset()`` is called, so multiple reads within the
|
|
same step (from different observation or reward terms) pay the
|
|
computation cost only once.
|
|
|
|
**Lifecycle methods:**
|
|
|
|
- ``edit_spec``: Add sensor elements to the MjSpec during scene
|
|
construction.
|
|
- ``initialize``: Post-compilation setup. Cache sensor indices,
|
|
allocate buffers, resolve references.
|
|
- ``update``: Called each physics step. Invalidates the data cache.
|
|
Override to maintain per-step state (e.g., air time counters).
|
|
- ``reset``: Called on environment reset. Invalidates the data cache.
|
|
Override to clear per-environment state.
|
|
- ``_compute_data``: Compute and return the sensor output. Called
|
|
lazily by the ``data`` property when the cache is stale.
|
|
|
|
``ContactSensor`` and ``RayCastSensor`` are the most complete
|
|
reference implementations for custom sensor development.
|
|
|
|
.. toctree::
|
|
:maxdepth: 1
|
|
:hidden:
|
|
|
|
raycast_sensor
|
|
rgbd_camera
|