From 37a4cb11ffa275cf63ceb7040fce41bdde976f49 Mon Sep 17 00:00:00 2001 From: HaamsRee <75466389+HaamsRee@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:02:50 -0500 Subject: [PATCH 1/2] Fix STAR setup discard of mounted tips --- .../backends/hamilton/STAR_backend.py | 51 ++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py index 97ce7093dfd..9ee967b564d 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_backend.py @@ -2095,8 +2095,19 @@ async def set_up_pip(): # Skip pip-channel I/O; the __init__ defaults stand in. # TODO: does not yet gate request_tip_presence or instrument-init moves. return - if not initialized or any(tip_presences): + has_tips = any(tip_presences) + if has_tips: + await self._initialize_pip_without_tip_discard() + elif not initialized: await self.initialize_pip() + if has_tips: + await self._discard_existing_tips_to_waste(tip_presences) + tip_presences_after_discard = await self.request_tip_presence() + if any(tip_presences_after_discard): + raise RuntimeError( + "Failed to discard tips during backend initialization: " + f"{tip_presences_after_discard}" + ) self._channels_minimum_y_spacing = await self.channels_request_y_minimum_spacing() # VW is not supported on firmware from 2016 or older (see issue #1004). Skip the @@ -6301,6 +6312,44 @@ async def initialize_pip(self): discarding_method=0, ) + async def _initialize_pip_without_tip_discard(self) -> None: + """Initialize pip channels without using the DI command to discard mounted tips.""" + dy = (4050 - 2175) // (self.num_channels - 1) + y_positions = [4050 - i * dy for i in range(self.num_channels)] + + await self.initialize_pipetting_channels( + x_positions=[ + int(self.extended_conf.tip_waste_x_position * 10) + ], # Tip eject waste X position. + y_positions=y_positions, + begin_of_tip_deposit_process=int(self._channel_traversal_height * 10), + end_of_tip_deposit_process=1220, + z_position_at_end_of_a_command=3600, + tip_pattern=[False] * self.num_channels, + tip_type=4, # TODO: get from tip types + discarding_method=0, + ) + + async def _discard_existing_tips_to_waste(self, tip_presences: List[Optional[bool]]) -> None: + """Discard tips found on channels during backend initialization.""" + dy = (4050 - 2175) // (self.num_channels - 1) + y_positions = [4050 - i * dy for i in range(self.num_channels)] + x_position = int(self.extended_conf.tip_waste_x_position * 10) + + trash = self.deck.get_trash_area() + max_z = trash.get_location_wrt(self.deck).z + + await self.discard_tip( + x_positions=[x_position] * self.num_channels, + y_positions=y_positions, + tip_pattern=[bool(tip_present) for tip_present in tip_presences], + begin_tip_deposit_process=round((max_z + 59.9) * 10), + end_tip_deposit_process=round((max_z + 49.9) * 10), + minimum_traverse_height_at_beginning_of_a_command=int(self._channel_traversal_height * 10), + z_position_at_end_of_a_command=int(self._channel_traversal_height * 10), + discarding_method=TipDropMethod.PLACE_SHIFT, + ) + async def initialize_pipetting_channels( self, x_positions: List[int] = [0], From 3c0d116701ac921b2916565295e753245e25521f Mon Sep 17 00:00:00 2001 From: HaamsRee <75466389+HaamsRee@users.noreply.github.com> Date: Wed, 8 Jul 2026 15:54:29 -0500 Subject: [PATCH 2/2] Test STAR setup discard commands --- .../backends/hamilton/STAR_tests.py | 61 +++++++++++++++++++ 1 file changed, 61 insertions(+) diff --git a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py index 78574f81406..d06f040e2f1 100644 --- a/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py +++ b/pylabrobot/liquid_handling/backends/hamilton/STAR_tests.py @@ -2227,6 +2227,67 @@ async def test_1000uL_tips(self): tip_rack.unassign() +class TestSTARSetupTipDiscard(unittest.IsolatedAsyncioTestCase): + """Tests for setup-time handling of tips already mounted on STAR pip channels.""" + + def _make_backend(self, num_channels: int = 4) -> STARBackend: + backend = STARBackend(read_timeout=1) + backend._num_channels = num_channels + backend._extended_conf = _DEFAULT_EXTENDED_CONFIGURATION + backend._iswap_parked = True + # The setup-time discard helper derives the TR Z range from the deck trash resource. + backend.set_deck(STARLetDeck()) + backend._write_and_read_command = unittest.mock.AsyncMock(return_value="C0DIid0001er00/00") + return backend + + async def test_initialize_pip_keeps_existing_active_tip_pattern(self): + backend = self._make_backend() + + await backend.initialize_pip() + + # The public initialization wrapper should keep its previous behavior for normal init. + backend._write_and_read_command.assert_has_calls( + [ + _any_write_and_read_command_call( + "C0DIid0001xp08000&yp4050 3425 2800 2175tp2450tz1220te3600tm1 1 1 1tt04ti0" + ) + ] + ) + + async def test_initialize_pip_without_tip_discard_uses_inactive_tip_pattern(self): + backend = self._make_backend() + + await backend._initialize_pip_without_tip_discard() + + # Mounted-tip setup uses DI for initialization only: tm0 prevents DI from discarding tips. + backend._write_and_read_command.assert_has_calls( + [ + _any_write_and_read_command_call( + "C0DIid0001xp08000&yp4050 3425 2800 2175tp2450tz1220te3600tm0 0 0 0tt04ti0" + ) + ] + ) + + async def test_discard_existing_tips_to_waste_uses_tr_with_tip_presence_pattern(self): + backend = self._make_backend() + backend._write_and_read_command.return_value = ( + "C0TRid0001er00/00kz000 000 000 000vz000 000 000 000" + ) + + await backend._discard_existing_tips_to_waste([True, False, True, False]) + + # Actual setup-time tip disposal happens through TR and preserves the mounted-tip pattern. + # The tp/tz values come from the same trash-height formula used by normal drop_tips(). + backend._write_and_read_command.assert_has_calls( + [ + _any_write_and_read_command_call( + "C0TRid0001xp08000 08000 08000 08000yp4050 3425 2800 2175" + "tm1 0 1 0tp1970tz1870th2450te2450ti0" + ) + ] + ) + + class TestChannelsMinimumYSpacing(unittest.IsolatedAsyncioTestCase): """Test that different channel spacing configurations produce different behavior.