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, shared-data): Expand Labware architecture to accommodate Lids #17072

Open
wants to merge 12 commits into
base: edge
Choose a base branch
from

Conversation

CaseyBatten
Copy link
Contributor

@CaseyBatten CaseyBatten commented Dec 10, 2024

Overview

Covers EXEC-1000, EXEC-1001, EXEC-1002, EXEC-1004

This PR seeks to remove Lids as a handled labware concept during a protocol, and treat them more as an attribute of a labware rather than a primary labware themselves.

TODO:

  • Add Unit Tests

Test Plan and Hands on Testing

(Assuming Max Supported Version is 2,22)

  • Existing TC Lid protocol PASSES analysis and executes without issue
  • load_lid_stack command causes analysis failure
  • load_labware with lid parameter causes analysis failure
    (Assuming Max Supported Version bumped to 2,23)
  • Existing TC Lid protocol FAILS analysis
  • load_lid_stack command loads a stack of X lids at given location, passes analysis
  • load_labware command while utilizing the lid parameter results in a loaded labware and lid, passes analysis

Changelog

  • Expanded the Labware Schema to account for stackLimit, isDeckSlotCompatible, and compatibleParentLabware fields to allow for more accurate representation of lid behavior.
  • Expanded load_labware PAPI commands to accept optional lid field
  • Introduced load_lid_stack command to allow for the loading of lids in batches
  • Engine and state update expansion to account for changes in Lid Labware behavior
  • Addition of new Protocol engine "Lid Stack" Labware definition to create a handleable object to be returned to the user. In the follow up PR to introduce move lid we will be adding a method to "deallocate" these "Labwares" when they're "done"

Review requests

  • Do the command results we're returning make sense? Is there something more the client app will need?
  • We don't want to return a handleable labware PAPI context for lids to users ever, so I am not adding them to the core_map upon load, would this have any problematic knock on effects?

Risk assessment

Medium - we will be gating the behavior behind an API version so that users working with TC lids in existing protocols on existing API versions are unaffected, but future protocol drafting and labware behavior will be effected.

@CaseyBatten CaseyBatten requested a review from vegano1 December 10, 2024 16:49
Copy link
Contributor

@jbleon95 jbleon95 left a comment

Choose a reason for hiding this comment

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

I still need to get through more of the protocol engine code, but I already have some comments and suggestions for change

:param str load_name: A string to use for looking up a lid definition.
You can find the ``load_name`` for any standard lid on the Opentrons
`Labware Library <https://labware.opentrons.com>`_.
:param location: ither a :ref:`deck slot <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.

Suggested change
:param location: ither a :ref:`deck slot <deck-slots>`,
:param location: Either a :ref:`deck slot <deck-slots>`,

def load_lid_stack(
self,
load_name: str,
location: Union[DeckLocation, Labware],
Copy link
Contributor

Choose a reason for hiding this comment

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

I feel like this load_lid_stack in ProtocolContext should only be responsible for loading on a DeckLocation, and there should be a separate load_lid_stack in the Labware context, which fits with our existing pattern of loading adapters and labware onto an existing labware

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Hmm you're right that there should be one available for the labware context as well (to do things like adapter.load_lid_stack(load_name, quantity)), however in fitting with our current structure for load_labware, where we allow loading labware and it's adapter simultaneously, I would actually propose adding an optional adapter field to this new command so that we more closely mirror the setup of load_labware's current cross-context behavior.

Copy link
Contributor

Choose a reason for hiding this comment

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

The main difference there with the optional adapter field is it's also loading/creating the adapter along with the labware, not placing an existing adapter object on the labware. It's the reason why I have no issue with the optional lid field added to load_labware and the like. I'm not sure I'd like putting an optional labware name string here instead and loading that, than the lid stack on top of that, but it'd be closer to that pattern we've established

quantity: int,
namespace: Optional[str] = None,
version: Optional[int] = None,
) -> Union[DeckLocation, Labware]:
Copy link
Contributor

Choose a reason for hiding this comment

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

Is there a reason why we are returning the location when it's already one of the arguments? This also is going against our pattern of returning the object being loaded, I'd rather just return nothing if we're not representing lids as Python object in the protocol.

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 one of the items currently in debate. Seth had an interesting proposal that the "Lid Stack" item should be a literal flat empty labware object that we return, which constitutes the "bottom" of the stack and is what will get handled when determining sources and destinations for lid stacks. The current approach is based on the idea that stacks are really just living dictionaries of "stacks" of lids around the deck, keyed by location. In theory both of these approaches get the same end result, however what the user handles in their protocol changes slightly between an Abstract "Labware" like object and a literal "Location" they'd refer to as a general alias for their stack. I'll be exploring what the former implementation idea would look like, though deallocating them or dealing with "empty" stacks might be a little awkward in places.

"""
load_location: Union[DeckSlotName, StagingSlotName, Labware]
if isinstance(location, Labware):
load_location = location
Copy link
Contributor

Choose a reason for hiding this comment

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

if we are going to be passing a labware here (see my above comment for why I think we shouldn't), it should be a labware core

quantity=params.quantity,
labware_ids=None,
)
for i in range(params.quantity):
Copy link
Contributor

Choose a reason for hiding this comment

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

this whole loop is confusing to read I feel like this whole thing can be rewritten as a sequence of list comprehensions, which might be easier to comprehend (pun semi-intended). As it is it's a lot of index manipulation on very similarly named lists and sets in a single block

)

if labware_ids is None:
labware_ids = []
Copy link
Contributor

Choose a reason for hiding this comment

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

Easy list comprehension rewrite

version=version,
)

if definition.stackLimit is not None and quantity > definition.stackLimit:
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we not have a default stack limit?


# Allow propagation of ModuleNotLoadedError.
load_labware_data_list = []
for i in range(quantity):
Copy link
Contributor

Choose a reason for hiding this comment

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

Again, I dislike the pattern of using for i in range(...) and special logic for a specific index number, since most of them can be rewritten as list comprehensions. This one for instance could easily be (after checking the initial location)

locations = [location]
locations.extend([OnLabwareLocation(labwareId=labware_id) for labware_id in labware_Ids)
load_labware_data_list = [LoadedLabwareData(....) for location in locations]

Something like this

def _add_loaded_lid_stack(self, state_update: update_types.StateUpdate) -> None:
loaded_lid_stack_update = state_update.loaded_lid_stack
if loaded_lid_stack_update != update_types.NO_CHANGE:
for i in range(len(loaded_lid_stack_update.labware_ids)):
Copy link
Contributor

Choose a reason for hiding this comment

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

Suggested change
for i in range(len(loaded_lid_stack_update.labware_ids)):
for labware_id in loaded_lid_stack_update.labware_ids:

@CaseyBatten CaseyBatten requested a review from a team as a code owner December 13, 2024 20:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants