Rerun Helpers in the Artefacts Toolkit
The Artefacts Toolkit Rerun Helpers provide convenient functions for tests that record a Rerun .rrd file during a simulation and then make their assertions against that recording, rather than against the simulator itself.
This splits a test into two independent halves. The simulator (or a logger node next to it) records while the test itself then asserts against the recording, so no ROS or simulator knowledge is needed to write the assertions.
Import with:
from artefacts_toolkit.rerun import recorder, reader, video
- The helpers wrap the parts of
rerun-sdkthat change between releases (recording sinks, the dataframe/query API), so test code written against them keeps working across Rerun upgrades. - Data columns are named
"/entity:Archetype:field", for example"/base:Transform3D:translation"or"/base/yaw:Scalars:scalars". Usereader.get_columnsto list the columns in a recording. - Every
readerandvideofunction accepts either the path to a.rrdfile or the object returned byreader.load.
Functions
recorder.get_output_dirrecorder.start_recordingreader.loadreader.get_entity_pathsreader.get_timelinesreader.get_columnsreader.get_columnreader.get_final_messagereader.get_message_countreader.to_dataframevideo.extract_videovideo.extract_camera_image
Function Reference
recorder.get_output_dir
Returns (and creates) the directory that recordings and other test outputs should be written to.
recorder.get_output_dir(
directory=None
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
directory |
str or Path |
Explicit directory to use instead of the defaults | None |
Returns
Path: The absolute path of the output directory.
- When
directoryis not given, theARTEFACTS_SCENARIO_UPLOAD_DIRenvironment variable is used. It is set byartefacts run, and everything in it is uploaded to the Artefacts Dashboard when the scenario finishes. - When neither is available (for example running
pytestlocally),./test_outputsis used.
Example
The same directory is resolved on the simulator side and on the test side, so both agree on where the recording is:
from artefacts_toolkit.rerun import recorder
OUTPUT_FOLDER = recorder.get_output_dir()
rrd_path = OUTPUT_FOLDER / "my-rerun-recording.rrd"
recorder.start_recording
Starts a Rerun recording saved to directory/filename and returns the recording stream and the file path.
recorder.start_recording(
application_id,
filename="recording.rrd",
directory=None,
handle_sigterm=True
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
application_id |
str |
Name of the application shown in the Rerun viewer | Required |
filename |
str or Path |
Name of the .rrd file. May also be an absolute path, in which case directory is ignored |
"recording.rrd" |
directory |
str or Path |
Directory to save to, resolved like get_output_dir |
None |
handle_sigterm |
bool |
Close the recording and exit cleanly when the process receives SIGTERM | True |
Returns
tuple: Returns a tuple containing:
recording(rerun.RecordingStream): The started recording. It is also made the global recording, so plainrr.log(...)andrr.set_time(...)calls go to it.path(Path): Path to the.rrdfile being written.
- A stale file of the same name is removed before recording starts.
- Test harnesses usually stop the simulator with
Popen.terminate()(SIGTERM). Withhandle_sigterm, the recording is closed first, so the file is complete even if the harness then kills the process during slower cleanup such as closing video encoders. - Some native libraries reset signal handlers when they initialise (limxsdk’s
Robot.initdoes). Callstart_recordingafter them. - Recordings are automatically uploaded to the Artefacts Dashboard unless you use the
--no-uploadflag withartefacts run.
Example
In the following example the simulator records the base pose and the joystick commands on a sim_time timeline, and is stopped by the test harness once the mission ends. Setting your own timeline with rr.set_time is what lets the test later line data up by simulation time rather than wall-clock time.
import rerun as rr
import simulator as sim
from artefacts_toolkit.rerun import recorder
class RerunSimulator(sim.SimulatorMujoco):
def _set_sim_time(self):
rr.set_time("sim_time", duration=float(self.mujoco_data.time))
def _log_joy(self, joy):
self._set_sim_time()
rr.log("cmd/fwd", rr.Scalars(joy.axes[1]))
rr.log("cmd/yaw", rr.Scalars(joy.axes[2]))
def _read_state(self):
super()._read_state()
q = self.mujoco_data.qpos # [0:3] base xyz, [3:7] base quat wxyz
w, x, y, z = q[3:7]
self._set_sim_time()
rr.log("base", rr.Transform3D(translation=q[0:3],
quaternion=rr.Quaternion(xyzw=[x, y, z, w])))
rr.log("base/yaw", rr.Scalars(yaw_from_quat(w, x, y, z)))
def main():
robot = sim.Robot(sim.RobotType.Tron2, True)
robot.init("127.0.0.1")
# After robot.init(): the SDK resets signal handlers
recorder.start_recording("tron2_loop", "recording-loop.rrd")
RerunSimulator(...).run()
reader.load
Loads a recording into memory once, so several assertions can share it without re-reading the file.
reader.load(
rrd
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str or Path |
Path to the .rrd file |
Required |
Returns
ChunkStore: The loaded recording. Every other reader and video function accepts it in place of the path.
- Files written by older Rerun versions, or by a process that was killed before finishing, are read as well.
Example
from artefacts_toolkit.rerun import reader
@pytest.fixture(scope="module")
def recording(recording_path):
assert recording_path.exists(), f"Recording not found at {recording_path}"
return reader.load(recording_path)
reader.get_entity_paths
Returns all entity paths logged in the recording.
reader.get_entity_paths(
rrd
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
Returns
list[str]: The entity paths, for example ["/base", "/base/yaw", "/cmd/fwd"].
Example
assert "/bodies/uwb_tag" in reader.get_entity_paths(recording), "target was never logged"
reader.get_timelines
Returns the names of the timelines in the recording.
reader.get_timelines(
rrd
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
Returns
list[str]: The timeline names, for example ["log_time", "sim_time"].
log_timeis always present: Rerun adds it automatically. The timeline you set yourself withrr.set_timeis the one the other helpers use by default.
reader.get_columns
Returns the data column names of the recording, optionally only those of one entity.
reader.get_columns(
rrd,
entity=None
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
entity |
str |
Only list the columns of this entity | None |
Returns
list[str]: Column names in the "/entity:Archetype:field" form.
Example
>>> reader.get_columns(recording, "/base")
['/base:Transform3D:quaternion', '/base:Transform3D:translation']
reader.get_column
Returns the times and values of one column, ordered by a timeline.
reader.get_column(
rrd,
column,
timeline=None
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
column |
str |
Column name, e.g. "/base/yaw:Scalars:scalars" |
Required |
timeline |
str |
Timeline to order by. Defaults to the recording’s own timeline (the first one that is not log_time), else log_time |
None |
Returns
tuple: Returns a tuple containing:
times(numpy.ndarray): Float seconds for duration timelines,datetime64for timestamp timelines, integers for sequence timelines.values(numpy.ndarray): Shape(n,)for scalars and(n, 3)for 3-vectors. When rows hold a varying number of instances (e.g. point clouds), an object array of per-row arrays.
- Raises
KeyErrorwhen the column does not exist.
Example
times, yaw = reader.get_column(recording, "/base/yaw:Scalars:scalars")
assert times[-1] > 60.0, "mission ended early"
assert np.abs(yaw).max() <= np.pi
reader.get_final_message
Retrieves the last value logged to a column, or the value of a static column.
reader.get_final_message(
rrd,
column
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
column |
str |
Column name, e.g. "/base:Transform3D:translation" |
Required |
Returns
Any: The value, unwrapped: a float for Scalars, a length-3 array for a translation, an (n, 3) array for a Points3D with several points.
Example
Waypoints are logged once (static) as Points3D, and the test measures how far the robot strays from the track they describe:
wp_col = "/network/mission_waypoints:Points3D:positions"
assert wp_col in reader.get_columns(recording), f"Could not find {wp_col} in recording"
track = np.vstack(reader.get_final_message(recording, wp_col))[:, :2] # N x 2
reader.get_message_count
Returns the number of log calls made to an entity.
reader.get_message_count(
rrd,
entity
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
entity |
str |
Entity path, e.g. "/cam" |
Required |
Returns
int: The number of (non-static) rows logged to the entity, 0 if it does not exist.
Example
assert reader.get_message_count(recording, "/cam") >= 100, "camera stopped publishing"
reader.to_dataframe
Returns the recording as a pandas DataFrame, one row per time on the chosen timeline.
Different entities are usually logged at different rates: a pose every simulation step, a joystick command only when it changes. With step, every selected entity is resampled onto one uniform time grid and, with fill_latest_at, gaps are filled with the latest earlier value, so a sparse command topic lines up row by row with a dense pose stream and the two can be compared directly.
reader.to_dataframe(
rrd,
contents=None,
index=None,
step=None,
fill_latest_at=True
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
contents |
str or list[str] |
Entities to include: "/base" for one entity, "/base/**" for a subtree, or a list of those. All entities when None |
None |
index |
str |
Timeline to use as the row index. Defaults like get_column |
None |
step |
float |
Resample onto a uniform grid of this many seconds, from the first to the last time | None |
fill_latest_at |
bool |
Fill gaps with the latest earlier value of each column | True |
Returns
pandas.DataFrame: The timeline column comes first (float seconds for duration timelines), followed by one column per "/entity:Archetype:field". Static data is joined onto every row. Scalars columns are plain float64 (NaN where nothing was logged); vectors are arrays.
- Without
step, there is one row per time at which any of the selected entities was logged. - Vector cells are numpy arrays.
np.vstack(df[column])turns a translation column into an(n, 3)array.
Example
@pytest.fixture(scope="module")
def df(recording_path):
"""Whole recording resampled onto a uniform 20 ms sim-time grid."""
frame = reader.to_dataframe(recording_path, ["/base/**", "/cmd/**"], index="sim_time", step=0.02)
out = frame.dropna(subset=["/base:Transform3D:translation"]).reset_index(drop=True)
out["x"] = [t[0] for t in out["/base:Transform3D:translation"]]
out["y"] = [t[1] for t in out["/base:Transform3D:translation"]]
out["yaw"] = out["/base/yaw:Scalars:scalars"]
out["cmd_fwd"] = out["/cmd/fwd:Scalars:scalars"].fillna(0.0)
return out
def test_straights_cover_side_length(df, straights):
for i, (a, b) in enumerate(straights):
dist = math.hypot(df["x"][b - 1] - df["x"][a], df["y"][b - 1] - df["y"][a])
assert abs(dist - SIDE_LENGTH) < DIST_TOLERANCE
video.extract_video
Creates an MP4 video from a camera entity in the recording.
video.extract_video(
rrd,
entity,
output_path,
timeline=None,
frame_rate=20
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
entity |
str |
Entity path of the camera, e.g. "/cam" |
Required |
output_path |
str or Path |
Path where the video will be saved (.mp4) | Required |
timeline |
str |
Timeline to order frames by. Defaults like get_column |
None |
frame_rate |
int |
Frame rate used when encoding rr.Image frames |
20 |
Returns
Path: The path of the saved video.
- Works for entities logged as
rr.Imageframes (encoded with H.264 atframe_rate) and forrr.VideoStreamentities with H.264 samples (remuxed as-is, keeping the recording’s timestamps). - The
.mp4extension is added if not present inoutput_path. - By saving to the directory returned by
recorder.get_output_dir, the video is automatically uploaded to the Artefacts Dashboard.
Example
from artefacts_toolkit.rerun import recorder, video
video.extract_video(recording, "/cam", recorder.get_output_dir() / "head_camera.mp4")
video.extract_camera_image
Saves the last image logged to a camera entity as a PNG.
video.extract_camera_image(
rrd,
entity,
output_dir="output"
)
Parameters
| Parameter | Type | Description | Default |
|---|---|---|---|
rrd |
str, Path or ChunkStore |
Path to the recording, or a loaded one | Required |
entity |
str |
Entity path of the camera, e.g. "/cam" |
Required |
output_dir |
str or Path |
Directory where to save the extracted image | "output" |
Returns
Path: The path of the saved image, output_dir/<entity>.last.png with / replaced by _ (for example output/_cam.last.png).
- Only
rr.Imageentities with 8-bit L, RGB, RGBA, BGR or BGRA pixels are supported.
Example
video.extract_camera_image(recording, "/cam", recorder.get_output_dir())