Skip to content

Commit 60df23c

Browse files
authored
feat(api): allow lids on h/s to be moved when h/s is latched (#18891)
Helps with EXEC-1647 # Overview It's been observed that moving lids with a gripper (specifically the Greiner labware lids) to labware on H/S is more reliable when the labware itself is latched and doesn't have any room to wiggle on the H/S- specifically when placed on a universal adapter. So we should allow lids to be placed on H/S labware with the labware latched. ## Risk assessment Low- medium. This change will allow any labware categorized as a lid to be placed on H/S (either directly or on a stack of adpater/labware(s)) without doing a labware latch check. This puts the onus on the user to make sure to leave the latch closed for the lids. Which is not too much of a responsibility and has no dire consequences in reality even if the latch stays open. We also plan on testing whether we need to add any other restrictions to improve the user experience.
1 parent 41c97ad commit 60df23c

File tree

4 files changed

+75
-4
lines changed

4 files changed

+75
-4
lines changed

api/src/opentrons/protocol_engine/execution/labware_movement.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -277,9 +277,12 @@ async def ensure_movement_not_obstructed_by_module(
277277
await self._tc_movement_flagger.ensure_labware_in_open_thermocycler(
278278
labware_parent=parent
279279
)
280-
await self._hs_movement_flagger.raise_if_labware_latched_on_heater_shaker(
281-
labware_parent=parent
282-
)
280+
if not self._state_store.labware.is_lid(labware_id):
281+
# Lid placement is actually improved by holding the labware latched on the H/S
282+
# So, we skip this check for lids.
283+
await self._hs_movement_flagger.raise_if_labware_latched_on_heater_shaker(
284+
labware_parent=parent
285+
)
283286
except ThermocyclerNotOpenError:
284287
raise LabwareMovementNotAllowedError(
285288
"Cannot move labware to or from a Thermocycler with its lid closed."

api/src/opentrons/protocol_engine/state/labware.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1029,6 +1029,10 @@ def is_absorbance_reader_lid(self, labware_id: str) -> bool:
10291029
self.get(labware_id).loadName
10301030
)
10311031

1032+
def is_lid(self, labware_id: str) -> bool:
1033+
"""Check if labware is a lid."""
1034+
return LabwareRole.lid in self.get_definition(labware_id).allowedRoles
1035+
10321036
def raise_if_labware_inaccessible_by_pipette(self, labware_id: str) -> None:
10331037
"""Raise an error if the specified location cannot be reached via a pipette."""
10341038
labware = self.get(labware_id)

api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ async def test_ensure_movement_obstructed_by_thermocycler_raises(
557557
labware_parent=ModuleLocation(moduleId="a-thermocycler-id")
558558
)
559559
).then_raise(ThermocyclerNotOpenError("Thou shall not pass!"))
560-
560+
decoy.when(state_store.labware.is_lid(labware_id="labware-id")).then_return(False)
561561
with pytest.raises(LabwareMovementNotAllowedError):
562562
await subject.ensure_movement_not_obstructed_by_module(
563563
labware_id="labware-id", new_location=to_loc
@@ -573,6 +573,7 @@ async def test_ensure_movement_not_obstructed_by_modules(
573573
decoy.when(
574574
state_store.labware.get_parent_location(labware_id="labware-id")
575575
).then_return(ModuleLocation(moduleId="a-rando-module-id"))
576+
decoy.when(state_store.labware.is_lid(labware_id="labware-id")).then_return(False)
576577
await subject.ensure_movement_not_obstructed_by_module(
577578
labware_id="labware-id",
578579
new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
@@ -604,6 +605,7 @@ async def test_ensure_movement_obstructed_by_heater_shaker_raises(
604605
decoy.when(
605606
state_store.labware.get_parent_location(labware_id="labware-id")
606607
).then_return(from_loc)
608+
decoy.when(state_store.labware.is_lid(labware_id="labware-id")).then_return(False)
607609
decoy.when(
608610
await heater_shaker_movement_flagger.raise_if_labware_latched_on_heater_shaker(
609611
labware_parent=ModuleLocation(moduleId="a-heater-shaker-id")
@@ -629,3 +631,39 @@ async def test_ensure_movement_not_obstructed_does_not_raise_for_slot_locations(
629631
labware_id="labware-id",
630632
new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
631633
)
634+
635+
636+
@pytest.mark.parametrize(
637+
argnames=["from_loc", "to_loc"],
638+
argvalues=[
639+
(
640+
ModuleLocation(moduleId="a-heater-shaker-id"),
641+
DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
642+
),
643+
(
644+
DeckSlotLocation(slotName=DeckSlotName.SLOT_3),
645+
ModuleLocation(moduleId="a-heater-shaker-id"),
646+
),
647+
],
648+
)
649+
async def test_ensure_movement_not_obstructed_does_not_raise_for_lids_on_hs(
650+
decoy: Decoy,
651+
subject: LabwareMovementHandler,
652+
heater_shaker_movement_flagger: HeaterShakerMovementFlagger,
653+
state_store: StateStore,
654+
from_loc: NonStackedLocation,
655+
to_loc: LabwareLocation,
656+
) -> None:
657+
"""It should not raise any errors when moving lid from a latched H/S."""
658+
decoy.when(
659+
state_store.labware.get_parent_location(labware_id="labware-id")
660+
).then_return(from_loc)
661+
decoy.when(state_store.labware.is_lid(labware_id="labware-id")).then_return(True)
662+
decoy.when(
663+
await heater_shaker_movement_flagger.raise_if_labware_latched_on_heater_shaker(
664+
labware_parent=ModuleLocation(moduleId="a-heater-shaker-id")
665+
)
666+
).then_raise(HeaterShakerLabwareLatchNotOpenError("Thou shall not take!"))
667+
await subject.ensure_movement_not_obstructed_by_module(
668+
labware_id="labware-id", new_location=to_loc
669+
)

api/tests/opentrons/protocol_engine/state/test_labware_view_old.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,14 @@
108108
offsetId=None,
109109
)
110110

111+
tiprack_lid = LoadedLabware(
112+
id="tiprack-lid-id",
113+
loadName="tiprack-lid-load-name",
114+
location=DeckSlotLocation(slotName=DeckSlotName.SLOT_1),
115+
definitionUri="some-tiprack-lid-uri",
116+
offsetId=None,
117+
)
118+
111119

112120
def get_labware_view(
113121
labware_by_id: Optional[Dict[str, LoadedLabware]] = None,
@@ -722,6 +730,24 @@ def test_is_tiprack(
722730
assert subject.is_tiprack(labware_id="reservoir-id") is False
723731

724732

733+
def test_is_lid(
734+
reservoir_def: LabwareDefinition, tiprack_lid_def: LabwareDefinition
735+
) -> None:
736+
"""It should return True if labware is a lid."""
737+
subject = get_labware_view(
738+
labware_by_id={
739+
"reservoir-id": reservoir,
740+
"tiprack-lid-id": tiprack_lid,
741+
},
742+
definitions_by_uri={
743+
"some-reservoir-uri": reservoir_def,
744+
"some-tiprack-lid-uri": tiprack_lid_def,
745+
},
746+
)
747+
assert subject.is_lid(labware_id="reservoir-id") is False
748+
assert subject.is_lid(labware_id="tiprack-lid-id") is True
749+
750+
725751
def test_get_load_name(reservoir_def: LabwareDefinition) -> None:
726752
"""It should return the load name."""
727753
subject = get_labware_view(

0 commit comments

Comments
 (0)