mjlab/docs/source/recorders.rst
Upstream Snapshot 32a241c28f
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
Import upstream snapshot c19f713c415a699a79d71cd96aa13c3104a05047
Upstream: https://github.com/michaelgillett/mjlab
Upstream-Commit: c19f713c415a699a79d71cd96aa13c3104a05047
Upstream-Branch: main
2026-08-28 15:42:17 +08:00

122 lines
4.5 KiB
ReStructuredText

.. _recorders:
Recorders
=========
The recorder manager provides lifecycle hooks for logging data during
rollouts. Unlike rewards, recorders have no effect on the optimization
loop. They exist purely to let you capture observations, actions, or any
other environment state without modifying mjlab internals.
Each recorder term is a class that you implement. mjlab calls its methods
at the right moments and leaves all I/O decisions to you. If the ``recorders``
dictionary on ``ManagerBasedRlEnvCfg`` is empty, the environment substitutes a
lightweight no-op manager with zero overhead.
Lifecycle hooks
---------------
The manager exposes three hooks per term:
``record_pre_reset(env_ids)``
Called inside ``env.step()`` before terminated environments are reset.
``obs_buf`` holds the observation from the end of the *previous* step
(the input the agent used to choose the terminal action, not the
post-action terminal state). ``action_manager.action`` holds the
terminal action and is still valid here; it will be zeroed for these
environments by ``_reset_idx`` immediately after. ``reward_buf`` holds
the terminal reward. This is the right place to record the terminal
transition ``(obs_t, action_t, reward_t, done=True)``.
``record_post_reset(env_ids)``
Called after a reset completes and fresh observations are available.
Fires at the end of ``env.reset()`` (all environments) and within
``env.step()`` after each batch of terminated environments is reset.
``obs_buf[env_ids]`` holds the initial observation of the new episode;
``action_manager.action[env_ids]`` is zero. Use this to initialize
per-episode state or record the first observation.
``record_post_step()``
Called at the end of every ``env.step()`` with fresh observations.
For environments that reset during this step, ``action_manager.action``
has been zeroed and ``obs_buf`` holds the initial state of the new
episode rather than the post-action terminal observation. Use
``record_pre_reset`` for those environments' terminal transitions and
``self._env.reset_buf`` to identify which environments reset.
``close()``
Called when the environment closes. Release file handles and flush
buffers here.
Writing a recorder term
------------------------
Subclass :class:`~mjlab.managers.RecorderTerm` and override whichever
hooks you need. The environment is available as ``self._env``, giving
access to ``self._env.obs_buf``, ``self._env.action_manager.action``, and
all other managers.
.. code-block:: python
import csv
from mjlab.managers import RecorderTerm, RecorderTermCfg
class CsvRecorder(RecorderTerm):
def __init__(self, cfg, env):
super().__init__(cfg, env)
self._file = open(cfg.params["path"], "w", newline="")
self._writer = csv.writer(self._file)
def record_pre_reset(self, env_ids):
# Terminal transition: action is still intact here.
# It will be zeroed by _reset_idx immediately after this returns.
obs = self._env.obs_buf["actor"][env_ids].cpu().numpy()
act = self._env.action_manager.action[env_ids].cpu().numpy()
for o, a in zip(obs, act):
self._writer.writerow(o.tolist() + a.tolist())
def record_post_step(self):
# Skip envs that just reset: their terminal pair was written
# in record_pre_reset and their action is now zeroed.
mask = ~self._env.reset_buf
obs = self._env.obs_buf["actor"][mask].cpu().numpy()
act = self._env.action_manager.action[mask].cpu().numpy()
for o, a in zip(obs, act):
self._writer.writerow(o.tolist() + a.tolist())
def close(self):
self._file.close()
The term receives the full ``cfg`` object so it can read any values you
put in ``cfg.params``.
Registration
------------
Add the term to the ``recorders`` dictionary on your environment config:
.. code-block:: python
from dataclasses import dataclass, field
from mjlab.managers import RecorderTermCfg
@dataclass
class MyEnvCfg(SomeTaskEnvCfg):
recorders: dict = field(default_factory=lambda: {
"csv": RecorderTermCfg(
func=CsvRecorder,
params={"path": "rollout.csv"},
)
})
Multiple terms can be registered under different keys and run together.
.. note::
``func`` must be a :class:`~mjlab.managers.RecorderTerm` subclass.
Function-based terms are not supported because recorder terms are
stateful.