Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
.DS_Store
.venv/
__pycache__/
*.py[cod]
Expand All @@ -7,3 +8,5 @@ __pycache__/
build/
dist/
*.egg-info/

docs/lmsf/
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -186,6 +186,32 @@ The cloning example writes separate `assembly`, `transformation`, and `plating`

The manifest uses logical container and well names; physical bindings remain in `plan.json`. Rewriting an identical bundle succeeds. If any generated file would replace different contents, `write()` rejects the write before changing any bundle files.

The HTML worksheet shows initial loads, numbered operations, liquid-class settings, and calculated final volumes. Hover or focus a material, location, resource, or liquid class to inspect its recorded details; click to keep the card open and press Escape to dismiss it. Location cards distinguish initial loads, planned final volumes, output samples, and incoming transfers. The first paragraph of `Protocol.description` is the overview; subsequent paragraphs appear in expandable protocol notes. Styles and interactions are embedded for offline use, with a plain reference section when JavaScript is disabled and a separate print layout.

The [worksheet UI guide](docs/worksheet-ui.md) describes its typed view data, reusable templates, and interactive components.

The preparation preview shows logical plate layouts and reagent inputs. Step through or play the procedure to see source volumes decrease and destination wells fill; a schematic pipette and directional paths show each transfer. Distributions visit each destination, and mixes animate in place. Click a procedure step number to preview that state. Well details and reagent amounts follow the selected step. The preview uses the compiler's volume accounting, with animation timing chosen for readability. It shows planned liquid movement; physical well bindings are available in the detail cards.

Liquid-class cards plot flow rate against time for the selected operation. Aspiration is positive and dispensing negative; mixing repeats those phases. Durations are ideal calculations from nominal volume divided by the recorded rate, excluding robot movement, pauses, ramps, air gaps, and volume correction. These diagrams are not measured device traces. Native Hamilton profiles require rates captured during STAR compilation.

## Concentration series

Preparation proceeds across the plate: medium first, then vehicle and each stock, followed by inoculation and mixing. Equal doses from the same source share a `distribute()` operation and one tip. Unique doses remain individual transfers. Preparing the full supplied layout requires 327 tips, including source resuspension and destination mixing. The worksheet reports the tip count alongside the operation count.

The standalone [concentration-series example](examples/concentration_series/) reads a [plate layout](examples/concentration_series/layout.csv) and [protocol configuration](examples/concentration_series/config.toml), then records ordinary transfers and mixes. The supplied layout preserves the well coordinates and target concentrations of an LMSF fitness export, with generic strain and plasmid names. Every CSV row is prepared by default: 96 wells covering 24 conditions for each of four plasmid samples. An optional `wells` list in the configuration selects a subset explicitly. `lab.units.dilution_volume()` calculates stock volumes; the recipe balances vehicle and medium, inoculates, and mixes. Source identities and volumes, working-stock selection, and liquid classes are explicit. The TOML also declares development OT-2 and STAR deck layouts, parsed directly into Lab's existing `Deck` types, with reservoir sources and enough tips for the full plate.

`main()` parses the TOML into `ProtocolConfig` and the CSV into `PlateLayout` using Pydantic. The reusable quantity fields and liquid-class schemas come from [`lab.config`](docs/configuration.md), available through `lab-compiler[config]` and included in development dependencies (`uv sync --locked`). `prepare_wells()` checks the inputs together and calculates doses; `protocol()` consumes those prepared wells and the validated configuration to record operations. The configuration mirrors the TOML: `medium.name` is `config.medium.name`, and liquid-class names remain names until protocol construction resolves them. Saved configuration comes from that same model.

```sh
uv run --no-sync python -m examples.concentration_series \
--csv examples/concentration_series/layout.csv \
--config examples/concentration_series/config.toml --target manual
```

Use `--target ot2` or `--target star` with the corresponding optional SDK installed. Output goes to `build/concentration_series/csv/<target>/`; `--out` selects another directory. Rerunning replaces the generated bundle in that directory. The example ends with an inoculated plate; incubation and measurement need separately specified methods.

`LiquidClass` supplies unit-checked aspiration and dispense rates on either robot family. `HamiltonLiquidClass` references a native STAR built-in, whose definition is captured during compilation. Both can be passed as `liquid_class=` to `transfer()`, `distribute()`, and `mix()`. See [liquid classes](docs/liquid-classes.md) for examples, settings coverage, calibration boundaries, and the data needed to compare with VENUS.

## Development

From the repository root:
Expand All @@ -197,6 +223,8 @@ uv run --no-sync mypy
uv run --no-sync pytest
```

The concentration-series tests use the same `layout.csv` and `config.toml` as the example. They exercise parsing, dose calculations, compilation, robot simulation, and provenance without additional data files or configuration.

CI runs the OT-2 equivalence tests against a pinned PUDU revision. To compare against another checkout, set `PUDU_SOURCE` to its `src` directory. When this variable is set, missing PUDU source or the Opentrons SDK fails the run.

```sh
Expand Down
47 changes: 47 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Typed protocol configuration

`lab.config` supplies Pydantic fields and models for application input files. Install `lab-compiler[config]` to use it; repository development dependencies also include Pydantic. Importing `lab` and using `Protocol` do not require Pydantic.

Define the application's schema using ordinary Pydantic fields and validators. Lab's quantity fields handle parsing, dimensional checks, Decimal normalization, JSON serialization, and JSON Schema:

```python
from lab.config import ConfigModel, Concentration, NonnegativeVolume, Volume


class ProtocolConfig(ConfigModel):
final_volume: Volume
source_dead_volume: NonnegativeVolume
stock_concentration: Concentration


config = ProtocolConfig.model_validate(
{
"final_volume": "0.2 mL",
"source_dead_volume": "20 uL",
"stock_concentration": "1 mM",
}
)
print(config.final_volume) # 2E+2 microliter
print(config.model_dump_json())
```

| Field type | Accepted values and Python representation |
| --- | --- |
| `Volume` | Positive finite quantity, normalized to microlitres |
| `NonnegativeVolume` | As above, allowing zero; useful for dead volumes |
| `FlowRate` | Positive finite quantity, normalized to microlitres/second |
| `Concentration` | Nonnegative finite quantity, normalized to mole/litre or gram/litre |
| `ConcentrationUnit` | A separate mass/volume or amount/volume unit; retains its original scale for CSV numeric columns |
| `VolumeFraction` | Finite Decimal from zero through one |
| `NonnegativeDecimal` | Finite Decimal greater than or equal to zero |
| `NonemptyString` | String containing at least one non-whitespace character |

Quantity fields accept unit-bearing strings or Pint `Quantity` objects. They reject bare numbers and incompatible dimensions. Mass concentration cannot be converted to molar concentration without additional information. Quantities serialize as unit-bearing strings and appear as strings in JSON Schema; the validation runs when parsing, not in JSON Schema itself.

`ConfigModel` rejects unknown fields, allows these Pint types, and freezes attribute assignment. Its contained dictionaries are still mutable, following Pydantic's usual behavior. Parse the files once before building a protocol. Apply application-specific checks, such as stock coverage and composition balance, before recording operations; the compiler subsequently checks inventory, capacity, and equipment constraints.

`LiquidClassConfig` is a discriminated union of `PortableLiquidClassConfig` (`kind = "portable"`, aspiration/dispense rates) and `HamiltonLiquidClassConfig` (`kind = "hamilton"`, native symbol). `MixingConfig` supplies a volume, positive integer cycle count, and liquid-class name. Names remain references within the application's configuration; the application checks them against its registry and explicitly constructs Lab liquid classes. See [liquid classes](liquid-classes.md).

Existing Lab dataclasses can also be fields. For example, `deck: lab.deck.Deck` parses nested containers and layouts into typed Lab objects, including enums, locations, and Decimal limits. Pydantic applies their existing dataclass validation. The [concentration-series configuration](../examples/concentration_series/config.toml) demonstrates this without a separate deck parser or schema.

For applications that do not use Pydantic, `lab.units.parse_quantity(value, unit="microliter")` provides the same normalization. `unit` may also be a tuple of alternatives for different dimensions, such as `("mole/liter", "gram/liter")`. The first compatible unit is used. `positive=False` permits zero; negative or nonfinite values always fail. This input parser does not change the `Protocol` API: operations take quantity objects.
87 changes: 87 additions & 0 deletions docs/liquid-classes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
# Liquid classes

A liquid class supplies pipetting settings for a particular operation. Attach it to `Protocol.transfer()`, `distribute()`, or `mix()`. It is separate from the material identity and concentration: two transfers of the same material can need different settings because their tips, volumes, or destinations differ.

## Portable custom settings

```python
from lab import LiquidClass, Protocol, seconds, uL

gentle = LiquidClass(
"gentle culture",
aspiration_flow_rate=20 * uL / seconds,
dispense_flow_rate=30 * uL / seconds,
)
p = Protocol("Culture aliquot")
source = p.container("source", contents="culture", volume=200 * uL, capacity=360 * uL)
destination = p.container("destination", capacity=360 * uL)
p.transfer(source, destination, volume=50 * uL, liquid_class=gentle)
p.mix(destination, volume=40 * uL, cycles=3, liquid_class=gentle)
```

`LiquidClass` currently expresses **aspiration and dispense flow rates**. Both are required, finite, positive quantities with volume/time dimensions. Construction converts them to immutable Decimal µL/s values. A name describes the settings; it does not identify a calibrated fluid in a robot's catalogue.

| Target | Application |
| --- | --- |
| Manual | Print the name and both rates beside the operation |
| OT-2 and Flex | Set the selected pipette's absolute aspiration/dispense rates for the operation, then restore its previous rates |
| STAR | Pass absolute `flow_rates` to each PyLabRobot aspiration and dispense |

Mixing uses these same rates for each cycle. Distribution applies them to the target's aspirations and dispenses; distribution batching remains target-specific. With `liquid_class=None`, the operation uses the existing target defaults.

This portable class is deliberately a small settings profile. It does **not** specify a volume-correction curve, air gap, blowout, liquid-level detection, heights, surface following, settling times, or dispensing mode. Those retain the target/SDK defaults. In particular, changing rates or naming a class `dmso` does not select a Hamilton DMSO calibration: the generated STAR program does not populate PyLabRobot liquid-identity trackers, so automatic class selection uses the SDK's default liquid. Use an explicit Hamilton reference when the native calibration is required.

The common meaning of µL/s supports software portability. Equivalent physical delivery still depends on the device, tips, liquid, volume, and method. Add further portable fields when their meaning and lowering can be demonstrated on the relevant targets; keep device-specific calibration separate.

## Hamilton built-ins

PyLabRobot provides a catalogue imported from VENUS, and its STAR backend accepts native classes on both aspiration and dispense. A native definition contains more than rates, including a volume-correction curve. See the [PyLabRobot liquid-class guide](https://docs.pylabrobot.org/stable/user_guide/00_liquid-handling/hamilton-star/hamilton-liquid-classes.html).

Reference an exact symbol through Lab's SDK-independent type:

```python
from lab import HamiltonLiquidClass

water = HamiltonLiquidClass("HighVolumeFilter_Water_DispenseSurface_Part")
p.transfer(source, destination, volume=50 * uL, liquid_class=water)
```

The symbol above is an illustration of lookup, not a recommendation for the concentration example's 300 µL tips. Select a definition validated for the intended liquid, tip/filter, channel/head, volume range, and dispense mode. Lab does not infer this compatibility from the symbol's spelling.

With `lab-compiler[star]` installed, discover the available symbols without connecting to hardware:

```python
from lab.targets.star import hamilton_liquid_class_names

names = hamilton_liquid_class_names()
print([name for name in names if "DMSO" in name])
```

STAR compilation checks that the symbol resolves to a native liquid-class object. It snapshots the definition into `plan.json` and the generated program, alongside the required PyLabRobot version. The program reconstructs that captured definition and passes it to both operations. Unknown symbols fail compilation. OT-2 and Flex reject Hamilton references; manual compilation can document a reference without installing PyLabRobot but cannot resolve it.

The native backend performs volume correction. Lab records requested liquid volumes, so it does not pre-correct them or apply the curve a second time. Its inventory calculation and channel/tip checks concern nominal volumes; they are not a physical calibration or an exhaustive check of corrected piston movements and transport air. A native class on `mix()` applies to repeated ordinary aspirations and dispenses, not a separate Hamilton firmware mixing command.

Capturing the definition does not imply that every field is used by the pinned backend. PyLabRobot's method-level defaults and implementation determine effective behavior, including which class fields are consumed. Native built-ins are supported here; importing an arbitrary local VENUS class or creating a full native Hamilton definition through Lab is not yet implemented. For a custom class beyond portable rates, first obtain its actual parameter table and method usage, then add a typed native representation and trace tests for the fields required.

## CSV preparation

The standalone [concentration-series example](../examples/concentration_series/) reads its class registry from the [protocol configuration TOML](../examples/concentration_series/config.toml) supplied with `--config`. Each stock, the medium, the balancing vehicle, and each culture names a class. Source and destination mixing name their classes separately: the final mixture is not interchangeable with its constituent stocks.

`ProtocolConfig` is a Pydantic schema with the same nesting and field names as the TOML, validated once in `main()` before protocol construction. Its liquid-class and mixing schemas come from the optional [`lab.config`](configuration.md) module. `[liquid_classes.aqueous]` becomes `config.liquid_classes["aqueous"]`, a typed configuration entry. `config.medium.liquid_class` remains the string `"aqueous"`; the schema checks that this name exists. A Hamilton entry's TOML key is its local alias, and its `name` is the native STAR catalogue symbol.

At the start of `protocol()`, a dictionary comprehension converts the entries to Lab's `LiquidClass` or `HamiltonLiquidClass` objects. Operations look them up explicitly, for example `liquid_class=liquid_classes[config.medium.liquid_class]`. Configuration loading does not substitute objects for names. The plan records `config.model_dump_json()` from the validated model used by the recipe.

```toml
[liquid_classes.culture]
kind = "portable"
aspiration_flow_rate = "20 uL/s"
dispense_flow_rate = "20 uL/s"
```

For STAR, a class table can instead contain `kind = "hamilton"` and `name = "<exact built-in symbol>"`. The example numbers are development inputs. They are not measured performance data or reconstructed LMSF settings.

## Establishing LMSF equivalence

Ask for one matched run containing the destination CSV, source setup, resolved LMSF parameters, complete VENUS method export and dependencies, native liquid-class definitions, and a simulation trace plus a successful instrument trace. For each pipetting step, identify the selected class and any method overrides: tip/filter and channel/head, aspiration/dispense modes, LLD and heights, mixing, air/blowout, settling, and correction settings. Record VENUS, firmware, and instrument configuration versions.

Compare the generated sequence and effective settings with that run before measuring delivery accuracy and precision over the actual volume range. A matching class name, nominal volume ledger, or software trace alone does not demonstrate the same physical result. These artifacts also establish whether direct PyLabRobot execution is suitable or whether LMSF needs to retain VENUS as its execution layer.
Loading
Loading