Skip to content

SDK

0.0.12

Notebook code is pandas code. A SignalFrame and a SignalSeries now answer to pandas' name for everything they do natively, and mean what pandas means by it, so a cell written the way you would write it for a DataFrame runs. A pandas name they don't have raises an error naming the .to_pandas() call that does.

Units convert, and a unit never lies: where the SDK cannot know a result's unit, the result has none, and the numbers are always pandas' numbers. On our notebook coding eval, production gpt-6-luna's first-attempt cells went from 73% correct to 95 of 95.

Added

  • pandas' names on a series. gt/lt/eq/…, isin, astype, fillna, replace, where, clip, shift (also freq=), rolling("1s"), resample("1s").mean(), describe(), quantile, idxmax, value_counts, reindex, asof, corr (spearman without scipy), and add/sub/mul/div with fill_value=. Masks combine with &, |, ^ and ~, and a series slices by time: speed["+10s":"+20s"]. Read more.
  • pandas' names on a frame. frame[label] = series and assign() add a column that keeps its unit. frame[mask], iloc and loc select rows, and frame.iloc[-1] is one row. groupby, corr, index, dtypes, shape, to_numpy(), fillna(), rename(columns=) and SignalFrame.from_pandas() round out the hand-off. Read more.
  • Unit conversion. power.to("kW"), energy.to("Wh"), current.integrate().to("Ah"), SI prefixes, and the units vehicle and battery catalogs use. V + mV reads in V. A degC reading converts to degF with its offset, and a difference of readings is a ΔdegC that converts by scale alone.
  • runs() turns a boolean series into one row per event: start, end, duration, samples, and whether both ends were recorded. min_samples= and min_duration= keep the long ones: (speed == 0).runs(min_duration="2s").
  • rate() is the change per second between neighboring samples, in unit/s.
  • "Did you mean" when a name misses: frame["pack_voltage"], catalog["motor_torque"], and catalog.match("torque") all name what you meant.
  • numpy interop. np.sqrt(series), np.isfinite(series) and np.cumsum(series) return series on the same time axis, with the unit they imply.
  • submit_text= relabels an action's Execute button: @action("Flash Firmware", submit_text="Flash").
  • read_only=True marks an action that changes nothing, such as one that lists adapters. Zelos AI runs it without asking first: @action("Detect Adapters", read_only=True).
  • zelos_sdk.agent_url() returns the agent URL init() settled on, or None before init().
  • TraceLoggingHandler takes an existing TraceSource, so log records land on the source an extension already owns. source_name= still works.
  • TraceReader reports each event's event_type.
  • schemas.CanFrame, the zelos.can.frame.v1 schema for undecoded CAN frames.
  • Batch emit and flush across the extension ABI, for producers that emit from a native thread.

Changed

  • frame.signals is a SignalCatalog, the same object agent.signals() returns, with keys(), values() and items(). Iteration, len(), and int and slice indexing are unchanged.
  • frame.columns is the column labels, as in pandas, and iterating a frame yields labels. The column metadata is frame.meta.
  • A missing sample compares as pandas compares NaN: unequal to everything, and neither above nor below anything. A NaN is a missing sample.
  • Arithmetic between series on different time axes aligns them by timestamp, as pandas does, instead of raising. Comparisons still need one axis.
  • A result whose unit cannot be known has none. T * 2, -T, abs(T) and T.sum() of a temperature reading keep their numbers and drop the unit, and so does multiplying or dividing by a list or array. A number keeps the unit: power * 0.001 is still in W, so use .to("kW").
  • A derived name says what was computed. A number joins it (v × 1000, torque < 0), and diff, abs, cumsum, isna, notna and negation name themselves.
  • f"{v:.2f}" formats the bare number; f"{v}" and print(v) still show the unit.
  • A boolean sum() is an int, and series == "RUN" compares sample by sample.
  • plot() fits the y-axis to the data, as the app's plot panels do; zero=True restores the baseline. The legend sits on top and wraps long names.
  • A check on a reshaped frame (ffill(), dropna(), resample()) evaluates the samples the frame holds, not the stored ones it was reshaped from.
  • Check artifacts from the CLI and the pytest plugin share one format, schema_version 3, and either loads back as a suite.

Deprecated

  • frame.column_names — use frame.columns.

Fixed

  • series == "RUN" returned False, Python's identity fallback, instead of comparing samples.
  • integrate() and derivative() computed in float epoch seconds, about 1 µs of resolution; they now work in nanosecond offsets.
  • t - t.mean() of a temperature read degC; it is a ΔdegC.
  • A locally built frame showed no units in its repr, and frame[signal] raised for its own signals.
  • A check on a CamelCase signal, which every DBC signal is, failed with "No field named".
  • The pytest plugin failed to load on Python 3.10.
  • A time in a named zone, such as a Timestamp in US/Eastern, raised "time must be a datetime".

Upgrade notes

Most 0.0.11 code runs unchanged. Three spellings change meaning:

# 1. frame.columns is labels now; the metadata moved.
[c.signal for c in frame.columns]     # before
[c.signal for c in frame.meta]        # after

# 2. Iterating a frame yields labels; the Signals are in frame.signals.
for signal in frame: ...              # before
for signal in frame.signals: ...      # after

# 3. Units come back from pandas only when you name them.
SignalFrame.from_pandas(df, units=frame.signals)
0.0.11

The read side of the SDK — everything you do after the data is recorded — is now one surface. connect() returns a handle that browses the catalog, queries live and recorded data, computes on unit-carrying series, charts, checks invariants, and re-opens .trz files, with one glob grammar and one time grammar across every call. It is the surface Zelos Notebooks run on.

Everything you already wrote keeps working: every old spelling is kept as a deprecated alias that warns once. The behavior changes below are the exceptions, and each one replaces a result that was quietly wrong.

Added

  • connect() — one entry point for a running agent. It verifies the connection before returning, reads ZELOS_AGENT_URL when given no argument, and works as a context manager. Read more.
  • SignalFrame — agent.query(paths, start=…) returns a frame that is one row per distinct timestamp, one column per signal, and null wherever a signal had no sample. ffill(), dropna(), between(), resample(), head(), rename(), short_names() and describe() operate on it directly. Read more.
  • SignalSeries carries its unit through arithmetic. voltage * current arrives as W, .integrate() over time gives J, and power / current cancels back to V. Adding mismatched units raises instead of lying, and reductions return a NamedScalar that prints 3.629 V.
  • plot() on a frame or a series emits Vega-Lite. It renders inline in the app, offline in an exported HTML page, and in any Jupyter viewer — with nothing added to dependencies.
  • One glob grammar. * and ? expand against the catalog everywhere a path is accepted: match(), query(), latest(), at(), window() and export().
  • One time grammar. The sign tells a time from a duration: "-30s" is a point on the clock, "30s" is a length. ISO 8601 strings, datetime and timedelta are accepted wherever they fit.
  • agent.check.that(lhs, op, rhs) reports a verdict instead of raising, so a suite always runs to completion. Every result carries its evidence — the first violating sample on a failure, the closest approach on a pass — with a timestamp. agent.check.count() measures without judging. Read more.
  • CheckResults.raise_if_failed() raises AssertionError listing every check that did not pass, each with its predicate, window and evidence. For a single check, assert agent.check.that(...) — a result is truthy when it passed.
  • agent.window() returns a ReplayWindow — the opening snapshot plus one entry per change — for states and events, where a query would return thousands of rows repeating one string.
  • agent.watch() streams live values on an interval, with a deadline.
  • agent.export() saves a window to a .trz file, and .open() re-reads it with the same calls you used against the agent.
  • Everything renders itself. Agent, SignalCatalog, SignalFrame, SignalSeries, Snapshot, ReplayWindow, Trace and CheckResult all print a useful summary and render as a rich table in a notebook. to_pandas() is for handing data to pandas, not for looking at it.

Changed

  • A multi-signal frame is one row per timestamp, not one row per message table. Cells are null where a signal had no sample; nothing is interpolated on the way in.
  • A bare int passed where a time belongs raises below 1015 instead of being read as an epoch in nanoseconds. A number that small is nearly always seconds by mistake — write "-30s".
  • A naive datetime is accepted and read as UTC instead of being rejected.
  • Wildcards in query() expand against the query window rather than a fixed 60-second catalog, so a glob over a long window matches what the window actually contains.
  • latest() takes a wildcard. agent.latest("bus0/BMS_message/cells.*") returns a Snapshot of every match; one literal path still returns a single LatestValue.
  • Trace.latest() / Trace.at() and the TraceSet pair return the live shapes — a LatestValue for one literal path, a Snapshot otherwise — and ReplayWindow.snapshot is a Snapshot rather than a list.
  • TraceSet.query() on offset bounds puts the runs on one grid. Every sample is numbered by how many sample periods into its own run it sits, and the runs join on that number at the coarsest run's rate, so two runs recorded at different sub-second phases share rows instead of leaving each other's columns null. Only the edge of the window can leave a row unshared, when it holds one more sample of one run than of the other. to_pandas() on such a frame indexes by offset, not by a 1970 date.
  • Trace.signals() and TraceSet.signals() come back sorted by path, and so do the columns of a Trace.query() frame.
  • Relative times on a file anchor to the file's own span rather than to wall-clock now.
  • where() masks out values as nulls instead of NaN.
  • Mixing a series with a pandas or numpy object raises instead of silently producing garbage. Convert explicitly with to_pandas().
  • count() returns a CheckResult whose .count is None on error, instead of 0 — a failed measurement no longer reads as a real zero.
  • A literal-vs-literal type mismatch in a check raises when the check is built, instead of producing an error row at evaluation.
  • to_pandas() returns a timezone-aware UTC DatetimeIndex. Pass index=False for time as an ordinary column.
  • producers= on query(), latest(), at() and window() defaults to every connected producer instead of localhost only. On check.that() / check.count() it stays the local store; a check reports one verdict per target.
  • A trace duration check (temporal="for_duration" / "within_duration") anchors at the end of the recording — its last N seconds — instead of the start.
  • trace.check accepts lookback= for signature parity with agent.check and ignores it.
  • Trace checks no longer emit zelos.check.result.v1 events.
  • std() uses ddof=1 — the sample standard deviation, matching pandas.
  • median() is exact instead of approximate.
  • An enum value compares equal to its wire string: DataType.Int32 == "int32".
  • A derived series names itself. voltage * current is voltage × current, and ÷, +, − follow; a chart legend never reads null.
  • A NamedScalar prints as a measurement: four significant digits and the unit (3.629 V), with f"{peak:.2f}" keeping the unit too. float(peak) is still the exact value.
  • ExportResult and TraceSet.series() render themselves — one row per producer, one column per run.
  • from zelos_sdk import * binds the agent TimeRange.
  • Every repr shares one time and number format — UTC to the millisecond, four significant digits, the unit after the value — and a check result renders as a checkerboard row: when it happened, the rule with the measured value in it, the window, the verdict.
  • window(duration=…) with a plain number means seconds, not nanoseconds.
  • TraceSet.time_range is a TimeRange.
  • watch() yields a Snapshot — a Mapping that renders itself — instead of a plain dict.
  • A cell that raises AssertionError fails the notebook run (exit code 1); any other exception errors it.
  • --param names must be ASCII Python identifiers — no keywords and no dunders — so a parameter is always usable as params.<name> in a cell. A bad name is refused before the run starts.
  • Notebook front matter no longer gains an empty dependencies: list when the notebook declares none.
  • Core dependencies shrank to jsonschema and pyarrow. pytest, rich and colorama are no longer installed by default; pip install "zelos-sdk[test]" restores pytest and rich, and [notebook] adds the analysis stack (pandas, numpy).

Deprecated

Each of these still works and raises one DeprecationWarning naming its replacement. Nothing is removed.

Old New
Agent.connect() zelos_sdk.connect(target, timeout=…)
Agent.health_check() Agent.health()
Agent.resolve_paths() agent.signals().match(pattern)
SignalCatalog.by_path(path) catalog[path]
SignalCatalog.to_list() list(catalog)
SignalSeries.derive() SignalSeries.from_arrow(values, name=…, unit=…)
SignalSeries.get() SignalSeries.to_pandas()
SignalSeries.time_arrow() the SignalSeries.time attribute
sort_order= sort="asc" / sort="desc"
relative_start= / relative_end= start="+30s" / end="+30s"
time_mode= nothing — the bounds pick the axis
export(paths=…) export(signals=…)
lookback= on query() nothing — wildcards resolve against the query window

Kept without a warning: the [analysis] extra is an alias of [notebook]; TraceSet.traces and TraceSet.max_duration remain as properties beside the new TraceSet.time_range; and every alphabetic and natural-language operator spelling (gt, is greater than, is not equal to, in, …) still parses to the same canonical operator as its symbol.

Fixed

  • integrate() and derivative() refuse a series with nulls instead of guessing across a gap, and name ffill() / dropna() as the fix.

Upgrade notes

Three one-line fixes cover almost every migration:

# 1. to_pandas() already indexes by time.
frame.to_pandas().set_index("time")   # before
frame.to_pandas()                     # after — or to_pandas(index=False) to keep the column

# 2. Look a signal up by path with an index.
catalog.by_path("bus0/BMS_message/status.pack_current")   # before
catalog["bus0/BMS_message/status.pack_current"]           # after

# 3. A relative time is a signed string, not a number.
agent.query(paths, start=-30)      # before
agent.query(paths, start="-30s")   # after

If you combine columns from different messages, add .ffill().dropna() — a frame is joined on time and sparse by construction, and integrate() / derivative() refuse a series with nulls rather than guessing across a gap.

0.0.10

Added

  • Agent SDK (zelos_sdk.agent): Connect to a running Zelos Agent from Python and query live or recorded data through one handle — browse the signal catalog, query and downsample time-series into Arrow/pandas, read latest values, watch for live updates, run actions, manage extensions, and open .trz trace files. Read more.
  • Layout version history: agent.layouts CRUD (list/show/create/update/delete) plus full version history — list a layout's versions, restore a previous version, or label one. Read more.
  • Typed Check API: agent.check.that(lhs, op, rhs) asserts a predicate against a live signal or .trz trace, with temporal quantifiers such as always/eventually evaluated server-side; agent.check.suite(...) runs a JSON suite of checks in one round-trip. Read more.
  • zelos_sdk.schemas.Log — a predefined schema for typed log events (zelos.log.v1); pass a schema class directly to add_event(name, schema) instead of a field list. Read more.
  • TraceSource.log_batch() — log a pyarrow.RecordBatch or Table directly for zero-copy, high-throughput ingestion.
  • TraceSource.log_many() — log a list of (time_ns, event_name, data) tuples in a single call, cutting Python↔Rust overhead for high-frequency sources like CAN decoders.
  • TraceSource(name, strict=True) — opt into strict type coercion; rejects lossy float→int narrowing instead of silently truncating it.
  • TraceNamespace.drain() — flush every source and block until all queued events reach subscribers, for deterministic teardown ahead of closing a TraceWriter.

Changed

  • Action execution now returns a typed ActionExecuteResult (value + status) with PASS/FAIL/ERROR/DONE states instead of a bare value; the old ActionResult/ExecuteStatus names remain as deprecated aliases.
  • Action JSON Schema and UI Schema property order now matches @action declaration order end-to-end instead of being alphabetized, so generated forms render fields in the order they were declared.

Fixed

  • SignalFrame now resolves columns correctly when the same signal name appears across multiple producers or segments, raising AmbiguousSignal on a genuine tie instead of matching the wrong column.
  • TraceReader no longer drops a trace's last row when its timestamp has sub-microsecond precision.
  • TraceWriter now captures every event logged before its with block exits, without needing a sleep before close to avoid losing in-flight data.
  • TraceSourceCache now stores a Python None as a null value instead of raising an error.
0.0.9

Added

  • FolderPickerWidget for Actions, enabling folder selection in extension UIs
  • pyarrow as a direct dependency — no longer needs to be installed separately for TraceReader

Fixed

  • NaN and Infinity float values are now properly stored as null and read back as None instead of causing serialization errors
0.0.8

Added

  • TraceReader API: Read .trz trace files for offline analysis, debugging, and post-processing. Read more.
    • Query specific signals and time ranges from trace files
    • Discover available fields hierarchically (sources → events → fields)
    • List data segments and metadata
    • Full PyArrow integration for efficient data processing
  • Trace File Utilities:
    • Hook to open existing trace files (useful for merging WAL and TRZ after ungraceful exit)
0.0.7

Added

  • Added value-table support to TraceSourceCacheLast
  • Additional widgets and validation hooks for Actions

Fixed

  • Addressed lingering lint warnings across pytest helpers and ensured artifact directories are configurable in automated runs.
0.0.6

Added

  • Action decorators now capture a field_type, enabling richer, type-aware rendering in the Zelos App without custom widgets.
  • Action parameters default to required=True, surfacing missing inputs during validation instead of at execution time.

Improved

  • Updated examples and tests to exercise the stricter defaults so extension authors can adopt the new metadata with confidence.
0.0.5

Added

  • Zelos Actions: A powerful new way to script interactions with your devices and services.
    • Define actions with simple Python functions and decorators.
    • Automatic discovery and registration of actions.
    • Support for various field types for action inputs, including object.
    • Note: Only available in the python release of the SDK

Changed

  • Dependencies: Removed the setuptools dependency.

Fixed

  • Zelos Trace:
    • Resolved a deadlock that could occur in async contexts.
    • Fixed an issue that could prevent querying data if a TraceSegmentStart message was missed.
    • Fixed URL handling in the TracePublishClient.