Initial commit

This commit is contained in:
Zhongwei Li
2025-11-30 08:30:10 +08:00
commit f0bd18fb4e
824 changed files with 331919 additions and 0 deletions

View 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

View 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

View 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

View 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

View 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

View 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