Initial commit
This commit is contained in:
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