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(protocol-engine): Add tryLiquidProbe to complement liquidProbe #15667

Merged
merged 10 commits into from
Jul 17, 2024
10 changes: 10 additions & 0 deletions api/src/opentrons/protocol_engine/commands/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -331,6 +331,11 @@
LiquidProbeCreate,
LiquidProbeResult,
LiquidProbeCommandType,
TryLiquidProbe,
TryLiquidProbeParams,
TryLiquidProbeCreate,
TryLiquidProbeResult,
TryLiquidProbeCommandType,
)

__all__ = [
Expand Down Expand Up @@ -580,4 +585,9 @@
"LiquidProbeCreate",
"LiquidProbeResult",
"LiquidProbeCommandType",
"TryLiquidProbe",
"TryLiquidProbeParams",
"TryLiquidProbeCreate",
"TryLiquidProbeResult",
"TryLiquidProbeCommandType",
]
25 changes: 22 additions & 3 deletions api/src/opentrons/protocol_engine/commands/command_unions.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Union types of concrete command definitions."""

from typing import Annotated, Iterable, Type, Union, get_type_hints
from collections.abc import Collection
from typing import Annotated, Type, Union, get_type_hints

from pydantic import Field

Expand Down Expand Up @@ -313,6 +314,11 @@
LiquidProbeCreate,
LiquidProbeResult,
LiquidProbeCommandType,
TryLiquidProbe,
TryLiquidProbeParams,
TryLiquidProbeCreate,
TryLiquidProbeResult,
TryLiquidProbeCommandType,
)

Command = Annotated[
Expand Down Expand Up @@ -353,6 +359,7 @@
VerifyTipPresence,
GetTipPresence,
LiquidProbe,
TryLiquidProbe,
heater_shaker.WaitForTemperature,
heater_shaker.SetTargetTemperature,
heater_shaker.DeactivateHeater,
Expand Down Expand Up @@ -421,6 +428,7 @@
VerifyTipPresenceParams,
GetTipPresenceParams,
LiquidProbeParams,
TryLiquidProbeParams,
heater_shaker.WaitForTemperatureParams,
heater_shaker.SetTargetTemperatureParams,
heater_shaker.DeactivateHeaterParams,
Expand Down Expand Up @@ -487,6 +495,7 @@
VerifyTipPresenceCommandType,
GetTipPresenceCommandType,
LiquidProbeCommandType,
TryLiquidProbeCommandType,
heater_shaker.WaitForTemperatureCommandType,
heater_shaker.SetTargetTemperatureCommandType,
heater_shaker.DeactivateHeaterCommandType,
Expand Down Expand Up @@ -554,6 +563,7 @@
VerifyTipPresenceCreate,
GetTipPresenceCreate,
LiquidProbeCreate,
TryLiquidProbeCreate,
heater_shaker.WaitForTemperatureCreate,
heater_shaker.SetTargetTemperatureCreate,
heater_shaker.DeactivateHeaterCreate,
Expand Down Expand Up @@ -622,6 +632,7 @@
VerifyTipPresenceResult,
GetTipPresenceResult,
LiquidProbeResult,
TryLiquidProbeResult,
heater_shaker.WaitForTemperatureResult,
heater_shaker.SetTargetTemperatureResult,
heater_shaker.DeactivateHeaterResult,
Expand Down Expand Up @@ -671,12 +682,20 @@


def _map_create_types_by_params_type(
create_types: Iterable[Type[CommandCreate]],
create_types: Collection[Type[CommandCreate]],
) -> dict[Type[CommandParams], Type[CommandCreate]]:
def get_params_type(create_type: Type[CommandCreate]) -> Type[CommandParams]:
return get_type_hints(create_type)["params"] # type: ignore[no-any-return]

return {get_params_type(create_type): create_type for create_type in create_types}
result = {get_params_type(create_type): create_type for create_type in create_types}

# This isn't an inherent requirement of opentrons.protocol_engine,
# but this mapping is only useful to higher-level code if this holds true.
assert len(result) == len(
create_types
), "Param models should map to create models 1:1."

return result


CREATE_TYPES_BY_PARAMS_TYPE = _map_create_types_by_params_type(
Expand Down
144 changes: 130 additions & 14 deletions api/src/opentrons/protocol_engine/commands/liquid_probe.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Liquid-probe command for OT3 hardware. request, result, and implementation models."""
"""The liquidProbe and tryLiquidProbe commands."""

from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type, Union
from opentrons.protocol_engine.errors.exceptions import MustHomeError, TipNotEmptyError
Expand Down Expand Up @@ -34,30 +35,59 @@


LiquidProbeCommandType = Literal["liquidProbe"]
TryLiquidProbeCommandType = Literal["tryLiquidProbe"]

Comment on lines 37 to +38
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The names liquidProbe/tryLiquidProbe are inconsistent with verifyTipPresence/getTipPresence. I really don't know what to do about this at this point, since it seems like it'd be annoying to rename liquidProbe, but I'm open to ideas.

Copy link
Member

Choose a reason for hiding this comment

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

Would it be annoying? I don't think anybody uses it right now. I think we could do either

  • VerifyLiquidPresence/GetLiquidPresence, if we want to add a distinct command for get liquid height
  • If we don't want to change LiquidProbe, VerifyLiquidPresence seems like a good standalone command name honestly. Probably would want its own file though.


# Both command variants should have identical parameters.
# But we need two separate parameter model classes because
# `command_unions.CREATE_TYPES_BY_PARAMS_TYPE` needs to be a 1:1 mapping.
class _CommonParams(PipetteIdMixin, WellLocationMixin):
pass

class LiquidProbeParams(PipetteIdMixin, WellLocationMixin):
"""Parameters required to liquid probe a specific well."""

class LiquidProbeParams(_CommonParams):
"""Parameters required for a `liquidProbe` command."""

pass


class TryLiquidProbeParams(_CommonParams):
"""Parameters required for a `tryLiquidProbe` command."""

pass


class LiquidProbeResult(DestinationPositionResult):
"""Result data from the execution of a liquid-probe command."""
"""Result data from the execution of a `liquidProbe` command."""

z_position: float = Field(
..., description="The Z coordinate, in mm, of the found liquid in deck space."
)
# New fields should use camelCase. z_position is snake_case for historical reasons.


_ExecuteReturn = Union[
class TryLiquidProbeResult(DestinationPositionResult):
"""Result data from the execution of a `tryLiquidProbe` command."""

z_position: Optional[float] = Field(
...,
description=(
"The Z coordinate, in mm, of the found liquid in deck space."
" If no liquid was found, `null` or omitted."
),
)


_LiquidProbeExecuteReturn = Union[
SuccessData[LiquidProbeResult, None],
DefinedErrorData[LiquidNotFoundError, LiquidNotFoundErrorInternalData],
]
_TryLiquidProbeExecuteReturn = SuccessData[TryLiquidProbeResult, None]


class LiquidProbeImplementation(AbstractCommandImpl[LiquidProbeParams, _ExecuteReturn]):
class LiquidProbeImplementation(
AbstractCommandImpl[LiquidProbeParams, _LiquidProbeExecuteReturn]
):
"""The implementation of a `liquidProbe` command."""

def __init__(
Expand All @@ -71,16 +101,19 @@ def __init__(
self._pipetting = pipetting
self._model_utils = model_utils

async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn:
async def execute(self, params: _CommonParams) -> _LiquidProbeExecuteReturn:
"""Move to and liquid probe the requested well.

Return the z-position of the found liquid.
If no liquid is found, return a LiquidNotFoundError as a defined error.

Raises:
TipNotAttachedError: if there is no tip attached to the pipette
MustHomeError: if the plunger is not in a valid position
TipNotEmptyError: if the tip starts with liquid in it
LiquidNotFoundError: if liquid is not found during the probe process.
TipNotAttachedError: as an undefined error, if there is not tip attached to
the pipette.
TipNotEmptyError: as an undefined error, if the tip starts with liquid
in it.
MustHomeError: as an undefined error, if the plunger is not in a valid
position.
"""
pipette_id = params.pipetteId
labware_id = params.labwareId
Expand Down Expand Up @@ -139,8 +172,68 @@ async def execute(self, params: LiquidProbeParams) -> _ExecuteReturn:
)


class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurrence]):
"""LiquidProbe command model."""
class TryLiquidProbeImplementation(
AbstractCommandImpl[TryLiquidProbeParams, _TryLiquidProbeExecuteReturn]
):
"""The implementation of a `tryLiquidProbe` command."""

def __init__(
self,
movement: MovementHandler,
pipetting: PipettingHandler,
model_utils: ModelUtils,
**kwargs: object,
) -> None:
self._movement = movement
self._pipetting = pipetting
self._model_utils = model_utils

async def execute(self, params: _CommonParams) -> _TryLiquidProbeExecuteReturn:
"""Execute a `tryLiquidProbe` command.

`tryLiquidProbe` is identical to `liquidProbe`, except that if no liquid is
found, `tryLiquidProbe` returns a success result with `z_position=null` instead
of a defined error.
"""
# We defer to the `liquidProbe` implementation. If it returns a defined
# `liquidNotFound` error, we remap that to a success result.
# Otherwise, we return the result or propagate the exception unchanged.

original_impl = LiquidProbeImplementation(
movement=self._movement,
pipetting=self._pipetting,
model_utils=self._model_utils,
)
original_result = await original_impl.execute(params)

match original_result:
case DefinedErrorData(
public=LiquidNotFoundError(),
private=LiquidNotFoundErrorInternalData() as original_private,
):
return SuccessData(
public=TryLiquidProbeResult(
z_position=None,
position=original_private.position,
),
private=None,
)
case SuccessData(
public=LiquidProbeResult() as original_public, private=None
):
return SuccessData(
public=TryLiquidProbeResult(
position=original_public.position,
z_position=original_public.z_position,
),
private=None,
)


class LiquidProbe(
BaseCommand[LiquidProbeParams, LiquidProbeResult, LiquidNotFoundError]
):
"""The model for a full `liquidProbe` command."""

commandType: LiquidProbeCommandType = "liquidProbe"
params: LiquidProbeParams
Expand All @@ -149,10 +242,33 @@ class LiquidProbe(BaseCommand[LiquidProbeParams, LiquidProbeResult, ErrorOccurre
_ImplementationCls: Type[LiquidProbeImplementation] = LiquidProbeImplementation


class TryLiquidProbe(
BaseCommand[TryLiquidProbeParams, TryLiquidProbeResult, ErrorOccurrence]
):
"""The model for a full `tryLiquidProbe` command."""

commandType: TryLiquidProbeCommandType = "tryLiquidProbe"
params: TryLiquidProbeParams
result: Optional[TryLiquidProbeResult]

_ImplementationCls: Type[
TryLiquidProbeImplementation
] = TryLiquidProbeImplementation


class LiquidProbeCreate(BaseCommandCreate[LiquidProbeParams]):
"""Create LiquidProbe command request model."""
"""The request model for a `liquidProbe` command."""

commandType: LiquidProbeCommandType = "liquidProbe"
params: LiquidProbeParams

_CommandCls: Type[LiquidProbe] = LiquidProbe


class TryLiquidProbeCreate(BaseCommandCreate[TryLiquidProbeParams]):
"""The request model for a `tryLiquidProbe` command."""

commandType: TryLiquidProbeCommandType = "tryLiquidProbe"
params: TryLiquidProbeParams

_CommandCls: Type[TryLiquidProbe] = TryLiquidProbe
Loading
Loading