From dd74c58b1396dbd19c71dcee73beb5af06d01527 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 19 Sep 2024 13:35:17 -0400 Subject: [PATCH 01/51] initial implementation --- .../protocol_engine/commands/aspirate.py | 7 +- .../protocol_engine/execution/movement.py | 2 + .../protocol_engine/state/geometry.py | 4 + .../opentrons/protocol_engine/state/motion.py | 8 +- api/src/opentrons/protocol_engine/types.py | 7 + .../protocol_engine/commands/test_aspirate.py | 4 + .../execution/test_movement_handler.py | 2 + .../protocol_engine/state/test_motion_view.py | 5 +- shared-data/command/schemas/9.json | 1098 +++++++++++++---- 9 files changed, 888 insertions(+), 249 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index ac0e34424e6..d40cfae5a74 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -86,6 +86,8 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName + well_location = params.wellLocation + volume = params.volume ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( pipette_id=pipette_id @@ -116,8 +118,9 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, - well_location=params.wellLocation, + well_location=well_location, current_well=current_well, + operation_volume=-volume, ) deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) state_update.set_pipette_location( @@ -130,7 +133,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: try: volume_aspirated = await self._pipetting.aspirate_in_place( pipette_id=pipette_id, - volume=params.volume, + volume=volume, flow_rate=params.flowRate, command_note_adder=self._command_note_adder, ) diff --git a/api/src/opentrons/protocol_engine/execution/movement.py b/api/src/opentrons/protocol_engine/execution/movement.py index ae4fe27db10..ccd60760702 100644 --- a/api/src/opentrons/protocol_engine/execution/movement.py +++ b/api/src/opentrons/protocol_engine/execution/movement.py @@ -71,6 +71,7 @@ async def move_to_well( force_direct: bool = False, minimum_z_height: Optional[float] = None, speed: Optional[float] = None, + operation_volume: Optional[float] = None, ) -> Point: """Move to a specific well.""" self._state_store.labware.raise_if_labware_inaccessible_by_pipette( @@ -129,6 +130,7 @@ async def move_to_well( current_well=current_well, force_direct=force_direct, minimum_z_height=minimum_z_height, + operation_volume=operation_volume, ) speed = self._state_store.pipettes.get_movement_speed( diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index a0fef65e7ee..876bf0a4a87 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -420,6 +420,7 @@ def get_well_position( labware_id: str, well_name: str, well_location: Optional[WellLocation] = None, + operation_volume: Optional[float] = None, ) -> Point: """Given relative well location in a labware, get absolute position.""" labware_pos = self.get_labware_position(labware_id) @@ -438,6 +439,9 @@ def get_well_position( labware_id, well_name ) if liquid_height is not None: + # use operation_volume + if operation_volume: + pass offset = offset.copy(update={"z": offset.z + liquid_height}) else: raise errors.LiquidHeightUnknownError( diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index d5c9cee53bc..0787a59831c 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -96,6 +96,7 @@ def get_movement_waypoints_to_well( current_well: Optional[CurrentWell] = None, force_direct: bool = False, minimum_z_height: Optional[float] = None, + operation_volume: Optional[float] = None, ) -> List[motion_planning.Waypoint]: """Calculate waypoints to a destination that's specified as a well.""" location = current_well or self._pipettes.get_current_location() @@ -107,9 +108,10 @@ def get_movement_waypoints_to_well( destination_cp = CriticalPoint.XY_CENTER destination = self._geometry.get_well_position( - labware_id, - well_name, - well_location, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + operation_volume=operation_volume, ) move_type = _move_types.get_move_type_to_well( diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 519d39b6ec7..e3315ef7cb0 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -239,11 +239,18 @@ class WellOffset(BaseModel): z: float = 0 +class WellVolumeOffset(BaseModel): + """A z-offset to account for volume in an operation.""" + + volumeOffset: Union[float, Literal["operationVolume"]] = 0 + + class WellLocation(BaseModel): """A relative location in reference to a well's location.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) + volumeOffset: WellVolumeOffset = Field(default_factory=WellVolumeOffset) class DropTipWellLocation(BaseModel): diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 779242ccb84..795910ee95d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -79,6 +79,7 @@ async def test_aspirate_implementation_no_prep( well_name="A3", well_location=location, current_well=None, + operation_volume=-50, ), ).then_return(Point(x=1, y=2, z=3)) @@ -145,6 +146,7 @@ async def test_aspirate_implementation_with_prep( labware_id="123", well_name="A3", ), + operation_volume=-50, ), ).then_return(Point(x=1, y=2, z=3)) @@ -210,6 +212,7 @@ async def test_aspirate_raises_volume_error( well_name="A3", well_location=location, current_well=None, + operation_volume=-50, ), ).then_return(Point(1, 2, 3)) @@ -267,6 +270,7 @@ async def test_overpressure_error( well_name=well_name, well_location=well_location, current_well=None, + operation_volume=-50, ), ).then_return(position) diff --git a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py index 7737775c4fb..c45211238eb 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_movement_handler.py @@ -149,6 +149,7 @@ async def test_move_to_well( current_well=None, force_direct=True, minimum_z_height=12.3, + operation_volume=None, ) ).then_return( [Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER), Waypoint(Point(4, 5, 6))] @@ -257,6 +258,7 @@ async def test_move_to_well_from_starting_location( well_location=well_location, force_direct=False, minimum_z_height=None, + operation_volume=None, ) ).then_return([Waypoint(Point(1, 2, 3), CriticalPoint.XY_CENTER)]) diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index 703d813373b..e20f88cb68f 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -309,7 +309,7 @@ def test_get_movement_waypoints_to_well_for_y_center( ).then_return(False) decoy.when( - geometry_view.get_well_position("labware-id", "well-name", WellLocation()) + geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None) ).then_return(Point(x=4, y=5, z=6)) decoy.when( @@ -391,7 +391,7 @@ def test_get_movement_waypoints_to_well_for_xy_center( ).then_return(True) decoy.when( - geometry_view.get_well_position("labware-id", "well-name", WellLocation()) + geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None) ).then_return(Point(x=4, y=5, z=6)) decoy.when( @@ -460,6 +460,7 @@ def test_get_movement_waypoints_to_well_raises( labware_id="labware-id", well_name="A1", well_location=None, + operation_volume=None, ) ).then_return(Point(x=4, y=5, z=6)) decoy.when(pipette_view.get_current_location()).then_return(None) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 263330eb6de..11bdbc031b0 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -293,7 +293,12 @@ "WellOrigin": { "title": "WellOrigin", "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", - "enum": ["top", "bottom", "center", "meniscus"], + "enum": [ + "top", + "bottom", + "center", + "meniscus" + ], "type": "string" }, "WellOffset": { @@ -318,6 +323,28 @@ } } }, + "WellVolumeOffset": { + "title": "WellVolumeOffset", + "description": "A z-offset to account for volume in an operation.", + "type": "object", + "properties": { + "volumeOffset": { + "title": "Volumeoffset", + "default": 0, + "anyOf": [ + { + "type": "number" + }, + { + "enum": [ + "operationVolume" + ], + "type": "string" + } + ] + } + } + }, "WellLocation": { "title": "WellLocation", "description": "A relative location in reference to a well's location.", @@ -333,6 +360,9 @@ }, "offset": { "$ref": "#/definitions/WellOffset" + }, + "volumeOffset": { + "$ref": "#/definitions/WellVolumeOffset" } } }, @@ -378,12 +408,22 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "flowRate", + "volume", + "pipetteId" + ] }, "CommandIntent": { "title": "CommandIntent", "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", - "enum": ["protocol", "setup", "fixit"], + "enum": [ + "protocol", + "setup", + "fixit" + ], "type": "string" }, "AspirateCreate": { @@ -394,7 +434,9 @@ "commandType": { "title": "Commandtype", "default": "aspirate", - "enum": ["aspirate"], + "enum": [ + "aspirate" + ], "type": "string" }, "params": { @@ -414,7 +456,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "AspirateInPlaceParams": { "title": "AspirateInPlaceParams", @@ -439,7 +483,11 @@ "type": "string" } }, - "required": ["flowRate", "volume", "pipetteId"] + "required": [ + "flowRate", + "volume", + "pipetteId" + ] }, "AspirateInPlaceCreate": { "title": "AspirateInPlaceCreate", @@ -449,7 +497,9 @@ "commandType": { "title": "Commandtype", "default": "aspirateInPlace", - "enum": ["aspirateInPlace"], + "enum": [ + "aspirateInPlace" + ], "type": "string" }, "params": { @@ -469,7 +519,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CommentParams": { "title": "CommentParams", @@ -482,7 +534,9 @@ "type": "string" } }, - "required": ["message"] + "required": [ + "message" + ] }, "CommentCreate": { "title": "CommentCreate", @@ -492,7 +546,9 @@ "commandType": { "title": "Commandtype", "default": "comment", - "enum": ["comment"], + "enum": [ + "comment" + ], "type": "string" }, "params": { @@ -512,7 +568,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "ConfigureForVolumeParams": { "title": "ConfigureForVolumeParams", @@ -536,7 +594,10 @@ "type": "string" } }, - "required": ["pipetteId", "volume"] + "required": [ + "pipetteId", + "volume" + ] }, "ConfigureForVolumeCreate": { "title": "ConfigureForVolumeCreate", @@ -546,7 +607,9 @@ "commandType": { "title": "Commandtype", "default": "configureForVolume", - "enum": ["configureForVolume"], + "enum": [ + "configureForVolume" + ], "type": "string" }, "params": { @@ -566,7 +629,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "AllNozzleLayoutConfiguration": { "title": "AllNozzleLayoutConfiguration", @@ -576,7 +641,9 @@ "style": { "title": "Style", "default": "ALL", - "enum": ["ALL"], + "enum": [ + "ALL" + ], "type": "string" } } @@ -589,17 +656,26 @@ "style": { "title": "Style", "default": "SINGLE", - "enum": ["SINGLE"], + "enum": [ + "SINGLE" + ], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": ["A1", "H1", "A12", "H12"], + "enum": [ + "A1", + "H1", + "A12", + "H12" + ], "type": "string" } }, - "required": ["primaryNozzle"] + "required": [ + "primaryNozzle" + ] }, "RowNozzleLayoutConfiguration": { "title": "RowNozzleLayoutConfiguration", @@ -609,17 +685,26 @@ "style": { "title": "Style", "default": "ROW", - "enum": ["ROW"], + "enum": [ + "ROW" + ], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": ["A1", "H1", "A12", "H12"], + "enum": [ + "A1", + "H1", + "A12", + "H12" + ], "type": "string" } }, - "required": ["primaryNozzle"] + "required": [ + "primaryNozzle" + ] }, "ColumnNozzleLayoutConfiguration": { "title": "ColumnNozzleLayoutConfiguration", @@ -629,17 +714,26 @@ "style": { "title": "Style", "default": "COLUMN", - "enum": ["COLUMN"], + "enum": [ + "COLUMN" + ], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": ["A1", "H1", "A12", "H12"], + "enum": [ + "A1", + "H1", + "A12", + "H12" + ], "type": "string" } }, - "required": ["primaryNozzle"] + "required": [ + "primaryNozzle" + ] }, "QuadrantNozzleLayoutConfiguration": { "title": "QuadrantNozzleLayoutConfiguration", @@ -649,13 +743,20 @@ "style": { "title": "Style", "default": "QUADRANT", - "enum": ["QUADRANT"], + "enum": [ + "QUADRANT" + ], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": ["A1", "H1", "A12", "H12"], + "enum": [ + "A1", + "H1", + "A12", + "H12" + ], "type": "string" }, "frontRightNozzle": { @@ -671,7 +772,11 @@ "type": "string" } }, - "required": ["primaryNozzle", "frontRightNozzle", "backLeftNozzle"] + "required": [ + "primaryNozzle", + "frontRightNozzle", + "backLeftNozzle" + ] }, "ConfigureNozzleLayoutParams": { "title": "ConfigureNozzleLayoutParams", @@ -704,7 +809,10 @@ ] } }, - "required": ["pipetteId", "configurationParams"] + "required": [ + "pipetteId", + "configurationParams" + ] }, "ConfigureNozzleLayoutCreate": { "title": "ConfigureNozzleLayoutCreate", @@ -714,7 +822,9 @@ "commandType": { "title": "Commandtype", "default": "configureNozzleLayout", - "enum": ["configureNozzleLayout"], + "enum": [ + "configureNozzleLayout" + ], "type": "string" }, "params": { @@ -734,7 +844,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CustomParams": { "title": "CustomParams", @@ -750,7 +862,9 @@ "commandType": { "title": "Commandtype", "default": "custom", - "enum": ["custom"], + "enum": [ + "custom" + ], "type": "string" }, "params": { @@ -770,7 +884,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DispenseParams": { "title": "DispenseParams", @@ -819,7 +935,13 @@ "type": "number" } }, - "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "flowRate", + "volume", + "pipetteId" + ] }, "DispenseCreate": { "title": "DispenseCreate", @@ -829,7 +951,9 @@ "commandType": { "title": "Commandtype", "default": "dispense", - "enum": ["dispense"], + "enum": [ + "dispense" + ], "type": "string" }, "params": { @@ -849,7 +973,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DispenseInPlaceParams": { "title": "DispenseInPlaceParams", @@ -879,7 +1005,11 @@ "type": "number" } }, - "required": ["flowRate", "volume", "pipetteId"] + "required": [ + "flowRate", + "volume", + "pipetteId" + ] }, "DispenseInPlaceCreate": { "title": "DispenseInPlaceCreate", @@ -889,7 +1019,9 @@ "commandType": { "title": "Commandtype", "default": "dispenseInPlace", - "enum": ["dispenseInPlace"], + "enum": [ + "dispenseInPlace" + ], "type": "string" }, "params": { @@ -909,7 +1041,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "BlowOutParams": { "title": "BlowOutParams", @@ -947,7 +1081,12 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "flowRate", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "flowRate", + "pipetteId" + ] }, "BlowOutCreate": { "title": "BlowOutCreate", @@ -957,7 +1096,9 @@ "commandType": { "title": "Commandtype", "default": "blowout", - "enum": ["blowout"], + "enum": [ + "blowout" + ], "type": "string" }, "params": { @@ -977,7 +1118,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "BlowOutInPlaceParams": { "title": "BlowOutInPlaceParams", @@ -996,7 +1139,10 @@ "type": "string" } }, - "required": ["flowRate", "pipetteId"] + "required": [ + "flowRate", + "pipetteId" + ] }, "BlowOutInPlaceCreate": { "title": "BlowOutInPlaceCreate", @@ -1006,7 +1152,9 @@ "commandType": { "title": "Commandtype", "default": "blowOutInPlace", - "enum": ["blowOutInPlace"], + "enum": [ + "blowOutInPlace" + ], "type": "string" }, "params": { @@ -1026,12 +1174,19 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DropTipWellOrigin": { "title": "DropTipWellOrigin", "description": "The origin of a DropTipWellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n DEFAULT: the default drop-tip location of the well,\n based on pipette configuration and length of the tip.", - "enum": ["top", "bottom", "center", "default"], + "enum": [ + "top", + "bottom", + "center", + "default" + ], "type": "string" }, "DropTipWellLocation": { @@ -1093,7 +1248,11 @@ "type": "boolean" } }, - "required": ["pipetteId", "labwareId", "wellName"] + "required": [ + "pipetteId", + "labwareId", + "wellName" + ] }, "DropTipCreate": { "title": "DropTipCreate", @@ -1103,7 +1262,9 @@ "commandType": { "title": "Commandtype", "default": "dropTip", - "enum": ["dropTip"], + "enum": [ + "dropTip" + ], "type": "string" }, "params": { @@ -1123,7 +1284,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DropTipInPlaceParams": { "title": "DropTipInPlaceParams", @@ -1141,7 +1304,9 @@ "type": "boolean" } }, - "required": ["pipetteId"] + "required": [ + "pipetteId" + ] }, "DropTipInPlaceCreate": { "title": "DropTipInPlaceCreate", @@ -1151,7 +1316,9 @@ "commandType": { "title": "Commandtype", "default": "dropTipInPlace", - "enum": ["dropTipInPlace"], + "enum": [ + "dropTipInPlace" + ], "type": "string" }, "params": { @@ -1171,7 +1338,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "MotorAxis": { "title": "MotorAxis", @@ -1191,7 +1360,11 @@ "MountType": { "title": "MountType", "description": "An enumeration.", - "enum": ["left", "right", "extension"], + "enum": [ + "left", + "right", + "extension" + ], "type": "string" }, "HomeParams": { @@ -1224,7 +1397,9 @@ "commandType": { "title": "Commandtype", "default": "home", - "enum": ["home"], + "enum": [ + "home" + ], "type": "string" }, "params": { @@ -1244,7 +1419,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "RetractAxisParams": { "title": "RetractAxisParams", @@ -1260,7 +1437,9 @@ ] } }, - "required": ["axis"] + "required": [ + "axis" + ] }, "RetractAxisCreate": { "title": "RetractAxisCreate", @@ -1270,7 +1449,9 @@ "commandType": { "title": "Commandtype", "default": "retractAxis", - "enum": ["retractAxis"], + "enum": [ + "retractAxis" + ], "type": "string" }, "params": { @@ -1290,7 +1471,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeckSlotName": { "title": "DeckSlotName", @@ -1336,7 +1519,9 @@ ] } }, - "required": ["slotName"] + "required": [ + "slotName" + ] }, "ModuleLocation": { "title": "ModuleLocation", @@ -1349,7 +1534,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "OnLabwareLocation": { "title": "OnLabwareLocation", @@ -1362,7 +1549,9 @@ "type": "string" } }, - "required": ["labwareId"] + "required": [ + "labwareId" + ] }, "AddressableAreaLocation": { "title": "AddressableAreaLocation", @@ -1375,7 +1564,9 @@ "type": "string" } }, - "required": ["addressableAreaName"] + "required": [ + "addressableAreaName" + ] }, "LoadLabwareParams": { "title": "LoadLabwareParams", @@ -1396,7 +1587,9 @@ "$ref": "#/definitions/OnLabwareLocation" }, { - "enum": ["offDeck"], + "enum": [ + "offDeck" + ], "type": "string" }, { @@ -1430,7 +1623,12 @@ "type": "string" } }, - "required": ["location", "loadName", "namespace", "version"] + "required": [ + "location", + "loadName", + "namespace", + "version" + ] }, "LoadLabwareCreate": { "title": "LoadLabwareCreate", @@ -1440,7 +1638,9 @@ "commandType": { "title": "Commandtype", "default": "loadLabware", - "enum": ["loadLabware"], + "enum": [ + "loadLabware" + ], "type": "string" }, "params": { @@ -1460,7 +1660,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "ReloadLabwareParams": { "title": "ReloadLabwareParams", @@ -1473,7 +1675,9 @@ "type": "string" } }, - "required": ["labwareId"] + "required": [ + "labwareId" + ] }, "ReloadLabwareCreate": { "title": "ReloadLabwareCreate", @@ -1483,7 +1687,9 @@ "commandType": { "title": "Commandtype", "default": "reloadLabware", - "enum": ["reloadLabware"], + "enum": [ + "reloadLabware" + ], "type": "string" }, "params": { @@ -1503,7 +1709,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "LoadLiquidParams": { "title": "LoadLiquidParams", @@ -1529,7 +1737,11 @@ } } }, - "required": ["liquidId", "labwareId", "volumeByWell"] + "required": [ + "liquidId", + "labwareId", + "volumeByWell" + ] }, "LoadLiquidCreate": { "title": "LoadLiquidCreate", @@ -1539,7 +1751,9 @@ "commandType": { "title": "Commandtype", "default": "loadLiquid", - "enum": ["loadLiquid"], + "enum": [ + "loadLiquid" + ], "type": "string" }, "params": { @@ -1559,7 +1773,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "ModuleModel": { "title": "ModuleModel", @@ -1605,7 +1821,10 @@ "type": "string" } }, - "required": ["model", "location"] + "required": [ + "model", + "location" + ] }, "LoadModuleCreate": { "title": "LoadModuleCreate", @@ -1615,7 +1834,9 @@ "commandType": { "title": "Commandtype", "default": "loadModule", - "enum": ["loadModule"], + "enum": [ + "loadModule" + ], "type": "string" }, "params": { @@ -1635,7 +1856,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "PipetteNameType": { "title": "PipetteNameType", @@ -1698,7 +1921,10 @@ "type": "boolean" } }, - "required": ["pipetteName", "mount"] + "required": [ + "pipetteName", + "mount" + ] }, "LoadPipetteCreate": { "title": "LoadPipetteCreate", @@ -1708,7 +1934,9 @@ "commandType": { "title": "Commandtype", "default": "loadPipette", - "enum": ["loadPipette"], + "enum": [ + "loadPipette" + ], "type": "string" }, "params": { @@ -1728,12 +1956,18 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "LabwareMovementStrategy": { "title": "LabwareMovementStrategy", "description": "Strategy to use for labware movement.", - "enum": ["usingGripper", "manualMoveWithPause", "manualMoveWithoutPause"], + "enum": [ + "usingGripper", + "manualMoveWithPause", + "manualMoveWithoutPause" + ], "type": "string" }, "LabwareOffsetVector": { @@ -1754,7 +1988,11 @@ "type": "number" } }, - "required": ["x", "y", "z"] + "required": [ + "x", + "y", + "z" + ] }, "MoveLabwareParams": { "title": "MoveLabwareParams", @@ -1780,7 +2018,9 @@ "$ref": "#/definitions/OnLabwareLocation" }, { - "enum": ["offDeck"], + "enum": [ + "offDeck" + ], "type": "string" }, { @@ -1815,7 +2055,11 @@ ] } }, - "required": ["labwareId", "newLocation", "strategy"] + "required": [ + "labwareId", + "newLocation", + "strategy" + ] }, "MoveLabwareCreate": { "title": "MoveLabwareCreate", @@ -1825,7 +2069,9 @@ "commandType": { "title": "Commandtype", "default": "moveLabware", - "enum": ["moveLabware"], + "enum": [ + "moveLabware" + ], "type": "string" }, "params": { @@ -1845,12 +2091,18 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "MovementAxis": { "title": "MovementAxis", "description": "Axis on which to issue a relative movement.", - "enum": ["x", "y", "z"], + "enum": [ + "x", + "y", + "z" + ], "type": "string" }, "MoveRelativeParams": { @@ -1877,7 +2129,11 @@ "type": "number" } }, - "required": ["pipetteId", "axis", "distance"] + "required": [ + "pipetteId", + "axis", + "distance" + ] }, "MoveRelativeCreate": { "title": "MoveRelativeCreate", @@ -1887,7 +2143,9 @@ "commandType": { "title": "Commandtype", "default": "moveRelative", - "enum": ["moveRelative"], + "enum": [ + "moveRelative" + ], "type": "string" }, "params": { @@ -1907,7 +2165,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeckPoint": { "title": "DeckPoint", @@ -1927,7 +2187,11 @@ "type": "number" } }, - "required": ["x", "y", "z"] + "required": [ + "x", + "y", + "z" + ] }, "MoveToCoordinatesParams": { "title": "MoveToCoordinatesParams", @@ -1965,7 +2229,10 @@ ] } }, - "required": ["pipetteId", "coordinates"] + "required": [ + "pipetteId", + "coordinates" + ] }, "MoveToCoordinatesCreate": { "title": "MoveToCoordinatesCreate", @@ -1975,7 +2242,9 @@ "commandType": { "title": "Commandtype", "default": "moveToCoordinates", - "enum": ["moveToCoordinates"], + "enum": [ + "moveToCoordinates" + ], "type": "string" }, "params": { @@ -1995,7 +2264,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "MoveToWellParams": { "title": "MoveToWellParams", @@ -2043,7 +2314,11 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "pipetteId" + ] }, "MoveToWellCreate": { "title": "MoveToWellCreate", @@ -2053,7 +2328,9 @@ "commandType": { "title": "Commandtype", "default": "moveToWell", - "enum": ["moveToWell"], + "enum": [ + "moveToWell" + ], "type": "string" }, "params": { @@ -2073,7 +2350,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "AddressableOffsetVector": { "title": "AddressableOffsetVector", @@ -2093,7 +2372,11 @@ "type": "number" } }, - "required": ["x", "y", "z"] + "required": [ + "x", + "y", + "z" + ] }, "MoveToAddressableAreaParams": { "title": "MoveToAddressableAreaParams", @@ -2147,7 +2430,10 @@ "type": "boolean" } }, - "required": ["pipetteId", "addressableAreaName"] + "required": [ + "pipetteId", + "addressableAreaName" + ] }, "MoveToAddressableAreaCreate": { "title": "MoveToAddressableAreaCreate", @@ -2157,7 +2443,9 @@ "commandType": { "title": "Commandtype", "default": "moveToAddressableArea", - "enum": ["moveToAddressableArea"], + "enum": [ + "moveToAddressableArea" + ], "type": "string" }, "params": { @@ -2177,7 +2465,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "MoveToAddressableAreaForDropTipParams": { "title": "MoveToAddressableAreaForDropTipParams", @@ -2237,7 +2527,10 @@ "type": "boolean" } }, - "required": ["pipetteId", "addressableAreaName"] + "required": [ + "pipetteId", + "addressableAreaName" + ] }, "MoveToAddressableAreaForDropTipCreate": { "title": "MoveToAddressableAreaForDropTipCreate", @@ -2247,7 +2540,9 @@ "commandType": { "title": "Commandtype", "default": "moveToAddressableAreaForDropTip", - "enum": ["moveToAddressableAreaForDropTip"], + "enum": [ + "moveToAddressableAreaForDropTip" + ], "type": "string" }, "params": { @@ -2267,7 +2562,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "PrepareToAspirateParams": { "title": "PrepareToAspirateParams", @@ -2280,7 +2577,9 @@ "type": "string" } }, - "required": ["pipetteId"] + "required": [ + "pipetteId" + ] }, "PrepareToAspirateCreate": { "title": "PrepareToAspirateCreate", @@ -2290,7 +2589,9 @@ "commandType": { "title": "Commandtype", "default": "prepareToAspirate", - "enum": ["prepareToAspirate"], + "enum": [ + "prepareToAspirate" + ], "type": "string" }, "params": { @@ -2310,7 +2611,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "WaitForResumeParams": { "title": "WaitForResumeParams", @@ -2332,7 +2635,10 @@ "commandType": { "title": "Commandtype", "default": "waitForResume", - "enum": ["waitForResume", "pause"], + "enum": [ + "waitForResume", + "pause" + ], "type": "string" }, "params": { @@ -2352,7 +2658,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "WaitForDurationParams": { "title": "WaitForDurationParams", @@ -2370,7 +2678,9 @@ "type": "string" } }, - "required": ["seconds"] + "required": [ + "seconds" + ] }, "WaitForDurationCreate": { "title": "WaitForDurationCreate", @@ -2380,7 +2690,9 @@ "commandType": { "title": "Commandtype", "default": "waitForDuration", - "enum": ["waitForDuration"], + "enum": [ + "waitForDuration" + ], "type": "string" }, "params": { @@ -2400,7 +2712,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "PickUpTipParams": { "title": "PickUpTipParams", @@ -2432,7 +2746,11 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "pipetteId" + ] }, "PickUpTipCreate": { "title": "PickUpTipCreate", @@ -2442,7 +2760,9 @@ "commandType": { "title": "Commandtype", "default": "pickUpTip", - "enum": ["pickUpTip"], + "enum": [ + "pickUpTip" + ], "type": "string" }, "params": { @@ -2462,7 +2782,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "SavePositionParams": { "title": "SavePositionParams", @@ -2486,7 +2808,9 @@ "type": "boolean" } }, - "required": ["pipetteId"] + "required": [ + "pipetteId" + ] }, "SavePositionCreate": { "title": "SavePositionCreate", @@ -2496,7 +2820,9 @@ "commandType": { "title": "Commandtype", "default": "savePosition", - "enum": ["savePosition"], + "enum": [ + "savePosition" + ], "type": "string" }, "params": { @@ -2516,7 +2842,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "SetRailLightsParams": { "title": "SetRailLightsParams", @@ -2529,7 +2857,9 @@ "type": "boolean" } }, - "required": ["on"] + "required": [ + "on" + ] }, "SetRailLightsCreate": { "title": "SetRailLightsCreate", @@ -2539,7 +2869,9 @@ "commandType": { "title": "Commandtype", "default": "setRailLights", - "enum": ["setRailLights"], + "enum": [ + "setRailLights" + ], "type": "string" }, "params": { @@ -2559,7 +2891,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "TouchTipParams": { "title": "TouchTipParams", @@ -2602,7 +2936,11 @@ "type": "number" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "pipetteId" + ] }, "TouchTipCreate": { "title": "TouchTipCreate", @@ -2612,7 +2950,9 @@ "commandType": { "title": "Commandtype", "default": "touchTip", - "enum": ["touchTip"], + "enum": [ + "touchTip" + ], "type": "string" }, "params": { @@ -2632,12 +2972,20 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "StatusBarAnimation": { "title": "StatusBarAnimation", "description": "Status Bar animation options.", - "enum": ["idle", "confirm", "updating", "disco", "off"] + "enum": [ + "idle", + "confirm", + "updating", + "disco", + "off" + ] }, "SetStatusBarParams": { "title": "SetStatusBarParams", @@ -2653,7 +3001,9 @@ ] } }, - "required": ["animation"] + "required": [ + "animation" + ] }, "SetStatusBarCreate": { "title": "SetStatusBarCreate", @@ -2663,7 +3013,9 @@ "commandType": { "title": "Commandtype", "default": "setStatusBar", - "enum": ["setStatusBar"], + "enum": [ + "setStatusBar" + ], "type": "string" }, "params": { @@ -2683,18 +3035,28 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "TipPresenceStatus": { "title": "TipPresenceStatus", "description": "Tip presence status reported by a pipette.", - "enum": ["present", "absent", "unknown"], + "enum": [ + "present", + "absent", + "unknown" + ], "type": "string" }, "InstrumentSensorId": { "title": "InstrumentSensorId", "description": "Primary and secondary sensor ids.", - "enum": ["primary", "secondary", "both"], + "enum": [ + "primary", + "secondary", + "both" + ], "type": "string" }, "VerifyTipPresenceParams": { @@ -2724,7 +3086,10 @@ ] } }, - "required": ["pipetteId", "expectedState"] + "required": [ + "pipetteId", + "expectedState" + ] }, "VerifyTipPresenceCreate": { "title": "VerifyTipPresenceCreate", @@ -2734,7 +3099,9 @@ "commandType": { "title": "Commandtype", "default": "verifyTipPresence", - "enum": ["verifyTipPresence"], + "enum": [ + "verifyTipPresence" + ], "type": "string" }, "params": { @@ -2754,7 +3121,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "GetTipPresenceParams": { "title": "GetTipPresenceParams", @@ -2767,7 +3136,9 @@ "type": "string" } }, - "required": ["pipetteId"] + "required": [ + "pipetteId" + ] }, "GetTipPresenceCreate": { "title": "GetTipPresenceCreate", @@ -2777,7 +3148,9 @@ "commandType": { "title": "Commandtype", "default": "getTipPresence", - "enum": ["getTipPresence"], + "enum": [ + "getTipPresence" + ], "type": "string" }, "params": { @@ -2797,7 +3170,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "LiquidProbeParams": { "title": "LiquidProbeParams", @@ -2829,7 +3204,11 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "pipetteId" + ] }, "LiquidProbeCreate": { "title": "LiquidProbeCreate", @@ -2839,7 +3218,9 @@ "commandType": { "title": "Commandtype", "default": "liquidProbe", - "enum": ["liquidProbe"], + "enum": [ + "liquidProbe" + ], "type": "string" }, "params": { @@ -2859,7 +3240,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "TryLiquidProbeParams": { "title": "TryLiquidProbeParams", @@ -2891,7 +3274,11 @@ "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": [ + "labwareId", + "wellName", + "pipetteId" + ] }, "TryLiquidProbeCreate": { "title": "TryLiquidProbeCreate", @@ -2901,7 +3288,9 @@ "commandType": { "title": "Commandtype", "default": "tryLiquidProbe", - "enum": ["tryLiquidProbe"], + "enum": [ + "tryLiquidProbe" + ], "type": "string" }, "params": { @@ -2921,7 +3310,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", @@ -2939,7 +3330,9 @@ "type": "number" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate": { "title": "WaitForTemperatureCreate", @@ -2949,7 +3342,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/waitForTemperature", - "enum": ["heaterShaker/waitForTemperature"], + "enum": [ + "heaterShaker/waitForTemperature" + ], "type": "string" }, "params": { @@ -2969,7 +3364,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureParams": { "title": "SetTargetTemperatureParams", @@ -2987,7 +3384,10 @@ "type": "number" } }, - "required": ["moduleId", "celsius"] + "required": [ + "moduleId", + "celsius" + ] }, "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate": { "title": "SetTargetTemperatureCreate", @@ -2997,7 +3397,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/setTargetTemperature", - "enum": ["heaterShaker/setTargetTemperature"], + "enum": [ + "heaterShaker/setTargetTemperature" + ], "type": "string" }, "params": { @@ -3017,7 +3419,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeactivateHeaterParams": { "title": "DeactivateHeaterParams", @@ -3030,7 +3434,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DeactivateHeaterCreate": { "title": "DeactivateHeaterCreate", @@ -3040,7 +3446,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/deactivateHeater", - "enum": ["heaterShaker/deactivateHeater"], + "enum": [ + "heaterShaker/deactivateHeater" + ], "type": "string" }, "params": { @@ -3060,7 +3468,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "SetAndWaitForShakeSpeedParams": { "title": "SetAndWaitForShakeSpeedParams", @@ -3078,7 +3488,10 @@ "type": "number" } }, - "required": ["moduleId", "rpm"] + "required": [ + "moduleId", + "rpm" + ] }, "SetAndWaitForShakeSpeedCreate": { "title": "SetAndWaitForShakeSpeedCreate", @@ -3088,7 +3501,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/setAndWaitForShakeSpeed", - "enum": ["heaterShaker/setAndWaitForShakeSpeed"], + "enum": [ + "heaterShaker/setAndWaitForShakeSpeed" + ], "type": "string" }, "params": { @@ -3108,7 +3523,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeactivateShakerParams": { "title": "DeactivateShakerParams", @@ -3121,7 +3538,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DeactivateShakerCreate": { "title": "DeactivateShakerCreate", @@ -3131,7 +3550,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/deactivateShaker", - "enum": ["heaterShaker/deactivateShaker"], + "enum": [ + "heaterShaker/deactivateShaker" + ], "type": "string" }, "params": { @@ -3151,7 +3572,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "OpenLabwareLatchParams": { "title": "OpenLabwareLatchParams", @@ -3164,7 +3587,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "OpenLabwareLatchCreate": { "title": "OpenLabwareLatchCreate", @@ -3174,7 +3599,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/openLabwareLatch", - "enum": ["heaterShaker/openLabwareLatch"], + "enum": [ + "heaterShaker/openLabwareLatch" + ], "type": "string" }, "params": { @@ -3194,7 +3621,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CloseLabwareLatchParams": { "title": "CloseLabwareLatchParams", @@ -3207,7 +3636,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "CloseLabwareLatchCreate": { "title": "CloseLabwareLatchCreate", @@ -3217,7 +3648,9 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/closeLabwareLatch", - "enum": ["heaterShaker/closeLabwareLatch"], + "enum": [ + "heaterShaker/closeLabwareLatch" + ], "type": "string" }, "params": { @@ -3237,7 +3670,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DisengageParams": { "title": "DisengageParams", @@ -3250,7 +3685,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DisengageCreate": { "title": "DisengageCreate", @@ -3260,7 +3697,9 @@ "commandType": { "title": "Commandtype", "default": "magneticModule/disengage", - "enum": ["magneticModule/disengage"], + "enum": [ + "magneticModule/disengage" + ], "type": "string" }, "params": { @@ -3280,7 +3719,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "EngageParams": { "title": "EngageParams", @@ -3298,7 +3739,10 @@ "type": "number" } }, - "required": ["moduleId", "height"] + "required": [ + "moduleId", + "height" + ] }, "EngageCreate": { "title": "EngageCreate", @@ -3308,7 +3752,9 @@ "commandType": { "title": "Commandtype", "default": "magneticModule/engage", - "enum": ["magneticModule/engage"], + "enum": [ + "magneticModule/engage" + ], "type": "string" }, "params": { @@ -3328,7 +3774,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureParams": { "title": "SetTargetTemperatureParams", @@ -3346,7 +3794,10 @@ "type": "number" } }, - "required": ["moduleId", "celsius"] + "required": [ + "moduleId", + "celsius" + ] }, "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureCreate": { "title": "SetTargetTemperatureCreate", @@ -3356,7 +3807,9 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/setTargetTemperature", - "enum": ["temperatureModule/setTargetTemperature"], + "enum": [ + "temperatureModule/setTargetTemperature" + ], "type": "string" }, "params": { @@ -3376,7 +3829,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", @@ -3394,7 +3849,9 @@ "type": "number" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureCreate": { "title": "WaitForTemperatureCreate", @@ -3404,7 +3861,9 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/waitForTemperature", - "enum": ["temperatureModule/waitForTemperature"], + "enum": [ + "temperatureModule/waitForTemperature" + ], "type": "string" }, "params": { @@ -3424,7 +3883,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeactivateTemperatureParams": { "title": "DeactivateTemperatureParams", @@ -3437,7 +3898,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DeactivateTemperatureCreate": { "title": "DeactivateTemperatureCreate", @@ -3447,7 +3910,9 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/deactivate", - "enum": ["temperatureModule/deactivate"], + "enum": [ + "temperatureModule/deactivate" + ], "type": "string" }, "params": { @@ -3467,7 +3932,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "SetTargetBlockTemperatureParams": { "title": "SetTargetBlockTemperatureParams", @@ -3495,7 +3962,10 @@ "type": "number" } }, - "required": ["moduleId", "celsius"] + "required": [ + "moduleId", + "celsius" + ] }, "SetTargetBlockTemperatureCreate": { "title": "SetTargetBlockTemperatureCreate", @@ -3505,7 +3975,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/setTargetBlockTemperature", - "enum": ["thermocycler/setTargetBlockTemperature"], + "enum": [ + "thermocycler/setTargetBlockTemperature" + ], "type": "string" }, "params": { @@ -3525,7 +3997,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "WaitForBlockTemperatureParams": { "title": "WaitForBlockTemperatureParams", @@ -3538,7 +4012,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "WaitForBlockTemperatureCreate": { "title": "WaitForBlockTemperatureCreate", @@ -3548,7 +4024,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/waitForBlockTemperature", - "enum": ["thermocycler/waitForBlockTemperature"], + "enum": [ + "thermocycler/waitForBlockTemperature" + ], "type": "string" }, "params": { @@ -3568,7 +4046,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "SetTargetLidTemperatureParams": { "title": "SetTargetLidTemperatureParams", @@ -3586,7 +4066,10 @@ "type": "number" } }, - "required": ["moduleId", "celsius"] + "required": [ + "moduleId", + "celsius" + ] }, "SetTargetLidTemperatureCreate": { "title": "SetTargetLidTemperatureCreate", @@ -3596,7 +4079,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/setTargetLidTemperature", - "enum": ["thermocycler/setTargetLidTemperature"], + "enum": [ + "thermocycler/setTargetLidTemperature" + ], "type": "string" }, "params": { @@ -3616,7 +4101,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "WaitForLidTemperatureParams": { "title": "WaitForLidTemperatureParams", @@ -3629,7 +4116,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "WaitForLidTemperatureCreate": { "title": "WaitForLidTemperatureCreate", @@ -3639,7 +4128,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/waitForLidTemperature", - "enum": ["thermocycler/waitForLidTemperature"], + "enum": [ + "thermocycler/waitForLidTemperature" + ], "type": "string" }, "params": { @@ -3659,7 +4150,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeactivateBlockParams": { "title": "DeactivateBlockParams", @@ -3672,7 +4165,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DeactivateBlockCreate": { "title": "DeactivateBlockCreate", @@ -3682,7 +4177,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/deactivateBlock", - "enum": ["thermocycler/deactivateBlock"], + "enum": [ + "thermocycler/deactivateBlock" + ], "type": "string" }, "params": { @@ -3702,7 +4199,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "DeactivateLidParams": { "title": "DeactivateLidParams", @@ -3715,7 +4214,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "DeactivateLidCreate": { "title": "DeactivateLidCreate", @@ -3725,7 +4226,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/deactivateLid", - "enum": ["thermocycler/deactivateLid"], + "enum": [ + "thermocycler/deactivateLid" + ], "type": "string" }, "params": { @@ -3745,7 +4248,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidParams": { "title": "OpenLidParams", @@ -3758,7 +4263,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate": { "title": "OpenLidCreate", @@ -3768,7 +4275,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/openLid", - "enum": ["thermocycler/openLid"], + "enum": [ + "thermocycler/openLid" + ], "type": "string" }, "params": { @@ -3788,7 +4297,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidParams": { "title": "CloseLidParams", @@ -3801,7 +4312,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate": { "title": "CloseLidCreate", @@ -3811,7 +4324,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/closeLid", - "enum": ["thermocycler/closeLid"], + "enum": [ + "thermocycler/closeLid" + ], "type": "string" }, "params": { @@ -3831,7 +4346,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "RunProfileStepParams": { "title": "RunProfileStepParams", @@ -3849,7 +4366,10 @@ "type": "number" } }, - "required": ["celsius", "holdSeconds"] + "required": [ + "celsius", + "holdSeconds" + ] }, "RunProfileParams": { "title": "RunProfileParams", @@ -3875,7 +4395,10 @@ "type": "number" } }, - "required": ["moduleId", "profile"] + "required": [ + "moduleId", + "profile" + ] }, "RunProfileCreate": { "title": "RunProfileCreate", @@ -3885,7 +4408,9 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/runProfile", - "enum": ["thermocycler/runProfile"], + "enum": [ + "thermocycler/runProfile" + ], "type": "string" }, "params": { @@ -3905,7 +4430,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams": { "title": "CloseLidParams", @@ -3918,7 +4445,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate": { "title": "CloseLidCreate", @@ -3928,7 +4457,9 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/closeLid", - "enum": ["absorbanceReader/closeLid"], + "enum": [ + "absorbanceReader/closeLid" + ], "type": "string" }, "params": { @@ -3948,7 +4479,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidParams": { "title": "OpenLidParams", @@ -3961,7 +4494,9 @@ "type": "string" } }, - "required": ["moduleId"] + "required": [ + "moduleId" + ] }, "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate": { "title": "OpenLidCreate", @@ -3971,7 +4506,9 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/openLid", - "enum": ["absorbanceReader/openLid"], + "enum": [ + "absorbanceReader/openLid" + ], "type": "string" }, "params": { @@ -3991,7 +4528,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "InitializeParams": { "title": "InitializeParams", @@ -4009,7 +4548,10 @@ "type": "integer" } }, - "required": ["moduleId", "sampleWavelength"] + "required": [ + "moduleId", + "sampleWavelength" + ] }, "InitializeCreate": { "title": "InitializeCreate", @@ -4019,7 +4561,9 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/initialize", - "enum": ["absorbanceReader/initialize"], + "enum": [ + "absorbanceReader/initialize" + ], "type": "string" }, "params": { @@ -4039,7 +4583,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "ReadAbsorbanceParams": { "title": "ReadAbsorbanceParams", @@ -4057,7 +4603,10 @@ "type": "integer" } }, - "required": ["moduleId", "sampleWavelength"] + "required": [ + "moduleId", + "sampleWavelength" + ] }, "ReadAbsorbanceCreate": { "title": "ReadAbsorbanceCreate", @@ -4067,7 +4616,9 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/read", - "enum": ["absorbanceReader/read"], + "enum": [ + "absorbanceReader/read" + ], "type": "string" }, "params": { @@ -4087,12 +4638,17 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CalibrateGripperParamsJaw": { "title": "CalibrateGripperParamsJaw", "description": "An enumeration.", - "enum": ["front", "rear"] + "enum": [ + "front", + "rear" + ] }, "Vec3f": { "title": "Vec3f", @@ -4112,7 +4668,11 @@ "type": "number" } }, - "required": ["x", "y", "z"] + "required": [ + "x", + "y", + "z" + ] }, "CalibrateGripperParams": { "title": "CalibrateGripperParams", @@ -4137,7 +4697,9 @@ ] } }, - "required": ["jaw"] + "required": [ + "jaw" + ] }, "CalibrateGripperCreate": { "title": "CalibrateGripperCreate", @@ -4147,7 +4709,9 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibrateGripper", - "enum": ["calibration/calibrateGripper"], + "enum": [ + "calibration/calibrateGripper" + ], "type": "string" }, "params": { @@ -4167,7 +4731,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CalibratePipetteParams": { "title": "CalibratePipetteParams", @@ -4183,7 +4749,9 @@ ] } }, - "required": ["mount"] + "required": [ + "mount" + ] }, "CalibratePipetteCreate": { "title": "CalibratePipetteCreate", @@ -4193,7 +4761,9 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibratePipette", - "enum": ["calibration/calibratePipette"], + "enum": [ + "calibration/calibratePipette" + ], "type": "string" }, "params": { @@ -4213,7 +4783,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "CalibrateModuleParams": { "title": "CalibrateModuleParams", @@ -4239,7 +4811,11 @@ ] } }, - "required": ["moduleId", "labwareId", "mount"] + "required": [ + "moduleId", + "labwareId", + "mount" + ] }, "CalibrateModuleCreate": { "title": "CalibrateModuleCreate", @@ -4249,7 +4825,9 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibrateModule", - "enum": ["calibration/calibrateModule"], + "enum": [ + "calibration/calibrateModule" + ], "type": "string" }, "params": { @@ -4269,12 +4847,17 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "MaintenancePosition": { "title": "MaintenancePosition", "description": "Maintenance position options.", - "enum": ["attachPlate", "attachInstrument"] + "enum": [ + "attachPlate", + "attachInstrument" + ] }, "MoveToMaintenancePositionParams": { "title": "MoveToMaintenancePositionParams", @@ -4299,7 +4882,9 @@ ] } }, - "required": ["mount"] + "required": [ + "mount" + ] }, "MoveToMaintenancePositionCreate": { "title": "MoveToMaintenancePositionCreate", @@ -4309,7 +4894,9 @@ "commandType": { "title": "Commandtype", "default": "calibration/moveToMaintenancePosition", - "enum": ["calibration/moveToMaintenancePosition"], + "enum": [ + "calibration/moveToMaintenancePosition" + ], "type": "string" }, "params": { @@ -4329,7 +4916,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "UnsafeBlowOutInPlaceParams": { "title": "UnsafeBlowOutInPlaceParams", @@ -4348,7 +4937,10 @@ "type": "string" } }, - "required": ["flowRate", "pipetteId"] + "required": [ + "flowRate", + "pipetteId" + ] }, "UnsafeBlowOutInPlaceCreate": { "title": "UnsafeBlowOutInPlaceCreate", @@ -4358,7 +4950,9 @@ "commandType": { "title": "Commandtype", "default": "unsafe/blowOutInPlace", - "enum": ["unsafe/blowOutInPlace"], + "enum": [ + "unsafe/blowOutInPlace" + ], "type": "string" }, "params": { @@ -4378,7 +4972,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "UnsafeDropTipInPlaceParams": { "title": "UnsafeDropTipInPlaceParams", @@ -4396,7 +4992,9 @@ "type": "boolean" } }, - "required": ["pipetteId"] + "required": [ + "pipetteId" + ] }, "UnsafeDropTipInPlaceCreate": { "title": "UnsafeDropTipInPlaceCreate", @@ -4406,7 +5004,9 @@ "commandType": { "title": "Commandtype", "default": "unsafe/dropTipInPlace", - "enum": ["unsafe/dropTipInPlace"], + "enum": [ + "unsafe/dropTipInPlace" + ], "type": "string" }, "params": { @@ -4426,7 +5026,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "UpdatePositionEstimatorsParams": { "title": "UpdatePositionEstimatorsParams", @@ -4441,7 +5043,9 @@ } } }, - "required": ["axes"] + "required": [ + "axes" + ] }, "UpdatePositionEstimatorsCreate": { "title": "UpdatePositionEstimatorsCreate", @@ -4451,7 +5055,9 @@ "commandType": { "title": "Commandtype", "default": "unsafe/updatePositionEstimators", - "enum": ["unsafe/updatePositionEstimators"], + "enum": [ + "unsafe/updatePositionEstimators" + ], "type": "string" }, "params": { @@ -4471,7 +5077,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] }, "UnsafeEngageAxesParams": { "title": "UnsafeEngageAxesParams", @@ -4486,7 +5094,9 @@ } } }, - "required": ["axes"] + "required": [ + "axes" + ] }, "UnsafeEngageAxesCreate": { "title": "UnsafeEngageAxesCreate", @@ -4496,7 +5106,9 @@ "commandType": { "title": "Commandtype", "default": "unsafe/engageAxes", - "enum": ["unsafe/engageAxes"], + "enum": [ + "unsafe/engageAxes" + ], "type": "string" }, "params": { @@ -4516,7 +5128,9 @@ "type": "string" } }, - "required": ["params"] + "required": [ + "params" + ] } }, "$id": "opentronsCommandSchemaV9", From e21054482b5717f9aed27191bc1d5a8027770316 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 19 Sep 2024 14:49:14 -0400 Subject: [PATCH 02/51] update --- api/src/opentrons/protocol_engine/state/geometry.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 876bf0a4a87..84c4e7f4311 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -435,14 +435,12 @@ def get_well_position( elif well_location.origin == WellOrigin.CENTER: offset = offset.copy(update={"z": offset.z + well_depth / 2.0}) elif well_location.origin == WellOrigin.MENISCUS: - liquid_height = self._wells.get_last_measured_liquid_height( + starting_liquid_height = self._wells.get_last_measured_liquid_height( labware_id, well_name ) - if liquid_height is not None: - # use operation_volume - if operation_volume: - pass - offset = offset.copy(update={"z": offset.z + liquid_height}) + if starting_liquid_height is not None: + height_after_operation = self.get_ending_height(starting_liquid_height, operation_volume) + offset = offset.copy(update={"z": offset.z + height_after_operation}) else: raise errors.LiquidHeightUnknownError( "Must liquid probe before specifying WellOrigin.MENISCUS." From 0deed9a76cdf66fa3fb8b023c1b1ac572b71c4df Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 19 Sep 2024 15:41:09 -0400 Subject: [PATCH 03/51] updated get_well_position --- api/src/opentrons/protocol_engine/state/geometry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 84c4e7f4311..11f7b182df1 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -439,7 +439,7 @@ def get_well_position( labware_id, well_name ) if starting_liquid_height is not None: - height_after_operation = self.get_ending_height(starting_liquid_height, operation_volume) + height_after_operation = self.get_ending_height(starting_liquid_height, well_location.volumeOffset) offset = offset.copy(update={"z": offset.z + height_after_operation}) else: raise errors.LiquidHeightUnknownError( From 8f713dfc6869e1b58b4fd86642b49d84c86ab5d2 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 19 Sep 2024 16:57:50 -0400 Subject: [PATCH 04/51] updated implementation --- .../opentrons/protocol_engine/commands/aspirate.py | 3 +-- api/src/opentrons/protocol_engine/state/geometry.py | 13 +++++++++++-- api/src/opentrons/protocol_engine/types.py | 2 +- .../protocol_engine/state/test_geometry_view.py | 1 + 4 files changed, 14 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index d40cfae5a74..35002bb408c 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -86,7 +86,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName - well_location = params.wellLocation volume = params.volume ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( @@ -118,7 +117,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, - well_location=well_location, + well_location=params.wellLocation, current_well=current_well, operation_volume=-volume, ) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 11f7b182df1..68eeb75448a 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -439,8 +439,17 @@ def get_well_position( labware_id, well_name ) if starting_liquid_height is not None: - height_after_operation = self.get_ending_height(starting_liquid_height, well_location.volumeOffset) - offset = offset.copy(update={"z": offset.z + height_after_operation}) + if well_location.volumeOffset.volumeOffset == "operationVolume": + volume = operation_volume or 0.0 + else: + volume = well_location.volumeOffset.volumeOffset + # height_after_operation = self.get_ending_height(labware_id, well_name, starting_liquid_height, volume) + height_after_operation = starting_liquid_height # delete, use above method once implemented + if volume: # delete + pass # delete + offset = offset.copy( + update={"z": offset.z + height_after_operation} + ) else: raise errors.LiquidHeightUnknownError( "Must liquid probe before specifying WellOrigin.MENISCUS." diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index e3315ef7cb0..bf2fd3e0c97 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -242,7 +242,7 @@ class WellOffset(BaseModel): class WellVolumeOffset(BaseModel): """A z-offset to account for volume in an operation.""" - volumeOffset: Union[float, Literal["operationVolume"]] = 0 + volumeOffset: Union[float, Literal["operationVolume"]] = 0.0 class WellLocation(BaseModel): diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 1854d08523a..d19862969b4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1543,6 +1543,7 @@ def test_get_well_position_with_meniscus_offset( origin=WellOrigin.MENISCUS, offset=WellOffset(x=2, y=3, z=4), ), + operation_volume=0.0, ) assert result == Point( From 64fc6e2848affd1d4b1ad0359b0a287acd283898 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 19 Sep 2024 17:03:51 -0400 Subject: [PATCH 05/51] update schema 9 --- shared-data/command/schemas/9.json | 1079 +++++++--------------------- 1 file changed, 244 insertions(+), 835 deletions(-) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 11bdbc031b0..6ad70fe0cc4 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -293,12 +293,7 @@ "WellOrigin": { "title": "WellOrigin", "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", - "enum": [ - "top", - "bottom", - "center", - "meniscus" - ], + "enum": ["top", "bottom", "center", "meniscus"], "type": "string" }, "WellOffset": { @@ -330,15 +325,13 @@ "properties": { "volumeOffset": { "title": "Volumeoffset", - "default": 0, + "default": 0.0, "anyOf": [ { "type": "number" }, { - "enum": [ - "operationVolume" - ], + "enum": ["operationVolume"], "type": "string" } ] @@ -408,22 +401,12 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "flowRate", - "volume", - "pipetteId" - ] + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"] }, "CommandIntent": { "title": "CommandIntent", "description": "Run intent for a given command.\n\nProps:\n PROTOCOL: the command is part of the protocol run itself.\n SETUP: the command is part of the setup phase of a run.", - "enum": [ - "protocol", - "setup", - "fixit" - ], + "enum": ["protocol", "setup", "fixit"], "type": "string" }, "AspirateCreate": { @@ -434,9 +417,7 @@ "commandType": { "title": "Commandtype", "default": "aspirate", - "enum": [ - "aspirate" - ], + "enum": ["aspirate"], "type": "string" }, "params": { @@ -456,9 +437,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "AspirateInPlaceParams": { "title": "AspirateInPlaceParams", @@ -483,11 +462,7 @@ "type": "string" } }, - "required": [ - "flowRate", - "volume", - "pipetteId" - ] + "required": ["flowRate", "volume", "pipetteId"] }, "AspirateInPlaceCreate": { "title": "AspirateInPlaceCreate", @@ -497,9 +472,7 @@ "commandType": { "title": "Commandtype", "default": "aspirateInPlace", - "enum": [ - "aspirateInPlace" - ], + "enum": ["aspirateInPlace"], "type": "string" }, "params": { @@ -519,9 +492,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CommentParams": { "title": "CommentParams", @@ -534,9 +505,7 @@ "type": "string" } }, - "required": [ - "message" - ] + "required": ["message"] }, "CommentCreate": { "title": "CommentCreate", @@ -546,9 +515,7 @@ "commandType": { "title": "Commandtype", "default": "comment", - "enum": [ - "comment" - ], + "enum": ["comment"], "type": "string" }, "params": { @@ -568,9 +535,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "ConfigureForVolumeParams": { "title": "ConfigureForVolumeParams", @@ -594,10 +559,7 @@ "type": "string" } }, - "required": [ - "pipetteId", - "volume" - ] + "required": ["pipetteId", "volume"] }, "ConfigureForVolumeCreate": { "title": "ConfigureForVolumeCreate", @@ -607,9 +569,7 @@ "commandType": { "title": "Commandtype", "default": "configureForVolume", - "enum": [ - "configureForVolume" - ], + "enum": ["configureForVolume"], "type": "string" }, "params": { @@ -629,9 +589,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "AllNozzleLayoutConfiguration": { "title": "AllNozzleLayoutConfiguration", @@ -641,9 +599,7 @@ "style": { "title": "Style", "default": "ALL", - "enum": [ - "ALL" - ], + "enum": ["ALL"], "type": "string" } } @@ -656,26 +612,17 @@ "style": { "title": "Style", "default": "SINGLE", - "enum": [ - "SINGLE" - ], + "enum": ["SINGLE"], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": [ - "A1", - "H1", - "A12", - "H12" - ], + "enum": ["A1", "H1", "A12", "H12"], "type": "string" } }, - "required": [ - "primaryNozzle" - ] + "required": ["primaryNozzle"] }, "RowNozzleLayoutConfiguration": { "title": "RowNozzleLayoutConfiguration", @@ -685,26 +632,17 @@ "style": { "title": "Style", "default": "ROW", - "enum": [ - "ROW" - ], + "enum": ["ROW"], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": [ - "A1", - "H1", - "A12", - "H12" - ], + "enum": ["A1", "H1", "A12", "H12"], "type": "string" } }, - "required": [ - "primaryNozzle" - ] + "required": ["primaryNozzle"] }, "ColumnNozzleLayoutConfiguration": { "title": "ColumnNozzleLayoutConfiguration", @@ -714,26 +652,17 @@ "style": { "title": "Style", "default": "COLUMN", - "enum": [ - "COLUMN" - ], + "enum": ["COLUMN"], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": [ - "A1", - "H1", - "A12", - "H12" - ], + "enum": ["A1", "H1", "A12", "H12"], "type": "string" } }, - "required": [ - "primaryNozzle" - ] + "required": ["primaryNozzle"] }, "QuadrantNozzleLayoutConfiguration": { "title": "QuadrantNozzleLayoutConfiguration", @@ -743,20 +672,13 @@ "style": { "title": "Style", "default": "QUADRANT", - "enum": [ - "QUADRANT" - ], + "enum": ["QUADRANT"], "type": "string" }, "primaryNozzle": { "title": "Primarynozzle", "description": "The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - "enum": [ - "A1", - "H1", - "A12", - "H12" - ], + "enum": ["A1", "H1", "A12", "H12"], "type": "string" }, "frontRightNozzle": { @@ -772,11 +694,7 @@ "type": "string" } }, - "required": [ - "primaryNozzle", - "frontRightNozzle", - "backLeftNozzle" - ] + "required": ["primaryNozzle", "frontRightNozzle", "backLeftNozzle"] }, "ConfigureNozzleLayoutParams": { "title": "ConfigureNozzleLayoutParams", @@ -809,10 +727,7 @@ ] } }, - "required": [ - "pipetteId", - "configurationParams" - ] + "required": ["pipetteId", "configurationParams"] }, "ConfigureNozzleLayoutCreate": { "title": "ConfigureNozzleLayoutCreate", @@ -822,9 +737,7 @@ "commandType": { "title": "Commandtype", "default": "configureNozzleLayout", - "enum": [ - "configureNozzleLayout" - ], + "enum": ["configureNozzleLayout"], "type": "string" }, "params": { @@ -844,9 +757,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CustomParams": { "title": "CustomParams", @@ -862,9 +773,7 @@ "commandType": { "title": "Commandtype", "default": "custom", - "enum": [ - "custom" - ], + "enum": ["custom"], "type": "string" }, "params": { @@ -884,9 +793,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DispenseParams": { "title": "DispenseParams", @@ -935,13 +842,7 @@ "type": "number" } }, - "required": [ - "labwareId", - "wellName", - "flowRate", - "volume", - "pipetteId" - ] + "required": ["labwareId", "wellName", "flowRate", "volume", "pipetteId"] }, "DispenseCreate": { "title": "DispenseCreate", @@ -951,9 +852,7 @@ "commandType": { "title": "Commandtype", "default": "dispense", - "enum": [ - "dispense" - ], + "enum": ["dispense"], "type": "string" }, "params": { @@ -973,9 +872,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DispenseInPlaceParams": { "title": "DispenseInPlaceParams", @@ -1005,11 +902,7 @@ "type": "number" } }, - "required": [ - "flowRate", - "volume", - "pipetteId" - ] + "required": ["flowRate", "volume", "pipetteId"] }, "DispenseInPlaceCreate": { "title": "DispenseInPlaceCreate", @@ -1019,9 +912,7 @@ "commandType": { "title": "Commandtype", "default": "dispenseInPlace", - "enum": [ - "dispenseInPlace" - ], + "enum": ["dispenseInPlace"], "type": "string" }, "params": { @@ -1041,9 +932,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "BlowOutParams": { "title": "BlowOutParams", @@ -1081,12 +970,7 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "flowRate", - "pipetteId" - ] + "required": ["labwareId", "wellName", "flowRate", "pipetteId"] }, "BlowOutCreate": { "title": "BlowOutCreate", @@ -1096,9 +980,7 @@ "commandType": { "title": "Commandtype", "default": "blowout", - "enum": [ - "blowout" - ], + "enum": ["blowout"], "type": "string" }, "params": { @@ -1118,9 +1000,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "BlowOutInPlaceParams": { "title": "BlowOutInPlaceParams", @@ -1139,10 +1019,7 @@ "type": "string" } }, - "required": [ - "flowRate", - "pipetteId" - ] + "required": ["flowRate", "pipetteId"] }, "BlowOutInPlaceCreate": { "title": "BlowOutInPlaceCreate", @@ -1152,9 +1029,7 @@ "commandType": { "title": "Commandtype", "default": "blowOutInPlace", - "enum": [ - "blowOutInPlace" - ], + "enum": ["blowOutInPlace"], "type": "string" }, "params": { @@ -1174,19 +1049,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DropTipWellOrigin": { "title": "DropTipWellOrigin", "description": "The origin of a DropTipWellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n DEFAULT: the default drop-tip location of the well,\n based on pipette configuration and length of the tip.", - "enum": [ - "top", - "bottom", - "center", - "default" - ], + "enum": ["top", "bottom", "center", "default"], "type": "string" }, "DropTipWellLocation": { @@ -1248,11 +1116,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId", - "labwareId", - "wellName" - ] + "required": ["pipetteId", "labwareId", "wellName"] }, "DropTipCreate": { "title": "DropTipCreate", @@ -1262,9 +1126,7 @@ "commandType": { "title": "Commandtype", "default": "dropTip", - "enum": [ - "dropTip" - ], + "enum": ["dropTip"], "type": "string" }, "params": { @@ -1284,9 +1146,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DropTipInPlaceParams": { "title": "DropTipInPlaceParams", @@ -1304,9 +1164,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId" - ] + "required": ["pipetteId"] }, "DropTipInPlaceCreate": { "title": "DropTipInPlaceCreate", @@ -1316,9 +1174,7 @@ "commandType": { "title": "Commandtype", "default": "dropTipInPlace", - "enum": [ - "dropTipInPlace" - ], + "enum": ["dropTipInPlace"], "type": "string" }, "params": { @@ -1338,9 +1194,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "MotorAxis": { "title": "MotorAxis", @@ -1360,11 +1214,7 @@ "MountType": { "title": "MountType", "description": "An enumeration.", - "enum": [ - "left", - "right", - "extension" - ], + "enum": ["left", "right", "extension"], "type": "string" }, "HomeParams": { @@ -1397,9 +1247,7 @@ "commandType": { "title": "Commandtype", "default": "home", - "enum": [ - "home" - ], + "enum": ["home"], "type": "string" }, "params": { @@ -1419,9 +1267,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "RetractAxisParams": { "title": "RetractAxisParams", @@ -1437,9 +1283,7 @@ ] } }, - "required": [ - "axis" - ] + "required": ["axis"] }, "RetractAxisCreate": { "title": "RetractAxisCreate", @@ -1449,9 +1293,7 @@ "commandType": { "title": "Commandtype", "default": "retractAxis", - "enum": [ - "retractAxis" - ], + "enum": ["retractAxis"], "type": "string" }, "params": { @@ -1471,9 +1313,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeckSlotName": { "title": "DeckSlotName", @@ -1519,9 +1359,7 @@ ] } }, - "required": [ - "slotName" - ] + "required": ["slotName"] }, "ModuleLocation": { "title": "ModuleLocation", @@ -1534,9 +1372,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "OnLabwareLocation": { "title": "OnLabwareLocation", @@ -1549,9 +1385,7 @@ "type": "string" } }, - "required": [ - "labwareId" - ] + "required": ["labwareId"] }, "AddressableAreaLocation": { "title": "AddressableAreaLocation", @@ -1564,9 +1398,7 @@ "type": "string" } }, - "required": [ - "addressableAreaName" - ] + "required": ["addressableAreaName"] }, "LoadLabwareParams": { "title": "LoadLabwareParams", @@ -1587,9 +1419,7 @@ "$ref": "#/definitions/OnLabwareLocation" }, { - "enum": [ - "offDeck" - ], + "enum": ["offDeck"], "type": "string" }, { @@ -1623,12 +1453,7 @@ "type": "string" } }, - "required": [ - "location", - "loadName", - "namespace", - "version" - ] + "required": ["location", "loadName", "namespace", "version"] }, "LoadLabwareCreate": { "title": "LoadLabwareCreate", @@ -1638,9 +1463,7 @@ "commandType": { "title": "Commandtype", "default": "loadLabware", - "enum": [ - "loadLabware" - ], + "enum": ["loadLabware"], "type": "string" }, "params": { @@ -1660,9 +1483,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "ReloadLabwareParams": { "title": "ReloadLabwareParams", @@ -1675,9 +1496,7 @@ "type": "string" } }, - "required": [ - "labwareId" - ] + "required": ["labwareId"] }, "ReloadLabwareCreate": { "title": "ReloadLabwareCreate", @@ -1687,9 +1506,7 @@ "commandType": { "title": "Commandtype", "default": "reloadLabware", - "enum": [ - "reloadLabware" - ], + "enum": ["reloadLabware"], "type": "string" }, "params": { @@ -1709,9 +1526,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "LoadLiquidParams": { "title": "LoadLiquidParams", @@ -1737,11 +1552,7 @@ } } }, - "required": [ - "liquidId", - "labwareId", - "volumeByWell" - ] + "required": ["liquidId", "labwareId", "volumeByWell"] }, "LoadLiquidCreate": { "title": "LoadLiquidCreate", @@ -1751,9 +1562,7 @@ "commandType": { "title": "Commandtype", "default": "loadLiquid", - "enum": [ - "loadLiquid" - ], + "enum": ["loadLiquid"], "type": "string" }, "params": { @@ -1773,9 +1582,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "ModuleModel": { "title": "ModuleModel", @@ -1821,10 +1628,7 @@ "type": "string" } }, - "required": [ - "model", - "location" - ] + "required": ["model", "location"] }, "LoadModuleCreate": { "title": "LoadModuleCreate", @@ -1834,9 +1638,7 @@ "commandType": { "title": "Commandtype", "default": "loadModule", - "enum": [ - "loadModule" - ], + "enum": ["loadModule"], "type": "string" }, "params": { @@ -1856,9 +1658,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "PipetteNameType": { "title": "PipetteNameType", @@ -1921,10 +1721,7 @@ "type": "boolean" } }, - "required": [ - "pipetteName", - "mount" - ] + "required": ["pipetteName", "mount"] }, "LoadPipetteCreate": { "title": "LoadPipetteCreate", @@ -1934,9 +1731,7 @@ "commandType": { "title": "Commandtype", "default": "loadPipette", - "enum": [ - "loadPipette" - ], + "enum": ["loadPipette"], "type": "string" }, "params": { @@ -1956,18 +1751,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "LabwareMovementStrategy": { "title": "LabwareMovementStrategy", "description": "Strategy to use for labware movement.", - "enum": [ - "usingGripper", - "manualMoveWithPause", - "manualMoveWithoutPause" - ], + "enum": ["usingGripper", "manualMoveWithPause", "manualMoveWithoutPause"], "type": "string" }, "LabwareOffsetVector": { @@ -1988,11 +1777,7 @@ "type": "number" } }, - "required": [ - "x", - "y", - "z" - ] + "required": ["x", "y", "z"] }, "MoveLabwareParams": { "title": "MoveLabwareParams", @@ -2018,9 +1803,7 @@ "$ref": "#/definitions/OnLabwareLocation" }, { - "enum": [ - "offDeck" - ], + "enum": ["offDeck"], "type": "string" }, { @@ -2055,11 +1838,7 @@ ] } }, - "required": [ - "labwareId", - "newLocation", - "strategy" - ] + "required": ["labwareId", "newLocation", "strategy"] }, "MoveLabwareCreate": { "title": "MoveLabwareCreate", @@ -2069,9 +1848,7 @@ "commandType": { "title": "Commandtype", "default": "moveLabware", - "enum": [ - "moveLabware" - ], + "enum": ["moveLabware"], "type": "string" }, "params": { @@ -2091,18 +1868,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "MovementAxis": { "title": "MovementAxis", "description": "Axis on which to issue a relative movement.", - "enum": [ - "x", - "y", - "z" - ], + "enum": ["x", "y", "z"], "type": "string" }, "MoveRelativeParams": { @@ -2129,11 +1900,7 @@ "type": "number" } }, - "required": [ - "pipetteId", - "axis", - "distance" - ] + "required": ["pipetteId", "axis", "distance"] }, "MoveRelativeCreate": { "title": "MoveRelativeCreate", @@ -2143,9 +1910,7 @@ "commandType": { "title": "Commandtype", "default": "moveRelative", - "enum": [ - "moveRelative" - ], + "enum": ["moveRelative"], "type": "string" }, "params": { @@ -2165,9 +1930,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeckPoint": { "title": "DeckPoint", @@ -2187,11 +1950,7 @@ "type": "number" } }, - "required": [ - "x", - "y", - "z" - ] + "required": ["x", "y", "z"] }, "MoveToCoordinatesParams": { "title": "MoveToCoordinatesParams", @@ -2229,10 +1988,7 @@ ] } }, - "required": [ - "pipetteId", - "coordinates" - ] + "required": ["pipetteId", "coordinates"] }, "MoveToCoordinatesCreate": { "title": "MoveToCoordinatesCreate", @@ -2242,9 +1998,7 @@ "commandType": { "title": "Commandtype", "default": "moveToCoordinates", - "enum": [ - "moveToCoordinates" - ], + "enum": ["moveToCoordinates"], "type": "string" }, "params": { @@ -2264,9 +2018,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "MoveToWellParams": { "title": "MoveToWellParams", @@ -2314,11 +2066,7 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "pipetteId" - ] + "required": ["labwareId", "wellName", "pipetteId"] }, "MoveToWellCreate": { "title": "MoveToWellCreate", @@ -2328,9 +2076,7 @@ "commandType": { "title": "Commandtype", "default": "moveToWell", - "enum": [ - "moveToWell" - ], + "enum": ["moveToWell"], "type": "string" }, "params": { @@ -2350,9 +2096,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "AddressableOffsetVector": { "title": "AddressableOffsetVector", @@ -2372,11 +2116,7 @@ "type": "number" } }, - "required": [ - "x", - "y", - "z" - ] + "required": ["x", "y", "z"] }, "MoveToAddressableAreaParams": { "title": "MoveToAddressableAreaParams", @@ -2430,10 +2170,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId", - "addressableAreaName" - ] + "required": ["pipetteId", "addressableAreaName"] }, "MoveToAddressableAreaCreate": { "title": "MoveToAddressableAreaCreate", @@ -2443,9 +2180,7 @@ "commandType": { "title": "Commandtype", "default": "moveToAddressableArea", - "enum": [ - "moveToAddressableArea" - ], + "enum": ["moveToAddressableArea"], "type": "string" }, "params": { @@ -2465,9 +2200,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "MoveToAddressableAreaForDropTipParams": { "title": "MoveToAddressableAreaForDropTipParams", @@ -2527,10 +2260,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId", - "addressableAreaName" - ] + "required": ["pipetteId", "addressableAreaName"] }, "MoveToAddressableAreaForDropTipCreate": { "title": "MoveToAddressableAreaForDropTipCreate", @@ -2540,9 +2270,7 @@ "commandType": { "title": "Commandtype", "default": "moveToAddressableAreaForDropTip", - "enum": [ - "moveToAddressableAreaForDropTip" - ], + "enum": ["moveToAddressableAreaForDropTip"], "type": "string" }, "params": { @@ -2562,9 +2290,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "PrepareToAspirateParams": { "title": "PrepareToAspirateParams", @@ -2577,9 +2303,7 @@ "type": "string" } }, - "required": [ - "pipetteId" - ] + "required": ["pipetteId"] }, "PrepareToAspirateCreate": { "title": "PrepareToAspirateCreate", @@ -2589,9 +2313,7 @@ "commandType": { "title": "Commandtype", "default": "prepareToAspirate", - "enum": [ - "prepareToAspirate" - ], + "enum": ["prepareToAspirate"], "type": "string" }, "params": { @@ -2611,9 +2333,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "WaitForResumeParams": { "title": "WaitForResumeParams", @@ -2635,10 +2355,7 @@ "commandType": { "title": "Commandtype", "default": "waitForResume", - "enum": [ - "waitForResume", - "pause" - ], + "enum": ["waitForResume", "pause"], "type": "string" }, "params": { @@ -2658,9 +2375,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "WaitForDurationParams": { "title": "WaitForDurationParams", @@ -2678,9 +2393,7 @@ "type": "string" } }, - "required": [ - "seconds" - ] + "required": ["seconds"] }, "WaitForDurationCreate": { "title": "WaitForDurationCreate", @@ -2690,9 +2403,7 @@ "commandType": { "title": "Commandtype", "default": "waitForDuration", - "enum": [ - "waitForDuration" - ], + "enum": ["waitForDuration"], "type": "string" }, "params": { @@ -2712,9 +2423,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "PickUpTipParams": { "title": "PickUpTipParams", @@ -2746,11 +2455,7 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "pipetteId" - ] + "required": ["labwareId", "wellName", "pipetteId"] }, "PickUpTipCreate": { "title": "PickUpTipCreate", @@ -2760,9 +2465,7 @@ "commandType": { "title": "Commandtype", "default": "pickUpTip", - "enum": [ - "pickUpTip" - ], + "enum": ["pickUpTip"], "type": "string" }, "params": { @@ -2782,9 +2485,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "SavePositionParams": { "title": "SavePositionParams", @@ -2808,9 +2509,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId" - ] + "required": ["pipetteId"] }, "SavePositionCreate": { "title": "SavePositionCreate", @@ -2820,9 +2519,7 @@ "commandType": { "title": "Commandtype", "default": "savePosition", - "enum": [ - "savePosition" - ], + "enum": ["savePosition"], "type": "string" }, "params": { @@ -2842,9 +2539,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "SetRailLightsParams": { "title": "SetRailLightsParams", @@ -2857,9 +2552,7 @@ "type": "boolean" } }, - "required": [ - "on" - ] + "required": ["on"] }, "SetRailLightsCreate": { "title": "SetRailLightsCreate", @@ -2869,9 +2562,7 @@ "commandType": { "title": "Commandtype", "default": "setRailLights", - "enum": [ - "setRailLights" - ], + "enum": ["setRailLights"], "type": "string" }, "params": { @@ -2891,9 +2582,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "TouchTipParams": { "title": "TouchTipParams", @@ -2936,11 +2625,7 @@ "type": "number" } }, - "required": [ - "labwareId", - "wellName", - "pipetteId" - ] + "required": ["labwareId", "wellName", "pipetteId"] }, "TouchTipCreate": { "title": "TouchTipCreate", @@ -2950,9 +2635,7 @@ "commandType": { "title": "Commandtype", "default": "touchTip", - "enum": [ - "touchTip" - ], + "enum": ["touchTip"], "type": "string" }, "params": { @@ -2972,20 +2655,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "StatusBarAnimation": { "title": "StatusBarAnimation", "description": "Status Bar animation options.", - "enum": [ - "idle", - "confirm", - "updating", - "disco", - "off" - ] + "enum": ["idle", "confirm", "updating", "disco", "off"] }, "SetStatusBarParams": { "title": "SetStatusBarParams", @@ -3001,9 +2676,7 @@ ] } }, - "required": [ - "animation" - ] + "required": ["animation"] }, "SetStatusBarCreate": { "title": "SetStatusBarCreate", @@ -3013,9 +2686,7 @@ "commandType": { "title": "Commandtype", "default": "setStatusBar", - "enum": [ - "setStatusBar" - ], + "enum": ["setStatusBar"], "type": "string" }, "params": { @@ -3035,28 +2706,18 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "TipPresenceStatus": { "title": "TipPresenceStatus", "description": "Tip presence status reported by a pipette.", - "enum": [ - "present", - "absent", - "unknown" - ], + "enum": ["present", "absent", "unknown"], "type": "string" }, "InstrumentSensorId": { "title": "InstrumentSensorId", "description": "Primary and secondary sensor ids.", - "enum": [ - "primary", - "secondary", - "both" - ], + "enum": ["primary", "secondary", "both"], "type": "string" }, "VerifyTipPresenceParams": { @@ -3086,10 +2747,7 @@ ] } }, - "required": [ - "pipetteId", - "expectedState" - ] + "required": ["pipetteId", "expectedState"] }, "VerifyTipPresenceCreate": { "title": "VerifyTipPresenceCreate", @@ -3099,9 +2757,7 @@ "commandType": { "title": "Commandtype", "default": "verifyTipPresence", - "enum": [ - "verifyTipPresence" - ], + "enum": ["verifyTipPresence"], "type": "string" }, "params": { @@ -3121,9 +2777,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "GetTipPresenceParams": { "title": "GetTipPresenceParams", @@ -3136,9 +2790,7 @@ "type": "string" } }, - "required": [ - "pipetteId" - ] + "required": ["pipetteId"] }, "GetTipPresenceCreate": { "title": "GetTipPresenceCreate", @@ -3148,9 +2800,7 @@ "commandType": { "title": "Commandtype", "default": "getTipPresence", - "enum": [ - "getTipPresence" - ], + "enum": ["getTipPresence"], "type": "string" }, "params": { @@ -3170,9 +2820,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "LiquidProbeParams": { "title": "LiquidProbeParams", @@ -3204,11 +2852,7 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "pipetteId" - ] + "required": ["labwareId", "wellName", "pipetteId"] }, "LiquidProbeCreate": { "title": "LiquidProbeCreate", @@ -3218,9 +2862,7 @@ "commandType": { "title": "Commandtype", "default": "liquidProbe", - "enum": [ - "liquidProbe" - ], + "enum": ["liquidProbe"], "type": "string" }, "params": { @@ -3240,9 +2882,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "TryLiquidProbeParams": { "title": "TryLiquidProbeParams", @@ -3274,11 +2914,7 @@ "type": "string" } }, - "required": [ - "labwareId", - "wellName", - "pipetteId" - ] + "required": ["labwareId", "wellName", "pipetteId"] }, "TryLiquidProbeCreate": { "title": "TryLiquidProbeCreate", @@ -3288,9 +2924,7 @@ "commandType": { "title": "Commandtype", "default": "tryLiquidProbe", - "enum": [ - "tryLiquidProbe" - ], + "enum": ["tryLiquidProbe"], "type": "string" }, "params": { @@ -3310,9 +2944,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", @@ -3330,9 +2962,7 @@ "type": "number" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__heater_shaker__wait_for_temperature__WaitForTemperatureCreate": { "title": "WaitForTemperatureCreate", @@ -3342,9 +2972,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/waitForTemperature", - "enum": [ - "heaterShaker/waitForTemperature" - ], + "enum": ["heaterShaker/waitForTemperature"], "type": "string" }, "params": { @@ -3364,9 +2992,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureParams": { "title": "SetTargetTemperatureParams", @@ -3384,10 +3010,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "celsius" - ] + "required": ["moduleId", "celsius"] }, "opentrons__protocol_engine__commands__heater_shaker__set_target_temperature__SetTargetTemperatureCreate": { "title": "SetTargetTemperatureCreate", @@ -3397,9 +3020,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/setTargetTemperature", - "enum": [ - "heaterShaker/setTargetTemperature" - ], + "enum": ["heaterShaker/setTargetTemperature"], "type": "string" }, "params": { @@ -3419,9 +3040,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeactivateHeaterParams": { "title": "DeactivateHeaterParams", @@ -3434,9 +3053,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DeactivateHeaterCreate": { "title": "DeactivateHeaterCreate", @@ -3446,9 +3063,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/deactivateHeater", - "enum": [ - "heaterShaker/deactivateHeater" - ], + "enum": ["heaterShaker/deactivateHeater"], "type": "string" }, "params": { @@ -3468,9 +3083,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "SetAndWaitForShakeSpeedParams": { "title": "SetAndWaitForShakeSpeedParams", @@ -3488,10 +3101,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "rpm" - ] + "required": ["moduleId", "rpm"] }, "SetAndWaitForShakeSpeedCreate": { "title": "SetAndWaitForShakeSpeedCreate", @@ -3501,9 +3111,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/setAndWaitForShakeSpeed", - "enum": [ - "heaterShaker/setAndWaitForShakeSpeed" - ], + "enum": ["heaterShaker/setAndWaitForShakeSpeed"], "type": "string" }, "params": { @@ -3523,9 +3131,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeactivateShakerParams": { "title": "DeactivateShakerParams", @@ -3538,9 +3144,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DeactivateShakerCreate": { "title": "DeactivateShakerCreate", @@ -3550,9 +3154,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/deactivateShaker", - "enum": [ - "heaterShaker/deactivateShaker" - ], + "enum": ["heaterShaker/deactivateShaker"], "type": "string" }, "params": { @@ -3572,9 +3174,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "OpenLabwareLatchParams": { "title": "OpenLabwareLatchParams", @@ -3587,9 +3187,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "OpenLabwareLatchCreate": { "title": "OpenLabwareLatchCreate", @@ -3599,9 +3197,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/openLabwareLatch", - "enum": [ - "heaterShaker/openLabwareLatch" - ], + "enum": ["heaterShaker/openLabwareLatch"], "type": "string" }, "params": { @@ -3621,9 +3217,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CloseLabwareLatchParams": { "title": "CloseLabwareLatchParams", @@ -3636,9 +3230,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "CloseLabwareLatchCreate": { "title": "CloseLabwareLatchCreate", @@ -3648,9 +3240,7 @@ "commandType": { "title": "Commandtype", "default": "heaterShaker/closeLabwareLatch", - "enum": [ - "heaterShaker/closeLabwareLatch" - ], + "enum": ["heaterShaker/closeLabwareLatch"], "type": "string" }, "params": { @@ -3670,9 +3260,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DisengageParams": { "title": "DisengageParams", @@ -3685,9 +3273,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DisengageCreate": { "title": "DisengageCreate", @@ -3697,9 +3283,7 @@ "commandType": { "title": "Commandtype", "default": "magneticModule/disengage", - "enum": [ - "magneticModule/disengage" - ], + "enum": ["magneticModule/disengage"], "type": "string" }, "params": { @@ -3719,9 +3303,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "EngageParams": { "title": "EngageParams", @@ -3739,10 +3321,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "height" - ] + "required": ["moduleId", "height"] }, "EngageCreate": { "title": "EngageCreate", @@ -3752,9 +3331,7 @@ "commandType": { "title": "Commandtype", "default": "magneticModule/engage", - "enum": [ - "magneticModule/engage" - ], + "enum": ["magneticModule/engage"], "type": "string" }, "params": { @@ -3774,9 +3351,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureParams": { "title": "SetTargetTemperatureParams", @@ -3794,10 +3369,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "celsius" - ] + "required": ["moduleId", "celsius"] }, "opentrons__protocol_engine__commands__temperature_module__set_target_temperature__SetTargetTemperatureCreate": { "title": "SetTargetTemperatureCreate", @@ -3807,9 +3379,7 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/setTargetTemperature", - "enum": [ - "temperatureModule/setTargetTemperature" - ], + "enum": ["temperatureModule/setTargetTemperature"], "type": "string" }, "params": { @@ -3829,9 +3399,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureParams": { "title": "WaitForTemperatureParams", @@ -3849,9 +3417,7 @@ "type": "number" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__temperature_module__wait_for_temperature__WaitForTemperatureCreate": { "title": "WaitForTemperatureCreate", @@ -3861,9 +3427,7 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/waitForTemperature", - "enum": [ - "temperatureModule/waitForTemperature" - ], + "enum": ["temperatureModule/waitForTemperature"], "type": "string" }, "params": { @@ -3883,9 +3447,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeactivateTemperatureParams": { "title": "DeactivateTemperatureParams", @@ -3898,9 +3460,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DeactivateTemperatureCreate": { "title": "DeactivateTemperatureCreate", @@ -3910,9 +3470,7 @@ "commandType": { "title": "Commandtype", "default": "temperatureModule/deactivate", - "enum": [ - "temperatureModule/deactivate" - ], + "enum": ["temperatureModule/deactivate"], "type": "string" }, "params": { @@ -3932,9 +3490,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "SetTargetBlockTemperatureParams": { "title": "SetTargetBlockTemperatureParams", @@ -3962,10 +3518,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "celsius" - ] + "required": ["moduleId", "celsius"] }, "SetTargetBlockTemperatureCreate": { "title": "SetTargetBlockTemperatureCreate", @@ -3975,9 +3528,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/setTargetBlockTemperature", - "enum": [ - "thermocycler/setTargetBlockTemperature" - ], + "enum": ["thermocycler/setTargetBlockTemperature"], "type": "string" }, "params": { @@ -3997,9 +3548,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "WaitForBlockTemperatureParams": { "title": "WaitForBlockTemperatureParams", @@ -4012,9 +3561,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "WaitForBlockTemperatureCreate": { "title": "WaitForBlockTemperatureCreate", @@ -4024,9 +3571,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/waitForBlockTemperature", - "enum": [ - "thermocycler/waitForBlockTemperature" - ], + "enum": ["thermocycler/waitForBlockTemperature"], "type": "string" }, "params": { @@ -4046,9 +3591,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "SetTargetLidTemperatureParams": { "title": "SetTargetLidTemperatureParams", @@ -4066,10 +3609,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "celsius" - ] + "required": ["moduleId", "celsius"] }, "SetTargetLidTemperatureCreate": { "title": "SetTargetLidTemperatureCreate", @@ -4079,9 +3619,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/setTargetLidTemperature", - "enum": [ - "thermocycler/setTargetLidTemperature" - ], + "enum": ["thermocycler/setTargetLidTemperature"], "type": "string" }, "params": { @@ -4101,9 +3639,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "WaitForLidTemperatureParams": { "title": "WaitForLidTemperatureParams", @@ -4116,9 +3652,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "WaitForLidTemperatureCreate": { "title": "WaitForLidTemperatureCreate", @@ -4128,9 +3662,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/waitForLidTemperature", - "enum": [ - "thermocycler/waitForLidTemperature" - ], + "enum": ["thermocycler/waitForLidTemperature"], "type": "string" }, "params": { @@ -4150,9 +3682,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeactivateBlockParams": { "title": "DeactivateBlockParams", @@ -4165,9 +3695,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DeactivateBlockCreate": { "title": "DeactivateBlockCreate", @@ -4177,9 +3705,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/deactivateBlock", - "enum": [ - "thermocycler/deactivateBlock" - ], + "enum": ["thermocycler/deactivateBlock"], "type": "string" }, "params": { @@ -4199,9 +3725,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "DeactivateLidParams": { "title": "DeactivateLidParams", @@ -4214,9 +3738,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "DeactivateLidCreate": { "title": "DeactivateLidCreate", @@ -4226,9 +3748,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/deactivateLid", - "enum": [ - "thermocycler/deactivateLid" - ], + "enum": ["thermocycler/deactivateLid"], "type": "string" }, "params": { @@ -4248,9 +3768,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidParams": { "title": "OpenLidParams", @@ -4263,9 +3781,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__thermocycler__open_lid__OpenLidCreate": { "title": "OpenLidCreate", @@ -4275,9 +3791,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/openLid", - "enum": [ - "thermocycler/openLid" - ], + "enum": ["thermocycler/openLid"], "type": "string" }, "params": { @@ -4297,9 +3811,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidParams": { "title": "CloseLidParams", @@ -4312,9 +3824,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__thermocycler__close_lid__CloseLidCreate": { "title": "CloseLidCreate", @@ -4324,9 +3834,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/closeLid", - "enum": [ - "thermocycler/closeLid" - ], + "enum": ["thermocycler/closeLid"], "type": "string" }, "params": { @@ -4346,9 +3854,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "RunProfileStepParams": { "title": "RunProfileStepParams", @@ -4366,10 +3872,7 @@ "type": "number" } }, - "required": [ - "celsius", - "holdSeconds" - ] + "required": ["celsius", "holdSeconds"] }, "RunProfileParams": { "title": "RunProfileParams", @@ -4395,10 +3898,7 @@ "type": "number" } }, - "required": [ - "moduleId", - "profile" - ] + "required": ["moduleId", "profile"] }, "RunProfileCreate": { "title": "RunProfileCreate", @@ -4408,9 +3908,7 @@ "commandType": { "title": "Commandtype", "default": "thermocycler/runProfile", - "enum": [ - "thermocycler/runProfile" - ], + "enum": ["thermocycler/runProfile"], "type": "string" }, "params": { @@ -4430,9 +3928,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidParams": { "title": "CloseLidParams", @@ -4445,9 +3941,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__absorbance_reader__close_lid__CloseLidCreate": { "title": "CloseLidCreate", @@ -4457,9 +3951,7 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/closeLid", - "enum": [ - "absorbanceReader/closeLid" - ], + "enum": ["absorbanceReader/closeLid"], "type": "string" }, "params": { @@ -4479,9 +3971,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidParams": { "title": "OpenLidParams", @@ -4494,9 +3984,7 @@ "type": "string" } }, - "required": [ - "moduleId" - ] + "required": ["moduleId"] }, "opentrons__protocol_engine__commands__absorbance_reader__open_lid__OpenLidCreate": { "title": "OpenLidCreate", @@ -4506,9 +3994,7 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/openLid", - "enum": [ - "absorbanceReader/openLid" - ], + "enum": ["absorbanceReader/openLid"], "type": "string" }, "params": { @@ -4528,9 +4014,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "InitializeParams": { "title": "InitializeParams", @@ -4548,10 +4032,7 @@ "type": "integer" } }, - "required": [ - "moduleId", - "sampleWavelength" - ] + "required": ["moduleId", "sampleWavelength"] }, "InitializeCreate": { "title": "InitializeCreate", @@ -4561,9 +4042,7 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/initialize", - "enum": [ - "absorbanceReader/initialize" - ], + "enum": ["absorbanceReader/initialize"], "type": "string" }, "params": { @@ -4583,9 +4062,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "ReadAbsorbanceParams": { "title": "ReadAbsorbanceParams", @@ -4603,10 +4080,7 @@ "type": "integer" } }, - "required": [ - "moduleId", - "sampleWavelength" - ] + "required": ["moduleId", "sampleWavelength"] }, "ReadAbsorbanceCreate": { "title": "ReadAbsorbanceCreate", @@ -4616,9 +4090,7 @@ "commandType": { "title": "Commandtype", "default": "absorbanceReader/read", - "enum": [ - "absorbanceReader/read" - ], + "enum": ["absorbanceReader/read"], "type": "string" }, "params": { @@ -4638,17 +4110,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CalibrateGripperParamsJaw": { "title": "CalibrateGripperParamsJaw", "description": "An enumeration.", - "enum": [ - "front", - "rear" - ] + "enum": ["front", "rear"] }, "Vec3f": { "title": "Vec3f", @@ -4668,11 +4135,7 @@ "type": "number" } }, - "required": [ - "x", - "y", - "z" - ] + "required": ["x", "y", "z"] }, "CalibrateGripperParams": { "title": "CalibrateGripperParams", @@ -4697,9 +4160,7 @@ ] } }, - "required": [ - "jaw" - ] + "required": ["jaw"] }, "CalibrateGripperCreate": { "title": "CalibrateGripperCreate", @@ -4709,9 +4170,7 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibrateGripper", - "enum": [ - "calibration/calibrateGripper" - ], + "enum": ["calibration/calibrateGripper"], "type": "string" }, "params": { @@ -4731,9 +4190,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CalibratePipetteParams": { "title": "CalibratePipetteParams", @@ -4749,9 +4206,7 @@ ] } }, - "required": [ - "mount" - ] + "required": ["mount"] }, "CalibratePipetteCreate": { "title": "CalibratePipetteCreate", @@ -4761,9 +4216,7 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibratePipette", - "enum": [ - "calibration/calibratePipette" - ], + "enum": ["calibration/calibratePipette"], "type": "string" }, "params": { @@ -4783,9 +4236,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "CalibrateModuleParams": { "title": "CalibrateModuleParams", @@ -4811,11 +4262,7 @@ ] } }, - "required": [ - "moduleId", - "labwareId", - "mount" - ] + "required": ["moduleId", "labwareId", "mount"] }, "CalibrateModuleCreate": { "title": "CalibrateModuleCreate", @@ -4825,9 +4272,7 @@ "commandType": { "title": "Commandtype", "default": "calibration/calibrateModule", - "enum": [ - "calibration/calibrateModule" - ], + "enum": ["calibration/calibrateModule"], "type": "string" }, "params": { @@ -4847,17 +4292,12 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "MaintenancePosition": { "title": "MaintenancePosition", "description": "Maintenance position options.", - "enum": [ - "attachPlate", - "attachInstrument" - ] + "enum": ["attachPlate", "attachInstrument"] }, "MoveToMaintenancePositionParams": { "title": "MoveToMaintenancePositionParams", @@ -4882,9 +4322,7 @@ ] } }, - "required": [ - "mount" - ] + "required": ["mount"] }, "MoveToMaintenancePositionCreate": { "title": "MoveToMaintenancePositionCreate", @@ -4894,9 +4332,7 @@ "commandType": { "title": "Commandtype", "default": "calibration/moveToMaintenancePosition", - "enum": [ - "calibration/moveToMaintenancePosition" - ], + "enum": ["calibration/moveToMaintenancePosition"], "type": "string" }, "params": { @@ -4916,9 +4352,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "UnsafeBlowOutInPlaceParams": { "title": "UnsafeBlowOutInPlaceParams", @@ -4937,10 +4371,7 @@ "type": "string" } }, - "required": [ - "flowRate", - "pipetteId" - ] + "required": ["flowRate", "pipetteId"] }, "UnsafeBlowOutInPlaceCreate": { "title": "UnsafeBlowOutInPlaceCreate", @@ -4950,9 +4381,7 @@ "commandType": { "title": "Commandtype", "default": "unsafe/blowOutInPlace", - "enum": [ - "unsafe/blowOutInPlace" - ], + "enum": ["unsafe/blowOutInPlace"], "type": "string" }, "params": { @@ -4972,9 +4401,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "UnsafeDropTipInPlaceParams": { "title": "UnsafeDropTipInPlaceParams", @@ -4992,9 +4419,7 @@ "type": "boolean" } }, - "required": [ - "pipetteId" - ] + "required": ["pipetteId"] }, "UnsafeDropTipInPlaceCreate": { "title": "UnsafeDropTipInPlaceCreate", @@ -5004,9 +4429,7 @@ "commandType": { "title": "Commandtype", "default": "unsafe/dropTipInPlace", - "enum": [ - "unsafe/dropTipInPlace" - ], + "enum": ["unsafe/dropTipInPlace"], "type": "string" }, "params": { @@ -5026,9 +4449,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "UpdatePositionEstimatorsParams": { "title": "UpdatePositionEstimatorsParams", @@ -5043,9 +4464,7 @@ } } }, - "required": [ - "axes" - ] + "required": ["axes"] }, "UpdatePositionEstimatorsCreate": { "title": "UpdatePositionEstimatorsCreate", @@ -5055,9 +4474,7 @@ "commandType": { "title": "Commandtype", "default": "unsafe/updatePositionEstimators", - "enum": [ - "unsafe/updatePositionEstimators" - ], + "enum": ["unsafe/updatePositionEstimators"], "type": "string" }, "params": { @@ -5077,9 +4494,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] }, "UnsafeEngageAxesParams": { "title": "UnsafeEngageAxesParams", @@ -5094,9 +4509,7 @@ } } }, - "required": [ - "axes" - ] + "required": ["axes"] }, "UnsafeEngageAxesCreate": { "title": "UnsafeEngageAxesCreate", @@ -5106,9 +4519,7 @@ "commandType": { "title": "Commandtype", "default": "unsafe/engageAxes", - "enum": [ - "unsafe/engageAxes" - ], + "enum": ["unsafe/engageAxes"], "type": "string" }, "params": { @@ -5128,9 +4539,7 @@ "type": "string" } }, - "required": [ - "params" - ] + "required": ["params"] } }, "$id": "opentronsCommandSchemaV9", From 1bcc578a99d6ad3864e7914ec40012b064384081 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 20 Sep 2024 14:04:19 -0400 Subject: [PATCH 06/51] updated docstring --- api/src/opentrons/protocol_engine/commands/aspirate.py | 5 ++--- api/src/opentrons/protocol_engine/types.py | 5 ++++- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 35002bb408c..a861b2002bd 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -86,7 +86,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName - volume = params.volume ready_to_aspirate = self._pipetting.get_is_ready_to_aspirate( pipette_id=pipette_id @@ -119,7 +118,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_name=well_name, well_location=params.wellLocation, current_well=current_well, - operation_volume=-volume, + operation_volume=-params.volume, ) deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) state_update.set_pipette_location( @@ -132,7 +131,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: try: volume_aspirated = await self._pipetting.aspirate_in_place( pipette_id=pipette_id, - volume=volume, + volume=params.volume, flow_rate=params.flowRate, command_note_adder=self._command_note_adder, ) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index bf2fd3e0c97..ff6c2a85b6c 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -240,7 +240,10 @@ class WellOffset(BaseModel): class WellVolumeOffset(BaseModel): - """A z-offset to account for volume in an operation.""" + """A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS. + + For example, this parameter should be used for MoveToWell commands prior to AspirateInPlace commands. + """ volumeOffset: Union[float, Literal["operationVolume"]] = 0.0 From 33e61080c49b723d828aaddd1ce16339516c785c Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 20 Sep 2024 14:10:52 -0400 Subject: [PATCH 07/51] updated schema --- shared-data/command/schemas/9.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 6ad70fe0cc4..3f7d8022d9b 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -320,7 +320,7 @@ }, "WellVolumeOffset": { "title": "WellVolumeOffset", - "description": "A z-offset to account for volume in an operation.", + "description": "A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS.\n\nFor example, this parameter should be used for MoveToWell commands prior to AspirateInPlace commands.", "type": "object", "properties": { "volumeOffset": { From fd93d0e8b94da7613164258e90ffa7a4158fbe72 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 20 Sep 2024 16:48:53 -0400 Subject: [PATCH 08/51] updated docstring --- api/src/opentrons/protocol_engine/types.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index ff6c2a85b6c..3e9ddfa81c5 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -242,7 +242,10 @@ class WellOffset(BaseModel): class WellVolumeOffset(BaseModel): """A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS. - For example, this parameter should be used for MoveToWell commands prior to AspirateInPlace commands. + Specifying `operationVolume` results in this class acting as a sentinel and should be used when + volume can be determined from the command parameters, for example commanding Aspirate. A volume + should be specified when it cannot be determined from the command parameters, for example commanding + MoveToWell prior to AspirateInPlace. """ volumeOffset: Union[float, Literal["operationVolume"]] = 0.0 From a40b66b19110376d4bb8ba94e7de06c8e0780ae5 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 20 Sep 2024 17:03:12 -0400 Subject: [PATCH 09/51] updated schema and formatted --- api/src/opentrons/protocol_engine/types.py | 6 +++--- shared-data/command/schemas/9.json | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 3e9ddfa81c5..4c897c17be9 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -242,9 +242,9 @@ class WellOffset(BaseModel): class WellVolumeOffset(BaseModel): """A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS. - Specifying `operationVolume` results in this class acting as a sentinel and should be used when - volume can be determined from the command parameters, for example commanding Aspirate. A volume - should be specified when it cannot be determined from the command parameters, for example commanding + Specifying `operationVolume` results in this class acting as a sentinel and should be used when + volume can be determined from the command parameters, for example commanding Aspirate. A volume + should be specified when it cannot be determined from the command parameters, for example commanding MoveToWell prior to AspirateInPlace. """ diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 3f7d8022d9b..79e690fbd38 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -320,7 +320,7 @@ }, "WellVolumeOffset": { "title": "WellVolumeOffset", - "description": "A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS.\n\nFor example, this parameter should be used for MoveToWell commands prior to AspirateInPlace commands.", + "description": "A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS.\n\nSpecifying `operationVolume` results in this class acting as a sentinel and should be used when\nvolume can be determined from the command parameters, for example commanding Aspirate. A volume\nshould be specified when it cannot be determined from the command parameters, for example commanding\nMoveToWell prior to AspirateInPlace.", "type": "object", "properties": { "volumeOffset": { From add66381037d67b8989423a7a8f9c718ebba2265 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 23 Sep 2024 10:31:21 -0400 Subject: [PATCH 10/51] fleshed out height-to-volume-to-height --- api/src/opentrons/protocol_engine/state/geometry.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 68eeb75448a..d17b8ca5c7f 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -443,7 +443,10 @@ def get_well_position( volume = operation_volume or 0.0 else: volume = well_location.volumeOffset.volumeOffset - # height_after_operation = self.get_ending_height(labware_id, well_name, starting_liquid_height, volume) + # make below 3 lines their own method? + # volume_at_starting_height = self.get_volume_at_height(labware_id=labware_id, well_id=well_name, target_height=starting_liquid_height) # confirm well_id=well_name + # ending_volume = volume_at_starting_height + volume + # height_after_operation = self.get_height_at_volume(labware_id=labware_id, well_id=well_name, target_volume=ending_volume) # confirm well_id=well_name height_after_operation = starting_liquid_height # delete, use above method once implemented if volume: # delete pass # delete From f2b6bc7a737cf9bd603ff4043ce663e6ca9fb3c2 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 23 Sep 2024 16:53:31 -0400 Subject: [PATCH 11/51] created command-specific WellLocations and move_to_well() subfunctions --- .../protocol_engine/commands/liquid_probe.py | 12 ++++--- .../protocol_engine/commands/pick_up_tip.py | 12 ++++--- .../protocol_engine/state/geometry.py | 29 +++++++++++++---- api/src/opentrons/protocol_engine/types.py | 32 ++++++++++++------- 4 files changed, 59 insertions(+), 26 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index 142cd93aba4..fce70ff6a8e 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -13,11 +13,10 @@ PipetteLiquidNotFoundError, ) -from ..types import DeckPoint +from ..types import LiquidProbeWellLocation, DeckPoint from .pipetting_common import ( LiquidNotFoundError, PipetteIdMixin, - WellLocationMixin, DestinationPositionResult, ) from .command import ( @@ -42,8 +41,13 @@ # 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 _CommonParams(PipetteIdMixin): + labwareId: str = Field(..., description="Identifier of labware to use.") + wellName: str = Field(..., description="Name of well to use in labware.") + wellLocation: LiquidProbeWellLocation = Field( + default_factory=LiquidProbeWellLocation, + description="Relative well location at which to liquid probe.", + ) class LiquidProbeParams(_CommonParams): diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index c30d2f953db..8ff4cf1b421 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -9,10 +9,9 @@ from ..errors import ErrorOccurrence, TipNotAttachedError from ..resources import ModelUtils from ..state import update_types -from ..types import DeckPoint +from ..types import PickUpTipWellLocation, DeckPoint from .pipetting_common import ( PipetteIdMixin, - WellLocationMixin, DestinationPositionResult, ) from .command import ( @@ -31,10 +30,15 @@ PickUpTipCommandType = Literal["pickUpTip"] -class PickUpTipParams(PipetteIdMixin, WellLocationMixin): +class PickUpTipParams(PipetteIdMixin): """Payload needed to move a pipette to a specific well.""" - pass + labwareId: str = Field(..., description="Identifier of labware to use.") + wellName: str = Field(..., description="Name of well to use in labware.") + wellLocation: PickUpTipWellLocation = Field( + default_factory=PickUpTipWellLocation, + description="Relative well location at which to pick up the tip.", + ) class PickUpTipResult(DestinationPositionResult): diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index d17b8ca5c7f..daf3e05065a 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -443,13 +443,7 @@ def get_well_position( volume = operation_volume or 0.0 else: volume = well_location.volumeOffset.volumeOffset - # make below 3 lines their own method? - # volume_at_starting_height = self.get_volume_at_height(labware_id=labware_id, well_id=well_name, target_height=starting_liquid_height) # confirm well_id=well_name - # ending_volume = volume_at_starting_height + volume - # height_after_operation = self.get_height_at_volume(labware_id=labware_id, well_id=well_name, target_volume=ending_volume) # confirm well_id=well_name - height_after_operation = starting_liquid_height # delete, use above method once implemented - if volume: # delete - pass # delete + height_after_operation = self.get_height_after_volume(labware_id=labware_id, well_name=well_name, starting_height=starting_liquid_height, volume=volume) offset = offset.copy( update={"z": offset.z + height_after_operation} ) @@ -1232,3 +1226,24 @@ def get_well_volumetric_capacity( message=f"No InnerWellGeometry found for well id: {well_id}" ) return get_well_volumetric_capacity(well_geometry) + + def get_volume_at_height( + self, labware_id: str, well_id: str, target_height: float + ) -> float: + pass + + def get_height_at_volume( + self, labware_id: str, well_id: str, target_volume: float + ) -> float: + pass + + def get_height_after_volume( + self, labware_id: str, well_name: str, starting_height: float, volume: float + ) -> float: + well_def = self._labware.get_well_definition(labware_id, well_name) + well_id = well_def.geometryDefinitionId + starting_volume = self.get_volume_at_height(labware_id=labware_id, well_id=well_id, target_height=starting_height) + ending_volume = starting_volume + volume + ending_height = self.get_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=ending_volume) + # return ending_height + return starting_height # delete, use line above once sub-methods implemented diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 4c897c17be9..a825ee9bd34 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -239,24 +239,34 @@ class WellOffset(BaseModel): z: float = 0 -class WellVolumeOffset(BaseModel): - """A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS. +# get rid of operationVolume below and operation_volume parameter in methods? +# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location +class WellLocation(BaseModel): + """A relative location in reference to a well's location.""" # update - Specifying `operationVolume` results in this class acting as a sentinel and should be used when - volume can be determined from the command parameters, for example commanding Aspirate. A volume - should be specified when it cannot be determined from the command parameters, for example commanding - MoveToWell prior to AspirateInPlace. - """ + origin: WellOrigin = WellOrigin.TOP + offset: WellOffset = Field(default_factory=WellOffset) + volumeOffset: Union[float, Literal["operationVolume"]] = Field(default=0.0, description="""A volume of liquid to account for when + executing commands with an origin of WellOrigin.MENISCUS. Specifying + `operationVolume` results in this class acting as a sentinel and should + be used when volume can be determined from the command parameters, for + example commanding Aspirate. A volume should be specified when it cannot + be determined from the command parameters, for example commanding + MoveToWell prior to AspirateInPlace.""") # update comment - volumeOffset: Union[float, Literal["operationVolume"]] = 0.0 +class LiquidProbeWellLocation(BaseModel): + """A relative location in reference to a well's location.""" # update -class WellLocation(BaseModel): - """A relative location in reference to a well's location.""" + origin: WellOrigin = WellOrigin.TOP + offset: WellOffset = Field(default_factory=WellOffset) + + +class PickUpTipWellLocation(BaseModel): + """A relative location in reference to a well's location.""" # update origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) - volumeOffset: WellVolumeOffset = Field(default_factory=WellVolumeOffset) class DropTipWellLocation(BaseModel): From 087caaf86d072a73a78b6ecb583099d94302480f Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Tue, 24 Sep 2024 13:58:17 -0400 Subject: [PATCH 12/51] added LiquidHandlingWellLocation and PickUpTipWellLocation --- .../protocol_api/core/engine/deck_conflict.py | 9 +- .../protocol_api/core/engine/instrument.py | 40 ++++--- api/src/opentrons/protocol_engine/__init__.py | 4 + .../protocol_engine/commands/aspirate.py | 4 +- .../protocol_engine/commands/blow_out.py | 4 +- .../protocol_engine/commands/dispense.py | 4 +- .../protocol_engine/commands/liquid_probe.py | 12 +-- .../protocol_engine/commands/pick_up_tip.py | 4 +- .../commands/pipetting_common.py | 19 +++- .../protocol_engine/execution/movement.py | 5 +- .../protocol_engine/state/geometry.py | 101 ++++++++++++++---- .../opentrons/protocol_engine/state/motion.py | 5 +- api/src/opentrons/protocol_engine/types.py | 37 ++++--- .../core/engine/test_instrument_core.py | 10 +- .../protocol_engine/commands/test_aspirate.py | 23 ++-- .../protocol_engine/commands/test_blow_out.py | 11 +- .../protocol_engine/commands/test_dispense.py | 11 +- .../commands/test_pick_up_tip.py | 9 +- .../protocol_engine/state/command_fixtures.py | 13 +-- .../protocol_runner/test_json_translator.py | 17 +-- .../protocol/models/__init__.py | 2 + .../protocol/models/protocol_schema_v8.py | 2 + .../protocol/models/shared_models.py | 9 +- 23 files changed, 248 insertions(+), 107 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py index 6ebb47f0ac8..58eaeca3294 100644 --- a/api/src/opentrons/protocol_api/core/engine/deck_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/deck_conflict.py @@ -28,6 +28,8 @@ AddressableAreaLocation, OFF_DECK_LOCATION, WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, DropTipWellLocation, ) from opentrons.protocol_engine.errors.exceptions import LabwareNotLoadedOnModuleError @@ -198,7 +200,12 @@ def check_safe_for_pipette_movement( pipette_id: str, labware_id: str, well_name: str, - well_location: Union[WellLocation, DropTipWellLocation], + well_location: Union[ + WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, + DropTipWellLocation, + ], ) -> None: """Check if the labware is safe to move to with a pipette in partial tip configuration. diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 55519e7899c..e3ded94a19c 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -146,12 +146,10 @@ def aspirate( well_name = well_core.get_name() labware_id = well_core.labware_id - well_location = ( - self._engine_client.state.geometry.get_relative_well_location( - labware_id=labware_id, - well_name=well_name, - absolute_point=location.point, - ) + well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, ) deck_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, @@ -237,12 +235,10 @@ def dispense( well_name = well_core.get_name() labware_id = well_core.labware_id - well_location = ( - self._engine_client.state.geometry.get_relative_well_location( - labware_id=labware_id, - well_name=well_name, - absolute_point=location.point, - ) + well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, ) deck_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, @@ -314,12 +310,10 @@ def blow_out( well_name = well_core.get_name() labware_id = well_core.labware_id - well_location = ( - self._engine_client.state.geometry.get_relative_well_location( - labware_id=labware_id, - well_name=well_name, - absolute_point=location.point, - ) + well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, ) deck_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, @@ -416,10 +410,12 @@ def pick_up_tip( well_name = well_core.get_name() labware_id = well_core.labware_id - well_location = self._engine_client.state.geometry.get_relative_well_location( - labware_id=labware_id, - well_name=well_name, - absolute_point=location.point, + well_location = ( + self._engine_client.state.geometry.get_relative_pick_up_tip_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, + ) ) deck_conflict.check_safe_for_tip_pickup_and_return( engine_state=self._engine_client.state, diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 14c5c3f3fc5..2c8171a0e83 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -47,6 +47,8 @@ LoadedPipette, MotorAxis, WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, DropTipWellLocation, WellOrigin, DropTipWellOrigin, @@ -109,6 +111,8 @@ "LoadedPipette", "MotorAxis", "WellLocation", + "LiquidHandlingWellLocation", + "PickUpTipWellLocation", "DropTipWellLocation", "WellOrigin", "DropTipWellOrigin", diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index a861b2002bd..8336de45d2c 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -9,7 +9,7 @@ PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, - WellLocationMixin, + LiquidHandlingWellLocationMixin, BaseLiquidHandlingResult, DestinationPositionResult, ) @@ -38,7 +38,7 @@ class AspirateParams( - PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, WellLocationMixin + PipetteIdMixin, AspirateVolumeMixin, FlowRateMixin, LiquidHandlingWellLocationMixin ): """Parameters required to aspirate from a specific well.""" diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index 9954ef07cfa..0ded626da62 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -9,7 +9,7 @@ from .pipetting_common import ( PipetteIdMixin, FlowRateMixin, - WellLocationMixin, + LiquidHandlingWellLocationMixin, DestinationPositionResult, ) from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData @@ -25,7 +25,7 @@ BlowOutCommandType = Literal["blowout"] -class BlowOutParams(PipetteIdMixin, FlowRateMixin, WellLocationMixin): +class BlowOutParams(PipetteIdMixin, FlowRateMixin, LiquidHandlingWellLocationMixin): """Payload required to blow-out a specific well.""" pass diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index a2d0738b546..4179f8524c0 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -13,7 +13,7 @@ PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, - WellLocationMixin, + LiquidHandlingWellLocationMixin, BaseLiquidHandlingResult, DestinationPositionResult, OverpressureError, @@ -36,7 +36,7 @@ class DispenseParams( - PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, WellLocationMixin + PipetteIdMixin, DispenseVolumeMixin, FlowRateMixin, LiquidHandlingWellLocationMixin ): """Payload required to dispense to a specific well.""" diff --git a/api/src/opentrons/protocol_engine/commands/liquid_probe.py b/api/src/opentrons/protocol_engine/commands/liquid_probe.py index fce70ff6a8e..142cd93aba4 100644 --- a/api/src/opentrons/protocol_engine/commands/liquid_probe.py +++ b/api/src/opentrons/protocol_engine/commands/liquid_probe.py @@ -13,10 +13,11 @@ PipetteLiquidNotFoundError, ) -from ..types import LiquidProbeWellLocation, DeckPoint +from ..types import DeckPoint from .pipetting_common import ( LiquidNotFoundError, PipetteIdMixin, + WellLocationMixin, DestinationPositionResult, ) from .command import ( @@ -41,13 +42,8 @@ # 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): - labwareId: str = Field(..., description="Identifier of labware to use.") - wellName: str = Field(..., description="Name of well to use in labware.") - wellLocation: LiquidProbeWellLocation = Field( - default_factory=LiquidProbeWellLocation, - description="Relative well location at which to liquid probe.", - ) +class _CommonParams(PipetteIdMixin, WellLocationMixin): + pass class LiquidProbeParams(_CommonParams): diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 8ff4cf1b421..0e9aebd7c17 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -110,10 +110,12 @@ async def execute( pipette_id = params.pipetteId labware_id = params.labwareId well_name = params.wellName - well_location = params.wellLocation state_update = update_types.StateUpdate() + well_location = self._state_view.geometry.convert_pick_up_tip_location( + well_location=params.wellLocation + ) position = await self._movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, diff --git a/api/src/opentrons/protocol_engine/commands/pipetting_common.py b/api/src/opentrons/protocol_engine/commands/pipetting_common.py index 29aabcb78df..e663b831874 100644 --- a/api/src/opentrons/protocol_engine/commands/pipetting_common.py +++ b/api/src/opentrons/protocol_engine/commands/pipetting_common.py @@ -5,7 +5,7 @@ from opentrons.protocol_engine.errors.error_occurrence import ErrorOccurrence -from ..types import WellLocation, DeckPoint +from ..types import WellLocation, LiquidHandlingWellLocation, DeckPoint class PipetteIdMixin(BaseModel): @@ -68,6 +68,23 @@ class WellLocationMixin(BaseModel): ) +class LiquidHandlingWellLocationMixin(BaseModel): + """Mixin for command requests that take a location that's somewhere in a well.""" + + labwareId: str = Field( + ..., + description="Identifier of labware to use.", + ) + wellName: str = Field( + ..., + description="Name of well to use in labware.", + ) + wellLocation: LiquidHandlingWellLocation = Field( + default_factory=LiquidHandlingWellLocation, + description="Relative well location at which to perform the operation", + ) + + class MovementMixin(BaseModel): """Mixin for command requests that move a pipette.""" diff --git a/api/src/opentrons/protocol_engine/execution/movement.py b/api/src/opentrons/protocol_engine/execution/movement.py index ccd60760702..8aba23b124f 100644 --- a/api/src/opentrons/protocol_engine/execution/movement.py +++ b/api/src/opentrons/protocol_engine/execution/movement.py @@ -2,7 +2,7 @@ from __future__ import annotations import logging -from typing import Optional, List +from typing import Optional, List, Union from opentrons.types import Point, MountType from opentrons.hardware_control import HardwareControlAPI @@ -10,6 +10,7 @@ from ..types import ( WellLocation, + LiquidHandlingWellLocation, DeckPoint, MovementAxis, MotorAxis, @@ -66,7 +67,7 @@ async def move_to_well( pipette_id: str, labware_id: str, well_name: str, - well_location: Optional[WellLocation] = None, + well_location: Optional[Union[WellLocation, LiquidHandlingWellLocation]] = None, current_well: Optional[CurrentWell] = None, force_direct: bool = False, minimum_z_height: Optional[float] = None, diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index daf3e05065a..be2ad1e4324 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -26,7 +26,9 @@ LoadedLabware, LoadedModule, WellLocation, + LiquidHandlingWellLocation, DropTipWellLocation, + PickUpTipWellLocation, WellOrigin, DropTipWellOrigin, WellOffset, @@ -419,7 +421,9 @@ def get_well_position( self, labware_id: str, well_name: str, - well_location: Optional[WellLocation] = None, + well_location: Optional[ + Union[WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation] + ] = None, operation_volume: Optional[float] = None, ) -> Point: """Given relative well location in a labware, get absolute position.""" @@ -430,27 +434,33 @@ def get_well_position( offset = WellOffset(x=0, y=0, z=well_depth) if well_location is not None: offset = well_location.offset + starting_liquid_height: Union[float, None] = 0.0 if well_location.origin == WellOrigin.TOP: - offset = offset.copy(update={"z": offset.z + well_depth}) + starting_liquid_height = well_depth elif well_location.origin == WellOrigin.CENTER: - offset = offset.copy(update={"z": offset.z + well_depth / 2.0}) + starting_liquid_height = well_depth / 2.0 elif well_location.origin == WellOrigin.MENISCUS: starting_liquid_height = self._wells.get_last_measured_liquid_height( - labware_id, well_name + labware_id=labware_id, well_name=well_name ) - if starting_liquid_height is not None: - if well_location.volumeOffset.volumeOffset == "operationVolume": - volume = operation_volume or 0.0 - else: - volume = well_location.volumeOffset.volumeOffset - height_after_operation = self.get_height_after_volume(labware_id=labware_id, well_name=well_name, starting_height=starting_liquid_height, volume=volume) - offset = offset.copy( - update={"z": offset.z + height_after_operation} - ) - else: + if starting_liquid_height is None: raise errors.LiquidHeightUnknownError( "Must liquid probe before specifying WellOrigin.MENISCUS." ) + if isinstance(well_location, PickUpTipWellLocation): + volume = 0.0 + elif isinstance(well_location.volumeOffset, float): + volume = well_location.volumeOffset + elif well_location.volumeOffset == "operationVolume": + volume = operation_volume or 0.0 + assert isinstance(starting_liquid_height, float) + height_after_operation = self.get_height_after_volume( + labware_id=labware_id, + well_name=well_name, + starting_height=starting_liquid_height, + volume=volume, + ) + offset = offset.copy(update={"z": offset.z + height_after_operation}) return Point( x=labware_pos.x + offset.x + well_def.x, @@ -485,6 +495,32 @@ def get_relative_well_location( return WellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)) + def get_relative_liquid_handling_well_location( + self, + labware_id: str, + well_name: str, + absolute_point: Point, + ) -> LiquidHandlingWellLocation: + """Given absolute position, get relative location of a well in a labware.""" + well_absolute_point = self.get_well_position(labware_id, well_name) + delta = absolute_point - well_absolute_point + + return LiquidHandlingWellLocation( + offset=WellOffset(x=delta.x, y=delta.y, z=delta.z) + ) + + def get_relative_pick_up_tip_well_location( + self, + labware_id: str, + well_name: str, + absolute_point: Point, + ) -> PickUpTipWellLocation: + """Given absolute position, get relative location of a well in a labware.""" + well_absolute_point = self.get_well_position(labware_id, well_name) + delta = absolute_point - well_absolute_point + + return PickUpTipWellLocation(offset=WellOffset(x=delta.x, y=delta.y, z=delta.z)) + def get_well_height( self, labware_id: str, @@ -608,6 +644,19 @@ def get_checked_tip_drop_location( ), ) + def convert_pick_up_tip_location( + self, well_location: PickUpTipWellLocation + ) -> WellLocation: + """Convert PickUpTipWellLocation to WellLocation.""" + return WellLocation( + origin=well_location.origin, + offset=WellOffset( + x=well_location.offset.x, + y=well_location.offset.y, + z=well_location.offset.z, + ), + ) + # TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411 def _get_staging_slot_name(self, labware_id: str) -> str: """Get the staging slot name that the labware is on.""" @@ -1226,24 +1275,34 @@ def get_well_volumetric_capacity( message=f"No InnerWellGeometry found for well id: {well_id}" ) return get_well_volumetric_capacity(well_geometry) - + def get_volume_at_height( self, labware_id: str, well_id: str, target_height: float ) -> float: - pass + """Return the volume of liquid in a labware well at a given liquid height (with reference to the well bottom).""" + return 0.0 def get_height_at_volume( self, labware_id: str, well_id: str, target_volume: float ) -> float: - pass + """Return the height of liquid in a labware well at a given liquid volume.""" + return 0.0 def get_height_after_volume( self, labware_id: str, well_name: str, starting_height: float, volume: float ) -> float: + """Return the height of liquid in a labware well after a given volume has been handled, given a starting liquid height (with reference to the well bottom).""" well_def = self._labware.get_well_definition(labware_id, well_name) - well_id = well_def.geometryDefinitionId - starting_volume = self.get_volume_at_height(labware_id=labware_id, well_id=well_id, target_height=starting_height) + well_id = well_def.geometryDefinitionId # make non-optional eventually + assert isinstance(well_id, str) + starting_volume = self.get_volume_at_height( + labware_id=labware_id, well_id=well_id, target_height=starting_height + ) ending_volume = starting_volume + volume - ending_height = self.get_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=ending_volume) + ending_height = self.get_height_at_volume( + labware_id=labware_id, well_id=well_id, target_volume=ending_volume + ) + if ending_height: # delete + pass # delete # return ending_height - return starting_height # delete, use line above once sub-methods implemented + return starting_height # delete, use line above once sub-methods implemented diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index 0787a59831c..abc40bb0da2 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -1,6 +1,6 @@ """Motion state store and getters.""" from dataclasses import dataclass -from typing import List, Optional +from typing import List, Optional, Union from opentrons.types import MountType, Point from opentrons.hardware_control.types import CriticalPoint @@ -15,6 +15,7 @@ from ..types import ( MotorAxis, WellLocation, + LiquidHandlingWellLocation, CurrentWell, CurrentPipetteLocation, AddressableOffsetVector, @@ -89,7 +90,7 @@ def get_movement_waypoints_to_well( pipette_id: str, labware_id: str, well_name: str, - well_location: Optional[WellLocation], + well_location: Optional[Union[WellLocation, LiquidHandlingWellLocation]], origin: Point, origin_cp: Optional[CriticalPoint], max_travel_z: float, diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index a825ee9bd34..190c46e3d4e 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -239,31 +239,44 @@ class WellOffset(BaseModel): z: float = 0 -# get rid of operationVolume below and operation_volume parameter in methods? # TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location class WellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update + """A relative location in reference to a well's location.""" # update origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) - volumeOffset: Union[float, Literal["operationVolume"]] = Field(default=0.0, description="""A volume of liquid to account for when - executing commands with an origin of WellOrigin.MENISCUS. Specifying - `operationVolume` results in this class acting as a sentinel and should - be used when volume can be determined from the command parameters, for - example commanding Aspirate. A volume should be specified when it cannot - be determined from the command parameters, for example commanding - MoveToWell prior to AspirateInPlace.""") # update comment + volumeOffset: float = Field( + default=0.0, + description="""A volume of liquid to account for when + executing commands with an origin of WellOrigin.MENISCUS. Specifying + `operationVolume` results in this class acting as a sentinel and should + be used when volume can be determined from the command parameters, for + example commanding Aspirate. A volume should be specified when it cannot + be determined from the command parameters, for example commanding + MoveToWell prior to AspirateInPlace.""", + ) # update comment -class LiquidProbeWellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update +# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location +class LiquidHandlingWellLocation(BaseModel): + """A relative location in reference to a well's location.""" # update origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) + volumeOffset: Union[float, Literal["operationVolume"]] = Field( + default=0.0, + description="""A volume of liquid to account for when + executing commands with an origin of WellOrigin.MENISCUS. Specifying + `operationVolume` results in this class acting as a sentinel and should + be used when volume can be determined from the command parameters, for + example commanding Aspirate. A volume should be specified when it cannot + be determined from the command parameters, for example commanding + MoveToWell prior to AspirateInPlace.""", + ) # update comment class PickUpTipWellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update + """A relative location in reference to a well's location.""" # update origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 8854c070ef0..1c7585417c3 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -16,6 +16,8 @@ LoadedPipette, MotorAxis, WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, WellOffset, WellOrigin, DropTipWellLocation, @@ -290,7 +292,7 @@ def test_pick_up_tip( pipetteId="abc123", labwareId="labware-id", wellName="well-name", - wellLocation=WellLocation( + wellLocation=PickUpTipWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ) @@ -518,7 +520,7 @@ def test_aspirate_from_well( pipetteId="abc123", labwareId="123abc", wellName="my cool well", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), volume=12.34, @@ -632,7 +634,7 @@ def test_blow_out_to_well( pipetteId="abc123", labwareId="123abc", wellName="my cool well", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), flowRate=6.7, @@ -743,7 +745,7 @@ def test_dispense_to_well( pipetteId="abc123", labwareId="123abc", wellName="my cool well", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), volume=12.34, diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 795910ee95d..8c6f71b18bc 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -8,7 +8,12 @@ from opentrons.protocol_engine.commands.pipetting_common import OverpressureError from opentrons.protocol_engine.state import update_types from opentrons.types import MountType, Point -from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint +from opentrons.protocol_engine import ( + LiquidHandlingWellLocation, + WellOrigin, + WellOffset, + DeckPoint, +) from opentrons.protocol_engine.commands.aspirate import ( AspirateParams, @@ -59,7 +64,9 @@ async def test_aspirate_implementation_no_prep( mock_command_note_adder: CommandNoteAdder, ) -> None: """An Aspirate should have an execution implementation without preparing to aspirate.""" - location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) data = AspirateParams( pipetteId="abc", @@ -117,7 +124,9 @@ async def test_aspirate_implementation_with_prep( subject: AspirateImplementation, ) -> None: """An Aspirate should have an execution implementation with preparing to aspirate.""" - location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) data = AspirateParams( pipetteId="abc", @@ -178,7 +187,7 @@ async def test_aspirate_implementation_with_prep( pipette_id="abc", labware_id="123", well_name="A3", - well_location=WellLocation(origin=WellOrigin.TOP), + well_location=LiquidHandlingWellLocation(origin=WellOrigin.TOP), ), await pipetting.prepare_for_aspirate(pipette_id="abc"), ) @@ -192,7 +201,9 @@ async def test_aspirate_raises_volume_error( subject: AspirateImplementation, ) -> None: """Should raise an assertion error for volume larger than working volume.""" - location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) data = AspirateParams( pipetteId="abc", @@ -241,7 +252,7 @@ async def test_overpressure_error( pipette_id = "pipette-id" labware_id = "labware-id" well_name = "well-name" - well_location = WellLocation( + well_location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index d762d18096e..8ceb4ab17c3 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -2,7 +2,12 @@ from decoy import Decoy from opentrons.types import Point -from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint +from opentrons.protocol_engine import ( + LiquidHandlingWellLocation, + WellOrigin, + WellOffset, + DeckPoint, +) from opentrons.protocol_engine.state import update_types from opentrons.protocol_engine.state.state import StateView from opentrons.protocol_engine.commands import ( @@ -33,7 +38,9 @@ async def test_blow_out_implementation( pipetting=pipetting, ) - location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) + location = LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) + ) data = BlowOutParams( pipetteId="pipette-id", diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 223cfcc78c9..497e1573796 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -6,7 +6,12 @@ from opentrons_shared_data.errors.exceptions import PipetteOverpressureError -from opentrons.protocol_engine import WellLocation, WellOrigin, WellOffset, DeckPoint +from opentrons.protocol_engine import ( + LiquidHandlingWellLocation, + WellOrigin, + WellOffset, + DeckPoint, +) from opentrons.protocol_engine.execution import MovementHandler, PipettingHandler from opentrons.protocol_engine.state import update_types from opentrons.types import Point @@ -40,7 +45,7 @@ async def test_dispense_implementation( subject: DispenseImplementation, ) -> None: """It should move to the target location and then dispense.""" - well_location = WellLocation( + well_location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) @@ -97,7 +102,7 @@ async def test_overpressure_error( pipette_id = "pipette-id" labware_id = "labware-id" well_name = "well-name" - well_location = WellLocation( + well_location = LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index 9b92fd219a0..ce2b2cd2431 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -5,7 +5,12 @@ from opentrons.types import MountType, Point -from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint +from opentrons.protocol_engine import ( + WellLocation, + PickUpTipWellLocation, + WellOffset, + DeckPoint, +) from opentrons.protocol_engine.errors import TipNotAttachedError from opentrons.protocol_engine.execution import MovementHandler, TipHandler from opentrons.protocol_engine.resources import ModelUtils @@ -61,7 +66,7 @@ async def test_success( pipetteId="pipette-id", labwareId="labware-id", wellName="A3", - wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3)), + wellLocation=PickUpTipWellLocation(offset=WellOffset(x=1, y=2, z=3)), ) ) diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index df7fb4dca9a..0cdb7a4b8b1 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -13,6 +13,7 @@ ModuleDefinition, MovementAxis, WellLocation, + LiquidHandlingWellLocation, LabwareLocation, DeckSlotLocation, LabwareMovementStrategy, @@ -195,7 +196,7 @@ def create_aspirate_command( flow_rate: float, labware_id: str = "labware-id", well_name: str = "A1", - well_location: Optional[WellLocation] = None, + well_location: Optional[LiquidHandlingWellLocation] = None, destination: DeckPoint = DeckPoint(x=0, y=0, z=0), ) -> cmd.Aspirate: """Get a completed Aspirate command.""" @@ -203,7 +204,7 @@ def create_aspirate_command( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, - wellLocation=well_location or WellLocation(), + wellLocation=well_location or LiquidHandlingWellLocation(), volume=volume, flowRate=flow_rate, ) @@ -248,7 +249,7 @@ def create_dispense_command( flow_rate: float, labware_id: str = "labware-id", well_name: str = "A1", - well_location: Optional[WellLocation] = None, + well_location: Optional[LiquidHandlingWellLocation] = None, destination: DeckPoint = DeckPoint(x=0, y=0, z=0), ) -> cmd.Dispense: """Get a completed Dispense command.""" @@ -256,7 +257,7 @@ def create_dispense_command( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, - wellLocation=well_location or WellLocation(), + wellLocation=well_location or LiquidHandlingWellLocation(), volume=volume, flowRate=flow_rate, ) @@ -495,7 +496,7 @@ def create_blow_out_command( flow_rate: float, labware_id: str = "labware-id", well_name: str = "A1", - well_location: Optional[WellLocation] = None, + well_location: Optional[LiquidHandlingWellLocation] = None, destination: DeckPoint = DeckPoint(x=0, y=0, z=0), ) -> cmd.BlowOut: """Get a completed BlowOut command.""" @@ -503,7 +504,7 @@ def create_blow_out_command( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, - wellLocation=well_location or WellLocation(), + wellLocation=well_location or LiquidHandlingWellLocation(), flowRate=flow_rate, ) result = cmd.BlowOutResult(position=destination) diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index d7c82ca7475..bc1c75a20b8 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -26,6 +26,7 @@ Location, ProfileStep, WellLocation as SD_WellLocation, + LiquidHandlingWellLocation as SD_LiquidHandlingWellLocation, OffsetVector, Metadata as SD_Metadata, Module, @@ -40,6 +41,8 @@ DeckPoint, DeckSlotLocation, WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, DropTipWellLocation, WellOrigin, DropTipWellOrigin, @@ -91,7 +94,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - wellLocation=SD_WellLocation( + liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -106,7 +109,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=7.89), ), @@ -153,7 +156,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - wellLocation=SD_WellLocation( + liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -167,7 +170,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=7.89), ), @@ -241,7 +244,7 @@ pipetteId="pipette-id-1", labwareId="labware-id-2", wellName="A1", - wellLocation=WellLocation(), + wellLocation=PickUpTipWellLocation(), ) ), ), @@ -425,7 +428,7 @@ pipetteId="pipette-id-1", labwareId="labware-id-2", wellName="A1", - wellLocation=SD_WellLocation( + liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -437,7 +440,7 @@ pipetteId="pipette-id-1", labwareId="labware-id-2", wellName="A1", - wellLocation=WellLocation( + wellLocation=LiquidHandlingWellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=7.89), ), diff --git a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py index 76f8449d93d..e0c653727e0 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py @@ -10,6 +10,7 @@ Location, ProfileStep, WellLocation, + LiquidHandlingWellLocation, OffsetVector, Dimensions, GroupMetadata, @@ -35,6 +36,7 @@ "Location", "ProfileStep", "WellLocation", + "LiquidHandlingWellLocation", "OffsetVector", "Dimensions", "GroupMetadata", diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py index ce5b814d73f..26d369a74f1 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py @@ -9,6 +9,7 @@ Location, ProfileStep, WellLocation, + LiquidHandlingWellLocation, OffsetVector, Metadata, DesignerApplication, @@ -33,6 +34,7 @@ class Params(BaseModel): volume: Optional[float] flowRate: Optional[float] wellLocation: Optional[WellLocation] + liquidHandlingWellLocation: Optional[LiquidHandlingWellLocation] waitForResume: Optional[Literal[True]] seconds: Optional[float] minimumZHeight: Optional[float] diff --git a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py index 8cf3276f71f..21249f53d0f 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Dict, Any +from typing import Optional, List, Dict, Any, Union from typing_extensions import Literal from enum import Enum from pydantic import BaseModel @@ -92,6 +92,13 @@ class ProfileStep(BaseModel): class WellLocation(BaseModel): origin: Optional[str] offset: Optional[OffsetVector] + volumeOffset: Optional[float] + + +class LiquidHandlingWellLocation(BaseModel): + origin: Optional[str] + offset: Optional[OffsetVector] + volumeOffset: Optional[Union[float, Literal["operationVolume"]]] class Liquid(BaseModel): From 3d531375044b846f3d0f48b42ce9c30f54a0fa07 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Tue, 24 Sep 2024 16:18:43 -0400 Subject: [PATCH 13/51] split get_well_position into submethods --- .../protocol_engine/state/geometry.py | 116 ++++++++++++------ 1 file changed, 76 insertions(+), 40 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index be2ad1e4324..afeea03b5f0 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -417,13 +417,15 @@ def get_labware_position(self, labware_id: str) -> Point: z=origin_pos.z + cal_offset.z, ) + WellLocations = Union[ + WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation + ] + def get_well_position( self, labware_id: str, well_name: str, - well_location: Optional[ - Union[WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation] - ] = None, + well_location: Optional[WellLocations] = None, operation_volume: Optional[float] = None, ) -> Point: """Given relative well location in a labware, get absolute position.""" @@ -434,33 +436,14 @@ def get_well_position( offset = WellOffset(x=0, y=0, z=well_depth) if well_location is not None: offset = well_location.offset - starting_liquid_height: Union[float, None] = 0.0 - if well_location.origin == WellOrigin.TOP: - starting_liquid_height = well_depth - elif well_location.origin == WellOrigin.CENTER: - starting_liquid_height = well_depth / 2.0 - elif well_location.origin == WellOrigin.MENISCUS: - starting_liquid_height = self._wells.get_last_measured_liquid_height( - labware_id=labware_id, well_name=well_name - ) - if starting_liquid_height is None: - raise errors.LiquidHeightUnknownError( - "Must liquid probe before specifying WellOrigin.MENISCUS." - ) - if isinstance(well_location, PickUpTipWellLocation): - volume = 0.0 - elif isinstance(well_location.volumeOffset, float): - volume = well_location.volumeOffset - elif well_location.volumeOffset == "operationVolume": - volume = operation_volume or 0.0 - assert isinstance(starting_liquid_height, float) - height_after_operation = self.get_height_after_volume( + offset_adjustment = self.get_well_offset_adjustment( labware_id=labware_id, well_name=well_name, - starting_height=starting_liquid_height, - volume=volume, + well_location=well_location, + well_depth=well_depth, + operation_volume=operation_volume, ) - offset = offset.copy(update={"z": offset.z + height_after_operation}) + offset = offset.copy(update={"z": offset.z + offset_adjustment}) return Point( x=labware_pos.x + offset.x + well_def.x, @@ -1262,6 +1245,59 @@ def get_offset_location(self, labware_id: str) -> Optional[LabwareOffsetLocation return None + def get_well_offset_adjustment( + self, + labware_id: str, + well_name: str, + well_location: WellLocations, + well_depth: float, + operation_volume: Optional[float] = None, + ) -> float: + """Return a z-axis distance that accounts for well handling height and operation volume (with reference to the well bottom).""" + initial_handling_height = self.get_well_handling_height( + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + well_depth=well_depth, + ) + if isinstance(well_location, PickUpTipWellLocation): + volume = 0.0 + elif isinstance(well_location.volumeOffset, float): + volume = well_location.volumeOffset + elif well_location.volumeOffset == "operationVolume": + volume = operation_volume or 0.0 + + return self.get_well_height_after_volume( + labware_id=labware_id, + well_name=well_name, + initial_height=initial_handling_height, + volume=volume, + ) + + def get_well_handling_height( + self, + labware_id: str, + well_name: str, + well_location: WellLocations, + well_depth: float, + ) -> float: + """Return the handling height for a labware well (with reference to the well bottom).""" + handling_height: Union[float, None] = 0.0 + if well_location.origin == WellOrigin.TOP: + handling_height = well_depth + elif well_location.origin == WellOrigin.CENTER: + handling_height = well_depth / 2.0 + elif well_location.origin == WellOrigin.MENISCUS: + handling_height = self._wells.get_last_measured_liquid_height( + labware_id=labware_id, well_name=well_name + ) + if handling_height is None: + raise errors.LiquidHeightUnknownError( + "Must liquid probe before specifying WellOrigin.MENISCUS." + ) + assert isinstance(handling_height, float) + return handling_height + def get_well_volumetric_capacity( self, labware_id: str, well_id: str ) -> List[Tuple[float, float]]: @@ -1276,33 +1312,33 @@ def get_well_volumetric_capacity( ) return get_well_volumetric_capacity(well_geometry) - def get_volume_at_height( + def get_well_volume_at_height( self, labware_id: str, well_id: str, target_height: float ) -> float: """Return the volume of liquid in a labware well at a given liquid height (with reference to the well bottom).""" return 0.0 - def get_height_at_volume( + def get_well_height_at_volume( self, labware_id: str, well_id: str, target_volume: float ) -> float: """Return the height of liquid in a labware well at a given liquid volume.""" return 0.0 - def get_height_after_volume( - self, labware_id: str, well_name: str, starting_height: float, volume: float + def get_well_height_after_volume( + self, labware_id: str, well_name: str, initial_height: float, volume: float ) -> float: - """Return the height of liquid in a labware well after a given volume has been handled, given a starting liquid height (with reference to the well bottom).""" + """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" well_def = self._labware.get_well_definition(labware_id, well_name) well_id = well_def.geometryDefinitionId # make non-optional eventually assert isinstance(well_id, str) - starting_volume = self.get_volume_at_height( - labware_id=labware_id, well_id=well_id, target_height=starting_height + initial_volume = self.get_well_volume_at_height( + labware_id=labware_id, well_id=well_id, target_height=initial_height ) - ending_volume = starting_volume + volume - ending_height = self.get_height_at_volume( - labware_id=labware_id, well_id=well_id, target_volume=ending_volume + final_volume = initial_volume + volume + final_height = self.get_well_height_at_volume( # just return this method call + labware_id=labware_id, well_id=well_id, target_volume=final_volume ) - if ending_height: # delete + if final_height: # delete pass # delete - # return ending_height - return starting_height # delete, use line above once sub-methods implemented + # return final_height + return initial_height # delete, use line above once sub-methods implemented From 68981a32e63b2debbe918db19b5c64af385da48f Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 12:40:41 -0400 Subject: [PATCH 14/51] test fixes --- .../protocol_engine/commands/pick_up_tip.py | 2 +- .../protocol_engine/state/geometry.py | 46 ++++---- .../core/engine/test_instrument_core.py | 32 ++++-- .../commands/test_pick_up_tip.py | 12 +++ .../protocol_runner/test_json_translator.py | 4 +- shared-data/command/schemas/9.json | 101 ++++++++++++------ 6 files changed, 126 insertions(+), 71 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py index 0e9aebd7c17..6134d451675 100644 --- a/api/src/opentrons/protocol_engine/commands/pick_up_tip.py +++ b/api/src/opentrons/protocol_engine/commands/pick_up_tip.py @@ -113,7 +113,7 @@ async def execute( state_update = update_types.StateUpdate() - well_location = self._state_view.geometry.convert_pick_up_tip_location( + well_location = self._state_view.geometry.convert_pick_up_tip_well_location( well_location=params.wellLocation ) position = await self._movement.move_to_well( diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index afeea03b5f0..f3c9addbd1c 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -627,18 +627,11 @@ def get_checked_tip_drop_location( ), ) - def convert_pick_up_tip_location( + def convert_pick_up_tip_well_location( self, well_location: PickUpTipWellLocation ) -> WellLocation: """Convert PickUpTipWellLocation to WellLocation.""" - return WellLocation( - origin=well_location.origin, - offset=WellOffset( - x=well_location.offset.x, - y=well_location.offset.y, - z=well_location.offset.z, - ), - ) + return WellLocation(origin=well_location.origin, offset=well_location.offset) # TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411 def _get_staging_slot_name(self, labware_id: str) -> str: @@ -1282,21 +1275,22 @@ def get_well_handling_height( well_depth: float, ) -> float: """Return the handling height for a labware well (with reference to the well bottom).""" - handling_height: Union[float, None] = 0.0 + handling_height = 0.0 if well_location.origin == WellOrigin.TOP: handling_height = well_depth elif well_location.origin == WellOrigin.CENTER: handling_height = well_depth / 2.0 elif well_location.origin == WellOrigin.MENISCUS: - handling_height = self._wells.get_last_measured_liquid_height( + liquid_height = self._wells.get_last_measured_liquid_height( labware_id=labware_id, well_name=well_name ) - if handling_height is None: + if liquid_height is None: raise errors.LiquidHeightUnknownError( "Must liquid probe before specifying WellOrigin.MENISCUS." ) - assert isinstance(handling_height, float) - return handling_height + else: + handling_height = liquid_height + return float(handling_height) def get_well_volumetric_capacity( self, labware_id: str, well_id: str @@ -1330,15 +1324,15 @@ def get_well_height_after_volume( """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" well_def = self._labware.get_well_definition(labware_id, well_name) well_id = well_def.geometryDefinitionId # make non-optional eventually - assert isinstance(well_id, str) - initial_volume = self.get_well_volume_at_height( - labware_id=labware_id, well_id=well_id, target_height=initial_height - ) - final_volume = initial_volume + volume - final_height = self.get_well_height_at_volume( # just return this method call - labware_id=labware_id, well_id=well_id, target_volume=final_volume - ) - if final_height: # delete - pass # delete - # return final_height - return initial_height # delete, use line above once sub-methods implemented + if well_id is not None: + initial_volume = self.get_well_volume_at_height( + labware_id=labware_id, well_id=well_id, target_height=initial_height + ) + final_volume = initial_volume + volume + if final_volume: # delete + pass # delete + # return self.get_well_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=final_volume) + return initial_height # delete, use line above once sub-methods implemented + else: + # raise exception?! + return initial_height diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 1c7585417c3..ea5b22aa989 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -258,12 +258,14 @@ def test_pick_up_tip( ) decoy.when( - mock_engine_client.state.geometry.get_relative_well_location( + mock_engine_client.state.geometry.get_relative_pick_up_tip_well_location( labware_id="labware-id", well_name="well-name", absolute_point=Point(1, 2, 3), ) - ).then_return(WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1))) + ).then_return( + PickUpTipWellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)) + ) subject.pick_up_tip( location=location, @@ -283,7 +285,7 @@ def test_pick_up_tip( pipette_id="abc123", labware_id="labware-id", well_name="well-name", - well_location=WellLocation( + well_location=PickUpTipWellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), @@ -491,10 +493,14 @@ def test_aspirate_from_well( ) decoy.when( - mock_engine_client.state.geometry.get_relative_well_location( + mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) ) - ).then_return(WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1))) + ).then_return( + LiquidHandlingWellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + ) + ) subject.aspirate( location=location, @@ -612,10 +618,14 @@ def test_blow_out_to_well( ) decoy.when( - mock_engine_client.state.geometry.get_relative_well_location( + mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) ) - ).then_return(WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1))) + ).then_return( + LiquidHandlingWellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + ) + ) subject.blow_out(location=location, well_core=well_core, in_place=False) @@ -715,10 +725,14 @@ def test_dispense_to_well( decoy.when(mock_protocol_core.api_version).then_return(MAX_SUPPORTED_VERSION) decoy.when( - mock_engine_client.state.geometry.get_relative_well_location( + mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) ) - ).then_return(WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1))) + ).then_return( + LiquidHandlingWellLocation( + origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + ) + ) subject.dispense( location=location, diff --git a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py index ce2b2cd2431..a9ebbfd1e3a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py +++ b/api/tests/opentrons/protocol_engine/commands/test_pick_up_tip.py @@ -44,6 +44,12 @@ async def test_success( decoy.when(state_view.pipettes.get_mount("pipette-id")).then_return(MountType.LEFT) + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset(x=1, y=2, z=3)) + ) + ).then_return(WellLocation(offset=WellOffset(x=1, y=2, z=3))) + decoy.when( await movement.move_to_well( pipette_id="pipette-id", @@ -109,6 +115,12 @@ async def test_tip_physically_missing_error( error_id = "error-id" error_created_at = datetime(1234, 5, 6) + decoy.when( + state_view.geometry.convert_pick_up_tip_well_location( + well_location=PickUpTipWellLocation(offset=WellOffset()) + ) + ).then_return(WellLocation(offset=WellOffset())) + decoy.when( await movement.move_to_well( pipette_id="pipette-id", diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index bc1c75a20b8..20aa8b6c36f 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -850,7 +850,9 @@ def test_load_command( ) assert v6_output == [expected_output] assert v7_output == [expected_output] - assert v8_output == [expected_output] + assert v8_output == [ + expected_output + ] # origin.TOP, offset.z=0 != origin.BOTTOM, offset.z=7.89 def test_load_liquid( diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 79e690fbd38..26230b0c78b 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -318,28 +318,8 @@ } } }, - "WellVolumeOffset": { - "title": "WellVolumeOffset", - "description": "A volume of liquid to account for when executing commands with an origin of WellOrigin.MENISCUS.\n\nSpecifying `operationVolume` results in this class acting as a sentinel and should be used when\nvolume can be determined from the command parameters, for example commanding Aspirate. A volume\nshould be specified when it cannot be determined from the command parameters, for example commanding\nMoveToWell prior to AspirateInPlace.", - "type": "object", - "properties": { - "volumeOffset": { - "title": "Volumeoffset", - "default": 0.0, - "anyOf": [ - { - "type": "number" - }, - { - "enum": ["operationVolume"], - "type": "string" - } - ] - } - } - }, - "WellLocation": { - "title": "WellLocation", + "LiquidHandlingWellLocation": { + "title": "LiquidHandlingWellLocation", "description": "A relative location in reference to a well's location.", "type": "object", "properties": { @@ -355,7 +335,18 @@ "$ref": "#/definitions/WellOffset" }, "volumeOffset": { - "$ref": "#/definitions/WellVolumeOffset" + "title": "Volumeoffset", + "description": "A volume of liquid to account for when\n executing commands with an origin of WellOrigin.MENISCUS. Specifying\n `operationVolume` results in this class acting as a sentinel and should\n be used when volume can be determined from the command parameters, for\n example commanding Aspirate. A volume should be specified when it cannot\n be determined from the command parameters, for example commanding\n MoveToWell prior to AspirateInPlace.", + "default": 0.0, + "anyOf": [ + { + "type": "number" + }, + { + "enum": ["operationVolume"], + "type": "string" + } + ] } } }, @@ -379,7 +370,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -815,7 +806,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -954,7 +945,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -2020,6 +2011,30 @@ }, "required": ["params"] }, + "WellLocation": { + "title": "WellLocation", + "description": "A relative location in reference to a well's location.", + "type": "object", + "properties": { + "origin": { + "default": "top", + "allOf": [ + { + "$ref": "#/definitions/WellOrigin" + } + ] + }, + "offset": { + "$ref": "#/definitions/WellOffset" + }, + "volumeOffset": { + "title": "Volumeoffset", + "description": "A volume of liquid to account for when\n executing commands with an origin of WellOrigin.MENISCUS. Specifying\n `operationVolume` results in this class acting as a sentinel and should\n be used when volume can be determined from the command parameters, for\n example commanding Aspirate. A volume should be specified when it cannot\n be determined from the command parameters, for example commanding\n MoveToWell prior to AspirateInPlace.", + "default": 0.0, + "type": "number" + } + } + }, "MoveToWellParams": { "title": "MoveToWellParams", "description": "Payload required to move a pipette to a specific well.", @@ -2425,11 +2440,34 @@ }, "required": ["params"] }, + "PickUpTipWellLocation": { + "title": "PickUpTipWellLocation", + "description": "A relative location in reference to a well's location.", + "type": "object", + "properties": { + "origin": { + "default": "top", + "allOf": [ + { + "$ref": "#/definitions/WellOrigin" + } + ] + }, + "offset": { + "$ref": "#/definitions/WellOffset" + } + } + }, "PickUpTipParams": { "title": "PickUpTipParams", "description": "Payload needed to move a pipette to a specific well.", "type": "object", "properties": { + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, "labwareId": { "title": "Labwareid", "description": "Identifier of labware to use.", @@ -2442,20 +2480,15 @@ }, "wellLocation": { "title": "Welllocation", - "description": "Relative well location at which to perform the operation", + "description": "Relative well location at which to pick up the tip.", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/PickUpTipWellLocation" } ] - }, - "pipetteId": { - "title": "Pipetteid", - "description": "Identifier of pipette to use for liquid handling.", - "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": ["pipetteId", "labwareId", "wellName"] }, "PickUpTipCreate": { "title": "PickUpTipCreate", From a9b6dfbd6285ff10056484c41f162e7ff9e1244d Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 12:58:07 -0400 Subject: [PATCH 15/51] updated typescript --- shared-data/command/types/gantry.ts | 3 +- shared-data/command/types/pipetting.ts | 38 +++++++++++++++++++++++--- 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/shared-data/command/types/gantry.ts b/shared-data/command/types/gantry.ts index 1762c608499..44d5a352e2d 100644 --- a/shared-data/command/types/gantry.ts +++ b/shared-data/command/types/gantry.ts @@ -126,12 +126,13 @@ export interface MoveToWellParams { labwareId: string wellName: string wellLocation?: { - origin?: 'top' | 'bottom' + origin?: 'top' | 'center' | 'bottom' | 'meniscus' offset?: { x?: number y?: number z?: number } + volumeOffset?: number } minimumZHeight?: number forceDirect?: boolean diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index 7c4c46ef487..32c9609737d 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -221,10 +221,10 @@ export interface TryLiquidProbeRunTimeCommand export type AspDispAirgapParams = FlowRateParams & PipetteAccessParams & VolumeParams & - WellLocationParam + LiquidHandlingWellLocationParam export type BlowoutParams = FlowRateParams & PipetteAccessParams & - WellLocationParam + LiquidHandlingWellLocationParam export type TouchTipParams = PipetteAccessParams & WellLocationParam export type DropTipParams = PipetteAccessParams & { wellLocation?: { @@ -237,7 +237,17 @@ export type DropTipParams = PipetteAccessParams & { } } } -export type PickUpTipParams = TouchTipParams +export type PickUpTipParams = PipetteAccessParams & { + wellLocation?: { + origin?: 'top' | 'center' | 'bottom' | 'meniscus' + offset?: { + // mm values all default to 0 + x?: number + y?: number + z?: number + } + } +} interface AddressableOffsetVector { x: number @@ -294,7 +304,24 @@ interface VolumeParams { interface WellLocationParam { wellLocation?: { // default value is 'top' - origin?: 'top' | 'center' | 'bottom' + origin?: 'top' | 'center' | 'bottom' | 'meniscus' + offset?: { + // mm + // all values default to 0 + x?: number + y?: number + z?: number + } + // µL + // default is 0 + volumeOffset?: number + } +} + +interface LiquidHandlingWellLocationParam { + wellLocation?: { + // default value is 'top' + origin?: 'top' | 'center' | 'bottom' | 'meniscus' offset?: { // mm // all values default to 0 @@ -302,6 +329,9 @@ interface WellLocationParam { y?: number z?: number } + // µL + // default is 0 + volumeOffset?: number | 'operationVolume' } } From 8c0f5ce82dfc7a18a3fe1789e4cf9c95b364c0f2 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 14:07:37 -0400 Subject: [PATCH 16/51] schema and test fixes --- .../opentrons/protocol_runner/test_json_translator.py | 10 ++++------ .../opentrons_shared_data/protocol/models/__init__.py | 4 ++++ .../protocol/models/protocol_schema_v8.py | 5 +++-- .../protocol/models/shared_models.py | 10 ++++++++++ 4 files changed, 21 insertions(+), 8 deletions(-) diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 20aa8b6c36f..d587aa49ca2 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -94,7 +94,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( + wellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -156,7 +156,7 @@ volume=1.23, flowRate=4.56, wellName="A1", - liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( + wellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -428,7 +428,7 @@ pipetteId="pipette-id-1", labwareId="labware-id-2", wellName="A1", - liquidHandlingWellLocation=SD_LiquidHandlingWellLocation( + wellLocation=SD_LiquidHandlingWellLocation( origin="bottom", offset=OffsetVector(x=0, y=0, z=7.89), ), @@ -850,9 +850,7 @@ def test_load_command( ) assert v6_output == [expected_output] assert v7_output == [expected_output] - assert v8_output == [ - expected_output - ] # origin.TOP, offset.z=0 != origin.BOTTOM, offset.z=7.89 + assert v8_output == [expected_output] def test_load_liquid( diff --git a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py index e0c653727e0..a8aed145690 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py @@ -11,6 +11,8 @@ ProfileStep, WellLocation, LiquidHandlingWellLocation, + DropTipWellLocation, + PickUpTipWellLocation, OffsetVector, Dimensions, GroupMetadata, @@ -37,6 +39,8 @@ "ProfileStep", "WellLocation", "LiquidHandlingWellLocation", + "DropTipWellLocation", + "PickUpTipWellLocation", "OffsetVector", "Dimensions", "GroupMetadata", diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py index 26d369a74f1..a5121574acb 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py @@ -10,6 +10,8 @@ ProfileStep, WellLocation, LiquidHandlingWellLocation, + DropTipWellLocation, + PickUpTipWellLocation, OffsetVector, Metadata, DesignerApplication, @@ -33,8 +35,7 @@ class Params(BaseModel): wellName: Optional[str] volume: Optional[float] flowRate: Optional[float] - wellLocation: Optional[WellLocation] - liquidHandlingWellLocation: Optional[LiquidHandlingWellLocation] + wellLocation: Optional[Union[WellLocation, LiquidHandlingWellLocation, DropTipWellLocation, PickUpTipWellLocation]] waitForResume: Optional[Literal[True]] seconds: Optional[float] minimumZHeight: Optional[float] diff --git a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py index 21249f53d0f..dad7d0a1e4b 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py @@ -101,6 +101,16 @@ class LiquidHandlingWellLocation(BaseModel): volumeOffset: Optional[Union[float, Literal["operationVolume"]]] +class DropTipWellLocation(BaseModel): + origin: Optional[str] + offset: Optional[OffsetVector] + + +class PickUpTipWellLocation(BaseModel): + origin: Optional[str] + offset: Optional[OffsetVector] + + class Liquid(BaseModel): displayName: str description: str From 2bc1d669606cbb19f650b5e62c6bc8a5db4a72e1 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 14:12:07 -0400 Subject: [PATCH 17/51] revert typescript work --- shared-data/command/types/gantry.ts | 3 +- shared-data/command/types/pipetting.ts | 38 +++----------------------- 2 files changed, 5 insertions(+), 36 deletions(-) diff --git a/shared-data/command/types/gantry.ts b/shared-data/command/types/gantry.ts index 44d5a352e2d..1762c608499 100644 --- a/shared-data/command/types/gantry.ts +++ b/shared-data/command/types/gantry.ts @@ -126,13 +126,12 @@ export interface MoveToWellParams { labwareId: string wellName: string wellLocation?: { - origin?: 'top' | 'center' | 'bottom' | 'meniscus' + origin?: 'top' | 'bottom' offset?: { x?: number y?: number z?: number } - volumeOffset?: number } minimumZHeight?: number forceDirect?: boolean diff --git a/shared-data/command/types/pipetting.ts b/shared-data/command/types/pipetting.ts index 32c9609737d..7c4c46ef487 100644 --- a/shared-data/command/types/pipetting.ts +++ b/shared-data/command/types/pipetting.ts @@ -221,10 +221,10 @@ export interface TryLiquidProbeRunTimeCommand export type AspDispAirgapParams = FlowRateParams & PipetteAccessParams & VolumeParams & - LiquidHandlingWellLocationParam + WellLocationParam export type BlowoutParams = FlowRateParams & PipetteAccessParams & - LiquidHandlingWellLocationParam + WellLocationParam export type TouchTipParams = PipetteAccessParams & WellLocationParam export type DropTipParams = PipetteAccessParams & { wellLocation?: { @@ -237,17 +237,7 @@ export type DropTipParams = PipetteAccessParams & { } } } -export type PickUpTipParams = PipetteAccessParams & { - wellLocation?: { - origin?: 'top' | 'center' | 'bottom' | 'meniscus' - offset?: { - // mm values all default to 0 - x?: number - y?: number - z?: number - } - } -} +export type PickUpTipParams = TouchTipParams interface AddressableOffsetVector { x: number @@ -304,24 +294,7 @@ interface VolumeParams { interface WellLocationParam { wellLocation?: { // default value is 'top' - origin?: 'top' | 'center' | 'bottom' | 'meniscus' - offset?: { - // mm - // all values default to 0 - x?: number - y?: number - z?: number - } - // µL - // default is 0 - volumeOffset?: number - } -} - -interface LiquidHandlingWellLocationParam { - wellLocation?: { - // default value is 'top' - origin?: 'top' | 'center' | 'bottom' | 'meniscus' + origin?: 'top' | 'center' | 'bottom' offset?: { // mm // all values default to 0 @@ -329,9 +302,6 @@ interface LiquidHandlingWellLocationParam { y?: number z?: number } - // µL - // default is 0 - volumeOffset?: number | 'operationVolume' } } From 8731a196ac82292f7030e1c976984ee74f383263 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 15:50:04 -0400 Subject: [PATCH 18/51] cleaned up comments --- api/src/opentrons/protocol_engine/types.py | 29 ++++++---------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 190c46e3d4e..324cd1245ca 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -239,44 +239,31 @@ class WellOffset(BaseModel): z: float = 0 -# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location +# TODO(pm, 2024-09-23): PE should raise error if volumeOffset or WellOrigin.MENISCUS specified with a tip rack location class WellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update + """A relative location in reference to a well's location.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) volumeOffset: float = Field( default=0.0, - description="""A volume of liquid to account for when - executing commands with an origin of WellOrigin.MENISCUS. Specifying - `operationVolume` results in this class acting as a sentinel and should - be used when volume can be determined from the command parameters, for - example commanding Aspirate. A volume should be specified when it cannot - be determined from the command parameters, for example commanding - MoveToWell prior to AspirateInPlace.""", - ) # update comment + description="""A volume of liquid, in µL, to offset the z-axis offset.""", + ) -# TODO(pm, 2024-09-23): PE should raise error if volumeOffset specified with a tip rack location class LiquidHandlingWellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update + """A relative location in reference to a well's location. To be used with commands that handle liquids.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) volumeOffset: Union[float, Literal["operationVolume"]] = Field( default=0.0, - description="""A volume of liquid to account for when - executing commands with an origin of WellOrigin.MENISCUS. Specifying - `operationVolume` results in this class acting as a sentinel and should - be used when volume can be determined from the command parameters, for - example commanding Aspirate. A volume should be specified when it cannot - be determined from the command parameters, for example commanding - MoveToWell prior to AspirateInPlace.""", - ) # update comment + description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the volume command parameter.""", + ) class PickUpTipWellLocation(BaseModel): - """A relative location in reference to a well's location.""" # update + """A relative location in reference to a well's location. To be used for picking up tips.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) From 32760c3f5e639aa4edf1e92a3c4d7636f93eaeb2 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 15:56:32 -0400 Subject: [PATCH 19/51] updated command schema and formatted --- shared-data/command/schemas/9.json | 8 ++++---- .../protocol/models/protocol_schema_v8.py | 9 ++++++++- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index 26230b0c78b..5fd4e78e629 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -320,7 +320,7 @@ }, "LiquidHandlingWellLocation": { "title": "LiquidHandlingWellLocation", - "description": "A relative location in reference to a well's location.", + "description": "A relative location in reference to a well's location. To be used with commands that handle liquids.", "type": "object", "properties": { "origin": { @@ -336,7 +336,7 @@ }, "volumeOffset": { "title": "Volumeoffset", - "description": "A volume of liquid to account for when\n executing commands with an origin of WellOrigin.MENISCUS. Specifying\n `operationVolume` results in this class acting as a sentinel and should\n be used when volume can be determined from the command parameters, for\n example commanding Aspirate. A volume should be specified when it cannot\n be determined from the command parameters, for example commanding\n MoveToWell prior to AspirateInPlace.", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the volume command parameter.", "default": 0.0, "anyOf": [ { @@ -2029,7 +2029,7 @@ }, "volumeOffset": { "title": "Volumeoffset", - "description": "A volume of liquid to account for when\n executing commands with an origin of WellOrigin.MENISCUS. Specifying\n `operationVolume` results in this class acting as a sentinel and should\n be used when volume can be determined from the command parameters, for\n example commanding Aspirate. A volume should be specified when it cannot\n be determined from the command parameters, for example commanding\n MoveToWell prior to AspirateInPlace.", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", "default": 0.0, "type": "number" } @@ -2442,7 +2442,7 @@ }, "PickUpTipWellLocation": { "title": "PickUpTipWellLocation", - "description": "A relative location in reference to a well's location.", + "description": "A relative location in reference to a well's location. To be used for picking up tips.", "type": "object", "properties": { "origin": { diff --git a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py index a5121574acb..23560d4de13 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/protocol_schema_v8.py @@ -35,7 +35,14 @@ class Params(BaseModel): wellName: Optional[str] volume: Optional[float] flowRate: Optional[float] - wellLocation: Optional[Union[WellLocation, LiquidHandlingWellLocation, DropTipWellLocation, PickUpTipWellLocation]] + wellLocation: Optional[ + Union[ + WellLocation, + LiquidHandlingWellLocation, + DropTipWellLocation, + PickUpTipWellLocation, + ] + ] waitForResume: Optional[Literal[True]] seconds: Optional[float] minimumZHeight: Optional[float] From 957c573760e259b3b295856ddbbec3b2a11ec11f Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 25 Sep 2024 16:39:18 -0400 Subject: [PATCH 20/51] removed comment --- api/src/opentrons/protocol_engine/types.py | 1 - 1 file changed, 1 deletion(-) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 324cd1245ca..39e4bf881f2 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -239,7 +239,6 @@ class WellOffset(BaseModel): z: float = 0 -# TODO(pm, 2024-09-23): PE should raise error if volumeOffset or WellOrigin.MENISCUS specified with a tip rack location class WellLocation(BaseModel): """A relative location in reference to a well's location.""" From df076166b5841edc47f17de0c5fbca2a991e8066 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 26 Sep 2024 12:23:56 -0400 Subject: [PATCH 21/51] added validate_well_position() --- .../protocol_engine/errors/__init__.py | 2 ++ .../protocol_engine/errors/exceptions.py | 13 ++++++++++ .../protocol_engine/state/geometry.py | 25 +++++++++++++++++-- 3 files changed, 38 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index e0f60a5cd45..bc4b866ec61 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -72,6 +72,7 @@ InvalidLiquidHeightFound, LiquidHeightUnknownError, InvalidWellDefinitionError, + OperationLocationNotInWellError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -152,4 +153,5 @@ "InvalidLiquidHeightFound", "LiquidHeightUnknownError", "InvalidWellDefinitionError", + "OperationLocationNotInWellError", ] diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index 57d420124a7..dad964a92c3 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1082,3 +1082,16 @@ def __init__( ) -> None: """Build an InvalidWellDefinitionError.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class OperationLocationNotInWellError(ProtocolEngineError): + """Raised when a calculated operation location is not within a well.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an OperationLocationNotInWellError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index f3c9addbd1c..005f0764c66 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -19,6 +19,7 @@ LabwareNotLoadedOnModuleError, LabwareMovementNotAllowedError, InvalidWellDefinitionError, + OperationLocationNotInWellError, ) from ..resources import fixture_validation from ..types import ( @@ -421,6 +422,23 @@ def get_labware_position(self, labware_id: str) -> Point: WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation ] + def validate_well_position( + self, + well_location: WellLocations, + well_depth: float, + z_offset: float, + ) -> None: + """Raise exception if operation location is not within well. Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.""" + if z_offset < 0: + if isinstance(well_location, PickUpTipWellLocation): + raise OperationLocationNotInWellError( + f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well" + ) + else: + raise OperationLocationNotInWellError( + f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location below the bottom of the well" + ) + def get_well_position( self, labware_id: str, @@ -444,6 +462,9 @@ def get_well_position( operation_volume=operation_volume, ) offset = offset.copy(update={"z": offset.z + offset_adjustment}) + self.validate_well_position( + well_location=well_location, well_depth=well_depth, z_offset=offset.z + ) return Point( x=labware_pos.x + offset.x + well_def.x, @@ -1323,7 +1344,7 @@ def get_well_height_after_volume( ) -> float: """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" well_def = self._labware.get_well_definition(labware_id, well_name) - well_id = well_def.geometryDefinitionId # make non-optional eventually + well_id = well_def.geometryDefinitionId if well_id is not None: initial_volume = self.get_well_volume_at_height( labware_id=labware_id, well_id=well_id, target_height=initial_height @@ -1334,5 +1355,5 @@ def get_well_height_after_volume( # return self.get_well_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=final_volume) return initial_height # delete, use line above once sub-methods implemented else: - # raise exception?! + # raise InvalidWellDefinitionError(message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}") # use this return initial_height From da7f47977e3a0e4d3f9701f3a7e8d0a27cfbb9c7 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 26 Sep 2024 12:58:49 -0400 Subject: [PATCH 22/51] added TODO --- api/src/opentrons/protocol_engine/state/geometry.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index e8fbce91467..5a3472c4af4 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1328,9 +1328,12 @@ def get_well_height_after_volume( labware_id=labware_id, well_id=well_id, target_height=initial_height ) final_volume = initial_volume + volume - return self.get_well_height_at_volume(labware_id=labware_id, well_id=well_id, target_volume=final_volume) + return self.get_well_height_at_volume( + labware_id=labware_id, well_id=well_id, target_volume=final_volume + ) else: - # raise InvalidWellDefinitionError(message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}") # use this + # TODO (pbm, 09-26-2024): implement below line once more well definitions are implemented + # raise InvalidWellDefinitionError(message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}") return initial_height def get_well_volume_at_height( From a9ca313c1709875f04c31873065ef1645b54b550 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 26 Sep 2024 21:52:07 -0400 Subject: [PATCH 23/51] removed TODO and fixed robot-server tavern tests --- .../protocol_engine/state/geometry.py | 21 +++++++++++-------- .../protocols/test_v6_json_upload.tavern.yaml | 6 ++++++ .../test_v8_json_upload_flex.tavern.yaml | 6 ++++++ .../test_v8_json_upload_ot2.tavern.yaml | 6 ++++++ .../test_json_v6_protocol_run.tavern.yaml | 4 ++++ .../runs/test_json_v6_run_failure.tavern.yaml | 1 + .../test_json_v7_protocol_run.tavern.yaml | 4 ++++ .../runs/test_papi_v2_run_failure.tavern.yaml | 1 + 8 files changed, 40 insertions(+), 9 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 5a3472c4af4..4931613cf45 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1285,12 +1285,15 @@ def get_well_offset_adjustment( elif well_location.volumeOffset == "operationVolume": volume = operation_volume or 0.0 - return self.get_well_height_after_volume( - labware_id=labware_id, - well_name=well_name, - initial_height=initial_handling_height, - volume=volume, - ) + if volume: + return self.get_well_height_after_volume( + labware_id=labware_id, + well_name=well_name, + initial_height=initial_handling_height, + volume=volume, + ) + else: + return initial_handling_height def get_well_handling_height( self, @@ -1332,9 +1335,9 @@ def get_well_height_after_volume( labware_id=labware_id, well_id=well_id, target_volume=final_volume ) else: - # TODO (pbm, 09-26-2024): implement below line once more well definitions are implemented - # raise InvalidWellDefinitionError(message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}") - return initial_height + raise InvalidWellDefinitionError( + message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}" + ) def get_well_volume_at_height( self, labware_id: str, well_id: str, target_height: float diff --git a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml index 1b0c603b38a..717280a6703 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v6_json_upload.tavern.yaml @@ -310,6 +310,7 @@ stages: x: 0 y: 0 z: 2 + volumeOffset: 0 volume: 5 flowRate: 3 result: @@ -344,6 +345,7 @@ stages: x: 0 y: 0 z: 1 + volumeOffset: 0 volume: 4.5 flowRate: 2.5 result: @@ -367,6 +369,7 @@ stages: x: 0 y: 0 z: 11 + volumeOffset: 0 radius: 0.5 speed: 42.0 result: @@ -389,6 +392,7 @@ stages: x: 0 y: 0 z: 12 + volumeOffset: 0 flowRate: 2 result: position: { 'x': 284.635, 'y': 56.025, 'z': 169.25 } @@ -410,6 +414,7 @@ stages: x: 0 y: 0 z: 0 + volumeOffset: 0 forceDirect: false result: position: @@ -432,6 +437,7 @@ stages: x: 2 y: 3 z: 10 + volumeOffset: 0 minimumZHeight: 35 forceDirect: true speed: 12.3 diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml index e53e3014c7d..35801f8719a 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_flex.tavern.yaml @@ -329,6 +329,7 @@ stages: x: 0.0 y: 0.0 z: 2.0 + volumeOffset: 0 volume: 5.0 flowRate: 3.0 result: @@ -363,6 +364,7 @@ stages: x: 0.0 y: 0.0 z: 1.0 + volumeOffset: 0 volume: 4.5 flowRate: 2.5 result: @@ -386,6 +388,7 @@ stages: x: 0.0 y: 0.0 z: 11.0 + volumeOffset: 0 radius: 1.0 speed: 42.0 result: @@ -408,6 +411,7 @@ stages: x: 0.0 y: 0.0 z: 12.0 + volumeOffset: 0 flowRate: 2.0 result: position: { 'x': 342.38, 'y': 65.24, 'z': 51.05 } @@ -446,6 +450,7 @@ stages: x: 0 y: 0 z: 0 + volumeOffset: 0 forceDirect: false speed: 12.3 result: @@ -468,6 +473,7 @@ stages: x: 2.0 y: 3.0 z: 10.0 + volumeOffset: 0 minimumZHeight: 35.0 forceDirect: true result: diff --git a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml index 55d4378a2b2..f85e307e961 100644 --- a/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml +++ b/robot-server/tests/integration/http_api/protocols/test_v8_json_upload_ot2.tavern.yaml @@ -329,6 +329,7 @@ stages: x: 0.0 y: 0.0 z: 2.0 + volumeOffset: 0 volume: 5.0 flowRate: 3.0 result: @@ -368,6 +369,7 @@ stages: x: 0.0 y: 0.0 z: 1.0 + volumeOffset: 0 volume: 4.5 flowRate: 2.5 result: @@ -391,6 +393,7 @@ stages: x: 0.0 y: 0.0 z: 11.0 + volumeOffset: 0 radius: 1.0 result: position: { 'x': 280.805, 'y': 65.115, 'z': 94.3 } @@ -412,6 +415,7 @@ stages: x: 0.0 y: 0.0 z: 12.0 + volumeOffset: 0 flowRate: 2.0 result: position: { 'x': 280.805, 'y': 65.115, 'z': 95.3 } @@ -450,6 +454,7 @@ stages: x: 0 y: 0 z: 0 + volumeOffset: 0 forceDirect: false result: position: { 'x': 289.805, 'y': 65.115, 'z': 98.25 } @@ -471,6 +476,7 @@ stages: x: 2.0 y: 3.0 z: 10.0 + volumeOffset: 0 minimumZHeight: 35.0 forceDirect: true result: diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml index 28d39bcfa77..48e1088eb4c 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_protocol_run.tavern.yaml @@ -322,6 +322,7 @@ stages: x: 0 'y': 0 z: 2 + volumeOffset: 0 volume: 5 flowRate: 3 - id: !anystr @@ -342,6 +343,7 @@ stages: x: 0 'y': 0 z: 1 + volumeOffset: 0 volume: 4.5 flowRate: 2.5 - id: !anystr @@ -362,6 +364,7 @@ stages: x: 0 'y': 0 z: 0 + volumeOffset: 0 forceDirect: false - id: !anystr key: !anystr @@ -381,6 +384,7 @@ stages: x: 2 y: 3 z: 10 + volumeOffset: 0 minimumZHeight: 35 forceDirect: true speed: 12.3 diff --git a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml index 681e25042d0..e89681be0ac 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v6_run_failure.tavern.yaml @@ -121,5 +121,6 @@ stages: x: 0 y: 0 z: 1 + volumeOffset: 0 flowRate: 3.78 volume: 100 diff --git a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml index 70737a7f6c3..0915fb69f12 100644 --- a/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_json_v7_protocol_run.tavern.yaml @@ -322,6 +322,7 @@ stages: x: 0 'y': 0 z: 2 + volumeOffset: 0 volume: 5 flowRate: 3 - id: !anystr @@ -342,6 +343,7 @@ stages: x: 0 'y': 0 z: 1 + volumeOffset: 0 volume: 4.5 flowRate: 2.5 - id: !anystr @@ -362,6 +364,7 @@ stages: x: 0 'y': 0 z: 0 + volumeOffset: 0 forceDirect: false - id: !anystr key: !anystr @@ -381,6 +384,7 @@ stages: x: 2 y: 3 z: 10 + volumeOffset: 0 minimumZHeight: 35 forceDirect: true speed: 12.3 diff --git a/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml b/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml index 468b5b46209..804b0b0e620 100644 --- a/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml +++ b/robot-server/tests/integration/http_api/runs/test_papi_v2_run_failure.tavern.yaml @@ -122,5 +122,6 @@ stages: x: 0 y: 0 z: 0 + volumeOffset: 0 flowRate: 150 volume: 100 From 22d2c397fd81e12d96a3777f39786987a2ea90a4 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 11:23:44 -0400 Subject: [PATCH 24/51] added test, need to fix. added well_location guardrails --- .../protocol_engine/commands/aspirate.py | 8 +++- .../protocol_engine/commands/dispense.py | 10 +++- .../state/test_geometry_view.py | 46 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 8336de45d2c..c5ff9ffddf6 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -112,11 +112,17 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_name=well_name, ) + well_location = params.wellLocation + if well_location.origin == WellOrigin.MENISCUS: + well_location.volumeOffset = "operationVolume" + if well_location.offset.z == 0.0: + well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + position = await self._movement.move_to_well( pipette_id=pipette_id, labware_id=labware_id, well_name=well_name, - well_location=params.wellLocation, + well_location=well_location, current_well=current_well, operation_volume=-params.volume, ) diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 4179f8524c0..d0af063dcb8 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -7,7 +7,7 @@ from pydantic import Field -from ..types import DeckPoint +from ..types import DeckPoint, WellOrigin from ..state.update_types import StateUpdate from .pipetting_common import ( PipetteIdMixin, @@ -76,11 +76,17 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: """Move to and dispense to the requested well.""" state_update = StateUpdate() + well_location = params.wellLocation + if well_location.origin == WellOrigin.MENISCUS: + well_location.volumeOffset = 0.0 + if well_location.offset.z == 0.0: + well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + position = await self._movement.move_to_well( pipette_id=params.pipetteId, labware_id=params.labwareId, well_name=params.wellName, - well_location=params.wellLocation, + well_location=well_location, ) deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) state_update.set_pipette_location( diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index f6206a4682e..5bad07dc1e4 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1273,6 +1273,52 @@ def test_get_well_position( ) +def test_get_well_position_meniscus( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should be able to get the position of a well top in a labware.""" + labware_data = LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="definition-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + offsetId="offset-id", + ) + calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) + slot_pos = Point(4, 5, 6) + well_def = well_plate_def.wells["B2"] + + decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( + calibration_offset + ) + decoy.when( + mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + + well_location = WellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-2) + ) + + result = subject.get_well_position("labware-id", "B2", well_location) + + assert result == Point( + x=slot_pos[0] + 1 + well_def.x, + y=slot_pos[1] - 2 + well_def.y, + z=slot_pos[2] + 3 + well_def.z + well_def.depth, + ) + + def test_get_well_height( decoy: Decoy, well_plate_def: LabwareDefinition, From 17ce9b1c60040f58682f10d4e028d4ccfe4ffac5 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 13:51:00 -0400 Subject: [PATCH 25/51] refactored geometry methods and added helper function in labware --- .../protocol_engine/state/geometry.py | 43 +++++-------------- .../protocol_engine/state/labware.py | 28 +++++++++++- 2 files changed, 38 insertions(+), 33 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 4931613cf45..361450a048a 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -9,6 +9,7 @@ from opentrons.types import Point, DeckSlotName, StagingSlotName, MountType from opentrons_shared_data.labware.constants import WELL_NAME_PATTERN +from opentrons_shared_data.labware.labware_definition import InnerWellGeometry from opentrons_shared_data.deck.types import CutoutFixture from opentrons_shared_data.pipette import PIPETTE_X_SPAN from opentrons_shared_data.pipette.types import ChannelCount @@ -1324,49 +1325,27 @@ def get_well_height_after_volume( self, labware_id: str, well_name: str, initial_height: float, volume: float ) -> float: """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" - well_def = self._labware.get_well_definition(labware_id, well_name) - well_id = well_def.geometryDefinitionId - if well_id is not None: - initial_volume = self.get_well_volume_at_height( - labware_id=labware_id, well_id=well_id, target_height=initial_height - ) - final_volume = initial_volume + volume - return self.get_well_height_at_volume( - labware_id=labware_id, well_id=well_id, target_volume=final_volume - ) - else: - raise InvalidWellDefinitionError( - message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}" - ) + well_geometry = self._labware.get_well_geometry(labware_id, well_name) + initial_volume = self.get_well_volume_at_height( + well_geometry=well_geometry, target_height=initial_height + ) + final_volume = initial_volume + volume + return self.get_well_height_at_volume( + well_geometry=well_geometry, target_volume=final_volume + ) def get_well_volume_at_height( - self, labware_id: str, well_id: str, target_height: float + self, well_geometry: InnerWellGeometry, target_height: float ) -> float: """Return the volume of liquid in a labware well at a given liquid height (with reference to the well bottom).""" - labware_def = self._labware.get_definition(labware_id) - if labware_def.innerLabwareGeometry is None: - raise InvalidWellDefinitionError(message="No InnerLabwareGeometry found.") - well_geometry = labware_def.innerLabwareGeometry.get(well_id) - if well_geometry is None: - raise InvalidWellDefinitionError( - message=f"No InnerWellGeometry found for well id: {well_id}" - ) return find_volume_at_well_height( target_height=target_height, well_geometry=well_geometry ) def get_well_height_at_volume( - self, labware_id: str, well_id: str, target_volume: float + self, well_geometry: InnerWellGeometry, target_volume: float ) -> float: """Return the height of liquid in a labware well at a given liquid volume.""" - labware_def = self._labware.get_definition(labware_id) - if labware_def.innerLabwareGeometry is None: - raise InvalidWellDefinitionError(message="No InnerLabwareGeometry found.") - well_geometry = labware_def.innerLabwareGeometry.get(well_id) - if well_geometry is None: - raise InvalidWellDefinitionError( - message=f"No InnerWellGeometry found for well id: {well_id}" - ) return find_height_at_well_volume( target_volume=target_volume, well_geometry=well_geometry ) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 0db6b310e1e..e2d7d8c7fc2 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -18,7 +18,10 @@ from opentrons.protocol_engine.state import update_types from opentrons_shared_data.deck.types import DeckDefinitionV5 from opentrons_shared_data.gripper.constants import LABWARE_GRIP_FORCE -from opentrons_shared_data.labware.labware_definition import LabwareRole +from opentrons_shared_data.labware.labware_definition import ( + LabwareRole, + InnerWellGeometry, +) from opentrons_shared_data.pipette.types import LabwareUri from opentrons.types import DeckSlotName, StagingSlotName, MountType @@ -462,6 +465,29 @@ def get_well_definition( f"{well_name} does not exist in {labware_id}." ) from e + def get_well_geometry( + self, labware_id: str, well_name: Optional[str] = None + ) -> InnerWellGeometry: + """Get a well's inner geometry by labware and well name.""" + labware_def = self.get_definition(labware_id) + if labware_def.innerLabwareGeometry is None: + raise errors.InvalidWellDefinitionError( + message=f"No innerLabwareGeometry found for labware_id: {labware_id}." + ) + well_def = self.get_well_definition(labware_id, well_name) + well_id = well_def.geometryDefinitionId + if well_id is None: + raise errors.InvalidWellDefinitionError( + message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}" + ) + else: + well_geometry = labware_def.innerLabwareGeometry.get(well_id) + if well_geometry is None: + raise errors.InvalidWellDefinitionError( + message=f"No innerLabwareGeometry found for well_id: {well_id} in labware_id: {labware_id}" + ) + return well_geometry + def get_well_size( self, labware_id: str, well_name: str ) -> Tuple[float, float, float]: From 9d559a6f511f73d8f13176173d822a2db64939ea Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 15:53:52 -0400 Subject: [PATCH 26/51] refactored geometry and added validate_dispense_volume_into_well --- .../protocol_engine/commands/aspirate.py | 2 +- .../protocol_engine/commands/dispense.py | 26 +++++-- .../protocol_engine/errors/__init__.py | 2 + .../protocol_engine/state/geometry.py | 71 +++++++++++++------ .../protocol_engine/commands/test_dispense.py | 7 +- 5 files changed, 78 insertions(+), 30 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index c5ff9ffddf6..0bb827ba099 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -116,7 +116,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: if well_location.origin == WellOrigin.MENISCUS: well_location.volumeOffset = "operationVolume" if well_location.offset.z == 0.0: - well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? position = await self._movement.move_to_well( pipette_id=pipette_id, diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index d0af063dcb8..aa3a59befae 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -30,6 +30,7 @@ if TYPE_CHECKING: from ..execution import MovementHandler, PipettingHandler from ..resources import ModelUtils + from ..state.state import StateView DispenseCommandType = Literal["dispense"] @@ -63,11 +64,13 @@ class DispenseImplementation(AbstractCommandImpl[DispenseParams, _ExecuteReturn] def __init__( self, + state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, model_utils: ModelUtils, **kwargs: object, ) -> None: + self._state_view = state_view self._movement = movement self._pipetting = pipetting self._model_utils = model_utils @@ -75,31 +78,40 @@ def __init__( async def execute(self, params: DispenseParams) -> _ExecuteReturn: """Move to and dispense to the requested well.""" state_update = StateUpdate() - well_location = params.wellLocation + labware_id = params.labwareId + well_name = params.wellName + volume = params.volume + if well_location.origin == WellOrigin.MENISCUS: well_location.volumeOffset = 0.0 if well_location.offset.z == 0.0: - well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + self._state_view.geometry.validate_dispense_volume_into_well( + labware_id=labware_id, + well_name=well_name, + well_location=well_location, + volume=volume, + ) position = await self._movement.move_to_well( pipette_id=params.pipetteId, - labware_id=params.labwareId, - well_name=params.wellName, + labware_id=labware_id, + well_name=well_name, well_location=well_location, ) deck_point = DeckPoint.construct(x=position.x, y=position.y, z=position.z) state_update.set_pipette_location( pipette_id=params.pipetteId, - new_labware_id=params.labwareId, - new_well_name=params.wellName, + new_labware_id=labware_id, + new_well_name=well_name, new_deck_point=deck_point, ) try: volume = await self._pipetting.dispense_in_place( pipette_id=params.pipetteId, - volume=params.volume, + volume=volume, flow_rate=params.flowRate, push_out=params.pushOut, ) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index bc4b866ec61..ee4447b38bf 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -73,6 +73,7 @@ LiquidHeightUnknownError, InvalidWellDefinitionError, OperationLocationNotInWellError, + InvalidDispenseVolumeError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -154,4 +155,5 @@ "LiquidHeightUnknownError", "InvalidWellDefinitionError", "OperationLocationNotInWellError", + "InvalidDispenseVolumeError", ] diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 361450a048a..dfb2f3cdbd3 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -19,7 +19,6 @@ LabwareNotLoadedOnLabwareError, LabwareNotLoadedOnModuleError, LabwareMovementNotAllowedError, - InvalidWellDefinitionError, OperationLocationNotInWellError, ) from ..resources import fixture_validation @@ -1287,15 +1286,31 @@ def get_well_offset_adjustment( volume = operation_volume or 0.0 if volume: + well_geometry = self._labware.get_well_geometry(labware_id, well_name) return self.get_well_height_after_volume( - labware_id=labware_id, - well_name=well_name, + well_geometry=well_geometry, initial_height=initial_handling_height, volume=volume, ) else: return initial_handling_height + def get_meniscus_height( + self, + labware_id: str, + well_name: str, + ) -> float: + """Returns stored meniscus height in specified well.""" + meniscus_height = self._wells.get_last_measured_liquid_height( + labware_id=labware_id, well_name=well_name + ) + if meniscus_height is None: + raise errors.LiquidHeightUnknownError( + "Must liquid probe before specifying WellOrigin.MENISCUS." + ) + else: + return meniscus_height + def get_well_handling_height( self, labware_id: str, @@ -1310,22 +1325,15 @@ def get_well_handling_height( elif well_location.origin == WellOrigin.CENTER: handling_height = well_depth / 2.0 elif well_location.origin == WellOrigin.MENISCUS: - liquid_height = self._wells.get_last_measured_liquid_height( + handling_height = self.get_meniscus_height( labware_id=labware_id, well_name=well_name ) - if liquid_height is None: - raise errors.LiquidHeightUnknownError( - "Must liquid probe before specifying WellOrigin.MENISCUS." - ) - else: - handling_height = liquid_height return float(handling_height) def get_well_height_after_volume( - self, labware_id: str, well_name: str, initial_height: float, volume: float + self, well_geometry: InnerWellGeometry, initial_height: float, volume: float ) -> float: """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" - well_geometry = self._labware.get_well_geometry(labware_id, well_name) initial_volume = self.get_well_volume_at_height( well_geometry=well_geometry, target_height=initial_height ) @@ -1351,15 +1359,36 @@ def get_well_height_at_volume( ) def get_well_volumetric_capacity( - self, labware_id: str, well_id: str + self, well_geometry: InnerWellGeometry ) -> List[Tuple[float, float]]: """Return a map of heights to partial volumes.""" - labware_def = self._labware.get_definition(labware_id) - if labware_def.innerLabwareGeometry is None: - raise InvalidWellDefinitionError(message="No InnerLabwareGeometry found.") - well_geometry = labware_def.innerLabwareGeometry.get(well_id) - if well_geometry is None: - raise InvalidWellDefinitionError( - message=f"No InnerWellGeometry found for well id: {well_id}" - ) return get_well_volumetric_capacity(well_geometry) + + def validate_dispense_volume_into_well( + self, + labware_id: str, + well_name: str, + well_location: WellLocations, + volume: float, + ) -> None: + """Raise InvalidDispenseVolumeError if planned dispense volume will overflow well.""" + well_def = self._labware.get_well_definition(labware_id, well_name) + well_volumetric_capacity = well_def.totalLiquidVolume + if well_location.origin == WellOrigin.MENISCUS: + well_geometry = self._labware.get_well_geometry(labware_id, well_name) + meniscus_height = self.get_meniscus_height( + labware_id=labware_id, well_name=well_name + ) + meniscus_volume = self.get_well_volume_at_height( + well_geometry=well_geometry, target_height=meniscus_height + ) + remaining_volume = well_volumetric_capacity - meniscus_volume + if volume > remaining_volume: + raise errors.InvalidDispenseVolumeError( + f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})" + ) + else: + if volume > well_volumetric_capacity: + raise errors.InvalidDispenseVolumeError( + f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})" + ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 497e1573796..167223e6d9d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -14,6 +14,7 @@ ) from opentrons.protocol_engine.execution import MovementHandler, PipettingHandler from opentrons.protocol_engine.state import update_types +from opentrons.protocol_engine.state.state import StateView from opentrons.types import Point from opentrons.protocol_engine.commands.command import SuccessData, DefinedErrorData @@ -28,13 +29,17 @@ @pytest.fixture def subject( + state_view: StateView, movement: MovementHandler, pipetting: PipettingHandler, model_utils: ModelUtils, ) -> DispenseImplementation: """Get the implementation subject.""" return DispenseImplementation( - movement=movement, pipetting=pipetting, model_utils=model_utils + state_view=state_view, + movement=movement, + pipetting=pipetting, + model_utils=model_utils, ) From 54205b3a77ce28142221b69f3bf19ed92edfdcb4 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 17:09:49 -0400 Subject: [PATCH 27/51] updated long doc strings --- api/src/opentrons/protocol_engine/state/geometry.py | 12 +++++++++--- api/src/opentrons/protocol_engine/types.py | 8 ++++++-- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index dfb2f3cdbd3..0f39ac339ff 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -432,7 +432,9 @@ def validate_well_position( well_depth: float, z_offset: float, ) -> None: - """Raise exception if operation location is not within well. Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.""" + """Raise exception if operation location is not within well. + + Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.""" if z_offset < 0: if isinstance(well_location, PickUpTipWellLocation): raise OperationLocationNotInWellError( @@ -1271,7 +1273,9 @@ def get_well_offset_adjustment( well_depth: float, operation_volume: Optional[float] = None, ) -> float: - """Return a z-axis distance that accounts for well handling height and operation volume (with reference to the well bottom).""" + """Return a z-axis distance that accounts for well handling height and operation volume. + + Distance is with reference to the well bottom.""" initial_handling_height = self.get_well_handling_height( labware_id=labware_id, well_name=well_name, @@ -1333,7 +1337,9 @@ def get_well_handling_height( def get_well_height_after_volume( self, well_geometry: InnerWellGeometry, initial_height: float, volume: float ) -> float: - """Return the height of liquid in a labware well after a given volume has been handled, given an initial handling height (with reference to the well bottom).""" + """Return the height of liquid in a labware well after a given volume has been handled. + + This is given an initial handling height, with reference to the well bottom.""" initial_volume = self.get_well_volume_at_height( well_geometry=well_geometry, target_height=initial_height ) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 60c5bc61aee..e771c4863f8 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -251,7 +251,9 @@ class WellLocation(BaseModel): class LiquidHandlingWellLocation(BaseModel): - """A relative location in reference to a well's location. To be used with commands that handle liquids.""" + """A relative location in reference to a well's location. + + To be used with commands that handle liquids.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) @@ -262,7 +264,9 @@ class LiquidHandlingWellLocation(BaseModel): class PickUpTipWellLocation(BaseModel): - """A relative location in reference to a well's location. To be used for picking up tips.""" + """A relative location in reference to a well's location. + + To be used for picking up tips.""" origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) From d2af54ed93863adc2825fa31f32241666809bf0a Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 17:51:22 -0400 Subject: [PATCH 28/51] cleaned up comments and doc strings --- .../protocol_engine/commands/aspirate.py | 7 +++++-- .../protocol_engine/commands/dispense.py | 2 -- .../opentrons/protocol_engine/errors/__init__.py | 2 ++ .../protocol_engine/errors/exceptions.py | 16 ++++++++++++++++ .../opentrons/protocol_engine/state/geometry.py | 13 ++++++++----- api/src/opentrons/protocol_engine/types.py | 10 ++++++---- 6 files changed, 37 insertions(+), 13 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 0bb827ba099..501b4b79586 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -21,6 +21,7 @@ SuccessData, ) from ..errors.error_occurrence import ErrorOccurrence +from ..errors import InvalidAspirateLocationError from opentrons.hardware_control import HardwareControlAPI @@ -115,8 +116,10 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_location = params.wellLocation if well_location.origin == WellOrigin.MENISCUS: well_location.volumeOffset = "operationVolume" - if well_location.offset.z == 0.0: - well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? + if well_location.offset.z > 0.0: + raise InvalidAspirateLocationError( + f"Cannot specify a meniscus-relative Aspirate with a z-offset of {well_location.offset.z}" + ) position = await self._movement.move_to_well( pipette_id=pipette_id, diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index aa3a59befae..ecb48f4a318 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -85,8 +85,6 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: if well_location.origin == WellOrigin.MENISCUS: well_location.volumeOffset = 0.0 - if well_location.offset.z == 0.0: - well_location.offset.z = -2.0 # disallow offset.z > -1.0 ? self._state_view.geometry.validate_dispense_volume_into_well( labware_id=labware_id, well_name=well_name, diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index ee4447b38bf..161e45f82dd 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -74,6 +74,7 @@ InvalidWellDefinitionError, OperationLocationNotInWellError, InvalidDispenseVolumeError, + InvalidAspirateLocationError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -156,4 +157,5 @@ "InvalidWellDefinitionError", "OperationLocationNotInWellError", "InvalidDispenseVolumeError", + "InvalidAspirateLocationError", ] diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index dad964a92c3..aefd21ba6fa 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1095,3 +1095,19 @@ def __init__( ) -> None: """Build an OperationLocationNotInWellError.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class InvalidAspirateLocationError(ProtocolEngineError): + """Raised when a meniscus-relative Aspirate's z-offset is greater than 0.0. + + This would result in aspiration above the meniscus (in air), which should not be allowed. + """ + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an InvalidAspirateLocationError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 0f39ac339ff..f70ed0a90aa 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -434,7 +434,8 @@ def validate_well_position( ) -> None: """Raise exception if operation location is not within well. - Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration.""" + Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration. + """ if z_offset < 0: if isinstance(well_location, PickUpTipWellLocation): raise OperationLocationNotInWellError( @@ -1274,8 +1275,9 @@ def get_well_offset_adjustment( operation_volume: Optional[float] = None, ) -> float: """Return a z-axis distance that accounts for well handling height and operation volume. - - Distance is with reference to the well bottom.""" + + Distance is with reference to the well bottom. + """ initial_handling_height = self.get_well_handling_height( labware_id=labware_id, well_name=well_name, @@ -1338,8 +1340,9 @@ def get_well_height_after_volume( self, well_geometry: InnerWellGeometry, initial_height: float, volume: float ) -> float: """Return the height of liquid in a labware well after a given volume has been handled. - - This is given an initial handling height, with reference to the well bottom.""" + + This is given an initial handling height, with reference to the well bottom. + """ initial_volume = self.get_well_volume_at_height( well_geometry=well_geometry, target_height=initial_height ) diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index e771c4863f8..1994a67a190 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -252,8 +252,9 @@ class WellLocation(BaseModel): class LiquidHandlingWellLocation(BaseModel): """A relative location in reference to a well's location. - - To be used with commands that handle liquids.""" + + To be used with commands that handle liquids. + """ origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) @@ -265,8 +266,9 @@ class LiquidHandlingWellLocation(BaseModel): class PickUpTipWellLocation(BaseModel): """A relative location in reference to a well's location. - - To be used for picking up tips.""" + + To be used for picking up tips. + """ origin: WellOrigin = WellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) From ad334cc6f50f1cb6919b52abe30c29dd1452f798 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 30 Sep 2024 18:29:08 -0400 Subject: [PATCH 29/51] updated command schema 9 --- shared-data/command/schemas/9.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index cde23c24cab..7bcbe00ce3a 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -320,7 +320,7 @@ }, "LiquidHandlingWellLocation": { "title": "LiquidHandlingWellLocation", - "description": "A relative location in reference to a well's location. To be used with commands that handle liquids.", + "description": "A relative location in reference to a well's location.\n\nTo be used with commands that handle liquids.", "type": "object", "properties": { "origin": { @@ -2442,7 +2442,7 @@ }, "PickUpTipWellLocation": { "title": "PickUpTipWellLocation", - "description": "A relative location in reference to a well's location. To be used for picking up tips.", + "description": "A relative location in reference to a well's location.\n\nTo be used for picking up tips.", "type": "object", "properties": { "origin": { From a671aad65d9cd4bf9aaf3adcb3b0b186c105ba31 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 2 Oct 2024 13:48:13 -0400 Subject: [PATCH 30/51] refactored geometry and test --- .../protocol_engine/state/geometry.py | 33 +----- .../state/test_geometry_view.py | 109 ++++++++++-------- 2 files changed, 68 insertions(+), 74 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index f70ed0a90aa..a16989792b7 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -57,7 +57,6 @@ from .pipettes import PipetteView from .addressable_areas import AddressableAreaView from .frustum_helpers import ( - get_well_volumetric_capacity, find_volume_at_well_height, find_height_at_well_volume, ) @@ -1343,36 +1342,14 @@ def get_well_height_after_volume( This is given an initial handling height, with reference to the well bottom. """ - initial_volume = self.get_well_volume_at_height( - well_geometry=well_geometry, target_height=initial_height + initial_volume = find_volume_at_well_height( + target_height=initial_height, well_geometry=well_geometry ) final_volume = initial_volume + volume - return self.get_well_height_at_volume( - well_geometry=well_geometry, target_volume=final_volume - ) - - def get_well_volume_at_height( - self, well_geometry: InnerWellGeometry, target_height: float - ) -> float: - """Return the volume of liquid in a labware well at a given liquid height (with reference to the well bottom).""" - return find_volume_at_well_height( - target_height=target_height, well_geometry=well_geometry - ) - - def get_well_height_at_volume( - self, well_geometry: InnerWellGeometry, target_volume: float - ) -> float: - """Return the height of liquid in a labware well at a given liquid volume.""" return find_height_at_well_volume( - target_volume=target_volume, well_geometry=well_geometry + target_volume=final_volume, well_geometry=well_geometry ) - def get_well_volumetric_capacity( - self, well_geometry: InnerWellGeometry - ) -> List[Tuple[float, float]]: - """Return a map of heights to partial volumes.""" - return get_well_volumetric_capacity(well_geometry) - def validate_dispense_volume_into_well( self, labware_id: str, @@ -1388,8 +1365,8 @@ def validate_dispense_volume_into_well( meniscus_height = self.get_meniscus_height( labware_id=labware_id, well_name=well_name ) - meniscus_volume = self.get_well_volume_at_height( - well_geometry=well_geometry, target_height=meniscus_height + meniscus_volume = find_volume_at_well_height( + target_height=meniscus_height, well_geometry=well_geometry ) remaining_volume = well_volumetric_capacity - meniscus_volume if volume > remaining_volume: diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 5bad07dc1e4..57815b874bf 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -40,6 +40,7 @@ LoadedModule, ModuleModel, WellLocation, + LiquidHandlingWellLocation, WellOrigin, DropTipWellLocation, DropTipWellOrigin, @@ -91,6 +92,7 @@ from ..pipette_fixtures import get_default_nozzle_map from ..mock_circular_frusta import TEST_EXAMPLES as CIRCULAR_TEST_EXAMPLES from ..mock_rectangular_frusta import TEST_EXAMPLES as RECTANGULAR_TEST_EXAMPLES +from ...protocol_runner.test_json_translator import _load_labware_definition_data @pytest.fixture @@ -1273,52 +1275,6 @@ def test_get_well_position( ) -def test_get_well_position_meniscus( - decoy: Decoy, - well_plate_def: LabwareDefinition, - mock_labware_view: LabwareView, - mock_addressable_area_view: AddressableAreaView, - subject: GeometryView, -) -> None: - """It should be able to get the position of a well top in a labware.""" - labware_data = LoadedLabware( - id="labware-id", - loadName="load-name", - definitionUri="definition-uri", - location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), - offsetId="offset-id", - ) - calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) - slot_pos = Point(4, 5, 6) - well_def = well_plate_def.wells["B2"] - - decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) - decoy.when(mock_labware_view.get_definition("labware-id")).then_return( - well_plate_def - ) - decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( - calibration_offset - ) - decoy.when( - mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) - ).then_return(slot_pos) - decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( - well_def - ) - - well_location = WellLocation( - origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-2) - ) - - result = subject.get_well_position("labware-id", "B2", well_location) - - assert result == Point( - x=slot_pos[0] + 1 + well_def.x, - y=slot_pos[1] - 2 + well_def.y, - z=slot_pos[2] + 3 + well_def.z + well_def.depth, - ) - - def test_get_well_height( decoy: Decoy, well_plate_def: LabwareDefinition, @@ -1603,6 +1559,67 @@ def test_get_well_position_with_meniscus_offset( ) +def test_get_well_position_with_meniscus_and_volume_offset( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_well_view: WellView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should be able to get the position of a well center in a labware.""" + labware_data = LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="definition-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + offsetId="offset-id", + ) + calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) + slot_pos = Point(4, 5, 6) + well_def = well_plate_def.wells["B2"] + + decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( + calibration_offset + ) + decoy.when( + mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + decoy.when( + mock_well_view.get_last_measured_liquid_height("labware-id", "B2") + ).then_return(45.0) + labware_def = _load_labware_definition_data() + assert labware_def.innerLabwareGeometry is not None + inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] + decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( + inner_well_def + ) + + result = subject.get_well_position( + labware_id="labware-id", + well_name="B2", + well_location=LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=2, y=3, z=4), + volumeOffset="operationVolume", + ), + operation_volume=-323.0, + ) + + assert result == Point( + x=slot_pos[0] + 1 + well_def.x + 2, + y=slot_pos[1] - 2 + well_def.y + 3, + z=slot_pos[2] + 3 + well_def.z + 4 + 40.0, + ) + + def test_get_relative_well_location( decoy: Decoy, well_plate_def: LabwareDefinition, From 910d940fd0380e12ea341a7c4db30a26e32bc739 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 9 Oct 2024 15:56:51 -0400 Subject: [PATCH 31/51] updated tests --- .../protocol_engine/state/geometry.py | 1 + .../protocol_engine/commands/test_aspirate.py | 95 +++++++++ .../protocol_engine/commands/test_dispense.py | 58 ++++++ .../state/test_geometry_view.py | 182 +++++++++++++++++- 4 files changed, 332 insertions(+), 4 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index a16989792b7..6dc720de2c2 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -1374,6 +1374,7 @@ def validate_dispense_volume_into_well( f"Attempting to dispense {volume}µL of liquid into a well that can currently only hold {remaining_volume}µL (well {well_name} in labware_id: {labware_id})" ) else: + # TODO(pbm, 10-08-24): factor in well (LabwareStore) state volume if volume > well_volumetric_capacity: raise errors.InvalidDispenseVolumeError( f"Attempting to dispense {volume}µL of liquid into a well that can only hold {well_volumetric_capacity}µL (well {well_name} in labware_id: {labware_id})" diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 8c6f71b18bc..4a735d98074 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -32,6 +32,7 @@ from opentrons.protocol_engine.types import CurrentWell, LoadedPipette from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.notes import CommandNoteAdder +from opentrons.protocol_engine.errors import InvalidAspirateLocationError @pytest.fixture @@ -316,3 +317,97 @@ async def test_overpressure_error( ) ), ) + + +async def test_aspirate_implementation_meniscus( + decoy: Decoy, + state_view: StateView, + hardware_api: HardwareControlAPI, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + mock_command_note_adder: CommandNoteAdder, +) -> None: + """Aspirate should update WellVolumeOffset when called with WellOrigin.MENISCUS.""" + location = LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-1) + ) + updated_location = LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=0, y=0, z=-1), + volumeOffset="operationVolume", + ) + + data = AspirateParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=location, + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + + decoy.when( + await movement.move_to_well( + pipette_id="abc", + labware_id="123", + well_name="A3", + well_location=updated_location, + current_well=None, + operation_volume=-50, + ), + ).then_return(Point(x=1, y=2, z=3)) + + decoy.when( + await pipetting.aspirate_in_place( + pipette_id="abc", + volume=50, + flow_rate=1.23, + command_note_adder=mock_command_note_adder, + ), + ).then_return(50) + + result = await subject.execute(data) + + assert result == SuccessData( + public=AspirateResult(volume=50, position=DeckPoint(x=1, y=2, z=3)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="abc", + new_location=update_types.Well(labware_id="123", well_name="A3"), + new_deck_point=DeckPoint(x=1, y=2, z=3), + ) + ), + ) + + +async def test_aspirate_implementation_invalid_meniscus_location_error( + decoy: Decoy, + state_view: StateView, + hardware_api: HardwareControlAPI, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: AspirateImplementation, + mock_command_note_adder: CommandNoteAdder, +) -> None: + """Aspirate should raise InvalidAspirateLocationError when called with WellOrigin.MENISCUS and a WellOffset greater than 0.0 (ie aspiration from air).""" + location = LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=1) + ) + + data = AspirateParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=location, + volume=50, + flowRate=1.23, + ) + + decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) + + with pytest.raises(InvalidAspirateLocationError): + await subject.execute(data) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 167223e6d9d..89f0ac4048a 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -96,6 +96,64 @@ async def test_dispense_implementation( ) +async def test_dispense_implementation_meniscus( + decoy: Decoy, + movement: MovementHandler, + pipetting: PipettingHandler, + subject: DispenseImplementation, +) -> None: + """Dispense should update WellVolumeOffset when called with WellOrigin.MENISCUS.""" + well_location = LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-1) + ) + updated_location = LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=0, y=0, z=-1), + volumeOffset=0.0, + ) + + data = DispenseParams( + pipetteId="pipette-id-abc123", + labwareId="labware-id-abc123", + wellName="A3", + wellLocation=well_location, + volume=50, + flowRate=1.23, + ) + + decoy.when( + await movement.move_to_well( + pipette_id="pipette-id-abc123", + labware_id="labware-id-abc123", + well_name="A3", + well_location=updated_location, + ) + ).then_return(Point(x=1, y=2, z=3)) + + decoy.when( + await pipetting.dispense_in_place( + pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None + ) + ).then_return(42) + + result = await subject.execute(data) + + assert result == SuccessData( + public=DispenseResult(volume=42, position=DeckPoint(x=1, y=2, z=3)), + private=None, + state_update=update_types.StateUpdate( + pipette_location=update_types.PipetteLocationUpdate( + pipette_id="pipette-id-abc123", + new_location=update_types.Well( + labware_id="labware-id-abc123", + well_name="A3", + ), + new_deck_point=DeckPoint.construct(x=1, y=2, z=3), + ), + ), + ) + + async def test_overpressure_error( decoy: Decoy, movement: MovementHandler, diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 57815b874bf..04ef3082229 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1513,7 +1513,7 @@ def test_get_well_position_with_meniscus_offset( mock_addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: - """It should be able to get the position of a well center in a labware.""" + """It should be able to get the position of a well meniscus in a labware.""" labware_data = LoadedLabware( id="labware-id", loadName="load-name", @@ -1549,7 +1549,6 @@ def test_get_well_position_with_meniscus_offset( origin=WellOrigin.MENISCUS, offset=WellOffset(x=2, y=3, z=4), ), - operation_volume=0.0, ) assert result == Point( @@ -1559,7 +1558,7 @@ def test_get_well_position_with_meniscus_offset( ) -def test_get_well_position_with_meniscus_and_volume_offset( +def test_get_well_position_with_meniscus_and_literal_volume_offset( decoy: Decoy, well_plate_def: LabwareDefinition, mock_labware_view: LabwareView, @@ -1567,7 +1566,7 @@ def test_get_well_position_with_meniscus_and_volume_offset( mock_addressable_area_view: AddressableAreaView, subject: GeometryView, ) -> None: - """It should be able to get the position of a well center in a labware.""" + """It should be able to get the position of a well meniscus in a labware with a volume offset.""" labware_data = LoadedLabware( id="labware-id", loadName="load-name", @@ -1620,6 +1619,122 @@ def test_get_well_position_with_meniscus_and_volume_offset( ) +def test_get_well_position_with_meniscus_and_float_volume_offset( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_well_view: WellView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should be able to get the position of a well meniscus in a labware with a volume offset.""" + labware_data = LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="definition-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + offsetId="offset-id", + ) + calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) + slot_pos = Point(4, 5, 6) + well_def = well_plate_def.wells["B2"] + + decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( + calibration_offset + ) + decoy.when( + mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + decoy.when( + mock_well_view.get_last_measured_liquid_height("labware-id", "B2") + ).then_return(40.0) + labware_def = _load_labware_definition_data() + assert labware_def.innerLabwareGeometry is not None + inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] + decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( + inner_well_def + ) + + result = subject.get_well_position( + labware_id="labware-id", + well_name="B2", + well_location=LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=2, y=3, z=4), + volumeOffset=-323.0, + ), + ) + + assert result == Point( + x=slot_pos[0] + 1 + well_def.x + 2, + y=slot_pos[1] - 2 + well_def.y + 3, + z=slot_pos[2] + 3 + well_def.z + 4 + 35.0, + ) + + +def test_get_well_position_raises_validation_error( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_well_view: WellView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should raise a validation error when a volume offset is too large (ie location is below the well bottom).""" + labware_data = LoadedLabware( + id="labware-id", + loadName="load-name", + definitionUri="definition-uri", + location=DeckSlotLocation(slotName=DeckSlotName.SLOT_4), + offsetId="offset-id", + ) + calibration_offset = LabwareOffsetVector(x=1, y=-2, z=3) + slot_pos = Point(4, 5, 6) + well_def = well_plate_def.wells["B2"] + + decoy.when(mock_labware_view.get("labware-id")).then_return(labware_data) + decoy.when(mock_labware_view.get_definition("labware-id")).then_return( + well_plate_def + ) + decoy.when(mock_labware_view.get_labware_offset_vector("labware-id")).then_return( + calibration_offset + ) + decoy.when( + mock_addressable_area_view.get_addressable_area_position(DeckSlotName.SLOT_4.id) + ).then_return(slot_pos) + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + decoy.when( + mock_well_view.get_last_measured_liquid_height("labware-id", "B2") + ).then_return(45.0) + labware_def = _load_labware_definition_data() + assert labware_def.innerLabwareGeometry is not None + inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] + decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( + inner_well_def + ) + + with pytest.raises(errors.OperationLocationNotInWellError): + subject.get_well_position( + labware_id="labware-id", + well_name="B2", + well_location=LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=2, y=3, z=4), + volumeOffset="operationVolume", + ), + operation_volume=-3000.0, + ) + + def test_get_relative_well_location( decoy: Decoy, well_plate_def: LabwareDefinition, @@ -2897,3 +3012,62 @@ def _find_volume_from_height_(index: int) -> None: for i in range(len(frustum["height"])): _find_volume_from_height_(i) + + +def test_validate_dispense_volume_into_well_bottom( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + subject: GeometryView, +) -> None: + """It should raise an InvalidDispenseVolumeError if too much volume is specified.""" + well_def = well_plate_def.wells["B2"] + decoy.when(mock_labware_view.get_well_definition("labware-id", "B2")).then_return( + well_def + ) + + with pytest.raises(errors.InvalidDispenseVolumeError): + subject.validate_dispense_volume_into_well( + labware_id="labware-id", + well_name="B2", + well_location=LiquidHandlingWellLocation( + origin=WellOrigin.BOTTOM, + offset=WellOffset(x=2, y=3, z=4), + ), + volume=400.0, + ) + + +def test_validate_dispense_volume_into_well_meniscus( + decoy: Decoy, + mock_labware_view: LabwareView, + mock_well_view: WellView, + subject: GeometryView, +) -> None: + """It should raise an InvalidDispenseVolumeError if too much volume is specified.""" + labware_def = _load_labware_definition_data() + assert labware_def.wells is not None + well_def = labware_def.wells["A1"] + assert labware_def.innerLabwareGeometry is not None + inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] + + decoy.when(mock_labware_view.get_well_definition("labware-id", "A1")).then_return( + well_def + ) + decoy.when(mock_labware_view.get_well_geometry("labware-id", "A1")).then_return( + inner_well_def + ) + decoy.when( + mock_well_view.get_last_measured_liquid_height("labware-id", "A1") + ).then_return(45.0) + + with pytest.raises(errors.InvalidDispenseVolumeError): + subject.validate_dispense_volume_into_well( + labware_id="labware-id", + well_name="A1", + well_location=LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=2, y=3, z=4), + ), + volume=2000000.0, + ) From 38afbc35c73c36afa7da702f9d9b6c58a28a2056 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 9 Oct 2024 16:32:10 -0400 Subject: [PATCH 32/51] update needed after merging in edge --- .../protocol_api/core/engine/pipette_movement_conflict.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py index bfe98e1f217..b7b55abefee 100644 --- a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py @@ -19,6 +19,8 @@ DeckSlotLocation, OnLabwareLocation, WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, DropTipWellLocation, ) from opentrons.protocol_engine.types import ( @@ -66,7 +68,7 @@ def check_safe_for_pipette_movement( pipette_id: str, labware_id: str, well_name: str, - well_location: Union[WellLocation, DropTipWellLocation], + well_location: Union[WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation, DropTipWellLocation], ) -> None: """Check if the labware is safe to move to with a pipette in partial tip configuration. From b688aac7e73b3289c30a048ab4929ecadb8bd6e3 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 9 Oct 2024 17:10:46 -0400 Subject: [PATCH 33/51] follow-up format and lint --- .../protocol_api/core/engine/pipette_movement_conflict.py | 7 ++++++- .../opentrons/protocol_runner/test_json_translator.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py index b7b55abefee..46968c486d7 100644 --- a/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py +++ b/api/src/opentrons/protocol_api/core/engine/pipette_movement_conflict.py @@ -68,7 +68,12 @@ def check_safe_for_pipette_movement( pipette_id: str, labware_id: str, well_name: str, - well_location: Union[WellLocation, LiquidHandlingWellLocation, PickUpTipWellLocation, DropTipWellLocation], + well_location: Union[ + WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, + DropTipWellLocation, + ], ) -> None: """Check if the labware is safe to move to with a pipette in partial tip configuration. diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index 96710adea8a..d2d37772a5e 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -26,7 +26,6 @@ Location, ProfileStep, WellLocation as SD_WellLocation, - LiquidHandlingWellLocation as SD_LiquidHandlingWellLocation, OffsetVector, Metadata as SD_Metadata, Module, From f03bcb4c227b85dd311e61198402ba0351e542e0 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 10 Oct 2024 14:20:51 -0400 Subject: [PATCH 34/51] fixed up frustum_helpers and tests --- .../protocol_engine/commands/aspirate.py | 2 +- .../protocol_engine/state/frustum_helpers.py | 14 ++++++++----- .../state/test_geometry_view.py | 20 +++++++++---------- 3 files changed, 20 insertions(+), 16 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index f4edd7fcf1f..4a584298b10 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -118,7 +118,7 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_location.volumeOffset = "operationVolume" if well_location.offset.z > 0.0: raise InvalidAspirateLocationError( - f"Cannot specify a meniscus-relative Aspirate with a z-offset of {well_location.offset.z}" + f"Cannot specify a meniscus-relative Aspirate with a positive z-offset (specified {well_location.offset.z})" ) position = await self._movement.move_to_well( diff --git a/api/src/opentrons/protocol_engine/state/frustum_helpers.py b/api/src/opentrons/protocol_engine/state/frustum_helpers.py index 4f132ac3b40..09f726de767 100644 --- a/api/src/opentrons/protocol_engine/state/frustum_helpers.py +++ b/api/src/opentrons/protocol_engine/state/frustum_helpers.py @@ -375,18 +375,22 @@ def _find_height_in_partial_frustum( bottom_section_volume = 0.0 for section, capacity in zip(sorted_well, volumetric_capacity): section_top_height, section_volume = capacity - if bottom_section_volume < target_volume < section_volume: + if ( + bottom_section_volume + < target_volume + < (bottom_section_volume + section_volume) + ): relative_target_volume = target_volume - bottom_section_volume - relative_section_height = section.topHeight - section.bottomHeight + section_height = section.topHeight - section.bottomHeight partial_height = height_at_volume_within_section( section=section, target_volume_relative=relative_target_volume, - section_height=relative_section_height, + section_height=section_height, ) return partial_height + section.bottomHeight # bottom section volume should always be the volume enclosed in the previously # viewed section - bottom_section_volume = section_volume + bottom_section_volume += section_volume # if we've looked through all sections and can't find the target volume, raise an error raise InvalidLiquidHeightFound( @@ -399,7 +403,7 @@ def find_height_at_well_volume( ) -> float: """Find the height within a well, at a known volume.""" volumetric_capacity = get_well_volumetric_capacity(well_geometry) - max_volume = volumetric_capacity[-1][1] + max_volume = sum(row[1] for row in volumetric_capacity) if target_volume < 0 or target_volume > max_volume: raise InvalidLiquidHeightFound("Invalid target volume.") diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 5538228ca1f..9c05ff06852 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1609,13 +1609,13 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( offset=WellOffset(x=2, y=3, z=4), volumeOffset="operationVolume", ), - operation_volume=-323.0, + operation_volume=-1245.833, ) assert result == Point( x=slot_pos[0] + 1 + well_def.x + 2, y=slot_pos[1] - 2 + well_def.y + 3, - z=slot_pos[2] + 3 + well_def.z + 4 + 40.0, + z=slot_pos[2] + 3 + well_def.z + 4 + 20.0, ) @@ -1654,7 +1654,7 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( ) decoy.when( mock_well_view.get_last_measured_liquid_height("labware-id", "B2") - ).then_return(40.0) + ).then_return(45.0) labware_def = _load_labware_definition_data() assert labware_def.innerLabwareGeometry is not None inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] @@ -1668,14 +1668,14 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( well_location=LiquidHandlingWellLocation( origin=WellOrigin.MENISCUS, offset=WellOffset(x=2, y=3, z=4), - volumeOffset=-323.0, + volumeOffset=-1245.833, ), ) assert result == Point( x=slot_pos[0] + 1 + well_def.x + 2, y=slot_pos[1] - 2 + well_def.y + 3, - z=slot_pos[2] + 3 + well_def.z + 4 + 35.0, + z=slot_pos[2] + 3 + well_def.z + 4 + 20.0, ) @@ -1714,7 +1714,7 @@ def test_get_well_position_raises_validation_error( ) decoy.when( mock_well_view.get_last_measured_liquid_height("labware-id", "B2") - ).then_return(45.0) + ).then_return(40.0) labware_def = _load_labware_definition_data() assert labware_def.innerLabwareGeometry is not None inner_well_def = labware_def.innerLabwareGeometry["welldefinition1111"] @@ -1728,10 +1728,10 @@ def test_get_well_position_raises_validation_error( well_name="B2", well_location=LiquidHandlingWellLocation( origin=WellOrigin.MENISCUS, - offset=WellOffset(x=2, y=3, z=4), + offset=WellOffset(x=2, y=3, z=-40), volumeOffset="operationVolume", ), - operation_volume=-3000.0, + operation_volume=-100.0, ) @@ -3059,7 +3059,7 @@ def test_validate_dispense_volume_into_well_meniscus( ) decoy.when( mock_well_view.get_last_measured_liquid_height("labware-id", "A1") - ).then_return(45.0) + ).then_return(40.0) with pytest.raises(errors.InvalidDispenseVolumeError): subject.validate_dispense_volume_into_well( @@ -3069,5 +3069,5 @@ def test_validate_dispense_volume_into_well_meniscus( origin=WellOrigin.MENISCUS, offset=WellOffset(x=2, y=3, z=4), ), - volume=2000000.0, + volume=1100000.0, ) From 053a47c6342eb439c653fa436baf29bad36b893e Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 10 Oct 2024 16:11:10 -0400 Subject: [PATCH 35/51] updated validate_well_position and command schema 10 --- .../protocol_engine/state/geometry.py | 18 +++- .../opentrons/protocol_engine/state/motion.py | 1 + .../state/test_geometry_view.py | 20 +++++ .../protocol_engine/state/test_motion_view.py | 9 +- shared-data/command/schemas/10.json | 84 +++++++++++++++---- 5 files changed, 113 insertions(+), 19 deletions(-) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index f8c6b335f22..a64d13b597c 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -430,14 +430,25 @@ def get_labware_position(self, labware_id: str) -> Point: def validate_well_position( self, well_location: WellLocations, - well_depth: float, z_offset: float, + pipette_id: Optional[str] = None, ) -> None: """Raise exception if operation location is not within well. Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration. """ - if z_offset < 0: + invalid = False + if well_location.origin == WellOrigin.MENISCUS: + assert pipette_id is not None + lld_min_height = self._pipettes.get_current_tip_lld_settings( + pipette_id=pipette_id + ) + if z_offset < lld_min_height: + invalid = True + elif z_offset < 0: + invalid = True + + if invalid: if isinstance(well_location, PickUpTipWellLocation): raise OperationLocationNotInWellError( f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well" @@ -453,6 +464,7 @@ def get_well_position( well_name: str, well_location: Optional[WellLocations] = None, operation_volume: Optional[float] = None, + pipette_id: Optional[str] = None, ) -> Point: """Given relative well location in a labware, get absolute position.""" labware_pos = self.get_labware_position(labware_id) @@ -471,7 +483,7 @@ def get_well_position( ) offset = offset.copy(update={"z": offset.z + offset_adjustment}) self.validate_well_position( - well_location=well_location, well_depth=well_depth, z_offset=offset.z + well_location=well_location, z_offset=offset.z, pipette_id=pipette_id ) return Point( diff --git a/api/src/opentrons/protocol_engine/state/motion.py b/api/src/opentrons/protocol_engine/state/motion.py index 05a8b5a8949..c9aa146715b 100644 --- a/api/src/opentrons/protocol_engine/state/motion.py +++ b/api/src/opentrons/protocol_engine/state/motion.py @@ -113,6 +113,7 @@ def get_movement_waypoints_to_well( well_name=well_name, well_location=well_location, operation_volume=operation_volume, + pipette_id=pipette_id, ) move_type = _move_types.get_move_type_to_well( diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 10638aea500..070eeb229bb 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1511,6 +1511,7 @@ def test_get_well_position_with_meniscus_offset( mock_labware_view: LabwareView, mock_well_view: WellView, mock_addressable_area_view: AddressableAreaView, + mock_pipette_view: PipetteView, subject: GeometryView, ) -> None: """It should be able to get the position of a well meniscus in a labware.""" @@ -1541,6 +1542,9 @@ def test_get_well_position_with_meniscus_offset( decoy.when( mock_well_view.get_last_measured_liquid_height("labware-id", "B2") ).then_return(70.5) + decoy.when( + mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") + ).then_return(0.5) result = subject.get_well_position( labware_id="labware-id", @@ -1549,6 +1553,7 @@ def test_get_well_position_with_meniscus_offset( origin=WellOrigin.MENISCUS, offset=WellOffset(x=2, y=3, z=4), ), + pipette_id="pipette-id", ) assert result == Point( @@ -1564,6 +1569,7 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( mock_labware_view: LabwareView, mock_well_view: WellView, mock_addressable_area_view: AddressableAreaView, + mock_pipette_view: PipetteView, subject: GeometryView, ) -> None: """It should be able to get the position of a well meniscus in a labware with a volume offset.""" @@ -1600,6 +1606,9 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( inner_well_def ) + decoy.when( + mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") + ).then_return(0.5) result = subject.get_well_position( labware_id="labware-id", @@ -1610,6 +1619,7 @@ def test_get_well_position_with_meniscus_and_literal_volume_offset( volumeOffset="operationVolume", ), operation_volume=-1245.833, + pipette_id="pipette-id", ) assert result == Point( @@ -1625,6 +1635,7 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( mock_labware_view: LabwareView, mock_well_view: WellView, mock_addressable_area_view: AddressableAreaView, + mock_pipette_view: PipetteView, subject: GeometryView, ) -> None: """It should be able to get the position of a well meniscus in a labware with a volume offset.""" @@ -1661,6 +1672,9 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( inner_well_def ) + decoy.when( + mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") + ).then_return(0.5) result = subject.get_well_position( labware_id="labware-id", @@ -1670,6 +1684,7 @@ def test_get_well_position_with_meniscus_and_float_volume_offset( offset=WellOffset(x=2, y=3, z=4), volumeOffset=-1245.833, ), + pipette_id="pipette-id", ) assert result == Point( @@ -1685,6 +1700,7 @@ def test_get_well_position_raises_validation_error( mock_labware_view: LabwareView, mock_well_view: WellView, mock_addressable_area_view: AddressableAreaView, + mock_pipette_view: PipetteView, subject: GeometryView, ) -> None: """It should raise a validation error when a volume offset is too large (ie location is below the well bottom).""" @@ -1721,6 +1737,9 @@ def test_get_well_position_raises_validation_error( decoy.when(mock_labware_view.get_well_geometry("labware-id", "B2")).then_return( inner_well_def ) + decoy.when( + mock_pipette_view.get_current_tip_lld_settings(pipette_id="pipette-id") + ).then_return(0.5) with pytest.raises(errors.OperationLocationNotInWellError): subject.get_well_position( @@ -1732,6 +1751,7 @@ def test_get_well_position_raises_validation_error( volumeOffset="operationVolume", ), operation_volume=-100.0, + pipette_id="pipette-id", ) diff --git a/api/tests/opentrons/protocol_engine/state/test_motion_view.py b/api/tests/opentrons/protocol_engine/state/test_motion_view.py index e20f88cb68f..9e7307f29a7 100644 --- a/api/tests/opentrons/protocol_engine/state/test_motion_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_motion_view.py @@ -309,7 +309,9 @@ def test_get_movement_waypoints_to_well_for_y_center( ).then_return(False) decoy.when( - geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None) + geometry_view.get_well_position( + "labware-id", "well-name", WellLocation(), None, "pipette-id" + ) ).then_return(Point(x=4, y=5, z=6)) decoy.when( @@ -391,7 +393,9 @@ def test_get_movement_waypoints_to_well_for_xy_center( ).then_return(True) decoy.when( - geometry_view.get_well_position("labware-id", "well-name", WellLocation(), None) + geometry_view.get_well_position( + "labware-id", "well-name", WellLocation(), None, "pipette-id" + ) ).then_return(Point(x=4, y=5, z=6)) decoy.when( @@ -461,6 +465,7 @@ def test_get_movement_waypoints_to_well_raises( well_name="A1", well_location=None, operation_volume=None, + pipette_id="pipette-id", ) ).then_return(Point(x=4, y=5, z=6)) decoy.when(pipette_view.get_current_location()).then_return(None) diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index 6eb524b9a45..b6bdd71046d 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -326,9 +326,9 @@ } } }, - "WellLocation": { - "title": "WellLocation", - "description": "A relative location in reference to a well's location.", + "LiquidHandlingWellLocation": { + "title": "LiquidHandlingWellLocation", + "description": "A relative location in reference to a well's location.\n\nTo be used with commands that handle liquids.", "type": "object", "properties": { "origin": { @@ -341,6 +341,20 @@ }, "offset": { "$ref": "#/definitions/WellOffset" + }, + "volumeOffset": { + "title": "Volumeoffset", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the volume command parameter.", + "default": 0.0, + "anyOf": [ + { + "type": "number" + }, + { + "enum": ["operationVolume"], + "type": "string" + } + ] } } }, @@ -364,7 +378,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -800,7 +814,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -939,7 +953,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/LiquidHandlingWellLocation" } ] }, @@ -2005,6 +2019,30 @@ }, "required": ["params"] }, + "WellLocation": { + "title": "WellLocation", + "description": "A relative location in reference to a well's location.", + "type": "object", + "properties": { + "origin": { + "default": "top", + "allOf": [ + { + "$ref": "#/definitions/WellOrigin" + } + ] + }, + "offset": { + "$ref": "#/definitions/WellOffset" + }, + "volumeOffset": { + "title": "Volumeoffset", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", + "default": 0.0, + "type": "number" + } + } + }, "MoveToWellParams": { "title": "MoveToWellParams", "description": "Payload required to move a pipette to a specific well.", @@ -2410,11 +2448,34 @@ }, "required": ["params"] }, + "PickUpTipWellLocation": { + "title": "PickUpTipWellLocation", + "description": "A relative location in reference to a well's location.\n\nTo be used for picking up tips.", + "type": "object", + "properties": { + "origin": { + "default": "top", + "allOf": [ + { + "$ref": "#/definitions/WellOrigin" + } + ] + }, + "offset": { + "$ref": "#/definitions/WellOffset" + } + } + }, "PickUpTipParams": { "title": "PickUpTipParams", "description": "Payload needed to move a pipette to a specific well.", "type": "object", "properties": { + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" + }, "labwareId": { "title": "Labwareid", "description": "Identifier of labware to use.", @@ -2427,20 +2488,15 @@ }, "wellLocation": { "title": "Welllocation", - "description": "Relative well location at which to perform the operation", + "description": "Relative well location at which to pick up the tip.", "allOf": [ { - "$ref": "#/definitions/WellLocation" + "$ref": "#/definitions/PickUpTipWellLocation" } ] - }, - "pipetteId": { - "title": "Pipetteid", - "description": "Identifier of pipette to use for liquid handling.", - "type": "string" } }, - "required": ["labwareId", "wellName", "pipetteId"] + "required": ["pipetteId", "labwareId", "wellName"] }, "PickUpTipCreate": { "title": "PickUpTipCreate", From 05a075c5a3e6401033b063573284fc9dcc663248 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 10 Oct 2024 17:13:33 -0400 Subject: [PATCH 36/51] added tiprack check to move_to_well --- .../protocol_engine/commands/move_to_well.py | 35 +++++++--- .../commands/test_move_to_well.py | 69 ++++++++++++++++++- 2 files changed, 93 insertions(+), 11 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 9695ccb3bc0..5b5f32a1f95 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from ..types import DeckPoint +from ..types import DeckPoint, WellOrigin from .pipetting_common import ( PipetteIdMixin, WellLocationMixin, @@ -13,9 +13,11 @@ from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData from ..errors.error_occurrence import ErrorOccurrence from ..state import update_types +from ..errors import LabwareIsTipRackError if TYPE_CHECKING: from ..execution import MovementHandler + from ..state.state import StateView MoveToWellCommandType = Literal["moveToWell"] @@ -37,29 +39,44 @@ class MoveToWellImplementation( ): """Move to well command implementation.""" - def __init__(self, movement: MovementHandler, **kwargs: object) -> None: + def __init__( + self, state_view: StateView, movement: MovementHandler, **kwargs: object + ) -> None: + self._state_view = state_view self._movement = movement async def execute( self, params: MoveToWellParams ) -> SuccessData[MoveToWellResult, None]: """Move the requested pipette to the requested well.""" + pipette_id = params.pipetteId + labware_id = params.labwareId + well_name = params.wellName + well_location = params.wellLocation + state_update = update_types.StateUpdate() + if (self._state_view.labware.is_tiprack(labware_id)) and ( + well_location.origin == WellOrigin.MENISCUS or well_location.volumeOffset + ): + raise LabwareIsTipRackError( + "Cannot specify a WellLocation with an origin of MENISCUS or any volumeOffset with movement to a tip rack" + ) + x, y, z = await self._movement.move_to_well( - pipette_id=params.pipetteId, - labware_id=params.labwareId, - well_name=params.wellName, - well_location=params.wellLocation, + pipette_id=pipette_id, + labware_id=labware_id, + well_name=well_name, + well_location=well_location, force_direct=params.forceDirect, minimum_z_height=params.minimumZHeight, speed=params.speed, ) deck_point = DeckPoint.construct(x=x, y=y, z=z) state_update.set_pipette_location( - pipette_id=params.pipetteId, - new_labware_id=params.labwareId, - new_well_name=params.wellName, + pipette_id=pipette_id, + new_labware_id=labware_id, + new_well_name=well_name, new_deck_point=deck_point, ) diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index d91822979f2..73ef22e04c6 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -1,7 +1,14 @@ """Test move to well commands.""" +import pytest from decoy import Decoy -from opentrons.protocol_engine import WellLocation, WellOffset, DeckPoint +from opentrons.protocol_engine import ( + WellLocation, + WellOrigin, + WellOffset, + DeckPoint, + errors, +) from opentrons.protocol_engine.execution import MovementHandler from opentrons.protocol_engine.state import update_types from opentrons.types import Point @@ -12,14 +19,22 @@ MoveToWellResult, MoveToWellImplementation, ) +from opentrons.protocol_engine.state.state import StateView + + +@pytest.fixture +def mock_state_view(decoy: Decoy) -> StateView: + """Get a mock StateView.""" + return decoy.mock(cls=StateView) async def test_move_to_well_implementation( decoy: Decoy, + state_view: StateView, movement: MovementHandler, ) -> None: """A MoveToWell command should have an execution implementation.""" - subject = MoveToWellImplementation(movement=movement) + subject = MoveToWellImplementation(state_view=state_view, movement=movement) data = MoveToWellParams( pipetteId="abc", @@ -56,3 +71,53 @@ async def test_move_to_well_implementation( ) ), ) + + +async def test_move_to_well_with_tip_rack_and_meniscus( + decoy: Decoy, + mock_state_view: StateView, + movement: MovementHandler, +) -> None: + """It should disallow movement to a tip rack when MENISCUS is specified.""" + subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) + + data = MoveToWellParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation( + origin=WellOrigin.MENISCUS, offset=WellOffset(x=1, y=2, z=3) + ), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + ) + + decoy.when(mock_state_view.labware.is_tiprack("123")).then_return(True) + + with pytest.raises(errors.LabwareIsTipRackError): + await subject.execute(data) + + +async def test_move_to_well_with_tip_rack_and_volume_offset( + decoy: Decoy, + mock_state_view: StateView, + movement: MovementHandler, +) -> None: + """It should disallow movement to a tip rack when volumeOffset is specified.""" + subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) + + data = MoveToWellParams( + pipetteId="abc", + labwareId="123", + wellName="A3", + wellLocation=WellLocation(offset=WellOffset(x=1, y=2, z=3), volumeOffset=-40.0), + forceDirect=True, + minimumZHeight=4.56, + speed=7.89, + ) + + decoy.when(mock_state_view.labware.is_tiprack("123")).then_return(True) + + with pytest.raises(errors.LabwareIsTipRackError): + await subject.execute(data) From 97858d8618be75a746084f29af8ab3209c5d7ae4 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 11 Oct 2024 14:05:31 -0400 Subject: [PATCH 37/51] updates to relax WellLocation contraints based on PR review --- .../protocol_engine/commands/aspirate.py | 5 ----- .../protocol_engine/commands/dispense.py | 2 -- .../protocol_engine/errors/exceptions.py | 16 ---------------- .../opentrons/protocol_engine/state/geometry.py | 13 ++++++++----- .../opentrons/protocol_engine/state/labware.py | 1 + 5 files changed, 9 insertions(+), 28 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/aspirate.py b/api/src/opentrons/protocol_engine/commands/aspirate.py index 4a584298b10..14b59248216 100644 --- a/api/src/opentrons/protocol_engine/commands/aspirate.py +++ b/api/src/opentrons/protocol_engine/commands/aspirate.py @@ -21,7 +21,6 @@ SuccessData, ) from ..errors.error_occurrence import ErrorOccurrence -from ..errors import InvalidAspirateLocationError from opentrons.hardware_control import HardwareControlAPI @@ -116,10 +115,6 @@ async def execute(self, params: AspirateParams) -> _ExecuteReturn: well_location = params.wellLocation if well_location.origin == WellOrigin.MENISCUS: well_location.volumeOffset = "operationVolume" - if well_location.offset.z > 0.0: - raise InvalidAspirateLocationError( - f"Cannot specify a meniscus-relative Aspirate with a positive z-offset (specified {well_location.offset.z})" - ) position = await self._movement.move_to_well( pipette_id=pipette_id, diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index db2ad66734e..19b98755bbc 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -83,8 +83,6 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: well_name = params.wellName volume = params.volume - if well_location.origin == WellOrigin.MENISCUS: - well_location.volumeOffset = 0.0 self._state_view.geometry.validate_dispense_volume_into_well( labware_id=labware_id, well_name=well_name, diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index aefd21ba6fa..dad964a92c3 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1095,19 +1095,3 @@ def __init__( ) -> None: """Build an OperationLocationNotInWellError.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) - - -class InvalidAspirateLocationError(ProtocolEngineError): - """Raised when a meniscus-relative Aspirate's z-offset is greater than 0.0. - - This would result in aspiration above the meniscus (in air), which should not be allowed. - """ - - def __init__( - self, - message: Optional[str] = None, - details: Optional[Dict[str, Any]] = None, - wrapping: Optional[Sequence[EnumeratedError]] = None, - ) -> None: - """Build an InvalidAspirateLocationError.""" - super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index a64d13b597c..7740b622a5f 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -437,18 +437,21 @@ def validate_well_position( Primarily this checks if there is not enough liquid in a well to do meniscus-relative static aspiration. """ - invalid = False if well_location.origin == WellOrigin.MENISCUS: assert pipette_id is not None lld_min_height = self._pipettes.get_current_tip_lld_settings( pipette_id=pipette_id ) if z_offset < lld_min_height: - invalid = True + if isinstance(well_location, PickUpTipWellLocation): + raise OperationLocationNotInWellError( + f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location that could be below the bottom of the well" + ) + else: + raise OperationLocationNotInWellError( + f"Specifying {well_location.origin} with an offset of {well_location.offset} and a volume offset of {well_location.volumeOffset} results in an operation location that could be below the bottom of the well" + ) elif z_offset < 0: - invalid = True - - if invalid: if isinstance(well_location, PickUpTipWellLocation): raise OperationLocationNotInWellError( f"Specifying {well_location.origin} with an offset of {well_location.offset} results in an operation location below the bottom of the well" diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 6157ebecd44..907c9d20e2e 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -500,6 +500,7 @@ def get_well_geometry( self, labware_id: str, well_name: Optional[str] = None ) -> InnerWellGeometry: """Get a well's inner geometry by labware and well name.""" + # TODO(pbm, 10-11-24): wordsmith this error labware_def = self.get_definition(labware_id) if labware_def.innerLabwareGeometry is None: raise errors.InvalidWellDefinitionError( From 4e6a6ba50b268b43197d8dd82985c3eb3bd058cd Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 11 Oct 2024 14:09:29 -0400 Subject: [PATCH 38/51] reverted command schema 9 --- shared-data/command/schemas/9.json | 84 +++++------------------------- 1 file changed, 14 insertions(+), 70 deletions(-) diff --git a/shared-data/command/schemas/9.json b/shared-data/command/schemas/9.json index e109914af52..546753b736d 100644 --- a/shared-data/command/schemas/9.json +++ b/shared-data/command/schemas/9.json @@ -310,9 +310,9 @@ } } }, - "LiquidHandlingWellLocation": { - "title": "LiquidHandlingWellLocation", - "description": "A relative location in reference to a well's location.\n\nTo be used with commands that handle liquids.", + "WellLocation": { + "title": "WellLocation", + "description": "A relative location in reference to a well's location.", "type": "object", "properties": { "origin": { @@ -325,20 +325,6 @@ }, "offset": { "$ref": "#/definitions/WellOffset" - }, - "volumeOffset": { - "title": "Volumeoffset", - "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the volume command parameter.", - "default": 0.0, - "anyOf": [ - { - "type": "number" - }, - { - "enum": ["operationVolume"], - "type": "string" - } - ] } } }, @@ -362,7 +348,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/LiquidHandlingWellLocation" + "$ref": "#/definitions/WellLocation" } ] }, @@ -798,7 +784,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/LiquidHandlingWellLocation" + "$ref": "#/definitions/WellLocation" } ] }, @@ -937,7 +923,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/LiquidHandlingWellLocation" + "$ref": "#/definitions/WellLocation" } ] }, @@ -2003,30 +1989,6 @@ }, "required": ["params"] }, - "WellLocation": { - "title": "WellLocation", - "description": "A relative location in reference to a well's location.", - "type": "object", - "properties": { - "origin": { - "default": "top", - "allOf": [ - { - "$ref": "#/definitions/WellOrigin" - } - ] - }, - "offset": { - "$ref": "#/definitions/WellOffset" - }, - "volumeOffset": { - "title": "Volumeoffset", - "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", - "default": 0.0, - "type": "number" - } - } - }, "MoveToWellParams": { "title": "MoveToWellParams", "description": "Payload required to move a pipette to a specific well.", @@ -2432,34 +2394,11 @@ }, "required": ["params"] }, - "PickUpTipWellLocation": { - "title": "PickUpTipWellLocation", - "description": "A relative location in reference to a well's location.\n\nTo be used for picking up tips.", - "type": "object", - "properties": { - "origin": { - "default": "top", - "allOf": [ - { - "$ref": "#/definitions/WellOrigin" - } - ] - }, - "offset": { - "$ref": "#/definitions/WellOffset" - } - } - }, "PickUpTipParams": { "title": "PickUpTipParams", "description": "Payload needed to move a pipette to a specific well.", "type": "object", "properties": { - "pipetteId": { - "title": "Pipetteid", - "description": "Identifier of pipette to use for liquid handling.", - "type": "string" - }, "labwareId": { "title": "Labwareid", "description": "Identifier of labware to use.", @@ -2472,15 +2411,20 @@ }, "wellLocation": { "title": "Welllocation", - "description": "Relative well location at which to pick up the tip.", + "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/PickUpTipWellLocation" + "$ref": "#/definitions/WellLocation" } ] + }, + "pipetteId": { + "title": "Pipetteid", + "description": "Identifier of pipette to use for liquid handling.", + "type": "string" } }, - "required": ["pipetteId", "labwareId", "wellName"] + "required": ["labwareId", "wellName", "pipetteId"] }, "PickUpTipCreate": { "title": "PickUpTipCreate", From f364bc9778a0ccd8b4a584edbcb457d36c413736 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 11 Oct 2024 14:30:34 -0400 Subject: [PATCH 39/51] linted, removed test, reverted protocol/models/ work --- .../protocol_engine/commands/dispense.py | 2 +- .../protocol_engine/errors/__init__.py | 2 -- .../protocol_engine/commands/test_aspirate.py | 30 ------------------- .../protocol/models/__init__.py | 6 ---- .../protocol/models/shared_models.py | 19 +----------- 5 files changed, 2 insertions(+), 57 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index 19b98755bbc..cd00d50df3b 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -7,7 +7,7 @@ from pydantic import Field -from ..types import DeckPoint, WellOrigin +from ..types import DeckPoint from ..state.update_types import StateUpdate from .pipetting_common import ( PipetteIdMixin, diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index 161e45f82dd..ee4447b38bf 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -74,7 +74,6 @@ InvalidWellDefinitionError, OperationLocationNotInWellError, InvalidDispenseVolumeError, - InvalidAspirateLocationError, ) from .error_occurrence import ErrorOccurrence, ProtocolCommandFailedError @@ -157,5 +156,4 @@ "InvalidWellDefinitionError", "OperationLocationNotInWellError", "InvalidDispenseVolumeError", - "InvalidAspirateLocationError", ] diff --git a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py index 4a735d98074..8d6f6d92179 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_aspirate.py +++ b/api/tests/opentrons/protocol_engine/commands/test_aspirate.py @@ -32,7 +32,6 @@ from opentrons.protocol_engine.types import CurrentWell, LoadedPipette from opentrons.hardware_control import HardwareControlAPI from opentrons.protocol_engine.notes import CommandNoteAdder -from opentrons.protocol_engine.errors import InvalidAspirateLocationError @pytest.fixture @@ -382,32 +381,3 @@ async def test_aspirate_implementation_meniscus( ) ), ) - - -async def test_aspirate_implementation_invalid_meniscus_location_error( - decoy: Decoy, - state_view: StateView, - hardware_api: HardwareControlAPI, - movement: MovementHandler, - pipetting: PipettingHandler, - subject: AspirateImplementation, - mock_command_note_adder: CommandNoteAdder, -) -> None: - """Aspirate should raise InvalidAspirateLocationError when called with WellOrigin.MENISCUS and a WellOffset greater than 0.0 (ie aspiration from air).""" - location = LiquidHandlingWellLocation( - origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=1) - ) - - data = AspirateParams( - pipetteId="abc", - labwareId="123", - wellName="A3", - wellLocation=location, - volume=50, - flowRate=1.23, - ) - - decoy.when(pipetting.get_is_ready_to_aspirate(pipette_id="abc")).then_return(True) - - with pytest.raises(InvalidAspirateLocationError): - await subject.execute(data) diff --git a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py index a8aed145690..76f8449d93d 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/__init__.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/__init__.py @@ -10,9 +10,6 @@ Location, ProfileStep, WellLocation, - LiquidHandlingWellLocation, - DropTipWellLocation, - PickUpTipWellLocation, OffsetVector, Dimensions, GroupMetadata, @@ -38,9 +35,6 @@ "Location", "ProfileStep", "WellLocation", - "LiquidHandlingWellLocation", - "DropTipWellLocation", - "PickUpTipWellLocation", "OffsetVector", "Dimensions", "GroupMetadata", diff --git a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py index dad7d0a1e4b..8cf3276f71f 100644 --- a/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py +++ b/shared-data/python/opentrons_shared_data/protocol/models/shared_models.py @@ -1,4 +1,4 @@ -from typing import Optional, List, Dict, Any, Union +from typing import Optional, List, Dict, Any from typing_extensions import Literal from enum import Enum from pydantic import BaseModel @@ -92,23 +92,6 @@ class ProfileStep(BaseModel): class WellLocation(BaseModel): origin: Optional[str] offset: Optional[OffsetVector] - volumeOffset: Optional[float] - - -class LiquidHandlingWellLocation(BaseModel): - origin: Optional[str] - offset: Optional[OffsetVector] - volumeOffset: Optional[Union[float, Literal["operationVolume"]]] - - -class DropTipWellLocation(BaseModel): - origin: Optional[str] - offset: Optional[OffsetVector] - - -class PickUpTipWellLocation(BaseModel): - origin: Optional[str] - offset: Optional[OffsetVector] class Liquid(BaseModel): From 029ecea60c4dc87bd99086ab9d66bf9aafb8801c Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Fri, 11 Oct 2024 14:35:41 -0400 Subject: [PATCH 40/51] eliminated test that doesn't do anything --- .../protocol_engine/commands/test_dispense.py | 58 ------------------- 1 file changed, 58 deletions(-) diff --git a/api/tests/opentrons/protocol_engine/commands/test_dispense.py b/api/tests/opentrons/protocol_engine/commands/test_dispense.py index 89f0ac4048a..167223e6d9d 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_dispense.py +++ b/api/tests/opentrons/protocol_engine/commands/test_dispense.py @@ -96,64 +96,6 @@ async def test_dispense_implementation( ) -async def test_dispense_implementation_meniscus( - decoy: Decoy, - movement: MovementHandler, - pipetting: PipettingHandler, - subject: DispenseImplementation, -) -> None: - """Dispense should update WellVolumeOffset when called with WellOrigin.MENISCUS.""" - well_location = LiquidHandlingWellLocation( - origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=-1) - ) - updated_location = LiquidHandlingWellLocation( - origin=WellOrigin.MENISCUS, - offset=WellOffset(x=0, y=0, z=-1), - volumeOffset=0.0, - ) - - data = DispenseParams( - pipetteId="pipette-id-abc123", - labwareId="labware-id-abc123", - wellName="A3", - wellLocation=well_location, - volume=50, - flowRate=1.23, - ) - - decoy.when( - await movement.move_to_well( - pipette_id="pipette-id-abc123", - labware_id="labware-id-abc123", - well_name="A3", - well_location=updated_location, - ) - ).then_return(Point(x=1, y=2, z=3)) - - decoy.when( - await pipetting.dispense_in_place( - pipette_id="pipette-id-abc123", volume=50, flow_rate=1.23, push_out=None - ) - ).then_return(42) - - result = await subject.execute(data) - - assert result == SuccessData( - public=DispenseResult(volume=42, position=DeckPoint(x=1, y=2, z=3)), - private=None, - state_update=update_types.StateUpdate( - pipette_location=update_types.PipetteLocationUpdate( - pipette_id="pipette-id-abc123", - new_location=update_types.Well( - labware_id="labware-id-abc123", - well_name="A3", - ), - new_deck_point=DeckPoint.construct(x=1, y=2, z=3), - ), - ), - ) - - async def test_overpressure_error( decoy: Decoy, movement: MovementHandler, From 38235fecc6fce2a3a36e3c5a63ac9727b07e8f54 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Mon, 14 Oct 2024 17:57:17 -0400 Subject: [PATCH 41/51] Created PickUpTipWellOrigin without MENISCUS enum and updated BlowOut command to use WellLocation --- .../protocol_api/core/engine/instrument.py | 10 +-- api/src/opentrons/protocol_engine/__init__.py | 2 + .../protocol_engine/commands/blow_out.py | 4 +- .../protocol_engine/state/geometry.py | 4 +- api/src/opentrons/protocol_engine/types.py | 19 +++++- .../core/engine/test_instrument_core.py | 19 +++--- .../protocol_engine/commands/test_blow_out.py | 6 +- .../protocol_engine/state/command_fixtures.py | 4 +- .../protocol_runner/test_json_translator.py | 2 +- shared-data/command/schemas/10.json | 62 ++++++++++--------- 10 files changed, 78 insertions(+), 54 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index abb5f3862ce..ece3d74ada0 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -310,10 +310,12 @@ def blow_out( well_name = well_core.get_name() labware_id = well_core.labware_id - well_location = self._engine_client.state.geometry.get_relative_liquid_handling_well_location( - labware_id=labware_id, - well_name=well_name, - absolute_point=location.point, + well_location = ( + self._engine_client.state.geometry.get_relative_well_location( + labware_id=labware_id, + well_name=well_name, + absolute_point=location.point, + ) ) pipette_movement_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, diff --git a/api/src/opentrons/protocol_engine/__init__.py b/api/src/opentrons/protocol_engine/__init__.py index 2c8171a0e83..25599189916 100644 --- a/api/src/opentrons/protocol_engine/__init__.py +++ b/api/src/opentrons/protocol_engine/__init__.py @@ -52,6 +52,7 @@ DropTipWellLocation, WellOrigin, DropTipWellOrigin, + PickUpTipWellOrigin, WellOffset, ModuleModel, ModuleDefinition, @@ -116,6 +117,7 @@ "DropTipWellLocation", "WellOrigin", "DropTipWellOrigin", + "PickUpTipWellOrigin", "WellOffset", "ModuleModel", "ModuleDefinition", diff --git a/api/src/opentrons/protocol_engine/commands/blow_out.py b/api/src/opentrons/protocol_engine/commands/blow_out.py index 0ded626da62..9954ef07cfa 100644 --- a/api/src/opentrons/protocol_engine/commands/blow_out.py +++ b/api/src/opentrons/protocol_engine/commands/blow_out.py @@ -9,7 +9,7 @@ from .pipetting_common import ( PipetteIdMixin, FlowRateMixin, - LiquidHandlingWellLocationMixin, + WellLocationMixin, DestinationPositionResult, ) from .command import AbstractCommandImpl, BaseCommand, BaseCommandCreate, SuccessData @@ -25,7 +25,7 @@ BlowOutCommandType = Literal["blowout"] -class BlowOutParams(PipetteIdMixin, FlowRateMixin, LiquidHandlingWellLocationMixin): +class BlowOutParams(PipetteIdMixin, FlowRateMixin, WellLocationMixin): """Payload required to blow-out a specific well.""" pass diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 7740b622a5f..42ef50d110b 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -675,7 +675,9 @@ def convert_pick_up_tip_well_location( self, well_location: PickUpTipWellLocation ) -> WellLocation: """Convert PickUpTipWellLocation to WellLocation.""" - return WellLocation(origin=well_location.origin, offset=well_location.offset) + return WellLocation( + origin=WellOrigin(well_location.origin.value), offset=well_location.offset + ) # TODO(jbl 11-30-2023) fold this function into get_ancestor_slot_name see RSS-411 def _get_staging_slot_name(self, labware_id: str) -> str: diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py index 1994a67a190..72daafd3a52 100644 --- a/api/src/opentrons/protocol_engine/types.py +++ b/api/src/opentrons/protocol_engine/types.py @@ -205,6 +205,7 @@ class WellOrigin(str, Enum): TOP: the top-center of the well BOTTOM: the bottom-center of the well CENTER: the middle-center of the well + MENISCUS: the meniscus-center of the well """ TOP = "top" @@ -213,6 +214,20 @@ class WellOrigin(str, Enum): MENISCUS = "meniscus" +class PickUpTipWellOrigin(str, Enum): + """The origin of a PickUpTipWellLocation offset. + + Props: + TOP: the top-center of the well + BOTTOM: the bottom-center of the well + CENTER: the middle-center of the well + """ + + TOP = "top" + BOTTOM = "bottom" + CENTER = "center" + + class DropTipWellOrigin(str, Enum): """The origin of a DropTipWellLocation offset. @@ -260,7 +275,7 @@ class LiquidHandlingWellLocation(BaseModel): offset: WellOffset = Field(default_factory=WellOffset) volumeOffset: Union[float, Literal["operationVolume"]] = Field( default=0.0, - description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the volume command parameter.""", + description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""", ) @@ -270,7 +285,7 @@ class PickUpTipWellLocation(BaseModel): To be used for picking up tips. """ - origin: WellOrigin = WellOrigin.TOP + origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP offset: WellOffset = Field(default_factory=WellOffset) diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index b7c1b82d5be..3883d150067 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -20,6 +20,7 @@ PickUpTipWellLocation, WellOffset, WellOrigin, + PickUpTipWellOrigin, DropTipWellLocation, DropTipWellOrigin, ) @@ -266,7 +267,9 @@ def test_pick_up_tip( absolute_point=Point(1, 2, 3), ) ).then_return( - PickUpTipWellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1)) + PickUpTipWellLocation( + origin=PickUpTipWellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + ) ) subject.pick_up_tip( @@ -288,7 +291,7 @@ def test_pick_up_tip( labware_id="labware-id", well_name="well-name", well_location=PickUpTipWellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + origin=PickUpTipWellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ), mock_engine_client.execute_command( @@ -297,7 +300,7 @@ def test_pick_up_tip( labwareId="labware-id", wellName="well-name", wellLocation=PickUpTipWellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) + origin=PickUpTipWellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), ) ), @@ -620,14 +623,10 @@ def test_blow_out_to_well( ) decoy.when( - mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( + mock_engine_client.state.geometry.get_relative_well_location( labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) ) - ).then_return( - LiquidHandlingWellLocation( - origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) - ) - ) + ).then_return(WellLocation(origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1))) subject.blow_out(location=location, well_core=well_core, in_place=False) @@ -646,7 +645,7 @@ def test_blow_out_to_well( pipetteId="abc123", labwareId="123abc", wellName="my cool well", - wellLocation=LiquidHandlingWellLocation( + wellLocation=WellLocation( origin=WellOrigin.TOP, offset=WellOffset(x=3, y=2, z=1) ), flowRate=6.7, diff --git a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py index 8ceb4ab17c3..72eeee1532b 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_blow_out.py +++ b/api/tests/opentrons/protocol_engine/commands/test_blow_out.py @@ -3,7 +3,7 @@ from opentrons.types import Point from opentrons.protocol_engine import ( - LiquidHandlingWellLocation, + WellLocation, WellOrigin, WellOffset, DeckPoint, @@ -38,9 +38,7 @@ async def test_blow_out_implementation( pipetting=pipetting, ) - location = LiquidHandlingWellLocation( - origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1) - ) + location = WellLocation(origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=1)) data = BlowOutParams( pipetteId="pipette-id", diff --git a/api/tests/opentrons/protocol_engine/state/command_fixtures.py b/api/tests/opentrons/protocol_engine/state/command_fixtures.py index ab864d8f3d5..9c4665d31a2 100644 --- a/api/tests/opentrons/protocol_engine/state/command_fixtures.py +++ b/api/tests/opentrons/protocol_engine/state/command_fixtures.py @@ -512,7 +512,7 @@ def create_blow_out_command( flow_rate: float, labware_id: str = "labware-id", well_name: str = "A1", - well_location: Optional[LiquidHandlingWellLocation] = None, + well_location: Optional[WellLocation] = None, destination: DeckPoint = DeckPoint(x=0, y=0, z=0), ) -> cmd.BlowOut: """Get a completed BlowOut command.""" @@ -520,7 +520,7 @@ def create_blow_out_command( pipetteId=pipette_id, labwareId=labware_id, wellName=well_name, - wellLocation=well_location or LiquidHandlingWellLocation(), + wellLocation=well_location or WellLocation(), flowRate=flow_rate, ) result = cmd.BlowOutResult(position=destination) diff --git a/api/tests/opentrons/protocol_runner/test_json_translator.py b/api/tests/opentrons/protocol_runner/test_json_translator.py index d2d37772a5e..e2735e4cdbc 100644 --- a/api/tests/opentrons/protocol_runner/test_json_translator.py +++ b/api/tests/opentrons/protocol_runner/test_json_translator.py @@ -441,7 +441,7 @@ pipetteId="pipette-id-1", labwareId="labware-id-2", wellName="A1", - wellLocation=LiquidHandlingWellLocation( + wellLocation=WellLocation( origin=WellOrigin.BOTTOM, offset=WellOffset(x=0, y=0, z=7.89), ), diff --git a/shared-data/command/schemas/10.json b/shared-data/command/schemas/10.json index b6bdd71046d..6508269ac62 100644 --- a/shared-data/command/schemas/10.json +++ b/shared-data/command/schemas/10.json @@ -300,7 +300,7 @@ "definitions": { "WellOrigin": { "title": "WellOrigin", - "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", + "description": "Origin of WellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well\n MENISCUS: the meniscus-center of the well", "enum": ["top", "bottom", "center", "meniscus"], "type": "string" }, @@ -344,7 +344,7 @@ }, "volumeOffset": { "title": "Volumeoffset", - "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the volume command parameter.", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset. When \"operationVolume\" is specified, this volume is pulled from the command volume parameter.", "default": 0.0, "anyOf": [ { @@ -933,6 +933,30 @@ }, "required": ["params"] }, + "WellLocation": { + "title": "WellLocation", + "description": "A relative location in reference to a well's location.", + "type": "object", + "properties": { + "origin": { + "default": "top", + "allOf": [ + { + "$ref": "#/definitions/WellOrigin" + } + ] + }, + "offset": { + "$ref": "#/definitions/WellOffset" + }, + "volumeOffset": { + "title": "Volumeoffset", + "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", + "default": 0.0, + "type": "number" + } + } + }, "BlowOutParams": { "title": "BlowOutParams", "description": "Payload required to blow-out a specific well.", @@ -953,7 +977,7 @@ "description": "Relative well location at which to perform the operation", "allOf": [ { - "$ref": "#/definitions/LiquidHandlingWellLocation" + "$ref": "#/definitions/WellLocation" } ] }, @@ -2019,30 +2043,6 @@ }, "required": ["params"] }, - "WellLocation": { - "title": "WellLocation", - "description": "A relative location in reference to a well's location.", - "type": "object", - "properties": { - "origin": { - "default": "top", - "allOf": [ - { - "$ref": "#/definitions/WellOrigin" - } - ] - }, - "offset": { - "$ref": "#/definitions/WellOffset" - }, - "volumeOffset": { - "title": "Volumeoffset", - "description": "A volume of liquid, in \u00b5L, to offset the z-axis offset.", - "default": 0.0, - "type": "number" - } - } - }, "MoveToWellParams": { "title": "MoveToWellParams", "description": "Payload required to move a pipette to a specific well.", @@ -2448,6 +2448,12 @@ }, "required": ["params"] }, + "PickUpTipWellOrigin": { + "title": "PickUpTipWellOrigin", + "description": "The origin of a PickUpTipWellLocation offset.\n\nProps:\n TOP: the top-center of the well\n BOTTOM: the bottom-center of the well\n CENTER: the middle-center of the well", + "enum": ["top", "bottom", "center"], + "type": "string" + }, "PickUpTipWellLocation": { "title": "PickUpTipWellLocation", "description": "A relative location in reference to a well's location.\n\nTo be used for picking up tips.", @@ -2457,7 +2463,7 @@ "default": "top", "allOf": [ { - "$ref": "#/definitions/WellOrigin" + "$ref": "#/definitions/PickUpTipWellOrigin" } ] }, From cedc8b4252194b1b37dadd78a6f908fc82980585 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Tue, 15 Oct 2024 16:59:52 -0400 Subject: [PATCH 42/51] added Papi support for Well.meniscus Location --- .../protocol_api/core/engine/instrument.py | 3 ++ .../opentrons/protocol_api/core/instrument.py | 1 + .../core/legacy/legacy_instrument_core.py | 1 + .../legacy_instrument_core.py | 1 + .../protocol_api/instrument_context.py | 3 ++ api/src/opentrons/protocol_api/labware.py | 2 +- api/src/opentrons/protocol_api/validation.py | 8 +++- api/src/opentrons/types.py | 8 +++- .../protocol_api/test_instrument_context.py | 47 +++++++++++++++++++ .../opentrons/protocol_api/test_validation.py | 15 ++++++ 10 files changed, 86 insertions(+), 3 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index ece3d74ada0..46283810899 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -112,6 +112,7 @@ def aspirate( rate: float, flow_rate: float, in_place: bool, + is_meniscus: Optional[bool] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: @@ -151,6 +152,8 @@ def aspirate( well_name=well_name, absolute_point=location.point, ) + if is_meniscus: + well_location.volumeOffset = "operationVolume" pipette_movement_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, pipette_id=self._pipette_id, diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index 1695f96e5db..b0b00040b1f 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -33,6 +33,7 @@ def aspirate( rate: float, flow_rate: float, in_place: bool, + is_meniscus: Optional[bool] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index a831a9113f2..bb9aee0bf7d 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -80,6 +80,7 @@ def aspirate( rate: float, flow_rate: float, in_place: bool, + is_meniscus: Optional[bool] = None, ) -> None: """Aspirate a given volume of liquid from the specified location. Args: diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index 1471af79fe8..bbf972e31e3 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -91,6 +91,7 @@ def aspirate( rate: float, flow_rate: float, in_place: bool, + is_meniscus: Optional[bool] = None, ) -> None: if self.get_current_volume() == 0: # Make sure we're at the top of the labware and clear of any diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index b158ff8c75f..ca1720f107d 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -217,6 +217,7 @@ def aspirate( ) ) + is_meniscus: Optional[bool] = None well: Optional[labware.Well] = None move_to_location: types.Location last_location = self._get_last_location_by_api_version() @@ -237,6 +238,7 @@ def aspirate( z=self._well_bottom_clearances.aspirate ) well = target.well + is_meniscus = target.is_meniscus if isinstance(target, validation.PointTarget): move_to_location = target.location if isinstance(target, (TrashBin, WasteChute)): @@ -282,6 +284,7 @@ def aspirate( rate=rate, flow_rate=flow_rate, in_place=target.in_place, + is_meniscus=is_meniscus if is_meniscus is not None else None, ) return self diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 6f38c123c7a..300cfbb8a31 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -232,7 +232,7 @@ def meniscus(self, z: float = 0.0) -> Location: :meta private: """ - return Location(self._core.get_meniscus(z_offset=z), self) + return Location(self._core.get_meniscus(z_offset=z), self, True) @requires_version(2, 8) def from_center_cartesian(self, x: float, y: float, z: float) -> Point: diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 43c83eca2e0..79e895424c5 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -418,6 +418,7 @@ class WellTarget(NamedTuple): well: Well location: Optional[Location] in_place: bool + is_meniscus: Optional[bool] = None class PointTarget(NamedTuple): @@ -476,7 +477,12 @@ def validate_location( _, well = target_location.labware.get_parent_labware_and_well() return ( - WellTarget(well=well, location=target_location, in_place=in_place) + WellTarget( + well=well, + location=target_location, + in_place=in_place, + is_meniscus=target_location.is_meniscus, + ) if well is not None else PointTarget(location=target_location, in_place=in_place) ) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index 324b6a23d23..66e670b0386 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -1,7 +1,7 @@ from __future__ import annotations import enum from math import sqrt, isclose -from typing import TYPE_CHECKING, Any, NamedTuple, Iterator, Union, List +from typing import TYPE_CHECKING, Any, NamedTuple, Iterator, Union, List, Optional from opentrons_shared_data.robot.types import RobotType @@ -116,10 +116,12 @@ def __init__( None, "ModuleContext", ], + is_meniscus: Optional[bool] = None, ): self._point = point self._given_labware = labware self._labware = LabwareLike(labware) + self._is_meniscus = is_meniscus # todo(mm, 2021-10-01): Figure out how to get .point and .labware to show up # in the rendered docs, and then update the class docstring to use cross-references. @@ -132,6 +134,10 @@ def point(self) -> Point: def labware(self) -> LabwareLike: return self._labware + @property + def is_meniscus(self) -> Optional[bool]: + return self._is_meniscus + def __iter__(self) -> Iterator[Union[Point, LabwareLike]]: """Iterable interface to support unpacking. Like a tuple. diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 4478c250b8c..e4960d2c26b 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -339,6 +339,7 @@ def test_aspirate( volume=42.0, rate=1.23, flow_rate=5.67, + is_meniscus=None, ), times=1, ) @@ -376,6 +377,49 @@ def test_aspirate_well_location( volume=42.0, rate=1.23, flow_rate=5.67, + is_meniscus=None, + ), + times=1, + ) + + +def test_aspirate_meniscus_well_location( + decoy: Decoy, + mock_instrument_core: InstrumentCore, + subject: InstrumentContext, + mock_protocol_core: ProtocolCore, +) -> None: + """It should aspirate to a well.""" + mock_well = decoy.mock(cls=Well) + input_location = Location(point=Point(2, 2, 2), labware=mock_well, is_meniscus=True) + last_location = Location(point=Point(9, 9, 9), labware=None) + decoy.when(mock_instrument_core.get_mount()).then_return(Mount.RIGHT) + + decoy.when(mock_protocol_core.get_last_location(Mount.RIGHT)).then_return( + last_location + ) + decoy.when( + mock_validation.validate_location( + location=input_location, last_location=last_location + ) + ).then_return( + WellTarget( + well=mock_well, location=input_location, in_place=False, is_meniscus=True + ) + ) + decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) + + subject.aspirate(volume=42.0, location=input_location, rate=1.23) + + decoy.verify( + mock_instrument_core.aspirate( + location=input_location, + well_core=mock_well._core, + in_place=False, + volume=42.0, + rate=1.23, + flow_rate=5.67, + is_meniscus=True, ), times=1, ) @@ -412,6 +456,7 @@ def test_aspirate_from_coordinates( volume=42.0, rate=1.23, flow_rate=5.67, + is_meniscus=None, ), times=1, ) @@ -1325,6 +1370,7 @@ def test_aspirate_0_volume_means_aspirate_everything( volume=200, rate=1.23, flow_rate=5.67, + is_meniscus=None, ), times=1, ) @@ -1364,6 +1410,7 @@ def test_aspirate_0_volume_means_aspirate_nothing( volume=0, rate=1.23, flow_rate=5.67, + is_meniscus=None, ), times=1, ) diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 2a2ed6375b0..969a66accf2 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -489,6 +489,21 @@ def test_validate_location_with_well(decoy: Decoy) -> None: assert result == expected_result +def test_validate_location_with_meniscus_well(decoy: Decoy) -> None: + """Should return a WellTarget with location.""" + mock_well = decoy.mock(cls=Well) + input_location = Location( + point=Point(x=1, y=1, z=1), labware=mock_well, is_meniscus=True + ) + expected_result = subject.WellTarget( + well=mock_well, location=input_location, in_place=False, is_meniscus=True + ) + + result = subject.validate_location(location=input_location, last_location=None) + + assert result == expected_result + + def test_validate_last_location(decoy: Decoy) -> None: """Should return a WellTarget with location.""" mock_well = decoy.mock(cls=Well) From 7fe0451c96e779d6f4c61a8376782e053fc50823 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 16 Oct 2024 15:01:39 -0400 Subject: [PATCH 43/51] reduce PE constraints --- .../protocol_engine/commands/dispense.py | 7 +---- .../protocol_engine/commands/move_to_well.py | 9 ++++--- .../commands/test_move_to_well.py | 27 ------------------- 3 files changed, 6 insertions(+), 37 deletions(-) diff --git a/api/src/opentrons/protocol_engine/commands/dispense.py b/api/src/opentrons/protocol_engine/commands/dispense.py index cd00d50df3b..7e18cc6560b 100644 --- a/api/src/opentrons/protocol_engine/commands/dispense.py +++ b/api/src/opentrons/protocol_engine/commands/dispense.py @@ -83,12 +83,7 @@ async def execute(self, params: DispenseParams) -> _ExecuteReturn: well_name = params.wellName volume = params.volume - self._state_view.geometry.validate_dispense_volume_into_well( - labware_id=labware_id, - well_name=well_name, - well_location=well_location, - volume=volume, - ) + # TODO(pbm, 10-15-24): call self._state_view.geometry.validate_dispense_volume_into_well() position = await self._movement.move_to_well( pipette_id=params.pipetteId, diff --git a/api/src/opentrons/protocol_engine/commands/move_to_well.py b/api/src/opentrons/protocol_engine/commands/move_to_well.py index 5b5f32a1f95..309f2e89513 100644 --- a/api/src/opentrons/protocol_engine/commands/move_to_well.py +++ b/api/src/opentrons/protocol_engine/commands/move_to_well.py @@ -3,7 +3,7 @@ from typing import TYPE_CHECKING, Optional, Type from typing_extensions import Literal -from ..types import DeckPoint, WellOrigin +from ..types import DeckPoint from .pipetting_common import ( PipetteIdMixin, WellLocationMixin, @@ -56,11 +56,12 @@ async def execute( state_update = update_types.StateUpdate() - if (self._state_view.labware.is_tiprack(labware_id)) and ( - well_location.origin == WellOrigin.MENISCUS or well_location.volumeOffset + if ( + self._state_view.labware.is_tiprack(labware_id) + and well_location.volumeOffset ): raise LabwareIsTipRackError( - "Cannot specify a WellLocation with an origin of MENISCUS or any volumeOffset with movement to a tip rack" + "Cannot specify a WellLocation with a volumeOffset with movement to a tip rack" ) x, y, z = await self._movement.move_to_well( diff --git a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py index 73ef22e04c6..1b01009dc0e 100644 --- a/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py +++ b/api/tests/opentrons/protocol_engine/commands/test_move_to_well.py @@ -4,7 +4,6 @@ from opentrons.protocol_engine import ( WellLocation, - WellOrigin, WellOffset, DeckPoint, errors, @@ -73,32 +72,6 @@ async def test_move_to_well_implementation( ) -async def test_move_to_well_with_tip_rack_and_meniscus( - decoy: Decoy, - mock_state_view: StateView, - movement: MovementHandler, -) -> None: - """It should disallow movement to a tip rack when MENISCUS is specified.""" - subject = MoveToWellImplementation(state_view=mock_state_view, movement=movement) - - data = MoveToWellParams( - pipetteId="abc", - labwareId="123", - wellName="A3", - wellLocation=WellLocation( - origin=WellOrigin.MENISCUS, offset=WellOffset(x=1, y=2, z=3) - ), - forceDirect=True, - minimumZHeight=4.56, - speed=7.89, - ) - - decoy.when(mock_state_view.labware.is_tiprack("123")).then_return(True) - - with pytest.raises(errors.LabwareIsTipRackError): - await subject.execute(data) - - async def test_move_to_well_with_tip_rack_and_volume_offset( decoy: Decoy, mock_state_view: StateView, From fc0572f6da8ebb4018740920d34960a02af2b0a4 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Wed, 16 Oct 2024 15:02:39 -0400 Subject: [PATCH 44/51] Papi refactor to eliminate calculations prior to PE execution --- .../protocol_api/core/engine/instrument.py | 5 +++-- .../opentrons/protocol_api/core/instrument.py | 1 + .../core/legacy/legacy_instrument_core.py | 1 + .../legacy_simulator/legacy_instrument_core.py | 1 + .../protocol_api/instrument_context.py | 3 +++ api/src/opentrons/protocol_api/labware.py | 6 ++---- .../protocol_engine/state/geometry.py | 18 ++++++++++++------ .../core/engine/test_instrument_core.py | 10 ++++++++-- api/tests/opentrons/protocol_api/test_well.py | 5 ++--- 9 files changed, 33 insertions(+), 17 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/instrument.py b/api/src/opentrons/protocol_api/core/engine/instrument.py index 46283810899..4474a174a85 100644 --- a/api/src/opentrons/protocol_api/core/engine/instrument.py +++ b/api/src/opentrons/protocol_api/core/engine/instrument.py @@ -151,9 +151,8 @@ def aspirate( labware_id=labware_id, well_name=well_name, absolute_point=location.point, + is_meniscus=is_meniscus, ) - if is_meniscus: - well_location.volumeOffset = "operationVolume" pipette_movement_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, pipette_id=self._pipette_id, @@ -183,6 +182,7 @@ def dispense( flow_rate: float, in_place: bool, push_out: Optional[float], + is_meniscus: Optional[bool] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: @@ -242,6 +242,7 @@ def dispense( labware_id=labware_id, well_name=well_name, absolute_point=location.point, + is_meniscus=is_meniscus, ) pipette_movement_conflict.check_safe_for_pipette_movement( engine_state=self._engine_client.state, diff --git a/api/src/opentrons/protocol_api/core/instrument.py b/api/src/opentrons/protocol_api/core/instrument.py index b0b00040b1f..7d1816e1044 100644 --- a/api/src/opentrons/protocol_api/core/instrument.py +++ b/api/src/opentrons/protocol_api/core/instrument.py @@ -56,6 +56,7 @@ def dispense( flow_rate: float, in_place: bool, push_out: Optional[float], + is_meniscus: Optional[bool] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py index bb9aee0bf7d..ed1e0d607c9 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_instrument_core.py @@ -123,6 +123,7 @@ def dispense( flow_rate: float, in_place: bool, push_out: Optional[float], + is_meniscus: Optional[bool] = None, ) -> None: """Dispense a given volume of liquid into the specified location. Args: diff --git a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py index bbf972e31e3..55bde6c0a75 100644 --- a/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py +++ b/api/src/opentrons/protocol_api/core/legacy_simulator/legacy_instrument_core.py @@ -133,6 +133,7 @@ def dispense( flow_rate: float, in_place: bool, push_out: Optional[float], + is_meniscus: Optional[bool] = None, ) -> None: if isinstance(location, (TrashBin, WasteChute)): raise APIVersionError( diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index ca1720f107d..0357adfbfb5 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -387,6 +387,7 @@ def dispense( # noqa: C901 ) ) well: Optional[labware.Well] = None + is_meniscus: Optional[bool] = None last_location = self._get_last_location_by_api_version() try: @@ -405,6 +406,7 @@ def dispense( # noqa: C901 well = target.well if target.location: move_to_location = target.location + is_meniscus = target.is_meniscus elif well.parent._core.is_fixed_trash(): move_to_location = target.well.top() else: @@ -449,6 +451,7 @@ def dispense( # noqa: C901 flow_rate=flow_rate, in_place=False, push_out=push_out, + is_meniscus=is_meniscus if is_meniscus is not None else None, ) return self diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 300cfbb8a31..c45aa3eaa3b 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -226,13 +226,11 @@ def meniscus(self, z: float = 0.0) -> Location: """ :param z: An offset on the z-axis, in mm. Positive offsets are higher and negative offsets are lower. - :return: A :py:class:`~opentrons.types.Location` corresponding to the - absolute position of the meniscus-center of the well, plus the ``z`` offset - (if specified). + :return: A :py:class:`~opentrons.types.Location` that holds the ``z`` offset in its point.z field. :meta private: """ - return Location(self._core.get_meniscus(z_offset=z), self, True) + return Location(point=Point(x=0, y=0, z=z), labware=self, is_meniscus=True) @requires_version(2, 8) def from_center_cartesian(self, x: float, y: float, z: float) -> Point: diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index 42ef50d110b..f65fff2dd9d 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -527,14 +527,20 @@ def get_relative_liquid_handling_well_location( labware_id: str, well_name: str, absolute_point: Point, + is_meniscus: Optional[bool] = None, ) -> LiquidHandlingWellLocation: """Given absolute position, get relative location of a well in a labware.""" - well_absolute_point = self.get_well_position(labware_id, well_name) - delta = absolute_point - well_absolute_point - - return LiquidHandlingWellLocation( - offset=WellOffset(x=delta.x, y=delta.y, z=delta.z) - ) + if is_meniscus: + return LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset(x=0, y=0, z=absolute_point.z), + ) + else: + well_absolute_point = self.get_well_position(labware_id, well_name) + delta = absolute_point - well_absolute_point + return LiquidHandlingWellLocation( + offset=WellOffset(x=delta.x, y=delta.y, z=delta.z) + ) def get_relative_pick_up_tip_well_location( self, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py index 3883d150067..9952e0166e9 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_instrument_core.py @@ -499,7 +499,10 @@ def test_aspirate_from_well( decoy.when( mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( - labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) + labware_id="123abc", + well_name="my cool well", + absolute_point=Point(1, 2, 3), + is_meniscus=None, ) ).then_return( LiquidHandlingWellLocation( @@ -727,7 +730,10 @@ def test_dispense_to_well( decoy.when( mock_engine_client.state.geometry.get_relative_liquid_handling_well_location( - labware_id="123abc", well_name="my cool well", absolute_point=Point(1, 2, 3) + labware_id="123abc", + well_name="my cool well", + absolute_point=Point(1, 2, 3), + is_meniscus=None, ) ).then_return( LiquidHandlingWellLocation( diff --git a/api/tests/opentrons/protocol_api/test_well.py b/api/tests/opentrons/protocol_api/test_well.py index 3a2ba81b9fa..ef1eed84c62 100644 --- a/api/tests/opentrons/protocol_api/test_well.py +++ b/api/tests/opentrons/protocol_api/test_well.py @@ -103,12 +103,11 @@ def test_well_center(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> N def test_well_meniscus(decoy: Decoy, mock_well_core: WellCore, subject: Well) -> None: """It should get a Location representing the meniscus of the well.""" - decoy.when(mock_well_core.get_meniscus(z_offset=4.2)).then_return(Point(1, 2, 3)) - result = subject.meniscus(4.2) assert isinstance(result, Location) - assert result.point == Point(1, 2, 3) + assert result.point == Point(0, 0, 4.2) + assert result.is_meniscus is True assert result.labware.as_well() is subject From 81da6d01cd21650e2f97772167165fdcf54f3931 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 12:43:56 -0400 Subject: [PATCH 45/51] fixed Papi to not calculate meniscus location prior to PE execution --- .../protocol_api/instrument_context.py | 41 +++++++++++++------ api/src/opentrons/protocol_api/validation.py | 7 ++-- api/src/opentrons/types.py | 3 +- .../protocol_api/test_instrument_context.py | 11 ++--- .../opentrons/protocol_api/test_validation.py | 15 ------- api/tests/opentrons/test_types.py | 9 ++-- 6 files changed, 46 insertions(+), 40 deletions(-) diff --git a/api/src/opentrons/protocol_api/instrument_context.py b/api/src/opentrons/protocol_api/instrument_context.py index 0357adfbfb5..880626b53c9 100644 --- a/api/src/opentrons/protocol_api/instrument_context.py +++ b/api/src/opentrons/protocol_api/instrument_context.py @@ -217,9 +217,9 @@ def aspirate( ) ) - is_meniscus: Optional[bool] = None - well: Optional[labware.Well] = None move_to_location: types.Location + well: Optional[labware.Well] = None + is_meniscus: Optional[bool] = None last_location = self._get_last_location_by_api_version() try: target = validation.validate_location( @@ -233,18 +233,13 @@ def aspirate( "knows where it is." ) from e - if isinstance(target, validation.WellTarget): - move_to_location = target.location or target.well.bottom( - z=self._well_bottom_clearances.aspirate - ) - well = target.well - is_meniscus = target.is_meniscus - if isinstance(target, validation.PointTarget): - move_to_location = target.location if isinstance(target, (TrashBin, WasteChute)): raise ValueError( "Trash Bin and Waste Chute are not acceptable location parameters for Aspirate commands." ) + move_to_location, well, is_meniscus = self._handle_aspirate_target( + target=target + ) if self.api_version >= APIVersion(2, 11): instrument.validate_takes_liquid( location=move_to_location, @@ -284,7 +279,7 @@ def aspirate( rate=rate, flow_rate=flow_rate, in_place=target.in_place, - is_meniscus=is_meniscus if is_meniscus is not None else None, + is_meniscus=is_meniscus, ) return self @@ -406,7 +401,7 @@ def dispense( # noqa: C901 well = target.well if target.location: move_to_location = target.location - is_meniscus = target.is_meniscus + is_meniscus = target.location.is_meniscus elif well.parent._core.is_fixed_trash(): move_to_location = target.well.top() else: @@ -451,7 +446,6 @@ def dispense( # noqa: C901 flow_rate=flow_rate, in_place=False, push_out=push_out, - is_meniscus=is_meniscus if is_meniscus is not None else None, ) return self @@ -473,6 +467,7 @@ def dispense( # noqa: C901 flow_rate=flow_rate, in_place=target.in_place, push_out=push_out, + is_meniscus=is_meniscus, ) return self @@ -2197,6 +2192,26 @@ def _raise_if_configuration_not_supported_by_pipette( ) # SINGLE, QUADRANT and ALL are supported by all pipettes + def _handle_aspirate_target( + self, target: validation.ValidTarget + ) -> tuple[types.Location, Optional[labware.Well], Optional[bool]]: + move_to_location: types.Location + well: Optional[labware.Well] = None + is_meniscus: Optional[bool] = None + if isinstance(target, validation.WellTarget): + well = target.well + if target.location: + move_to_location = target.location + is_meniscus = target.location.is_meniscus + + else: + move_to_location = target.well.bottom( + z=self._well_bottom_clearances.aspirate + ) + if isinstance(target, validation.PointTarget): + move_to_location = target.location + return (move_to_location, well, is_meniscus) + class AutoProbeDisable: """Use this class to temporarily disable automatic liquid presence detection.""" diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index 79e895424c5..dc12165dced 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -418,7 +418,6 @@ class WellTarget(NamedTuple): well: Well location: Optional[Location] in_place: bool - is_meniscus: Optional[bool] = None class PointTarget(NamedTuple): @@ -436,10 +435,13 @@ class LocationTypeError(TypeError): """Error representing that the location supplied is of different expected type.""" +ValidTarget = Union[WellTarget, PointTarget, TrashBin, WasteChute] + + def validate_location( location: Union[Location, Well, TrashBin, WasteChute, None], last_location: Optional[Location], -) -> Union[WellTarget, PointTarget, TrashBin, WasteChute]: +) -> ValidTarget: """Validate a given location for a liquid handling command. Args: @@ -481,7 +483,6 @@ def validate_location( well=well, location=target_location, in_place=in_place, - is_meniscus=target_location.is_meniscus, ) if well is not None else PointTarget(location=target_location, in_place=in_place) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index 66e670b0386..bcb0d543d51 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -154,6 +154,7 @@ def __eq__(self, other: object) -> bool: isinstance(other, Location) and other._point == self._point and other._labware == self._labware + and other._is_meniscus == self._is_meniscus ) def move(self, point: Point) -> "Location": @@ -179,7 +180,7 @@ def move(self, point: Point) -> "Location": return Location(point=self.point + point, labware=self._given_labware) def __repr__(self) -> str: - return f"Location(point={repr(self._point)}, labware={self._labware})" + return f"Location(point={repr(self._point)}, labware={self._labware}, is_meniscus={self._is_meniscus if self._is_meniscus is not None else False})" # TODO(mc, 2020-10-22): use MountType implementation for Mount diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index e4960d2c26b..0697b2ddc8a 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -402,11 +402,7 @@ def test_aspirate_meniscus_well_location( mock_validation.validate_location( location=input_location, last_location=last_location ) - ).then_return( - WellTarget( - well=mock_well, location=input_location, in_place=False, is_meniscus=True - ) - ) + ).then_return(WellTarget(well=mock_well, location=input_location, in_place=False)) decoy.when(mock_instrument_core.get_aspirate_flow_rate(1.23)).then_return(5.67) subject.aspirate(volume=42.0, location=input_location, rate=1.23) @@ -970,6 +966,7 @@ def test_dispense_with_location( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1008,6 +1005,7 @@ def test_dispense_with_well_location( rate=1.23, flow_rate=3.0, push_out=7, + is_meniscus=None, ), times=1, ) @@ -1048,6 +1046,7 @@ def test_dispense_with_well( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1302,6 +1301,7 @@ def test_dispense_0_volume_means_dispense_everything( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) @@ -1331,6 +1331,7 @@ def test_dispense_0_volume_means_dispense_nothing( rate=1.23, flow_rate=5.67, push_out=None, + is_meniscus=None, ), times=1, ) diff --git a/api/tests/opentrons/protocol_api/test_validation.py b/api/tests/opentrons/protocol_api/test_validation.py index 969a66accf2..2a2ed6375b0 100644 --- a/api/tests/opentrons/protocol_api/test_validation.py +++ b/api/tests/opentrons/protocol_api/test_validation.py @@ -489,21 +489,6 @@ def test_validate_location_with_well(decoy: Decoy) -> None: assert result == expected_result -def test_validate_location_with_meniscus_well(decoy: Decoy) -> None: - """Should return a WellTarget with location.""" - mock_well = decoy.mock(cls=Well) - input_location = Location( - point=Point(x=1, y=1, z=1), labware=mock_well, is_meniscus=True - ) - expected_result = subject.WellTarget( - well=mock_well, location=input_location, in_place=False, is_meniscus=True - ) - - result = subject.validate_location(location=input_location, last_location=None) - - assert result == expected_result - - def test_validate_last_location(decoy: Decoy) -> None: """Should return a WellTarget with location.""" mock_well = decoy.mock(cls=Well) diff --git a/api/tests/opentrons/test_types.py b/api/tests/opentrons/test_types.py index 6cd93dce125..77249fa0492 100644 --- a/api/tests/opentrons/test_types.py +++ b/api/tests/opentrons/test_types.py @@ -29,7 +29,7 @@ def test_location_repr_labware(min_lw: Labware) -> None: loc = Location(point=Point(x=1.1, y=2.1, z=3.5), labware=min_lw) assert ( f"{loc}" - == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck)" + == "Location(point=Point(x=1.1, y=2.1, z=3.5), labware=minimal labware on deck, is_meniscus=False)" ) @@ -38,14 +38,17 @@ def test_location_repr_well(min_lw: Labware) -> None: loc = Location(point=Point(x=1, y=2, z=3), labware=min_lw.wells()[0]) assert ( f"{loc}" - == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck)" + == "Location(point=Point(x=1, y=2, z=3), labware=A1 of minimal labware on deck, is_meniscus=False)" ) def test_location_repr_slot() -> None: """It should represent labware as a slot""" loc = Location(point=Point(x=-1, y=2, z=3), labware="1") - assert f"{loc}" == "Location(point=Point(x=-1, y=2, z=3), labware=1)" + assert ( + f"{loc}" + == "Location(point=Point(x=-1, y=2, z=3), labware=1, is_meniscus=False)" + ) @pytest.mark.parametrize( From b9aba1f379322bb2967d62cd3cb0420f356870bb Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 12:44:33 -0400 Subject: [PATCH 46/51] fixed simulation/analysis --- api/src/opentrons/protocol_engine/execution/pipetting.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/api/src/opentrons/protocol_engine/execution/pipetting.py b/api/src/opentrons/protocol_engine/execution/pipetting.py index ae35e779761..2964f02d183 100644 --- a/api/src/opentrons/protocol_engine/execution/pipetting.py +++ b/api/src/opentrons/protocol_engine/execution/pipetting.py @@ -288,8 +288,8 @@ async def liquid_probe_in_place( well_location: WellLocation, ) -> float: """Detect liquid level.""" - # TODO (pm, 6-18-24): return a value of worth if needed - return 0.0 + well_def = self._state_view.labware.get_well_definition(labware_id, well_name) + return well_def.depth def _validate_tip_attached(self, pipette_id: str, command_name: str) -> None: """Validate if there is a tip attached.""" From 618e92fc8b7ca5e1b60ac7f42441eb1c71087de4 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 15:52:57 -0400 Subject: [PATCH 47/51] Hid Location.is_meniscus from docs --- api/src/opentrons/protocol_api/labware.py | 4 +++- api/src/opentrons/types.py | 9 ++++++--- .../opentrons/protocol_api/test_instrument_context.py | 4 +++- 3 files changed, 12 insertions(+), 5 deletions(-) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index c45aa3eaa3b..939cedb07b4 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -230,7 +230,9 @@ def meniscus(self, z: float = 0.0) -> Location: :meta private: """ - return Location(point=Point(x=0, y=0, z=z), labware=self, is_meniscus=True) + return Location( + point=Point(x=0, y=0, z=z), labware=self, _ot_internal_is_meniscus=True + ) @requires_version(2, 8) def from_center_cartesian(self, x: float, y: float, z: float) -> Point: diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index bcb0d543d51..37d04aa30bc 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -79,7 +79,9 @@ def magnitude_to(self, other: Any) -> float: class Location: - """A location to target as a motion. + """Location(point: Point, labware: Union["Labware", "Well", str, "ModuleGeometry", LabwareLike, None, "ModuleContext"]) + + A location to target as a motion. The location contains a :py:class:`.Point` (in :ref:`protocol-api-deck-coords`) and possibly an associated @@ -116,12 +118,13 @@ def __init__( None, "ModuleContext", ], - is_meniscus: Optional[bool] = None, + *, + _ot_internal_is_meniscus: Optional[bool] = None, ): self._point = point self._given_labware = labware self._labware = LabwareLike(labware) - self._is_meniscus = is_meniscus + self._is_meniscus = _ot_internal_is_meniscus # todo(mm, 2021-10-01): Figure out how to get .point and .labware to show up # in the rendered docs, and then update the class docstring to use cross-references. diff --git a/api/tests/opentrons/protocol_api/test_instrument_context.py b/api/tests/opentrons/protocol_api/test_instrument_context.py index 0697b2ddc8a..069330036ec 100644 --- a/api/tests/opentrons/protocol_api/test_instrument_context.py +++ b/api/tests/opentrons/protocol_api/test_instrument_context.py @@ -391,7 +391,9 @@ def test_aspirate_meniscus_well_location( ) -> None: """It should aspirate to a well.""" mock_well = decoy.mock(cls=Well) - input_location = Location(point=Point(2, 2, 2), labware=mock_well, is_meniscus=True) + input_location = Location( + point=Point(2, 2, 2), labware=mock_well, _ot_internal_is_meniscus=True + ) last_location = Location(point=Point(9, 9, 9), labware=None) decoy.when(mock_instrument_core.get_mount()).then_return(Mount.RIGHT) From fb8a20c72208125cc3e2c9d6f324be287736d544 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 16:41:17 -0400 Subject: [PATCH 48/51] added geometry test --- api/src/opentrons/types.py | 2 +- .../state/test_geometry_view.py | 25 +++++++++++++++++++ 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/api/src/opentrons/types.py b/api/src/opentrons/types.py index 37d04aa30bc..22611393f40 100644 --- a/api/src/opentrons/types.py +++ b/api/src/opentrons/types.py @@ -80,7 +80,7 @@ def magnitude_to(self, other: Any) -> float: class Location: """Location(point: Point, labware: Union["Labware", "Well", str, "ModuleGeometry", LabwareLike, None, "ModuleContext"]) - + A location to target as a motion. The location contains a :py:class:`.Point` (in diff --git a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py index 070eeb229bb..7a94f06ca09 100644 --- a/api/tests/opentrons/protocol_engine/state/test_geometry_view.py +++ b/api/tests/opentrons/protocol_engine/state/test_geometry_view.py @@ -1808,6 +1808,31 @@ def test_get_relative_well_location( ) +def test_get_relative_liquid_handling_well_location( + decoy: Decoy, + well_plate_def: LabwareDefinition, + mock_labware_view: LabwareView, + mock_addressable_area_view: AddressableAreaView, + subject: GeometryView, +) -> None: + """It should get the relative location of a well given an absolute position.""" + result = subject.get_relative_liquid_handling_well_location( + labware_id="labware-id", + well_name="B2", + absolute_point=Point(x=0, y=0, z=-2), + is_meniscus=True, + ) + + assert result == LiquidHandlingWellLocation( + origin=WellOrigin.MENISCUS, + offset=WellOffset.construct( + x=0.0, + y=0.0, + z=cast(float, pytest.approx(-2)), + ), + ) + + def test_get_nominal_effective_tip_length( decoy: Decoy, mock_labware_view: LabwareView, From 6feaff0e909a2f719100280cd26a8a79b142356c Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 16:54:08 -0400 Subject: [PATCH 49/51] tidied up --- api/src/opentrons/protocol_api/labware.py | 2 +- api/src/opentrons/protocol_api/validation.py | 6 +----- api/src/opentrons/protocol_engine/state/geometry.py | 5 ++++- 3 files changed, 6 insertions(+), 7 deletions(-) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index 939cedb07b4..0e8a17d07d3 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -226,7 +226,7 @@ def meniscus(self, z: float = 0.0) -> Location: """ :param z: An offset on the z-axis, in mm. Positive offsets are higher and negative offsets are lower. - :return: A :py:class:`~opentrons.types.Location` that holds the ``z`` offset in its point.z field. + :return: A :py:class:`~opentrons.types.Location` that indicates location is meniscus and that holds the ``z`` offset in its point.z field. :meta private: """ diff --git a/api/src/opentrons/protocol_api/validation.py b/api/src/opentrons/protocol_api/validation.py index dc12165dced..630211e9ac6 100644 --- a/api/src/opentrons/protocol_api/validation.py +++ b/api/src/opentrons/protocol_api/validation.py @@ -479,11 +479,7 @@ def validate_location( _, well = target_location.labware.get_parent_labware_and_well() return ( - WellTarget( - well=well, - location=target_location, - in_place=in_place, - ) + WellTarget(well=well, location=target_location, in_place=in_place) if well is not None else PointTarget(location=target_location, in_place=in_place) ) diff --git a/api/src/opentrons/protocol_engine/state/geometry.py b/api/src/opentrons/protocol_engine/state/geometry.py index f65fff2dd9d..125be3339a9 100644 --- a/api/src/opentrons/protocol_engine/state/geometry.py +++ b/api/src/opentrons/protocol_engine/state/geometry.py @@ -529,7 +529,10 @@ def get_relative_liquid_handling_well_location( absolute_point: Point, is_meniscus: Optional[bool] = None, ) -> LiquidHandlingWellLocation: - """Given absolute position, get relative location of a well in a labware.""" + """Given absolute position, get relative location of a well in a labware. + + If is_meniscus is True, absolute_point will hold the z-offset in its z field. + """ if is_meniscus: return LiquidHandlingWellLocation( origin=WellOrigin.MENISCUS, From 936b91c82474f465fdd5e3f08a9d2a68b72973fb Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 17:45:40 -0400 Subject: [PATCH 50/51] addressed TODO to more appropriately name exceptions --- .../protocol_engine/errors/__init__.py | 6 ++++-- .../protocol_engine/errors/exceptions.py | 19 ++++++++++++++++--- .../protocol_engine/state/labware.py | 13 ++++++------- 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index ee4447b38bf..304f7db1fff 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -71,7 +71,8 @@ CommandNotAllowedError, InvalidLiquidHeightFound, LiquidHeightUnknownError, - InvalidWellDefinitionError, + IncompleteLabwareDefinitionError, + IncompleteWellDefinitionError, OperationLocationNotInWellError, InvalidDispenseVolumeError, ) @@ -153,7 +154,8 @@ "CommandNotAllowedError", "InvalidLiquidHeightFound", "LiquidHeightUnknownError", - "InvalidWellDefinitionError", + "IncompleteLabwareDefinitionError", + "IncompleteWellDefinitionError", "OperationLocationNotInWellError", "InvalidDispenseVolumeError", ] diff --git a/api/src/opentrons/protocol_engine/errors/exceptions.py b/api/src/opentrons/protocol_engine/errors/exceptions.py index dad964a92c3..27987e5d91e 100644 --- a/api/src/opentrons/protocol_engine/errors/exceptions.py +++ b/api/src/opentrons/protocol_engine/errors/exceptions.py @@ -1071,8 +1071,8 @@ def __init__( super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) -class InvalidWellDefinitionError(ProtocolEngineError): - """Raised when an InnerWellGeometry definition is invalid.""" +class IncompleteLabwareDefinitionError(ProtocolEngineError): + """Raised when a labware definition lacks innerLabwareGeometry in general or for a specific well_id.""" def __init__( self, @@ -1080,7 +1080,20 @@ def __init__( details: Optional[Dict[str, Any]] = None, wrapping: Optional[Sequence[EnumeratedError]] = None, ) -> None: - """Build an InvalidWellDefinitionError.""" + """Build an IncompleteLabwareDefinitionError.""" + super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) + + +class IncompleteWellDefinitionError(ProtocolEngineError): + """Raised when a well definition lacks a geometryDefinitionId.""" + + def __init__( + self, + message: Optional[str] = None, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build an IncompleteWellDefinitionError.""" super().__init__(ErrorCodes.GENERAL_ERROR, message, details, wrapping) diff --git a/api/src/opentrons/protocol_engine/state/labware.py b/api/src/opentrons/protocol_engine/state/labware.py index 907c9d20e2e..b852311163b 100644 --- a/api/src/opentrons/protocol_engine/state/labware.py +++ b/api/src/opentrons/protocol_engine/state/labware.py @@ -500,23 +500,22 @@ def get_well_geometry( self, labware_id: str, well_name: Optional[str] = None ) -> InnerWellGeometry: """Get a well's inner geometry by labware and well name.""" - # TODO(pbm, 10-11-24): wordsmith this error labware_def = self.get_definition(labware_id) if labware_def.innerLabwareGeometry is None: - raise errors.InvalidWellDefinitionError( - message=f"No innerLabwareGeometry found for labware_id: {labware_id}." + raise errors.IncompleteLabwareDefinitionError( + message=f"No innerLabwareGeometry found in labware definition for labware_id: {labware_id}." ) well_def = self.get_well_definition(labware_id, well_name) well_id = well_def.geometryDefinitionId if well_id is None: - raise errors.InvalidWellDefinitionError( - message=f"No geometryDefinitionId found for well: {well_name} in labware_id: {labware_id}" + raise errors.IncompleteWellDefinitionError( + message=f"No geometryDefinitionId found in well definition for well: {well_name} in labware_id: {labware_id}" ) else: well_geometry = labware_def.innerLabwareGeometry.get(well_id) if well_geometry is None: - raise errors.InvalidWellDefinitionError( - message=f"No innerLabwareGeometry found for well_id: {well_id} in labware_id: {labware_id}" + raise errors.IncompleteLabwareDefinitionError( + message=f"No innerLabwareGeometry found in labware definition for well_id: {well_id} in labware_id: {labware_id}" ) return well_geometry From 665e8154e22750cf42620e59b0ae4b092a3400b0 Mon Sep 17 00:00:00 2001 From: pmoegenburg Date: Thu, 17 Oct 2024 18:38:23 -0400 Subject: [PATCH 51/51] eliminated unneeded method --- .../opentrons/protocol_api/core/engine/well.py | 11 ----------- .../core/legacy/legacy_well_core.py | 4 ---- api/src/opentrons/protocol_api/core/well.py | 4 ---- .../protocol_api/core/engine/test_well_core.py | 17 ----------------- 4 files changed, 36 deletions(-) diff --git a/api/src/opentrons/protocol_api/core/engine/well.py b/api/src/opentrons/protocol_api/core/engine/well.py index ec7307a6a90..6743a8a39c5 100644 --- a/api/src/opentrons/protocol_api/core/engine/well.py +++ b/api/src/opentrons/protocol_api/core/engine/well.py @@ -125,17 +125,6 @@ def get_center(self) -> Point: well_location=WellLocation(origin=WellOrigin.CENTER), ) - def get_meniscus(self, z_offset: float) -> Point: - """Get the coordinate of the well's meniscus, with a z-offset.""" - return self._engine_client.state.geometry.get_well_position( - well_name=self._name, - labware_id=self._labware_id, - well_location=WellLocation( - origin=WellOrigin.MENISCUS, - offset=WellOffset(x=0, y=0, z=z_offset), - ), - ) - def load_liquid( self, liquid: Liquid, diff --git a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py index f37aefbd4be..a88dd2eee80 100644 --- a/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py +++ b/api/src/opentrons/protocol_api/core/legacy/legacy_well_core.py @@ -106,10 +106,6 @@ def get_center(self) -> Point: """Get the coordinate of the well's center.""" return self._geometry.center() - def get_meniscus(self, z_offset: float) -> Point: - """This will never be called because it was added in API 2.21.""" - assert False, "get_meniscus only supported in API 2.21 & later" - def load_liquid( self, liquid: Liquid, diff --git a/api/src/opentrons/protocol_api/core/well.py b/api/src/opentrons/protocol_api/core/well.py index 81dddede2f1..bd58963a59c 100644 --- a/api/src/opentrons/protocol_api/core/well.py +++ b/api/src/opentrons/protocol_api/core/well.py @@ -71,10 +71,6 @@ def get_bottom(self, z_offset: float) -> Point: def get_center(self) -> Point: """Get the coordinate of the well's center.""" - @abstractmethod - def get_meniscus(self, z_offset: float) -> Point: - """Get the coordinate of the well's meniscus, with an z-offset.""" - @abstractmethod def load_liquid( self, diff --git a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py index 96efbbdde8d..31b562f7e81 100644 --- a/api/tests/opentrons/protocol_api/core/engine/test_well_core.py +++ b/api/tests/opentrons/protocol_api/core/engine/test_well_core.py @@ -149,23 +149,6 @@ def test_get_center( assert subject.get_center() == Point(1, 2, 3) -def test_get_meniscus( - decoy: Decoy, mock_engine_client: EngineClient, subject: WellCore -) -> None: - """It should get a well bottom.""" - decoy.when( - mock_engine_client.state.geometry.get_well_position( - labware_id="labware-id", - well_name="well-name", - well_location=WellLocation( - origin=WellOrigin.MENISCUS, offset=WellOffset(x=0, y=0, z=2.5) - ), - ) - ).then_return(Point(1, 2, 3)) - - assert subject.get_meniscus(z_offset=2.5) == Point(1, 2, 3) - - def test_has_tip( decoy: Decoy, mock_engine_client: EngineClient, subject: WellCore ) -> None: