533 lines
13 KiB
Markdown
533 lines
13 KiB
Markdown
# 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
|