Skip to content

Commit b83a102

Browse files
authored
STARBackend can_reach_position and ensure_can_reach_position (#739)
1 parent 7edd849 commit b83a102

File tree

1 file changed

+54
-0
lines changed

1 file changed

+54
-0
lines changed

pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@
4848
MultiHeadDispensePlate,
4949
Pickup,
5050
PickupTipRack,
51+
PipettingOp,
5152
ResourceDrop,
5253
ResourceMove,
5354
ResourcePickup,
@@ -1180,6 +1181,8 @@ def __init__(
11801181

11811182
self._iswap_version: Optional[str] = None # loaded lazily
11821183

1184+
self._setup_done = False
1185+
11831186
@property
11841187
def unsafe(self) -> "UnSafe":
11851188
"""Actions that have a higher risk of damaging the robot. Use with care!"""
@@ -1452,8 +1455,51 @@ async def set_up_arm_modules():
14521455
# the core grippers.
14531456
self._core_parked = True
14541457

1458+
self._setup_done = True
1459+
1460+
async def stop(self):
1461+
await super().stop()
1462+
self._setup_done = False
1463+
1464+
@property
1465+
def setup_done(self) -> bool:
1466+
return self._setup_done
1467+
14551468
# ============== LiquidHandlerBackend methods ==============
14561469

1470+
def can_reach_position(self, channel_idx: int, position: Coordinate) -> bool:
1471+
"""Check if a position is reachable by a channel (center-based)."""
1472+
if not (0 <= channel_idx < self.num_channels):
1473+
raise ValueError(f"Channel {channel_idx} is out of range for this robot.")
1474+
1475+
# frontmost channel can go to y=6, every channel after that is about 8.9 mm further back
1476+
min_y_pos = 6 + 8.9 * (self.num_channels - channel_idx - 1)
1477+
if position.y < min_y_pos:
1478+
return False
1479+
1480+
# backmost channel can go to y=601.6, every channel before that is about 8.9 mm further forward
1481+
max_y_pos = 601.6 - 8.9 * channel_idx
1482+
if position.y > max_y_pos:
1483+
return False
1484+
1485+
return True
1486+
1487+
def ensure_can_reach_position(
1488+
self, use_channels: List[int], ops: Sequence[PipettingOp], op_name: str
1489+
):
1490+
locs = [(op.resource.get_location_wrt(self.deck, y="c") + op.offset) for op in ops]
1491+
cant_reach = [
1492+
channel_idx
1493+
for channel_idx, loc in zip(use_channels, locs)
1494+
if not self.can_reach_position(channel_idx, loc)
1495+
]
1496+
if len(cant_reach) > 0:
1497+
raise ValueError(
1498+
f"Channels {cant_reach} cannot reach their target positions in '{op_name}' operation.\n"
1499+
"Robots with more than 8 channels have limited Y-axis reach per channel; they don't have random access to the full deck area.\n"
1500+
"Try the operation with different channels or a different target position (i.e. different labware placement)."
1501+
)
1502+
14571503
async def pick_up_tips(
14581504
self,
14591505
ops: List[Pickup],
@@ -1465,6 +1511,8 @@ async def pick_up_tips(
14651511
):
14661512
"""Pick up tips from a resource."""
14671513

1514+
self.ensure_can_reach_position(use_channels, ops, "pick_up_tips")
1515+
14681516
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
14691517

14701518
tip_spots = [op.resource for op in ops]
@@ -1538,6 +1586,8 @@ async def drop_tips(
15381586
default if *all* tips are being dropped to a tip spot.
15391587
"""
15401588

1589+
self.ensure_can_reach_position(use_channels, ops, "drop_tips")
1590+
15411591
if drop_method is None:
15421592
if any(not isinstance(op.resource, TipSpot) for op in ops):
15431593
drop_method = TipDropMethod.PLACE_SHIFT
@@ -1812,6 +1862,8 @@ async def aspirate(
18121862
)
18131863
# # # delete # # #
18141864

1865+
self.ensure_can_reach_position(use_channels, ops, "aspirate")
1866+
18151867
x_positions, y_positions, channels_involved = self._ops_to_fw_positions(ops, use_channels)
18161868

18171869
n = len(ops)
@@ -2169,6 +2221,8 @@ async def dispense(
21692221
auto_surface_following_distance: automatically compute the surface following distance based on the container height<->volume functions. Requires liquid height to be specified or `probe_liquid_height=True`.
21702222
"""
21712223

2224+
self.ensure_can_reach_position(use_channels, ops, "dispense")
2225+
21722226
n = len(ops)
21732227

21742228
if jet is None:

0 commit comments

Comments
 (0)