Skip to content

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.

from zelos_sdk import connect

agent = connect()
trace = agent.trace("/data/bench-run-14.trz")
trace

Save a slice of live data

export() asks each producer to write its part of a time window into one file:

result = agent.export("/data/bench-run-14.trz", start="-10m", overwrite=True)
result

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:

agent.export("/data/pack.trz", start="-10m", signals="bus0/BMS_message/*", overwrite=True)

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:

trace.info()

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:

frame = runs.query("bus0/BMS_message/cells.cell_0", start="start", end="+30s")

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:

runs.check.that(agent.signal("bus0/BMS_message/cells.cell_0"), ">", 3.0, last="30s")

Slice an archive into a smaller one

trace.export() writes a new file from a slice of this one, with the same bounds grammar:

trace.export("/data/incident.trz", start="-2m", overwrite=True)

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

  • Query live data

    The full read surface: globs, frames, series math, charts.

  • Checks

    Rules with evidence, on a file or on the live agent.

  • Notebooks

    The round-trip as a runnable document.