diff --git a/docs/contributor_guide/command-prefix-conventions.md b/docs/contributor_guide/command-prefix-conventions.md new file mode 100644 index 00000000000..a5f40161b42 --- /dev/null +++ b/docs/contributor_guide/command-prefix-conventions.md @@ -0,0 +1,162 @@ +# Command Prefix Conventions + +
+ +PyLabRobot uses a small set of standardized verb prefixes on method names. The prefix tells the caller two things at a glance: + +1. **Which world the method operates in** — the physical machine, or PyLabRobot's in-memory resource model. +2. **What kind of effect it has** — does it act, measure, read state, or write state? + +Following these conventions keeps the API predictable, makes code reviews faster, and removes whole categories of *"what does this method actually do?"* confusion. + +
+ Note: For AI coding assistants reading this repo +
+

This convention is partially adopted. A significant fraction of existing methods predate it and use names from before this convention existed. Treat this document as the source of truth when:

+ +

Do not silently rename existing methods to align with the convention as part of unrelated work. Harmonization is a separate, dedicated effort; flag non-compliant names you encounter, but keep your PR scoped to the task you were asked to do.

+
+ +
+ +## The standard + +Two domains (physical machine vs in-memory resource model), six categories, one prefix table per domain. The rest of this document explains the reasoning and edge cases; **this section is the convention**. + +### Physical machine commands + +| Category | Prefixes | Meaning | +|-------------|-------------------------------------------------------|--------------------------------------------------------------------------------------| +| ACTION | `move_`, `aspirate_`, `dispense_`, `pickup_`, `shake_` | Command the machine to do something physical. | +| MEASUREMENT | `measure_`, `read_`, `sense_`, `capture_` | Trigger a transducer/sensor and return the sampled value. | +| MEM-READ | `request_` | Ask the machine to return a value it already holds (register, EEPROM, status flag). | +| MEM-WRITE | `set_` | Write a configuration value to the machine (register, EEPROM, parameter). | + +### Resource-model commands + +| Category | Prefixes | Meaning | +|----------|-----------------------------------|--------------------------------------------------------------------------| +| QUERY | `get_` | Look up a resource or value from the in-memory model. | +| UPDATE | `update_`, `assign_`, `unassign_` | Mutate the in-memory model (change a tracked value, attach/detach resources). | + +### Forbidden synonyms + +Do not introduce new methods using these prefixes. They duplicate categories already covered above and dilute the convention: + +`fetch_`, `retrieve_`, `obtain_`, `acquire_`, `grab_`, `pull_`, `poll_`, `query_` (use `get_` for the model or `request_` for the machine), `write_` (use `set_`), `put_` (use `set_` or `assign_`), `add_*_to_*` / `remove_*_from_*` for parent–child operations (use `assign_` / `unassign_`). + +If you believe an existing prefix is wrong for your case, please open a discussion on the forum before adding a new verb. + +
+ +## Choosing the right prefix + +Walk down this decision flow when naming a new method. + +1. **Does the method touch hardware at all?** + - **No** → resource-model command. Go to step 4. + - **Yes** → physical-machine command. Go to step 2. +2. **Does the method cause physical motion, fluid transfer, or otherwise change the state of the world?** Use an ACTION prefix. +3. **Does it return a value?** + - **Value comes from a transducer/sensor sample** (absorbance, weight, capacitive tip sense, camera image, temperature reading) → MEASUREMENT prefix. Use the verb that fits the modality: `measure_`, `read_`, `sense_`, `capture_`. + - **Value comes from the machine's stored state** (a serial number, a configured offset, a status flag, an EEPROM register) → `request_`. + - **Method writes a value into the machine's stored state** → `set_`. +4. **Resource-model only.** + - **Returns something** (single object or a filtered collection) → `get_`. + - **Mutates a tracked value** (e.g. liquid volume in a well) → `update_`. + - **Attaches/detaches a child resource** → `assign_` / `unassign_`. + +### Picking among the MEASUREMENT verbs + +All four verbs mean *"trigger a sample and return the value."* **`measure_` is the default** — use it unless one of the other three reads more naturally for the specific modality: + +- `measure_` — the default. Pairs naturally with most physical quantities: `Scale.measure_weight`, `ScilaBackend.measure_temperature`. +- `read_` — accepted for quantities idiomatically "read off" an instrument display, mainly photometric plate-reader signals: `Reader.read_absorbance`, `Reader.read_fluorescence`, `Reader.read_luminescence`. +- `sense_` — for discrete/boolean detection where the natural verb is "sense": capacitive tip sense, optical break-beam, limit-switch state. *Reserved; no methods currently use this prefix.* +- `capture_` — for imaging, where "capture" is the standard verb: `capture_image`, `capture_well`. The current public method `Imager.capture()` is a bare verb under this convention and a candidate for harmonization. + +When in doubt, pick `measure_`. If a related method on the same class already uses `read_` for an analogous quantity, match it rather than introducing a sibling prefix. + +
+ +## Background + +
+ Why this exists +
+

Without a convention, equivalent operations end up with inconsistent names. The same method might be called get_temperature on one backend, read_temperature on another, and request_temperature on a third — even though all three round-trip to the device. Worse, get_resource (a pure in-memory lookup) and get_temperature (a firmware query that blocks on serial I/O) look identical at the call site despite having very different cost and failure modes.

+

The discussion that led to this convention lives on the forum: Standardised PLR Command Prefix Proposal.

+
+ +
+ The two axes behind the standard +
+

The two domains in the standard above — physical machine and resource model — are governed by different prefixes precisely because they have different cost and failure modes. A machine request_ call round-trips over serial and can time out; a model get_ call is an in-memory lookup that cannot fail in the same way. Conflating them means callers cannot tell from the name what they are about to pay for.

+

Axis 1 — Domain:

+ +

Axis 2 — Effect:

+ +
+ +
+ +## Edge cases + +**"Position" — measurement or memory read?** If the position is sampled via an encoder query that the firmware returns from a cached register, prefer `request_position` (state read). If a fresh encoder sample is triggered, prefer `measure_position`. When the distinction is invisible to the caller and the firmware itself blurs it, default to `request_`. + +**Methods that act *and* return a value.** Some action methods naturally return data (e.g. an aspirate that returns the actual displaced volume from a pressure trace). Name by the primary effect — the action — and document the return value. `aspirate_` stays `aspirate_`, even though it returns something. + +**Methods that update both model and hardware.** A backend method that sets a hardware parameter *and* records the new value in the resource model is still primarily a machine write — use `set_`. The model update is an implementation detail of staying in sync. + +**Properties vs methods.** For trivial, cheap, side-effect-free reads of model state, prefer a `@property` over a `get_` method (`plate.num_items`, not `plate.get_num_items()`). The prefix convention applies to methods; properties are exempt. + +
+ +## Quick examples + +All method names below are verified against the current codebase. Names marked `# COMPLIANT` follow the convention as written; names marked `# NON-COMPLIANT` exist today but violate the convention and are candidates for harmonization. + +```python +# Physical machine — COMPLIANT +await backend.move_channel_x(channel=0, x=100) # ACTION +await backend.aspirate(...) # ACTION +absorbance = await reader.read_absorbance(plate, ...) # MEASUREMENT +weight = await scale.measure_weight() # MEASUREMENT +present = await backend.request_tip_presence() # MEM-READ +serial = await el406.request_serial_number() # MEM-READ +await star.set_x_offset_x_axis_iswap(x_offset=0) # MEM-WRITE + +# Resource model — COMPLIANT +plate = deck.get_resource("plate_01") # QUERY +items = plate.get_items(["A1", "B1"]) # QUERY +deck.assign_child_resource(plate, location=...) # UPDATE +deck.unassign_child_resource(plate) # UPDATE + +# Existing methods that do NOT yet follow this convention +# (these are real methods today and are valid harmonization targets): +await imager.capture(...) # NON-COMPLIANT — bare verb; should be e.g. `capture_image` +well.set_volume(50.0) # NON-COMPLIANT — model mutation using machine-reserved `set_`; + # should be `update_volume` +tracker.add_liquid(50.0) # NON-COMPLIANT — should be e.g. `update_liquid` (UPDATE); + # same for `remove_liquid`, `add_tip`, `remove_tip` +await vantage.query_tip_presence() # NON-COMPLIANT — uses forbidden `query_` synonym + # (note Vantage also exposes the compliant + # `request_tip_presence` alongside it) +``` + +
+ +## Discussion + +Naming questions and proposed additions to this convention belong on the [forum](https://discuss.pylabrobot.org). When proposing a new prefix, include the category it would join, why none of the existing prefixes fit, and one or two real call-site examples. diff --git a/docs/contributor_guide/index.md b/docs/contributor_guide/index.md index 06292a05ccd..5143f78f22e 100644 --- a/docs/contributor_guide/index.md +++ b/docs/contributor_guide/index.md @@ -7,6 +7,7 @@ contributing how-to-open-source contributing-to-docs +command-prefix-conventions ```