Monitor Framework
plesty.lib.monitor formalizes the platform’s live view: a Monitor
projects data as it arrives onto whatever should be drawn. Where an
analyzer is a pull-style transform of a finished dataset, a
monitor is a push-style projection of a running one — a measuring
experiment’s step records, a device’s telemetry, a hand-fed stream.
Watching never disturbs measuring. A monitor holds no device handle, sends no command, and writes nothing back; it reads what the experiment already persisted. That is why a viewer can be started, closed, and restarted in the middle of an eight-hour run without anyone thinking twice.
The three layers
The contract splits so that no layer depends on the one above it:
Layer |
What it does |
Depends on |
|---|---|---|
Source |
|
nothing (stdlib) |
Projection |
|
the data layer |
Rendering |
|
a GUI toolkit ( |
Only the third layer knows a toolkit exists, so the first two are testable headless and the same monitor renders in a Qt shell, in a recording, or into a report.
The contract
A subclass declares the channels it consumes and implements exactly two
methods — traces() (what is drawn, declared once) and update() (what to
draw for one frame):
from plesty.lib.monitor import Frame, Monitor, TraceData, TraceSpec
class Spectrum(Monitor):
title = "Spectrum"
input_schema = {
"wavelength": {"dtype": "array_float", "unit": "nm",
"description": "Wavelength axis."},
"counts": {"dtype": "array_float", "unit": "counts",
"description": "Detector counts."},
}
def traces(self) -> list[TraceSpec]:
return [TraceSpec("spectrum", "line", x_label="Wavelength (nm)",
y_label="Counts", color="cyan")]
def update(self, frame: Frame) -> dict[str, TraceData]:
return {"spectrum": TraceData(x=frame["wavelength"], y=frame["counts"])}
submit() runs the base machinery around update():
Frame validation — channel names, dtypes, and shapes are checked against
input_schema; array channels are coerced toPlestyArraywith the schema unit, because frames arrive from JSON records and telemetry events rather than from typed Python.update()— the subclass projection, pure with respect to the outside world: it reads the frame and its own accumulated state, nothing else.Trace check — the returned keys must be declared traces; a trace left out keeps what it was showing.
Fan-out — every attached renderer is drawn, and
snapshot()keeps the last payload so a renderer attaching later shows the current state instead of an empty plot.
Schema entries use the same dtype vocabulary as device and analyzer schemas
(resolve_dtype), with the same shape and required
keys.
TraceSpec.color names a design token ("cyan", "violet", "mint", …),
never a hex value: the renderer resolves it against the
theme, so a view drawn in the shell, in a report, and on a
second rig looks the same.
Sources
Every source answers one question — what appeared since I last asked? —
through a non-blocking poll(). No source starts a thread or blocks, so the
clock belongs to the caller: a GUI timer, a test loop, a notebook cell.
Source |
Follows |
Typical use |
|---|---|---|
|
|
any running experiment |
|
|
live device readings |
|
whatever the producer emits |
tests, notebooks |
RunSource is the generic live view of any PLESTY experiment: the
experiment framework appends one record line per completed
step, and that line is the commit record of the write, so following the file
needs no cooperation from the experiment and cannot disturb it. Each poll
reads only the bytes appended since the last one, so following a run of
thousands of steps on a network share stays cheap. It also means a viewer
started mid-run still sees every earlier row. A Run from
plesty.lib.experiment builds the source for you (run.source(mapper)),
live or replayed.
from plesty.lib.monitor import RunSource
source = RunSource(
"runs/qd_fss_20260804-214826",
# The experiment's record shape stays its own business; the mapper picks
# out the channels this view declares. Returning None drops a record —
# bookkeeping steps such as the rig identity are not measurements.
mapper=lambda rec: None if "hwp_deg" not in rec else {
"wavelength": rec["wavelength_nm"], "counts": rec["counts"],
},
)
monitor = Spectrum(source)
monitor.pump() # → submits every row persisted so far
source.finished # → the journal says the run is over
Push-style producers (telemetry hooks fired on a device thread) only buffer; the frames are handed out on the consumer’s thread. Buffers are bounded, so a slow consumer drops old frames rather than exhausting memory.
Design rules
Read-only. A monitor never writes back. Anything that commands hardware is a control panel, not a monitor — a different
Panelin the same shell.No threads, no polling loops. The framework owns no clock; sources are drained by whoever has one. This mirrors the telemetry system, which owns no loops either.
Projection is pure.
update()touches no device, file, or GUI object. That is what makes a monitor testable without a display and reusable in a recording or an offline re-render.Bounded memory.
historycaps the frames kept; accumulating views cap their own series. A day-long run must not grow the viewer without bound.Errors stay local. A failing view never blinds the others: the shell catches per-panel failures and shows them in that panel’s title bar.
Where concrete monitors live
plesty-lib ships the contract, not the views — a spectrum plot is not core
infrastructure. Reusable views live in their own package
(plesty-common-monitors); rig-specific ones live next to the experiment
that needs them.
Watching a run: Viz
Everything about watching is the same for every experiment — which run
(the newest, or --run), where the share is mounted, live or replayed, in a
window or rendered offscreen to a video, recorded or not. Viz owns all of
it; the experiment writes one function that says which record key means
what, and that is its whole viz module:
# plesty/pol_pl/viz.py
import sys
from plesty.common_monitors import SeriesMonitor, SpectrumMonitor, WaterfallMonitor
from plesty.common_monitors.series import series_mapper
from plesty.common_monitors.spectrum import spectrum_mapper
from plesty.lib.monitor import Viz
from plesty.lib.ui import MonitorPanel
viz = Viz("Polarization PL", experiment="pol_pl")
@viz.panels
def panels(run):
spectra = spectrum_mapper("data_file", locate=run.local, row_key="hwp_deg")
yield MonitorPanel(SpectrumMonitor(run.source(spectra), name="spectrum"), weight=3)
yield MonitorPanel(WaterfallMonitor(run.source(spectra), name="map", y_label="HWP angle (deg)"), weight=3)
if run.config.get("powermeter"):
power = run.source(series_mapper("hwp_deg", "power_w", y_scale=1e6))
yield MonitorPanel(SeriesMonitor(power, name="power", y_label="Power (uW)"), weight=2)
if __name__ == "__main__":
sys.exit(viz.main())
python -m plesty.pol_pl.viz monitor # follow the newest run live
python -m plesty.pol_pl.viz monitor --replay 2 --record demo.mp4
python -m plesty.pol_pl.viz render --run pol_pl_20260805-091909 # <run-id>.mp4, no window
The function receives a Run:
run.source(mapper) is already a RunSource or a ReplaySource as the
subcommand decided — call it once per view: a source hands each record
out once, so two views polling one source would split the rows between them
(a mapper, by contrast, is freely shared); run.local translates the acquiring host’s paths onto
this machine, run.config is the frozen configuration. Runs are looked for
under --run-root (default PLESTY_DATA_MOUNT, else runs); --mount
and --data-dir override the share’s two spellings for runs that did not
record them. render sets Qt’s offscreen platform before the toolkit is
imported and stops by itself once every source is exhausted; neither
--help nor resolve_run needs the gui extra.
Testing a monitor
NullRenderer records draw calls instead of drawing them, so the whole path
— source → validated frame → projection — is testable with no GUI:
monitor = Spectrum(source)
renderer = monitor.attach(NullRenderer())
assert monitor.pump() == 92
assert len(renderer.drawn) == 92
assert monitor.snapshot()["spectrum"].y.max() > 0
See also
Shell framework — the home window that hosts monitors as rearrangeable sub-windows.
Experiment framework — what
RunSourcefollows.Device telemetry — what
TelemetrySourcesubscribes to.