-
Notifications
You must be signed in to change notification settings - Fork 178
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(api): build out well state for liquid height tracking (#15681)
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.
- Loading branch information
1 parent
bebf688
commit 9869356
Showing
15 changed files
with
277 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
"""Basic well data state and store.""" | ||
from dataclasses import dataclass | ||
from datetime import datetime | ||
from typing import Dict, List, Optional | ||
from opentrons.protocol_engine.actions.actions import ( | ||
FailCommandAction, | ||
SucceedCommandAction, | ||
) | ||
from opentrons.protocol_engine.commands.liquid_probe import LiquidProbeResult | ||
from opentrons.protocol_engine.commands.pipetting_common import LiquidNotFoundError | ||
from opentrons.protocol_engine.types import LiquidHeightInfo, LiquidHeightSummary | ||
|
||
from ._abstract_store import HasState, HandlesActions | ||
from ..actions import Action | ||
from ..commands import Command | ||
|
||
|
||
@dataclass | ||
class WellState: | ||
"""State of all wells.""" | ||
|
||
measured_liquid_heights: Dict[str, Dict[str, LiquidHeightInfo]] | ||
|
||
|
||
class WellStore(HasState[WellState], HandlesActions): | ||
"""Well state container.""" | ||
|
||
_state: WellState | ||
|
||
def __init__(self) -> None: | ||
"""Initialize a well store and its state.""" | ||
self._state = WellState(measured_liquid_heights={}) | ||
|
||
def handle_action(self, action: Action) -> None: | ||
"""Modify state in reaction to an action.""" | ||
if isinstance(action, SucceedCommandAction): | ||
self._handle_succeeded_command(action.command) | ||
if isinstance(action, FailCommandAction): | ||
self._handle_failed_command(action) | ||
|
||
def _handle_succeeded_command(self, command: Command) -> None: | ||
if isinstance(command.result, LiquidProbeResult): | ||
self._set_liquid_height( | ||
labware_id=command.params.labwareId, | ||
well_name=command.params.wellName, | ||
height=command.result.z_position, | ||
time=command.createdAt, | ||
) | ||
|
||
def _handle_failed_command(self, action: FailCommandAction) -> None: | ||
if isinstance(action.error, LiquidNotFoundError): | ||
self._set_liquid_height( | ||
labware_id=action.error.private.labware_id, | ||
well_name=action.error.private.well_name, | ||
height=0, | ||
time=action.failed_at, | ||
) | ||
|
||
def _set_liquid_height( | ||
self, labware_id: str, well_name: str, height: float, time: datetime | ||
) -> None: | ||
"""Set the liquid height of the well.""" | ||
lhi = LiquidHeightInfo(height=height, last_measured=time) | ||
if labware_id not in self._state.measured_liquid_heights: | ||
self._state.measured_liquid_heights[labware_id] = {} | ||
self._state.measured_liquid_heights[labware_id][well_name] = lhi | ||
|
||
|
||
class WellView(HasState[WellState]): | ||
"""Read-only well state view.""" | ||
|
||
_state: WellState | ||
|
||
def __init__(self, state: WellState) -> None: | ||
"""Initialize the computed view of well state. | ||
Arguments: | ||
state: Well state dataclass used for all calculations. | ||
""" | ||
self._state = state | ||
|
||
def get_all(self) -> List[LiquidHeightSummary]: | ||
"""Get all well liquid heights.""" | ||
all_heights: List[LiquidHeightSummary] = [] | ||
for labware, wells in self._state.measured_liquid_heights.items(): | ||
for well, lhi in wells.items(): | ||
lhs = LiquidHeightSummary( | ||
labware_id=labware, | ||
well_name=well, | ||
height=lhi.height, | ||
last_measured=lhi.last_measured, | ||
) | ||
all_heights.append(lhs) | ||
return all_heights | ||
|
||
def get_all_in_labware(self, labware_id: str) -> List[LiquidHeightSummary]: | ||
"""Get all well liquid heights for a particular labware.""" | ||
all_heights: List[LiquidHeightSummary] = [] | ||
for well, lhi in self._state.measured_liquid_heights[labware_id].items(): | ||
lhs = LiquidHeightSummary( | ||
labware_id=labware_id, | ||
well_name=well, | ||
height=lhi.height, | ||
last_measured=lhi.last_measured, | ||
) | ||
all_heights.append(lhs) | ||
return all_heights | ||
|
||
def get_last_measured_liquid_height( | ||
self, labware_id: str, well_name: str | ||
) -> Optional[float]: | ||
"""Returns the height of the liquid according to the most recent liquid level probe to this well. | ||
Returns None if no liquid probe has been done. | ||
""" | ||
try: | ||
height = self._state.measured_liquid_heights[labware_id][well_name].height | ||
return height | ||
except KeyError: | ||
return None | ||
|
||
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: | ||
return bool( | ||
self._state.measured_liquid_heights[labware_id][well_name].height | ||
) | ||
except KeyError: | ||
return False |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
28 changes: 28 additions & 0 deletions
28
api/tests/opentrons/protocol_engine/state/test_well_store.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,28 @@ | ||
"""Well state store tests.""" | ||
import pytest | ||
from opentrons.protocol_engine.state.wells import WellStore | ||
from opentrons.protocol_engine.actions.actions import SucceedCommandAction | ||
|
||
from .command_fixtures import create_liquid_probe_command | ||
|
||
|
||
@pytest.fixture | ||
def subject() -> WellStore: | ||
"""Well store test subject.""" | ||
return WellStore() | ||
|
||
|
||
def test_handles_liquid_probe_success(subject: WellStore) -> None: | ||
"""It should add the well to the state after a successful liquid probe.""" | ||
labware_id = "labware-id" | ||
well_name = "well-name" | ||
|
||
liquid_probe = create_liquid_probe_command() | ||
|
||
subject.handle_action( | ||
SucceedCommandAction(private_result=None, command=liquid_probe) | ||
) | ||
|
||
assert len(subject.state.measured_liquid_heights) == 1 | ||
|
||
assert subject.state.measured_liquid_heights[labware_id][well_name].height == 0.5 |
51 changes: 51 additions & 0 deletions
51
api/tests/opentrons/protocol_engine/state/test_well_view.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
"""Well view tests.""" | ||
from datetime import datetime | ||
from opentrons.protocol_engine.types import LiquidHeightInfo | ||
import pytest | ||
from opentrons.protocol_engine.state.wells import WellState, WellView | ||
|
||
|
||
@pytest.fixture | ||
def subject() -> WellView: | ||
"""Get a well view test subject.""" | ||
labware_id = "labware-id" | ||
well_name = "well-name" | ||
height_info = LiquidHeightInfo(height=0.5, last_measured=datetime.now()) | ||
state = WellState(measured_liquid_heights={labware_id: {well_name: height_info}}) | ||
|
||
return WellView(state) | ||
|
||
|
||
def test_get_all(subject: WellView) -> None: | ||
"""Should return a list of well heights.""" | ||
assert subject.get_all()[0].height == 0.5 | ||
|
||
|
||
def test_get_last_measured_liquid_height(subject: WellView) -> None: | ||
"""Should return the height of a single well correctly for valid wells.""" | ||
labware_id = "labware-id" | ||
well_name = "well-name" | ||
|
||
invalid_labware_id = "invalid-labware-id" | ||
invalid_well_name = "invalid-well-name" | ||
|
||
assert ( | ||
subject.get_last_measured_liquid_height(invalid_labware_id, invalid_well_name) | ||
is None | ||
) | ||
assert subject.get_last_measured_liquid_height(labware_id, well_name) == 0.5 | ||
|
||
|
||
def test_has_measured_liquid_height(subject: WellView) -> None: | ||
"""Should return True for measured wells and False for ones that have no measurements.""" | ||
labware_id = "labware-id" | ||
well_name = "well-name" | ||
|
||
invalid_labware_id = "invalid-labware-id" | ||
invalid_well_name = "invalid-well-name" | ||
|
||
assert ( | ||
subject.has_measured_liquid_height(invalid_labware_id, invalid_well_name) | ||
is False | ||
) | ||
assert subject.has_measured_liquid_height(labware_id, well_name) is True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.