Open Trace Files¶
A .trz file is a recording: the bytes the bench produced, frozen. The agent opens one and answers the same calls it answers for live data — query, at, window, latest, check, export — so an analysis you wrote against the live agent runs unchanged against the archive.
Save a slice of live data¶
export() asks each producer to write its part of a time window into one file:
The result reports per producer, so one failing source cannot silently hollow out the archive: result.ok is True only when every producer succeeded, and result.results maps each producer to its outcome. result.open() hands you the Trace without repeating the path.
Narrow what goes in with signals= — the same glob grammar as everywhere else — or omit it to export the whole catalog:
The file is written on the agent's filesystem. On a cross-host target overwrite=False is refused outright — the agent has no way to atomically replace a file on another machine — so pass overwrite=True to opt in.
Time on a trace¶
This is the one thing that differs from live data, and it is the thing that makes archives comparable: the file is the clock. The wall clock never enters into a trace query.
| Bound | Means |
|---|---|
"-60s" |
60 seconds before the end of the recording |
"+30s" |
30 seconds after the start of the recording |
"start" / "end" |
the file's own bounds |
datetime(...), ISO 8601 |
that instant, taken literally |
| omitted | the matching end of the file |
So trace.query(paths, start="-60s") is "the last minute of the run" and trace.query(paths, start="start", end="+30s") is "the first thirty seconds" — both meaningful in a recording made last week, and both the same words you would write against a live agent.
A window entirely outside the file raises ValueError naming the file's extent; one that merely overhangs is clamped.
Query a trace¶
trace.signals()
trace.signals().match("bus0/BMS_message/cells.*")
frame = trace.query("bus0/BMS_message/cells.*", start="-60s")
frame
Everything a live frame does, a trace frame does: the join rule, ffill()/dropna(), unit-carrying series, plot(), describe(). Omit both bounds and you get the file's full extent — on a live agent start is required, because "all of time" is not a window.
trace.info() is the file's own metadata — size, extent, segments, tables, row count:
Cursors, windows and checks¶
The same three calls, scoped to the file:
trace.at(["bus1/inverter_status.*"], cursor)
trace.window("bus1/inverter_status.mode", start="start", duration="30s")
trace.latest("bus0/BMS_message/status.pack_current") # the file's last value
trace.check.that(frame["bus0/BMS_message/cells.cell_0"], ">", 3.0)
A check on a trace reads exactly as it does live and produces the same result and evidence — see Checks. The two duration temporals are the exception: on live data they wait for data that has not arrived, and on a recording there is nothing to wait for, so temporal="for_duration" and temporal="within_duration" read duration_s as a window anchored at the end of the recording — the last ten seconds of the file, not the next ten seconds of the clock.
Compare runs¶
traces() opens several files as one set. Each file is a run. Pass a list and the runs are named after the file stems; pass a mapping and you name them yourself:
runs = agent.traces({"baseline": "/data/run_a.trz", "candidate": "/data/run_b.trz"})
runs
runs.runs # ['baseline', 'candidate']
Offset bounds — "start", "+30s", "-60s", or none at all — align every run at t = 0, and the frame's axis becomes seconds from each run's own start. That is what makes "the first thirty seconds of the run" comparable across recordings made on different days:
An absolute bound — a datetime or an ISO instant — selects the wall-clock axis instead, for the case where the runs really did happen at the same time.
A set frame has one column per (run, signal) pair, so frame[path] is ambiguous. Each column names its run instead, as <run>::<path>, under the set's own run names — the ones you passed traces(), or the file stems when you passed a list. series() is how you index by run:
frame.keys()
# ['baseline::bus0/BMS_message/cells.cell_0', 'candidate::bus0/BMS_message/cells.cell_0']
frame["baseline::bus0/BMS_message/cells.cell_0"]
frame.short_names().keys()
# ['baseline · cell_0', 'candidate · cell_0']
runs.series(frame, "bus0/BMS_message/cells.cell_0")
# SeriesByRun(2 runs: baseline len=30, candidate len=30)
SeriesByRun is a read-only mapping, so by_run["baseline"] and dict(by_run) both work.
A check on a set returns one CheckResult per run, keyed by label:
Slice an archive into a smaller one¶
trace.export() writes a new file from a slice of this one, with the same bounds grammar:
Closing¶
A trace holds a file open on the agent. Close it when you are done, or use the context manager and forget about it:
with agent.trace("/data/bench-run-14.trz") as trace:
frame = trace.query("bus0/BMS_message/cells.*", start="start", end="+1m")
with agent.traces(["/data/run_a.trz", "/data/run_b.trz"]) as runs:
...
agent.close_traces() releases every trace this agent has open.
Errors¶
| Error | When |
|---|---|
TraceNotFound |
no file at that path on the agent's host |
SignalNotFound |
the path is not in this file's catalog |
ValueError |
a window entirely outside the file's extent |
FileExistsError |
export(overwrite=False) onto an existing file |
What's next¶
-
The full read surface: globs, frames, series math, charts.
-
Rules with evidence, on a file or on the live agent.
-
The round-trip as a runnable document.