-
Notifications
You must be signed in to change notification settings - Fork 178
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
Conversation
There was a problem hiding this 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: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
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: |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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.
There was a problem hiding this 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]]] |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/ |
Codecov ReportAll modified and coverable lines are covered by tests ✅
Additional details and impacted files@@ Coverage Diff @@
## edge #15681 +/- ##
=======================================
Coverage 92.43% 92.43%
=======================================
Files 77 77
Lines 1283 1283
=======================================
Hits 1186 1186
Misses 97 97
Flags with carried forward coverage won't be shown. Click here to find out more. |
There was a problem hiding this 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(): |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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(): |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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.
A PR has been opened to address analyses snapshot changes. Please review the changes here: https://github.com/Opentrons/opentrons/pull/ |
Nevermind I did need the snapshot changes. The snapshot stuff on this branch is unrelated to the issues we were having earlier. |
There was a problem hiding this 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!
5718edb
to
bb66627
Compare
Add a well state class that will be used for now just for liquid height tracking but in the future could be built out more. re EXEC-603
bb66627
to
f6eef7a
Compare
There was a problem hiding this 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.
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:height
value of 0: the most recent probe into that well found no liquid.A quick word on the implementation of the new
LiquidHeightInfo
type. It has two attributes,height
andlast_measured
. Thelast_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 aliquid_probe
, so it's useful to know what the last time was we did aliquid_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 theLiquidHeightInfo
type.Test Plan
Unit tests written and passed for well_view and well_store.
Changelog
WellState
class, along with complementaryWellView
andWellStore
LiquidHeightInfo
type, which is used to store info about recent liquid height measurements.LiquidHeightSummary
type, which is used in StateSummary.Review requests
Risk assessment
Low.