6161from pylabrobot .resources .errors import CrossContaminationError , HasTipError
6262from pylabrobot .resources .liquid import Liquid
6363from pylabrobot .resources .rotation import Rotation
64+ from pylabrobot .serializer import deserialize , serialize
6465from pylabrobot .tilting .tilter import Tilter
6566
6667from .backends import LiquidHandlerBackend
@@ -118,12 +119,18 @@ class LiquidHandler(Resource, Machine):
118119 defined in `pyhamilton.liquid_handling.backends`) to communicate with the liquid handler.
119120 """
120121
121- def __init__ (self , backend : LiquidHandlerBackend , deck : Deck ):
122+ def __init__ (
123+ self ,
124+ backend : LiquidHandlerBackend ,
125+ deck : Deck ,
126+ default_offset_head96 : Optional [Coordinate ] = None ,
127+ ):
122128 """Initialize a LiquidHandler.
123129
124130 Args:
125131 backend: Backend to use.
126132 deck: Deck to use.
133+ default_offset_head96: Base offset applied to all 96-head operations.
127134 """
128135
129136 Resource .__init__ (
@@ -149,6 +156,10 @@ def __init__(self, backend: LiquidHandlerBackend, deck: Deck):
149156
150157 self ._blow_out_air_volume : Optional [List [Optional [float ]]] = None
151158
159+ # Default offset applied to all 96-head operations. Any offset passed to a 96-head method is
160+ # added to this value.
161+ self .default_offset_head96 : Coordinate = default_offset_head96 or Coordinate .zero ()
162+
152163 # assign deck as only child resource, and set location of self to origin.
153164 self .location = Coordinate .zero ()
154165 super ().assign_child_resource (deck , location = deck .location or Coordinate .zero ())
@@ -1339,10 +1350,13 @@ async def pick_up_tips96(
13391350
13401351 Args:
13411352 tip_rack: The tip rack to pick up tips from.
1342- offset: The offset to use when picking up tips, optional.
1353+ offset: Additional offset to use when picking up tips. This is added to
1354+ :attr:`default_offset_head96`.
13431355 backend_kwargs: Additional keyword arguments for the backend, optional.
13441356 """
13451357
1358+ offset = self .default_offset_head96 + offset
1359+
13461360 self ._log_command (
13471361 "pick_up_tips96" ,
13481362 tip_rack = tip_rack ,
@@ -1406,13 +1420,16 @@ async def drop_tips96(
14061420
14071421 Args:
14081422 resource: The tip rack to drop tips to.
1409- offset: The offset to use when dropping tips.
1423+ offset: Additional offset to use when dropping tips. This is added to
1424+ :attr:`default_offset_head96`.
14101425 allow_nonzero_volume: If `True`, the tip will be dropped even if its volume is not zero (there
14111426 is liquid in the tip). If `False`, a RuntimeError will be raised if the tip has nonzero
14121427 volume.
14131428 backend_kwargs: Additional keyword arguments for the backend, optional.
14141429 """
14151430
1431+ offset = self .default_offset_head96 + offset
1432+
14161433 self ._log_command (
14171434 "drop_tips96" ,
14181435 resource = resource ,
@@ -1566,7 +1583,8 @@ async def aspirate96(
15661583 resource (Union[Plate, Container, List[Well]]): Resource object or list of wells.
15671584 volume (float): The volume to aspirate through each channel
15681585 offset (Coordinate): Adjustment to where the 96 head should go to aspirate relative to where
1569- the plate or container is defined to be. Defaults to Coordinate.zero().
1586+ the plate or container is defined to be. Added to :attr:`default_offset_head96`.
1587+ Defaults to :func:`Coordinate.zero`.
15701588 flow_rate ([Optional[float]]): The flow rate to use when aspirating, in ul/s. If `None`, the
15711589 backend default will be used.
15721590 liquid_height ([Optional[float]]): The height of the liquid in the well wrt the bottom, in
@@ -1576,6 +1594,8 @@ async def aspirate96(
15761594 backend_kwargs: Additional keyword arguments for the backend, optional.
15771595 """
15781596
1597+ offset = self .default_offset_head96 + offset
1598+
15791599 self ._log_command (
15801600 "aspirate96" ,
15811601 resource = resource ,
@@ -1684,7 +1704,7 @@ async def aspirate96(
16841704
16851705 try :
16861706 await self .backend .aspirate96 (aspiration = aspiration , ** backend_kwargs )
1687- except Exception as error :
1707+ except Exception :
16881708 for channel in self .head96 .values ():
16891709 channel .get_tip ().tracker .rollback ()
16901710 for container in containers :
@@ -1719,7 +1739,8 @@ async def dispense96(
17191739 resource (Union[Plate, Container, List[Well]]): Resource object or list of wells.
17201740 volume (float): The volume to dispense through each channel
17211741 offset (Coordinate): Adjustment to where the 96 head should go to aspirate relative to where
1722- the plate or container is defined to be. Defaults to Coordinate.zero().
1742+ the plate or container is defined to be. Added to :attr:`default_offset_head96`.
1743+ Defaults to :func:`Coordinate.zero`.
17231744 flow_rate ([Optional[float]]): The flow rate to use when dispensing, in ul/s. If `None`, the
17241745 backend default will be used.
17251746 liquid_height ([Optional[float]]): The height of the liquid in the well wrt the bottom, in
@@ -1729,6 +1750,8 @@ async def dispense96(
17291750 backend_kwargs: Additional keyword arguments for the backend, optional.
17301751 """
17311752
1753+ offset = self .default_offset_head96 + offset
1754+
17321755 self ._log_command (
17331756 "dispense96" ,
17341757 resource = resource ,
@@ -1829,7 +1852,7 @@ async def dispense96(
18291852
18301853 try :
18311854 await self .backend .dispense96 (dispense = dispense , ** backend_kwargs )
1832- except Exception as error :
1855+ except Exception :
18331856 for channel in self .head96 .values ():
18341857 channel .get_tip ().tracker .rollback ()
18351858 for container in containers :
@@ -2325,7 +2348,11 @@ async def move_plate(
23252348 )
23262349
23272350 def serialize (self ):
2328- return {** Resource .serialize (self ), ** Machine .serialize (self )}
2351+ return {
2352+ ** Resource .serialize (self ),
2353+ ** Machine .serialize (self ),
2354+ "default_offset_head96" : serialize (self .default_offset_head96 ),
2355+ }
23292356
23302357 @classmethod
23312358 def deserialize (cls , data : dict , allow_marshal : bool = False ) -> LiquidHandler :
@@ -2338,7 +2365,18 @@ def deserialize(cls, data: dict, allow_marshal: bool = False) -> LiquidHandler:
23382365 deck_data = data ["children" ][0 ]
23392366 deck = Deck .deserialize (data = deck_data , allow_marshal = allow_marshal )
23402367 backend = LiquidHandlerBackend .deserialize (data = data ["backend" ])
2341- return cls (deck = deck , backend = backend )
2368+
2369+ if "default_offset_head96" in data :
2370+ default_offset = deserialize (data ["default_offset_head96" ], allow_marshal = allow_marshal )
2371+ assert isinstance (default_offset , Coordinate )
2372+ else :
2373+ default_offset = Coordinate .zero ()
2374+
2375+ return cls (
2376+ deck = deck ,
2377+ backend = backend ,
2378+ default_offset_head96 = default_offset ,
2379+ )
23422380
23432381 @classmethod
23442382 def load (cls , path : str ) -> LiquidHandler :
0 commit comments