Initial commit
This commit is contained in:
179
skills/pylabrobot/SKILL.md
Normal file
179
skills/pylabrobot/SKILL.md
Normal file
@@ -0,0 +1,179 @@
|
||||
---
|
||||
name: pylabrobot
|
||||
description: Laboratory automation toolkit for controlling liquid handlers, plate readers, pumps, heater shakers, incubators, centrifuges, and analytical equipment. Use this skill when automating laboratory workflows, programming liquid handling robots (Hamilton STAR, Opentrons OT-2, Tecan EVO), integrating lab equipment, managing deck layouts and resources (plates, tips, containers), reading plates, or creating reproducible laboratory protocols. Applicable for both simulated protocols and physical hardware control.
|
||||
---
|
||||
|
||||
# PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
PyLabRobot is a hardware-agnostic, pure Python Software Development Kit for automated and autonomous laboratories. Use this skill to control liquid handling robots, plate readers, pumps, heater shakers, incubators, centrifuges, and other laboratory automation equipment through a unified Python interface that works across platforms (Windows, macOS, Linux).
|
||||
|
||||
## When to Use This Skill
|
||||
|
||||
Use this skill when:
|
||||
- Programming liquid handling robots (Hamilton STAR/STARlet, Opentrons OT-2, Tecan EVO)
|
||||
- Automating laboratory workflows involving pipetting, sample preparation, or analytical measurements
|
||||
- Managing deck layouts and laboratory resources (plates, tips, containers, troughs)
|
||||
- Integrating multiple lab devices (liquid handlers, plate readers, heater shakers, pumps)
|
||||
- Creating reproducible laboratory protocols with state management
|
||||
- Simulating protocols before running on physical hardware
|
||||
- Reading plates using BMG CLARIOstar or other supported plate readers
|
||||
- Controlling temperature, shaking, centrifugation, or other material handling operations
|
||||
- Working with laboratory automation in Python
|
||||
|
||||
## Core Capabilities
|
||||
|
||||
PyLabRobot provides comprehensive laboratory automation through six main capability areas, each detailed in the references/ directory:
|
||||
|
||||
### 1. Liquid Handling (`references/liquid-handling.md`)
|
||||
|
||||
Control liquid handling robots for aspirating, dispensing, and transferring liquids. Key operations include:
|
||||
- **Basic Operations**: Aspirate, dispense, transfer liquids between wells
|
||||
- **Tip Management**: Pick up, drop, and track pipette tips automatically
|
||||
- **Advanced Techniques**: Multi-channel pipetting, serial dilutions, plate replication
|
||||
- **Volume Tracking**: Automatic tracking of liquid volumes in wells
|
||||
- **Hardware Support**: Hamilton STAR/STARlet, Opentrons OT-2, Tecan EVO, and others
|
||||
|
||||
### 2. Resource Management (`references/resources.md`)
|
||||
|
||||
Manage laboratory resources in a hierarchical system:
|
||||
- **Resource Types**: Plates, tip racks, troughs, tubes, carriers, and custom labware
|
||||
- **Deck Layout**: Assign resources to deck positions with coordinate systems
|
||||
- **State Management**: Track tip presence, liquid volumes, and resource states
|
||||
- **Serialization**: Save and load deck layouts and states from JSON files
|
||||
- **Resource Discovery**: Access wells, tips, and containers through intuitive APIs
|
||||
|
||||
### 3. Hardware Backends (`references/hardware-backends.md`)
|
||||
|
||||
Connect to diverse laboratory equipment through backend abstraction:
|
||||
- **Liquid Handlers**: Hamilton STAR (full support), Opentrons OT-2, Tecan EVO
|
||||
- **Simulation**: ChatterboxBackend for protocol testing without hardware
|
||||
- **Platform Support**: Works on Windows, macOS, Linux, and Raspberry Pi
|
||||
- **Backend Switching**: Change robots by swapping backend without rewriting protocols
|
||||
|
||||
### 4. Analytical Equipment (`references/analytical-equipment.md`)
|
||||
|
||||
Integrate plate readers and analytical instruments:
|
||||
- **Plate Readers**: BMG CLARIOstar for absorbance, luminescence, fluorescence
|
||||
- **Scales**: Mettler Toledo integration for mass measurements
|
||||
- **Integration Patterns**: Combine liquid handlers with analytical equipment
|
||||
- **Automated Workflows**: Move plates between devices automatically
|
||||
|
||||
### 5. Material Handling (`references/material-handling.md`)
|
||||
|
||||
Control environmental and material handling equipment:
|
||||
- **Heater Shakers**: Hamilton HeaterShaker, Inheco ThermoShake
|
||||
- **Incubators**: Inheco and Thermo Fisher incubators with temperature control
|
||||
- **Centrifuges**: Agilent VSpin with bucket positioning and spin control
|
||||
- **Pumps**: Cole Parmer Masterflex for fluid pumping operations
|
||||
- **Temperature Control**: Set and monitor temperatures during protocols
|
||||
|
||||
### 6. Visualization & Simulation (`references/visualization.md`)
|
||||
|
||||
Visualize and simulate laboratory protocols:
|
||||
- **Browser Visualizer**: Real-time 3D visualization of deck state
|
||||
- **Simulation Mode**: Test protocols without physical hardware
|
||||
- **State Tracking**: Monitor tip presence and liquid volumes visually
|
||||
- **Deck Editor**: Graphical tool for designing deck layouts
|
||||
- **Protocol Validation**: Verify protocols before running on hardware
|
||||
|
||||
## Quick Start
|
||||
|
||||
To get started with PyLabRobot, install the package and initialize a liquid handler:
|
||||
|
||||
```python
|
||||
# Install PyLabRobot
|
||||
# uv pip install pylabrobot
|
||||
|
||||
# Basic liquid handling setup
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# Basic operations
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.aspirate(plate["A1"], vols=100)
|
||||
await lh.dispense(plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
## Working with References
|
||||
|
||||
This skill organizes detailed information across multiple reference files. Load the relevant reference when:
|
||||
- **Liquid Handling**: Writing pipetting protocols, tip management, transfers
|
||||
- **Resources**: Defining deck layouts, managing plates/tips, custom labware
|
||||
- **Hardware Backends**: Connecting to specific robots, switching platforms
|
||||
- **Analytical Equipment**: Integrating plate readers, scales, or analytical devices
|
||||
- **Material Handling**: Using heater shakers, incubators, centrifuges, pumps
|
||||
- **Visualization**: Simulating protocols, visualizing deck states
|
||||
|
||||
All reference files can be found in the `references/` directory and contain comprehensive examples, API usage patterns, and best practices.
|
||||
|
||||
## Best Practices
|
||||
|
||||
When creating laboratory automation protocols with PyLabRobot:
|
||||
|
||||
1. **Start with Simulation**: Use ChatterboxBackend and the visualizer to test protocols before running on hardware
|
||||
2. **Enable Tracking**: Turn on tip tracking and volume tracking for accurate state management
|
||||
3. **Resource Naming**: Use clear, descriptive names for all resources (plates, tip racks, containers)
|
||||
4. **State Serialization**: Save deck layouts and states to JSON for reproducibility
|
||||
5. **Error Handling**: Implement proper async error handling for hardware operations
|
||||
6. **Temperature Control**: Set temperatures early as heating/cooling takes time
|
||||
7. **Modular Protocols**: Break complex workflows into reusable functions
|
||||
8. **Documentation**: Reference official docs at https://docs.pylabrobot.org for latest features
|
||||
|
||||
## Common Workflows
|
||||
|
||||
### Liquid Transfer Protocol
|
||||
|
||||
```python
|
||||
# Setup
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tip_rack")
|
||||
source_plate = Cos_96_DW_1mL(name="source")
|
||||
dest_plate = Cos_96_DW_1mL(name="dest")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(source_plate, rails=10)
|
||||
lh.deck.assign_child_resource(dest_plate, rails=15)
|
||||
|
||||
# Transfer protocol
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(source_plate["A1:H12"], dest_plate["A1:H12"], vols=100)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Plate Reading Workflow
|
||||
|
||||
```python
|
||||
# Setup plate reader
|
||||
from pylabrobot.plate_reading import PlateReader
|
||||
from pylabrobot.plate_reading.clario_star_backend import CLARIOstarBackend
|
||||
|
||||
pr = PlateReader(name="CLARIOstar", backend=CLARIOstarBackend())
|
||||
await pr.setup()
|
||||
|
||||
# Set temperature and read
|
||||
await pr.set_temperature(37)
|
||||
await pr.open()
|
||||
# (manually or robotically load plate)
|
||||
await pr.close()
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- **Official Documentation**: https://docs.pylabrobot.org
|
||||
- **GitHub Repository**: https://github.com/PyLabRobot/pylabrobot
|
||||
- **Community Forum**: https://discuss.pylabrobot.org
|
||||
- **PyPI Package**: https://pypi.org/project/PyLabRobot/
|
||||
|
||||
For detailed usage of specific capabilities, refer to the corresponding reference file in the `references/` directory.
|
||||
464
skills/pylabrobot/references/analytical-equipment.md
Normal file
464
skills/pylabrobot/references/analytical-equipment.md
Normal file
@@ -0,0 +1,464 @@
|
||||
# Analytical Equipment in PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
PyLabRobot integrates with analytical equipment including plate readers, scales, and other measurement devices. This allows automated workflows that combine liquid handling with analytical measurements.
|
||||
|
||||
## Plate Readers
|
||||
|
||||
### BMG CLARIOstar (Plus)
|
||||
|
||||
The BMG Labtech CLARIOstar and CLARIOstar Plus are microplate readers that measure absorbance, luminescence, and fluorescence.
|
||||
|
||||
#### Hardware Setup
|
||||
|
||||
**Physical Connections:**
|
||||
1. IEC C13 power cord to mains power
|
||||
2. USB-B cable to computer (with security screws on device end)
|
||||
3. Optional: RS-232 port for plate stacking units
|
||||
|
||||
**Communication:**
|
||||
- Serial connection through FTDI/USB-A at firmware level
|
||||
- Cross-platform support (Windows, macOS, Linux)
|
||||
|
||||
#### Software Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.plate_reading import PlateReader
|
||||
from pylabrobot.plate_reading.clario_star_backend import CLARIOstarBackend
|
||||
|
||||
# Create backend
|
||||
backend = CLARIOstarBackend()
|
||||
|
||||
# Initialize plate reader
|
||||
pr = PlateReader(
|
||||
name="CLARIOstar",
|
||||
backend=backend,
|
||||
size_x=0.0, # Physical dimensions not critical for plate readers
|
||||
size_y=0.0,
|
||||
size_z=0.0
|
||||
)
|
||||
|
||||
# Setup (initializes device)
|
||||
await pr.setup()
|
||||
|
||||
# When done
|
||||
await pr.stop()
|
||||
```
|
||||
|
||||
#### Basic Operations
|
||||
|
||||
**Opening and Closing:**
|
||||
|
||||
```python
|
||||
# Open loading tray
|
||||
await pr.open()
|
||||
|
||||
# (Load plate manually or robotically)
|
||||
|
||||
# Close loading tray
|
||||
await pr.close()
|
||||
```
|
||||
|
||||
**Temperature Control:**
|
||||
|
||||
```python
|
||||
# Set temperature (in Celsius)
|
||||
await pr.set_temperature(37)
|
||||
|
||||
# Note: Reaching temperature is slow
|
||||
# Set temperature early in protocol
|
||||
```
|
||||
|
||||
**Reading Measurements:**
|
||||
|
||||
```python
|
||||
# Absorbance reading
|
||||
data = await pr.read_absorbance(wavelength=450) # nm
|
||||
|
||||
# Luminescence reading
|
||||
data = await pr.read_luminescence()
|
||||
|
||||
# Fluorescence reading
|
||||
data = await pr.read_fluorescence(
|
||||
excitation_wavelength=485, # nm
|
||||
emission_wavelength=535 # nm
|
||||
)
|
||||
```
|
||||
|
||||
#### Data Format
|
||||
|
||||
Plate reader methods return array data:
|
||||
|
||||
```python
|
||||
import numpy as np
|
||||
|
||||
# Read absorbance
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
|
||||
# data is typically a 2D array (8x12 for 96-well plate)
|
||||
print(f"Data shape: {data.shape}")
|
||||
print(f"Well A1: {data[0][0]}")
|
||||
print(f"Well H12: {data[7][11]}")
|
||||
|
||||
# Convert to DataFrame for easier handling
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(data)
|
||||
```
|
||||
|
||||
#### Integration with Liquid Handler
|
||||
|
||||
Combine plate reading with liquid handling:
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
from pylabrobot.plate_reading import PlateReader
|
||||
from pylabrobot.plate_reading.clario_star_backend import CLARIOstarBackend
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# Initialize plate reader
|
||||
pr = PlateReader(name="CLARIOstar", backend=CLARIOstarBackend())
|
||||
await pr.setup()
|
||||
|
||||
# Set temperature early
|
||||
await pr.set_temperature(37)
|
||||
|
||||
try:
|
||||
# Prepare samples with liquid handler
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
reagent_plate = Cos_96_DW_1mL(name="reagents")
|
||||
assay_plate = Cos_96_DW_1mL(name="assay")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(reagent_plate, rails=10)
|
||||
lh.deck.assign_child_resource(assay_plate, rails=15)
|
||||
|
||||
# Transfer samples
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
reagent_plate["A1:H12"],
|
||||
assay_plate["A1:H12"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Move plate to reader (manual or robotic arm)
|
||||
print("Move assay plate to plate reader")
|
||||
input("Press Enter when plate is loaded...")
|
||||
|
||||
# Read plate
|
||||
await pr.open()
|
||||
# (plate loaded here)
|
||||
await pr.close()
|
||||
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
print(f"Absorbance data: {data}")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await pr.stop()
|
||||
```
|
||||
|
||||
#### Advanced Features
|
||||
|
||||
**Development Status:**
|
||||
|
||||
Some CLARIOstar features are under development:
|
||||
- Spectral scanning
|
||||
- Injector needle control
|
||||
- Detailed measurement parameter configuration
|
||||
- Well-specific reading patterns
|
||||
|
||||
Check current documentation for latest feature support.
|
||||
|
||||
#### Best Practices
|
||||
|
||||
1. **Temperature Control**: Set temperature early as heating is slow
|
||||
2. **Plate Loading**: Ensure plate is properly seated before closing
|
||||
3. **Measurement Selection**: Choose appropriate wavelengths for your assay
|
||||
4. **Data Validation**: Check measurement quality and expected ranges
|
||||
5. **Error Handling**: Handle timeout and communication errors
|
||||
6. **Maintenance**: Keep optics clean per manufacturer guidelines
|
||||
|
||||
#### Example: Complete Plate Reading Workflow
|
||||
|
||||
```python
|
||||
async def run_plate_reading_assay():
|
||||
"""Complete workflow with sample prep and reading"""
|
||||
|
||||
# Initialize equipment
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
pr = PlateReader(name="CLARIOstar", backend=CLARIOstarBackend())
|
||||
|
||||
await lh.setup()
|
||||
await pr.setup()
|
||||
|
||||
# Set plate reader temperature
|
||||
await pr.set_temperature(37)
|
||||
|
||||
try:
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
samples = Cos_96_DW_1mL(name="samples")
|
||||
assay_plate = Cos_96_DW_1mL(name="assay")
|
||||
substrate = Trough_100ml(name="substrate")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(substrate, rails=5)
|
||||
lh.deck.assign_child_resource(samples, rails=10)
|
||||
lh.deck.assign_child_resource(assay_plate, rails=15)
|
||||
|
||||
# Transfer samples
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
samples["A1:H12"],
|
||||
assay_plate["A1:H12"],
|
||||
vols=50
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Add substrate
|
||||
await lh.pick_up_tips(tip_rack["A2:H2"])
|
||||
for col in range(1, 13):
|
||||
await lh.transfer(
|
||||
substrate["channel_1"],
|
||||
assay_plate[f"A{col}:H{col}"],
|
||||
vols=50
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Incubate (if needed)
|
||||
# await asyncio.sleep(300) # 5 minutes
|
||||
|
||||
# Move to plate reader
|
||||
print("Transfer assay plate to CLARIOstar")
|
||||
input("Press Enter when ready...")
|
||||
|
||||
await pr.open()
|
||||
input("Press Enter when plate is loaded...")
|
||||
await pr.close()
|
||||
|
||||
# Read absorbance
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
|
||||
# Process results
|
||||
import pandas as pd
|
||||
df = pd.DataFrame(
|
||||
data,
|
||||
index=[f"{r}" for r in "ABCDEFGH"],
|
||||
columns=[f"{c}" for c in range(1, 13)]
|
||||
)
|
||||
|
||||
print("Absorbance Results:")
|
||||
print(df)
|
||||
|
||||
# Save results
|
||||
df.to_csv("plate_reading_results.csv")
|
||||
|
||||
return df
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await pr.stop()
|
||||
|
||||
# Run assay
|
||||
results = await run_plate_reading_assay()
|
||||
```
|
||||
|
||||
## Scales
|
||||
|
||||
### Mettler Toledo Scales
|
||||
|
||||
PyLabRobot supports Mettler Toledo scales for mass measurements.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.scales import Scale
|
||||
from pylabrobot.scales.mettler_toledo_backend import MettlerToledoBackend
|
||||
|
||||
# Create scale
|
||||
scale = Scale(
|
||||
name="analytical_scale",
|
||||
backend=MettlerToledoBackend()
|
||||
)
|
||||
|
||||
await scale.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
```python
|
||||
# Get weight measurement
|
||||
weight = await scale.get_weight() # Returns weight in grams
|
||||
print(f"Weight: {weight} g")
|
||||
|
||||
# Tare (zero) the scale
|
||||
await scale.tare()
|
||||
|
||||
# Get multiple measurements
|
||||
weights = []
|
||||
for i in range(5):
|
||||
w = await scale.get_weight()
|
||||
weights.append(w)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
average_weight = sum(weights) / len(weights)
|
||||
print(f"Average weight: {average_weight} g")
|
||||
```
|
||||
|
||||
#### Integration with Liquid Handler
|
||||
|
||||
```python
|
||||
# Weigh samples during protocol
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
scale = Scale(name="scale", backend=MettlerToledoBackend())
|
||||
|
||||
await lh.setup()
|
||||
await scale.setup()
|
||||
|
||||
try:
|
||||
# Tare scale
|
||||
await scale.tare()
|
||||
|
||||
# Dispense liquid
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.aspirate(reagent["A1"], vols=1000)
|
||||
|
||||
# (Move to scale position)
|
||||
|
||||
# Dispense and weigh
|
||||
await lh.dispense(container, vols=1000)
|
||||
weight = await scale.get_weight()
|
||||
|
||||
print(f"Dispensed weight: {weight} g")
|
||||
|
||||
# Calculate actual volume (assuming density = 1 g/mL for water)
|
||||
actual_volume = weight * 1000 # Convert g to µL
|
||||
print(f"Actual volume: {actual_volume} µL")
|
||||
|
||||
await lh.drop_tips()
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await scale.stop()
|
||||
```
|
||||
|
||||
## Other Analytical Devices
|
||||
|
||||
### Flow Cytometers
|
||||
|
||||
Some flow cytometer integrations are in development. Check current documentation for support status.
|
||||
|
||||
### Spectrophotometers
|
||||
|
||||
Additional spectrophotometer models may be supported. Check documentation for current device compatibility.
|
||||
|
||||
## Multi-Device Workflows
|
||||
|
||||
### Coordinating Multiple Devices
|
||||
|
||||
```python
|
||||
async def multi_device_workflow():
|
||||
"""Coordinate liquid handler, plate reader, and scale"""
|
||||
|
||||
# Initialize all devices
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
pr = PlateReader(name="CLARIOstar", backend=CLARIOstarBackend())
|
||||
scale = Scale(name="scale", backend=MettlerToledoBackend())
|
||||
|
||||
await lh.setup()
|
||||
await pr.setup()
|
||||
await scale.setup()
|
||||
|
||||
try:
|
||||
# 1. Weigh reagent
|
||||
await scale.tare()
|
||||
# (place container on scale)
|
||||
reagent_weight = await scale.get_weight()
|
||||
|
||||
# 2. Prepare samples with liquid handler
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(source["A1:H12"], dest["A1:H12"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# 3. Read plate
|
||||
await pr.open()
|
||||
# (load plate)
|
||||
await pr.close()
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
|
||||
return {
|
||||
"reagent_weight": reagent_weight,
|
||||
"absorbance_data": data
|
||||
}
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await pr.stop()
|
||||
await scale.stop()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Device Initialization**: Setup all devices at start of protocol
|
||||
2. **Error Handling**: Handle communication errors gracefully
|
||||
3. **Cleanup**: Always call `stop()` on all devices
|
||||
4. **Timing**: Account for device-specific timing (temperature equilibration, measurement time)
|
||||
5. **Calibration**: Follow manufacturer calibration procedures
|
||||
6. **Data Validation**: Verify measurements are within expected ranges
|
||||
7. **Documentation**: Record device settings and parameters
|
||||
8. **Integration Testing**: Test multi-device workflows thoroughly
|
||||
9. **Concurrent Operations**: Use async to overlap operations when possible
|
||||
10. **Data Storage**: Save raw data with metadata (timestamps, settings)
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Kinetic Plate Reading
|
||||
|
||||
```python
|
||||
async def kinetic_reading(num_reads: int, interval: int):
|
||||
"""Perform kinetic plate reading"""
|
||||
|
||||
pr = PlateReader(name="CLARIOstar", backend=CLARIOstarBackend())
|
||||
await pr.setup()
|
||||
|
||||
try:
|
||||
await pr.set_temperature(37)
|
||||
await pr.open()
|
||||
# (load plate)
|
||||
await pr.close()
|
||||
|
||||
results = []
|
||||
for i in range(num_reads):
|
||||
data = await pr.read_absorbance(wavelength=450)
|
||||
timestamp = time.time()
|
||||
results.append({
|
||||
"read_number": i + 1,
|
||||
"timestamp": timestamp,
|
||||
"data": data
|
||||
})
|
||||
|
||||
if i < num_reads - 1:
|
||||
await asyncio.sleep(interval)
|
||||
|
||||
return results
|
||||
|
||||
finally:
|
||||
await pr.stop()
|
||||
|
||||
# Read every 30 seconds for 10 minutes
|
||||
results = await kinetic_reading(num_reads=20, interval=30)
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Plate Reading Documentation: https://docs.pylabrobot.org/user_guide/02_analytical/
|
||||
- BMG CLARIOstar Guide: https://docs.pylabrobot.org/user_guide/02_analytical/plate-reading/bmg-clariostar.html
|
||||
- API Reference: https://docs.pylabrobot.org/api/pylabrobot.plate_reading.html
|
||||
- Supported Equipment: https://docs.pylabrobot.org/user_guide/machines.html
|
||||
480
skills/pylabrobot/references/hardware-backends.md
Normal file
480
skills/pylabrobot/references/hardware-backends.md
Normal file
@@ -0,0 +1,480 @@
|
||||
# Hardware Backends in PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
PyLabRobot uses a backend abstraction system that allows the same protocol code to run on different liquid handling robots and platforms. Backends handle device-specific communication while the `LiquidHandler` frontend provides a unified interface.
|
||||
|
||||
## Backend Architecture
|
||||
|
||||
### How Backends Work
|
||||
|
||||
1. **Frontend**: `LiquidHandler` class provides high-level API
|
||||
2. **Backend**: Device-specific class handles hardware communication
|
||||
3. **Protocol**: Same code works across different backends
|
||||
|
||||
```python
|
||||
# Same protocol code
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.aspirate(plate["A1"], vols=100)
|
||||
await lh.dispense(plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Works with any backend (STAR, Opentrons, simulation, etc.)
|
||||
```
|
||||
|
||||
### Backend Interface
|
||||
|
||||
All backends inherit from `LiquidHandlerBackend` and implement:
|
||||
- `setup()`: Initialize connection to hardware
|
||||
- `stop()`: Close connection and cleanup
|
||||
- Device-specific command methods (aspirate, dispense, etc.)
|
||||
|
||||
## Supported Backends
|
||||
|
||||
### Hamilton STAR (Full Support)
|
||||
|
||||
The Hamilton STAR and STARlet liquid handling robots have full PyLabRobot support.
|
||||
|
||||
**Setup:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
|
||||
# Create STAR backend
|
||||
backend = STAR()
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=backend, deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
```
|
||||
|
||||
**Platform Support:**
|
||||
- Windows ✅
|
||||
- macOS ✅
|
||||
- Linux ✅
|
||||
- Raspberry Pi ✅
|
||||
|
||||
**Communication:**
|
||||
- USB connection to robot
|
||||
- Direct firmware commands
|
||||
- No Hamilton software required
|
||||
|
||||
**Features:**
|
||||
- Full liquid handling operations
|
||||
- CO-RE tip support
|
||||
- 96-channel head support (if equipped)
|
||||
- Temperature control
|
||||
- Carrier and rail-based positioning
|
||||
|
||||
**Deck Types:**
|
||||
```python
|
||||
from pylabrobot.resources import STARLetDeck, STARDeck
|
||||
|
||||
# For STARlet (smaller deck)
|
||||
deck = STARLetDeck()
|
||||
|
||||
# For STAR (full deck)
|
||||
deck = STARDeck()
|
||||
```
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck, TIP_CAR_480_A00, Cos_96_DW_1mL
|
||||
|
||||
# Initialize
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
|
||||
# Assign to rails
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(plate, rails=10)
|
||||
|
||||
# Execute protocol
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
### Opentrons OT-2 (Supported)
|
||||
|
||||
The Opentrons OT-2 is supported through the Opentrons HTTP API.
|
||||
|
||||
**Setup:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import OpentronsBackend
|
||||
from pylabrobot.resources import OTDeck
|
||||
|
||||
# Create Opentrons backend (requires robot IP address)
|
||||
backend = OpentronsBackend(host="192.168.1.100") # Replace with your robot's IP
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=backend, deck=OTDeck())
|
||||
await lh.setup()
|
||||
```
|
||||
|
||||
**Platform Support:**
|
||||
- Any platform with network access to OT-2
|
||||
|
||||
**Communication:**
|
||||
- HTTP API over network
|
||||
- Requires robot IP address
|
||||
- No Opentrons app required
|
||||
|
||||
**Features:**
|
||||
- 8-channel pipette support
|
||||
- Single-channel pipette support
|
||||
- Standard OT-2 deck layout
|
||||
- Coordinate-based positioning
|
||||
|
||||
**Limitations:**
|
||||
- Uses older Opentrons HTTP API
|
||||
- Some features may be limited compared to STAR
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import OpentronsBackend
|
||||
from pylabrobot.resources import OTDeck
|
||||
|
||||
# Initialize with robot IP
|
||||
lh = LiquidHandler(
|
||||
backend=OpentronsBackend(host="192.168.1.100"),
|
||||
deck=OTDeck()
|
||||
)
|
||||
await lh.setup()
|
||||
|
||||
# Load deck layout
|
||||
lh.deck = Deck.load_from_json_file("opentrons_layout.json")
|
||||
|
||||
# Execute protocol
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
### Tecan EVO (Work in Progress)
|
||||
|
||||
Support for Tecan EVO liquid handling robots is under development.
|
||||
|
||||
**Current Status:**
|
||||
- Work-in-progress
|
||||
- Basic commands may be available
|
||||
- Check documentation for current feature support
|
||||
|
||||
**Setup (when available):**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import TecanBackend
|
||||
from pylabrobot.resources import TecanDeck
|
||||
|
||||
backend = TecanBackend()
|
||||
lh = LiquidHandler(backend=backend, deck=TecanDeck())
|
||||
```
|
||||
|
||||
### Hamilton Vantage (Mostly Supported)
|
||||
|
||||
Hamilton Vantage has "mostly" complete support.
|
||||
|
||||
**Setup:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling.backends import Vantage
|
||||
from pylabrobot.resources import VantageDeck
|
||||
|
||||
lh = LiquidHandler(backend=Vantage(), deck=VantageDeck())
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- Similar to STAR support
|
||||
- Some advanced features may be limited
|
||||
|
||||
## Simulation Backend
|
||||
|
||||
### ChatterboxBackend (Simulation)
|
||||
|
||||
Test protocols without physical hardware using the simulation backend.
|
||||
|
||||
**Setup:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
|
||||
# Create simulation backend
|
||||
backend = ChatterboxBackend(num_channels=8)
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=backend, deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
```
|
||||
|
||||
**Features:**
|
||||
- No hardware required
|
||||
- Simulates all liquid handling operations
|
||||
- Works with visualizer for real-time feedback
|
||||
- Validates protocol logic
|
||||
- Tracks tips and volumes
|
||||
|
||||
**Use Cases:**
|
||||
- Protocol development and testing
|
||||
- Training and education
|
||||
- CI/CD pipeline testing
|
||||
- Debugging without hardware access
|
||||
|
||||
**Example:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
from pylabrobot.resources import STARLetDeck, TIP_CAR_480_A00, Cos_96_DW_1mL
|
||||
from pylabrobot.resources import set_tip_tracking, set_volume_tracking
|
||||
|
||||
# Enable tracking for simulation
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Initialize with simulation backend
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(num_channels=8),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
await lh.setup()
|
||||
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(plate, rails=10)
|
||||
|
||||
# Set initial volumes
|
||||
for well in plate.children:
|
||||
well.tracker.set_liquids([(None, 200)])
|
||||
|
||||
# Run simulated protocol
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(plate["A1:H1"], plate["A2:H2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Check results
|
||||
print(f"A1 volume: {plate['A1'].tracker.get_volume()} µL") # 100 µL
|
||||
print(f"A2 volume: {plate['A2'].tracker.get_volume()} µL") # 100 µL
|
||||
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
## Switching Backends
|
||||
|
||||
### Backend-Agnostic Protocols
|
||||
|
||||
Write protocols that work with any backend:
|
||||
|
||||
```python
|
||||
def get_backend(robot_type: str):
|
||||
"""Factory function to create appropriate backend"""
|
||||
if robot_type == "star":
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
return STAR()
|
||||
elif robot_type == "opentrons":
|
||||
from pylabrobot.liquid_handling.backends import OpentronsBackend
|
||||
return OpentronsBackend(host="192.168.1.100")
|
||||
elif robot_type == "simulation":
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
return ChatterboxBackend()
|
||||
else:
|
||||
raise ValueError(f"Unknown robot type: {robot_type}")
|
||||
|
||||
def get_deck(robot_type: str):
|
||||
"""Factory function to create appropriate deck"""
|
||||
if robot_type == "star":
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
return STARLetDeck()
|
||||
elif robot_type == "opentrons":
|
||||
from pylabrobot.resources import OTDeck
|
||||
return OTDeck()
|
||||
elif robot_type == "simulation":
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
return STARLetDeck()
|
||||
else:
|
||||
raise ValueError(f"Unknown robot type: {robot_type}")
|
||||
|
||||
# Use in protocol
|
||||
robot_type = "simulation" # Change to "star" or "opentrons" as needed
|
||||
backend = get_backend(robot_type)
|
||||
deck = get_deck(robot_type)
|
||||
|
||||
lh = LiquidHandler(backend=backend, deck=deck)
|
||||
await lh.setup()
|
||||
|
||||
# Protocol code works with any backend
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Development Workflow
|
||||
|
||||
1. **Develop**: Write protocol using ChatterboxBackend
|
||||
2. **Test**: Run with visualizer to validate logic
|
||||
3. **Verify**: Test on simulation with real deck layout
|
||||
4. **Deploy**: Switch to hardware backend (STAR, Opentrons)
|
||||
|
||||
```python
|
||||
# Development
|
||||
lh = LiquidHandler(backend=ChatterboxBackend(), deck=STARLetDeck())
|
||||
|
||||
# ... develop protocol ...
|
||||
|
||||
# Production (just change backend)
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
```
|
||||
|
||||
## Backend Configuration
|
||||
|
||||
### Custom Backend Parameters
|
||||
|
||||
Some backends accept configuration parameters:
|
||||
|
||||
```python
|
||||
# Opentrons with custom parameters
|
||||
backend = OpentronsBackend(
|
||||
host="192.168.1.100",
|
||||
port=31950 # Default Opentrons API port
|
||||
)
|
||||
|
||||
# ChatterboxBackend with custom channels
|
||||
backend = ChatterboxBackend(
|
||||
num_channels=8 # 8-channel simulation
|
||||
)
|
||||
```
|
||||
|
||||
### Connection Troubleshooting
|
||||
|
||||
**Hamilton STAR:**
|
||||
- Ensure USB cable is connected
|
||||
- Check that no other software is using the robot
|
||||
- Verify firmware is up to date
|
||||
- On macOS/Linux, may need USB permissions
|
||||
|
||||
**Opentrons OT-2:**
|
||||
- Verify robot IP address is correct
|
||||
- Check network connectivity (ping robot)
|
||||
- Ensure robot is powered on
|
||||
- Confirm Opentrons app is not blocking API access
|
||||
|
||||
**General:**
|
||||
- Use `await lh.setup()` to test connection
|
||||
- Check error messages for specific issues
|
||||
- Ensure proper permissions for device access
|
||||
|
||||
## Backend-Specific Features
|
||||
|
||||
### Hamilton STAR Specific
|
||||
|
||||
```python
|
||||
# Access backend directly for hardware-specific features
|
||||
star_backend = lh.backend
|
||||
|
||||
# Hamilton-specific commands (if needed)
|
||||
# Most operations should go through LiquidHandler interface
|
||||
```
|
||||
|
||||
### Opentrons Specific
|
||||
|
||||
```python
|
||||
# Opentrons-specific configuration
|
||||
ot_backend = lh.backend
|
||||
|
||||
# Access OT-2 API directly if needed (advanced)
|
||||
# Most operations should go through LiquidHandler interface
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Abstract Hardware**: Write backend-agnostic protocols when possible
|
||||
2. **Test in Simulation**: Always test with ChatterboxBackend first
|
||||
3. **Factory Pattern**: Use factory functions to create backends
|
||||
4. **Error Handling**: Handle connection errors gracefully
|
||||
5. **Documentation**: Document which backends your protocol supports
|
||||
6. **Configuration**: Use config files for backend parameters
|
||||
7. **Version Control**: Track backend versions and compatibility
|
||||
8. **Cleanup**: Always call `await lh.stop()` to release hardware
|
||||
9. **Single Connection**: Only one program should connect to hardware at a time
|
||||
10. **Platform Testing**: Test on target platform before deployment
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Multi-Backend Support
|
||||
|
||||
```python
|
||||
import asyncio
|
||||
from typing import Literal
|
||||
|
||||
async def run_protocol(
|
||||
robot_type: Literal["star", "opentrons", "simulation"],
|
||||
visualize: bool = False
|
||||
):
|
||||
"""Run protocol on specified backend"""
|
||||
|
||||
# Create backend
|
||||
if robot_type == "star":
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
backend = STAR()
|
||||
deck = STARLetDeck()
|
||||
elif robot_type == "opentrons":
|
||||
from pylabrobot.liquid_handling.backends import OpentronsBackend
|
||||
backend = OpentronsBackend(host="192.168.1.100")
|
||||
deck = OTDeck()
|
||||
elif robot_type == "simulation":
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
backend = ChatterboxBackend()
|
||||
deck = STARLetDeck()
|
||||
|
||||
# Initialize
|
||||
lh = LiquidHandler(backend=backend, deck=deck)
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Load deck layout (backend-agnostic)
|
||||
# lh.deck = Deck.load_from_json_file(f"{robot_type}_layout.json")
|
||||
|
||||
# Execute protocol (backend-agnostic)
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
print("Protocol completed successfully!")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
|
||||
# Run on different backends
|
||||
await run_protocol("simulation") # Test in simulation
|
||||
await run_protocol("star") # Run on Hamilton STAR
|
||||
await run_protocol("opentrons") # Run on Opentrons OT-2
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Backend Documentation: https://docs.pylabrobot.org/user_guide/backends.html
|
||||
- Supported Machines: https://docs.pylabrobot.org/user_guide/machines.html
|
||||
- API Reference: https://docs.pylabrobot.org/api/pylabrobot.liquid_handling.backends.html
|
||||
- GitHub Examples: https://github.com/PyLabRobot/pylabrobot/tree/main/examples
|
||||
403
skills/pylabrobot/references/liquid-handling.md
Normal file
403
skills/pylabrobot/references/liquid-handling.md
Normal file
@@ -0,0 +1,403 @@
|
||||
# Liquid Handling with PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
The liquid handling module (`pylabrobot.liquid_handling`) provides a unified interface for controlling liquid handling robots. The `LiquidHandler` class serves as the main interface for all pipetting operations, working across different hardware platforms through backend abstraction.
|
||||
|
||||
## Basic Setup
|
||||
|
||||
### Initializing a Liquid Handler
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
|
||||
# Create liquid handler with STAR backend
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# When done
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
### Switching Between Backends
|
||||
|
||||
Change robots by swapping the backend without rewriting protocols:
|
||||
|
||||
```python
|
||||
# Hamilton STAR
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
|
||||
# Opentrons OT-2
|
||||
from pylabrobot.liquid_handling.backends import OpentronsBackend
|
||||
lh = LiquidHandler(backend=OpentronsBackend(host="192.168.1.100"), deck=OTDeck())
|
||||
|
||||
# Simulation (no hardware required)
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
lh = LiquidHandler(backend=ChatterboxBackend(), deck=STARLetDeck())
|
||||
```
|
||||
|
||||
## Core Operations
|
||||
|
||||
### Tip Management
|
||||
|
||||
Picking up and dropping tips is fundamental to liquid handling operations:
|
||||
|
||||
```python
|
||||
# Pick up tips from specific positions
|
||||
await lh.pick_up_tips(tip_rack["A1"]) # Single tip
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"]) # Row of 8 tips
|
||||
await lh.pick_up_tips(tip_rack["A1:A12"]) # Column of 12 tips
|
||||
|
||||
# Drop tips
|
||||
await lh.drop_tips() # Drop at current location
|
||||
await lh.drop_tips(waste) # Drop at specific location
|
||||
|
||||
# Return tips to original rack
|
||||
await lh.return_tips()
|
||||
```
|
||||
|
||||
**Tip Tracking**: Enable automatic tip tracking to monitor tip usage:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import set_tip_tracking
|
||||
set_tip_tracking(True) # Enable globally
|
||||
```
|
||||
|
||||
### Aspirating Liquids
|
||||
|
||||
Draw liquid from wells or containers:
|
||||
|
||||
```python
|
||||
# Basic aspiration
|
||||
await lh.aspirate(plate["A1"], vols=100) # 100 µL from A1
|
||||
|
||||
# Multiple wells with same volume
|
||||
await lh.aspirate(plate["A1:H1"], vols=100) # 100 µL from each well
|
||||
|
||||
# Multiple wells with different volumes
|
||||
await lh.aspirate(
|
||||
plate["A1:A3"],
|
||||
vols=[100, 150, 200] # Different volumes
|
||||
)
|
||||
|
||||
# Advanced parameters
|
||||
await lh.aspirate(
|
||||
plate["A1"],
|
||||
vols=100,
|
||||
flow_rate=50, # µL/s
|
||||
liquid_height=5, # mm from bottom
|
||||
blow_out_air_volume=10 # µL air
|
||||
)
|
||||
```
|
||||
|
||||
### Dispensing Liquids
|
||||
|
||||
Dispense liquid into wells or containers:
|
||||
|
||||
```python
|
||||
# Basic dispensing
|
||||
await lh.dispense(plate["A2"], vols=100) # 100 µL to A2
|
||||
|
||||
# Multiple wells
|
||||
await lh.dispense(plate["A1:H1"], vols=100) # 100 µL to each
|
||||
|
||||
# Different volumes
|
||||
await lh.dispense(
|
||||
plate["A1:A3"],
|
||||
vols=[100, 150, 200]
|
||||
)
|
||||
|
||||
# Advanced parameters
|
||||
await lh.dispense(
|
||||
plate["A2"],
|
||||
vols=100,
|
||||
flow_rate=50, # µL/s
|
||||
liquid_height=2, # mm from bottom
|
||||
blow_out_air_volume=10 # µL air
|
||||
)
|
||||
```
|
||||
|
||||
### Transferring Liquids
|
||||
|
||||
Transfer combines aspirate and dispense in a single operation:
|
||||
|
||||
```python
|
||||
# Basic transfer
|
||||
await lh.transfer(
|
||||
source=source_plate["A1"],
|
||||
dest=dest_plate["A1"],
|
||||
vols=100
|
||||
)
|
||||
|
||||
# Multiple transfers (same tips)
|
||||
await lh.transfer(
|
||||
source=source_plate["A1:H1"],
|
||||
dest=dest_plate["A1:H1"],
|
||||
vols=100
|
||||
)
|
||||
|
||||
# Different volumes per well
|
||||
await lh.transfer(
|
||||
source=source_plate["A1:A3"],
|
||||
dest=dest_plate["B1:B3"],
|
||||
vols=[50, 100, 150]
|
||||
)
|
||||
|
||||
# With tip handling
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
source=source_plate["A1:H12"],
|
||||
dest=dest_plate["A1:H12"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
## Advanced Techniques
|
||||
|
||||
### Serial Dilutions
|
||||
|
||||
Create serial dilutions across plate rows or columns:
|
||||
|
||||
```python
|
||||
# 2-fold serial dilution
|
||||
source_vols = [100, 50, 50, 50, 50, 50, 50, 50]
|
||||
dest_vols = [0, 50, 50, 50, 50, 50, 50, 50]
|
||||
|
||||
# Add diluent first
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(
|
||||
source=buffer["A1"],
|
||||
dest=plate["A2:A8"],
|
||||
vols=50
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Perform serial dilution
|
||||
await lh.pick_up_tips(tip_rack["A2"])
|
||||
for i in range(7):
|
||||
await lh.aspirate(plate[f"A{i+1}"], vols=50)
|
||||
await lh.dispense(plate[f"A{i+2}"], vols=50)
|
||||
# Mix
|
||||
await lh.aspirate(plate[f"A{i+2}"], vols=50)
|
||||
await lh.dispense(plate[f"A{i+2}"], vols=50)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Plate Replication
|
||||
|
||||
Copy an entire plate layout to another plate:
|
||||
|
||||
```python
|
||||
# Setup tips
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
|
||||
# Replicate 96-well plate (12 columns)
|
||||
for col in range(1, 13):
|
||||
await lh.transfer(
|
||||
source=source_plate[f"A{col}:H{col}"],
|
||||
dest=dest_plate[f"A{col}:H{col}"],
|
||||
vols=100
|
||||
)
|
||||
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Multi-Channel Pipetting
|
||||
|
||||
Use multiple channels simultaneously for parallel operations:
|
||||
|
||||
```python
|
||||
# 8-channel transfer (entire row)
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
source=source_plate["A1:H1"],
|
||||
dest=dest_plate["A1:H1"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Process entire plate with 8-channel
|
||||
for col in range(1, 13):
|
||||
await lh.pick_up_tips(tip_rack[f"A{col}:H{col}"])
|
||||
await lh.transfer(
|
||||
source=source_plate[f"A{col}:H{col}"],
|
||||
dest=dest_plate[f"A{col}:H{col}"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Mixing Liquids
|
||||
|
||||
Mix liquids by repeatedly aspirating and dispensing:
|
||||
|
||||
```python
|
||||
# Mix by aspiration/dispensing
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
|
||||
# Mix 5 times
|
||||
for _ in range(5):
|
||||
await lh.aspirate(plate["A1"], vols=80)
|
||||
await lh.dispense(plate["A1"], vols=80)
|
||||
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
## Volume Tracking
|
||||
|
||||
Track liquid volumes in wells automatically:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import set_volume_tracking
|
||||
|
||||
# Enable volume tracking globally
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Set initial volumes
|
||||
plate["A1"].tracker.set_liquids([(None, 200)]) # 200 µL
|
||||
|
||||
# After aspirating 100 µL
|
||||
await lh.aspirate(plate["A1"], vols=100)
|
||||
print(plate["A1"].tracker.get_volume()) # 100 µL
|
||||
|
||||
# Check remaining volume
|
||||
remaining = plate["A1"].tracker.get_volume()
|
||||
```
|
||||
|
||||
## Liquid Classes
|
||||
|
||||
Define liquid properties for optimal pipetting:
|
||||
|
||||
```python
|
||||
# Liquid classes control aspiration/dispense parameters
|
||||
from pylabrobot.liquid_handling import LiquidClass
|
||||
|
||||
# Create custom liquid class
|
||||
water = LiquidClass(
|
||||
name="Water",
|
||||
aspiration_flow_rate=100,
|
||||
dispense_flow_rate=150,
|
||||
aspiration_mix_flow_rate=100,
|
||||
dispense_mix_flow_rate=100,
|
||||
air_transport_retract_dist=10
|
||||
)
|
||||
|
||||
# Use with operations
|
||||
await lh.aspirate(
|
||||
plate["A1"],
|
||||
vols=100,
|
||||
liquid_class=water
|
||||
)
|
||||
```
|
||||
|
||||
## Error Handling
|
||||
|
||||
Handle errors in liquid handling operations:
|
||||
|
||||
```python
|
||||
try:
|
||||
await lh.setup()
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(source["A1"], dest["A1"], vols=100)
|
||||
await lh.drop_tips()
|
||||
except Exception as e:
|
||||
print(f"Error during liquid handling: {e}")
|
||||
# Attempt to drop tips if holding them
|
||||
try:
|
||||
await lh.drop_tips()
|
||||
except:
|
||||
pass
|
||||
finally:
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Setup and Stop**: Call `await lh.setup()` before operations and `await lh.stop()` when done
|
||||
2. **Enable Tracking**: Use tip tracking and volume tracking for accurate state management
|
||||
3. **Tip Management**: Always pick up tips before aspirating and drop them when done
|
||||
4. **Flow Rates**: Adjust flow rates based on liquid viscosity and vessel type
|
||||
5. **Liquid Height**: Set appropriate aspiration/dispense heights to avoid splashing
|
||||
6. **Error Handling**: Use try/finally blocks to ensure proper cleanup
|
||||
7. **Test in Simulation**: Use ChatterboxBackend to test protocols before running on hardware
|
||||
8. **Volume Limits**: Respect tip volume limits and well capacities
|
||||
9. **Mixing**: Mix after dispensing viscous liquids or when accuracy is critical
|
||||
10. **Documentation**: Document liquid classes and custom parameters for reproducibility
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Complete Liquid Handling Protocol
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck, TIP_CAR_480_A00, Cos_96_DW_1mL
|
||||
from pylabrobot.resources import set_tip_tracking, set_volume_tracking
|
||||
|
||||
# Enable tracking
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Initialize
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
source = Cos_96_DW_1mL(name="source")
|
||||
dest = Cos_96_DW_1mL(name="dest")
|
||||
|
||||
# Assign to deck
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(source, rails=10)
|
||||
lh.deck.assign_child_resource(dest, rails=15)
|
||||
|
||||
# Set initial volumes
|
||||
for well in source.children:
|
||||
well.tracker.set_liquids([(None, 200)])
|
||||
|
||||
# Execute protocol
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
source=source["A1:H12"],
|
||||
dest=dest["A1:H12"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
## Hardware-Specific Notes
|
||||
|
||||
### Hamilton STAR
|
||||
|
||||
- Supports full liquid handling capabilities
|
||||
- Uses USB connection for communication
|
||||
- Firmware commands executed directly
|
||||
- Supports CO-RE (Compressed O-Ring Expansion) tips
|
||||
|
||||
### Opentrons OT-2
|
||||
|
||||
- Requires IP address for network connection
|
||||
- Uses HTTP API for communication
|
||||
- Limited to 8-channel and single-channel pipettes
|
||||
- Simpler deck layout compared to STAR
|
||||
|
||||
### Tecan EVO
|
||||
|
||||
- Work-in-progress support
|
||||
- Similar capabilities to Hamilton STAR
|
||||
- Check current compatibility status in documentation
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Official Liquid Handling Guide: https://docs.pylabrobot.org/user_guide/basic.html
|
||||
- API Reference: https://docs.pylabrobot.org/api/pylabrobot.liquid_handling.html
|
||||
- Example Protocols: https://github.com/PyLabRobot/pylabrobot/tree/main/examples
|
||||
620
skills/pylabrobot/references/material-handling.md
Normal file
620
skills/pylabrobot/references/material-handling.md
Normal file
@@ -0,0 +1,620 @@
|
||||
# Material Handling Equipment in PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
PyLabRobot integrates with material handling equipment including heater shakers, incubators, centrifuges, and pumps. These devices enable environmental control, sample preparation, and automated workflows beyond basic liquid handling.
|
||||
|
||||
## Heater Shakers
|
||||
|
||||
### Hamilton HeaterShaker
|
||||
|
||||
The Hamilton HeaterShaker provides temperature control and orbital shaking for microplates.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.heating_shaking import HeaterShaker
|
||||
from pylabrobot.heating_shaking.hamilton import HamiltonHeaterShakerBackend
|
||||
|
||||
# Create heater shaker
|
||||
hs = HeaterShaker(
|
||||
name="heater_shaker_1",
|
||||
backend=HamiltonHeaterShakerBackend(),
|
||||
size_x=156.0,
|
||||
size_y= 156.0,
|
||||
size_z=18.0
|
||||
)
|
||||
|
||||
await hs.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
**Temperature Control:**
|
||||
|
||||
```python
|
||||
# Set temperature (Celsius)
|
||||
await hs.set_temperature(37)
|
||||
|
||||
# Get current temperature
|
||||
temp = await hs.get_temperature()
|
||||
print(f"Current temperature: {temp}°C")
|
||||
|
||||
# Turn off heating
|
||||
await hs.set_temperature(None)
|
||||
```
|
||||
|
||||
**Shaking Control:**
|
||||
|
||||
```python
|
||||
# Start shaking (RPM)
|
||||
await hs.set_shake_rate(300) # 300 RPM
|
||||
|
||||
# Stop shaking
|
||||
await hs.set_shake_rate(0)
|
||||
```
|
||||
|
||||
**Plate Operations:**
|
||||
|
||||
```python
|
||||
# Lock plate in position
|
||||
await hs.lock_plate()
|
||||
|
||||
# Unlock plate
|
||||
await hs.unlock_plate()
|
||||
```
|
||||
|
||||
#### Integration with Liquid Handler
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
|
||||
# Initialize devices
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
hs = HeaterShaker(name="hs", backend=HamiltonHeaterShakerBackend())
|
||||
|
||||
await lh.setup()
|
||||
await hs.setup()
|
||||
|
||||
try:
|
||||
# Assign heater shaker to deck
|
||||
lh.deck.assign_child_resource(hs, rails=8)
|
||||
|
||||
# Prepare samples
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
|
||||
# Place plate on heater shaker
|
||||
hs.assign_child_resource(plate, location=(0, 0, 0))
|
||||
|
||||
# Transfer reagents to plate on heater shaker
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(reagent["A1:H1"], plate["A1:H1"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Lock plate and start incubation
|
||||
await hs.lock_plate()
|
||||
await hs.set_temperature(37)
|
||||
await hs.set_shake_rate(300)
|
||||
|
||||
# Incubate
|
||||
import asyncio
|
||||
await asyncio.sleep(600) # 10 minutes
|
||||
|
||||
# Stop shaking and heating
|
||||
await hs.set_shake_rate(0)
|
||||
await hs.set_temperature(None)
|
||||
await hs.unlock_plate()
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await hs.stop()
|
||||
```
|
||||
|
||||
#### Multiple Heater Shakers
|
||||
|
||||
The HamiltonHeaterShakerBackend handles multiple units:
|
||||
|
||||
```python
|
||||
# Backend automatically manages multiple heater shakers
|
||||
hs1 = HeaterShaker(name="hs1", backend=HamiltonHeaterShakerBackend())
|
||||
hs2 = HeaterShaker(name="hs2", backend=HamiltonHeaterShakerBackend())
|
||||
|
||||
await hs1.setup()
|
||||
await hs2.setup()
|
||||
|
||||
# Assign to different deck positions
|
||||
lh.deck.assign_child_resource(hs1, rails=8)
|
||||
lh.deck.assign_child_resource(hs2, rails=12)
|
||||
|
||||
# Control independently
|
||||
await hs1.set_temperature(37)
|
||||
await hs2.set_temperature(42)
|
||||
```
|
||||
|
||||
### Inheco ThermoShake
|
||||
|
||||
The Inheco ThermoShake provides temperature control and shaking.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.heating_shaking import HeaterShaker
|
||||
from pylabrobot.heating_shaking.inheco import InhecoThermoShakeBackend
|
||||
|
||||
hs = HeaterShaker(
|
||||
name="thermoshake",
|
||||
backend=InhecoThermoShakeBackend(),
|
||||
size_x=156.0,
|
||||
size_y=156.0,
|
||||
size_z=18.0
|
||||
)
|
||||
|
||||
await hs.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
Similar to Hamilton HeaterShaker:
|
||||
|
||||
```python
|
||||
# Temperature control
|
||||
await hs.set_temperature(37)
|
||||
temp = await hs.get_temperature()
|
||||
|
||||
# Shaking control
|
||||
await hs.set_shake_rate(300)
|
||||
|
||||
# Plate locking
|
||||
await hs.lock_plate()
|
||||
await hs.unlock_plate()
|
||||
```
|
||||
|
||||
## Incubators
|
||||
|
||||
### Inheco Incubators
|
||||
|
||||
PyLabRobot supports various Inheco incubator models for temperature-controlled plate storage.
|
||||
|
||||
#### Supported Models
|
||||
|
||||
- Inheco Single Plate Incubator
|
||||
- Inheco Multi-Plate Incubators
|
||||
- Other Inheco temperature controllers
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.temperature_control import TemperatureController
|
||||
from pylabrobot.temperature_control.inheco import InhecoBackend
|
||||
|
||||
# Create incubator
|
||||
incubator = TemperatureController(
|
||||
name="incubator",
|
||||
backend=InhecoBackend(),
|
||||
size_x=156.0,
|
||||
size_y=156.0,
|
||||
size_z=50.0
|
||||
)
|
||||
|
||||
await incubator.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
```python
|
||||
# Set temperature
|
||||
await incubator.set_temperature(37)
|
||||
|
||||
# Get temperature
|
||||
temp = await incubator.get_temperature()
|
||||
print(f"Incubator temperature: {temp}°C")
|
||||
|
||||
# Turn off
|
||||
await incubator.set_temperature(None)
|
||||
```
|
||||
|
||||
### Thermo Fisher Cytomat Incubators
|
||||
|
||||
Cytomat incubators provide automated plate storage with temperature and CO2 control.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.incubation import Incubator
|
||||
from pylabrobot.incubation.cytomat_backend import CytomatBackend
|
||||
|
||||
incubator = Incubator(
|
||||
name="cytomat",
|
||||
backend=CytomatBackend()
|
||||
)
|
||||
|
||||
await incubator.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
```python
|
||||
# Store plate
|
||||
await incubator.store_plate(plate_id="plate_001", position=1)
|
||||
|
||||
# Retrieve plate
|
||||
await incubator.retrieve_plate(position=1)
|
||||
|
||||
# Set environmental conditions
|
||||
await incubator.set_temperature(37)
|
||||
await incubator.set_co2(5.0) # 5% CO2
|
||||
```
|
||||
|
||||
## Centrifuges
|
||||
|
||||
### Agilent VSpin
|
||||
|
||||
The Agilent VSpin is a vacuum-assisted centrifuge for plate processing.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.centrifuge import Centrifuge
|
||||
from pylabrobot.centrifuge.vspin import VSpinBackend
|
||||
|
||||
centrifuge = Centrifuge(
|
||||
name="vspin",
|
||||
backend=VSpinBackend()
|
||||
)
|
||||
|
||||
await centrifuge.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
**Door Control:**
|
||||
|
||||
```python
|
||||
# Open door
|
||||
await centrifuge.open_door()
|
||||
|
||||
# Close door
|
||||
await centrifuge.close_door()
|
||||
|
||||
# Lock door
|
||||
await centrifuge.lock_door()
|
||||
|
||||
# Unlock door
|
||||
await centrifuge.unlock_door()
|
||||
```
|
||||
|
||||
**Bucket Positioning:**
|
||||
|
||||
```python
|
||||
# Move bucket to loading position
|
||||
await centrifuge.move_bucket_to_loading()
|
||||
|
||||
# Move bucket to home position
|
||||
await centrifuge.move_bucket_to_home()
|
||||
```
|
||||
|
||||
**Spinning:**
|
||||
|
||||
```python
|
||||
# Run centrifuge
|
||||
await centrifuge.spin(
|
||||
speed=2000, # RPM
|
||||
duration=300 # seconds
|
||||
)
|
||||
|
||||
# Stop spinning
|
||||
await centrifuge.stop_spin()
|
||||
```
|
||||
|
||||
#### Integration Example
|
||||
|
||||
```python
|
||||
async def centrifuge_workflow():
|
||||
"""Complete centrifugation workflow"""
|
||||
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
centrifuge = Centrifuge(name="vspin", backend=VSpinBackend())
|
||||
|
||||
await lh.setup()
|
||||
await centrifuge.setup()
|
||||
|
||||
try:
|
||||
# Prepare samples
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(samples["A1:H12"], plate["A1:H12"], vols=200)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Load into centrifuge
|
||||
print("Move plate to centrifuge")
|
||||
await centrifuge.open_door()
|
||||
await centrifuge.move_bucket_to_loading()
|
||||
input("Press Enter when plate is loaded...")
|
||||
|
||||
await centrifuge.move_bucket_to_home()
|
||||
await centrifuge.close_door()
|
||||
await centrifuge.lock_door()
|
||||
|
||||
# Centrifuge
|
||||
await centrifuge.spin(speed=2000, duration=300)
|
||||
|
||||
# Unload
|
||||
await centrifuge.unlock_door()
|
||||
await centrifuge.open_door()
|
||||
await centrifuge.move_bucket_to_loading()
|
||||
input("Press Enter when plate is removed...")
|
||||
|
||||
await centrifuge.move_bucket_to_home()
|
||||
await centrifuge.close_door()
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await centrifuge.stop()
|
||||
```
|
||||
|
||||
## Pumps
|
||||
|
||||
### Cole Parmer Masterflex
|
||||
|
||||
PyLabRobot supports Cole Parmer Masterflex peristaltic pumps for fluid transfer.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.pumps import Pump
|
||||
from pylabrobot.pumps.cole_parmer import ColeParmerMasterflexBackend
|
||||
|
||||
pump = Pump(
|
||||
name="masterflex",
|
||||
backend=ColeParmerMasterflexBackend()
|
||||
)
|
||||
|
||||
await pump.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
**Running Pump:**
|
||||
|
||||
```python
|
||||
# Run for duration
|
||||
await pump.run_for_duration(
|
||||
duration=10, # seconds
|
||||
speed=50 # % of maximum
|
||||
)
|
||||
|
||||
# Run continuously
|
||||
await pump.start(speed=50)
|
||||
|
||||
# Stop pump
|
||||
await pump.stop()
|
||||
```
|
||||
|
||||
**Volume-Based Pumping:**
|
||||
|
||||
```python
|
||||
# Pump specific volume (requires calibration)
|
||||
await pump.pump_volume(
|
||||
volume=10, # mL
|
||||
speed=50 # % of maximum
|
||||
)
|
||||
```
|
||||
|
||||
#### Calibration
|
||||
|
||||
```python
|
||||
# Calibrate pump for volume accuracy
|
||||
# (requires known volume measurement)
|
||||
await pump.run_for_duration(duration=60, speed=50)
|
||||
actual_volume = 25.3 # mL measured
|
||||
|
||||
pump.calibrate(duration=60, speed=50, volume=actual_volume)
|
||||
```
|
||||
|
||||
### Agrowtek Pump Array
|
||||
|
||||
Support for Agrowtek pump arrays for multiple simultaneous fluid transfers.
|
||||
|
||||
#### Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.pumps import PumpArray
|
||||
from pylabrobot.pumps.agrowtek import AgrowtekBackend
|
||||
|
||||
pump_array = PumpArray(
|
||||
name="agrowtek",
|
||||
backend=AgrowtekBackend(),
|
||||
num_pumps=8
|
||||
)
|
||||
|
||||
await pump_array.setup()
|
||||
```
|
||||
|
||||
#### Operations
|
||||
|
||||
```python
|
||||
# Run specific pump
|
||||
await pump_array.run_pump(
|
||||
pump_number=1,
|
||||
duration=10,
|
||||
speed=50
|
||||
)
|
||||
|
||||
# Run multiple pumps simultaneously
|
||||
await pump_array.run_pumps(
|
||||
pump_numbers=[1, 2, 3],
|
||||
duration=10,
|
||||
speed=50
|
||||
)
|
||||
```
|
||||
|
||||
## Multi-Device Protocols
|
||||
|
||||
### Complex Workflow Example
|
||||
|
||||
```python
|
||||
async def complex_workflow():
|
||||
"""Multi-device automated workflow"""
|
||||
|
||||
# Initialize all devices
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
hs = HeaterShaker(name="hs", backend=HamiltonHeaterShakerBackend())
|
||||
centrifuge = Centrifuge(name="vspin", backend=VSpinBackend())
|
||||
pump = Pump(name="pump", backend=ColeParmerMasterflexBackend())
|
||||
|
||||
await lh.setup()
|
||||
await hs.setup()
|
||||
await centrifuge.setup()
|
||||
await pump.setup()
|
||||
|
||||
try:
|
||||
# 1. Sample preparation
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(samples["A1:H12"], plate["A1:H12"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# 2. Add reagent via pump
|
||||
await pump.pump_volume(volume=50, speed=50)
|
||||
|
||||
# 3. Mix on heater shaker
|
||||
await hs.lock_plate()
|
||||
await hs.set_temperature(37)
|
||||
await hs.set_shake_rate(300)
|
||||
await asyncio.sleep(600) # 10 min incubation
|
||||
await hs.set_shake_rate(0)
|
||||
await hs.set_temperature(None)
|
||||
await hs.unlock_plate()
|
||||
|
||||
# 4. Centrifuge
|
||||
await centrifuge.open_door()
|
||||
# (load plate)
|
||||
await centrifuge.close_door()
|
||||
await centrifuge.spin(speed=2000, duration=180)
|
||||
await centrifuge.open_door()
|
||||
# (unload plate)
|
||||
|
||||
# 5. Transfer supernatant
|
||||
await lh.pick_up_tips(tip_rack["A2:H2"])
|
||||
await lh.transfer(
|
||||
plate["A1:H12"],
|
||||
output_plate["A1:H12"],
|
||||
vols=80
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await hs.stop()
|
||||
await centrifuge.stop()
|
||||
await pump.stop()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Device Initialization**: Setup all devices at protocol start
|
||||
2. **Sequential Operations**: Material handling often requires sequential steps
|
||||
3. **Safety**: Always unlock/open doors before manual plate handling
|
||||
4. **Temperature Equilibration**: Allow time for devices to reach temperature
|
||||
5. **Error Handling**: Handle device errors gracefully with try/finally
|
||||
6. **State Verification**: Check device state before operations
|
||||
7. **Timing**: Account for device-specific delays (heating, centrifugation)
|
||||
8. **Maintenance**: Follow manufacturer maintenance schedules
|
||||
9. **Calibration**: Regularly calibrate pumps and temperature controllers
|
||||
10. **Documentation**: Record all device settings and parameters
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Temperature-Controlled Incubation
|
||||
|
||||
```python
|
||||
async def incubate_with_shaking(
|
||||
plate,
|
||||
temperature: float,
|
||||
shake_rate: int,
|
||||
duration: int
|
||||
):
|
||||
"""Incubate plate with temperature and shaking"""
|
||||
|
||||
hs = HeaterShaker(name="hs", backend=HamiltonHeaterShakerBackend())
|
||||
await hs.setup()
|
||||
|
||||
try:
|
||||
# Assign plate to heater shaker
|
||||
hs.assign_child_resource(plate, location=(0, 0, 0))
|
||||
|
||||
# Start incubation
|
||||
await hs.lock_plate()
|
||||
await hs.set_temperature(temperature)
|
||||
await hs.set_shake_rate(shake_rate)
|
||||
|
||||
# Wait
|
||||
await asyncio.sleep(duration)
|
||||
|
||||
# Stop
|
||||
await hs.set_shake_rate(0)
|
||||
await hs.set_temperature(None)
|
||||
await hs.unlock_plate()
|
||||
|
||||
finally:
|
||||
await hs.stop()
|
||||
|
||||
# Use in protocol
|
||||
await incubate_with_shaking(
|
||||
plate=assay_plate,
|
||||
temperature=37,
|
||||
shake_rate=300,
|
||||
duration=600 # 10 minutes
|
||||
)
|
||||
```
|
||||
|
||||
### Automated Plate Processing
|
||||
|
||||
```python
|
||||
async def process_plates(plate_list: list):
|
||||
"""Process multiple plates through workflow"""
|
||||
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
hs = HeaterShaker(name="hs", backend=HamiltonHeaterShakerBackend())
|
||||
|
||||
await lh.setup()
|
||||
await hs.setup()
|
||||
|
||||
try:
|
||||
for i, plate in enumerate(plate_list):
|
||||
print(f"Processing plate {i+1}/{len(plate_list)}")
|
||||
|
||||
# Transfer samples
|
||||
await lh.pick_up_tips(tip_rack[f"A{i+1}:H{i+1}"])
|
||||
await lh.transfer(
|
||||
source[f"A{i+1}:H{i+1}"],
|
||||
plate["A1:H1"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Incubate
|
||||
hs.assign_child_resource(plate, location=(0, 0, 0))
|
||||
await hs.lock_plate()
|
||||
await hs.set_temperature(37)
|
||||
await hs.set_shake_rate(300)
|
||||
await asyncio.sleep(300) # 5 min
|
||||
await hs.set_shake_rate(0)
|
||||
await hs.set_temperature(None)
|
||||
await hs.unlock_plate()
|
||||
hs.unassign_child_resource(plate)
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await hs.stop()
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Material Handling Documentation: https://docs.pylabrobot.org/user_guide/01_material-handling/
|
||||
- Heater Shakers: https://docs.pylabrobot.org/user_guide/01_material-handling/heating_shaking/
|
||||
- API Reference: https://docs.pylabrobot.org/api/
|
||||
- Supported Equipment: https://docs.pylabrobot.org/user_guide/machines.html
|
||||
489
skills/pylabrobot/references/resources.md
Normal file
489
skills/pylabrobot/references/resources.md
Normal file
@@ -0,0 +1,489 @@
|
||||
# Resource Management in PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
Resources in PyLabRobot represent laboratory equipment, labware, or components used in protocols. The resource system provides a hierarchical structure for managing plates, tip racks, troughs, tubes, carriers, and other labware with precise spatial positioning and state tracking.
|
||||
|
||||
## Resource Basics
|
||||
|
||||
### What is a Resource?
|
||||
|
||||
A resource represents:
|
||||
- A piece of labware (plate, tip rack, trough, tube)
|
||||
- Equipment (liquid handler, plate reader)
|
||||
- A part of labware (well, tip)
|
||||
- A container of labware (deck, carrier)
|
||||
|
||||
All resources inherit from the base `Resource` class and form a tree structure (arborescence) with parent-child relationships.
|
||||
|
||||
### Resource Attributes
|
||||
|
||||
Every resource requires:
|
||||
- **name**: Unique identifier for the resource
|
||||
- **size_x, size_y, size_z**: Dimensions in millimeters (cuboid representation)
|
||||
- **location**: Coordinate relative to parent's origin (optional, set when assigned)
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Resource
|
||||
|
||||
# Create a basic resource
|
||||
resource = Resource(
|
||||
name="my_resource",
|
||||
size_x=127.76, # mm
|
||||
size_y=85.48, # mm
|
||||
size_z=14.5 # mm
|
||||
)
|
||||
```
|
||||
|
||||
## Resource Types
|
||||
|
||||
### Plates
|
||||
|
||||
Microplates with wells for holding liquids:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import (
|
||||
Cos_96_DW_1mL, # 96-well plate, 1mL deep well
|
||||
Cos_96_DW_500ul, # 96-well plate, 500µL
|
||||
Plate_384_Sq, # 384-well square plate
|
||||
Cos_96_PCR # 96-well PCR plate
|
||||
)
|
||||
|
||||
# Create plate
|
||||
plate = Cos_96_DW_1mL(name="sample_plate")
|
||||
|
||||
# Access wells
|
||||
well_a1 = plate["A1"] # Single well
|
||||
row_a = plate["A1:H1"] # Entire row (A1-H1)
|
||||
col_1 = plate["A1:A12"] # Entire column (A1-A12)
|
||||
range_wells = plate["A1:C3"] # Range of wells
|
||||
all_wells = plate.children # All wells as list
|
||||
```
|
||||
|
||||
### Tip Racks
|
||||
|
||||
Containers holding pipette tips:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import (
|
||||
TIP_CAR_480_A00, # 96 standard tips
|
||||
HTF_L, # Hamilton tips, filtered
|
||||
TipRack # Generic tip rack
|
||||
)
|
||||
|
||||
# Create tip rack
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
|
||||
# Access tips
|
||||
tip_a1 = tip_rack["A1"] # Single tip position
|
||||
tips_row = tip_rack["A1:H1"] # Row of tips
|
||||
tips_col = tip_rack["A1:A12"] # Column of tips
|
||||
|
||||
# Check tip presence (requires tip tracking enabled)
|
||||
from pylabrobot.resources import set_tip_tracking
|
||||
set_tip_tracking(True)
|
||||
|
||||
has_tip = tip_rack["A1"].tracker.has_tip
|
||||
```
|
||||
|
||||
### Troughs
|
||||
|
||||
Reservoir containers for reagents:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Trough_100ml
|
||||
|
||||
# Create trough
|
||||
trough = Trough_100ml(name="buffer")
|
||||
|
||||
# Access channels
|
||||
channel_1 = trough["channel_1"]
|
||||
all_channels = trough.children
|
||||
```
|
||||
|
||||
### Tubes
|
||||
|
||||
Individual tubes or tube racks:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Tube, TubeRack
|
||||
|
||||
# Create tube rack
|
||||
tube_rack = TubeRack(name="samples")
|
||||
|
||||
# Access tubes
|
||||
tube_a1 = tube_rack["A1"]
|
||||
```
|
||||
|
||||
### Carriers
|
||||
|
||||
Platforms that hold plates, tips, or other labware:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import (
|
||||
PlateCarrier,
|
||||
TipCarrier,
|
||||
MFXCarrier
|
||||
)
|
||||
|
||||
# Carriers provide positions for labware
|
||||
carrier = PlateCarrier(name="plate_carrier")
|
||||
|
||||
# Assign plate to carrier
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
carrier.assign_child_resource(plate, location=(0, 0, 0))
|
||||
```
|
||||
|
||||
## Deck Management
|
||||
|
||||
### Working with Decks
|
||||
|
||||
The deck represents the robot's work surface:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import STARLetDeck, OTDeck
|
||||
|
||||
# Hamilton STARlet deck
|
||||
deck = STARLetDeck()
|
||||
|
||||
# Opentrons OT-2 deck
|
||||
deck = OTDeck()
|
||||
```
|
||||
|
||||
### Assigning Resources to Deck
|
||||
|
||||
Resources are assigned to specific deck positions using rails or coordinates:
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.resources import STARLetDeck, TIP_CAR_480_A00, Cos_96_DW_1mL
|
||||
|
||||
lh = LiquidHandler(backend=backend, deck=STARLetDeck())
|
||||
|
||||
# Assign using rail positions (Hamilton STAR)
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
source_plate = Cos_96_DW_1mL(name="source")
|
||||
dest_plate = Cos_96_DW_1mL(name="dest")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(source_plate, rails=10)
|
||||
lh.deck.assign_child_resource(dest_plate, rails=15)
|
||||
|
||||
# Assign using coordinates (x, y, z in mm)
|
||||
lh.deck.assign_child_resource(
|
||||
resource=tip_rack,
|
||||
location=(100, 200, 0)
|
||||
)
|
||||
```
|
||||
|
||||
### Unassigning Resources
|
||||
|
||||
Remove resources from deck:
|
||||
|
||||
```python
|
||||
# Unassign specific resource
|
||||
lh.deck.unassign_child_resource(tip_rack)
|
||||
|
||||
# Access assigned resources
|
||||
all_resources = lh.deck.children
|
||||
resource_names = [r.name for r in lh.deck.children]
|
||||
```
|
||||
|
||||
## Coordinate System
|
||||
|
||||
PyLabRobot uses a right-handed Cartesian coordinate system:
|
||||
|
||||
- **X-axis**: Left to right (increasing rightward)
|
||||
- **Y-axis**: Front to back (increasing toward back)
|
||||
- **Z-axis**: Down to up (increasing upward)
|
||||
- **Origin**: Bottom-front-left corner of parent
|
||||
|
||||
### Location Calculations
|
||||
|
||||
```python
|
||||
# Get absolute location (relative to deck/root)
|
||||
absolute_loc = plate.get_absolute_location()
|
||||
|
||||
# Get location relative to another resource
|
||||
relative_loc = well.get_location_wrt(deck)
|
||||
|
||||
# Get location relative to parent
|
||||
parent_relative = plate.location
|
||||
```
|
||||
|
||||
## State Management
|
||||
|
||||
### Tracking Liquid Volumes
|
||||
|
||||
Track liquid volumes in wells and containers:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import set_volume_tracking
|
||||
|
||||
# Enable volume tracking globally
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Set liquid in well
|
||||
plate["A1"].tracker.set_liquids([
|
||||
(None, 200) # (liquid_type, volume_in_uL)
|
||||
])
|
||||
|
||||
# Multiple liquids
|
||||
plate["A2"].tracker.set_liquids([
|
||||
("water", 100),
|
||||
("ethanol", 50)
|
||||
])
|
||||
|
||||
# Get current volume
|
||||
volume = plate["A1"].tracker.get_volume() # Returns total volume
|
||||
|
||||
# Get liquids
|
||||
liquids = plate["A1"].tracker.get_liquids() # Returns list of (type, vol) tuples
|
||||
```
|
||||
|
||||
### Tracking Tip Presence
|
||||
|
||||
Track which tips are present in tip racks:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import set_tip_tracking
|
||||
|
||||
# Enable tip tracking globally
|
||||
set_tip_tracking(True)
|
||||
|
||||
# Check if tip is present
|
||||
has_tip = tip_rack["A1"].tracker.has_tip
|
||||
|
||||
# Tips are automatically tracked when using pick_up_tips/drop_tips
|
||||
await lh.pick_up_tips(tip_rack["A1"]) # Marks tip as absent
|
||||
await lh.return_tips() # Marks tip as present
|
||||
```
|
||||
|
||||
## Serialization
|
||||
|
||||
### Saving and Loading Resources
|
||||
|
||||
Save resource definitions and states to JSON:
|
||||
|
||||
```python
|
||||
# Save resource definition
|
||||
plate.save("plate_definition.json")
|
||||
|
||||
# Load resource from JSON
|
||||
from pylabrobot.resources import Plate
|
||||
plate = Plate.load_from_json_file("plate_definition.json")
|
||||
|
||||
# Save deck layout
|
||||
lh.deck.save("deck_layout.json")
|
||||
|
||||
# Load deck layout
|
||||
from pylabrobot.resources import Deck
|
||||
deck = Deck.load_from_json_file("deck_layout.json")
|
||||
```
|
||||
|
||||
### State Serialization
|
||||
|
||||
Save and restore resource states separately from definitions:
|
||||
|
||||
```python
|
||||
# Save state (tip presence, liquid volumes)
|
||||
state = plate.serialize_state()
|
||||
with open("plate_state.json", "w") as f:
|
||||
json.dump(state, f)
|
||||
|
||||
# Load state
|
||||
with open("plate_state.json", "r") as f:
|
||||
state = json.load(f)
|
||||
plate.load_state(state)
|
||||
|
||||
# Save all states in hierarchy
|
||||
all_states = lh.deck.serialize_all_state()
|
||||
|
||||
# Load all states
|
||||
lh.deck.load_all_state(all_states)
|
||||
```
|
||||
|
||||
## Custom Resources
|
||||
|
||||
### Defining Custom Labware
|
||||
|
||||
Create custom labware when built-in resources don't match your equipment:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Plate, Well
|
||||
|
||||
# Define custom plate
|
||||
class CustomPlate(Plate):
|
||||
def __init__(self, name: str):
|
||||
super().__init__(
|
||||
name=name,
|
||||
size_x=127.76,
|
||||
size_y=85.48,
|
||||
size_z=14.5,
|
||||
num_items_x=12, # 12 columns
|
||||
num_items_y=8, # 8 rows
|
||||
dx=9.0, # Well spacing X
|
||||
dy=9.0, # Well spacing Y
|
||||
dz=0.0, # Well spacing Z (usually 0)
|
||||
item_dx=9.0, # Distance between well centers X
|
||||
item_dy=9.0 # Distance between well centers Y
|
||||
)
|
||||
|
||||
# Use custom plate
|
||||
custom_plate = CustomPlate(name="my_custom_plate")
|
||||
```
|
||||
|
||||
### Custom Wells
|
||||
|
||||
Define custom well geometry:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Well
|
||||
|
||||
# Create custom well
|
||||
well = Well(
|
||||
name="custom_well",
|
||||
size_x=8.0,
|
||||
size_y=8.0,
|
||||
size_z=10.5,
|
||||
max_volume=200, # µL
|
||||
bottom_shape="flat" # or "v", "u"
|
||||
)
|
||||
```
|
||||
|
||||
## Resource Discovery
|
||||
|
||||
### Finding Resources
|
||||
|
||||
Navigate the resource hierarchy:
|
||||
|
||||
```python
|
||||
# Get all wells in a plate
|
||||
wells = plate.children
|
||||
|
||||
# Find resource by name
|
||||
resource = lh.deck.get_resource("plate_name")
|
||||
|
||||
# Iterate through resources
|
||||
for resource in lh.deck.children:
|
||||
print(f"{resource.name}: {resource.get_absolute_location()}")
|
||||
|
||||
# Get wells by pattern
|
||||
wells_a = [w for w in plate.children if w.name.startswith("A")]
|
||||
```
|
||||
|
||||
### Resource Metadata
|
||||
|
||||
Access resource information:
|
||||
|
||||
```python
|
||||
# Resource properties
|
||||
print(f"Name: {plate.name}")
|
||||
print(f"Size: {plate.size_x} x {plate.size_y} x {plate.size_z} mm")
|
||||
print(f"Location: {plate.get_absolute_location()}")
|
||||
print(f"Parent: {plate.parent.name if plate.parent else None}")
|
||||
print(f"Children: {len(plate.children)}")
|
||||
|
||||
# Type checking
|
||||
from pylabrobot.resources import Plate, TipRack
|
||||
if isinstance(resource, Plate):
|
||||
print("This is a plate")
|
||||
elif isinstance(resource, TipRack):
|
||||
print("This is a tip rack")
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Unique Names**: Use descriptive, unique names for all resources
|
||||
2. **Enable Tracking**: Turn on tip and volume tracking for accurate state management
|
||||
3. **Coordinate Validation**: Verify resource positions don't overlap on deck
|
||||
4. **State Serialization**: Save deck layouts and states for reproducible protocols
|
||||
5. **Resource Cleanup**: Unassign resources when no longer needed
|
||||
6. **Custom Resources**: Define custom labware when built-in options don't match
|
||||
7. **Documentation**: Document custom resource dimensions and properties
|
||||
8. **Type Checking**: Use isinstance() to verify resource types before operations
|
||||
9. **Hierarchy Navigation**: Use parent/children relationships to navigate resource tree
|
||||
10. **JSON Storage**: Store deck layouts in JSON for version control and sharing
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Complete Deck Setup
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
from pylabrobot.resources import (
|
||||
STARLetDeck,
|
||||
TIP_CAR_480_A00,
|
||||
Cos_96_DW_1mL,
|
||||
Trough_100ml,
|
||||
set_tip_tracking,
|
||||
set_volume_tracking
|
||||
)
|
||||
|
||||
# Enable tracking
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Initialize liquid handler
|
||||
lh = LiquidHandler(backend=STAR(), deck=STARLetDeck())
|
||||
await lh.setup()
|
||||
|
||||
# Define resources
|
||||
tip_rack_1 = TIP_CAR_480_A00(name="tips_1")
|
||||
tip_rack_2 = TIP_CAR_480_A00(name="tips_2")
|
||||
source_plate = Cos_96_DW_1mL(name="source")
|
||||
dest_plate = Cos_96_DW_1mL(name="dest")
|
||||
buffer = Trough_100ml(name="buffer")
|
||||
|
||||
# Assign to deck
|
||||
lh.deck.assign_child_resource(tip_rack_1, rails=1)
|
||||
lh.deck.assign_child_resource(tip_rack_2, rails=2)
|
||||
lh.deck.assign_child_resource(buffer, rails=5)
|
||||
lh.deck.assign_child_resource(source_plate, rails=10)
|
||||
lh.deck.assign_child_resource(dest_plate, rails=15)
|
||||
|
||||
# Set initial volumes
|
||||
for well in source_plate.children:
|
||||
well.tracker.set_liquids([(None, 200)])
|
||||
|
||||
buffer["channel_1"].tracker.set_liquids([(None, 50000)]) # 50 mL
|
||||
|
||||
# Save deck layout
|
||||
lh.deck.save("my_protocol_deck.json")
|
||||
|
||||
# Save initial state
|
||||
import json
|
||||
with open("initial_state.json", "w") as f:
|
||||
json.dump(lh.deck.serialize_all_state(), f)
|
||||
```
|
||||
|
||||
### Loading Saved Deck
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Deck
|
||||
|
||||
# Load deck from file
|
||||
deck = Deck.load_from_json_file("my_protocol_deck.json")
|
||||
|
||||
# Load state
|
||||
import json
|
||||
with open("initial_state.json", "r") as f:
|
||||
state = json.load(f)
|
||||
deck.load_all_state(state)
|
||||
|
||||
# Use with liquid handler
|
||||
lh = LiquidHandler(backend=STAR(), deck=deck)
|
||||
await lh.setup()
|
||||
|
||||
# Access resources by name
|
||||
source_plate = deck.get_resource("source")
|
||||
dest_plate = deck.get_resource("dest")
|
||||
```
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Resource Documentation: https://docs.pylabrobot.org/resources/introduction.html
|
||||
- Custom Resources Guide: https://docs.pylabrobot.org/resources/custom-resources.html
|
||||
- API Reference: https://docs.pylabrobot.org/api/pylabrobot.resources.html
|
||||
- Deck Layouts: https://github.com/PyLabRobot/pylabrobot/tree/main/pylabrobot/resources/deck
|
||||
532
skills/pylabrobot/references/visualization.md
Normal file
532
skills/pylabrobot/references/visualization.md
Normal file
@@ -0,0 +1,532 @@
|
||||
# Visualization & Simulation in PyLabRobot
|
||||
|
||||
## Overview
|
||||
|
||||
PyLabRobot provides visualization and simulation tools for developing, testing, and validating laboratory protocols without physical hardware. The visualizer offers real-time 3D visualization of deck state, while simulation backends enable protocol testing and validation.
|
||||
|
||||
## The Visualizer
|
||||
|
||||
### What is the Visualizer?
|
||||
|
||||
The PyLabRobot Visualizer is a browser-based tool that:
|
||||
- Displays 3D visualization of the deck layout
|
||||
- Shows real-time tip presence and liquid volumes
|
||||
- Works with both simulated and physical robots
|
||||
- Provides interactive deck state inspection
|
||||
- Enables visual protocol validation
|
||||
|
||||
### Starting the Visualizer
|
||||
|
||||
The visualizer runs as a web server and displays in your browser:
|
||||
|
||||
```python
|
||||
from pylabrobot.visualizer import Visualizer
|
||||
|
||||
# Create visualizer
|
||||
vis = Visualizer()
|
||||
|
||||
# Start web server (opens browser automatically)
|
||||
await vis.start()
|
||||
|
||||
# Stop visualizer
|
||||
await vis.stop()
|
||||
```
|
||||
|
||||
**Default Settings:**
|
||||
- Port: 1234 (http://localhost:1234)
|
||||
- Opens browser automatically when started
|
||||
|
||||
### Connecting Liquid Handler to Visualizer
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
from pylabrobot.resources import STARLetDeck
|
||||
from pylabrobot.visualizer import Visualizer
|
||||
|
||||
# Create visualizer
|
||||
vis = Visualizer()
|
||||
await vis.start()
|
||||
|
||||
# Create liquid handler with simulation backend
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(num_channels=8),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
|
||||
# Connect liquid handler to visualizer
|
||||
lh.visualizer = vis
|
||||
|
||||
await lh.setup()
|
||||
|
||||
# Now all operations are visualized in real-time
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.aspirate(plate["A1:H1"], vols=100)
|
||||
await lh.dispense(plate["A2:H2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
```
|
||||
|
||||
### Tracking Features
|
||||
|
||||
#### Enable Tracking
|
||||
|
||||
For the visualizer to display tips and liquids, enable tracking:
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import set_tip_tracking, set_volume_tracking
|
||||
|
||||
# Enable globally (before creating resources)
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
```
|
||||
|
||||
#### Setting Initial Liquids
|
||||
|
||||
Define initial liquid contents for visualization:
|
||||
|
||||
```python
|
||||
# Set liquid in a single well
|
||||
plate["A1"].tracker.set_liquids([
|
||||
(None, 200) # (liquid_type, volume_in_µL)
|
||||
])
|
||||
|
||||
# Set multiple liquids in one well
|
||||
plate["A2"].tracker.set_liquids([
|
||||
("water", 100),
|
||||
("ethanol", 50)
|
||||
])
|
||||
|
||||
# Set liquids in multiple wells
|
||||
for well in plate["A1:H1"]:
|
||||
well.tracker.set_liquids([(None, 200)])
|
||||
|
||||
# Set liquids in entire plate
|
||||
for well in plate.children:
|
||||
well.tracker.set_liquids([("sample", 150)])
|
||||
```
|
||||
|
||||
#### Visualizing Tip Presence
|
||||
|
||||
```python
|
||||
# Tips are automatically tracked when using pick_up/drop operations
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"]) # Tips shown as absent in visualizer
|
||||
await lh.return_tips() # Tips shown as present in visualizer
|
||||
```
|
||||
|
||||
### Complete Visualizer Example
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
from pylabrobot.resources import (
|
||||
STARLetDeck,
|
||||
TIP_CAR_480_A00,
|
||||
Cos_96_DW_1mL,
|
||||
set_tip_tracking,
|
||||
set_volume_tracking
|
||||
)
|
||||
from pylabrobot.visualizer import Visualizer
|
||||
|
||||
# Enable tracking
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Create visualizer
|
||||
vis = Visualizer()
|
||||
await vis.start()
|
||||
|
||||
# Create liquid handler
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(num_channels=8),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
lh.visualizer = vis
|
||||
await lh.setup()
|
||||
|
||||
# Define resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
source_plate = Cos_96_DW_1mL(name="source")
|
||||
dest_plate = Cos_96_DW_1mL(name="dest")
|
||||
|
||||
# Assign to deck
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(source_plate, rails=10)
|
||||
lh.deck.assign_child_resource(dest_plate, rails=15)
|
||||
|
||||
# Set initial volumes
|
||||
for well in source_plate.children:
|
||||
well.tracker.set_liquids([("sample", 200)])
|
||||
|
||||
# Execute protocol with visualization
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
await lh.transfer(
|
||||
source_plate["A1:H12"],
|
||||
dest_plate["A1:H12"],
|
||||
vols=100
|
||||
)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Keep visualizer open to inspect final state
|
||||
input("Press Enter to close visualizer...")
|
||||
|
||||
# Cleanup
|
||||
await lh.stop()
|
||||
await vis.stop()
|
||||
```
|
||||
|
||||
## Deck Layout Editor
|
||||
|
||||
### Using the Deck Editor
|
||||
|
||||
PyLabRobot includes a graphical deck layout editor:
|
||||
|
||||
**Features:**
|
||||
- Visual deck design interface
|
||||
- Drag-and-drop resource placement
|
||||
- Edit initial liquid states
|
||||
- Set tip presence
|
||||
- Save/load layouts as JSON
|
||||
|
||||
**Usage:**
|
||||
- Accessed through the visualizer interface
|
||||
- Create layouts graphically instead of code
|
||||
- Export to JSON for use in protocols
|
||||
|
||||
### Loading Deck Layouts
|
||||
|
||||
```python
|
||||
from pylabrobot.resources import Deck
|
||||
|
||||
# Load deck from JSON file
|
||||
deck = Deck.load_from_json_file("my_deck_layout.json")
|
||||
|
||||
# Use with liquid handler
|
||||
lh = LiquidHandler(backend=backend, deck=deck)
|
||||
await lh.setup()
|
||||
|
||||
# Resources are already assigned
|
||||
source = deck.get_resource("source")
|
||||
dest = deck.get_resource("dest")
|
||||
tip_rack = deck.get_resource("tips")
|
||||
```
|
||||
|
||||
## Simulation
|
||||
|
||||
### ChatterboxBackend
|
||||
|
||||
The ChatterboxBackend simulates liquid handling operations:
|
||||
|
||||
**Features:**
|
||||
- No hardware required
|
||||
- Validates protocol logic
|
||||
- Tracks tips and volumes
|
||||
- Supports all liquid handling operations
|
||||
- Works with visualizer
|
||||
|
||||
**Setup:**
|
||||
|
||||
```python
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
|
||||
# Create simulation backend
|
||||
backend = ChatterboxBackend(
|
||||
num_channels=8 # Simulate 8-channel pipette
|
||||
)
|
||||
|
||||
# Use with liquid handler
|
||||
lh = LiquidHandler(backend=backend, deck=STARLetDeck())
|
||||
```
|
||||
|
||||
### Simulation Use Cases
|
||||
|
||||
#### Protocol Development
|
||||
|
||||
```python
|
||||
async def develop_protocol():
|
||||
"""Develop protocol using simulation"""
|
||||
|
||||
# Use simulation for development
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
|
||||
# Connect visualizer
|
||||
vis = Visualizer()
|
||||
await vis.start()
|
||||
lh.visualizer = vis
|
||||
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Develop and test protocol
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
print("Protocol development complete!")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await vis.stop()
|
||||
```
|
||||
|
||||
#### Protocol Validation
|
||||
|
||||
```python
|
||||
async def validate_protocol():
|
||||
"""Validate protocol logic without hardware"""
|
||||
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Setup resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(plate, rails=10)
|
||||
|
||||
# Set initial state
|
||||
for well in plate.children:
|
||||
well.tracker.set_liquids([(None, 200)])
|
||||
|
||||
# Execute protocol
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
|
||||
# Test different volumes
|
||||
test_volumes = [50, 100, 150]
|
||||
for i, vol in enumerate(test_volumes):
|
||||
await lh.transfer(
|
||||
plate[f"A{i+1}:H{i+1}"],
|
||||
plate[f"A{i+4}:H{i+4}"],
|
||||
vols=vol
|
||||
)
|
||||
|
||||
await lh.drop_tips()
|
||||
|
||||
# Validate volumes
|
||||
for i, vol in enumerate(test_volumes):
|
||||
for row in "ABCDEFGH":
|
||||
well = plate[f"{row}{i+4}"]
|
||||
actual_vol = well.tracker.get_volume()
|
||||
assert actual_vol == vol, f"Volume mismatch in {well.name}"
|
||||
|
||||
print("✓ Protocol validation passed!")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
#### Testing Edge Cases
|
||||
|
||||
```python
|
||||
async def test_edge_cases():
|
||||
"""Test protocol edge cases in simulation"""
|
||||
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Test 1: Empty well aspiration
|
||||
try:
|
||||
await lh.aspirate(empty_plate["A1"], vols=100)
|
||||
print("✗ Should have raised error for empty well")
|
||||
except Exception as e:
|
||||
print(f"✓ Correctly raised error: {e}")
|
||||
|
||||
# Test 2: Overfilling well
|
||||
try:
|
||||
await lh.dispense(small_well, vols=1000) # Too much
|
||||
print("✗ Should have raised error for overfilling")
|
||||
except Exception as e:
|
||||
print(f"✓ Correctly raised error: {e}")
|
||||
|
||||
# Test 3: Tip capacity
|
||||
try:
|
||||
await lh.aspirate(large_volume_well, vols=2000) # Exceeds tip capacity
|
||||
print("✗ Should have raised error for tip capacity")
|
||||
except Exception as e:
|
||||
print(f"✓ Correctly raised error: {e}")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
### CI/CD Integration
|
||||
|
||||
Use simulation for automated testing:
|
||||
|
||||
```python
|
||||
# test_protocols.py
|
||||
import pytest
|
||||
from pylabrobot.liquid_handling import LiquidHandler
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_transfer_protocol():
|
||||
"""Test liquid transfer protocol"""
|
||||
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Setup
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
|
||||
lh.deck.assign_child_resource(tip_rack, rails=1)
|
||||
lh.deck.assign_child_resource(plate, rails=10)
|
||||
|
||||
# Set initial volumes
|
||||
plate["A1"].tracker.set_liquids([(None, 200)])
|
||||
|
||||
# Execute
|
||||
await lh.pick_up_tips(tip_rack["A1"])
|
||||
await lh.transfer(plate["A1"], plate["A2"], vols=100)
|
||||
await lh.drop_tips()
|
||||
|
||||
# Assert
|
||||
assert plate["A1"].tracker.get_volume() == 100
|
||||
assert plate["A2"].tracker.get_volume() == 100
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
1. **Always Use Simulation First**: Develop and test protocols in simulation before running on hardware
|
||||
2. **Enable Tracking**: Turn on tip and volume tracking for accurate visualization
|
||||
3. **Set Initial States**: Define initial liquid volumes for realistic simulation
|
||||
4. **Visual Inspection**: Use visualizer to verify deck layout and protocol execution
|
||||
5. **Validate Logic**: Test edge cases and error conditions in simulation
|
||||
6. **Automated Testing**: Integrate simulation into CI/CD pipelines
|
||||
7. **Save Layouts**: Use JSON to save and share deck layouts
|
||||
8. **Document States**: Record initial states for reproducibility
|
||||
9. **Interactive Development**: Keep visualizer open during development
|
||||
10. **Protocol Refinement**: Iterate in simulation before hardware runs
|
||||
|
||||
## Common Patterns
|
||||
|
||||
### Development to Production Workflow
|
||||
|
||||
```python
|
||||
import os
|
||||
|
||||
# Configuration
|
||||
USE_HARDWARE = os.getenv("USE_HARDWARE", "false").lower() == "true"
|
||||
|
||||
# Create appropriate backend
|
||||
if USE_HARDWARE:
|
||||
from pylabrobot.liquid_handling.backends import STAR
|
||||
backend = STAR()
|
||||
print("Running on Hamilton STAR hardware")
|
||||
else:
|
||||
from pylabrobot.liquid_handling.backends.simulation import ChatterboxBackend
|
||||
backend = ChatterboxBackend()
|
||||
print("Running in simulation mode")
|
||||
|
||||
# Rest of protocol is identical
|
||||
lh = LiquidHandler(backend=backend, deck=STARLetDeck())
|
||||
|
||||
if not USE_HARDWARE:
|
||||
# Enable visualizer for simulation
|
||||
vis = Visualizer()
|
||||
await vis.start()
|
||||
lh.visualizer = vis
|
||||
|
||||
await lh.setup()
|
||||
|
||||
# Protocol execution
|
||||
# ... (same code for hardware and simulation)
|
||||
|
||||
# Run with: USE_HARDWARE=false python protocol.py # Simulation
|
||||
# Run with: USE_HARDWARE=true python protocol.py # Hardware
|
||||
```
|
||||
|
||||
### Visual Protocol Verification
|
||||
|
||||
```python
|
||||
async def visual_verification():
|
||||
"""Run protocol with visual verification pauses"""
|
||||
|
||||
vis = Visualizer()
|
||||
await vis.start()
|
||||
|
||||
lh = LiquidHandler(
|
||||
backend=ChatterboxBackend(),
|
||||
deck=STARLetDeck()
|
||||
)
|
||||
lh.visualizer = vis
|
||||
await lh.setup()
|
||||
|
||||
try:
|
||||
# Step 1
|
||||
await lh.pick_up_tips(tip_rack["A1:H1"])
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# Step 2
|
||||
await lh.aspirate(source["A1:H1"], vols=100)
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# Step 3
|
||||
await lh.dispense(dest["A1:H1"], vols=100)
|
||||
input("Press Enter to continue...")
|
||||
|
||||
# Step 4
|
||||
await lh.drop_tips()
|
||||
input("Press Enter to finish...")
|
||||
|
||||
finally:
|
||||
await lh.stop()
|
||||
await vis.stop()
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
### Visualizer Not Updating
|
||||
|
||||
- Ensure `lh.visualizer = vis` is set before operations
|
||||
- Check that tracking is enabled globally
|
||||
- Verify visualizer is running (`vis.start()`)
|
||||
- Refresh browser if connection is lost
|
||||
|
||||
### Tracking Not Working
|
||||
|
||||
```python
|
||||
# Must enable tracking BEFORE creating resources
|
||||
set_tip_tracking(True)
|
||||
set_volume_tracking(True)
|
||||
|
||||
# Then create resources
|
||||
tip_rack = TIP_CAR_480_A00(name="tips")
|
||||
plate = Cos_96_DW_1mL(name="plate")
|
||||
```
|
||||
|
||||
### Simulation Errors
|
||||
|
||||
- Simulation validates operations (e.g., can't aspirate from empty well)
|
||||
- Use try/except to handle validation errors
|
||||
- Check initial states are set correctly
|
||||
- Verify volumes don't exceed capacities
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Visualizer Documentation: https://docs.pylabrobot.org/user_guide/using-the-visualizer.html (if available)
|
||||
- Simulation Guide: https://docs.pylabrobot.org/user_guide/simulation.html (if available)
|
||||
- API Reference: https://docs.pylabrobot.org/api/pylabrobot.visualizer.html
|
||||
- GitHub Examples: https://github.com/PyLabRobot/pylabrobot/tree/main/examples
|
||||
Reference in New Issue
Block a user