Skip to content

Parameter set #450

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

Open
wants to merge 18 commits into
base: main
Choose a base branch
from
Open
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
- `location` parameter of `assign_child_resource` is not optional (https://github.com/PyLabRobot/pylabrobot/pull/336)
- `Resource.get_absolute_location` raises `NoLocationError` instead of `AssertionError` when absolute location is not defined (https://github.com/PyLabRobot/pylabrobot/pull/338)
- `no_trash` and `no_teaching_rack` were renamed to `with_trash` and `with_teaching_rack` to avoid double negatives (https://github.com/PyLabRobot/pylabrobot/pull/347)
- Hamilton liquid classes are no longer automatically inferred on the backends (`STAR`/`Vantage`). Instead, they create kwargs with `make_(asp|disp)(96)?_kwargs` (https://github.com/PyLabRobot/pylabrobot/pull/248)
- This also applies to volume correction curves, which are now the users' responsibility.

### Added

Expand Down
323 changes: 75 additions & 248 deletions pylabrobot/liquid_handling/backends/hamilton/STAR.py

Large diffs are not rendered by default.

76 changes: 59 additions & 17 deletions pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
from typing import cast

from pylabrobot.liquid_handling import LiquidHandler
from pylabrobot.liquid_handling.liquid_classes.hamilton.star import (
HighVolumeFilter_96COREHead1000ul_Water_DispenseSurface_Empty,
StandardVolumeFilter_Water_DispenseJet_Empty,
StandardVolumeFilter_Water_DispenseSurface,
)
from pylabrobot.liquid_handling.standard import GripDirection, Pickup
from pylabrobot.plate_reading import PlateReader
from pylabrobot.plate_reading.chatterbox import PlateReaderChatterboxBackend
Expand Down Expand Up @@ -264,6 +269,10 @@ def __init__(self, name: str):
self.STAR._iswap_parked = True
await self.lh.setup()

self.hlc = StandardVolumeFilter_Water_DispenseSurface.copy()
self.hlc.aspiration_air_transport_volume = 0
self.hlc.dispense_air_transport_volume = 0

async def asyncTearDown(self):
await self.lh.stop()

Expand Down Expand Up @@ -380,9 +389,15 @@ async def test_aspirate56(self):
await self.test_tip_pickup_56() # pick up tips first
assert self.plate.lid is not None
self.plate.lid.unassign()
corrected_vol = self.hlc.compute_corrected_volume(100)
for well in self.plate.get_items(["A1", "B1"]):
well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction
await self.lh.aspirate(self.plate["A1", "B1"], vols=[100, 100], use_channels=[4, 5])
well.tracker.set_liquids([(None, corrected_vol)])
await self.lh.aspirate(
self.plate["A1", "B1"],
vols=[corrected_vol] * 2,
use_channels=[4, 5],
**self.hlc.make_asp_kwargs(2),
)
self.STAR._write_and_read_command.assert_has_calls(
[
_any_write_and_read_command_call(
Expand Down Expand Up @@ -411,8 +426,9 @@ async def test_single_channel_aspiration(self):
assert self.plate.lid is not None
self.plate.lid.unassign()
well = self.plate.get_item("A1")
well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction
await self.lh.aspirate([well], vols=[100])
corrected_volume = self.hlc.compute_corrected_volume(100)
well.tracker.set_liquids([(None, corrected_volume)]) # liquid class correction
await self.lh.aspirate([well], vols=[corrected_volume], **self.hlc.make_asp_kwargs(1))
self.STAR._write_and_read_command.assert_has_calls(
[
_any_write_and_read_command_call(
Expand All @@ -432,8 +448,11 @@ async def test_single_channel_aspiration_liquid_height(self):
assert self.plate.lid is not None
self.plate.lid.unassign()
well = self.plate.get_item("A1")
well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction
await self.lh.aspirate([well], vols=[100], liquid_height=[10])
corrected_volume = self.hlc.compute_corrected_volume(100)
well.tracker.set_liquids([(None, corrected_volume)]) # liquid class correction
await self.lh.aspirate(
[well], vols=[corrected_volume], liquid_height=[10], **self.hlc.make_asp_kwargs(1)
)

# This passes the test, but is not the real command.
self.STAR._write_and_read_command.assert_has_calls(
Expand All @@ -455,9 +474,12 @@ async def test_multi_channel_aspiration(self):
assert self.plate.lid is not None
self.plate.lid.unassign()
wells = self.plate.get_items("A1:B1")
corrected_volume = self.hlc.compute_corrected_volume(100)
for well in wells:
well.tracker.set_liquids([(None, 100 * 1.072)]) # liquid class correction
await self.lh.aspirate(self.plate["A1:B1"], vols=[100] * 2)
well.tracker.set_liquids([(None, corrected_volume)]) # liquid class correction
await self.lh.aspirate(
self.plate["A1:B1"], vols=[corrected_volume] * 2, **self.hlc.make_asp_kwargs(2)
)

# This passes the test, but is not the real command.
self.STAR._write_and_read_command.assert_has_calls(
Expand All @@ -477,12 +499,14 @@ async def test_multi_channel_aspiration(self):

async def test_aspirate_single_resource(self):
self.lh.update_head_state({i: self.tip_rack.get_tip(i) for i in range(5)})
corrected_volume = self.hlc.compute_corrected_volume(10)
with no_volume_tracking():
await self.lh.aspirate(
[self.bb] * 5,
vols=[10] * 5,
vols=[corrected_volume] * 5,
use_channels=[0, 1, 2, 3, 4],
liquid_height=[1] * 5,
**self.hlc.make_asp_kwargs(5),
)
self.STAR._write_and_read_command.assert_has_calls(
[
Expand All @@ -506,14 +530,17 @@ async def test_aspirate_single_resource(self):

async def test_dispense_single_resource(self):
self.lh.update_head_state({i: self.tip_rack.get_tip(i) for i in range(5)})
hlc = StandardVolumeFilter_Water_DispenseJet_Empty
corrected_volume = hlc.compute_corrected_volume(10)
with no_volume_tracking():
await self.lh.dispense(
[self.bb] * 5,
vols=[10] * 5,
vols=[corrected_volume] * 5,
use_channels=[0, 1, 2, 3, 4],
liquid_height=[1] * 5,
blow_out=[True] * 5,
jet=[True] * 5,
**hlc.make_disp_kwargs(5),
)
self.STAR._write_and_read_command.assert_has_calls(
[
Expand All @@ -536,8 +563,16 @@ async def test_single_channel_dispense(self):
self.lh.update_head_state({0: self.tip_rack.get_tip("A1")})
assert self.plate.lid is not None
self.plate.lid.unassign()
hlc = StandardVolumeFilter_Water_DispenseJet_Empty
corrected_vol = hlc.compute_corrected_volume(100)
with no_volume_tracking():
await self.lh.dispense(self.plate["A1"], vols=[100], jet=[True], blow_out=[True])
await self.lh.dispense(
self.plate["A1"],
vols=[corrected_vol],
jet=[True],
blow_out=[True],
**hlc.make_disp_kwargs(1),
)
self.STAR._write_and_read_command.assert_has_calls(
[
_any_write_and_read_command_call(
Expand All @@ -548,15 +583,17 @@ async def test_single_channel_dispense(self):

async def test_multi_channel_dispense(self):
self.lh.update_head_state({0: self.tip_rack.get_tip("A1"), 1: self.tip_rack.get_tip("B1")})
# TODO: Hamilton liquid classes
assert self.plate.lid is not None
self.plate.lid.unassign()
hlc = StandardVolumeFilter_Water_DispenseJet_Empty
corrected_vol = hlc.compute_corrected_volume(100)
with no_volume_tracking():
await self.lh.dispense(
self.plate["A1:B1"],
vols=[100] * 2,
vols=[corrected_vol] * 2,
jet=[True] * 2,
blow_out=[True] * 2,
**hlc.make_disp_kwargs(2),
)

self.STAR._write_and_read_command.assert_has_calls(
Expand Down Expand Up @@ -605,10 +642,11 @@ async def test_core_96_aspirate(self):
await self.lh.pick_up_tips96(self.tip_rack2) # pick up high volume tips
self.STAR._write_and_read_command.reset_mock()

# TODO: Hamilton liquid classes
assert self.plate.lid is not None
self.plate.lid.unassign()
await self.lh.aspirate96(self.plate, volume=100, blow_out=True)
hlc = HighVolumeFilter_96COREHead1000ul_Water_DispenseSurface_Empty
corrected_volume = hlc.compute_corrected_volume(100)
await self.lh.aspirate96(self.plate, volume=corrected_volume, **hlc.make_asp96_kwargs())

# volume used to be 01072, but that was generated using a non-core liquid class.
self.STAR._write_and_read_command.assert_has_calls(
Expand All @@ -623,11 +661,15 @@ async def test_core_96_dispense(self):
await self.lh.pick_up_tips96(self.tip_rack2) # pick up high volume tips
if self.plate.lid is not None:
self.plate.lid.unassign()
await self.lh.aspirate96(self.plate, 100, blow_out=True) # aspirate first
hlc = HighVolumeFilter_96COREHead1000ul_Water_DispenseSurface_Empty
corrected_volume = hlc.compute_corrected_volume(100)
await self.lh.aspirate96(self.plate, corrected_volume, **hlc.make_asp96_kwargs())
self.STAR._write_and_read_command.reset_mock()

with no_volume_tracking():
await self.lh.dispense96(self.plate, 100, blow_out=True)
await self.lh.dispense96(
self.plate, corrected_volume, blow_out=True, **hlc.make_disp96_kwargs()
)

# volume used to be 01072, but that was generated using a non-core liquid class.
self.STAR._write_and_read_command.assert_has_calls(
Expand Down
Loading
Loading