Skip to content

Open Trace Files

Zelos records data as .trz files. Save a slice of live data into one, then re-open it later with the same query(), at(), window(), and SignalFrame tools — see Query Live Data for the analysis surface.

Snippets assume an agent connected via connect(), with paths from zelos live demo:

from zelos_sdk.agent import connect

with connect() as agent:
    ...

Save a slice of live data

agent.export() writes a .trz to the agent host, capturing every signal that produced data in [start, end].

result = agent.export(
    "/tmp/run.trz",
    start="-5m",
    end="now",
    overwrite=True,
)
print(result.path, "ok:", result.ok)
for producer, pr in result.results.items():
    print(producer, pr.ok, pr.error or "")

ExportResult carries the written path and a {producer: ExportProducerResult} map. result.ok is True only when every producer succeeded. Per-producer failures are preserved on result.results so one failed producer doesn't hide successful exports.

Localhost vs remote agents

  • Localhost. overwrite=False raises FileExistsError if the path exists. overwrite=True replaces.
  • Remote agent. Use overwrite=True; overwrite=False raises ValueError because the file check only works on localhost.
result = agent.export(
    "/var/lib/zelos/runs/2024-05-01.trz",
    start="2024-05-01T08:00:00Z",
    end="2024-05-01T09:00:00Z",
    overwrite=True,
)
if not result.ok:
    for producer, pr in result.results.items():
        if not pr.ok:
            print(producer, "failed:", pr.error)

producers=("localhost",) is the default; pass producers=() to capture every connected producer.

Open a trace

agent.open_trace(path) returns a Trace handle. The path lives on the agent's filesystem — the SDK does not upload files.

trace = agent.open_trace("/tmp/run.trz")
print(trace.path)                                 # resolved absolute path
print(trace.time_range.start, "→", trace.time_range.end)
print(trace.time_range.duration)

catalog = trace.signals()
print(len(catalog), "signal(s) in this trace")

time_range is a TimeRange with start, end, duration. Per-trace warnings land on catalog.warnings.

Path resolution

  • Localhost agent. Relative paths and ~ work.
  • Remote agent. Use absolute paths. ~ and relative paths raise ConnectionTargetError.
trace = agent.open_trace("~/runs/run.trz")    # OK on localhost
trace = agent.open_trace("/srv/runs/run.trz") # OK anywhere

Segments

for seg in trace.segments():
    print(seg.id, seg.producer, seg.start, "→", seg.end)

Query a trace

trace.query() returns a SignalFrame with the same shape, metadata, and SignalSeries analysis surface as live agent.query(). See Query Live Data for series math, frame iteration, unit composition, and Arrow / pandas hand-off.

frame = trace.query(
    [
        "bus0/BMS_message/status.cell_voltage",
        "bus0/BMS_message/status.pack_current",
    ],
    relative_start=0,
    relative_end=30,
    downsample=400,
)

voltage = frame["bus0/BMS_message/status.cell_voltage"]
current = frame["bus0/BMS_message/status.pack_current"]
power = voltage * current     # SignalSeries, unit "V·A"
print(power.legend, power.max())

Absolute vs relative bounds

Pick one shape:

  • Absolute with start / enddatetime, "now", ISO 8601, or "-30s"-style relative offsets, just like live queries.
  • Relative with relative_start / relative_end — floats, in seconds from t=0 of the trace.
# First 30 seconds of the recording, regardless of wall-clock time.
frame = trace.query("bus0/*", relative_start=0, relative_end=30)

# A specific wall-clock window.
frame = trace.query(
    "bus0/*",
    start=trace.time_range.start,
    end=trace.time_range.end,
)

You cannot mix absolute and relative bounds in the same call. Omit all four to query the full trace.

max_rows, sort, and downsample work the same as live queries — downsample=N runs the agent's M4 strategy with about N buckets across the queried time range:

frame = trace.query(
    "bus0/BMS_message/status.*",
    relative_start=0,
    relative_end=60,
    downsample=400,
    max_rows=50_000,
    sort="asc",
)

Read at a cursor / window snapshot

# Per-signal value at-or-before a cursor.
cursor = trace.time_range.start + (trace.time_range.duration / 2)
values = trace.at("bus0/BMS_message/status.*", cursor)
for path, lv in values.items():
    print(path, lv.formatted())

# Opening snapshot + change stream over a 10-second window.
window = trace.window(
    "bus1/inverter_status.*",
    start=trace.time_range.start,
    duration=10.0,
)
print(len(window.snapshot), "opening value(s),", len(window.changes), "change(s)")

at() always returns a dict[str, LatestValue]. window() returns a ReplayWindow. See Read latest values for the LatestValue field reference.

Open multiple traces together

agent.open_traces([...]) returns a TraceSet — one handle for querying across many traces.

traces = agent.open_traces([
    "/tmp/run-2024-05-01.trz",
    "/tmp/run-2024-05-02.trz",
])
print(traces.paths)                       # input order
print(traces.time_range.start)            # earliest start across all traces
print(traces.time_range.max_duration_s)   # longest single-trace duration

for t in traces.time_range.traces:
    print(t.path, "starts", t.start, "for", t.duration)

Compare two runs at t = 0

By default, every TraceSet query runs in relative mode — each trace is aligned at t = 0, so plotting two recordings overlays them naturally regardless of when each was captured.

# Both traces' first 30 seconds, aligned.
frame = traces.query(
    "bus0/BMS_message/status.cell_voltage",
    relative_start=0,
    relative_end=30,
)
# `frame` has one column per (trace, signal) pair.
for col in frame.columns[1:]:
    print(col.trace_path, col.path)

Use wall-clock time across traces

Pass time_mode="absolute" to query traces by their original timestamps:

frame = traces.query(
    "bus0/BMS_message/status.cell_voltage",
    time_mode="absolute",
    start=traces.time_range.start,
    end=traces.time_range.end,
)

time_mode="relative" (default) is incompatible with start / end; time_mode="absolute" is incompatible with relative_*. The SDK raises ValueError rather than silently coercing.

traceset.at(), traceset.window(), and traceset.export() all accept the same time_mode argument with the same default.

Pick the right mode

  • Relative — comparing runs: laps, test cycles, day-over-day operation.
  • Absolute — incident reconstruction: correlating signals from sources that ran simultaneously.

Export a trace (or slice) to a new file

result = trace.export(
    "/tmp/run-clipped.trz",
    relative_start=10,
    relative_end=70,
    overwrite=True,
)
print(result.ok, result.path)

traceset.export() merges every trace in the set into one file:

result = traces.export(
    "/tmp/runs-merged.trz",
    relative_start=0,
    relative_end=60,
    overwrite=True,
)

Both accept the same bound shapes as query(). Partial failures show up on result.results exactly like agent.export(). traceset.export() defaults to time_mode="relative"; pass time_mode="absolute" with absolute bounds to merge by wall-clock time.

Closing

agent.close_traces()  # releases every open trace on this agent

Use it when you need to free opened traces — for example, before opening a fresh batch in a long-lived process.

close_traces() is global

agent.close_traces() releases every open trace for this Agent. Coordinate if multiple parts of your code hold trace handles.

Errors

Trace handles raise the same exceptions as live queries (see Errors), plus two specific to traces:

from zelos_sdk.agent import AgentError, ConnectionTargetError, TraceNotFound

try:
    trace = agent.open_trace("/no/such/file.trz")
except TraceNotFound:
    ...   # path isn't on the agent's filesystem
except ConnectionTargetError:
    ...   # remote agent, and the path was relative or `~`-prefixed

SignalNotFound, AmbiguousSignal, NoData, and QueryRangeTooLarge apply equally to trace queries.

Tips

  • Open once, query many. Re-use the same Trace handle across related queries.
  • time_mode="relative" overlays runs. Plotting two laps, two test cycles, or two days of operation? TraceSet aligns them at t = 0 automatically.
  • time_mode="absolute" reconstructs incidents. When wall-clock correlation matters, pass time_mode="absolute" with absolute start / end.
  • Trust column metadata. When a query spans multiple traces, col.trace_path tells you which file each column came from.
  • trace.path is resolved. Even with a relative input, trace.path returns the resolved absolute form on localhost — handy for logging.
  • Release with agent.close_traces(). Applies to every Trace and TraceSet opened from the same Agent.

What's next

  • Query Live Data

    The same SignalFrame / SignalSeries analysis surface, applied to the live store.

  • Run Actions

    Inspect schemas, execute typed Actions, and handle results.

  • Quick Start

    Connect to an agent and run your first query in under 2 minutes.