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
122 lines
4.5 KiB
ReStructuredText
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.
|