Skip to content

feat(api): store labware objects in the stacker #18010

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 2 commits into
base: edge
Choose a base branch
from

Conversation

sfoster1
Copy link
Member

@sfoster1 sfoster1 commented Apr 8, 2025

We had this beautiful dream: the stacker wouldn't store labware. Instead, the stacker would know what kind of labware it should store, and how many of those labware it is storing, and objects would be created when you retrieved stuff. The problem with that is in the physical world there are labware objects in the stacker, and if we're not modeling them you can't know the state of those physical objects. So we have to reverse this decision and store the identity of the labware in the stacker.

This PR is the set of changes to accomplish that in the engine and account for it in the python API; the ts bindings; and at least somewhat in the app interface that already exists. It won't add any new API or app stuff.

The way that this works is:

  • The stacker contains a list of labware IDs that it contains
  • While a labware is in the stacker, its position in a stack (i.e. lid on labware, labware on adapter) is maintained but the stack as a whole is InStackerHopper(moduleId)
  • Labware must exist to go in to the stacker. That means that setStoredLabware and fill now handle making sure all the labware exists. They take lists of ids for PD compliance and also so that you can predefine labware if you want. They also still have a count argument. You can
    • Say a number of labware with count, and the command will create that many labware with arbitrary IDs
    • Not set count, and pass a list of IDs that don't currently exist, and the command will create labware with those IDs
    • Not set count, and pass a list of IDs that do currently exist, and the command will move those labware in
      • Those labware must be in OFF_DECK or there's an error
  • Labware is moved out of the stacker, so retrieve doesn't create labware anymore
  • When you remove labware with empty, it's moved to OFF_DECK

fill, setStoredLabware, and empty all now specify lots of labware location sequences and ids in their results because they create or move labware where they previously do not. When fill and setStoredLabware create labware, they gloss those labware as having an original position descending to SYSTEM. Otherwise, they're the real original locations.

to come out of draft

  • Fix the tests that already exist
  • Write new tests to cover extra verification of labware state for the whole new-by-count/new-by-id/existing-id thing
  • Write new tests to make sure we do locations appropriately
  • Test on hardware
  • Make commit names that aren't so silly that's what the squash is for

to merge

  • make a pr that implements this in the python api so it's testable

review requests

in addition to the standard stuff,

  • Do you feel like too much/too little/just enough is in stacker common
  • Is this an API the app can work with

@sfoster1 sfoster1 force-pushed the exec-1394-restore-labware-ids-to-stacker-state branch 10 times, most recently from 3bac787 to d116172 Compare April 10, 2025 21:21
@sfoster1 sfoster1 marked this pull request as ready for review April 10, 2025 21:22
@sfoster1 sfoster1 requested a review from a team as a code owner April 10, 2025 21:22
Copy link

codecov bot commented Apr 10, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 61.85%. Comparing base (ff9e95b) to head (1ea73fb).
Report is 9 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             edge   #18010      +/-   ##
==========================================
- Coverage   61.87%   61.85%   -0.02%     
==========================================
  Files        2989     2996       +7     
  Lines      232727   234328    +1601     
  Branches    20112    20421     +309     
==========================================
+ Hits       143993   144949     +956     
- Misses      88557    89202     +645     
  Partials      177      177              
Flag Coverage Δ
protocol-designer 19.17% <ø> (+0.31%) ⬆️
step-generation 4.37% <ø> (-0.04%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

see 55 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

labware: LoadedLabware,
) -> None:
definitions_by_id[labware.id] = definition
display_names_by_id[labware.id] = None
Copy link
Contributor

Choose a reason for hiding this comment

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

should this be None? we can get int from the definition?

Copy link
Member Author

Choose a reason for hiding this comment

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

This field is just for the user-specified display name, if any - the label argument to ProtocolContext.load_labware. At least that's how it is in the load labware engine command

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.

Refactor and new features look really great overall, thanks Seth! Stacker common is a bit massive now, but I think it makes sense for the logic to live there as opposed to split between retrieve/unsafe retrieve and store as it was. Logistical question, do we want to wait on this until the PAPI PR is up and merge them into eachother first before merging this to edge?

The structure for Labware group and location sequencing logic, as well as the changes to Store and Retrieve to track the contained labware groups make sense. Some notes below on some things I wanted to verify and also where we might need to change the app end logic since retrieve isn't the "loader" for labware anymore.

return group.lid.locationSequence


def _check_one_preloaded_labware_known(
Copy link
Contributor

Choose a reason for hiding this comment

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

This all seems like solid validation, should be zero cases of mixed state labware groups anyways so nice!

return False


def _check_one_preloaded_labware( # noqa: C901
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, but just wanted to verify one thing regarding lids. We are validating that lids must be on a labware location, but not that solo lids must be off deck. Are we fully planning to not distribute lids/lid stacks out of the stacker? We can always make that change later, not blocking.

Copy link
Member Author

Choose a reason for hiding this comment

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

Oh that's sort of a nightmare. I think we should leave that to a followup. I guess we could implement it in one of the following ways:

  1. in a stacker, the primary labware is a lid stack system object (0 height), each of which contains one lid. downside: can only retrieve one lid at a time. upside: follows our current system all the way through
  2. in a stacker, lids can be primary labware, so the stacker contains many StackerStoredLabwareGroups with one lid as the primary. downside: can only retrieve one lid at a time, doesn't follow our current pattern. upside: no synthetic labware
  3. we make a new kind of StackedStoredLabwareGroup or enhance the current one to allow for multiple lids in some way. downside: big change, upside: follows our current pattern and can retrieve more than one lid

Copy link
Contributor

Choose a reason for hiding this comment

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

(2) sounds the easiest to implement but with restrictions
(3) is extendable and will follow our retrieve guidelines

Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely leaning approach 3, given users will want to move lids in "stacks". From a functional standpoint though, they'd have to move each lid off their stack to its destination one at a time, or rebuild the stack piecemeal elsewhere on the deck lest they leave their stacker unusable until all labware is consumed from the stack. The other option would be giving them some way of moving the whole stack at once (not a fan).

Comment on lines +604 to +608
.set_batch_loaded_labware(
definitions_by_id=definitions_by_id,
offset_ids_by_id=offset_ids_by_id,
display_names_by_id=display_names_by_id,
new_locations_by_id=new_locations_by_id,
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 as is, but last time I was modifying how we split out some of the batch loaded labware behavior I was wondering if it would be better to just send a set of LoadedLabware objects right down the line through state update all the way to the labware store. No need to gut it now, but these various dicts just do the "breaking up" work here instead of down at the handler in the end, so we could in theory save all this for that end step. I think the only reason I didn't at the time is that the new_location_by_id dict had to exist for location sequences so I left it all be.

Copy link
Member Author

Choose a reason for hiding this comment

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

yeah. we have a soft preference to have the data in state updates and results be native data types, but I think that's just people pattern matching - don't think there's a big reason.

self._state_view,
[
NotOnDeckLocationSequenceComponent(
logicalLocationName=SYSTEM_LOCATION
Copy link
Contributor

Choose a reason for hiding this comment

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

Excellent, this aligns with the way in which we spawn new lid stack objects onto the deck during a move lid to empty deck slot command.

lid_def,
)
.update_flex_stacker_labware_pool_count(params.moduleId, count)
state_update, new_contained_labware = await build_or_assign_labware_to_hopper(
Copy link
Contributor

Choose a reason for hiding this comment

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

We will need to update the client and our starting deck state logic to be aware that this is our new "source" of a load labware type of action instead of retrieve.

]


def build_retrieve_labware_move_updates(
Copy link
Contributor

Choose a reason for hiding this comment

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

can you explain the method name?

Copy link
Member Author

Choose a reason for hiding this comment

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

build (retrieve labware) (move updates)

..., description="The number of labware loaded into the stacker labware pool."
..., description="The number of labware now stored in the hopper."
)
originalPrimaryLabwareLocationSequences: (
Copy link
Contributor

Choose a reason for hiding this comment

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

I might be missing something but why do we need this prop in setStoredLabware command?

Copy link
Member Author

Choose a reason for hiding this comment

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

It lets clients tell the engine to build its own labware without specifying all the ids, which the PAPI will want to do.

@sfoster1
Copy link
Member Author

Logistical question, do we want to wait on this until the PAPI PR is up and merge them into eachother first before merging this to edge?

I think we want to wait on this until the PAPI PR is up so that we can actually test it, but I'd prefer to merge them independently so there isn't just a(n even more) giant lump of a commit in history for ever

@sfoster1 sfoster1 force-pushed the exec-1394-restore-labware-ids-to-stacker-state branch 3 times, most recently from 2801588 to ea1ac45 Compare April 14, 2025 21:42
@sfoster1 sfoster1 requested a review from a team as a code owner April 14, 2025 21:42
@sfoster1 sfoster1 force-pushed the exec-1394-restore-labware-ids-to-stacker-state branch 2 times, most recently from 6b24dbc to 5fc54ed Compare April 15, 2025 16:49
@sfoster1 sfoster1 force-pushed the exec-1394-restore-labware-ids-to-stacker-state branch from 5fc54ed to 1ea73fb Compare April 15, 2025 17:17
group,
self._state_view,
[
NotOnDeckLocationSequenceComponent(
Copy link
Contributor

Choose a reason for hiding this comment

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

nice!

module_id=params.moduleId, count=stacker_state.pool_count + 1
state_update.update_flex_stacker_contained_labware(
module_id=params.moduleId,
contained_labware_bottom_first=(
Copy link
Contributor

Choose a reason for hiding this comment

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

beautiful ;-)

Copy link
Contributor

@TamarZanzouri TamarZanzouri left a comment

Choose a reason for hiding this comment

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

a lot to read but its perfect. nice job! once ci passes of course :-)

return False


def _check_one_preloaded_labware( # noqa: C901
Copy link
Contributor

Choose a reason for hiding this comment

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

(2) sounds the easiest to implement but with restrictions
(3) is extendable and will follow our retrieve guidelines

Copy link
Contributor

@ahiuchingau ahiuchingau 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 for tackling this – it is a lot of work!!

All of the labware-handling logic makes sense to me. I can help test on hardware tomorrow if no one has gotten to it yet.

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.

The test cases look good, the stacker common tests are very extensive and should give good resilience. Thank you for addressing my former comments as well, I especially think it makes sense to leave "independent lids" alone for now however I favor approach 3 from your suggestions.

A dream can still be beautiful, even if it is different!

return False


def _check_one_preloaded_labware( # noqa: C901
Copy link
Contributor

Choose a reason for hiding this comment

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

Definitely leaning approach 3, given users will want to move lids in "stacks". From a functional standpoint though, they'd have to move each lid off their stack to its destination one at a time, or rebuild the stack piecemeal elsewhere on the deck lest they leave their stacker unusable until all labware is consumed from the stack. The other option would be giving them some way of moving the whole stack at once (not a fan).

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.

4 participants