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 execution of aspirate steps in a liquid class based transfer #17092

Open
wants to merge 13 commits into
base: edge
Choose a base branch
from
139 changes: 132 additions & 7 deletions api/src/opentrons/protocol_api/core/engine/instrument.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,17 @@

from __future__ import annotations

from typing import Optional, TYPE_CHECKING, cast, Union, List
from typing import Optional, TYPE_CHECKING, cast, Union, List, Tuple
from opentrons.types import Location, Mount, NozzleConfigurationType, NozzleMapInterface
from opentrons.hardware_control import SyncHardwareAPI
from opentrons.hardware_control.dev_types import PipetteDict
from opentrons.protocols.api_support.util import FlowRates, find_value_for_api_version
from opentrons.protocols.api_support.types import APIVersion
from opentrons.protocols.advanced_control.transfers.common import TransferTipPolicyV2
from opentrons.protocols.advanced_control.transfers.common import (
TransferTipPolicyV2,
check_valid_volume_parameters,
expand_for_volume_constraints,
)
from opentrons.protocol_engine import commands as cmd
from opentrons.protocol_engine import (
DeckPoint,
Expand Down Expand Up @@ -38,6 +42,7 @@
)
from opentrons.protocol_api._nozzle_layout import NozzleLayout
from . import overlap_versions, pipette_movement_conflict
from . import transfer_components_executor as tx_comps_executor
Copy link
Contributor

Choose a reason for hiding this comment

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

Hehe, this name is really too long. Maybe just transfer_executor?

But it doesn't even behave like the traditional meaning of an executor, which is a thing where you submit high-level tasks to and it decides when and how to run the tasks -- here, you the caller are the one choosing when and how to invoke the functions in the executor.

I hate the name helper, but this file really is just a transfer_helper. Hm ...

Copy link
Member Author

Choose a reason for hiding this comment

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

Ya I struggled with the name..

Copy link
Contributor

Choose a reason for hiding this comment

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

As someone who dislikes the name helper almost as much as manager for classes, I agree that transfer_helper does appear to be the most straightforward, simplest, while still descriptive name for what this class is using

Copy link
Member Author

Choose a reason for hiding this comment

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

Cool, helper it is then


from .well import WellCore
from ..instrument import AbstractInstrument
Expand All @@ -46,6 +51,7 @@
if TYPE_CHECKING:
from .protocol import ProtocolCore
from opentrons.protocol_api._liquid import LiquidClass
from opentrons.protocol_api._liquid_properties import TransferProperties

_DISPENSE_VOLUME_VALIDATION_ADDED_IN = APIVersion(2, 17)

Expand Down Expand Up @@ -892,16 +898,131 @@ def load_liquid_class(
)
return result.liquidClassId

# TODO: update with getNextTip implementation
def get_next_tip(self) -> None:
"""Get the next tip to pick up."""

def transfer_liquid(
self,
liquid_class_id: str,
liquid_class: LiquidClass,
volume: float,
source: List[WellCore],
dest: List[WellCore],
source: List[Tuple[Location, WellCore]],
dest: List[Tuple[Location, WellCore]],
new_tip: TransferTipPolicyV2,
trash_location: Union[WellCore, Location, TrashBin, WasteChute],
tiprack_uri: str,
tip_drop_location: Union[WellCore, Location, TrashBin, WasteChute],
) -> None:
"""Execute transfer using liquid class properties.

Args:
liquid_class: The liquid class to use for transfer properties.
volume: Volume to transfer per well.
source: List of source wells, with each well represented as a tuple of
types.Location and WellCore.
types.Location is only necessary for saving the last accessed location.
dest: List of destination wells, with each well represented as a tuple of
types.Location and WellCore.
types.Location is only necessary for saving the last accessed location.
new_tip: Whether the transfer should use a new tip 'once', 'never', 'always',
or 'per source'.
tiprack_uri: The URI of the tiprack that the transfer settings are for.
tip_drop_location: Location where the tip will be dropped (if appropriate).
"""
# This function is WIP
# TODO: use the ID returned by load_liquid_class in command annotations
self.load_liquid_class(
liquid_class=liquid_class,
pipette_load_name=self.get_pipette_name(), # TODO: update this to use load name instead
tiprack_uri=tiprack_uri,
)
transfer_props = liquid_class.get_for(
# update this to fetch load name instead
pipette=self.get_pipette_name(),
tiprack=tiprack_uri,
)
aspirate_props = transfer_props.aspirate

check_valid_volume_parameters(
disposal_volume=0, # No disposal volume for 1-to-1 transfer
air_gap=aspirate_props.retract.air_gap_by_volume.get_for_volume(volume),
max_volume=self.get_max_volume(),
)
source_dest_per_volume_step = expand_for_volume_constraints(
volumes=[volume for _ in range(len(source))],
targets=zip(source, dest),
max_volume=self.get_max_volume(),
)
if new_tip == TransferTipPolicyV2.ONCE:
# TODO: update this once getNextTip is implemented
self.get_next_tip()
Copy link
Contributor

Choose a reason for hiding this comment

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

Hey, does the upcoming get_next_tip() return the tip to use (stateless), or does it alter some internal state to indicate the tip it chose?

Copy link
Member Author

Choose a reason for hiding this comment

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

It returns the tip to use in a stateless way

for step_volume, (src, dest) in source_dest_per_volume_step: # type: ignore[assignment]
Copy link
Contributor

Choose a reason for hiding this comment

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

This is more of a personal style thing, but this statement seems messy, aesthetically, with the tuple being unpacked from a tuple and the type ignore. I don't know if there's a better way to represent this, but maybe it could look cleaner by doing the second tuple unpacking elsewhere (and naming them source and destination, I sort of dislike the shortening of these variables)

Copy link
Member Author

Choose a reason for hiding this comment

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

Ya the transfer method is a work in progress. It's mostly there for showing how the aspirate and dispense methods will be used in the transfer. I'll address this in the last PR.

if new_tip == TransferTipPolicyV2.ALWAYS:
# TODO: update this once getNextTip is implemented
self.get_next_tip()

# TODO: add aspirate and dispense
Copy link
Contributor

Choose a reason for hiding this comment

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

Does this just call the aspirate_liquid_class() function below?

Copy link
Member Author

Choose a reason for hiding this comment

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

aspirate_liquid_class and the upcoming dispense_liquid_class functions


if new_tip == TransferTipPolicyV2.ALWAYS:
if isinstance(tip_drop_location, (TrashBin, WasteChute)):
self.drop_tip_in_disposal_location(
disposal_location=tip_drop_location,
home_after=False,
alternate_tip_drop=True,
)
elif isinstance(tip_drop_location, Location):
self.drop_tip(
location=tip_drop_location,
well_core=tip_drop_location.labware.as_well()._core, # type: ignore[arg-type]
home_after=False,
alternate_drop_location=True,
)

def aspirate_liquid_class(
self,
volume: float,
source: Tuple[Location, WellCore],
transfer_properties: TransferProperties,
) -> None:
"""Execute transfer using liquid class properties."""
"""Execute aspiration steps.

1. Submerge
2. Mix
3. pre-wet
4. Aspirate
5. Delay- wait inside the liquid
6. Aspirate retract
"""
aspirate_props = transfer_properties.aspirate
source_loc, source_well = source
aspirate_point = (
tx_comps_executor.absolute_point_from_position_reference_and_offset(
well=source_well,
position_reference=aspirate_props.position_reference,
offset=aspirate_props.offset,
)
)
aspirate_location = Location(aspirate_point, labware=source_loc.labware)

components_executer = tx_comps_executor.TransferComponentsExecutor(
instrument_core=self,
transfer_properties=transfer_properties,
target_location=aspirate_location,
target_well=source_well,
)
components_executer.submerge(
submerge_properties=aspirate_props.submerge,
# Assuming aspirate is not called with *liquid* in the tip
# TODO: evaluate if using the current volume to find air gap is not a good idea.
air_gap_volume=self.get_current_volume(),
)
# TODO: when aspirating for consolidation, do not perform mix
components_executer.mix(mix_properties=aspirate_props.mix)
# TODO: when aspirating for consolidation, do not preform pre-wet
components_executer.pre_wet(
volume=volume,
)
components_executer.aspirate_and_wait(volume=volume)
components_executer.retract_after_aspiration(volume=volume)

def retract(self) -> None:
"""Retract this instrument to the top of the gantry."""
Expand Down Expand Up @@ -994,3 +1115,7 @@ def nozzle_configuration_valid_for_lld(self) -> bool:
return self._engine_client.state.pipettes.get_nozzle_configuration_supports_lld(
self.pipette_id
)

def delay(self, seconds: float) -> None:
"""Call a protocol delay."""
self._protocol_core.delay(seconds=seconds, msg=None)
Loading
Loading