Skip to content

Commit

Permalink
Merge branch 'edge' into chore_update-pydantic-v2
Browse files Browse the repository at this point in the history
  • Loading branch information
sfoster1 committed Sep 5, 2024
2 parents 7fecd09 + 9ea3ebc commit b0c849f
Show file tree
Hide file tree
Showing 162 changed files with 6,794 additions and 1,824 deletions.
3 changes: 1 addition & 2 deletions api/docs/v2/parameters/use_case_cherrypicking.rst
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,6 @@ The entire start of the ``run()`` function, including a pipette and fixed labwar
)
# load trash bin
trash = protocol.load_trash_bin("A3")
)
# load destination plate in deck slot C2
dest_plate = protocol.load_labware(
load_name="opentrons_96_wellplate_200ul_pcr_full_skirt",
Expand Down Expand Up @@ -175,4 +174,4 @@ The last piece of information needed is the destination well. We take the index

With all the information gathered and stored in variables, all that's left is to pass that information as the arguments of ``transfer()``. With our example file, this will execute three transfers. By using a different CSV at run time, this code could complete up to 96 transfers (at which point it would run out of both tips and destination wells).

For more complex transfer behavior — such as adjusting location within the well — you could extend the CSV format and the associated code to work with additional data. And check out the `verified cherrypicking protocol <https://library.opentrons.com/p/flex-custom-parameters-cherrypicking>`_ in the Opentrons Protocol Library for further automation based on CSV data, including loading different types of plates, automatically loading tip racks, and more.
For more complex transfer behavior — such as adjusting location within the well — you could extend the CSV format and the associated code to work with additional data. And check out the `verified cherrypicking protocol <https://library.opentrons.com/p/flex-custom-parameters-cherrypicking>`_ in the Opentrons Protocol Library for further automation based on CSV data, including loading different types of plates, automatically loading tip racks, and more.
Original file line number Diff line number Diff line change
Expand Up @@ -204,6 +204,10 @@ async def disengage_axes(self, which: List[Axis]) -> None:
"""Disengage some axes."""
...

async def engage_axes(self, which: List[Axis]) -> None:
"""Engage some axes."""
...

async def retract(self, mount: MountArgType, margin: float = 10) -> None:
"""Pull the specified mount up to its home position.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -108,10 +108,15 @@ async def execute(
await ot3_api.move_axes(
{
Axis.Z_L: max_motion_range + _LEFT_MOUNT_Z_MARGIN,
}
)
await ot3_api.disengage_axes([Axis.Z_L])
await ot3_api.move_axes(
{
Axis.Z_R: max_motion_range + _RIGHT_MOUNT_Z_MARGIN,
}
)
await ot3_api.disengage_axes([Axis.Z_L, Axis.Z_R])
await ot3_api.disengage_axes([Axis.Z_R])

return SuccessData(public=MoveToMaintenancePositionResult(), private=None)

Expand Down
5 changes: 5 additions & 0 deletions api/src/opentrons/protocol_engine/commands/command_unions.py
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@
unsafe.UnsafeBlowOutInPlace,
unsafe.UnsafeDropTipInPlace,
unsafe.UpdatePositionEstimators,
unsafe.UnsafeEngageAxes,
],
Field(discriminator="commandType"),
]
Expand Down Expand Up @@ -467,6 +468,7 @@
unsafe.UnsafeBlowOutInPlaceParams,
unsafe.UnsafeDropTipInPlaceParams,
unsafe.UpdatePositionEstimatorsParams,
unsafe.UnsafeEngageAxesParams,
]

CommandType = Union[
Expand Down Expand Up @@ -539,6 +541,7 @@
unsafe.UnsafeBlowOutInPlaceCommandType,
unsafe.UnsafeDropTipInPlaceCommandType,
unsafe.UpdatePositionEstimatorsCommandType,
unsafe.UnsafeEngageAxesCommandType,
]

CommandCreate = Annotated[
Expand Down Expand Up @@ -612,6 +615,7 @@
unsafe.UnsafeBlowOutInPlaceCreate,
unsafe.UnsafeDropTipInPlaceCreate,
unsafe.UpdatePositionEstimatorsCreate,
unsafe.UnsafeEngageAxesCreate,
],
Field(discriminator="commandType"),
]
Expand Down Expand Up @@ -693,6 +697,7 @@
unsafe.UnsafeBlowOutInPlaceResult,
unsafe.UnsafeDropTipInPlaceResult,
unsafe.UpdatePositionEstimatorsResult,
unsafe.UnsafeEngageAxesResult,
]

# todo(mm, 2024-06-12): Ideally, command return types would have specific
Expand Down
14 changes: 14 additions & 0 deletions api/src/opentrons/protocol_engine/commands/unsafe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,14 @@
UpdatePositionEstimatorsCreate,
)

from .unsafe_engage_axes import (
UnsafeEngageAxesCommandType,
UnsafeEngageAxesParams,
UnsafeEngageAxesResult,
UnsafeEngageAxes,
UnsafeEngageAxesCreate,
)

__all__ = [
# Unsafe blow-out-in-place command models
"UnsafeBlowOutInPlaceCommandType",
Expand All @@ -42,4 +50,10 @@
"UpdatePositionEstimatorsResult",
"UpdatePositionEstimators",
"UpdatePositionEstimatorsCreate",
# Unsafe engage axes
"UnsafeEngageAxesCommandType",
"UnsafeEngageAxesParams",
"UnsafeEngageAxesResult",
"UnsafeEngageAxes",
"UnsafeEngageAxesCreate",
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
"""Update position estimators payload, result, and implementaiton."""

from __future__ import annotations
from pydantic import BaseModel, Field
from typing import TYPE_CHECKING, Optional, List, Type
from typing_extensions import Literal

from ...types import MotorAxis
from ..command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData
from ...errors.error_occurrence import ErrorOccurrence
from ...resources import ensure_ot3_hardware

from opentrons.hardware_control import HardwareControlAPI

if TYPE_CHECKING:
from ...execution import GantryMover


UnsafeEngageAxesCommandType = Literal["unsafe/engageAxes"]


class UnsafeEngageAxesParams(BaseModel):
"""Payload required for an UnsafeEngageAxes command."""

axes: List[MotorAxis] = Field(..., description="The axes for which to enable.")


class UnsafeEngageAxesResult(BaseModel):
"""Result data from the execution of an UnsafeEngageAxes command."""


class UnsafeEngageAxesImplementation(
AbstractCommandImpl[
UnsafeEngageAxesParams,
SuccessData[UnsafeEngageAxesResult, None],
]
):
"""Enable axes command implementation."""

def __init__(
self,
hardware_api: HardwareControlAPI,
gantry_mover: GantryMover,
**kwargs: object,
) -> None:
self._hardware_api = hardware_api
self._gantry_mover = gantry_mover

async def execute(
self, params: UnsafeEngageAxesParams
) -> SuccessData[UnsafeEngageAxesResult, None]:
"""Enable exes."""
ot3_hardware_api = ensure_ot3_hardware(self._hardware_api)
await ot3_hardware_api.engage_axes(
[
self._gantry_mover.motor_axis_to_hardware_axis(axis)
for axis in params.axes
]
)
return SuccessData(public=UnsafeEngageAxesResult(), private=None)


class UnsafeEngageAxes(
BaseCommand[UnsafeEngageAxesParams, UnsafeEngageAxesResult, ErrorOccurrence]
):
"""UnsafeEngageAxes command model."""

commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes"
params: UnsafeEngageAxesParams
result: Optional[UnsafeEngageAxesResult]

_ImplementationCls: Type[
UnsafeEngageAxesImplementation
] = UnsafeEngageAxesImplementation


class UnsafeEngageAxesCreate(BaseCommandCreate[UnsafeEngageAxesParams]):
"""UnsafeEngageAxes command request model."""

commandType: UnsafeEngageAxesCommandType = "unsafe/engageAxes"
params: UnsafeEngageAxesParams

_CommandCls: Type[UnsafeEngageAxes] = UnsafeEngageAxes
Original file line number Diff line number Diff line change
Expand Up @@ -84,4 +84,11 @@ def parse_as_csv(
rows.append(row)
except (UnicodeDecodeError, csv.Error):
raise ParameterValueError("Cannot parse provided CSV contents.")
return self._remove_trailing_empty_rows(rows)

@staticmethod
def _remove_trailing_empty_rows(rows: List[List[str]]) -> List[List[str]]:
"""Removes any trailing empty rows."""
while rows and rows[-1] == []:
rows.pop()
return rows
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ def subject(

@pytest.mark.ot3_only
@pytest.mark.parametrize("mount_type", [MountType.LEFT, MountType.RIGHT])
async def test_calibration_move_to_location_implementatio_for_attach_instrument(
async def test_calibration_move_to_location_implementation_for_attach_instrument(
decoy: Decoy,
subject: MoveToMaintenancePositionImplementation,
state_view: StateView,
Expand Down Expand Up @@ -79,7 +79,7 @@ async def test_calibration_move_to_location_implementatio_for_attach_instrument(

@pytest.mark.ot3_only
@pytest.mark.parametrize("mount_type", [MountType.LEFT, MountType.RIGHT])
async def test_calibration_move_to_location_implementatio_for_attach_plate(
async def test_calibration_move_to_location_implementation_for_attach_plate(
decoy: Decoy,
subject: MoveToMaintenancePositionImplementation,
state_view: StateView,
Expand Down Expand Up @@ -113,11 +113,18 @@ async def test_calibration_move_to_location_implementatio_for_attach_plate(
await ot3_hardware_api.move_axes(
position={
Axis.Z_L: 90,
}
),
await ot3_hardware_api.disengage_axes(
[Axis.Z_L],
),
await ot3_hardware_api.move_axes(
position={
Axis.Z_R: 105,
}
),
await ot3_hardware_api.disengage_axes(
[Axis.Z_L, Axis.Z_R],
[Axis.Z_R],
),
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""Test update-position-estimator commands."""
from decoy import Decoy

from opentrons.protocol_engine.commands.unsafe.unsafe_engage_axes import (
UnsafeEngageAxesParams,
UnsafeEngageAxesResult,
UnsafeEngageAxesImplementation,
)
from opentrons.protocol_engine.commands.command import SuccessData
from opentrons.protocol_engine.execution import GantryMover
from opentrons.protocol_engine.types import MotorAxis
from opentrons.hardware_control import OT3HardwareControlAPI
from opentrons.hardware_control.types import Axis


async def test_engage_axes_implementation(
decoy: Decoy, ot3_hardware_api: OT3HardwareControlAPI, gantry_mover: GantryMover
) -> None:
"""Test EngageAxes command execution."""
subject = UnsafeEngageAxesImplementation(
hardware_api=ot3_hardware_api, gantry_mover=gantry_mover
)

data = UnsafeEngageAxesParams(
axes=[MotorAxis.LEFT_Z, MotorAxis.LEFT_PLUNGER, MotorAxis.X, MotorAxis.Y]
)

decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_Z)).then_return(
Axis.Z_L
)
decoy.when(
gantry_mover.motor_axis_to_hardware_axis(MotorAxis.LEFT_PLUNGER)
).then_return(Axis.P_L)
decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.X)).then_return(
Axis.X
)
decoy.when(gantry_mover.motor_axis_to_hardware_axis(MotorAxis.Y)).then_return(
Axis.Y
)
decoy.when(
await ot3_hardware_api.update_axis_position_estimations(
[Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]
)
).then_return(None)

result = await subject.execute(data)

assert result == SuccessData(public=UnsafeEngageAxesResult(), private=None)

decoy.verify(
await ot3_hardware_api.engage_axes([Axis.Z_L, Axis.P_L, Axis.X, Axis.Y]),
)
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
from typing import List, Tuple
import pytest
import platform
from decoy import Decoy
Expand Down Expand Up @@ -44,6 +45,42 @@ def csv_file_different_delimiter() -> bytes:
return b"x:y:z\na,:1,:2\nb,:3,:4\nc,:5,:6"


@pytest.fixture
def csv_file_basic_trailing_empty() -> Tuple[bytes, List[List[str]]]:
"""A basic CSV file with quotes around strings and a trailing newline."""
return (
b'"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6\n',
[["x", "y", "z"], ["a", "1", "2"], ["b", "3", "4"], ["c", "5", "6"]],
)


@pytest.fixture
def csv_file_basic_three_trailing_empty() -> Tuple[bytes, List[List[str]]]:
"""A basic CSV file with quotes around strings and three trailing newlines."""
return (
b'"x","y","z"\n"a",1,2\n"b",3,4\n"c",5,6\n\n\n',
[["x", "y", "z"], ["a", "1", "2"], ["b", "3", "4"], ["c", "5", "6"]],
)


@pytest.fixture
def csv_file_empty_row_and_trailing_empty() -> Tuple[bytes, List[List[str]]]:
"""A basic CSV file with quotes around strings, an empty row, and a trailing newline."""
return (
b'"x","y","z"\n\n"b",3,4\n"c",5,6\n',
[["x", "y", "z"], [], ["b", "3", "4"], ["c", "5", "6"]],
)


@pytest.fixture
def csv_file_windows_empty_row_trailing_empty() -> Tuple[bytes, List[List[str]]]:
"""A basic CSV file with quotes around strings, Windows-style newlines, an empty row, and a trailing newline."""
return (
b'"x","y","z"\r\n\r\n"b",3,4\r\n"c",5,6\r\n',
[["x", "y", "z"], [], ["b", "3", "4"], ["c", "5", "6"]],
)


def test_csv_parameter(
decoy: Decoy, api_version: APIVersion, csv_file_basic: bytes
) -> None:
Expand Down Expand Up @@ -125,3 +162,35 @@ def test_csv_parameter_dont_detect_dialect(

assert rows[0] == ["x", ' "y"', ' "z"']
assert rows[1] == ["a", " 1", " 2"]


@pytest.mark.parametrize(
"csv_file_fixture",
[
"csv_file_basic_trailing_empty",
"csv_file_basic_three_trailing_empty",
"csv_file_empty_row_and_trailing_empty",
"csv_file_windows_empty_row_trailing_empty",
],
)
def test_csv_parameter_trailing_empties(
decoy: Decoy,
api_version: APIVersion,
request: pytest.FixtureRequest,
csv_file_fixture: str,
) -> None:
"""It should load the rows as all strings. Empty rows are allowed in the middle of the data but all trailing empty rows are removed."""
# Get the fixture value
csv_file: bytes
expected_output: List[List[str]]
csv_file, expected_output = request.getfixturevalue(csv_file_fixture)

subject = CSVParameter(csv_file, api_version)
parsed_csv = subject.parse_as_csv()

assert (
parsed_csv == expected_output
), f"Expected {expected_output}, but got {parsed_csv}"
assert len(parsed_csv) == len(
expected_output
), f"Expected {len(expected_output)} rows, but got {len(parsed_csv)}"
Loading

0 comments on commit b0c849f

Please sign in to comment.