# 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