Skip to content

What happened at the fault?

State forensics: the fault flag tells you that, the investigation is when and what else. This notebook replays the inverter's fault line with window(), pins the instant it went to Fault, freezes every other signal at that moment with at(), and charts the frequency line through the incident.

You'll use: window() snapshot and changes · at() for a freeze-frame · a cursor-relative query() · plot() · a check on the window you queried

Run it

zelos live demo --backfill 5m --duration 30m    # terminal 1: demo data

Copy the notebook below into fault-timeline.md, then:

zelos notebook run fault-timeline.md

The notebook

---
description: Pin the instant a fault fired and chart what the inverter did around it.
requires-python: ">=3.10"
dependencies:
  - zelos-sdk[notebook]
params:
  duration: "3m"
  around: 15
---

A fault flag tells you *that* something went wrong. The investigation is
about *when* — and what every other signal was doing at that exact moment.
This notebook replays the demo inverter's fault line, finds the instant it
went to `Fault`, and freezes the rest of the bench at that instant.

```python
from datetime import timedelta

from zelos_sdk import connect

agent = connect()
agent.latest("bus1/inverter_status.fault_state")
```

## Replay the transitions

`query()` would hand you one row per sample — thousands of rows repeating the
same string. `window()` is built for states: an opening snapshot plus one
entry per *change*. One param sets how far back the window starts and how long
it runs, so the two can't drift apart:

```python
replay = agent.window(
    "bus1/inverter_status.fault_state",
    start=f"-{params.duration}",
    duration=params.duration,
)
replay
```

## Pin the moment

The first transition into `Fault` is the incident cursor:

```python
faults = [c for c in replay.changes if c.value == "Fault"]
cursor = faults[0].time
print(f"{len(faults)} fault(s) in the window; investigating the first at {cursor:%H:%M:%S}")
```

## What was true at that instant?

`at()` reads every requested signal *at or before* the cursor — the state of
the world when the fault fired. Globs expand the same way they do in a
query, so one call takes the whole inverter plus the pack current:

```python
agent.at(
    ["bus1/inverter_status.*", "bus0/BMS_message/status.pack_current"],
    cursor,
)
```

## What did it look like?

A change's `.time` is a timezone-aware `datetime`, so it feeds straight back
into a query as an absolute bound — here, the seconds either side of the
incident:

```python
freq = agent.query(
    "bus1/inverter_status.grid_frequency",
    start=cursor - timedelta(seconds=params.around),
    end=cursor + timedelta(seconds=params.around),
)
freq.plot()
```

## Did anything else step out of line?

The forensic question the timeline doesn't answer: while the inverter was
faulting, did its frequency line stay inside the envelope it is supposed to
swing in? Check the series you just charted, and the rule covers exactly the
interval the chart shows:

```python
agent.check.that(
    freq["bus1/inverter_status.grid_frequency"].abs().rename("|grid frequency|"),
    "<=",
    100.0,
)
```

The workflow transfers directly to real hardware: replay the state line, pin
the transition, `at()` the suspects at that instant, chart the signal you
suspect, and check whatever the fault was supposed to protect.

What to notice

  • window() beats query() for states and events: an opening snapshot plus one entry per change, instead of thousands of duplicate rows. See Replay a window of changes.
  • at(paths, cursor) is the freeze-frame — the value of every requested signal at or before one instant, rendered as a table of paths and values.
  • A change's .time is a datetime, which is why the cursor can go straight back into at() and into a query's start= / end= as absolute bounds.
  • Durations are strings, offsets are numbers. duration="3m" and start="-3m" read the same grammar, so one param drives both; params.around is plain seconds because it feeds a timedelta.
  • A check on a queried series inherits that query's window — the rule here covers the interval the chart shows, and nothing else. See find the weak cell.
  • The demo's grid_frequency is a synthetic ±100 Hz swing, so the check is an envelope check; on real hardware this is where the nominal band and its trip points go.