diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 00000000000..6ba2db8b33f --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,15 @@ +# Agent guide + +Orientation for agents (and humans) working in this repo. + +## Guides + +- **Writing a device driver + its hello-world notebook:** [`docs/contributor_guide/device-driver-guide.md`](docs/contributor_guide/device-driver-guide.md) + +## Working principles + +- **Scope tightly.** Make the change that was asked for and nothing else — no drive-by rewrites of comments, constants, or logic, no unrequested docs/config side-quests. +- **Code documents what it does, not its history.** No diary/changelog comments ("NEW", "now", "previously"), no provenance/origin stories in code, commits, or PRs. Describe what the thing *is*. +- **Keep the repo permanent.** No one-time/backfill/migration scripts committed — run those ad-hoc. +- **Lint before shipping:** ruff + mypy (2-space indent), matching the repo's config. +- **Never drive real hardware without explicit, per-run approval.** diff --git a/docs/conf.py b/docs/conf.py index d5676a7e9b2..ff4d9971b99 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,8 +176,8 @@ "installation.html": "user_guide/installation.html", "contributing.html": "contributor_guide/index.html", "configuration.html": "user_guide/configuration.html", - "new-machine-type.html": "contributor_guide/new_machine_type.html", - "new-concrete-backend.html": "contributor_guide/new_concrete_backend.html", + "new-machine-type.html": "contributor_guide/device-driver-guide.html", + "new-concrete-backend.html": "contributor_guide/device-driver-guide.html", "how-to-open-source.html": "contributor_guide/how_to_open_source.html", "basic.html": "user_guide/basic.html", "using-the-visualizer.html": "user_guide/using_the_visualizer.html", diff --git a/docs/contributor_guide/device-driver-guide.md b/docs/contributor_guide/device-driver-guide.md new file mode 100644 index 00000000000..0d1351a1e56 --- /dev/null +++ b/docs/contributor_guide/device-driver-guide.md @@ -0,0 +1,56 @@ +# Writing PyLabRobot Device Drivers & Hello-World Guides + +How to add a device driver and its hello-world notebook, for humans and agents. Post on the [PyLabRobot forum](https://discuss.pylabrobot.org) before starting to avoid duplicated effort and get support. + +## 1. Understand the device + +Recover the protocol before writing code. + +- **Extract, don't guess.** Work from an authoritative source — firmware/protocol docs, manufacturer log files, or a reference binary. Capture the *complete* set of command frames, error codes, status values, and exact message text, filled with real values, not placeholders. +- **Get the wire format byte-exact:** transport (serial params / USB endpoint / socket), framing (delimiters, length fields, checksums), handshake (echo? ack? busy→ok?). +- **Note blocking vs non-blocking commands** and how faults (e-stop, jam) surface. + +## 2. Structure the driver + +Keep it small and idiomatic to PyLabRobot. + +- **One file, one plain class.** The old Driver/Backend split and capability machinery are deprecated — don't use them. Instead write a single plain class whose public methods are the device's operations, talking to hardware through a `pylabrobot.io` transport. Path `pylabrobot//.py`, re-exported from `__init__.py`. Promote to a `/` package only when it genuinely helps — a distinct subsystem, the protocol/framing layer, or a large command table — not by reflex. +- **Stay OS-agnostic:** no OS-specific libraries or DLLs. Running on Windows, Mac, and Linux is what keeps experiments portable and reproducible. +- **Async `setup()` / `stop()`** plus public operation methods, over PyLabRobot's transport primitives (`pylabrobot.io.serial.Serial`, etc.) — never the OS directly. +- **Prefer string `Literal[...]` over enums**, especially anything user-facing. A `Literal["standard", "head", "pump"]` argument plus an internal dict mapping to wire codes reads better at the call site than an enum import. `IntEnum` is fine in narrow internal cases (e.g. a fixed set of wire/register codes never exposed to callers). +- **API docs:** add `docs/api/pylabrobot..rst` plus a line in `docs/api/pylabrobot.rst`. + +### Idempotent public API + +The public surface must expose **no non-idempotent commands.** If the hardware only offers a raw toggle/flip, keep it private (`_toggle_x`) and expose move-to-state methods (`move_x_out` / `move_x_in`) that read current state, act only if needed, then confirm. This keeps the API safe to call repeatedly — the caller states intent ("be open"), not a blind toggle. + +### Unverified drivers + +If the driver hasn't been checked against real hardware, say so loudly: `setup()` should `logger.warning(...)` that it's untested and invite a change once someone verifies it. Don't quietly present untested code as ready. + +## 3. Code style + +- **Comments document what the code does, not its history.** No "NEW", "now", "previously", no emphasis-caps — the code is not a diary. State behavior as fact; plain rationale ("why") is welcome. +- **No provenance stories.** Describe what the thing *is*, not where it came from — in code, commits, or PRs. +- **No dead code**, and **no one-time scripts in the repo.** The codebase holds permanent software only; run backfills/migrations ad-hoc as thin inline invocations of the module's own functions. + +## 4. Hello-world notebook + +Every device ships a runnable notebook that takes a user from cabling to first command. Path `docs/user_guide///hello-world.ipynb`; wire it in via the `/index.md` `{toctree}` and add `/index` to the Manufacturers `{toctree}` in `docs/user_guide/index.md` (alphabetical). + +Sections (markdown cell then code cell): (1) title + property table + untested-warning; (2) how it talks — brief, since users care about the machine, not the wire; (3) physical setup; (4) `setup()`; (5) one section per operation. + +Cell rules: +- **One concept per code cell.** If a physical action happens between steps, that's two cells: `move_tray_out()` → place plate → `move_tray_in()`. Every code cell gets a preceding markdown cell. +- **Notebook JSON:** edit with a notebook-aware tool (plain-text replace is blocked on `.ipynb`); code cells `execution_count: null`, empty `outputs`; `nbformat: 4`, `nbformat_minor: 5`; every cell has an `id`; validate it parses. + +## 5. Verify & ship + +- **Lint + type-check:** ruff + mypy, 2-space indent. +- **Show commit + PR text and get confirmation before committing.** +- **Never drive real hardware without explicit per-run approval** — a previous OK doesn't carry to the next run. + +## 6. Working style + +- **Do exactly what's asked, nothing more** — no drive-by edits to comments/constants/logic, no side-quests. If a fix seems to need more, state the minimal fact and let the person decide. +- **Run at full speed;** parallelize independent work. diff --git a/docs/contributor_guide/index.md b/docs/contributor_guide/index.md index 06292a05ccd..4f3f8feb1fb 100644 --- a/docs/contributor_guide/index.md +++ b/docs/contributor_guide/index.md @@ -14,8 +14,7 @@ contributing-to-docs :maxdepth: 2 :caption: Adding Backends/Drivers -new-machine-type -new-concrete-backend +device-driver-guide ```
diff --git a/docs/contributor_guide/new-concrete-backend.md b/docs/contributor_guide/new-concrete-backend.md deleted file mode 100644 index 49420e133d0..00000000000 --- a/docs/contributor_guide/new-concrete-backend.md +++ /dev/null @@ -1,42 +0,0 @@ -# Adding Support for a New Machine of an Existing Type - -This guide explains how to add support for a new machine of an existing type. For example, if you want to add support for a new liquid handler, you should read this guide. If you want to add support for a new type of machine, you should read {doc}`this guide ` first. - -The machine types that are currently can be found [here](/user_guide/machines). - -Two documents that you can read before you start are: - -- [CONTRIBUTING.md](https://github.com/PyLabRobot/pylabrobot/blob/main/CONTRIBUTING.md): This document contains general information about contributing to PyLabRobot, and covers things like installation and testing. -- [How to Open Source](https://docs.pylabrobot.org/how-to-open-source.html): This document contains step-by-step instructions for contributing to an open source project. It is not specific to PyLabRobot, and serves as a reference. - -Thank you for contributing to PyLabRobot! - -## Background - -Backends are minimal classes that are responsible for communicating with a machine and are thus specific to one machine. Frontends are higher level classes that are responsible for orchestrating higher-level state and providing nice interfaces to users, and should work with any machine. For example, the STAR liquid handler backend is responsible for executing the liquid handling operations on a Hamilton STAR, while the LiquidHandler frontend is responsible for making sure a requested operation is valid given the current state of the deck. - -Backends should contain minimal state. We prefer to manage the state in the frontend, because this allows us to share the code across all machines of a type. For example, the liquid handler backend does not contain any information about the deck, because this is managed by the frontend. If a certain machine has a specific state that needs to be managed, like whether the gripper arm is parked on a liquid handling robot, that should be done by the backend because it is specific to the machine. - -## 0. Get in touch - -Please make a post on [the PyLabRobot Development forum](https://discuss.pylabrobot.org) to let us know what you are working on. This will help you avoid duplicating work, and it is also a good place to get support. - -## 1. Creating a new concrete backend class - -It is easiest to start by copying the abstract base class for the machine type to a new file. You will find this in `backend.py` in the module for the machine type. For example, the liquid handling abstract base class is located at `pylabrobot.liquid_handling.backends.backend`. You should copy this file to a new file called `.py` in the same directory. For example, the liquid handling backend for the Hamilton STAR is located at `pylabrobot.liquid_handling.backends.hamilton.STAR`. - -## 2. Implementing the abstract methods - -The abstract base class contains a number of abstract methods. These are the methods that are expected to be implemented by the concrete backend. You should implement these methods in the concrete backend. You can use the abstract base class as a reference for what the methods should do. - -PyLabRobot aims to be OS-agnostic, meaning that it should work on Windows, Mac, and Linux. This maximizes flexibility for users and the reproducibility of experiments. However, this also means that you should not use any OS-specific libraries or dlls in the backend. - -If an operation is not supported by the machine, you should raise a `NotImplementedError`. - -The actual process of implementing the methods varies widely from machine to machine. It is generally useful to search for firmware documents, search for log files generated by a manufacturer's software, or find other open source projects that have implemented the same machine. - -## 3. Adding documentation (recommended) - -Find the relevant module in the `docs` directory. For example, the liquid handling backends module is located at `docs/pylabrobot.liquid_handling.backends.rst`. Then, add the name of the new backend to make sure that the new backend is automatically documented in the API reference. - -If you want, you can also add a new page to the `docs` directory that explains how to use the new backend. This is not required, but it is strongly recommended. Experience has shown that this is the best way to get people to actually use the new backend. diff --git a/docs/contributor_guide/new-machine-type.md b/docs/contributor_guide/new-machine-type.md deleted file mode 100644 index 165eef0ca24..00000000000 --- a/docs/contributor_guide/new-machine-type.md +++ /dev/null @@ -1,68 +0,0 @@ -# Contributing a New Type of Machine to PLR - -PyLabRobot supports a number of different types of machines. They can be found [here](/user_guide/machines). - -If you want to add support for a new type of machine, this guide will explain the process. If you want to add a new machine for a type that already exists, you should read {doc}`this guide ` instead. - -This guide is not a definitive step-by-step guide (otherwise we would have automated it), but rather a collection of high-level ideas and suggestions. Often, it only becomes clear what the best abstractions are after two or more machines for a type have been implemented, so it is totally valid (and encouraged) to make some assumptions and then refactor later. - -Two documents that you can read before you start are: - -- [CONTRIBUTING.md](https://github.com/PyLabRobot/pylabrobot/blob/main/CONTRIBUTING.md): This document contains general information about contributing to PyLabRobot, and covers things like installation and testing. -- [How to Open Source](https://docs.pylabrobot.org/how-to-open-source.html): This document contains step-by-step instructions for contributing to an open source project. It is not specific to PyLabRobot, and serves as a reference. - -Thank you for contributing to PyLabRobot! - -## 0. Get in touch - -Please make a post on [the PyLabRobot Development forum](https://discuss.pylabrobot.org) to let us know what you are working on. This will help you avoid duplicating work, and it is also a good place to get support. - -## 1. Creating a new module - -Each machine type has its own module in PLR. For example, the liquid handling module is located at `pylabrobot.liquid_handling`. This module contains: - -- the machine front end: the user-facing API for the machine type. Example: `LiquidHandler`. -- the abstract base class for the machine type: the minimal set of atomic commands that the machine type is expected to support. Example: `LiquidHandlerBackend`. -- the concrete backends: the actual implementations of the abstract base class for specific machines. See {doc}`the concrete backends guide ` for more information. Example: `STAR`. - -## 2. Creating a new abstract backend class - -Abstract backends are used to define the interface for a type of machine in terms of the minimal set of atomic commands. For example, all liquid handlers should have an `aspirate` method. - -The commands should be interactive and minimal. Interactive means that the command is expected to be executed immediately when its method is called. Minimal means the commands cannot be broken into sub-commands that a user would reasonably want to use. For example, the abstract liquid handler backend contains commands for `aspirate` and `dispense`, but not `transfer` (a convenience method that exists on the frontend). At the same time, `aspirate` does move the pipetting head to a certain location because this move and the actual aspiration are reasonably expected to always occur together. For new machines, it is fine to make some assumptions and revisit them later. - -The purpose of minimality is to make adding new concrete backends as easy as possible. The purpose of interactivity is to make the iteration cycle when developing new methods as short as possible. - -You must put the abstract base class in `backend.py` in the module you created in step 1. The abstract class {class}`~pylabrobot.machine.MachineBackend` must be used as the base class for all backends. This class defines the `setup` and `stop` methods, which are used to initialize and stop the machine. - -## 3. Creating a new front end class - -Front ends are used to define the user-facing interface for a specific machine type, and shared across all machines of this type in PLR. They expose the atomic backend commands in addition to providing higher level utilities and orchestrating state. For example, `LiquidHandler` has a `transfer` method that is not defined in the abstract backend (it is not minimal), but instead simply calls `aspirate` and `dispense` on the backend. This way, the `transfer` implementation is shared across all supported liquid handling robots. `LiquidHandler` also maintains a reference to the deck, to make sure the requested operations are valid given the current state of the deck. - -The abstract class {class}`~pylabrobot.machine.MachineFrontend` must be used as the base class for all front ends. This class defines the `setup` and `stop` methods, which are used to initialize and stop the machine. It also defines the `backend` attribute, which is used to access the backend. - -You should put the front end in a file called `.py` in the module you created in step 1. For example, the liquid handling front end is located at `pylabrobot.liquid_handling.liquid_handler.py`. - -If your devices updates the resource tree or its state, the front end should handle this. See [the resources guide](/resources/introduction.md) for more information. - -## 4. Creating a new concrete backend for a specific machine - -Refer to the {doc}`the concrete backends guide `. - -## 5. Adding documentation (strongly recommended) - -Each module should have a corresponding documentation page in the `docs` directory. Experience has shown that this is the best way to get people to actually use the new module. - -### API documentation - -API documentation is generated automatically based on docstrings, but has to be linked to from the main API documentation. - -1. Create a new file in the `docs` directory called `pylabrobot..rst`. You can look at the existing files for examples. -2. Add a link to this file to the API documentation in [`docs/pylabrobot.rst`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/pylabrobot.rst). - -### Brief introduction - -It is also recommended to add a brief introduction to the module which explains what it is and how to use it. You can write this introduction in Markdown, reStructuredText, or a Jupyter notebook (recommended). You can look at [`basic.ipynb`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/basic.ipynb) for an example. - -1. Put the introduction in the `docs` folder. -2. Link to the new file from [`docs/index.rst`](https://github.com/PyLabRobot/pylabrobot/blob/main/docs/index.rst).