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): add command to configure pipette volume in Protocol Engine #13473

Merged
merged 20 commits into from
Sep 11, 2023

Conversation

Laura-Danielle
Copy link
Contributor

@Laura-Danielle Laura-Danielle commented Sep 6, 2023

Overview

Add the configure for liquid class command to protocol engine and allow virtual configs to return different liquid class configurations.

Changelog

  • Add new command to configure liquid for volume
  • Add a new equipment command that will configure the volume in the hardware controller and update the pipette state store
  • Update the hardware controller to again have a command to configure liquid for volume (a copy of work @sfoster1 had previously done)

@Laura-Danielle Laura-Danielle requested review from a team as code owners September 6, 2023 16:36
@codecov
Copy link

codecov bot commented Sep 6, 2023

Codecov Report

Merging #13473 (df0b79b) into low-volume-pipetting (e9243b1) will increase coverage by 0.03%.
The diff coverage is 69.23%.

Additional details and impacted files

Impacted file tree graph

@@                   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     
Flag Coverage Δ
app 69.05% <ø> (+0.17%) ⬆️
g-code-testing 96.44% <ø> (ø)
notify-server 89.13% <ø> (ø)
protocol-designer 46.09% <ø> (ø)
shared-data 74.26% <69.23%> (-0.03%) ⬇️
step-generation 87.18% <ø> (ø)

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

Files Changed Coverage Δ
api/src/opentrons/hardware_control/ot3api.py 79.26% <ø> (ø)
.../src/opentrons/protocol_engine/actions/__init__.py 100.00% <ø> (ø)
...i/src/opentrons/protocol_engine/actions/actions.py 100.00% <ø> (ø)
...src/opentrons/protocol_engine/commands/__init__.py 100.00% <ø> (ø)
.../src/opentrons/protocol_engine/commands/command.py 94.11% <ø> (-0.17%) ⬇️
...entrons/protocol_engine/commands/command_unions.py 100.00% <ø> (ø)
...opentrons/protocol_engine/commands/load_pipette.py 100.00% <ø> (ø)
...rc/opentrons/protocol_engine/execution/__init__.py 100.00% <ø> (ø)
...rons/protocol_engine/execution/command_executor.py 100.00% <ø> (ø)
...c/opentrons/protocol_engine/execution/equipment.py 100.00% <ø> (ø)
... and 4 more

... and 40 files with indirect coverage changes

@sfoster1 sfoster1 force-pushed the rss-313-add-configure-for-volume branch from 71b0b00 to e189e6d Compare September 6, 2023 17:29
@sfoster1 sfoster1 force-pushed the configure-liquid-class-protocol-engine branch from af0ec52 to 47b291e Compare September 6, 2023 18:24
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 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
Copy link
Member

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
Copy link
Member

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.

@sfoster1 sfoster1 force-pushed the rss-313-add-configure-for-volume branch from 431ac83 to cb6f5bc Compare September 6, 2023 18:50
@sfoster1 sfoster1 force-pushed the configure-liquid-class-protocol-engine branch from 47b291e to 7971f28 Compare September 6, 2023 18:50
@sfoster1 sfoster1 force-pushed the rss-313-add-configure-for-volume branch from cb6f5bc to 0f39436 Compare September 7, 2023 14:22
Base automatically changed from rss-313-add-configure-for-volume to low-volume-pipetting September 7, 2023 14:53
@sfoster1 sfoster1 force-pushed the configure-liquid-class-protocol-engine branch from 7971f28 to 60dfbea Compare September 7, 2023 15:23
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.

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):
Copy link
Member

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
Copy link
Member

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(
Copy link
Member

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)
Copy link
Member

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()
Copy link
Member

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:
Copy link
Member

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?

Comment on lines 365 to 388
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
Copy link
Contributor

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?

Copy link
Member

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

Laura-Danielle and others added 8 commits September 8, 2023 09:42
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.
@sfoster1 sfoster1 force-pushed the configure-liquid-class-protocol-engine branch from eaffb51 to 1f57a2b Compare September 8, 2023 13:42
pip = self.get_pipette(mount)
if pip.current_volume > 0:
# Switching liquid classes can't happen when there's already liquid
return
Copy link
Contributor

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?

Copy link
Member

Choose a reason for hiding this comment

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

yeah probably

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.

thank you for the changes! this looks great to me!

Laura-Danielle and others added 2 commits September 8, 2023 18:43
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.
@Laura-Danielle Laura-Danielle changed the title Configure liquid class protocol engine feat(api): add command to configure pipette volume in Protocol Engine Sep 11, 2023
@@ -500,3 +514,5 @@
calibration.CalibrateModuleResult,
calibration.MoveToMaintenancePositionResult,
]

CommandPrivateResult = Union[None, LoadPipettePrivateResult]
Copy link
Contributor

Choose a reason for hiding this comment

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

are we missing ConfigureForVolumePrivateResult ?

Copy link
Member

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(
Copy link
Contributor

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?

@TamarZanzouri
Copy link
Contributor

looks great to me! had a few questions but overall love the private setting change :-)

Copy link
Contributor

@jbleon95 jbleon95 left a 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.
Copy link
Contributor

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.

"""Ensure the requested volume can be configured for the given pipette.

Args:
pipette_id: An optional identifier to assign the pipette. If None, an
Copy link
Contributor

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-")
Copy link
Contributor

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(
Copy link
Contributor

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.

Copy link
Contributor Author

@Laura-Danielle Laura-Danielle left a 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.

@sfoster1 sfoster1 self-requested a review September 11, 2023 17:31
Copy link
Contributor

@SyntaxColoring SyntaxColoring left a 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!

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.

get rid of my own negative review

class ConfigureForVolumeResult(BaseModel):
"""Result data from execution of an ConfigureForVolume command."""

pipetteId: str = Field(
Copy link
Member

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
Copy link
Member

Choose a reason for hiding this comment

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

yeah probably

@sfoster1 sfoster1 merged commit 7448a1a into low-volume-pipetting Sep 11, 2023
42 of 46 checks passed
@sfoster1 sfoster1 deleted the configure-liquid-class-protocol-engine branch September 11, 2023 20:25
sfoster1 added a commit that referenced this pull request Sep 15, 2023
…#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]>
sfoster1 added a commit that referenced this pull request Sep 15, 2023
…#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]>
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.

5 participants