Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix star foil utils (for single/multiple well), add a foil doc #424

Merged
merged 4 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
192 changes: 192 additions & 0 deletions docs/user_guide/hamilton-star/foil.ipynb
Original file line number Diff line number Diff line change
@@ -0,0 +1,192 @@
{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Foil\n",
"\n",
"The :class:`~pylabrobot.liquid_handling.backends.hamilton.STAR.STAR` backend includes special utilities for working with foil-sealed plates, specifically:\n",
"\n",
"1. a function to pierce foil before aspirating from the plate, and\n",
"2. a function to keep the plate down while moving the channels up to avoid lifting the plate."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Example setup\n",
"\n",
"```{note}\n",
"While this example uses high volume tips, it _might_ be possible to use other tip types to pierce the foil. However, 50uL tips are very soft and probably can't be used.\n",
"```"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [],
"source": [
"from pylabrobot.liquid_handling import LiquidHandler, STAR\n",
"from pylabrobot.resources import STARLetDeck\n",
"from pylabrobot.resources import (\n",
" TIP_CAR_480_A00,\n",
" PLT_CAR_L5AC_A00,\n",
" HT,\n",
" AGenBio_4_wellplate_Vb\n",
")\n",
"\n",
"star = STAR()\n",
"lh = LiquidHandler(backend=star, deck=STARLetDeck())\n",
"await lh.setup()\n",
"\n",
"# assign a tip rack\n",
"tip_carrier = TIP_CAR_480_A00(name=\"tip_carrier\")\n",
"tip_carrier[1] = tip_rack = HT(name=\"tip_rack\")\n",
"lh.deck.assign_child_resource(tip_carrier, rails=1)\n",
"\n",
"# assign a plate\n",
"plt_carrier = PLT_CAR_L5AC_A00(name=\"plt_carrier\")\n",
"plt_carrier[0] = plate = AGenBio_4_wellplate_Vb(name=\"plate\")\n",
"lh.deck.assign_child_resource(plt_carrier, rails=10)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Breaking the foil before using a plate\n",
"\n",
"It is important to break the foil before aspirating because tiny foil pieces can stuck in the tip, drastically changing the liquid handling characteristics.\n",
"\n",
"In this example, we will use an 8 channel workcell and use the inner 6 channels for breaking the foil and then aspirating. We will use the outer 2 channels to keep the plate down while the inner channels are moving up."
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [],
"source": [
"well = plate.get_well(\"A1\")\n",
"await lh.pick_up_tips(tip_rack[\"A1:H1\"])"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [],
"source": [
"aspiration_channels = [1, 2, 3, 4, 5, 6]\n",
"hold_down_channels = [0, 7]\n",
"await star.pierce_foil(\n",
" wells=[well],\n",
" piercing_channels=aspiration_channels,\n",
" hold_down_channels=hold_down_channels,\n",
" move_inwards=4,\n",
" one_by_one=False,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [],
"source": [
"await lh.return_tips()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![gif of piercing foil](./img/pierce_foil.gif)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"## Holding the plate down\n",
"\n",
"Holding the plate down while moving channels up after aspiration consists of two parts:\n",
"1. Making the channels stay down after a liquid handling operation has finished. By default, STAR will move channels up to traversal height.\n",
"2. Putting two channels on the edges of the plate to hold it down, while moving the other channels up."
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [],
"source": [
"await lh.pick_up_tips(tip_rack[\"A2:H2\"])"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [],
"source": [
"num_channels = len(aspiration_channels)\n",
"await lh.aspirate(\n",
" [well]*num_channels, vols=[100]*num_channels, use_channels=aspiration_channels,\n",
"\n",
" # aspiration parameters (backend_kwargs)\n",
" min_z_endpos=well.get_absolute_location(z=\"cavity_bottom\").z, # z end position: where channels go after aspiration\n",
" surface_following_distance=0, # no moving in z dimension during aspiration\n",
" pull_out_distance_transport_air=[0] * num_channels # no moving up to aspirate transport air after aspiration\n",
")\n",
"\n",
"await star.step_off_foil(\n",
" well,\n",
" front_channel=7,\n",
" back_channel=0,\n",
" move_inwards=5,\n",
")"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [],
"source": [
"await lh.return_tips()"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![gif of holding down foil](./img/step_off_foil.gif)"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "env",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.15"
}
},
"nbformat": 4,
"nbformat_minor": 2
}
1 change: 1 addition & 0 deletions docs/user_guide/hamilton-star/hamilton-star.rst
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ Tools for working with Hamilton-STAR specific functions.
iswap-module
star_lld
z-probing
foil
Binary file added docs/user_guide/hamilton-star/img/pierce_foil.gif
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
69 changes: 41 additions & 28 deletions pylabrobot/liquid_handling/backends/hamilton/STAR.py
Original file line number Diff line number Diff line change
Expand Up @@ -7736,6 +7736,11 @@ async def pierce_foil(
x: float
ys: List[float]
z: float

# if only one well is give, but in a list, convert to Well so we fall into single-well logic.
if isinstance(wells, list) and len(wells) == 1:
wells = wells[0]

if isinstance(wells, Well):
well = wells
x, y, z = well.get_absolute_location("c", "c", "cavity_bottom")
Expand All @@ -7753,9 +7758,10 @@ async def pierce_foil(
assert (
len(set(w.get_absolute_location().x for w in wells)) == 1
), "Wells must be on the same column"
x = wells[0].get_absolute_location().x
ys = [well.get_absolute_location().y for well in wells]
z = wells[0].get_absolute_location(z="cavity_bottom").z
absolute_center = wells[0].get_absolute_location("c", "c", "cavity_bottom")
x = absolute_center.x
ys = [well.get_absolute_location(y="c").y for well in wells]
z = absolute_center.z

await self.move_channel_x(0, x=x)

Expand All @@ -7773,15 +7779,15 @@ async def pierce_foil(
)

await self.step_off_foil(
well,
[wells] if isinstance(wells, Well) else wells,
back_channel=hold_down_channels[0],
front_channel=hold_down_channels[1],
move_inwards=move_inwards,
)

async def step_off_foil(
self,
well: Well,
wells: Union[Well, List[Well]],
front_channel: int,
back_channel: int,
move_inwards: float = 2,
Expand All @@ -7804,10 +7810,11 @@ async def step_off_foil(
min_z_endpos=well.get_absolute_location(z="cavity_bottom").z,
surface_following_distance=0,
pull_out_distance_transport_air=[0] * 4)
await step_off_foil(lh.backend, well, front_channel=11, back_channel=6, move_inwards = 3)
await step_off_foil(lh.backend, [well], front_channel=11, back_channel=6, move_inwards=3)

Args:
well: Well in the plate to hold down. (x-coordinate of channels will be at center of well).
wells: Wells in the plate to hold down. (x-coordinate of channels will be at center of wells).
Must be sorted from back to front.
front_channel: The channel to place on the front of the plate.
back_channel: The channel to place on the back of the plate.
move_inwards: mm to move inwards (backward on the front channel; frontward on the back).
Expand All @@ -7819,22 +7826,30 @@ async def step_off_foil(
"front_channel should be in front of back_channel. " "Channels are 0-indexed from the back."
)

# Get the absolute locations for center front top and center back top
orientation = well.get_absolute_rotation().z % 90
if orientation == 0:
back_location = well.get_absolute_location("c", "b", "t")
front_location = well.get_absolute_location("c", "f", "t")
elif orientation == 90:
back_location = well.get_absolute_location("r", "c", "t")
front_location = well.get_absolute_location("l", "c", "t")
elif orientation == 180:
back_location = well.get_absolute_location("c", "f", "b")
front_location = well.get_absolute_location("c", "b", "b")
elif orientation == 270:
back_location = well.get_absolute_location("l", "c", "b")
front_location = well.get_absolute_location("r", "c", "b")
if isinstance(wells, Well):
wells = [wells]

plates = set(well.parent for well in wells)
assert len(plates) == 1, "All wells must be in the same plate"
plate = plates.pop()
assert plate is not None

z_location = plate.get_absolute_location(z="top").z

if plate.get_absolute_rotation().z % 360 == 0:
back_location = plate.get_absolute_location(y="b")
front_location = plate.get_absolute_location(y="f")
elif plate.get_absolute_rotation().z % 360 == 90:
back_location = plate.get_absolute_location(x="r")
front_location = plate.get_absolute_location(x="l")
elif plate.get_absolute_rotation().z % 360 == 180:
back_location = plate.get_absolute_location(y="f")
front_location = plate.get_absolute_location(y="b")
elif plate.get_absolute_rotation().z % 360 == 270:
back_location = plate.get_absolute_location(x="l")
front_location = plate.get_absolute_location(x="r")
else:
raise ValueError("Rotation of well must be a multiple of 90 degrees")
raise ValueError("Plate rotation must be a multiple of 90 degrees")

try:
# Then move all channels in the y-space simultaneously.
Expand All @@ -7845,17 +7860,15 @@ async def step_off_foil(
}
)

await self.move_channel_z(front_channel, front_location.z)
await self.move_channel_z(back_channel, back_location.z)
await self.move_channel_z(front_channel, z_location)
await self.move_channel_z(back_channel, z_location)
finally:
# Move channels that are lower than the `front_channel` and `back_channel` to
# the just above the foil, in case the foil pops up.
zs = await self.get_channels_z_positions()
indices = [channel_idx for channel_idx, z in zs.items() if z < front_location.z]
indices = [channel_idx for channel_idx, z in zs.items() if z < z_location]
idx = {
idx: front_location.z + move_height
for idx in indices
if idx not in (front_channel, back_channel)
idx: z_location + move_height for idx in indices if idx not in (front_channel, back_channel)
}
await self.position_channels_in_z_direction(idx)

Expand Down