Find the weak cell¶
The post-bench triage: eight cell voltages, one of them quietly dipping under the undervoltage floor once per discharge cycle. The demo generator plants this fault deliberately — the notebook finds it without being told where to look, then converts the eyeball diagnosis into a check that runs unattended.
You'll use: glob queries · describe() · frame.items() ranking · plot() · check.count() · check.that() on a series
Run it¶
Copy the notebook below into find-the-weak-cell.md, then:
The notebook¶
---
description: Rank a pack's cells and find the one that sags under load.
requires-python: ">=3.10"
dependencies:
- zelos-sdk[notebook]
params:
window: "-3m"
warn_floor: 3.0
hard_floor: 2.5
---
A battery pack is only as strong as its weakest cell. This notebook is the
triage you'd run after a bench session: pull every cell voltage, rank the
cells, spot the one that sags, and turn the eyeball diagnosis into a check
that can run unattended.
The demo generator plants this exact fault: one of the eight cells dips under
the 3.0 V undervoltage floor once per discharge cycle while the others stay
healthy. Let's find it, without being told which one it is.
```python
from zelos_sdk import connect
agent = connect()
frame = agent.query("bus0/BMS_message/cells.*", start=params.window)
frame
```
## Spread the pack out
`describe()` is the whole-pack summary — count, mean, standard deviation and
quartiles per column, with each column's unit on the first row. The healthy
cells cluster tightly; the weak one's minimum falls out of the pack:
```python
frame.short_names().describe()
```
## Name the culprit
Every column of a frame is a `SignalSeries`, so ranking is one `min()`:
```python
worst = min(frame.values(), key=lambda s: s.min())
worst
```
The series prints its path, unit, length, null count, time extent, and a
preview of its values — enough to confirm you grabbed the right column.
```python
worst.min()
```
An aggregation is a `NamedScalar`: a float that still knows what it measured
and what unit it is in, so the minimum reads as a voltage everywhere it lands.
## See the sag
All eight cells on one chart. `short_names()` puts `cell_0` in the legend
instead of the full path:
```python
frame.short_names().plot()
```
## How often did it breach?
Triage has two thresholds: a warning floor you want to know about, and a hard
floor that means "do not ship". `count()` answers the first — it measures
without judging, and returns the number:
```python
agent.check.count(worst, "<", params.warn_floor)
```
## Turn it into a check
The hard floor is the rule. Passing the *series* checks exactly what you just
queried and charted — the check inherits the frame's window, so there is no
second time argument to keep in sync:
```python
agent.check.that(worst, ">", params.hard_floor)
```
A check reports as a row carrying its result, the rule, and the evidence —
for a pass, the closest the data came to breaking it. The weak cell spends
the back of every discharge cycle under the warning floor, a few dozen
samples in three minutes, and bottoms out just above 2.8 V — under the
warning floor, over the hard one. That is the triage finding, and it is why
this notebook ends green.
Re-run this against your own pack by changing the query path — the notebook
is the procedure, the params are the knobs. For the unattended version that
fails CI when a cell sags, see the *release gate* example.
What to notice¶
describe()finds the fault before the chart does. The per-cell minimum separates the weak cell from the healthy cluster numerically; the chart then confirms it visually.- A series check inherits its window.
check.that(worst, ">", floor)checks the samples you queried, over the range you queried them for; passinglast=alongside a series is an error, because the two could disagree. See Check what you queried. count()is a measurement, not an assertion: it returns how many samples matched, and never fails a run. That is what makes it the right tool for a warning threshold you are still calibrating.- A failing check fails the run. This notebook stays green because the weak cell clears the hard floor. Point a rule at something that really is out of spec and the run exits
1— which is the whole idea behind the release gate; see Notebooks as tests. - Signals by path, series by column.
agent.signal(path)is a path-only handle the agent resolves at evaluation time;frame[path]is the data you already have. Use the handle when you want a window the frame doesn't cover. - Swap the query path for your own pack and the procedure transfers unchanged — the two floors are the contract knobs.