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

feat(api): Flex Stacker Module Support for EVT #17300

Merged
merged 16 commits into from
Jan 17, 2025
Merged

feat(api): Flex Stacker Module Support for EVT #17300

merged 16 commits into from
Jan 17, 2025

Conversation

ahiuchingau
Copy link
Contributor

@ahiuchingau ahiuchingau commented Jan 17, 2025

Overview

Covers EXEC-967, EXEC-965, EXEC-946, EXEC-1078

This PR introduces the .store(), .retrieve(), and .load_labware_to_hopper(...) commands. These commands allow the declaration of labware inside the hopper, retrieval of a handleable labware core from the stacker, and storage of labware into the stacker.

Test Plan and Hands on Testing

  • The following protocol should pass app analysis and execute as expected on a Flex:
# Make sure apiLevel is high enough that this protocol will be run through Protocol Engine.
requirements = {"robotType": "Flex", "apiLevel": "2.23"}


def run(protocol):
    stacker_c = protocol.load_module("flexStackerModuleV1", "C4")
    stacker_d = protocol.load_module("flexStackerModuleV1", "D4")
    stacker_c.load_labware_to_hopper(
        "opentrons_flex_96_tiprack_1000ul", 2)
    stacker_d.load_labware_to_hopper(
        "corning_96_wellplate_360ul_flat", 2)

    """You can now load labware on the stacker slot as a staging slot."""
    stacker_c.enter_static_mode()
    tiprack_d = stacker_c.load_labware("opentrons_flex_96_tiprack_1000ul")
    protocol.move_labware(tiprack_d, "D3", use_gripper=True)
    stacker_c.exit_static_mode()
    
    tiprack = stacker_c.retrieve()
    protocol.move_labware(tiprack, "C2", use_gripper=True)

    plate = stacker_d.retrieve()
    protocol.move_labware(plate, "D2", use_gripper=True)

    protocol.move_labware(tiprack, stacker_c, use_gripper=True)
    stacker_c.store(tiprack)

    protocol.move_labware(plate, stacker_d, use_gripper=True)
    stacker_d.store(plate)

Changelog

Review requests

Outside of the known unintentional allowance of loading multiple stackers in the same slot (due to skipping the modules map on the deck conflict checker), this EVT version is meant to track labware through an internally managed labware list in the modules substate. Future versions may not reflect that practice.

Are there problems with the remaining approaches in this PR, whose purpose is to enable to use of the Flex stacker alongside all 12 base deck slots for nominal protocol operation?

Risk assessment

Low - Mostly introduction of new behavior with limited overlap to existing features. One existing risk is that in this EVT build multiple Flex Stackers can be loaded to the same slot due to the Stacker skipping the deck map conflict checker for the modules map.

@ahiuchingau ahiuchingau marked this pull request as ready for review January 17, 2025 17:41
@ahiuchingau ahiuchingau requested review from a team as code owners January 17, 2025 17:41
@ahiuchingau ahiuchingau added the gen-analyses-snapshot-pr Generate a healing PR if the analyses snapshot test fails label Jan 17, 2025
module_core: legacy_module_core.LegacyModuleCore,
load_name: str,
quantity: int,
label: str | None = None,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's have this (and the ones below) Optional[str] for consistency

Copy link
Contributor

A PR has been opened to address analyses snapshot changes. Please review the changes here: #17304

Copy link
Contributor

@CaseyBatten CaseyBatten left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks solid, added some notes below on places for future improvements to allow us to consolidate this down to the labware state store instead of spread across the module substate. A true labware stack would take quite a bit of rearchitecting elsewhere, but might be worthwhile in the long run.

Comment on lines +160 to +167
if (
self._is_loading_to_module(
params.location, ModuleModel.FLEX_STACKER_MODULE_V1
)
and not self._state_view.modules.get_flex_stacker_substate(
params.location.moduleId
).in_static_mode
):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good for EVT purposes, eventually we might want to de-coordinate these things if we're able to source the labware locations to a true stack in the tower.

Comment on lines +222 to +228
if self._is_loading_to_module(
params.location, ModuleModel.FLEX_STACKER_MODULE_V1
):
state_update.load_flex_stacker_hopper_labware(
module_id=params.location.moduleId,
labware_id=loaded_labware.labware_id,
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense for this build.

Comment on lines +44 to +47
new_labware_ids.append(lw_change.labware_id)
elif isinstance(lw_change, FlexStackerRetrieveLabware):
new_labware_ids.remove(lw_change.labware_id)
elif isinstance(lw_change, FlexStackerStoreLabware):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm cool with the way these lists work for this build. Eventually we probably want to transition to letting the labware state store manage this by having them be part of a location hierarchy/stack. This can come later though as theres more architecture that would need changing to allow that to work.

Comment on lines +1334 to +1336
# loaded to column 3 but the addressable area is in column 4
assert deck_slot.value[-1] == "3"
return f"flexStackerModuleV1{deck_slot.value[0]}4"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is fine for the time being, but eventually we'll want to rethink the architecture below this to allow the module to actually load into D4 instead of a fake D4.

Copy link
Contributor

@SyntaxColoring SyntaxColoring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm about halfway through and this all looks good so far, thanks. Just various small things.

api/src/opentrons/protocol_api/core/engine/module_core.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/core/engine/protocol.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/module_contexts.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/module_contexts.py Outdated Show resolved Hide resolved
api/src/opentrons/protocol_api/module_contexts.py Outdated Show resolved Hide resolved

@requires_version(2, 23)
def store(self, labware: Labware) -> None:
"""Store a labware at the bottom of the labware stack.

:param labware: The labware object to store.
"""
assert labware._core is not None
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this doing anything? It looks like labware._core is statically typechecked to never be None.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is so that user can call this function:

stacker.store(plate)

In a refactor, we will add the labware id as a store command param so we can make sure the labware has already been loaded on the stacker slot.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Right but I mean, is the assert statement on line 1200 doing anything?

@@ -901,7 +920,11 @@ def load_module(
)
)
if isinstance(deck_slot, StagingSlotName):
raise ValueError("Cannot load a module onto a staging slot.")
# flex stacker modules can only be loaded into staging slot inside a protocol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this the right layer to enforce this? We don't want it in the Protocol Engine loadModule command?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we had to do this because the flex stacker is the very first module that provides an addressable area (deck column 4) in a different slot than its cutout mount (column 3).

Technically we are still loading the module to column 3 of the deck in the engine (because the engine expects only deck slots). But to users, they wouldn't care which deck cutout the module is being loaded into. It would be weird have user load the module into column 3 in the protocol when the observable addressable area is in column 4.

We did this here so that we can keep the same engine behavior for the flex stacker for fast development. In a future refactor, we should use addressable areas as the load location on the context level, instead of physical deck slots.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm only tenuously following, but I can catch up on this later. It doesn't seem like anything that should block this PR. Thanks for explaining!

Copy link
Member

@sfoster1 sfoster1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like a great first pass!

@@ -700,8 +700,17 @@ class FlexStackerCore(ModuleCore, AbstractFlexStackerCore):

_sync_module_hardware: SynchronousAdapter[hw_modules.FlexStacker]

def set_static_mode(self, static: bool) -> None:
"""Set the Flex Stacker's static mode."""
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what's a static mode?

Comment on lines +168 to +170
# labware loaded to the flex stacker hopper is considered offdeck. This is
# a temporary solution until the hopper can be represented as non-addressable
# addressable area in the deck configuration.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

non-addressable addressable area in the deck configuration

Is that the direction we're headed? I thought we wanted something that's like OFF_DECK but not OFF_DECK?

Copy link
Contributor

@SyntaxColoring SyntaxColoring left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you!

Comment on lines 1177 to 1178
labware_core = self._protocol_core.get_labware_on_module(self._core)
assert labware_core is not None, "Retrieve failed to return labware"
Copy link
Contributor

@SyntaxColoring SyntaxColoring Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aah, so any time this assert would have raised, you're expecting the self._core.retrieve() call a couple lines above to have raised, anyway?

Okay, that makes sense, and no, I don't think you need to change this assert in that case. I would probably leave a comment explaining this, though, or maybe elaborate on it in the assert message.

@@ -901,7 +920,11 @@ def load_module(
)
)
if isinstance(deck_slot, StagingSlotName):
raise ValueError("Cannot load a module onto a staging slot.")
# flex stacker modules can only be loaded into staging slot inside a protocol
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, I'm only tenuously following, but I can catch up on this later. It doesn't seem like anything that should block this PR. Thanks for explaining!

@ahiuchingau ahiuchingau merged commit 35686bc into edge Jan 17, 2025
36 of 38 checks passed
@ahiuchingau ahiuchingau deleted the EXEC-1078 branch January 17, 2025 21:26
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
gen-analyses-snapshot-pr Generate a healing PR if the analyses snapshot test fails
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants