plesty-lib 0.3.4__tar.gz → 0.3.5.dev2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/CHANGELOG.md +97 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/PKG-INFO +1 -1
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/composite_device.md +51 -1
- plesty_lib-0.3.5.dev2/docs/experiment.md +218 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/monitor.md +60 -6
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/gl-secret-detection-report.json +5 -5
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/__init__.py +13 -1
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/io.py +149 -22
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/composite_device.py +400 -26
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/__init__.py +9 -0
- plesty_lib-0.3.5.dev2/plesty/lib/experiment/assets/preflight.yaml +20 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/base_experiment.py +129 -27
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/journal.py +2 -2
- plesty_lib-0.3.5.dev2/plesty/lib/experiment/preflight.py +168 -0
- plesty_lib-0.3.5.dev2/plesty/lib/experiment/runs.py +356 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/__init__.py +2 -0
- plesty_lib-0.3.5.dev2/plesty/lib/monitor/assets/viz.yaml +37 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/sources.py +90 -60
- plesty_lib-0.3.5.dev2/plesty/lib/monitor/viz.py +261 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/tcp_ip_client.py +25 -4
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/shell.py +7 -2
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/logger.py +54 -20
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_composite_config.py +280 -4
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_base.py +4 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_experiment.py +48 -6
- plesty_lib-0.3.5.dev2/tests/test_experiment_preflight.py +114 -0
- plesty_lib-0.3.5.dev2/tests/test_experiment_runs.py +218 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_monitor.py +47 -14
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_utils.py +36 -0
- plesty_lib-0.3.5.dev2/tests/test_viz.py +151 -0
- plesty_lib-0.3.4/docs/experiment.md +0 -119
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/.gitignore +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/.gitlab-ci.yml +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/LICENSE +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/LICENSES/LGPL-3.0-or-later.txt +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/README.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/REUSE.toml +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/analyzer.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/api_reference.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/architecture.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/contributing.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/data_schemas.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/base_device.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/cmd_solver.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/error_handling.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/func_system.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/index.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/logging_system.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/param_system.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/test_helper.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/device/traffic_manager.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/device_layers.svg +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/device_standard.svg +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/imgs/plesty_framework.svg +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/index.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/quickstart.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/toc.yaml +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/docs/ui.md +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/analyzer/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/analyzer/base_analyzer.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/array.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/ctype_manager.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/table.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/types.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/data/units.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/async_wrapper.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_apt_device.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_device_sync.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_tcp_scpi_device.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/base_visa_scpi_device.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/device_utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/doc.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/funcs.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/params.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/device/telemetry.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/experiment/schedule.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/monitor/base_monitor.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/resource_manager.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/service/tcp_ip_server.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/apt.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/data_generator.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/sim/demo_device.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/iceblock.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/solver/scpi.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/client_field_test.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_func_system.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_param_system.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/device_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/experiment_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/field_test.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/field_test_concurrency.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/grouped_param_keys.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/report_artifact.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/resource_allocation.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/schema_params.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/test/schema_refresh.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/apt.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/apt_protocol.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/serial.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/tcp_ip.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/usb_utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/traffic/visa.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/assets/palette.json +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/assets/shell.qss +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/monitor_panel.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/panel.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/app.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/plot.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/qt/recorder.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/ui/theme.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/__init__.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/config.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/dll_utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/error_utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/registry.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/plesty/lib/utils/settings.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/pyproject.toml +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_analyzer.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_analyzer_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_async_wrapper_threading.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_client_field_test.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_config.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_array.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_io_hdf5.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_data_types.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_demo_device.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_apt.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_data_path.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_funcs.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_params.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_scpi.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_device_telemetry.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_experiment_pipeline.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_field_test.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_misc.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_report_artifact.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_schema_params.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_schema_refresh.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_service_manager.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_settings.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_shipped_imports.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_sim.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_solver.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_tcp_ip_resources.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_drivers.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_orphan.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_traffic_utils.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_ui.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/tests/test_visa_discovery.py +0 -0
- {plesty_lib-0.3.4 → plesty_lib-0.3.5.dev2}/uv.lock +0 -0
|
@@ -1,5 +1,102 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.3.5.dev2 — 2026-08-17
|
|
4
|
+
|
|
5
|
+
Development pre-release for field-testing the rig start-up (#36, #37):
|
|
6
|
+
bounded, visible connects, the preflight, and default logging. Same line as
|
|
7
|
+
0.3.5.dev1 — the final release is 0.4.0.
|
|
8
|
+
|
|
9
|
+
- **A run starts only on a rig that is really there** (pol-pl field
|
|
10
|
+
finding: a sweep went on with the powermeter server absent).
|
|
11
|
+
`CompositeDevice.preflight()` checks that every remote sub-device exposes
|
|
12
|
+
the methods listed under the new `requires` config key and answers
|
|
13
|
+
`identity()`; `Experiment.setup()` runs it and raises `RigNotReadyError`
|
|
14
|
+
on any problem. `plesty.lib.experiment.connect_rig(build, optional=…)`
|
|
15
|
+
is the start-up for every experiment's `__main__`: build, preflight, and
|
|
16
|
+
on a failure print which device, at which address, from which variable,
|
|
17
|
+
then ask `[r]etry / [a]bort / [s]kip` (skip only for optional
|
|
18
|
+
sub-devices, returned as `skipped`); non-interactive processes get the
|
|
19
|
+
diagnosis logged and `RigNotReadyError`. Text in
|
|
20
|
+
`experiment/assets/preflight.yaml`.
|
|
21
|
+
- **A composite that is given up closes its clients.** New
|
|
22
|
+
`CompositeDevice.release()` closes every remote client on its owning
|
|
23
|
+
thread (server-side disconnect with a 1 s window, linger 0) and stops the
|
|
24
|
+
threads; `disconnect_all()` does the same for remote clients, and a first
|
|
25
|
+
connect that fails closes the clients that did connect. A client left to
|
|
26
|
+
the garbage collector blocked the interpreter in its ZMQ context's
|
|
27
|
+
`__del__` — seen mid-run after `connect_rig` rebuilt a rig without the
|
|
28
|
+
powermeter. `connect_rig` releases a rig it gives up.
|
|
29
|
+
- **A first connect to a dead server now returns.** The client's connect
|
|
30
|
+
failure path closed its DEALER socket with the default infinite LINGER,
|
|
31
|
+
so `ctx.term()` blocked forever on the undelivered handshake — a rig built
|
|
32
|
+
against one absent server hung with no output, and no attempt ladder or
|
|
33
|
+
deadline could bound it. `_DeviceTCPIPClient` now drops undelivered
|
|
34
|
+
messages (linger 0) when a connect fails and on `close()`.
|
|
35
|
+
`CompositeDevice` also resolves every address before starting any connect
|
|
36
|
+
(a half-configured `.env` raises `ValueError` without touching a server)
|
|
37
|
+
and stops its worker threads when a first connect fails.
|
|
38
|
+
- **`CompositeDevice` signals sub-device connection state and bounds the first
|
|
39
|
+
connect** (#36). Each sub-device carries a `ConnectionState`
|
|
40
|
+
(`CONNECTING → CONNECTED → DEGRADED → DISCONNECTED`), read with
|
|
41
|
+
`status()` / `status(dev)` and observed with `on_status_change(callback)`;
|
|
42
|
+
every transition is logged (WARNING for `DEGRADED` / `DISCONNECTED`).
|
|
43
|
+
New config key `connect_deadline_s` (default 10 s): a server that does not
|
|
44
|
+
answer within it raises `DeviceUnreachableError(dev, address, elapsed_s)`
|
|
45
|
+
from the constructor instead of running attempts × timeout × backoff
|
|
46
|
+
(~40 s per dead server before). Under thread affinity the first connects
|
|
47
|
+
run in parallel on the sub-devices' own threads. `reconnect()` reuses the
|
|
48
|
+
sub-device's configured `timeout_ms` / `attempts` / `backoff_s` instead of
|
|
49
|
+
the 50 s defaults; a call timeout marks the sub-device `DEGRADED` before
|
|
50
|
+
the rebuild. `_DeviceTCPIPClient.set_default_timeout(ms)` added.
|
|
51
|
+
- **Default logging for experiments and composites** (#37): nothing is
|
|
52
|
+
silent anymore without module code. `Experiment` logs
|
|
53
|
+
`Run <id>: N step(s) (k already completed), run dir …`, `Setup: …`,
|
|
54
|
+
`Step i/N <id> (<op>)` / `Step i/N done in x s`, `Teardown: …`, and
|
|
55
|
+
`Run … completed (N steps) in x s`. `CompositeDevice` logs
|
|
56
|
+
`Connecting <dev> @ <addr>` / `Connected <dev> @ <addr> in x s`, each failed
|
|
57
|
+
attempt with the retry delay, `Reconnecting <dev> @ <addr>`, and the timeout
|
|
58
|
+
a retried call ran under. New `plesty.lib.utils.logger.ensure_logging()`
|
|
59
|
+
(root level + one console sink, idempotent) is called by both constructors;
|
|
60
|
+
`setup_logging(app)` builds on it and adds the per-process log file — the
|
|
61
|
+
first `app` names the file, so a rig built before its experiment no longer
|
|
62
|
+
steals the file name.
|
|
63
|
+
|
|
64
|
+
## 0.3.5.dev1 — 2026-08-17
|
|
65
|
+
|
|
66
|
+
Development pre-release for field-testing the run convention (#34); the
|
|
67
|
+
final release of this line is 0.4.0 — the API diff against 0.3.4 (the
|
|
68
|
+
`pattern=` arguments and `data_dir` attributes of the run sources are
|
|
69
|
+
gone, `Experiment(run_root=)` defaults to the mount) is a minor bump.
|
|
70
|
+
|
|
71
|
+
- `plesty.lib.monitor.Viz` (#34): the `monitor` / `render` command line over
|
|
72
|
+
an experiment's panels. The experiment registers one function
|
|
73
|
+
(`@viz.panels`, `Run` → panels) and calls `viz.main()`; run discovery,
|
|
74
|
+
`--run-root/--mount/--data-dir/--run`, live vs `--replay`, `--record`,
|
|
75
|
+
offscreen `render` with self-stop, and the text catalog
|
|
76
|
+
(`monitor/assets/viz.yaml`) are the lib's. `Run(..., data_dir=)` supplies
|
|
77
|
+
the host spelling for runs that did not record it.
|
|
78
|
+
- **Run layout: one `records.jsonl` per run instead of one `data/step_NNNN.json`
|
|
79
|
+
per step** (#34). Each completed step appends one fsynced line (its
|
|
80
|
+
`ResultDocument` + `index`; blobs still go to `data/`), and readers tail
|
|
81
|
+
the file by byte offset — writing and following a long run on a network
|
|
82
|
+
share no longer costs a file open and a directory listing per step. New:
|
|
83
|
+
`plesty.lib.data.append_record` / `read_records` / `record_value`;
|
|
84
|
+
`RunSource`/`ReplaySource` lose their `pattern` argument and gain `unread`
|
|
85
|
+
and `legacy`; the journal's `step_completed` carries `recorded` instead of
|
|
86
|
+
a `result` path; a resumed run also counts steps with a committed record
|
|
87
|
+
as done. Runs in the old layout are still read (`RunSource.legacy`).
|
|
88
|
+
- The run convention is spelled out once and exported (#34):
|
|
89
|
+
`plesty.lib.experiment.runs` — `run_id`/`parse_run_id`, `list_runs`,
|
|
90
|
+
`latest_run`, and `Run` (plan, config, journal status, records, and
|
|
91
|
+
`local()` translation of device-written paths onto this machine).
|
|
92
|
+
- One environment convention for the shared disk: `PLESTY_DATA_DIR` (as the
|
|
93
|
+
acquiring host sees it) and `PLESTY_DATA_MOUNT` (as this machine sees it).
|
|
94
|
+
`Experiment` reads both (or `data_dir=`/`data_mount=`), defaults
|
|
95
|
+
`run_root` to the mount when set, exposes `self.run_id`/`self.run_dir`
|
|
96
|
+
before `setup()` and `self.raw_dir` = `<data_dir>/<run_id>/raw` for the
|
|
97
|
+
writing devices, and journals all three with `run_started`. `run_root`
|
|
98
|
+
still defaults to `runs` without a mount.
|
|
99
|
+
|
|
3
100
|
## 0.3.4 — 2026-08-14
|
|
4
101
|
|
|
5
102
|
- The server never cancels a receive (#33): the old
|
|
@@ -60,7 +60,9 @@ composite = CompositeDevice(
|
|
|
60
60
|
|---|---|
|
|
61
61
|
| `address` | Explicit ZMQ address, skipping the environment |
|
|
62
62
|
| `env` | Environment variable to read, overriding the derived name |
|
|
63
|
-
| `timeout_ms`, `attempts`, `backoff_s` | Forwarded to `connect_client` |
|
|
63
|
+
| `timeout_ms`, `attempts`, `backoff_s` | Forwarded to `connect_client` (first connect and every reconnect) |
|
|
64
|
+
| `connect_deadline_s` | Budget for the *first* connect (default 10 s); past it the constructor raises `DeviceUnreachableError` |
|
|
65
|
+
| `requires` | Remote methods the server must expose; checked by `preflight()` (a wrong address connects fine but is not this device) |
|
|
64
66
|
|
|
65
67
|
The address comes from `address` if given, else the environment; an unset
|
|
66
68
|
variable raises a `ValueError` naming the variable to set.
|
|
@@ -106,10 +108,48 @@ explicitly.
|
|
|
106
108
|
`connect_client(address)` is the module-level builder used for the rebuilds:
|
|
107
109
|
`build_client` plus a connection-handshake retry with linear backoff, which
|
|
108
110
|
matters when several clients open back-to-back against the same server.
|
|
111
|
+
`reconnect(dev)` rebuilds with the sub-device's own `timeout_ms` /
|
|
112
|
+
`attempts` / `backoff_s`, not the defaults.
|
|
109
113
|
|
|
110
114
|
Every multi-device composite gets this robustness for free instead of
|
|
111
115
|
re-implementing it per module.
|
|
112
116
|
|
|
117
|
+
## Connection state and a bounded first connect
|
|
118
|
+
|
|
119
|
+
The composite is never silent about its servers. Constructing it logs one
|
|
120
|
+
line per sub-device (`Connecting hwp @ tcp://… (timeout 5000 ms) …`,
|
|
121
|
+
`Connected hwp @ tcp://… in 0.4 s`, or `Connect to tcp://… failed, attempt
|
|
122
|
+
1/4: … ; retrying in 2 s`) and a summary (`PolPlRig ready in 1.2 s:
|
|
123
|
+
hwp=tcp://…, spec=tcp://…`) — the console sink is switched on by the
|
|
124
|
+
composite itself (`ensure_logging()`), so this shows even when the composite
|
|
125
|
+
is built before the experiment that would configure logging.
|
|
126
|
+
|
|
127
|
+
The **first connect is bounded**: each configured sub-device gets
|
|
128
|
+
`connect_deadline_s` (default 10 s); a server that does not answer within it
|
|
129
|
+
raises `DeviceUnreachableError(dev, address, elapsed_s)` from the constructor
|
|
130
|
+
instead of grinding through attempts × timeout × backoff. Under thread
|
|
131
|
+
affinity the first connects run in parallel on the sub-devices' own threads,
|
|
132
|
+
so three dead servers cost one deadline, not three. Reconnects during a run
|
|
133
|
+
keep the attempt ladder — a server busy finishing a long move must be waited
|
|
134
|
+
for, not declared dead.
|
|
135
|
+
|
|
136
|
+
Each sub-device carries a `ConnectionState` — `CONNECTING → CONNECTED →
|
|
137
|
+
DEGRADED` (a call timed out, the client is being rebuilt) `→ DISCONNECTED`
|
|
138
|
+
(connect/reconnect failed, or `disconnect_all()`):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
composite.status() # {"hwp": ConnectionState.CONNECTED, "spec": ...}
|
|
142
|
+
composite.status("hwp") # ConnectionState.CONNECTED
|
|
143
|
+
|
|
144
|
+
@composite.on_status_change
|
|
145
|
+
def _show(dev, old, new, detail):
|
|
146
|
+
ui.set_led(dev, new) # runs on the thread that changed the state
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
Every transition is logged (INFO, or WARNING for `DEGRADED` /
|
|
150
|
+
`DISCONNECTED`) whether or not anyone subscribes; a subscriber that raises
|
|
151
|
+
is logged and ignored — a broken UI hook must not break the rig.
|
|
152
|
+
|
|
113
153
|
## One thread per sub-device
|
|
114
154
|
|
|
115
155
|
ZMQ sockets are not thread-safe, and even migrating one between threads is
|
|
@@ -161,6 +201,16 @@ for a data directory either fails or means nothing:
|
|
|
161
201
|
composite.set_data_path(frames_dir, devices=["spec"])
|
|
162
202
|
```
|
|
163
203
|
|
|
204
|
+
## Preflight
|
|
205
|
+
|
|
206
|
+
`preflight()` proves each sub-device is the device it is supposed to be and
|
|
207
|
+
answers: a remote sub-device must expose every method in its `requires`
|
|
208
|
+
list and its `identity()` must reply; a local one must have the required
|
|
209
|
+
attributes. It returns `{name: problem}` for the failures (empty when the
|
|
210
|
+
rig is ready) and logs one line per sub-device either way. `Experiment`
|
|
211
|
+
runs it in `setup()`; `plesty.lib.experiment.connect_rig` runs it at
|
|
212
|
+
start-up and asks the operator what to do (see the experiment guide).
|
|
213
|
+
|
|
164
214
|
## Interplay with experiments
|
|
165
215
|
|
|
166
216
|
`Experiment` accepts a `CompositeDevice` at construction; its default
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
# Experiment Framework
|
|
2
|
+
|
|
3
|
+
`plesty.lib.experiment` turns a measurement routine into a **reproducible,
|
|
4
|
+
crash-tolerant run**. The mental model is checkpointed training in deep
|
|
5
|
+
learning: the *plan* is the training schedule, the *journal* is the training
|
|
6
|
+
log, and the completed-step set plus persisted step results form the
|
|
7
|
+
checkpoint.
|
|
8
|
+
|
|
9
|
+
An `Experiment` orchestrates devices through a
|
|
10
|
+
[`CompositeDevice`](device/composite_device.md) — it receives Device
|
|
11
|
+
instances, never raw hardware.
|
|
12
|
+
|
|
13
|
+
## The pieces
|
|
14
|
+
|
|
15
|
+
| Piece | Role |
|
|
16
|
+
|---|---|
|
|
17
|
+
| `Step` | The smallest unit of work: a stable `id`, an operation name `op`, and its `params`. A step either completes (result persisted and journaled) or is re-run on resume. |
|
|
18
|
+
| `Plan` | The frozen, ordered schedule of steps plus the run configuration. Written to disk at run start, never mutated; a SHA-256 `content_hash()` identifies the schedule. |
|
|
19
|
+
| `Journal` | A crash-safe, append-only JSONL event log (`run_started`, `step_started`, `step_completed`, `step_failed`, `run_completed`/`_canceled`/`_aborted`). Replaying it derives the run state and the completed-step set. |
|
|
20
|
+
| `records.jsonl` | One line per completed step: its result document (`plesty.lib.data.append_record` / `read_records`). One file, appended and fsynced per step, tailed by offset — not one small file per step, which is slow to write and slow to follow on a network share. |
|
|
21
|
+
| `Experiment` | The async ABC tying it together: validate → journal → execute with retries → persist results → resume. |
|
|
22
|
+
| `Run` | Read access to a stored run — plan, config, journal status, records, and the translation of device-written paths onto this machine (`plesty.lib.experiment.runs`, which also spells out the naming below and finds runs: `list_runs`, `latest_run`). |
|
|
23
|
+
|
|
24
|
+
Every run owns a directory:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
<run_root>/<run_id>/ # run_id = <name>_<YYYYmmdd-HHMMSS>
|
|
28
|
+
├── plan.json # frozen schedule + config + content hash
|
|
29
|
+
├── journal.jsonl # append-only event log
|
|
30
|
+
├── records.jsonl # one line per completed step: its result document
|
|
31
|
+
└── data/ # blobs the records reference (arrays, images), if any
|
|
32
|
+
└── step_0003.npy
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Each record line is a typed [`ResultDocument`](data_schemas.md) with
|
|
36
|
+
provenance (`step_id`, `op`, `params`) plus the step `index`; a JSON value
|
|
37
|
+
result is inlined, an array or image stays a raw blob in `data/` that the
|
|
38
|
+
line references. The line is the commit — it is appended (and fsynced) only
|
|
39
|
+
after the blob is on disk, and a resumed run treats a committed record as a
|
|
40
|
+
completed step even if the crash came before its journal event. Runs
|
|
41
|
+
written by plesty-lib ≤ 0.3.4 (one `data/step_*.json` per step) are still
|
|
42
|
+
read by `RunSource`, `records()`, and `Run`.
|
|
43
|
+
|
|
44
|
+
## Writing an experiment
|
|
45
|
+
|
|
46
|
+
Subclasses override exactly three lifecycle hooks — `build_plan()`
|
|
47
|
+
(required), `setup()`/`teardown()` (optional, extend the defaults which
|
|
48
|
+
connect/disconnect all devices) — plus one public method per step operation:
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
import asyncio
|
|
52
|
+
from plesty.lib.data import PlestyArray
|
|
53
|
+
from plesty.lib.experiment import Experiment, Plan, Step
|
|
54
|
+
|
|
55
|
+
class LineScan(Experiment):
|
|
56
|
+
def build_plan(self) -> Plan:
|
|
57
|
+
steps = [
|
|
58
|
+
Step(id=f"scan[x={x}]", op="scan_point", params={"x": x})
|
|
59
|
+
for x in range(5)
|
|
60
|
+
]
|
|
61
|
+
return Plan(steps, config={"points": 5})
|
|
62
|
+
|
|
63
|
+
def scan_point(self, x: int) -> PlestyArray:
|
|
64
|
+
# drive devices via self.devices, return the measurement
|
|
65
|
+
return PlestyArray([float(x)], name="signal", unit="a.u.")
|
|
66
|
+
|
|
67
|
+
run_id = asyncio.run(LineScan(run_root="runs").run())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Rules that keep runs reproducible:
|
|
71
|
+
|
|
72
|
+
- **`build_plan()` must be deterministic** for a given configuration —
|
|
73
|
+
resume compares the regenerated plan against the persisted one by content
|
|
74
|
+
hash.
|
|
75
|
+
- **Step ids must be stable** (e.g. `"scan[x=3,y=5]"`) — never derived from
|
|
76
|
+
run time or randomness; resume matches completed steps by id.
|
|
77
|
+
- **Step operations are validated before anything runs**: every `Step.op`
|
|
78
|
+
must name an existing public method and may not be a reserved lifecycle
|
|
79
|
+
method (`build_plan`, `run`, `setup`, `teardown`). Violations raise
|
|
80
|
+
`InvalidOperationError` listing all offending steps — before any device is
|
|
81
|
+
touched or journal written.
|
|
82
|
+
|
|
83
|
+
Step methods may be sync or async; results are awaited automatically. Each
|
|
84
|
+
step is retried (`max_retries`, `retry_sleep` constructor arguments) before
|
|
85
|
+
the run aborts.
|
|
86
|
+
|
|
87
|
+
## Starting on a rig that is really there
|
|
88
|
+
|
|
89
|
+
An experiment's `__main__` builds the rig with `connect_rig` — the same
|
|
90
|
+
start for every experiment: connect the servers, run the composite's
|
|
91
|
+
`preflight()` (each sub-device exposes the methods its configuration
|
|
92
|
+
`requires` and answers `identity()`), and if anything is missing, tell the
|
|
93
|
+
operator which device, at which address, from which variable — and ask:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from plesty.lib.experiment import connect_rig
|
|
97
|
+
|
|
98
|
+
rig, skipped = connect_rig(
|
|
99
|
+
lambda skip: PolPlRig(with_powermeter="pm" not in skip),
|
|
100
|
+
optional={"pm"}, # the experiment can run without it
|
|
101
|
+
)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```text
|
|
105
|
+
The rig is not ready — 1 device(s) failed the preflight:
|
|
106
|
+
- pm: no answer from tcp://127.0.0.1:5557: TimeoutError('Server did not respond in time')
|
|
107
|
+
address tcp://127.0.0.1:5557 (from PM_ADDRESS in .env / the environment)
|
|
108
|
+
Check that the pm server(s) are running and that PM_ADDRESS point at them
|
|
109
|
+
(a wrong port connects fine but is not the expected device).
|
|
110
|
+
[r] retry (after fixing .env or starting the server) [a] abort [s] continue without pm
|
|
111
|
+
>
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
`r` rebuilds the rig (an edited `.env` or a freshly started server is picked
|
|
115
|
+
up), `a` raises `RigNotReadyError`, `s` — offered only when every failing
|
|
116
|
+
sub-device is in `optional` — returns the rig without them and names them in
|
|
117
|
+
`skipped`, so the experiment can drop the matching part of its plan. Without
|
|
118
|
+
a terminal on stdin nothing waits: the diagnosis is logged and
|
|
119
|
+
`RigNotReadyError` raised.
|
|
120
|
+
|
|
121
|
+
The check also runs inside `Experiment.setup()`: a run never starts on a
|
|
122
|
+
composite whose `preflight()` reports a problem, whether or not `connect_rig`
|
|
123
|
+
was used. Declare what each sub-device must be able to do in its
|
|
124
|
+
configuration — that is what turns "the port answers" into "it is the
|
|
125
|
+
stage":
|
|
126
|
+
|
|
127
|
+
```yaml
|
|
128
|
+
devices:
|
|
129
|
+
hwp: {timeout_ms: 5000, requires: [home_stage, move_absolute, get_position]}
|
|
130
|
+
spec: {timeout_ms: 5000, requires: [acquire, get_recent_file, write, query]}
|
|
131
|
+
pm: {timeout_ms: 5000, requires: [measure_power, write]}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## Data on a shared disk
|
|
135
|
+
|
|
136
|
+
Blobs a device writes itself (spectrometer frames, camera images) never
|
|
137
|
+
cross the network: the acquiring host saves them to a shared disk and the
|
|
138
|
+
step record carries only the path. Two machines spell that share
|
|
139
|
+
differently, so it is named once, by environment:
|
|
140
|
+
|
|
141
|
+
| Variable | Meaning |
|
|
142
|
+
|---|---|
|
|
143
|
+
| `PLESTY_DATA_DIR` | the share as the acquiring host sees it (e.g. `G:\RAWDATA`) |
|
|
144
|
+
| `PLESTY_DATA_MOUNT` | the same share as this machine sees it (e.g. `/mnt/group/RAWDATA`) |
|
|
145
|
+
|
|
146
|
+
The experiment reads both (or takes `data_dir=` / `data_mount=` at
|
|
147
|
+
construction). With the mount set, `run_root` defaults to it, so runs land
|
|
148
|
+
on the share as well. Before `setup()` is awaited the run id is known and
|
|
149
|
+
`self.raw_dir` names `<data_dir>/<run_id>/raw` in the host's spelling — one
|
|
150
|
+
folder per run, the same one on resume — which `setup()` routes the writing
|
|
151
|
+
devices to:
|
|
152
|
+
|
|
153
|
+
```python
|
|
154
|
+
async def setup(self) -> None:
|
|
155
|
+
await super().setup()
|
|
156
|
+
if self.raw_dir is not None:
|
|
157
|
+
self.devices.set_data_path(self.raw_dir, devices=["spec"])
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
`run_started` journals `data_dir`, `data_mount`, and `raw_dir`, so a reader
|
|
161
|
+
needs at most its own mount:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
from plesty.lib.experiment import Run, latest_run
|
|
165
|
+
|
|
166
|
+
run = Run(latest_run("/mnt/group/RAWDATA", name="pol_pl"))
|
|
167
|
+
run.config["powermeter"] # the frozen configuration
|
|
168
|
+
run.status()["state"] # "running", "completed", ...
|
|
169
|
+
for record in run.records():
|
|
170
|
+
run.local(record["data_file"]) # G:\RAWDATA\... → /mnt/group/RAWDATA/...
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
`Run.local` uses, in order, an explicit `mount=`, `PLESTY_DATA_MOUNT`, and
|
|
174
|
+
the mount journaled by the machine that ran the experiment (only when that
|
|
175
|
+
directory exists here); with neither a data root nor a mount the path is
|
|
176
|
+
returned as recorded — the case of reading on the acquiring host itself.
|
|
177
|
+
`run.source(mapper)` returns a `RunSource` (live) or, with
|
|
178
|
+
`Run(..., replay_rows=n)`, a `ReplaySource` — see [Monitors](monitor.md).
|
|
179
|
+
|
|
180
|
+
## Crash, cancel, resume
|
|
181
|
+
|
|
182
|
+
`run()` returns the run id. If the run is interrupted — `Ctrl-C`, a crash,
|
|
183
|
+
an aborting step — pass the id back to continue:
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
run_id = asyncio.run(LineScan(run_root="runs").run())
|
|
187
|
+
# ... interrupted?
|
|
188
|
+
asyncio.run(LineScan(run_root="runs").run(resume=run_id))
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
On resume the framework:
|
|
192
|
+
|
|
193
|
+
1. regenerates the plan and **refuses to continue if its content hash
|
|
194
|
+
differs** from the stored one (`PlanMismatchError`) — a silently changed
|
|
195
|
+
schedule or configuration cannot corrupt a run;
|
|
196
|
+
2. replays the journal to find completed steps and skips them;
|
|
197
|
+
3. journals the resume (`run_started` with `resumed: true`) and continues.
|
|
198
|
+
|
|
199
|
+
`teardown()` is always awaited — on completion, failure, and cancellation —
|
|
200
|
+
so devices are released no matter how the run ends.
|
|
201
|
+
|
|
202
|
+
## Contract tests without hardware
|
|
203
|
+
|
|
204
|
+
`plesty.lib.test.experiment_pipeline.ExperimentPipeline` verifies the
|
|
205
|
+
plan/checkpoint contract of an experiment module with **no instrument
|
|
206
|
+
attached** (it never calls `run()`). Five gates:
|
|
207
|
+
|
|
208
|
+
| Gate | Checks |
|
|
209
|
+
|---|---|
|
|
210
|
+
| 1 `test_experiment_subclass` | public `Experiment` subclass, instantiable without devices |
|
|
211
|
+
| 2 `test_plan_deterministic` | `build_plan()` twice → identical `content_hash()` |
|
|
212
|
+
| 3 `test_plan_ops_resolve` | every `Step.op` resolves via the framework's own plan validation |
|
|
213
|
+
| 4 `test_plan_serializable` | `Plan.save`/`load` round-trip preserves the hash |
|
|
214
|
+
| 5 `test_lifecycle_hooks` | `setup`/`teardown` overrides are async |
|
|
215
|
+
|
|
216
|
+
Experiment HUB modules expose one pytest function per gate (SDK gate E1
|
|
217
|
+
verifies their presence and passage — the experiment-tier mirror of device
|
|
218
|
+
gate d1); see [Test Helpers](device/test_helper.md) for the pattern.
|
|
@@ -82,16 +82,19 @@ clock belongs to the caller: a GUI timer, a test loop, a notebook cell.
|
|
|
82
82
|
|
|
83
83
|
| Source | Follows | Typical use |
|
|
84
84
|
|---|---|---|
|
|
85
|
-
| `RunSource` | `<run_dir>/
|
|
85
|
+
| `RunSource` | `<run_dir>/records.jsonl`, tailed by offset | any running experiment |
|
|
86
86
|
| `TelemetrySource` | `TelemetryEvent`s of devices | live device readings |
|
|
87
87
|
| `PushSource` | whatever the producer emits | tests, notebooks |
|
|
88
88
|
|
|
89
89
|
`RunSource` is the generic live view of *any* PLESTY experiment: the
|
|
90
|
-
[experiment framework](experiment.md)
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
90
|
+
[experiment framework](experiment.md) appends one record line per completed
|
|
91
|
+
step, and that line is the commit record of the write, so following the file
|
|
92
|
+
needs no cooperation from the experiment and cannot disturb it. Each poll
|
|
93
|
+
reads only the bytes appended since the last one, so following a run of
|
|
94
|
+
thousands of steps on a network share stays cheap. It also means a viewer
|
|
95
|
+
started mid-run still sees every earlier row. A `Run` from
|
|
96
|
+
`plesty.lib.experiment` builds the source for you (`run.source(mapper)`),
|
|
97
|
+
live or replayed.
|
|
95
98
|
|
|
96
99
|
```python
|
|
97
100
|
from plesty.lib.monitor import RunSource
|
|
@@ -137,6 +140,57 @@ infrastructure. Reusable views live in their own package
|
|
|
137
140
|
(`plesty-common-monitors`); rig-specific ones live next to the experiment
|
|
138
141
|
that needs them.
|
|
139
142
|
|
|
143
|
+
## Watching a run: `Viz`
|
|
144
|
+
|
|
145
|
+
Everything about *watching* is the same for every experiment — which run
|
|
146
|
+
(the newest, or `--run`), where the share is mounted, live or replayed, in a
|
|
147
|
+
window or rendered offscreen to a video, recorded or not. `Viz` owns all of
|
|
148
|
+
it; the experiment writes one function that says which record key means
|
|
149
|
+
what, and that is its whole viz module:
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# plesty/pol_pl/viz.py
|
|
153
|
+
import sys
|
|
154
|
+
|
|
155
|
+
from plesty.common_monitors import SeriesMonitor, SpectrumMonitor, WaterfallMonitor
|
|
156
|
+
from plesty.common_monitors.series import series_mapper
|
|
157
|
+
from plesty.common_monitors.spectrum import spectrum_mapper
|
|
158
|
+
from plesty.lib.monitor import Viz
|
|
159
|
+
from plesty.lib.ui import MonitorPanel
|
|
160
|
+
|
|
161
|
+
viz = Viz("Polarization PL", experiment="pol_pl")
|
|
162
|
+
|
|
163
|
+
@viz.panels
|
|
164
|
+
def panels(run):
|
|
165
|
+
spectra = spectrum_mapper("data_file", locate=run.local, row_key="hwp_deg")
|
|
166
|
+
yield MonitorPanel(SpectrumMonitor(run.source(spectra), name="spectrum"), weight=3)
|
|
167
|
+
yield MonitorPanel(WaterfallMonitor(run.source(spectra), name="map", y_label="HWP angle (deg)"), weight=3)
|
|
168
|
+
if run.config.get("powermeter"):
|
|
169
|
+
power = run.source(series_mapper("hwp_deg", "power_w", y_scale=1e6))
|
|
170
|
+
yield MonitorPanel(SeriesMonitor(power, name="power", y_label="Power (uW)"), weight=2)
|
|
171
|
+
|
|
172
|
+
if __name__ == "__main__":
|
|
173
|
+
sys.exit(viz.main())
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
```bash
|
|
177
|
+
python -m plesty.pol_pl.viz monitor # follow the newest run live
|
|
178
|
+
python -m plesty.pol_pl.viz monitor --replay 2 --record demo.mp4
|
|
179
|
+
python -m plesty.pol_pl.viz render --run pol_pl_20260805-091909 # <run-id>.mp4, no window
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
The function receives a [`Run`](experiment.md#data-on-a-shared-disk):
|
|
183
|
+
`run.source(mapper)` is already a `RunSource` or a `ReplaySource` as the
|
|
184
|
+
subcommand decided — call it **once per view**: a source hands each record
|
|
185
|
+
out once, so two views polling one source would split the rows between them
|
|
186
|
+
(a mapper, by contrast, is freely shared); `run.local` translates the acquiring host's paths onto
|
|
187
|
+
this machine, `run.config` is the frozen configuration. Runs are looked for
|
|
188
|
+
under `--run-root` (default `PLESTY_DATA_MOUNT`, else `runs`); `--mount`
|
|
189
|
+
and `--data-dir` override the share's two spellings for runs that did not
|
|
190
|
+
record them. `render` sets Qt's offscreen platform before the toolkit is
|
|
191
|
+
imported and stops by itself once every source is exhausted; neither
|
|
192
|
+
`--help` nor `resolve_run` needs the `gui` extra.
|
|
193
|
+
|
|
140
194
|
## Testing a monitor
|
|
141
195
|
|
|
142
196
|
`NullRenderer` records draw calls instead of drawing them, so the whole path
|
|
@@ -19,19 +19,19 @@
|
|
|
19
19
|
"version": "8.30.1"
|
|
20
20
|
},
|
|
21
21
|
"type": "secret_detection",
|
|
22
|
-
"start_time": "2026-08-
|
|
23
|
-
"end_time": "2026-08-
|
|
22
|
+
"start_time": "2026-08-17T15:29:32",
|
|
23
|
+
"end_time": "2026-08-17T15:29:32",
|
|
24
24
|
"status": "success",
|
|
25
25
|
"observability": {
|
|
26
26
|
"events": [
|
|
27
27
|
{
|
|
28
28
|
"event": "collect_secrets_analyzer_scan_metrics_from_pipeline",
|
|
29
|
-
"time_s": 0.
|
|
29
|
+
"time_s": 0.45769703,
|
|
30
30
|
"exit_code": 0,
|
|
31
31
|
"git_strategy": "FetchShallow",
|
|
32
|
-
"repo_size_kb":
|
|
32
|
+
"repo_size_kb": 708,
|
|
33
33
|
"commit_count": 1,
|
|
34
|
-
"bytes_scanned":
|
|
34
|
+
"bytes_scanned": 220,
|
|
35
35
|
"pipeline_type": "Tag"
|
|
36
36
|
},
|
|
37
37
|
{
|
|
@@ -15,7 +15,16 @@ from .types import (
|
|
|
15
15
|
cast_basic_type,
|
|
16
16
|
)
|
|
17
17
|
from .table import TableHeader, PlestyTable2D, PlestyTable3D
|
|
18
|
-
from .io import
|
|
18
|
+
from .io import (
|
|
19
|
+
ResultDocument,
|
|
20
|
+
append_record,
|
|
21
|
+
convert_to_hdf5,
|
|
22
|
+
load_document,
|
|
23
|
+
load_result,
|
|
24
|
+
read_records,
|
|
25
|
+
record_value,
|
|
26
|
+
save_result,
|
|
27
|
+
)
|
|
19
28
|
|
|
20
29
|
__all__ = [
|
|
21
30
|
"PlestyArray",
|
|
@@ -24,6 +33,9 @@ __all__ = [
|
|
|
24
33
|
"load_result",
|
|
25
34
|
"load_document",
|
|
26
35
|
"convert_to_hdf5",
|
|
36
|
+
"append_record",
|
|
37
|
+
"read_records",
|
|
38
|
+
"record_value",
|
|
27
39
|
"Units",
|
|
28
40
|
"TableHeader",
|
|
29
41
|
"PlestyTable2D",
|