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): build out well state for liquid height tracking #15681

Merged
merged 18 commits into from
Sep 6, 2024

Conversation

aaron-kulkarni
Copy link
Contributor

@aaron-kulkarni aaron-kulkarni commented Jul 16, 2024

Overview

re EXEC-603

This PR adds a new WellState class. This class currently has only one attribute, measured_liquid_heights, which is used in liquid height tracking. We created this new class(rather than just adding this attribute to labware state or something related), to allow for more detailed well state tracking in the future.

Details

measured_liquid_heights contains a Dict[labware_id, Dict[well_name, LiquidHeightInfo]]. This dictionary structure is meant to give time-efficient access to all wells of a particular labware, since the dictionary is primarily indexed by labware_id.

The presence/non-presence of a well in the WellState class indicates the following:

  • If a well is not present: that well has never been probed.
  • If a well is present but has a height value of 0: the most recent probe into that well found no liquid.
  • If a well is present and has a height value of > 0: the most recent probe into that well found liquid.

A quick word on the implementation of the new LiquidHeightInfo type. It has two attributes, height and last_measured. The last_measured attribute currently stores as a datetime, but it could be useful in the future to consider other alternatives. Maybe protocol steps?(or something similar). The reason we have a time-related attribute at all is because right now we don't track things like aspirates and dispenses, or even user inputted liquid loading(if that's the correct term). All liquid height state is determined only by doing a liquid_probe, so it's useful to know what the last time was we did a liquid_probe on this well.

For the purposes of the StateSummary, a new type was defined called LiquidHeightSummary. It has identifying information about the labware/well and the contents of the LiquidHeightInfo type.

Test Plan

Unit tests written and passed for well_view and well_store.

Changelog

  • Created WellState class, along with complementary WellView and WellStore
  • Created LiquidHeightInfo type, which is used to store info about recent liquid height measurements.
  • Created LiquidHeightSummary type, which is used in StateSummary.

Review requests

Risk assessment

Low.

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.

I think the structure of this is nice and we'll definitely want a WellStore, but I think we should put some thought in - and write that thought down - about the structure of the information within. I think we can write that down in the description, but honestly the description should be the longest part of this PR in that case. We should cover

  • How do we track the relationship of these Well objects to the labware that contains them?
  • What does the presence or absence of a Well in the wells data structure mean? What's it based on?
  • How do we indicate that a well has never been probed for the presence of liquid? Probed, and no liquid found? Probed, and liquid found? Probed, and liquid found, but known to have been refilled?
  • In general, what information should a human looking at a serialization of the Wells data structure be able to come to?

if isinstance(action, FailCommandAction):
self._handle_failed_command(action)

def _handle_succeded_command(self, command: Command) -> None:
Copy link
Member

Choose a reason for hiding this comment

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

Suggested change
def _handle_succeded_command(self, command: Command) -> None:
def _handle_succeeded_command(self, command: Command) -> None:

well = WellIdentifier(
labware_id=command.params.labwareId, well_name=command.params.wellName
)
if command.result is None:
Copy link
Member

Choose a reason for hiding this comment

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

does that make sense? Under what conditions would the command result 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.

Agreed it doesn't make sense. I initially did that to pass lint checks but I found a cleaner way around it.

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.

Looking good, but let's remove that commented out code and make sure the well identification information is in the summary too. Also maybe a format pass on the description?

class WellState:
"""State of all wells."""

# Dict[Labware: Dict[Wellname: [Height,TimeRecorded]]]
Copy link
Member

Choose a reason for hiding this comment

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

Remove commented out code

@@ -28,3 +29,4 @@ class StateSummary(BaseModel):
startedAt: Optional[datetime]
completedAt: Optional[datetime]
liquids: List[Liquid] = Field(default_factory=list)
wells: List[LiquidHeightInfo] = Field(default_factory=list)
Copy link
Member

Choose a reason for hiding this comment

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

This needs to have more of the data from the state - from just the summary, you wouldn't be able to tell what well is being talked about.

Copy link
Contributor

A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/

@aaron-kulkarni aaron-kulkarni requested a review from a team as a code owner July 22, 2024 13:54
Copy link

codecov bot commented Jul 22, 2024

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 92.43%. Comparing base (0902ff4) to head (3b8fd27).
Report is 56 commits behind head on edge.

Additional details and impacted files

Impacted file tree graph

@@           Coverage Diff           @@
##             edge   #15681   +/-   ##
=======================================
  Coverage   92.43%   92.43%           
=======================================
  Files          77       77           
  Lines        1283     1283           
=======================================
  Hits         1186     1186           
  Misses         97       97           
Flag Coverage Δ
g-code-testing 92.43% <ø> (ø)

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

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.

Some more small style things.

def get_all(self) -> List[LiquidHeightSummary]:
"""Get all well liquid heights."""
allHeights = [] # type: List[LiquidHeightSummary]
# for key, in self._state.measured_liquid_heights.items():
Copy link
Member

Choose a reason for hiding this comment

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

remove commented out code

"""Set the liquid height of the well."""
lhi = LiquidHeightInfo(height=height, last_measured=time)
try:
self._state.measured_liquid_heights[labware_id]
Copy link
Member

Choose a reason for hiding this comment

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

This is not really a great pattern. In Python it's usually better to catch exceptions than try up front (some people disagree, but let's accept it for the sake of argument), but that means "write the code you were going to write and then catch possible exceptions", not "do a special thing to try and cause an exception". There's two options to try:

  • Make self._state.measured_liquid_heights actually be a defaultdict that builds dictionaries as its defaults; however, this might not work great with serialization
  • Just have this code look more like if labware_id not in self._state.measured_liquid_heights: ...

# lhs = LiquidHeightSummary(labware_id=)
# allHeights.extend(a for a in val.values())
# return allHeights
for labware in self._state.measured_liquid_heights.keys():
Copy link
Member

Choose a reason for hiding this comment

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

nit: could be for labware, wells in self._state.measured_liquid_heights.items() and then for well, lhi in wells.items(). I htink it would be neater because it would be only accessing temporaries.


def get_all_in_labware(self, labware_id: str) -> List[LiquidHeightSummary]:
"""Get all well liquid heights for a particular labware."""
allHeights = [] # type: List[LiquidHeightSummary]
Copy link
Member

Choose a reason for hiding this comment

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

Prefer type annotations (i.e. all_heights: List[LiquidHeightSummary]) to type comments (i.e. what you have here)


def get_all(self) -> List[LiquidHeightSummary]:
"""Get all well liquid heights."""
allHeights = [] # type: List[LiquidHeightSummary]
Copy link
Member

Choose a reason for hiding this comment

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

prefer snake_case for variables


def get_all(self) -> List[LiquidHeightSummary]:
"""Get all well liquid heights."""
allHeights = [] # type: List[LiquidHeightSummary]
Copy link
Member

Choose a reason for hiding this comment

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

prefer type annotations to type comments

def has_measured_liquid_height(self, labware_id: str, well_name: str) -> bool:
"""Returns True if the well has been liquid level probed previously."""
try:
self._state.measured_liquid_heights[labware_id][well_name].height
Copy link
Member

Choose a reason for hiding this comment

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

here is a good example of handling exceptions as something that happens from normal code. This will work, but a better way to do it would be something like

try:
     return bool(self._state.measured_liquid_heights[labware_id][well_name])
except KeyError:
     return False

As with the inserting defaults example above, you really want the exception to be thrown while doing something you intend, not just doing something to see if it throws.

Copy link
Contributor

A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/

@aaron-kulkarni
Copy link
Contributor Author

Nevermind I did need the snapshot changes. The snapshot stuff on this branch is unrelated to the issues we were having earlier.

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.

I think this is a really solid first implementation, and I also think we shouldn't merge it to edge until we branch it to 8.0. So this request-changes is to mark that we shouldn't merge it until then, but I do think it's great!

@aaron-kulkarni aaron-kulkarni requested a review from a team as a code owner July 26, 2024 18:52
@aaron-kulkarni aaron-kulkarni requested review from smb2268 and removed request for a team July 26, 2024 18:52
@aaron-kulkarni aaron-kulkarni force-pushed the exec-603-add-last-liquid-height branch from 5718edb to bb66627 Compare July 29, 2024 20:40
@aaron-kulkarni aaron-kulkarni force-pushed the exec-603-add-last-liquid-height branch from bb66627 to f6eef7a Compare July 29, 2024 20:42
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.

Great! Thanks for finishing up the merge.

@sfoster1 sfoster1 merged commit 9869356 into edge Sep 6, 2024
21 checks passed
@sfoster1 sfoster1 deleted the exec-603-add-last-liquid-height branch September 6, 2024 20:29
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.

3 participants