-
Notifications
You must be signed in to change notification settings - Fork 179
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): add command to configure pipette volume in Protocol Engine #13473
feat(api): add command to configure pipette volume in Protocol Engine #13473
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## low-volume-pipetting #13473 +/- ##
========================================================
+ Coverage 71.35% 71.38% +0.03%
========================================================
Files 2418 2433 +15
Lines 67903 68064 +161
Branches 7876 7919 +43
========================================================
+ Hits 48452 48589 +137
- Misses 17609 17616 +7
- Partials 1842 1859 +17
Flags with carried forward coverage won't be shown. Click here to find out more.
|
71b0b00
to
e189e6d
Compare
af0ec52
to
47b291e
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.
I don't think this is quite what we want. We want an action that changes the liquid class by sending over the new name, and is handled by calling the set_liquid_class
method in hardware and then updating the loaded pipette config from hardware (or calling an equivalent function when simulating).
"""Update a pipette's current static config class based on a new liquid class.""" | ||
|
||
pipette_id: str | ||
serial_number: str |
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.
the serial number definitely shouldn't be in here - that would make it impossible for the action to exist in a json protocol that won't know the serial number
|
||
pipette_id: str | ||
serial_number: str | ||
config: pipette_data_provider.LoadedStaticPipetteData |
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 also doesn't seem right - the payload here should be the liquid class name and not much else. That will get executed by being passed down to the hardware.
431ac83
to
cb6f5bc
Compare
47b291e
to
7971f28
Compare
cb6f5bc
to
0f39436
Compare
7971f28
to
60dfbea
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.
Looks like exactly what we want, save the copypasta in the command implementation, sharing the logic, and fixing that one hysteresis logic defn
if volume > (self.liquid_class.max_volume * 0.75): | ||
return pip_types.LiquidClasses.default.name | ||
if self._liquid_class_name == pip_types.LiquidClasses.default: | ||
if volume < (self.liquid_class.max_volume * 0.5): |
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 wants to be < than half the max volume of the low volume default
Returns: | ||
A LoadedConfiguredVolumeData object. | ||
""" | ||
# TODO (spp, 2023-05-10): either raise error if using MountType.EXTENSION in |
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.
copypasta
except RuntimeError as e: | ||
raise FailedToLoadPipetteError(str(e)) from e | ||
|
||
pipette_dict = self._hardware_api.get_attached_instrument( |
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.
don't need to do this
# behavior of protocol_context.load_instrument, and is used here as a | ||
# pipette existence check | ||
try: | ||
await self._hardware_api.cache_instruments(cache_request) |
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.
don't need this
# is only to support protocol analysis, since the hardware simulator | ||
# does not cache requested virtual instruments. Remove per | ||
# https://opentrons.atlassian.net/browse/RLIQ-258 | ||
other_mount = mount.other_mount() |
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.
don't need the other mount and cache pipette stuff
is_p50_flex_pipette = ( | ||
"p50" in pipette_name_value and "flex" in pipette_name_value | ||
) | ||
if volume < 5 and is_p50_flex_pipette: |
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.
we should use the exact same logic - maybe make liquid_class_for_volume
a free function taking
- the requested volume
- the current liquid class
and reutrning a liquid class, that can get called both here and in hardware?
use_virtual_pipettes = self._state_store.config.use_virtual_pipettes | ||
|
||
pipette_name_value = ( | ||
pipette_name.value | ||
if isinstance(pipette_name, PipetteNameType) | ||
else pipette_name | ||
) | ||
|
||
pipette_id = pipette_id or self._model_utils.generate_id() | ||
|
||
if not use_virtual_pipettes: | ||
cache_request = {mount.to_hw_mount(): pipette_name_value} | ||
|
||
# TODO(mc, 2022-12-09): putting the other pipette in the cache request | ||
# is only to support protocol analysis, since the hardware simulator | ||
# does not cache requested virtual instruments. Remove per | ||
# https://opentrons.atlassian.net/browse/RLIQ-258 | ||
other_mount = mount.other_mount() | ||
other_pipette = self._state_store.pipettes.get_by_mount(other_mount) | ||
if other_pipette is not None: | ||
cache_request[other_mount.to_hw_mount()] = ( | ||
other_pipette.pipetteName.value | ||
if isinstance(other_pipette.pipetteName, PipetteNameType) | ||
else other_pipette.pipetteName |
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.
as we spoke about in the morning call. I guess we can extract this logic to a method bc we are doing the same for the load_pipette
?
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.
we don't need to do this logic in here at all I think
Rather than importing the concept of liquid classes to pipette state, we should just add a command to update the static configs from the user facing python API.
This way it can be shared between hardware and the virtual pipette data provider. Went back and forth on where to put this but ultimately this only depends on direct inputs and values from configuration, so shared-data it is.
Unfortunately, the virtual pipette data provider now must be stateful because it's emulating something stateful, and we should not just move the state to state because it would be doubling for unavoidable hardware state like the position of the pipette, and hardware controller really wants to run this. That said, we can now nicely use the exact same logic for the virtual pipette mirror as the actual hardware control. Needs new tests but doesn't break old ones.
eaffb51
to
1f57a2b
Compare
pip = self.get_pipette(mount) | ||
if pip.current_volume > 0: | ||
# Switching liquid classes can't happen when there's already liquid | ||
return |
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.
do we want to raise here instead?
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.
yeah probably
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.
thank you for the changes! this looks great to me!
When PE loaded pipettes, it would set the static config in its state by having the load method of the equipment handler side-effect emit a special action that only existed to update the configuration. This worked ok as long as you weren't too picky about wanting to be sure that the action was handled before the result of the command was evaluated. The problem is that now we do care about that, a lot (though it's not quite as evident in this commit). When we switch pickup and drop tip to reset the tip state of a pipette, we're going to need to set the configuration using this new private result method and rely on the results in the command results handler. This is implemented by poking a new typevar into CommandUpdateAction that can optionally carry a "private result", something that will not be exposed via HTTP or via the database but can carry information in a lifetime-synchronous way to the result. This required a lot of refactoring, but I think it's ended up pretty cleanly; the only test changes are directly required by the changed code.
@@ -500,3 +514,5 @@ | |||
calibration.CalibrateModuleResult, | |||
calibration.MoveToMaintenancePositionResult, | |||
] | |||
|
|||
CommandPrivateResult = Union[None, LoadPipettePrivateResult] |
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.
are we missing ConfigureForVolumePrivateResult
?
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.
we were!
tip_configuration = static_config.tip_configuration_lookup_table[ | ||
static_config.max_volume | ||
] | ||
tip_configuration = 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.
do we want to leave a comment about defaulting?
looks great to me! had a few questions but overall love the private setting change :-) |
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.
Reviewed the Protocol Engine portions of this PR, all look good to me outside of minor comments and fixing up the failing CI tests. Really like the private result addition, we can definitely use that in the future to clean up some result cruft.
ABC, | ||
Generic[CommandParamsT, CommandResultT, CommandPrivateResultT], | ||
): | ||
"""Abstract command creation and execution implementation. |
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.
Doc strings for this new Abstract Implementation should be updated to not just be copypasta'd from above.
api/src/opentrons/protocol_engine/commands/configure_for_volume.py
Outdated
Show resolved
Hide resolved
"""Ensure the requested volume can be configured for the given pipette. | ||
|
||
Args: | ||
pipette_id: An optional identifier to assign the pipette. If None, an |
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.
Not optional for this method
pipette_id, volume, model | ||
) | ||
|
||
serial_number = self._model_utils.generate_id(prefix="fake-serial-number-") |
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.
the fact that we are regenerating serial number here for the simulated case feels a bit iffy even though in practice I don't believe it will have any actionable impact on how we do analysis. The only place we are using serial number is for get_calibrated_tip_length
which already is referencing a fake serial number in the first place. Still just calling it out here for maybe a comment or not explaining this.
) -> None: | ||
"""Mock out move_types.py functions.""" | ||
for name, func in inspect.getmembers(pipette_data_provider, inspect.isfunction): | ||
monkeypatch.setattr(pipette_data_provider, name, decoy.mock(func=func)) | ||
monkeypatch.setattr( |
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.
Since VirtualPipetteDataProvider
is now a class we should avoid monkeypatching it here and instead make the below pytest fixture a decoy mock of that class, unless there is some functional reason why we are doing it this way.
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.
Cannot approve my own pull request but I like the private results update a lot.
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.
Generally looks great to me. Thanks!
api/src/opentrons/protocol_engine/commands/configure_for_volume.py
Outdated
Show resolved
Hide resolved
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.
get rid of my own negative review
class ConfigureForVolumeResult(BaseModel): | ||
"""Result data from execution of an ConfigureForVolume command.""" | ||
|
||
pipetteId: str = Field( |
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.
we don't really, might remove it
pip = self.get_pipette(mount) | ||
if pip.current_volume > 0: | ||
# Switching liquid classes can't happen when there's already liquid | ||
return |
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.
yeah probably
…#13473) Adds a new protocol engine command ConfigureForLiquidVolume that calls the hardware control method configure_for_liquid_volume (though in this protocol, there is no upward interface other than direct command injection). In addition, this PR has a major refactor of PE command execution to support "private results" - values that can come from a command that do not have to be (and in fact cannot be and are not) serialized, whether to persistent storage or over the wire to clients - they're held entirely internally. This functionality lets us replace side-effect emitting actions and therefore get rid of PipetteConfigAction, which only existed so loading pipettes could go update pipette config data. Instead, that's now a private result, both of LoadPipette and of the new action. Finally, this PR makes the virtual pipette data provider stateful because it's emulating something stateful, and we should not just move the state to state because it would be doubling for unavoidable hardware state like the position of the pipette, and hardware controller really wants to run this. Co-authored-by: Max Marrone <[email protected]> Co-authored-by: Seth Foster <[email protected]>
…#13473) Adds a new protocol engine command ConfigureForLiquidVolume that calls the hardware control method configure_for_liquid_volume (though in this protocol, there is no upward interface other than direct command injection). In addition, this PR has a major refactor of PE command execution to support "private results" - values that can come from a command that do not have to be (and in fact cannot be and are not) serialized, whether to persistent storage or over the wire to clients - they're held entirely internally. This functionality lets us replace side-effect emitting actions and therefore get rid of PipetteConfigAction, which only existed so loading pipettes could go update pipette config data. Instead, that's now a private result, both of LoadPipette and of the new action. Finally, this PR makes the virtual pipette data provider stateful because it's emulating something stateful, and we should not just move the state to state because it would be doubling for unavoidable hardware state like the position of the pipette, and hardware controller really wants to run this. Co-authored-by: Max Marrone <[email protected]> Co-authored-by: Seth Foster <[email protected]>
Overview
Add the configure for liquid class command to protocol engine and allow virtual configs to return different liquid class configurations.
Changelog