Splitting classes
yes this looks good. The split also future-proofs things, modes like TRF, fluorescence polarization, or AlphaScreen, would each become their own capability without touching the existing ones.
Return types
+1 on dataclasses for return types, and keyed by well name rather than array position.
One thought on return types (but it might be off topic): if we know the shape and dtype of the result before the read happens, we could have the capability declare a descriptor upfront. The descriptor is the schema of what data is about to be produced, shape, dtype, units, declared once before any measurement starts.
# Endpoint read of a 96-well plate
{"absorbance": {"dtype": "float", "shape": [8, 12], "unit": "OD"}}
# Kinetic run — 10 cycles of a 96-well plate
{"absorbance": {"dtype": "float", "shape": [10, 8, 12], "unit": "OD"}}
# Spectrum scan — 96-well plate across 24 wavelengths
{"absorbance": {"dtype": "float", "shape": [24, 8, 12], "unit": "OD"}}
The capability can build this from the plate geometry and the measurement parameters before the read starts. The actual data returned conforms to this schema.
Why a descriptor matters:
- Stable return types: the schema is the contract. If it changes, you know exactly what changed — a shape dimension, a unit, a dtype. Deprecation warnings can target specific fields.
- Consumers can prepare: storage backends allocate arrays, live plots set up axes, remote clients know what’s coming — all before the first measurement.
- Kinetic runs declare once: a 100-cycle kinetic run has one descriptor, not 100. Each cycle’s data conforms to the declared shape.
- Self-documenting: you know what the dimensions mean (rows, columns, time, wavelength) without inspecting the data.
This descriptor pattern is also borrowed from bluesky - descriptors, where instruments declare their data schema upfront before producing measurements.
Kinematics / time series
see Modeling plate reading capabilities - #2 by vcjdeboer
Plates vs arbitrary positions
+1 on read_positions as the single backend method, all three frontend methods collapsing into one atomic backend call is clean.
Also a side note: if read_positions receives dataclasses (name + coordinate) rather than live PLR objects, the backend boundary becomes serializable. If we applied that principle across all capabilities — liquid handling, arms, shaking — the entire driver interface would be serializable by design. That would make @koeng protobuf/networking work significantly easier (no custom converters), and it also makes drivers easier to test (just pass in data, no need to construct a full resource tree) and easier to write for new contributors (the driver contract is explicit about what data it needs).