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

fix(api): ER support for in place commands and blow out #16510

Merged
merged 5 commits into from
Oct 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -92,14 +92,14 @@ async def execute(self, params: AspirateInPlaceParams) -> _ExecuteReturn:
" so the plunger can be reset in a known safe position."
)
try:
current_position = await self._gantry_mover.get_position(params.pipetteId)
volume = await self._pipetting.aspirate_in_place(
pipette_id=params.pipetteId,
volume=params.volume,
flow_rate=params.flowRate,
command_note_adder=self._command_note_adder,
)
except PipetteOverpressureError as e:
current_position = await self._gantry_mover.get_position(params.pipetteId)
return DefinedErrorData(
public=OverpressureError(
id=self._model_utils.generate_id(),
Expand Down
69 changes: 53 additions & 16 deletions api/src/opentrons/protocol_engine/commands/blow_out.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
"""Blow-out command request, result, and implementation models."""
from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type
from typing import TYPE_CHECKING, Optional, Type, Union
from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from typing_extensions import Literal


from ..state.update_types import StateUpdate
from ..types import DeckPoint
from .pipetting_common import (
OverpressureError,
PipetteIdMixin,
FlowRateMixin,
WellLocationMixin,
DestinationPositionResult,
)
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from .command import (
AbstractCommandImpl,
BaseCommand,
BaseCommandCreate,
DefinedErrorData,
SuccessData,
)
from ..errors.error_occurrence import ErrorOccurrence

from opentrons.hardware_control import HardwareControlAPI
Expand All @@ -21,6 +29,8 @@
if TYPE_CHECKING:
from ..execution import MovementHandler, PipettingHandler
from ..state.state import StateView
from ..resources import ModelUtils


BlowOutCommandType = Literal["blowout"]

Expand All @@ -37,9 +47,13 @@ class BlowOutResult(DestinationPositionResult):
pass


class BlowOutImplementation(
AbstractCommandImpl[BlowOutParams, SuccessData[BlowOutResult, None]]
):
_ExecuteReturn = Union[
SuccessData[BlowOutResult, None],
DefinedErrorData[OverpressureError],
]


class BlowOutImplementation(AbstractCommandImpl[BlowOutParams, _ExecuteReturn]):
"""BlowOut command implementation."""

def __init__(
Expand All @@ -48,14 +62,16 @@ def __init__(
pipetting: PipettingHandler,
state_view: StateView,
hardware_api: HardwareControlAPI,
model_utils: ModelUtils,
**kwargs: object,
) -> None:
self._movement = movement
self._pipetting = pipetting
self._state_view = state_view
self._hardware_api = hardware_api
self._model_utils = model_utils

async def execute(self, params: BlowOutParams) -> SuccessData[BlowOutResult, None]:
async def execute(self, params: BlowOutParams) -> _ExecuteReturn:
"""Move to and blow-out the requested well."""
state_update = StateUpdate()

Expand All @@ -72,16 +88,37 @@ async def execute(self, params: BlowOutParams) -> SuccessData[BlowOutResult, Non
new_well_name=params.wellName,
new_deck_point=deck_point,
)

await self._pipetting.blow_out_in_place(
pipette_id=params.pipetteId, flow_rate=params.flowRate
)

return SuccessData(
public=BlowOutResult(position=deck_point),
private=None,
state_update=state_update,
)
try:
await self._pipetting.blow_out_in_place(
pipette_id=params.pipetteId, flow_rate=params.flowRate
)
except PipetteOverpressureError as e:
return DefinedErrorData(
public=OverpressureError(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
wrappedErrors=[
ErrorOccurrence.from_failed(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
error=e,
)
],
errorInfo={
"retryLocation": (
x,
y,
z,
)
},
),
)
else:
return SuccessData(
public=BlowOutResult(position=deck_point),
private=None,
state_update=state_update,
)


class BlowOut(BaseCommand[BlowOutParams, BlowOutResult, ErrorOccurrence]):
Expand Down
64 changes: 52 additions & 12 deletions api/src/opentrons/protocol_engine/commands/blow_out_in_place.py
Original file line number Diff line number Diff line change
@@ -1,23 +1,32 @@
"""Blow-out in place command request, result, and implementation models."""

from __future__ import annotations
from typing import TYPE_CHECKING, Optional, Type
from typing import TYPE_CHECKING, Optional, Type, Union
from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
from typing_extensions import Literal
from pydantic import BaseModel

from .pipetting_common import (
OverpressureError,
PipetteIdMixin,
FlowRateMixin,
)
from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from .command import (
AbstractCommandImpl,
BaseCommand,
BaseCommandCreate,
DefinedErrorData,
SuccessData,
)
from ..errors.error_occurrence import ErrorOccurrence

from opentrons.hardware_control import HardwareControlAPI


if TYPE_CHECKING:
from ..execution import PipettingHandler
from ..execution import PipettingHandler, GantryMover
from ..state.state import StateView
from ..resources import ModelUtils


BlowOutInPlaceCommandType = Literal["blowOutInPlace"]
Expand All @@ -35,8 +44,14 @@ class BlowOutInPlaceResult(BaseModel):
pass


_ExecuteReturn = Union[
SuccessData[BlowOutInPlaceResult, None],
DefinedErrorData[OverpressureError],
]


class BlowOutInPlaceImplementation(
AbstractCommandImpl[BlowOutInPlaceParams, SuccessData[BlowOutInPlaceResult, None]]
AbstractCommandImpl[BlowOutInPlaceParams, _ExecuteReturn]
):
"""BlowOutInPlace command implementation."""

Expand All @@ -45,21 +60,46 @@ def __init__(
pipetting: PipettingHandler,
state_view: StateView,
hardware_api: HardwareControlAPI,
model_utils: ModelUtils,
gantry_mover: GantryMover,
**kwargs: object,
) -> None:
self._pipetting = pipetting
self._state_view = state_view
self._hardware_api = hardware_api
self._model_utils = model_utils
self._gantry_mover = gantry_mover

async def execute(
self, params: BlowOutInPlaceParams
) -> SuccessData[BlowOutInPlaceResult, None]:
async def execute(self, params: BlowOutInPlaceParams) -> _ExecuteReturn:
"""Blow-out without moving the pipette."""
await self._pipetting.blow_out_in_place(
pipette_id=params.pipetteId, flow_rate=params.flowRate
)

return SuccessData(public=BlowOutInPlaceResult(), private=None)
try:
current_position = await self._gantry_mover.get_position(params.pipetteId)
await self._pipetting.blow_out_in_place(
pipette_id=params.pipetteId, flow_rate=params.flowRate
)
except PipetteOverpressureError as e:
return DefinedErrorData(
public=OverpressureError(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
wrappedErrors=[
ErrorOccurrence.from_failed(
id=self._model_utils.generate_id(),
createdAt=self._model_utils.get_timestamp(),
error=e,
)
],
errorInfo={
"retryLocation": (
current_position.x,
current_position.y,
current_position.z,
)
},
),
)
else:
return SuccessData(public=BlowOutInPlaceResult(), private=None)


class BlowOutInPlace(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,14 +70,14 @@ def __init__(
async def execute(self, params: DispenseInPlaceParams) -> _ExecuteReturn:
"""Dispense without moving the pipette."""
try:
current_position = await self._gantry_mover.get_position(params.pipetteId)
volume = await self._pipetting.dispense_in_place(
pipette_id=params.pipetteId,
volume=params.volume,
flow_rate=params.flowRate,
push_out=params.pushOut,
)
except PipetteOverpressureError as e:
current_position = await self._gantry_mover.get_position(params.pipetteId)
return DefinedErrorData(
public=OverpressureError(
id=self._model_utils.generate_id(),
Expand Down
83 changes: 76 additions & 7 deletions api/tests/opentrons/protocol_engine/commands/test_blow_out.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Test blow-out command."""
from decoy import Decoy
from datetime import datetime
from decoy import Decoy, matchers

from opentrons.protocol_engine.commands.pipetting_common import OverpressureError
from opentrons.protocol_engine.resources.model_utils import ModelUtils
from opentrons.types import Point
from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint
from opentrons.protocol_engine.state import update_types
Expand All @@ -10,29 +13,41 @@
BlowOutImplementation,
BlowOutParams,
)
from opentrons.protocol_engine.commands.command import SuccessData
from opentrons.protocol_engine.commands.command import DefinedErrorData, SuccessData
from opentrons.protocol_engine.execution import (
MovementHandler,
PipettingHandler,
)
from opentrons.hardware_control import HardwareControlAPI
from opentrons_shared_data.errors.exceptions import PipetteOverpressureError
import pytest


async def test_blow_out_implementation(
decoy: Decoy,
@pytest.fixture
def subject(
state_view: StateView,
hardware_api: HardwareControlAPI,
movement: MovementHandler,
model_utils: ModelUtils,
pipetting: PipettingHandler,
) -> None:
"""Test BlowOut command execution."""
subject = BlowOutImplementation(
) -> BlowOutImplementation:
"""Get the impelementation subject."""
return BlowOutImplementation(
state_view=state_view,
movement=movement,
hardware_api=hardware_api,
pipetting=pipetting,
model_utils=model_utils,
)


async def test_blow_out_implementation(
decoy: Decoy,
movement: MovementHandler,
pipetting: PipettingHandler,
subject: BlowOutImplementation,
) -> None:
"""Test BlowOut command execution."""
location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1))

data = BlowOutParams(
Expand Down Expand Up @@ -73,3 +88,57 @@ async def test_blow_out_implementation(
await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234),
times=1,
)


async def test_overpressure_error(
decoy: Decoy,
pipetting: PipettingHandler,
subject: BlowOutImplementation,
model_utils: ModelUtils,
movement: MovementHandler,
) -> None:
"""It should return an overpressure error if the hardware API indicates that."""
pipette_id = "pipette-id"

error_id = "error-id"
error_timestamp = datetime(year=2020, month=1, day=2)

location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1))

data = BlowOutParams(
pipetteId="pipette-id",
labwareId="labware-id",
wellName="C6",
wellLocation=location,
flowRate=1.234,
)

decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id=pipette_id)).then_return(
True
)

decoy.when(
await pipetting.blow_out_in_place(pipette_id="pipette-id", flow_rate=1.234)
).then_raise(PipetteOverpressureError())

decoy.when(model_utils.generate_id()).then_return(error_id)
decoy.when(model_utils.get_timestamp()).then_return(error_timestamp)
decoy.when(
await movement.move_to_well(
pipette_id="pipette-id",
labware_id="labware-id",
well_name="C6",
well_location=location,
)
).then_return(Point(x=1, y=2, z=3))

result = await subject.execute(data)

assert result == DefinedErrorData(
public=OverpressureError.construct(
id=error_id,
createdAt=error_timestamp,
wrappedErrors=[matchers.Anything()],
errorInfo={"retryLocation": (1, 2, 3)},
),
)
Loading
Loading