|
STAR
Cal STAR — firmware, DAQ server, and engine design
|
Functions | |
| Iterator[str] | throttle (Iterable[str] chunks, int bytes_per_sec) |
| np.ndarray | _epoch_seconds (col) |
| tuple[np.ndarray, str]|None | sensor_clock (str run_id, str entity) |
| Path | _parquet_path (str run_id, str component) |
| tuple[np.ndarray, np.ndarray] | load_series (str run_id, str component, str time_source="sensor") |
| tuple[np.ndarray, np.ndarray] | _slice (np.ndarray t, np.ndarray v, start, end) |
| tuple[np.ndarray, np.ndarray] | decimate_minmax (np.ndarray t, np.ndarray v, int max_points) |
| tuple[np.ndarray, np.ndarray] | decimate_discrete (np.ndarray t, np.ndarray v, int max_points) |
| dict | series_json (str run_id, list[str] components, start, end, int max_points, str time_source="sensor") |
| long_csv_rows (str run_id, list[str] components, start, end, str time_source="sensor") | |
| str | _csv_cell (str text) |
| wide_csv_rows (str run_id, list[str] components, start, end, str time_source="sensor") | |
Variables | |
| float | SENSOR_CLOCK_TOLERANCE_S = 60.0 |
| tuple | TIME_SOURCES = ("sensor", "db") |
| str | SOURCE_SENSOR = "sensor" |
| str | SOURCE_MONOTONIC = "monotonic" |
| str | SOURCE_DB = "db" |
Load cached parquet series: time-slice, min/max decimate, and wide CSV export.
Times are normalized to epoch seconds (float64) — uPlot's native x unit.
Which clock is the x-axis
-------------------------
Every row carries two. The parquet `time` column is elodin-db's own write time, stamped
as the bridge's TCP flush lands — and the boards ship ~9 samples per UDP packet, so a
whole packet's worth of samples gets written microseconds apart and then nothing for
~100 ms. Plotted on that, a steady 90 Hz channel becomes bursts of 9 points separated by
gaps. The row's `timestamp_ns` field is the real sample time, reconstructed by
BoardClockSync, and it is evenly spaced. `elodin-db` never learns this because
DatabaseConfig.cpp declares that field as an ordinary data column rather than wrapping it
in db.hpp's `builder::timestamp(...)` op, so the DB indexes on arrival.
So the default x-axis is the sensor clock, taken positionally from the sibling
`<entity>.timestamp_ns` parquet — the flattened export writes one file per field from the
same rows, so row i is row i in both.
Reconciling the two, and the publishers that stamp neither
----------------------------------------------------------
Both clocks are the same wall clock: the sample time is anchored to daq_bridge's
`system_clock` receive, and the DB time is elodin-db writing those same rows. They differ
only by write latency — measured at ~1 ms for raw channels, and growing to ~29 ms over a
run for calibrated ones, where the calibration service adds a republish hop. So mixing
them on one axis is coherent; it is the DB clock that drifts, which is the case for the
sensor clock being the default rather than an argument against it.
The sequencer (ACT_CMD, SEQUENCER.state) and the heartbeat router (BOARD.HB_*) stamp
`steady_clock` instead — monotonic since boot, ~0 rather than ~1.79e9. Taken literally
that lands them in 1970. But it is still a good clock, and CLOCK_MONOTONIC is
system-wide, so it differs from CLOCK_REALTIME by a constant over a run: recover that
constant as the median of (db time − stamp) and the channel gets its true emission time.
That is worth doing rather than falling back, because it is exactly the comparison people
care about — when a valve was commanded versus when chamber pressure moved — and the DB
write time it would otherwise use carries 12 ms of jitter (p99 55 ms) on ACT_CMD.
So each entity's clock is classified once:
offset = median(db_time − stamp)
|offset| small → already epoch → use the stamp as-is ("sensor")
|offset| large → monotonic, or a board → use stamp + offset ("monotonic")
whose clock is unset
no stamp column at all → use the DB write time ("db")
A re-anchored channel's *relative* timing is exact; its *absolute* position inherits the
median write latency as a few-ms bias. Worth knowing, not worth refusing to draw.
|
protected |
Quote a header cell only if it needs it. Role names are plain words today, but they are operator-typed config strings, so a comma must not shift the columns.
Definition at line 305 of file series.py.
Referenced by wide_csv_rows().
|
protected |
Parquet arrow timestamps → epoch seconds. Forced through ns so the result is right whatever resolution the column was stored at.
Definition at line 100 of file series.py.
Referenced by load_series(), and sensor_clock().
|
protected |
Definition at line 140 of file series.py.
Referenced by load_series().
|
protected |
Definition at line 188 of file series.py.
Referenced by long_csv_rows(), series_json(), and wide_csv_rows().
| tuple[np.ndarray, np.ndarray] backend.series.decimate_discrete | ( | np.ndarray | t, |
| np.ndarray | v, | ||
| int | max_points ) |
Run-length compress a step series: keep only value-change points plus the endpoints. Lossless for step rendering (repeated values are redundant when step-drawn), and collapses a state/actuator channel from 100k+ samples to the handful of transitions. If a channel is pathologically noisy and still exceeds max_points, stride down as a last resort.
Definition at line 229 of file series.py.
Referenced by series_json().
| tuple[np.ndarray, np.ndarray] backend.series.decimate_minmax | ( | np.ndarray | t, |
| np.ndarray | v, | ||
| int | max_points ) |
Bucket into ~max_points/2 windows, keep each window's min & max in time order. Preserves spikes (unlike stride sampling), which matters for pressure transients. Returns <= max_points points.
Definition at line 194 of file series.py.
References max.
Referenced by series_json().
| tuple[np.ndarray, np.ndarray] backend.series.load_series | ( | str | run_id, |
| str | component, | ||
| str | time_source = "sensor" ) |
Return (t_seconds, values) sorted by time. Cached in-process (immutable runs). With time_source="sensor" the x-axis is the row's own sample time where the publisher stamps a real clock, falling back to the DB's write time where it does not.
Definition at line 148 of file series.py.
References _epoch_seconds(), _parquet_path(), and sensor_clock().
Referenced by long_csv_rows(), series_json(), and wide_csv_rows().
| backend.series.long_csv_rows | ( | str | run_id, |
| list[str] | components, | ||
| start, | |||
| end, | |||
| str | time_source = "sensor" ) |
Yield a tidy/long CSV: `time,component,value`, one component at a time. Streams at constant memory — the right shape for a whole-run dump over many channels, where a wide aligned frame would explode to millions of rows × hundreds of mostly-NaN columns.
Definition at line 280 of file series.py.
References _slice(), and load_series().
| tuple[np.ndarray, str] | None backend.series.sensor_clock | ( | str | run_id, |
| str | entity ) |
The entity's own per-row time in epoch seconds and which kind of clock it was, or None when it has no timestamp column at all. Cached — immutable runs.
Definition at line 108 of file series.py.
References _epoch_seconds().
Referenced by load_series().
| dict backend.series.series_json | ( | str | run_id, |
| list[str] | components, | ||
| start, | |||
| end, | |||
| int | max_points, | ||
| str | time_source = "sensor" ) |
Per-component sliced + decimated arrays for plotting. Every series is capped to ~max_points: continuous via min/max (keeps spikes), discrete via run-length compression (keeps transitions). Without this cap, a long run's uncapped discrete channels return millions of points and crash the browser.
Definition at line 251 of file series.py.
References _slice(), decimate_discrete(), decimate_minmax(), and load_series().
| Iterator[str] backend.series.throttle | ( | Iterable[str] | chunks, |
| int | bytes_per_sec ) |
Pace an iterable of text chunks to at most `bytes_per_sec` cumulative throughput. Sleeps between chunks so total bytes / elapsed ≈ the target rate; with coarse chunks (~100 KB) the sleeps are few and cheap. This runs inside Starlette's threadpool (sync generator), so the sleep never blocks the event loop. <=0 disables throttling.
| backend.series.wide_csv_rows | ( | str | run_id, |
| list[str] | components, | ||
| start, | |||
| end, | |||
| str | time_source = "sensor" ) |
Yield CSV text rows: a 'time' column (epoch seconds) plus one column per component, aligned on the union of timestamps. Discrete series are forward-filled; continuous series are left as sampled (NaN between samples). Two header rows: the component name, then the role it played in this run (from the run's config snapshot; blank where config names nothing). The component name stays the column KEY — analysis scripts key on it — and the role rides underneath as the legend, so a spreadsheet says "Ox Upstream" without the file losing its stable identifiers.
Definition at line 313 of file series.py.
References _csv_cell(), _slice(), and load_series().