lab-compiler 0.2.0__tar.gz → 0.3.0__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.
Files changed (112) hide show
  1. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/.gitignore +3 -0
  2. lab_compiler-0.3.0/CHANGELOG.md +39 -0
  3. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/PKG-INFO +78 -20
  4. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/README.md +74 -19
  5. lab_compiler-0.3.0/docs/configuration.md +47 -0
  6. lab_compiler-0.3.0/docs/liquid-classes.md +87 -0
  7. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/docs/releasing.md +22 -14
  8. lab_compiler-0.3.0/docs/worksheet-ui.md +62 -0
  9. lab_compiler-0.3.0/examples/concentration_series/__init__.py +1 -0
  10. lab_compiler-0.3.0/examples/concentration_series/__main__.py +65 -0
  11. lab_compiler-0.3.0/examples/concentration_series/config.toml +183 -0
  12. lab_compiler-0.3.0/examples/concentration_series/inputs.py +258 -0
  13. lab_compiler-0.3.0/examples/concentration_series/layout.csv +97 -0
  14. lab_compiler-0.3.0/examples/concentration_series/protocol.py +165 -0
  15. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/integrations/labop/README.md +1 -1
  16. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/integrations/labop/export.py +5 -3
  17. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/pyproject.toml +4 -1
  18. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/scripts/check_install.py +2 -2
  19. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/__init__.py +3 -0
  20. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/_version.py +1 -1
  21. lab_compiler-0.3.0/src/lab/config.py +109 -0
  22. lab_compiler-0.3.0/src/lab/documents/__init__.py +6 -0
  23. lab_compiler-0.3.0/src/lab/documents/assets/flow-profile.css +17 -0
  24. lab_compiler-0.3.0/src/lab/documents/assets/flow-profile.js +33 -0
  25. lab_compiler-0.3.0/src/lab/documents/assets/inspector.css +29 -0
  26. lab_compiler-0.3.0/src/lab/documents/assets/inspector.js +204 -0
  27. lab_compiler-0.3.0/src/lab/documents/assets/preparation.css +95 -0
  28. lab_compiler-0.3.0/src/lab/documents/assets/preparation.js +235 -0
  29. lab_compiler-0.3.0/src/lab/documents/assets/print.css +31 -0
  30. lab_compiler-0.3.0/src/lab/documents/assets/responsive.css +17 -0
  31. lab_compiler-0.3.0/src/lab/documents/assets/transfer-motion.js +197 -0
  32. lab_compiler-0.3.0/src/lab/documents/assets/worksheet.css +75 -0
  33. lab_compiler-0.3.0/src/lab/documents/descriptions.py +68 -0
  34. lab_compiler-0.3.0/src/lab/documents/flow.py +93 -0
  35. lab_compiler-0.3.0/src/lab/documents/models.py +136 -0
  36. lab_compiler-0.3.0/src/lab/documents/preparation.py +112 -0
  37. lab_compiler-0.3.0/src/lab/documents/prepare.py +248 -0
  38. lab_compiler-0.3.0/src/lab/documents/render.py +50 -0
  39. lab_compiler-0.3.0/src/lab/documents/templates/components/detail_cards.html +48 -0
  40. lab_compiler-0.3.0/src/lab/documents/templates/components/flow_profile.html +46 -0
  41. lab_compiler-0.3.0/src/lab/documents/templates/components/inspector.html +7 -0
  42. lab_compiler-0.3.0/src/lab/documents/templates/components/plate_map.html +49 -0
  43. lab_compiler-0.3.0/src/lab/documents/templates/components/transfer_motion.html +17 -0
  44. lab_compiler-0.3.0/src/lab/documents/templates/components/ui.html +37 -0
  45. lab_compiler-0.3.0/src/lab/documents/templates/sections/details.html +19 -0
  46. lab_compiler-0.3.0/src/lab/documents/templates/sections/materials.html +36 -0
  47. lab_compiler-0.3.0/src/lab/documents/templates/sections/preparation.html +42 -0
  48. lab_compiler-0.3.0/src/lab/documents/templates/sections/procedure.html +34 -0
  49. lab_compiler-0.3.0/src/lab/documents/templates/sections/volumes.html +19 -0
  50. lab_compiler-0.3.0/src/lab/documents/templates/worksheet.html +44 -0
  51. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/stages/assembly.py +2 -6
  52. lab_compiler-0.3.0/src/lab/liquid_classes.py +64 -0
  53. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/model.py +21 -3
  54. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/protocol.py +42 -6
  55. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/opentrons.py +44 -9
  56. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/star.py +58 -6
  57. lab_compiler-0.3.0/src/lab/units.py +103 -0
  58. lab_compiler-0.3.0/tests/test_concentration_series.py +560 -0
  59. lab_compiler-0.3.0/tests/test_config.py +104 -0
  60. lab_compiler-0.3.0/tests/test_documents.py +170 -0
  61. lab_compiler-0.3.0/tests/test_liquid_classes.py +204 -0
  62. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_ot2_equivalence.py +32 -1
  63. lab_compiler-0.3.0/tests/test_preparation_view.py +152 -0
  64. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_protocol.py +1 -0
  65. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_targets.py +57 -0
  66. lab_compiler-0.3.0/tests/test_units.py +124 -0
  67. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/uv.lock +59 -1
  68. lab_compiler-0.2.0/CHANGELOG.md +0 -19
  69. lab_compiler-0.2.0/src/lab/documents.py +0 -129
  70. lab_compiler-0.2.0/src/lab/units.py +0 -46
  71. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/LICENSE +0 -0
  72. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/docs/assets/brand/wordmark-full-dark.svg +0 -0
  73. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/docs/assets/brand/wordmark-full-light.svg +0 -0
  74. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/docs/sbol-provenance.md +0 -0
  75. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/__init__.py +0 -0
  76. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/cloning.py +0 -0
  77. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/deck_layouts.py +0 -0
  78. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/labop_export.py +0 -0
  79. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/sbol_provenance.py +0 -0
  80. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/examples/transformation.py +0 -0
  81. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/integrations/labop/check.py +0 -0
  82. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/integrations/labop/requirements.txt +0 -0
  83. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/_identifiers.py +0 -0
  84. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/compiler.py +0 -0
  85. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/deck.py +0 -0
  86. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/equipment.py +0 -0
  87. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/__init__.py +0 -0
  88. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/__init__.py +0 -0
  89. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/addresses.py +0 -0
  90. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/decks.py +0 -0
  91. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/stages/__init__.py +0 -0
  92. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/stages/plating.py +0 -0
  93. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/stages/transformation.py +0 -0
  94. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/types.py +0 -0
  95. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/experiments/cloning/workflow.py +0 -0
  96. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/labware.py +0 -0
  97. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/py.typed +0 -0
  98. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/samples.py +0 -0
  99. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/__init__.py +0 -0
  100. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/liquid_handler.py +0 -0
  101. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/lower.py +0 -0
  102. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/targets/manual.py +0 -0
  103. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/src/lab/validation.py +0 -0
  104. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/cloning_fixture.py +0 -0
  105. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/conftest.py +0 -0
  106. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/target_fixture.py +0 -0
  107. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_deck.py +0 -0
  108. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_layouts.py +0 -0
  109. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_samples.py +0 -0
  110. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_sbol_annotations.py +0 -0
  111. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/test_stages.py +0 -0
  112. {lab_compiler-0.2.0 → lab_compiler-0.3.0}/tests/thermal_fixture.py +0 -0
@@ -1,3 +1,4 @@
1
+ .DS_Store
1
2
  .venv/
2
3
  __pycache__/
3
4
  *.py[cod]
@@ -7,3 +8,5 @@ __pycache__/
7
8
  build/
8
9
  dist/
9
10
  *.egg-info/
11
+
12
+ docs/lmsf/
@@ -0,0 +1,39 @@
1
+ # Changelog
2
+
3
+ ## 0.3.0
4
+
5
+ ### Changed behavior
6
+
7
+ - On OT-2 and Flex, `Protocol.thermocycle()` without `lid_temperature` now follows each block hold with the heated lid, disabling lid heating below 37 °C. An explicit lid temperature remains fixed. Thermocycling retains the final temperature settings after opening the lid; failures deactivate the module before propagating the error. `set_temperature()` also matches the thermocycler lid to the block. Generated STAR thermal callbacks document the same contract.
8
+ - Golden Gate assembly uses the automatic lid behavior instead of a fixed 42 °C lid, including during enzyme inactivation.
9
+ - Recorded transfer, distribution, and mixing steps in `plan.json` now include `liquid_class`, set to `null` when no class is supplied. Consumers of serialized operations should accept this additional field.
10
+
11
+ ### Added
12
+
13
+ - A standalone, printable protocol worksheet with material and well inspection, liquid-class flow profiles, and an interactive preparation preview. The preview follows the compiler's calculated volume snapshots and illustrates transfers, distributions, and mixes; it does not simulate physical delivery or robot trajectories. Templates, styles, and scripts ship in the package, with no network access required to view the worksheet.
14
+ - `LiquidClass` supplies explicit aspiration and dispense rates for `Protocol.transfer()`, `distribute()`, and `mix()` across manual, OT-2, Flex, and STAR targets. `HamiltonLiquidClass` references native STAR definitions, which are captured in the generated plan and program; Opentrons targets reject these references.
15
+ - Optional `lab.config` models and quantity fields, installed with `lab-compiler[config]`, support typed application configuration with Pydantic. The core package remains independent of Pydantic. `lab.units.parse_quantity()` normalizes unit-bearing inputs, and `dilution_volume()` calculates stock volumes for compatible concentrations.
16
+ - A concentration-series example reads a destination CSV and TOML configuration, validates doses and source requirements, and prepares an inoculated plate for manual, OT-2, or STAR compilation. The included layout covers a full 96-well plate and supports selecting a subset through configuration.
17
+
18
+ ### Dependencies and validation
19
+
20
+ - Jinja2 is now a core dependency for worksheet rendering. The optional robot SDK versions remain Opentrons 8.8.2 and PyLabRobot 0.2.1; supported Python versions remain 3.11 and 3.12.
21
+ - CI compares simulated OT-2 assembly, transformation, and plating behavior with a pinned PUDU revision, including lid settings, thermal profiles, transfers, and handoff wells. These required checks fail if the reference source or Opentrons SDK is missing.
22
+
23
+ ## 0.2.0
24
+
25
+ ### Migration from 0.1.0
26
+
27
+ - Pass shared decks with `lab.compile(protocol, deck=deck, liquid_handler=...)`. A positional second argument is now reserved for a concrete target.
28
+ - `lab.compile(protocol)` defaults to a manual target and writes a bundle to `~/.lab/<protocol>/<target>/`. Use `to=None` to compile without writing, `to=path` to choose a directory, or `LAB_HOME` to change the default root. Recompilation replaces the files in that protocol/target bundle; `Compilation.write()` still refuses to replace different existing artifacts.
29
+ - Replace the removed `lab.protocols` request and material API with the cloning experiment builders and `lab.samples`. `Assembly` and `Transformation` recipes take native `sbol3.Component` objects; pySBOL3 is now a core dependency.
30
+
31
+ ### Added
32
+
33
+ - Sample identities, lineage, design references, and optional implementation references are preserved in the protocol snapshot. Declared outputs produce `manifest.json` and are available through `Compilation.manifest`.
34
+ - Assembly, transformation, and plating examples carry native SBOL design identities through their planned outputs, with a separate example for computational design provenance.
35
+ - An optional LabOP integration exports prospective protocol plans using upstream LabOP. It is installed from the checkout in a separate environment and is not imported by the core package.
36
+
37
+ ### Repository
38
+
39
+ - Move the source repository to `https://github.com/MyersResearchGroup/LabCompiler` and update package URLs, README links, and publishing instructions. The PyPI package remains `lab-compiler`; the Python import remains `lab`.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: lab-compiler
3
- Version: 0.2.0
3
+ Version: 0.3.0
4
4
  Summary: A small Python compiler for inspectable laboratory protocols
5
5
  Project-URL: Homepage, https://github.com/MyersResearchGroup/LabCompiler
6
6
  Project-URL: Documentation, https://github.com/MyersResearchGroup/LabCompiler#readme
@@ -17,8 +17,11 @@ Classifier: Programming Language :: Python :: 3.12
17
17
  Classifier: Topic :: Scientific/Engineering
18
18
  Classifier: Typing :: Typed
19
19
  Requires-Python: <3.13,>=3.11
20
+ Requires-Dist: jinja2<4,>=3.1.6
20
21
  Requires-Dist: pint<0.27,>=0.24
21
22
  Requires-Dist: sbol3==1.2.0.post0
23
+ Provides-Extra: config
24
+ Requires-Dist: pydantic<3,>=2.13; extra == 'config'
22
25
  Provides-Extra: opentrons
23
26
  Requires-Dist: opentrons==8.8.2; extra == 'opentrons'
24
27
  Provides-Extra: star
@@ -33,7 +36,7 @@ Description-Content-Type: text/markdown
33
36
  </p>
34
37
 
35
38
  <p align="center">
36
- <em>A compiler for biological engineering. Describe experiments in Python, check the protocol, and produce a document or a robot program.</em>
39
+ <em>A compiler for liquid handling. Describe experiments in Python, check the protocol, and produce a document or a robot program.</em>
37
40
  </p>
38
41
 
39
42
  Lab Compiler is the Python compiler for laboratory work. A `Protocol` records materials, wells, steps, and optional sample identities and lineage. Compilation checks volumes, units, bindings, and sample declarations, then writes a printable document and code for the liquid handler you name.
@@ -59,34 +62,47 @@ Use `"lab-compiler[opentrons,star]"` to install both SDKs.
59
62
 
60
63
  ## Write a protocol
61
64
 
62
- Describe biological designs with native pySBOL3 components. An experiment builder records their laboratory operations in a `Protocol`, and `compile()` validates and compiles that protocol for the selected target.
65
+ A `Protocol` records what a scientist would write at the bench: the plates, what starts in each well, and every transfer and mix. This one prepares a two-fold dilution series of fluorescein in PBS, the standard curve for a plate reader.
63
66
 
64
67
  ```python
65
- import sbol3
66
- from lab import compile
68
+ from lab import Protocol, compile, uL
69
+ from lab.deck import Container, Deck, DeckSite
67
70
  from lab.equipment import LiquidHandler
68
- from lab.experiments.cloning import Transformation, build_transformation, transformation_deck
71
+ from lab.labware import PLATE_96
69
72
 
70
- designs = sbol3.Document()
71
- strain = sbol3.Component("https://example.org/my_strain", sbol3.SBO_FUNCTIONAL_ENTITY)
72
- chassis = sbol3.Component("https://example.org/my_cells", sbol3.SBO_FUNCTIONAL_ENTITY)
73
- plasmid = sbol3.Component("https://example.org/my_plasmid", sbol3.SBO_DNA)
74
- designs.add([strain, chassis, plasmid])
73
+ protocol = Protocol("fluorescein serial dilution")
74
+ sources = protocol.plate("sources", capacity=360 * uL)
75
+ assay = protocol.plate("assay", capacity=360 * uL)
75
76
 
76
- protocol = build_transformation(
77
- [Transformation(strain=strain, chassis=chassis, plasmids=[plasmid])],
78
- name="transformation",
79
- )
80
- compiled = compile(
81
- protocol,
82
- deck=transformation_deck(on_module=True),
83
- liquid_handler=LiquidHandler.OT2,
77
+ fluorescein, pbs, waste = sources["A1"], sources["A2"], sources["A3"]
78
+ protocol.load(fluorescein, "10 µM fluorescein in PBS", volume=150 * uL)
79
+ protocol.load(pbs, "PBS", volume=300 * uL)
80
+
81
+ dilutions = [assay[f"A{column}"] for column in range(1, 7)]
82
+ protocol.transfer(fluorescein, dilutions[0], volume=100 * uL)
83
+ protocol.distribute(pbs, dilutions[1:], volume=50 * uL)
84
+
85
+ # Carry 50 µL down the row, mixing each well before the next transfer.
86
+ for previous, current in zip(dilutions, dilutions[1:]):
87
+ protocol.transfer(previous, current, volume=50 * uL)
88
+ protocol.mix(current, volume=40 * uL, cycles=3)
89
+ protocol.transfer(dilutions[-1], waste, volume=50 * uL)
90
+ protocol.manual("Read fluorescence at 485 nm excitation and 525 nm emission.")
91
+
92
+ deck = Deck(
93
+ containers=(
94
+ Container(id="sources", labware=PLATE_96, site=DeckSite.PLATES),
95
+ Container(id="assay", labware=PLATE_96, site=DeckSite.PLATES),
96
+ )
84
97
  )
98
+ compiled = compile(protocol, deck=deck, liquid_handler=LiquidHandler.FLEX)
85
99
  ```
86
100
 
101
+ `compile()` checks every volume against the wells and the pipette, then writes a printable protocol and an Opentrons Flex program. The deck only says both plates sit in open plate positions; the Flex preset chooses the slots, tip rack, and pipette. Wells A1–A6 of the assay plate each finish with 50 µL.
102
+
87
103
  ## Describe a deck
88
104
 
89
- This OT-2 deck places two 96-well plates in slots 1 and 2, a 300 µL tip rack in slot 3, and a P300 pipette on the left mount. It uses the same equipment and placement types as the [deck layouts example](https://github.com/MyersResearchGroup/LabCompiler/blob/master/examples/deck_layouts.py).
105
+ A `DeckLayout` states the equipment and positions instead of leaving them to a preset. This OT-2 deck places the dilution protocol's `sources` and `assay` plates in slots 1 and 2, a 300 µL tip rack in slot 3, and a P300 pipette on the left mount. It uses the same equipment and placement types as the [deck layouts example](https://github.com/MyersResearchGroup/LabCompiler/blob/master/examples/deck_layouts.py).
90
106
 
91
107
  ```python
92
108
  from lab.deck import Deck, DeckLayout, Pipette, Placement, Slot, TipRack
@@ -132,6 +148,12 @@ deck = Deck(
132
148
  )
133
149
  ```
134
150
 
151
+ Compile the dilution protocol for that robot:
152
+
153
+ ```python
154
+ compiled = compile(protocol, deck=deck, liquid_handler=LiquidHandler.OT2)
155
+ ```
156
+
135
157
  ## Describe physical layouts in Lab
136
158
 
137
159
  Use `DeckLayout` when equipment or placement needs to be explicit. Each layout names its `LiquidHandler` and places the same shared containers. All objects below belong to Lab:
@@ -160,6 +182,8 @@ The compiler rejects missing layouts, conflicting placements, invalid holder ref
160
182
 
161
183
  For Opentrons, place thermal labware on a supported `Module`. STAR layouts currently support one independent `Channel` and external thermal integration. Declare thermal containers in `DeckLayout.external_thermal_resources` and supply the async `thermocycle` callback when the generated `run()` executes. The callback is responsible for controlling the device and returning the plate to its original position. Compilation checks the declaration; the software preview only reports the request. On-deck `Module` models are not yet supported by the STAR backend.
162
184
 
185
+ By default, thermocycling matches the lid target to each block target within the device's heated lid range and turns lid heating off below that range. On Opentrons, the lid follows block targets at 37 °C and above; colder holds turn off the lid heater. Persistent thermocycler holds use the same rule, including setup and plating. An explicit `lid_temperature` keeps the lid at a fixed target throughout a profile. Successful profiles keep their final temperature settings; a failed Opentrons profile shuts down both controllers. External STAR thermal callbacks follow the same contract when `lid_temperature=None`.
186
+
163
187
  ## Try it
164
188
 
165
189
  The examples live in the source repository. With [uv](https://docs.astral.sh/uv/) installed, set up Python 3.12 and both optional SDKs:
@@ -192,6 +216,32 @@ The cloning example writes separate `assembly`, `transformation`, and `plating`
192
216
 
193
217
  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.
194
218
 
219
+ 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.
220
+
221
+ The [worksheet UI guide](docs/worksheet-ui.md) describes its typed view data, reusable templates, and interactive components.
222
+
223
+ 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.
224
+
225
+ 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.
226
+
227
+ ## Concentration series
228
+
229
+ 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.
230
+
231
+ 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.
232
+
233
+ `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.
234
+
235
+ ```sh
236
+ uv run --no-sync python -m examples.concentration_series \
237
+ --csv examples/concentration_series/layout.csv \
238
+ --config examples/concentration_series/config.toml --target manual
239
+ ```
240
+
241
+ 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.
242
+
243
+ `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.
244
+
195
245
  ## Development
196
246
 
197
247
  From the repository root:
@@ -203,4 +253,12 @@ uv run --no-sync mypy
203
253
  uv run --no-sync pytest
204
254
  ```
205
255
 
256
+ 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.
257
+
258
+ 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.
259
+
260
+ ```sh
261
+ PUDU_SOURCE=/path/to/PUDU/src uv run --no-sync pytest -q tests/test_ot2_equivalence.py
262
+ ```
263
+
206
264
  See the [release guide](https://github.com/MyersResearchGroup/LabCompiler/blob/master/docs/releasing.md) for package validation and PyPI publishing, and the [release notes](https://github.com/MyersResearchGroup/LabCompiler/blob/master/CHANGELOG.md) for changes and migration guidance.
@@ -6,7 +6,7 @@
6
6
  </p>
7
7
 
8
8
  <p align="center">
9
- <em>A compiler for biological engineering. Describe experiments in Python, check the protocol, and produce a document or a robot program.</em>
9
+ <em>A compiler for liquid handling. Describe experiments in Python, check the protocol, and produce a document or a robot program.</em>
10
10
  </p>
11
11
 
12
12
  Lab Compiler is the Python compiler for laboratory work. A `Protocol` records materials, wells, steps, and optional sample identities and lineage. Compilation checks volumes, units, bindings, and sample declarations, then writes a printable document and code for the liquid handler you name.
@@ -32,34 +32,47 @@ Use `"lab-compiler[opentrons,star]"` to install both SDKs.
32
32
 
33
33
  ## Write a protocol
34
34
 
35
- Describe biological designs with native pySBOL3 components. An experiment builder records their laboratory operations in a `Protocol`, and `compile()` validates and compiles that protocol for the selected target.
35
+ A `Protocol` records what a scientist would write at the bench: the plates, what starts in each well, and every transfer and mix. This one prepares a two-fold dilution series of fluorescein in PBS, the standard curve for a plate reader.
36
36
 
37
37
  ```python
38
- import sbol3
39
- from lab import compile
38
+ from lab import Protocol, compile, uL
39
+ from lab.deck import Container, Deck, DeckSite
40
40
  from lab.equipment import LiquidHandler
41
- from lab.experiments.cloning import Transformation, build_transformation, transformation_deck
41
+ from lab.labware import PLATE_96
42
42
 
43
- designs = sbol3.Document()
44
- strain = sbol3.Component("https://example.org/my_strain", sbol3.SBO_FUNCTIONAL_ENTITY)
45
- chassis = sbol3.Component("https://example.org/my_cells", sbol3.SBO_FUNCTIONAL_ENTITY)
46
- plasmid = sbol3.Component("https://example.org/my_plasmid", sbol3.SBO_DNA)
47
- designs.add([strain, chassis, plasmid])
43
+ protocol = Protocol("fluorescein serial dilution")
44
+ sources = protocol.plate("sources", capacity=360 * uL)
45
+ assay = protocol.plate("assay", capacity=360 * uL)
48
46
 
49
- protocol = build_transformation(
50
- [Transformation(strain=strain, chassis=chassis, plasmids=[plasmid])],
51
- name="transformation",
52
- )
53
- compiled = compile(
54
- protocol,
55
- deck=transformation_deck(on_module=True),
56
- liquid_handler=LiquidHandler.OT2,
47
+ fluorescein, pbs, waste = sources["A1"], sources["A2"], sources["A3"]
48
+ protocol.load(fluorescein, "10 µM fluorescein in PBS", volume=150 * uL)
49
+ protocol.load(pbs, "PBS", volume=300 * uL)
50
+
51
+ dilutions = [assay[f"A{column}"] for column in range(1, 7)]
52
+ protocol.transfer(fluorescein, dilutions[0], volume=100 * uL)
53
+ protocol.distribute(pbs, dilutions[1:], volume=50 * uL)
54
+
55
+ # Carry 50 µL down the row, mixing each well before the next transfer.
56
+ for previous, current in zip(dilutions, dilutions[1:]):
57
+ protocol.transfer(previous, current, volume=50 * uL)
58
+ protocol.mix(current, volume=40 * uL, cycles=3)
59
+ protocol.transfer(dilutions[-1], waste, volume=50 * uL)
60
+ protocol.manual("Read fluorescence at 485 nm excitation and 525 nm emission.")
61
+
62
+ deck = Deck(
63
+ containers=(
64
+ Container(id="sources", labware=PLATE_96, site=DeckSite.PLATES),
65
+ Container(id="assay", labware=PLATE_96, site=DeckSite.PLATES),
66
+ )
57
67
  )
68
+ compiled = compile(protocol, deck=deck, liquid_handler=LiquidHandler.FLEX)
58
69
  ```
59
70
 
71
+ `compile()` checks every volume against the wells and the pipette, then writes a printable protocol and an Opentrons Flex program. The deck only says both plates sit in open plate positions; the Flex preset chooses the slots, tip rack, and pipette. Wells A1–A6 of the assay plate each finish with 50 µL.
72
+
60
73
  ## Describe a deck
61
74
 
62
- This OT-2 deck places two 96-well plates in slots 1 and 2, a 300 µL tip rack in slot 3, and a P300 pipette on the left mount. It uses the same equipment and placement types as the [deck layouts example](https://github.com/MyersResearchGroup/LabCompiler/blob/master/examples/deck_layouts.py).
75
+ A `DeckLayout` states the equipment and positions instead of leaving them to a preset. This OT-2 deck places the dilution protocol's `sources` and `assay` plates in slots 1 and 2, a 300 µL tip rack in slot 3, and a P300 pipette on the left mount. It uses the same equipment and placement types as the [deck layouts example](https://github.com/MyersResearchGroup/LabCompiler/blob/master/examples/deck_layouts.py).
63
76
 
64
77
  ```python
65
78
  from lab.deck import Deck, DeckLayout, Pipette, Placement, Slot, TipRack
@@ -105,6 +118,12 @@ deck = Deck(
105
118
  )
106
119
  ```
107
120
 
121
+ Compile the dilution protocol for that robot:
122
+
123
+ ```python
124
+ compiled = compile(protocol, deck=deck, liquid_handler=LiquidHandler.OT2)
125
+ ```
126
+
108
127
  ## Describe physical layouts in Lab
109
128
 
110
129
  Use `DeckLayout` when equipment or placement needs to be explicit. Each layout names its `LiquidHandler` and places the same shared containers. All objects below belong to Lab:
@@ -133,6 +152,8 @@ The compiler rejects missing layouts, conflicting placements, invalid holder ref
133
152
 
134
153
  For Opentrons, place thermal labware on a supported `Module`. STAR layouts currently support one independent `Channel` and external thermal integration. Declare thermal containers in `DeckLayout.external_thermal_resources` and supply the async `thermocycle` callback when the generated `run()` executes. The callback is responsible for controlling the device and returning the plate to its original position. Compilation checks the declaration; the software preview only reports the request. On-deck `Module` models are not yet supported by the STAR backend.
135
154
 
155
+ By default, thermocycling matches the lid target to each block target within the device's heated lid range and turns lid heating off below that range. On Opentrons, the lid follows block targets at 37 °C and above; colder holds turn off the lid heater. Persistent thermocycler holds use the same rule, including setup and plating. An explicit `lid_temperature` keeps the lid at a fixed target throughout a profile. Successful profiles keep their final temperature settings; a failed Opentrons profile shuts down both controllers. External STAR thermal callbacks follow the same contract when `lid_temperature=None`.
156
+
136
157
  ## Try it
137
158
 
138
159
  The examples live in the source repository. With [uv](https://docs.astral.sh/uv/) installed, set up Python 3.12 and both optional SDKs:
@@ -165,6 +186,32 @@ The cloning example writes separate `assembly`, `transformation`, and `plating`
165
186
 
166
187
  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.
167
188
 
189
+ 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.
190
+
191
+ The [worksheet UI guide](docs/worksheet-ui.md) describes its typed view data, reusable templates, and interactive components.
192
+
193
+ 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.
194
+
195
+ 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.
196
+
197
+ ## Concentration series
198
+
199
+ 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.
200
+
201
+ 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.
202
+
203
+ `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.
204
+
205
+ ```sh
206
+ uv run --no-sync python -m examples.concentration_series \
207
+ --csv examples/concentration_series/layout.csv \
208
+ --config examples/concentration_series/config.toml --target manual
209
+ ```
210
+
211
+ 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.
212
+
213
+ `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.
214
+
168
215
  ## Development
169
216
 
170
217
  From the repository root:
@@ -176,4 +223,12 @@ uv run --no-sync mypy
176
223
  uv run --no-sync pytest
177
224
  ```
178
225
 
226
+ 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.
227
+
228
+ 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.
229
+
230
+ ```sh
231
+ PUDU_SOURCE=/path/to/PUDU/src uv run --no-sync pytest -q tests/test_ot2_equivalence.py
232
+ ```
233
+
179
234
  See the [release guide](https://github.com/MyersResearchGroup/LabCompiler/blob/master/docs/releasing.md) for package validation and PyPI publishing, and the [release notes](https://github.com/MyersResearchGroup/LabCompiler/blob/master/CHANGELOG.md) for changes and migration guidance.
@@ -0,0 +1,47 @@
1
+ # Typed protocol configuration
2
+
3
+ `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.
4
+
5
+ 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:
6
+
7
+ ```python
8
+ from lab.config import ConfigModel, Concentration, NonnegativeVolume, Volume
9
+
10
+
11
+ class ProtocolConfig(ConfigModel):
12
+ final_volume: Volume
13
+ source_dead_volume: NonnegativeVolume
14
+ stock_concentration: Concentration
15
+
16
+
17
+ config = ProtocolConfig.model_validate(
18
+ {
19
+ "final_volume": "0.2 mL",
20
+ "source_dead_volume": "20 uL",
21
+ "stock_concentration": "1 mM",
22
+ }
23
+ )
24
+ print(config.final_volume) # 2E+2 microliter
25
+ print(config.model_dump_json())
26
+ ```
27
+
28
+ | Field type | Accepted values and Python representation |
29
+ | --- | --- |
30
+ | `Volume` | Positive finite quantity, normalized to microlitres |
31
+ | `NonnegativeVolume` | As above, allowing zero; useful for dead volumes |
32
+ | `FlowRate` | Positive finite quantity, normalized to microlitres/second |
33
+ | `Concentration` | Nonnegative finite quantity, normalized to mole/litre or gram/litre |
34
+ | `ConcentrationUnit` | A separate mass/volume or amount/volume unit; retains its original scale for CSV numeric columns |
35
+ | `VolumeFraction` | Finite Decimal from zero through one |
36
+ | `NonnegativeDecimal` | Finite Decimal greater than or equal to zero |
37
+ | `NonemptyString` | String containing at least one non-whitespace character |
38
+
39
+ 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.
40
+
41
+ `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.
42
+
43
+ `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).
44
+
45
+ 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.
46
+
47
+ 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.
@@ -0,0 +1,87 @@
1
+ # Liquid classes
2
+
3
+ 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.
4
+
5
+ ## Portable custom settings
6
+
7
+ ```python
8
+ from lab import LiquidClass, Protocol, seconds, uL
9
+
10
+ gentle = LiquidClass(
11
+ "gentle culture",
12
+ aspiration_flow_rate=20 * uL / seconds,
13
+ dispense_flow_rate=30 * uL / seconds,
14
+ )
15
+ p = Protocol("Culture aliquot")
16
+ source = p.container("source", contents="culture", volume=200 * uL, capacity=360 * uL)
17
+ destination = p.container("destination", capacity=360 * uL)
18
+ p.transfer(source, destination, volume=50 * uL, liquid_class=gentle)
19
+ p.mix(destination, volume=40 * uL, cycles=3, liquid_class=gentle)
20
+ ```
21
+
22
+ `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.
23
+
24
+ | Target | Application |
25
+ | --- | --- |
26
+ | Manual | Print the name and both rates beside the operation |
27
+ | OT-2 and Flex | Set the selected pipette's absolute aspiration/dispense rates for the operation, then restore its previous rates |
28
+ | STAR | Pass absolute `flow_rates` to each PyLabRobot aspiration and dispense |
29
+
30
+ 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.
31
+
32
+ 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.
33
+
34
+ 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.
35
+
36
+ ## Hamilton built-ins
37
+
38
+ 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).
39
+
40
+ Reference an exact symbol through Lab's SDK-independent type:
41
+
42
+ ```python
43
+ from lab import HamiltonLiquidClass
44
+
45
+ water = HamiltonLiquidClass("HighVolumeFilter_Water_DispenseSurface_Part")
46
+ p.transfer(source, destination, volume=50 * uL, liquid_class=water)
47
+ ```
48
+
49
+ 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.
50
+
51
+ With `lab-compiler[star]` installed, discover the available symbols without connecting to hardware:
52
+
53
+ ```python
54
+ from lab.targets.star import hamilton_liquid_class_names
55
+
56
+ names = hamilton_liquid_class_names()
57
+ print([name for name in names if "DMSO" in name])
58
+ ```
59
+
60
+ 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.
61
+
62
+ 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.
63
+
64
+ 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.
65
+
66
+ ## CSV preparation
67
+
68
+ 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.
69
+
70
+ `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.
71
+
72
+ 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.
73
+
74
+ ```toml
75
+ [liquid_classes.culture]
76
+ kind = "portable"
77
+ aspiration_flow_rate = "20 uL/s"
78
+ dispense_flow_rate = "20 uL/s"
79
+ ```
80
+
81
+ 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.
82
+
83
+ ## Establishing LMSF equivalence
84
+
85
+ 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.
86
+
87
+ 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.
@@ -26,42 +26,50 @@ After a repository transfer or rename, update the publisher in PyPI before publi
26
26
 
27
27
  ## Validate a release
28
28
 
29
- Update `src/lab/_version.py` when preparing a new version, then run from the repository root:
29
+ Update `src/lab/_version.py` and add the changes and migration guidance to `CHANGELOG.md` when preparing a new version, then run from the repository root:
30
30
 
31
31
  ```sh
32
32
  uv lock
33
- uv sync --locked --all-extras
33
+ uv sync --locked --all-extras --python 3.12
34
34
  uv run --no-sync ruff check .
35
35
  uv run --no-sync mypy
36
36
  uv run --no-sync pytest -q
37
- uv build --no-sources
38
- uv run --no-sync twine check --strict dist/*
37
+ uv build --no-sources --out-dir dist/0.3.0
38
+ uv run --no-sync twine check --strict dist/0.3.0/*
39
39
  ```
40
40
 
41
- Build into an empty `dist/` directory so old releases are not included. `uv build` creates a source distribution and builds the wheel from it. Confirm both artifacts can be installed in fresh environments; for version `0.2.0`:
41
+ Use an empty directory for the version being prepared so old releases are not included. `uv build` creates a source distribution and builds the wheel from it. Confirm both artifacts can be installed in fresh environments; for version `0.3.0`:
42
42
 
43
43
  ```sh
44
- uv venv /tmp/lab-compiler-wheel --python 3.12
45
- uv pip install --python /tmp/lab-compiler-wheel/bin/python dist/lab_compiler-0.2.0-py3-none-any.whl
46
- /tmp/lab-compiler-wheel/bin/python -I scripts/check_install.py
44
+ uv venv /tmp/lab-compiler-0.3.0-wheel --python 3.12
45
+ uv pip install --python /tmp/lab-compiler-0.3.0-wheel/bin/python dist/0.3.0/lab_compiler-0.3.0-py3-none-any.whl
46
+ /tmp/lab-compiler-0.3.0-wheel/bin/python -I scripts/check_install.py
47
47
 
48
- uv venv /tmp/lab-compiler-sdist --python 3.12
49
- uv pip install --python /tmp/lab-compiler-sdist/bin/python dist/lab_compiler-0.2.0.tar.gz
50
- /tmp/lab-compiler-sdist/bin/python -I scripts/check_install.py
48
+ uv venv /tmp/lab-compiler-0.3.0-sdist --python 3.12
49
+ uv pip install --python /tmp/lab-compiler-0.3.0-sdist/bin/python dist/0.3.0/lab_compiler-0.3.0.tar.gz
50
+ /tmp/lab-compiler-0.3.0-sdist/bin/python -I scripts/check_install.py
51
51
  ```
52
52
 
53
- The install check verifies distribution metadata, version agreement, the license and typing marker, and manual compilation without importing robot SDKs. `-I` ensures Python imports the installed package. CI runs the test suite with core dependencies and both optional SDKs on Python 3.11 and 3.12, then builds and checks both artifacts. PUDU equivalence tests skip unless the separate checkout is available at the path specified in `tests/test_ot2_equivalence.py`; CI does not fetch it. Robot tests use offline simulation and command recording.
53
+ The install check verifies distribution metadata, version agreement, the license and typing marker, and worksheet rendering from the installed templates and assets without importing Pydantic or robot SDKs. `-I` ensures Python imports the installed package. CI runs the test suite with core dependencies and both optional SDKs on Python 3.11 and 3.12, then builds and checks both artifacts. Robot tests use offline simulation and command recording.
54
+
55
+ CI also checks out PUDU at the revision pinned in `.github/workflows/ci.yml` and requires OT-2 equivalence before building distributions. To run the same comparison locally, check out that revision separately and set its source path explicitly:
56
+
57
+ ```sh
58
+ PUDU_SOURCE=/path/to/pinned/PUDU/src uv run --no-sync pytest -q tests/test_ot2_equivalence.py
59
+ ```
60
+
61
+ With `PUDU_SOURCE` set, missing reference source or a missing Opentrons SDK is an error. The ordinary local suite can skip these tests when no reference checkout is available. CI also validates the optional upstream LabOP integration in its separate environment.
54
62
 
55
63
  ## Publish
56
64
 
57
65
  1. Merge the release changes and confirm CI passes on the intended commit.
58
- 2. Create a GitHub release for that commit with a tag matching the package version exactly, such as `v0.2.0`.
66
+ 2. Create a GitHub release for that commit with a tag matching the package version exactly, such as `v0.3.0`, and use the corresponding section of `CHANGELOG.md` as its release notes.
59
67
  3. Publish the GitHub release. Draft releases do not upload packages.
60
68
  4. The `Publish to PyPI` workflow runs CI again for the release, checks the tag against the package version, and uploads the validated artifacts through the `pypi` environment. Approve the environment deployment if required reviewers are configured.
61
69
  5. Confirm the version appears on [PyPI](https://pypi.org/project/lab-compiler/) and install it in a fresh environment:
62
70
 
63
71
  ```sh
64
- python -m pip install "lab-compiler==0.2.0"
72
+ python -m pip install "lab-compiler==0.3.0"
65
73
  python -c "import lab; print(lab.__version__)"
66
74
  ```
67
75