Skip to content

Commit 72da930

Browse files
authored
tutorial for move_channel_{x,y,z} (#284)
1 parent 9621749 commit 72da930

File tree

5 files changed

+231
-0
lines changed

5 files changed

+231
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
8383
- Add `F.linear_tip_spot_generator` and `F.randomized_tip_spot_generator` for looping over tip spots, with caching (https://github.com/PyLabRobot/pylabrobot/pull/256)
8484
- Add `skip_autoload`, `skip_iswap`, and `skip_core96_head` flags to `STAR.setup` (https://github.com/PyLabRobot/pylabrobot/pull/263)
8585
- Add `skip_autoload`, `skip_iswap`, and `skip_core96_head` flags to `Vantage.setup` (https://github.com/PyLabRobot/pylabrobot/pull/263)
86+
- `Resource.get_highest_known_point` (https://github.com/PyLabRobot/pylabrobot/pull/284)
8687

8788
### Deprecated
8889

docs/user_guide/index.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ using-the-visualizer
1616
using-trackers
1717
writing-robot-agnostic-methods
1818
hamilton-star/hamilton-star
19+
moving-channels-around
1920
```
2021

2122
```{toctree}
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"metadata": {},
6+
"source": [
7+
"# Manually moving channels around\n",
8+
"\n",
9+
"![star supported](https://img.shields.io/badge/STAR-supported-blue)\n",
10+
"![Vantage supported](https://img.shields.io/badge/Vantage-supported-blue)\n",
11+
"![OT supported](https://img.shields.io/badge/OT-supported-blue)\n",
12+
"![EVO not tested](https://img.shields.io/badge/EVO-not%20tested-orange)\n",
13+
"\n",
14+
"With PLR, you can easily move individual channels around. This is useful for calibrating labware locations, calibrating labware sizes, and other things.\n",
15+
"\n",
16+
"```{warning}\n",
17+
"Be very careful about collisions! Move channels to a safe z height before traversing.\n",
18+
"```\n",
19+
"\n",
20+
"```{note}\n",
21+
"With Hamilton robots, when a tip is mounted, the z location will refer to the point of the pipetting tip. With no tip mounted, it will refer to the bottom of the channel.\n",
22+
"```"
23+
]
24+
},
25+
{
26+
"cell_type": "markdown",
27+
"metadata": {},
28+
"source": [
29+
"## Example: Hamilton STAR\n",
30+
"\n",
31+
"Here, we'll use a Hamilton STAR as an example. For other robots, simply change the deck layout, makign sure that you have at least a tip rack to use."
32+
]
33+
},
34+
{
35+
"cell_type": "code",
36+
"execution_count": 1,
37+
"metadata": {},
38+
"outputs": [],
39+
"source": [
40+
"from pylabrobot.liquid_handling import LiquidHandler, STAR\n",
41+
"from pylabrobot.resources import STARDeck, TIP_CAR_480_A00, HTF_L\n",
42+
"\n",
43+
"lh = LiquidHandler(backend=STAR(), deck=STARDeck())\n",
44+
"await lh.setup()\n",
45+
"\n",
46+
"# assign a tip rack\n",
47+
"tip_carrier = TIP_CAR_480_A00(name=\"tip_carrier\")\n",
48+
"tip_carrier[0] = tip_rack = HTF_L(name=\"tip_rack\")\n",
49+
"lh.deck.assign_child_resource(tip_carrier, rails=0)"
50+
]
51+
},
52+
{
53+
"cell_type": "markdown",
54+
"metadata": {},
55+
"source": [
56+
"## Moving channels\n",
57+
"\n",
58+
"All positions are in mm. The movements are to absolute positions. The origin will be near the left front bottom of the deck, but it differs between robots.\n",
59+
"\n",
60+
"* x: left (low) to right (high)\n",
61+
"* y: front (low) to back (high)\n",
62+
"* z: bottom (low) to top (high)"
63+
]
64+
},
65+
{
66+
"cell_type": "code",
67+
"execution_count": 2,
68+
"metadata": {},
69+
"outputs": [],
70+
"source": [
71+
"channel = 1 # the channel to use\n",
72+
"\n",
73+
"# start by picking up a single tip\n",
74+
"await lh.pick_up_tips(tip_rack[\"A1\"], use_channels=[channel])\n",
75+
"\n",
76+
"# prepare for manual operation\n",
77+
"# this will space the other channels to safe positions\n",
78+
"await lh.prepare_for_manual_channel_operation(channel)"
79+
]
80+
},
81+
{
82+
"cell_type": "markdown",
83+
"metadata": {},
84+
"source": [
85+
"Since the channnel will be above the tip rack, it should be safe to move up. We perform a quick check to make sure the z_safe is at least above the resources we know about."
86+
]
87+
},
88+
{
89+
"cell_type": "code",
90+
"execution_count": 3,
91+
"metadata": {},
92+
"outputs": [],
93+
"source": [
94+
"z_safe = 240 # WARNING: this might NOT be safe for your setup\n",
95+
"\n",
96+
"if z_safe <= lh.deck.get_heighest_known_point():\n",
97+
" raise ValueError(f\"z_safe position is not safe, it is lower than the highest known point: {lh.deck.get_heighest_known_point()}\")\n",
98+
"\n",
99+
"await lh.move_channel_z(channel, z_safe)"
100+
]
101+
},
102+
{
103+
"cell_type": "markdown",
104+
"metadata": {},
105+
"source": [
106+
"```{warning}\n",
107+
"The z position in the code above should be safe for most setups, but we can't guarantee that it will be safe for all setups. Move to a z position that is above all your labware before moving in the xy plane.\n",
108+
"```\n",
109+
"\n",
110+
"When the z position of the bottom of the tip is above the labware, you can move the channel around in the xy plane."
111+
]
112+
},
113+
{
114+
"cell_type": "code",
115+
"execution_count": 4,
116+
"metadata": {},
117+
"outputs": [],
118+
"source": [
119+
"# move the channel around\n",
120+
"await lh.move_channel_x(channel, 100)\n",
121+
"await lh.move_channel_y(channel, 100)"
122+
]
123+
},
124+
{
125+
"cell_type": "markdown",
126+
"metadata": {},
127+
"source": [
128+
"After reaching a spot where the channel can move down, you can use `move_channel_z` again."
129+
]
130+
},
131+
{
132+
"cell_type": "code",
133+
"execution_count": null,
134+
"metadata": {},
135+
"outputs": [],
136+
"source": [
137+
"await lh.move_channel_z(channel, 100)"
138+
]
139+
},
140+
{
141+
"cell_type": "markdown",
142+
"metadata": {},
143+
"source": [
144+
"Before returning the tip to the tip rack, make sure to move the channel to a safe z position again."
145+
]
146+
},
147+
{
148+
"cell_type": "code",
149+
"execution_count": null,
150+
"metadata": {},
151+
"outputs": [],
152+
"source": [
153+
"await lh.move_channel_z(channel, z_safe)"
154+
]
155+
},
156+
{
157+
"cell_type": "markdown",
158+
"metadata": {},
159+
"source": [
160+
"You can run the code above as often as you like. When you're done, you can return the channel to the tip rack."
161+
]
162+
},
163+
{
164+
"cell_type": "code",
165+
"execution_count": 5,
166+
"metadata": {},
167+
"outputs": [],
168+
"source": [
169+
"await lh.return_tips()"
170+
]
171+
}
172+
],
173+
"metadata": {
174+
"kernelspec": {
175+
"display_name": "env",
176+
"language": "python",
177+
"name": "python3"
178+
},
179+
"language_info": {
180+
"codemirror_mode": {
181+
"name": "ipython",
182+
"version": 3
183+
},
184+
"file_extension": ".py",
185+
"mimetype": "text/x-python",
186+
"name": "python",
187+
"nbconvert_exporter": "python",
188+
"pygments_lexer": "ipython3",
189+
"version": "3.11.2"
190+
}
191+
},
192+
"nbformat": 4,
193+
"nbformat_minor": 2
194+
}

pylabrobot/liquid_handling/liquid_handler.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2006,6 +2006,25 @@ def load(cls, path: str) -> LiquidHandler:
20062006
with open(path, "r", encoding="utf-8") as f:
20072007
return cls.deserialize(json.load(f))
20082008

2009+
async def prepare_for_manual_channel_operation(self, channel: int):
2010+
assert 0 <= channel < self.backend.num_channels, f"Invalid channel: {channel}"
2011+
await self.backend.prepare_for_manual_channel_operation(channel=channel)
2012+
2013+
async def move_channel_x(self, channel: int, x: float):
2014+
"""Move channel to absolute x position"""
2015+
assert 0 <= channel < self.backend.num_channels, f"Invalid channel: {channel}"
2016+
await self.backend.move_channel_x(channel=channel, x=x)
2017+
2018+
async def move_channel_y(self, channel: int, y: float):
2019+
"""Move channel to absolute y position"""
2020+
assert 0 <= channel < self.backend.num_channels, f"Invalid channel: {channel}"
2021+
await self.backend.move_channel_y(channel=channel, y=y)
2022+
2023+
async def move_channel_z(self, channel: int, z: float):
2024+
"""Move channel to absolute z position"""
2025+
assert 0 <= channel < self.backend.num_channels, f"Invalid channel: {channel}"
2026+
await self.backend.move_channel_z(channel=channel, z=z)
2027+
20092028
# -- Resource methods --
20102029

20112030
def assign_child_resource(

pylabrobot/resources/resource.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -737,3 +737,19 @@ def deregister_state_update_callback(self, callback: ResourceDidUpdateState):
737737
def _state_updated(self):
738738
for callback in self._resource_state_updated_callbacks:
739739
callback(self.serialize_state())
740+
741+
def get_heighest_known_point(self) -> float:
742+
"""Recursively finds the highest known point in absolute space. This ignores the top of the
743+
deck.
744+
745+
```{warning}
746+
This methods returns the highest KNOWN point. If you have labware that is not assigned in PLR,
747+
this method might not return the correct value.
748+
```
749+
"""
750+
heighest_point = self.get_absolute_location(z="t").z
751+
if self.name == "deck":
752+
heighest_point = 0
753+
for resource in self.children:
754+
heighest_point = max(heighest_point, resource.get_heighest_known_point())
755+
return heighest_point

0 commit comments

Comments
 (0)