diff --git a/.github/workflows/components-test-build-deploy.yaml b/.github/workflows/components-test-build-deploy.yaml index 60d3f19fc4e..d27fc1b5686 100644 --- a/.github/workflows/components-test-build-deploy.yaml +++ b/.github/workflows/components-test-build-deploy.yaml @@ -69,7 +69,7 @@ jobs: files: ./coverage/lcov.info flags: components - build-components: + build-components-storybook: name: 'build components artifact' runs-on: 'ubuntu-22.04' if: github.event_name != 'pull_request' @@ -102,11 +102,32 @@ jobs: with: name: 'components-artifact' path: storybook-static + + determine-build-type: + runs-on: 'ubuntu-latest' + name: 'Determine build type' + outputs: + type: ${{steps.determine-build-type.outputs.type}} + steps: + - id: determine-build-type + run: | + echo "Determining build type for event ${{github.event_type}} and ref ${{github.ref}}" + if [ "${{ format('{0}', github.ref == 'refs/heads/edge') }}" = "true" ] ; then + echo "storybook s3 builds for edge" + echo 'type=storybook' >> $GITHUB_OUTPUT + elif [ "${{ format('{0}', startsWith(github.ref, 'refs/tags/components')) }}" = "true" ] ; then + echo "publish builds for components tags" + echo 'type=publish' >> $GITHUB_OUTPUT + else + echo "No build for ref ${{github.ref}} and event ${{github.event_type}}" + echo 'type=none' >> $GITHUB_OUTPUT + fi + deploy-components: - name: 'deploy components artifact to S3' + name: 'deploy components storybook artifact to S3' runs-on: 'ubuntu-22.04' - needs: ['js-unit-test', 'build-components'] - if: github.event_name != 'pull_request' + needs: ['js-unit-test', 'build-components-storybook', 'determine-build-type'] + if: needs.determine-build-type.outputs.type != 'none' steps: - uses: 'actions/checkout@v3' # https://github.com/actions/checkout/issues/290 @@ -137,3 +158,58 @@ jobs: AWS_DEFAULT_REGION: us-east-2 run: | aws s3 sync ./dist s3://opentrons-components/${{ env.OT_BRANCH}} --acl public-read + + publish-components: + name: 'publish components package to npm' + runs-on: 'ubuntu-latest' + needs: ['js-unit-test', 'determine-build-type'] + if: needs.determine-build-type.outputs.type == 'publish' + steps: + - uses: 'actions/checkout@v3' + # https://github.com/actions/checkout/issues/290 + - name: 'Fix actions/checkout odd handling of tags' + if: startsWith(github.ref, 'refs/tags') + run: | + git fetch -f origin ${{ github.ref }}:${{ github.ref }} + git checkout ${{ github.ref }} + - uses: 'actions/setup-node@v3' + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - name: 'cache yarn cache' + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/.yarn-cache + ${{ github.workspace }}/.npm-cache + key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn- + - name: 'setup-js' + run: | + npm config set cache ./.npm-cache + yarn config set cache-folder ./.yarn-cache + yarn config set network-timeout 60000 + yarn + - name: 'build library' + run: | + make -C components lib + # replace package.json stub version number with version from tag + - name: 'set version number' + run: | + npm install -g json + VERSION_STRING=$(echo ${{ github.ref }} | sed 's/refs\/tags\/components@//') + json -I -f ./components/package.json -e "this.version=\"$VERSION_STRING\"" + json -I -f ./components/package.json -e "this.dependencies['@opentrons/shared-data']=\"$VERSION_STRING\"" + - uses: 'actions/setup-node@v3' + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - name: 'publish to npm registry' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + cd ./components && echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ./.npmrc && npm publish --access public + + + \ No newline at end of file diff --git a/.github/workflows/shared-data-test-lint-deploy.yaml b/.github/workflows/shared-data-test-lint-deploy.yaml index 690cb3fd8c5..585c52dd820 100644 --- a/.github/workflows/shared-data-test-lint-deploy.yaml +++ b/.github/workflows/shared-data-test-lint-deploy.yaml @@ -19,6 +19,8 @@ on: - '*hotfix*' tags: - 'v*' + - 'shared-data*' + - 'components*' pull_request: paths: - 'Makefile' @@ -127,7 +129,7 @@ jobs: key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} restore-keys: | js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn- - - name: 'setup-js' + - name: 'js deps' run: | npm config set cache ./.npm-cache yarn config set cache-folder ./.yarn-cache @@ -187,3 +189,76 @@ jobs: project: 'shared-data/python' repository_url: 'https://upload.pypi.org/legacy/' password: '${{ secrets.OT_PYPI_PASSWORD }}' + + publish-switch: + runs-on: 'ubuntu-latest' + name: 'Determine whether or not to publish artifacts' + outputs: + should_publish: ${{steps.publish-switch.outputs.should_publish}} + steps: + - id: publish-switch + run: | + echo "Determining whether to publish artifacts for event ${{github.event_type}} and ref ${{github.ref}}" + if [ "${{ format('{0}', startsWith(github.ref, 'refs/tags/shared-data')) }}" = "true"] ; then + echo "Publishing builds for shared-data@ tags" + echo 'should_publish=true' >> $GITHUB_OUTPUT + elif [ "${{ format('{0}', startsWith(github.ref, 'refs/tags/components')) }}" = "true"] ; then + echo "Publishing builds for components@ tags" + echo 'should_publish=true' >> $GITHUB_OUTPUT + else + echo "No publish for ref ${{github.ref}} and event ${{github.event_type}}" + echo 'should_publish=false' >> $GITHUB_OUTPUT + fi + + publish-to-npm: + name: 'publish shared-data package to npm' + runs-on: 'ubuntu-latest' + needs: ['js-test', 'publish-switch'] + if: needs.publish-switch.outputs.should_publish == 'true' + steps: + - uses: 'actions/checkout@v3' + # https://github.com/actions/checkout/issues/290 + - name: 'Fix actions/checkout odd handling of tags' + if: startsWith(github.ref, 'refs/tags') + run: | + git fetch -f origin ${{ github.ref }}:${{ github.ref }} + git checkout ${{ github.ref }} + - uses: 'actions/setup-node@v3' + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - name: 'cache yarn cache' + uses: actions/cache@v3 + with: + path: | + ${{ github.workspace }}/.yarn-cache + ${{ github.workspace }}/.npm-cache + key: js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn-${{ hashFiles('yarn.lock') }} + restore-keys: | + js-${{ secrets.GH_CACHE_VERSION }}-${{ runner.os }}-yarn- + - name: 'js deps' + run: | + npm config set cache ./.npm-cache + yarn config set cache-folder ./.yarn-cache + yarn config set network-timeout 60000 + yarn + - name: 'build library' + run: | + make -C shared-data lib-js + # replace package.json stub version number with version from tag + - name: 'set version number' + run: | + npm install -g json + VERSION_STRING=$(echo ${{ github.ref }} | sed -E 's/refs\/tags\/(components|shared-data)@//') + json -I -f ./shared-data/package.json -e "this.version=\"$VERSION_STRING\"" + cd ./shared-data + - uses: 'actions/setup-node@v3' + with: + node-version: '16' + registry-url: 'https://registry.npmjs.org' + - name: 'publish to npm registry' + env: + NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} + run: | + cd ./shared-data && echo "//registry.npmjs.org/:_authToken=${NODE_AUTH_TOKEN}" > ./.npmrc && npm publish --access public + diff --git a/Makefile b/Makefile index fa8f42e5b15..1fb16b8ba87 100755 --- a/Makefile +++ b/Makefile @@ -61,7 +61,6 @@ setup-js: yarn $(MAKE) -C $(APP_SHELL_DIR) setup $(MAKE) -C $(APP_SHELL_ODD_DIR) setup - $(MAKE) -C $(SHARED_DATA_DIR) setup-js PYTHON_SETUP_TARGETS := $(addsuffix -py-setup, $(PYTHON_DIRS)) diff --git a/api/docs/v2/robot_position.rst b/api/docs/v2/robot_position.rst index 2fbba1dab8a..658a44e3645 100644 --- a/api/docs/v2/robot_position.rst +++ b/api/docs/v2/robot_position.rst @@ -116,6 +116,8 @@ All positions relative to labware are adjusted automatically based on labware of You should only adjust labware offsets in your Python code if you plan to run your protocol in Jupyter Notebook or from the command line. See :ref:`using_lpc` in the Advanced Control article for information. +.. _protocol-api-deck-coords: + Position Relative to the Deck ============================= diff --git a/api/src/opentrons/hardware_control/__init__.py b/api/src/opentrons/hardware_control/__init__.py index 356923f1aff..b49f1462249 100644 --- a/api/src/opentrons/hardware_control/__init__.py +++ b/api/src/opentrons/hardware_control/__init__.py @@ -13,24 +13,29 @@ from .api import API from .pause_manager import PauseManager from .backends import Controller, Simulator -from .types import CriticalPoint, ExecutionState +from .types import CriticalPoint, ExecutionState, OT3Mount from .constants import DROP_TIP_RELEASE_DISTANCE from .thread_manager import ThreadManager from .execution_manager import ExecutionManager from .threaded_async_lock import ThreadedAsyncLock, ThreadedAsyncForbidden -from .protocols import HardwareControlInterface +from .protocols import HardwareControlInterface, FlexHardwareControlInterface from .instruments import AbstractInstrument, Gripper from typing import Union from .ot3_calibration import OT3Transforms from .robot_calibration import RobotCalibration +from opentrons.config.types import RobotConfig, OT3Config + +from opentrons.types import Mount # TODO (lc 12-05-2022) We should 1. figure out if we need # to globally export a class that is strictly used in the hardware controller # and 2. how to properly export an ot2 and ot3 pipette. from .instruments.ot2.pipette import Pipette -OT2HardwareControlAPI = HardwareControlInterface[RobotCalibration] -OT3HardwareControlAPI = HardwareControlInterface[OT3Transforms] +OT2HardwareControlAPI = HardwareControlInterface[RobotCalibration, Mount, RobotConfig] +OT3HardwareControlAPI = FlexHardwareControlInterface[ + OT3Transforms, Union[Mount, OT3Mount], OT3Config +] HardwareControlAPI = Union[OT2HardwareControlAPI, OT3HardwareControlAPI] ThreadManagedHardware = ThreadManager[HardwareControlAPI] @@ -55,4 +60,6 @@ "ThreadedAsyncForbidden", "ThreadManagedHardware", "SyncHardwareAPI", + "OT2HardwareControlAPI", + "OT3HardwareControlAPI", ] diff --git a/api/src/opentrons/hardware_control/api.py b/api/src/opentrons/hardware_control/api.py index 8a6f1164829..92971e43d27 100644 --- a/api/src/opentrons/hardware_control/api.py +++ b/api/src/opentrons/hardware_control/api.py @@ -88,7 +88,7 @@ class API( # of methods that are present in the protocol will call the (empty, # do-nothing) methods in the protocol. This will happily make all the # tests fail. - HardwareControlInterface[RobotCalibration], + HardwareControlInterface[RobotCalibration, top_types.Mount, RobotConfig], ): """This API is the primary interface to the hardware controller. @@ -426,6 +426,9 @@ async def update_firmware( firmware_file, checked_loop, explicit_modeset ) + def has_gripper(self) -> bool: + return False + async def cache_instruments( self, require: Optional[Dict[top_types.Mount, PipetteName]] = None ) -> None: diff --git a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py index 3eb3c863522..90667167216 100644 --- a/api/src/opentrons/hardware_control/instruments/ot3/gripper.py +++ b/api/src/opentrons/hardware_control/instruments/ot3/gripper.py @@ -22,7 +22,10 @@ ) from ..instrument_abc import AbstractInstrument from opentrons.hardware_control.dev_types import AttachedGripper, GripperDict -from opentrons_shared_data.errors.exceptions import CommandPreconditionViolated +from opentrons_shared_data.errors.exceptions import ( + CommandPreconditionViolated, + FailedGripperPickupError, +) from opentrons_shared_data.gripper import ( GripperDefinition, @@ -101,6 +104,10 @@ def remove_probe(self) -> None: assert self.attached_probe self._attached_probe = None + @property + def max_allowed_grip_error(self) -> float: + return self._geometry.max_allowed_grip_error + @property def jaw_width(self) -> float: jaw_max = self.geometry.jaw_width["max"] @@ -196,6 +203,23 @@ def check_calibration_pin_location_is_accurate(self) -> None: }, ) + def check_labware_pickup(self, labware_width: float) -> None: + """Ensure that a gripper pickup succeeded.""" + # check if the gripper is at an acceptable position after attempting to + # pick up labware + expected_gripper_position = labware_width + current_gripper_position = self.jaw_width + if ( + abs(current_gripper_position - expected_gripper_position) + > self.max_allowed_grip_error + ): + raise FailedGripperPickupError( + details={ + "expected jaw width": expected_gripper_position, + "actual jaw width": current_gripper_position, + }, + ) + def critical_point(self, cp_override: Optional[CriticalPoint] = None) -> Point: """ The vector from the gripper mount to the critical point, which is selectable diff --git a/api/src/opentrons/hardware_control/ot3_calibration.py b/api/src/opentrons/hardware_control/ot3_calibration.py index 6bca60bfed3..37a10522a93 100644 --- a/api/src/opentrons/hardware_control/ot3_calibration.py +++ b/api/src/opentrons/hardware_control/ot3_calibration.py @@ -47,7 +47,7 @@ from .util import DeckTransformState if TYPE_CHECKING: - from .ot3api import OT3API + from opentrons.hardware_control import OT3HardwareControlAPI LOG = getLogger(__name__) @@ -123,7 +123,7 @@ def _verify_height( async def _verify_edge_pos( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, search_axis: Union[Literal[Axis.X, Axis.Y]], found_edge: Point, @@ -177,7 +177,7 @@ def critical_edge_offset( async def find_edge_binary( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, slot_edge_nominal: Point, search_axis: Union[Literal[Axis.X, Axis.Y]], @@ -272,7 +272,7 @@ async def find_edge_binary( async def find_slot_center_binary( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, estimated_center: Point, raise_verify_error: bool = True, @@ -337,7 +337,7 @@ async def find_slot_center_binary( async def find_calibration_structure_height( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, nominal_center: Point, probe: InstrumentProbeType = InstrumentProbeType.PRIMARY, @@ -365,7 +365,7 @@ async def find_calibration_structure_height( async def _probe_deck_at( - api: OT3API, + api: OT3HardwareControlAPI, mount: OT3Mount, target: Point, settings: CapacitivePassSettings, @@ -390,7 +390,7 @@ async def _probe_deck_at( async def find_axis_center( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, minus_edge_nominal: Point, plus_edge_nominal: Point, @@ -530,7 +530,7 @@ def _edges_from_data( async def find_slot_center_noncontact( - hcapi: OT3API, mount: OT3Mount, estimated_center: Point + hcapi: OT3HardwareControlAPI, mount: OT3Mount, estimated_center: Point ) -> Point: NONCONTACT_INTERVAL_MM: float = 0.1 travel_center = estimated_center + Point(0, 0, NONCONTACT_INTERVAL_MM) @@ -552,7 +552,7 @@ async def find_slot_center_noncontact( async def find_calibration_structure_center( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, nominal_center: Point, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -574,7 +574,7 @@ async def find_calibration_structure_center( async def _calibrate_mount( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, slot: int = SLOT_CENTER, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -641,7 +641,7 @@ async def _calibrate_mount( async def find_calibration_structure_position( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, nominal_center: Point, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -673,7 +673,7 @@ async def find_calibration_structure_position( async def find_slot_center_binary_from_nominal_center( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, slot: int, ) -> Tuple[Point, Point]: @@ -698,7 +698,7 @@ async def find_slot_center_binary_from_nominal_center( async def _determine_transform_matrix( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, ) -> Tuple[types.AttitudeMatrix, Dict[str, Any]]: """ @@ -750,7 +750,7 @@ def gripper_pin_offsets_mean(front: Point, rear: Point) -> Point: async def calibrate_gripper_jaw( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, probe: GripperProbe, slot: int = 5, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -788,7 +788,7 @@ async def calibrate_gripper_jaw( async def calibrate_gripper( - hcapi: OT3API, offset_front: Point, offset_rear: Point + hcapi: OT3HardwareControlAPI, offset_front: Point, offset_rear: Point ) -> Point: """Calibrate gripper.""" offset = gripper_pin_offsets_mean(front=offset_front, rear=offset_rear) @@ -798,7 +798,7 @@ async def calibrate_gripper( async def find_pipette_offset( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: Literal[OT3Mount.LEFT, OT3Mount.RIGHT], slot: int = 5, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -829,7 +829,7 @@ async def find_pipette_offset( async def calibrate_pipette( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: Literal[OT3Mount.LEFT, OT3Mount.RIGHT], slot: int = 5, method: CalibrationMethod = CalibrationMethod.BINARY_SEARCH, @@ -852,7 +852,7 @@ async def calibrate_pipette( async def calibrate_module( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, slot: str, module_id: str, @@ -907,7 +907,7 @@ async def calibrate_module( async def calibrate_belts( - hcapi: OT3API, + hcapi: OT3HardwareControlAPI, mount: OT3Mount, pipette_id: str, ) -> Tuple[types.AttitudeMatrix, Dict[str, Any]]: @@ -1005,7 +1005,7 @@ def validate_attitude_deck_calibration( return DeckTransformState.OK -def delete_belt_calibration_data(hcapi: OT3API) -> None: +def delete_belt_calibration_data(hcapi: OT3HardwareControlAPI) -> None: delete_robot_belt_attitude() hcapi.reset_deck_calibration() diff --git a/api/src/opentrons/hardware_control/ot3api.py b/api/src/opentrons/hardware_control/ot3api.py index 5d429d7e11f..bafd2088c87 100644 --- a/api/src/opentrons/hardware_control/ot3api.py +++ b/api/src/opentrons/hardware_control/ot3api.py @@ -121,7 +121,7 @@ from . import modules from .ot3_calibration import OT3Transforms, OT3RobotCalibrationProvider -from .protocols import HardwareControlInterface +from .protocols import FlexHardwareControlInterface # TODO (lc 09/15/2022) We should update our pipette handler to reflect OT-3 properties # in a follow-up PR. @@ -197,7 +197,9 @@ class OT3API( # of methods that are present in the protocol will call the (empty, # do-nothing) methods in the protocol. This will happily make all the # tests fail. - HardwareControlInterface[OT3Transforms], + FlexHardwareControlInterface[ + OT3Transforms, Union[top_types.Mount, OT3Mount], OT3Config + ], ): """This API is the primary interface to the hardware controller. @@ -1321,6 +1323,9 @@ async def idle_gripper(self) -> None: except GripperNotPresentError: pass + def gripper_jaw_can_home(self) -> bool: + return self._gripper_handler.is_ready_for_jaw_home() + def _build_moves( self, origin: Dict[Axis, float], @@ -2266,7 +2271,7 @@ def reset_instrument( self._pipette_handler.reset_instrument(checked_mount) def get_instrument_offset( - self, mount: OT3Mount + self, mount: Union[top_types.Mount, OT3Mount] ) -> Union[GripperCalibrationOffset, PipetteOffsetSummary, None]: """Get instrument calibration data.""" # TODO (spp, 2023-04-19): We haven't introduced a 'calibration_offset' key in @@ -2275,11 +2280,13 @@ def get_instrument_offset( # to be a part of the dict, this getter can be updated to fetch pipette offset # from the dict, or just remove this getter entirely. - if mount == OT3Mount.GRIPPER: + ot3_mount = OT3Mount.from_mount(mount) + + if ot3_mount == OT3Mount.GRIPPER: gripper_dict = self._gripper_handler.get_gripper_dict() return gripper_dict["calibration_offset"] if gripper_dict else None else: - return self._pipette_handler.get_instrument_offset(mount=mount) + return self._pipette_handler.get_instrument_offset(mount=ot3_mount) async def reset_instrument_offset( self, mount: Union[top_types.Mount, OT3Mount], to_default: bool = True @@ -2544,29 +2551,6 @@ async def capacitive_probe( retract_after: bool = True, probe: Optional[InstrumentProbeType] = None, ) -> Tuple[float, bool]: - """Determine the position of something using the capacitive sensor. - - This function orchestrates detecting the position of a collision between the - capacitive probe on the tool on the specified mount, and some fixed element - of the robot. - - When calling this function, the mount's probe critical point should already - be aligned in the probe axis with the item to be probed. - - It will move the mount's probe critical point to a small distance behind - the expected position of the element (which is target_pos, in deck coordinates, - in the axis to be probed) while running the tool's capacitive sensor. When the - sensor senses contact, the mount stops. - - This function moves away and returns the sensed position. - - This sensed position can be used in several ways, including - - To get an absolute position in deck coordinates of whatever was - targeted, if something was guaranteed to be physically present. - - To detect whether a collision occured at all. If this function - returns a value far enough past the anticipated position, then it indicates - there was no material there. - """ if moving_axis not in [ Axis.X, Axis.Y, diff --git a/api/src/opentrons/hardware_control/protocols/__init__.py b/api/src/opentrons/hardware_control/protocols/__init__.py index d4250a5d589..4b85140f9ba 100644 --- a/api/src/opentrons/hardware_control/protocols/__init__.py +++ b/api/src/opentrons/hardware_control/protocols/__init__.py @@ -1,12 +1,12 @@ """Typing protocols describing a hardware controller.""" -from typing_extensions import Protocol +from typing_extensions import Protocol, Type from .module_provider import ModuleProvider from .hardware_manager import HardwareManager from .chassis_accessory_manager import ChassisAccessoryManager from .event_sourcer import EventSourcer from .liquid_handler import LiquidHandler -from .calibratable import Calibratable, CalibrationType +from .calibratable import Calibratable from .configurable import Configurable from .motion_controller import MotionController from .instrument_configurer import InstrumentConfigurer @@ -14,18 +14,31 @@ from .asyncio_configurable import AsyncioConfigurable from .stoppable import Stoppable from .simulatable import Simulatable +from .identifiable import Identifiable +from .gripper_controller import GripperController +from .flex_calibratable import FlexCalibratable +from .flex_instrument_configurer import FlexInstrumentConfigurer + +from .types import ( + CalibrationType, + MountArgType, + ConfigType, + OT2RobotType, + FlexRobotType, +) class HardwareControlInterface( ModuleProvider, ExecutionControllable, - LiquidHandler[CalibrationType], + LiquidHandler[CalibrationType, MountArgType, ConfigType], ChassisAccessoryManager, HardwareManager, AsyncioConfigurable, Stoppable, Simulatable, - Protocol[CalibrationType], + Identifiable[Type[OT2RobotType]], + Protocol[CalibrationType, MountArgType, ConfigType], ): """A mypy protocol for a hardware controller. @@ -41,11 +54,38 @@ class HardwareControlInterface( however, they can satisfy protocols. """ - ... + def get_robot_type(self) -> Type[OT2RobotType]: + return OT2RobotType + + +class FlexHardwareControlInterface( + ModuleProvider, + ExecutionControllable, + LiquidHandler[CalibrationType, MountArgType, ConfigType], + ChassisAccessoryManager, + HardwareManager, + AsyncioConfigurable, + Stoppable, + Simulatable, + GripperController, + FlexCalibratable, + FlexInstrumentConfigurer[MountArgType], + Identifiable[Type[FlexRobotType]], + Protocol[CalibrationType, MountArgType, ConfigType], +): + """A mypy protocol for a hardware controller with Flex-specific extensions. + + The interface for the Flex controller is mostly in-line with the OT-2 interface, + with some additional functionality and parameterization not supported on the OT-2. + """ + + def get_robot_type(self) -> Type[FlexRobotType]: + return FlexRobotType __all__ = [ "HardwareControlAPI", + "FlexHardwareControlInterface", "Simulatable", "Stoppable", "AsyncioConfigurable", @@ -59,4 +99,6 @@ class HardwareControlInterface( "ChassisAccessoryManager", "HardwareManager", "ModuleProvider", + "Identifiable", + "FlexCalibratable", ] diff --git a/api/src/opentrons/hardware_control/protocols/calibratable.py b/api/src/opentrons/hardware_control/protocols/calibratable.py index 8c8bb65be42..530765d8249 100644 --- a/api/src/opentrons/hardware_control/protocols/calibratable.py +++ b/api/src/opentrons/hardware_control/protocols/calibratable.py @@ -1,10 +1,8 @@ from typing_extensions import Protocol -from typing import TypeVar +from .types import CalibrationType from ..util import DeckTransformState -CalibrationType = TypeVar("CalibrationType") - class Calibratable(Protocol[CalibrationType]): """Protocol specifying calibration information""" diff --git a/api/src/opentrons/hardware_control/protocols/configurable.py b/api/src/opentrons/hardware_control/protocols/configurable.py index 716a74d71d2..8e880d524ad 100644 --- a/api/src/opentrons/hardware_control/protocols/configurable.py +++ b/api/src/opentrons/hardware_control/protocols/configurable.py @@ -1,21 +1,21 @@ -from typing import Union, Dict, Any +from typing import Dict, Any from typing_extensions import Protocol -from opentrons.config.types import RobotConfig, OT3Config +from .types import ConfigType from opentrons.hardware_control.types import HardwareFeatureFlags -class Configurable(Protocol): +class Configurable(Protocol[ConfigType]): """Protocol specifying hardware control configuration.""" - def get_config(self) -> Union[RobotConfig, OT3Config]: + def get_config(self) -> ConfigType: """Get the robot's configuration object. :returns .RobotConfig: The object. """ ... - def set_config(self, config: Union[RobotConfig, OT3Config]) -> None: + def set_config(self, config: ConfigType) -> None: """Replace the currently-loaded config""" ... @@ -29,11 +29,11 @@ def hardware_feature_flags(self, feature_flags: HardwareFeatureFlags) -> None: ... @property - def config(self) -> Union[RobotConfig, OT3Config]: + def config(self) -> ConfigType: ... @config.setter - def config(self, config: Union[RobotConfig, OT3Config]) -> None: + def config(self, config: ConfigType) -> None: ... async def update_config(self, **kwargs: Dict[str, Any]) -> None: diff --git a/api/src/opentrons/hardware_control/protocols/flex_calibratable.py b/api/src/opentrons/hardware_control/protocols/flex_calibratable.py new file mode 100644 index 00000000000..d424f3bc654 --- /dev/null +++ b/api/src/opentrons/hardware_control/protocols/flex_calibratable.py @@ -0,0 +1,99 @@ +from typing import Optional, Tuple, List, AsyncIterator, Union +import contextlib +from typing_extensions import Protocol + +from opentrons import types as top_types +from opentrons.config.types import ( + CapacitivePassSettings, +) +from opentrons.hardware_control.types import ( + Axis, + OT3Mount, + InstrumentProbeType, + GripperProbe, +) +from opentrons.hardware_control.instruments.ot3.instrument_calibration import ( + GripperCalibrationOffset, + PipetteOffsetSummary, +) +from opentrons.hardware_control.modules.module_calibration import ( + ModuleCalibrationOffset, +) + + +class FlexCalibratable(Protocol): + """Calibration extensions for Flex hardware.""" + + async def capacitive_probe( + self, + mount: OT3Mount, + moving_axis: Axis, + target_pos: float, + pass_settings: CapacitivePassSettings, + retract_after: bool = True, + probe: Optional[InstrumentProbeType] = None, + ) -> Tuple[float, bool]: + """Determine the position of something using the capacitive sensor. + + This function orchestrates detecting the position of a collision between the + capacitive probe on the tool on the specified mount, and some fixed element + of the robot. + + When calling this function, the mount's probe critical point should already + be aligned in the probe axis with the item to be probed. + + It will move the mount's probe critical point to a small distance behind + the expected position of the element (which is target_pos, in deck coordinates, + in the axis to be probed) while running the tool's capacitive sensor. When the + sensor senses contact, the mount stops. + + This function moves away and returns the sensed position. + + This sensed position can be used in several ways, including + - To get an absolute position in deck coordinates of whatever was + targeted, if something was guaranteed to be physically present. + - To detect whether a collision occured at all. If this function + returns a value far enough past the anticipated position, then it indicates + there was no material there. + """ + ... + + async def capacitive_sweep( + self, + mount: OT3Mount, + moving_axis: Axis, + begin: top_types.Point, + end: top_types.Point, + speed_mm_s: float, + ) -> List[float]: + ... + + # Note that there is a default implementation of this function to allow for + # the asynccontextmanager decorator to propagate properly. + @contextlib.asynccontextmanager + async def restore_system_constrants(self) -> AsyncIterator[None]: + yield + + async def set_system_constraints_for_calibration(self) -> None: + ... + + async def reset_instrument_offset( + self, mount: Union[top_types.Mount, OT3Mount], to_default: bool = True + ) -> None: + ... + + def add_gripper_probe(self, probe: GripperProbe) -> None: + ... + + def remove_gripper_probe(self) -> None: + ... + + async def save_instrument_offset( + self, mount: Union[top_types.Mount, OT3Mount], delta: top_types.Point + ) -> Union[GripperCalibrationOffset, PipetteOffsetSummary]: + ... + + async def save_module_offset( + self, module_id: str, mount: OT3Mount, slot: str, offset: top_types.Point + ) -> Optional[ModuleCalibrationOffset]: + ... diff --git a/api/src/opentrons/hardware_control/protocols/flex_instrument_configurer.py b/api/src/opentrons/hardware_control/protocols/flex_instrument_configurer.py new file mode 100644 index 00000000000..0606b8847f4 --- /dev/null +++ b/api/src/opentrons/hardware_control/protocols/flex_instrument_configurer.py @@ -0,0 +1,48 @@ +"""Flex-specific extensions to instrument configuration.""" +from typing import Union +from typing_extensions import Protocol + +from .types import MountArgType + +from opentrons.hardware_control.dev_types import ( + PipetteStateDict, +) +from opentrons.hardware_control.types import ( + TipStateType, +) +from opentrons.hardware_control.instruments.ot3.instrument_calibration import ( + PipetteOffsetSummary, + GripperCalibrationOffset, +) + + +class FlexInstrumentConfigurer(Protocol[MountArgType]): + """A protocol specifying Flex-specific extensions to instrument configuration.""" + + async def get_instrument_state( + self, + mount: MountArgType, + ) -> PipetteStateDict: + ... + + def get_instrument_offset( + self, mount: MountArgType + ) -> Union[GripperCalibrationOffset, PipetteOffsetSummary, None]: + ... + + async def get_tip_presence_status( + self, + mount: MountArgType, + ) -> TipStateType: + """Check tip presence status. + + If a high throughput pipette is present, + move the tip motors down before checking the sensor status. + """ + ... + + async def verify_tip_presence( + self, mount: MountArgType, expected: TipStateType + ) -> None: + """Check tip presence status and raise if it does not match `expected`.""" + ... diff --git a/api/src/opentrons/hardware_control/protocols/gripper_controller.py b/api/src/opentrons/hardware_control/protocols/gripper_controller.py new file mode 100644 index 00000000000..6d46bde5c37 --- /dev/null +++ b/api/src/opentrons/hardware_control/protocols/gripper_controller.py @@ -0,0 +1,44 @@ +"""Protocol specifying API gripper control.""" +from typing import Optional +from typing_extensions import Protocol + +from opentrons.hardware_control.dev_types import GripperDict +from opentrons.hardware_control.instruments.ot3.gripper import Gripper + + +class GripperController(Protocol): + """A protocol specifying gripper API functions.""" + + async def grip( + self, force_newtons: Optional[float] = None, stay_engaged: bool = True + ) -> None: + ... + + async def ungrip(self, force_newtons: Optional[float] = None) -> None: + """Release gripped object. + + To simply open the jaw, use `home_gripper_jaw` instead. + """ + ... + + async def idle_gripper(self) -> None: + """Move gripper to its idle, gripped position.""" + ... + + def gripper_jaw_can_home(self) -> bool: + """Check if it is valid to home the gripper jaw. + + This should return False if the API believes that the gripper is + currently holding something. + """ + ... + + @property + def attached_gripper(self) -> Optional[GripperDict]: + """Get a dict of all attached grippers.""" + ... + + @property + def hardware_gripper(self) -> Optional[Gripper]: + """Get attached gripper, if present.""" + ... diff --git a/api/src/opentrons/hardware_control/protocols/identifiable.py b/api/src/opentrons/hardware_control/protocols/identifiable.py new file mode 100644 index 00000000000..4e964f5633f --- /dev/null +++ b/api/src/opentrons/hardware_control/protocols/identifiable.py @@ -0,0 +1,16 @@ +from typing_extensions import Protocol + +from .types import ProtocolRobotType + + +class Identifiable(Protocol[ProtocolRobotType]): + """Protocol specifying support for hardware identification.""" + + def get_robot_type(self) -> ProtocolRobotType: + """Return the enumerated robot type that this API controls. + + When a caller needs to determine whether an API function is expected + to be present on a hardware_control instance, it is preferable to check + with this function rather than check the exact type via `isinstance`. + """ + ... diff --git a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py index 820757e5e6b..810caad667b 100644 --- a/api/src/opentrons/hardware_control/protocols/instrument_configurer.py +++ b/api/src/opentrons/hardware_control/protocols/instrument_configurer.py @@ -3,6 +3,7 @@ from opentrons_shared_data.pipette.dev_types import PipetteName from opentrons.types import Mount +from .types import MountArgType # TODO (lc 12-05-2022) This protocol has deviated from the OT3 api. We # need to figure out how to combine them again in follow-up refactors. @@ -11,10 +12,10 @@ from ..types import CriticalPoint -class InstrumentConfigurer(Protocol): +class InstrumentConfigurer(Protocol[MountArgType]): """A protocol specifying how to interact with instrument presence and detection.""" - def reset_instrument(self, mount: Optional[Mount] = None) -> None: + def reset_instrument(self, mount: Optional[MountArgType] = None) -> None: """ Reset the internal state of a pipette by its mount, without doing any lower level reconfiguration. This is useful to make sure that no @@ -66,7 +67,7 @@ def get_attached_instruments(self) -> Dict[Mount, PipetteDict]: """ ... - def get_attached_instrument(self, mount: Mount) -> PipetteDict: + def get_attached_instrument(self, mount: MountArgType) -> PipetteDict: """Get the status dict of a single cached instrument. Return values and caveats are as get_attached_instruments. @@ -91,7 +92,7 @@ def attached_pipettes(self) -> Dict[Mount, PipetteDict]: def calibrate_plunger( self, - mount: Mount, + mount: MountArgType, top: Optional[float] = None, bottom: Optional[float] = None, blow_out: Optional[float] = None, @@ -112,7 +113,7 @@ def calibrate_plunger( def set_flow_rate( self, - mount: Mount, + mount: MountArgType, aspirate: Optional[float] = None, dispense: Optional[float] = None, blow_out: Optional[float] = None, @@ -122,7 +123,7 @@ def set_flow_rate( def set_pipette_speed( self, - mount: Mount, + mount: MountArgType, aspirate: Optional[float] = None, dispense: Optional[float] = None, blow_out: Optional[float] = None, @@ -132,7 +133,7 @@ def set_pipette_speed( def get_instrument_max_height( self, - mount: Mount, + mount: MountArgType, critical_point: Optional[CriticalPoint] = None, ) -> float: """Return max achievable height of the attached instrument @@ -140,7 +141,7 @@ def get_instrument_max_height( """ ... - async def add_tip(self, mount: Mount, tip_length: float) -> None: + async def add_tip(self, mount: MountArgType, tip_length: float) -> None: """Inform the hardware that a tip is now attached to a pipette. This changes the critical point of the pipette to make sure that @@ -148,7 +149,7 @@ async def add_tip(self, mount: Mount, tip_length: float) -> None: """ ... - async def remove_tip(self, mount: Mount) -> None: + async def remove_tip(self, mount: MountArgType) -> None: """Inform the hardware that a tip is no longer attached to a pipette. This changes the critical point of the system to the end of the @@ -157,7 +158,7 @@ async def remove_tip(self, mount: Mount) -> None: ... def set_current_tiprack_diameter( - self, mount: Mount, tiprack_diameter: float + self, mount: MountArgType, tiprack_diameter: float ) -> None: """Inform the hardware of the diameter of the tiprack. @@ -166,7 +167,7 @@ def set_current_tiprack_diameter( """ ... - def set_working_volume(self, mount: Mount, tip_volume: float) -> None: + def set_working_volume(self, mount: MountArgType, tip_volume: float) -> None: """Inform the hardware how much volume a pipette can aspirate. This will set the limit of aspiration for the pipette, and is @@ -181,3 +182,12 @@ def hardware_instruments(self) -> Dict[Mount, Optional[Pipette]]: This should rarely be used. Do not write new code that uses it. """ ... + + def has_gripper(self) -> bool: + """Return whether there is a gripper attached to this instance. + + - On robots that do not support a gripper, this will always return False. + - On robots that support a gripper, this will return based on the current + presence of a gripper. + """ + ... diff --git a/api/src/opentrons/hardware_control/protocols/liquid_handler.py b/api/src/opentrons/hardware_control/protocols/liquid_handler.py index e46cea2fdc2..e55dbb88440 100644 --- a/api/src/opentrons/hardware_control/protocols/liquid_handler.py +++ b/api/src/opentrons/hardware_control/protocols/liquid_handler.py @@ -1,24 +1,24 @@ from typing import Optional from typing_extensions import Protocol -from opentrons.types import Mount +from .types import MountArgType, CalibrationType, ConfigType from .instrument_configurer import InstrumentConfigurer from .motion_controller import MotionController from .configurable import Configurable -from .calibratable import Calibratable, CalibrationType +from .calibratable import Calibratable class LiquidHandler( - InstrumentConfigurer, - MotionController, - Configurable, + InstrumentConfigurer[MountArgType], + MotionController[MountArgType], + Configurable[ConfigType], Calibratable[CalibrationType], - Protocol[CalibrationType], + Protocol[CalibrationType, MountArgType, ConfigType], ): async def update_nozzle_configuration_for_mount( self, - mount: Mount, + mount: MountArgType, back_left_nozzle: Optional[str], front_right_nozzle: Optional[str], starting_nozzle: Optional[str] = None, @@ -40,7 +40,7 @@ async def update_nozzle_configuration_for_mount( """ ... - async def configure_for_volume(self, mount: Mount, volume: float) -> None: + async def configure_for_volume(self, mount: MountArgType, volume: float) -> None: """ Configure a pipette to handle the specified volume. @@ -53,7 +53,9 @@ async def configure_for_volume(self, mount: Mount, volume: float) -> None: """ ... - async def prepare_for_aspirate(self, mount: Mount, rate: float = 1.0) -> None: + async def prepare_for_aspirate( + self, mount: MountArgType, rate: float = 1.0 + ) -> None: """ Prepare the pipette for aspiration. @@ -75,7 +77,7 @@ async def prepare_for_aspirate(self, mount: Mount, rate: float = 1.0) -> None: async def aspirate( self, - mount: Mount, + mount: MountArgType, volume: Optional[float] = None, rate: float = 1.0, ) -> None: @@ -102,7 +104,7 @@ async def aspirate( async def dispense( self, - mount: Mount, + mount: MountArgType, volume: Optional[float] = None, rate: float = 1.0, push_out: Optional[float] = None, @@ -119,7 +121,9 @@ async def dispense( """ ... - async def blow_out(self, mount: Mount, volume: Optional[float] = None) -> None: + async def blow_out( + self, mount: MountArgType, volume: Optional[float] = None + ) -> None: """ Force any remaining liquid to dispense. The liquid will be dispensed at the current location of pipette @@ -128,7 +132,7 @@ async def blow_out(self, mount: Mount, volume: Optional[float] = None) -> None: async def pick_up_tip( self, - mount: Mount, + mount: MountArgType, tip_length: float, presses: Optional[int] = None, increment: Optional[float] = None, @@ -154,7 +158,7 @@ async def pick_up_tip( async def drop_tip( self, - mount: Mount, + mount: MountArgType, home_after: bool = True, ) -> None: """ diff --git a/api/src/opentrons/hardware_control/protocols/motion_controller.py b/api/src/opentrons/hardware_control/protocols/motion_controller.py index 8d89bb7abc1..62b711aa261 100644 --- a/api/src/opentrons/hardware_control/protocols/motion_controller.py +++ b/api/src/opentrons/hardware_control/protocols/motion_controller.py @@ -1,11 +1,12 @@ from typing import Dict, List, Optional, Mapping from typing_extensions import Protocol -from opentrons.types import Mount, Point +from opentrons.types import Point from ..types import Axis, CriticalPoint, MotionChecks +from .types import MountArgType -class MotionController(Protocol): +class MotionController(Protocol[MountArgType]): """Protocol specifying fundamental motion controls.""" async def halt(self, disengage_before_stopping: bool = False) -> None: @@ -44,13 +45,13 @@ async def reset(self) -> None: # Gantry/frame (i.e. not pipette) action API async def home_z( self, - mount: Optional[Mount] = None, + mount: Optional[MountArgType] = None, allow_home_other: bool = True, ) -> None: """Home a selected z-axis, or both if not specified.""" ... - async def home_plunger(self, mount: Mount) -> None: + async def home_plunger(self, mount: MountArgType) -> None: """ Home the plunger motor for a mount, and then return it to the 'bottom' position. @@ -69,7 +70,7 @@ async def home(self, axes: Optional[List[Axis]] = None) -> None: async def current_position( self, - mount: Mount, + mount: MountArgType, critical_point: Optional[CriticalPoint] = None, refresh: bool = False, # TODO(mc, 2021-11-15): combine with `refresh` for more reliable @@ -97,7 +98,7 @@ async def current_position( async def gantry_position( self, - mount: Mount, + mount: MountArgType, critical_point: Optional[CriticalPoint] = None, refresh: bool = False, # TODO(mc, 2021-11-15): combine with `refresh` for more reliable @@ -114,7 +115,7 @@ async def gantry_position( async def move_to( self, - mount: Mount, + mount: MountArgType, abs_position: Point, speed: Optional[float] = None, critical_point: Optional[CriticalPoint] = None, @@ -173,7 +174,7 @@ async def move_axes( async def move_rel( self, - mount: Mount, + mount: MountArgType, delta: Point, speed: Optional[float] = None, max_speeds: Optional[Dict[Axis, float]] = None, @@ -203,7 +204,7 @@ async def disengage_axes(self, which: List[Axis]) -> None: """Disengage some axes.""" ... - async def retract(self, mount: Mount, margin: float = 10) -> None: + async def retract(self, mount: MountArgType, margin: float = 10) -> None: """Pull the specified mount up to its home position. Works regardless of critical point or home status. diff --git a/api/src/opentrons/hardware_control/protocols/types.py b/api/src/opentrons/hardware_control/protocols/types.py new file mode 100644 index 00000000000..bdd4a3799f4 --- /dev/null +++ b/api/src/opentrons/hardware_control/protocols/types.py @@ -0,0 +1,27 @@ +"""Types that are common across protocols.""" + +from typing import TypeVar, Union, Type +from opentrons.hardware_control.types import OT3Mount +from opentrons.types import Mount +from opentrons.config.types import RobotConfig, OT3Config + + +class OT2RobotType: + pass + + +class FlexRobotType: + pass + + +CalibrationType = TypeVar("CalibrationType") + +MountArgType = TypeVar( + "MountArgType", Mount, Union[OT3Mount, Mount], contravariant=True +) + +ConfigType = TypeVar("ConfigType", RobotConfig, OT3Config) + +ProtocolRobotType = TypeVar( + "ProtocolRobotType", Type[FlexRobotType], Type[OT2RobotType], covariant=True +) diff --git a/api/src/opentrons/protocol_api/labware.py b/api/src/opentrons/protocol_api/labware.py index ed94cb5b87f..eb25661bbf6 100644 --- a/api/src/opentrons/protocol_api/labware.py +++ b/api/src/opentrons/protocol_api/labware.py @@ -72,7 +72,7 @@ class Well: - Calculating positions relative to the well. See :ref:`position-relative-labware` for details. - - Returning well measurements. see :ref:`new-labware-well-properties` for details. + - Returning well measurements. See :ref:`new-labware-well-properties` for details. - Specifying what liquid should be in the well at the beginning of a protocol. See :ref:`labeling-liquids` for details. """ @@ -362,7 +362,7 @@ def __getitem__(self, key: str) -> Well: def uri(self) -> str: """A string fully identifying the labware. - :returns: The uri, ``"namespace/loadname/version"`` + :returns: The URI, ``"namespace/loadname/version"`` """ return self._core.get_uri() @@ -372,7 +372,7 @@ def parent(self) -> Union[str, Labware, ModuleTypes, OffDeckType]: """The parent of this labware---where this labware is loaded. Returns: - If the labware is directly on the robot's deck, the `str` name of the deck slot, + If the labware is directly on the robot's deck, the ``str`` name of the deck slot, like ``"D1"`` (Flex) or ``"1"`` (OT-2). See :ref:`deck-slots`. If the labware is on a module, a :py:class:`ModuleContext`. @@ -428,13 +428,13 @@ def name(self, new_name: str) -> None: @property # type: ignore[misc] @requires_version(2, 0) def load_name(self) -> str: - """The API load name of the labware definition""" + """The API load name of the labware definition.""" return self._core.load_name @property # type: ignore[misc] @requires_version(2, 0) def parameters(self) -> "LabwareParameters": - """Internal properties of a labware including type and quirks""" + """Internal properties of a labware including type and quirks.""" return self._core.get_parameters() @property # type: ignore @@ -558,7 +558,7 @@ def set_offset(self, x: float, y: float, z: float) -> None: (see :ref:`protocol-api-deck-coords`) that the motion system will add to any movement targeting this labware instance. - The offset will *not* apply to any other labware instances, + The offset *will not* apply to any other labware instances, even if those labware are of the same type. .. caution:: @@ -596,7 +596,7 @@ def calibrated_offset(self) -> Point: @requires_version(2, 0) def well(self, idx: Union[int, str]) -> Well: - """Deprecated---use result of `wells` or `wells_by_name`""" + """Deprecated. Use result of :py:meth:`wells` or :py:meth:`wells_by_name`.""" if isinstance(idx, int): return self.wells()[idx] elif isinstance(idx, str): @@ -609,20 +609,16 @@ def well(self, idx: Union[int, str]) -> Well: @requires_version(2, 0) def wells(self, *args: Union[str, int]) -> List[Well]: """ - Accessor function used to generate a list of wells in top -> down, - left -> right order. This is representative of moving down `rows` and - across `columns` (e.g. 'A1', 'B1', 'C1'...'A2', 'B2', 'C2') + Accessor function that generates a list of wells in a top down, + left to right order. This is representative of moving down rows and + across columns (i.e., A1, B1, C1…A2, B2, C2…). With indexing one can treat it as a typical python - list. To access well A1, for example, write: labware.wells()[0] + list. For example, access well A1 with ``labware.wells()[0]``. - Note that this method takes args for backward-compatibility, but use - of args is deprecated and will be removed in future versions. Args - can be either strings or integers, but must all be the same type (e.g.: - `self.wells(1, 4, 8)` or `self.wells('A1', 'B2')`, but - `self.wells('A1', 4)` is invalid. + Note that this method takes args for backward-compatibility. But using args is deprecated and will be removed in future versions. Args can be either strings or integers, but must all be the same type. For example, ``self.columns(1, 4, 8)`` or ``self.columns('1', '2')`` are valid, but ``self.columns('1', 4)`` is not. - :return: Ordered list of all wells in a labware + :return: Ordered list of all wells in a labware. """ if not args: return list(self._wells_by_name.values()) @@ -644,13 +640,13 @@ def wells(self, *args: Union[str, int]) -> List[Well]: @requires_version(2, 0) def wells_by_name(self) -> Dict[str, Well]: """ - Accessor function used to create a look-up table of Wells by name. + Accessor function used to create a look-up table of wells by name. - With indexing one can treat it as a typical python - dictionary whose keys are well names. To access well A1, for example, - write: labware.wells_by_name()['A1'] + With indexing one can treat it as a typical Python + dictionary whose keys are well names. For example, access well A1 + with ``labware.wells_by_name()['A1']``. - :return: Dictionary of well objects keyed by well name + :return: Dictionary of :py:class:`.Well` objects keyed by well name. """ return dict(self._wells_by_name) @@ -671,16 +667,12 @@ def rows(self, *args: Union[int, str]) -> List[List[Well]]: Accessor function used to navigate through a labware by row. With indexing one can treat it as a typical python nested list. - To access row A for example, write: labware.rows()[0]. This - will output ['A1', 'A2', 'A3', 'A4'...] + For example, access row A with ``labware.rows()[0]``. This + will output ``['A1', 'A2', 'A3', 'A4'...]``. - Note that this method takes args for backward-compatibility, but use - of args is deprecated and will be removed in future versions. Args - can be either strings or integers, but must all be the same type (e.g.: - `self.rows(1, 4, 8)` or `self.rows('A', 'B')`, but `self.rows('A', 4)` - is invalid. + Note that this method takes args for backward-compatibility. But using args is deprecated and will be removed in future versions. Args can be either strings or integers, but must all be the same type. For example, ``self.columns(1, 4, 8)`` or ``self.columns('1', '2')`` are valid, but ``self.columns('1', 4)`` is not. - :return: A list of row lists + :return: A list of row lists. """ if not args: return [ @@ -708,10 +700,10 @@ def rows_by_name(self) -> Dict[str, List[Well]]: Accessor function used to navigate through a labware by row name. With indexing one can treat it as a typical python dictionary. - To access row A for example, write: labware.rows_by_name()['A'] - This will output ['A1', 'A2', 'A3', 'A4'...]. + For example, access row A with ``labware.rows_by_name()['A']``. + This will output ``['A1', 'A2', 'A3', 'A4'...]``. - :return: Dictionary of Well lists keyed by row name + :return: Dictionary of :py:class:`.Well` lists keyed by row name. """ return { row_name: [self._wells_by_name[well_name] for well_name in row] @@ -733,17 +725,15 @@ def columns(self, *args: Union[int, str]) -> List[List[Well]]: Accessor function used to navigate through a labware by column. With indexing one can treat it as a typical python nested list. - To access row A for example, - write: labware.columns()[0] - This will output ['A1', 'B1', 'C1', 'D1'...]. + For example, access row A with ``labware.columns()[0]``. + This will output ``['A1', 'B1', 'C1', 'D1'...]``. - Note that this method takes args for backward-compatibility, but use - of args is deprecated and will be removed in future versions. Args - can be either strings or integers, but must all be the same type (e.g.: - `self.columns(1, 4, 8)` or `self.columns('1', '2')`, but - `self.columns('1', 4)` is invalid. + Note that this method takes args for backward-compatibility. But using args is deprecated and will be removed in future versions. Args + can be either strings or integers, but must all be the same type. For example, + ``self.columns(1, 4, 8)`` or ``self.columns('1', '2')`` are valid, but + ``self.columns('1', 4)`` is not. - :return: A list of column lists + :return: A list of column lists. """ if not args: return [ @@ -771,11 +761,10 @@ def columns_by_name(self) -> Dict[str, List[Well]]: Accessor function used to navigate through a labware by column name. With indexing one can treat it as a typical python dictionary. - To access row A for example, - write: labware.columns_by_name()['1'] - This will output ['A1', 'B1', 'C1', 'D1'...]. + For example, access row A with ``labware.columns_by_name()['1']``. + This will output ``['A1', 'B1', 'C1', 'D1'...]``. - :return: Dictionary of Well lists keyed by column name + :return: Dictionary of :py:class:`.Well` lists keyed by column name. """ return { column_name: [self._wells_by_name[well_name] for well_name in column] @@ -795,9 +784,9 @@ def columns_by_index(self) -> Dict[str, List[Well]]: @requires_version(2, 0) def highest_z(self) -> float: """ - The z-coordinate of the tallest single point anywhere on the labware. + The z-coordinate of the highest single point anywhere on the labware. - This is drawn from the 'dimensions'/'zDimension' elements of the + This is taken from the ``zDimension`` property of the ``dimensions`` object in the labware definition and takes into account the calibration offset. """ return self._core.highest_z @@ -835,7 +824,7 @@ def tip_length(self, length: float) -> None: raise APIVersionError("Labware.tip_length setter has been deprecated") # TODO(mc, 2023-02-06): this assert should be enough for mypy - # invvestigate if upgrading mypy allows the `cast` to be removed + # investigate if upgrading mypy allows the `cast` to be removed assert isinstance(self._core, LegacyLabwareCore) cast(LegacyLabwareCore, self._core).set_tip_length(length) diff --git a/api/src/opentrons/protocol_engine/errors/__init__.py b/api/src/opentrons/protocol_engine/errors/__init__.py index d330be61a3e..d3c3bb6d79e 100644 --- a/api/src/opentrons/protocol_engine/errors/__init__.py +++ b/api/src/opentrons/protocol_engine/errors/__init__.py @@ -138,4 +138,5 @@ "NotSupportedOnRobotType", # error occurrence models "ErrorOccurrence", + "FailedGripperPickupError", ] diff --git a/api/src/opentrons/protocol_engine/execution/labware_movement.py b/api/src/opentrons/protocol_engine/execution/labware_movement.py index b65e91d0845..5dfe3a151cd 100644 --- a/api/src/opentrons/protocol_engine/execution/labware_movement.py +++ b/api/src/opentrons/protocol_engine/execution/labware_movement.py @@ -100,7 +100,7 @@ async def move_labware_with_gripper( raise GripperNotAttachedError( "No gripper found for performing labware movements." ) - if not ot3api._gripper_handler.is_ready_for_jaw_home(): + if not ot3api.gripper_jaw_can_home(): raise CannotPerformGripperAction( "Cannot pick up labware when gripper is already gripping." ) @@ -135,7 +135,7 @@ async def move_labware_with_gripper( post_drop_slide_offset=post_drop_slide_offset, ) labware_grip_force = self._state_store.labware.get_grip_force(labware_id) - + gripper_opened = False for waypoint_data in movement_waypoints: if waypoint_data.jaw_open: if waypoint_data.dropping: @@ -146,12 +146,22 @@ async def move_labware_with_gripper( # on the side of a falling tiprack catches the jaw. await ot3api.disengage_axes([Axis.Z_G]) await ot3api.ungrip() + gripper_opened = True if waypoint_data.dropping: # We lost the position estimation after disengaging the axis, so # it is necessary to home it next await ot3api.home_z(OT3Mount.GRIPPER) else: await ot3api.grip(force_newtons=labware_grip_force) + # we only want to check position after the gripper has opened and + # should be holding labware + if gripper_opened: + assert ot3api.hardware_gripper + ot3api.hardware_gripper.check_labware_pickup( + labware_width=self._state_store.labware.get_dimensions( + labware_id + ).y + ) await ot3api.move_to( mount=gripper_mount, abs_position=waypoint_data.position ) diff --git a/api/src/opentrons/protocol_engine/resources/ot3_validation.py b/api/src/opentrons/protocol_engine/resources/ot3_validation.py index 8a555dd5f47..7b25bc35430 100644 --- a/api/src/opentrons/protocol_engine/resources/ot3_validation.py +++ b/api/src/opentrons/protocol_engine/resources/ot3_validation.py @@ -1,28 +1,21 @@ """Validation file for protocol engine commandsot.""" from __future__ import annotations -from typing import TYPE_CHECKING, Optional +from typing import Optional from opentrons.protocol_engine.errors import HardwareNotSupportedError +from opentrons.hardware_control.protocols.types import FlexRobotType -if TYPE_CHECKING: - from opentrons.hardware_control.ot3api import OT3API - from opentrons.hardware_control import HardwareControlAPI +from opentrons.hardware_control import HardwareControlAPI, OT3HardwareControlAPI def ensure_ot3_hardware( - hardware_api: HardwareControlAPI, error_msg: Optional[str] = None -) -> OT3API: + hardware_api: HardwareControlAPI, + error_msg: Optional[str] = None, +) -> OT3HardwareControlAPI: """Validate that the HardwareControlAPI is of OT-3 instance.""" - try: - from opentrons.hardware_control.ot3api import OT3API - except ImportError as exception: - raise HardwareNotSupportedError( - error_msg or "This command is supported by OT-3 only." - ) from exception + if hardware_api.get_robot_type() == FlexRobotType: + return hardware_api # type: ignore - if not isinstance(hardware_api, OT3API): - raise HardwareNotSupportedError( - error_msg or "This command is supported by OT-3 only." - ) - - return hardware_api + raise HardwareNotSupportedError( + error_msg or "This command is supported by OT-3 only." + ) diff --git a/api/tests/opentrons/hardware_control/test_gripper.py b/api/tests/opentrons/hardware_control/test_gripper.py index 02d2285bdb0..3f7266399ef 100644 --- a/api/tests/opentrons/hardware_control/test_gripper.py +++ b/api/tests/opentrons/hardware_control/test_gripper.py @@ -1,5 +1,7 @@ -from typing import Optional, Callable, TYPE_CHECKING +from typing import Optional, Callable, TYPE_CHECKING, Any, Generator import pytest +from contextlib import nullcontext +from unittest.mock import MagicMock, patch, PropertyMock from opentrons.types import Point from opentrons.calibration_storage import types as cal_types @@ -7,6 +9,7 @@ from opentrons.hardware_control.types import CriticalPoint from opentrons.config import gripper_config from opentrons_shared_data.gripper import GripperModel +from opentrons_shared_data.errors.exceptions import FailedGripperPickupError if TYPE_CHECKING: from opentrons.hardware_control.instruments.ot3.instrument_calibration import ( @@ -26,6 +29,24 @@ def fake_offset() -> "GripperCalibrationOffset": return load_gripper_calibration_offset("fakeid123") +@pytest.fixture +def mock_jaw_width() -> Generator[MagicMock, None, None]: + with patch( + "opentrons.hardware_control.instruments.ot3.gripper.Gripper.jaw_width", + new_callable=PropertyMock, + ) as jaw_width: + yield jaw_width + + +@pytest.fixture +def mock_max_grip_error() -> Generator[MagicMock, None, None]: + with patch( + "opentrons.hardware_control.instruments.ot3.gripper.Gripper.max_allowed_grip_error", + new_callable=PropertyMock, + ) as max_error: + yield max_error + + @pytest.mark.ot3_only def test_id_get_added_to_dict(fake_offset: "GripperCalibrationOffset") -> None: gripr = gripper.Gripper(fake_gripper_conf, fake_offset, "fakeid123") @@ -67,6 +88,32 @@ def test_load_gripper_cal_offset(fake_offset: "GripperCalibrationOffset") -> Non ) +@pytest.mark.ot3_only +@pytest.mark.parametrize( + argnames=["jaw_width_val", "error_context"], + argvalues=[ + (89, nullcontext()), + (100, pytest.raises(FailedGripperPickupError)), + (50, pytest.raises(FailedGripperPickupError)), + (85, nullcontext()), + ], +) +def test_check_labware_pickup( + mock_jaw_width: Any, + mock_max_grip_error: Any, + jaw_width_val: float, + error_context: Any, +) -> None: + """Test that FailedGripperPickupError is raised correctly.""" + # This should only be triggered when the difference between the + # gripper jaw and labware widths is greater than the max allowed error. + gripr = gripper.Gripper(fake_gripper_conf, fake_offset, "fakeid123") + mock_jaw_width.return_value = jaw_width_val + mock_max_grip_error.return_value = 6 + with error_context: + gripr.check_labware_pickup(85) + + @pytest.mark.ot3_only def test_reload_instrument_cal_ot3(fake_offset: "GripperCalibrationOffset") -> None: old_gripper = gripper.Gripper( diff --git a/api/tests/opentrons/protocol_engine/conftest.py b/api/tests/opentrons/protocol_engine/conftest.py index 8574cefe248..d703a964078 100644 --- a/api/tests/opentrons/protocol_engine/conftest.py +++ b/api/tests/opentrons/protocol_engine/conftest.py @@ -20,6 +20,7 @@ from opentrons.hardware_control import HardwareControlAPI, OT2HardwareControlAPI from opentrons.hardware_control.api import API +from opentrons.hardware_control.protocols.types import FlexRobotType, OT2RobotType if TYPE_CHECKING: from opentrons.hardware_control.ot3api import OT3API @@ -34,7 +35,9 @@ def hardware_api(decoy: Decoy) -> HardwareControlAPI: @pytest.fixture def ot2_hardware_api(decoy: Decoy) -> API: """Get a mocked out OT-2 hardware API.""" - return decoy.mock(cls=API) + mock = decoy.mock(cls=API) + decoy.when(mock.get_robot_type()).then_return(OT2RobotType) + return mock @pytest.mark.ot3_only @@ -44,7 +47,9 @@ def ot3_hardware_api(decoy: Decoy) -> OT3API: try: from opentrons.hardware_control.ot3api import OT3API - return decoy.mock(cls=OT3API) + mock = decoy.mock(cls=OT3API) + decoy.when(mock.get_robot_type()).then_return(FlexRobotType) + return mock except ImportError: # TODO (tz, 9-23-22) Figure out a better way to use this fixture with OT-3 api only. return None # type: ignore[return-value] diff --git a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py index 838039488f0..9a7caf18c0f 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_labware_movement_handler.py @@ -5,7 +5,7 @@ import pytest from decoy import Decoy, matchers -from typing import TYPE_CHECKING, Union, Optional +from typing import TYPE_CHECKING, Union, Optional, Tuple from opentrons.protocol_engine.execution import EquipmentHandler, MovementHandler from opentrons.hardware_control import HardwareControlAPI @@ -22,6 +22,7 @@ LabwareLocation, NonStackedLocation, LabwareMovementOffsetData, + Dimensions, ) from opentrons.protocol_engine.execution.thermocycler_plate_lifter import ( ThermocyclerPlateLifter, @@ -85,6 +86,22 @@ def heater_shaker_movement_flagger(decoy: Decoy) -> HeaterShakerMovementFlagger: return decoy.mock(cls=HeaterShakerMovementFlagger) +@pytest.fixture +def hardware_gripper_offset_data() -> Tuple[ + LabwareMovementOffsetData, LabwareMovementOffsetData +]: + """Get a set of mocked labware offset data.""" + user_offset_data = LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=123, y=234, z=345), + dropOffset=LabwareOffsetVector(x=111, y=222, z=333), + ) + final_offset_data = LabwareMovementOffsetData( + pickUpOffset=LabwareOffsetVector(x=-1, y=-2, z=-3), + dropOffset=LabwareOffsetVector(x=1, y=2, z=3), + ) + return user_offset_data, final_offset_data + + def default_experimental_movement_data() -> LabwareMovementOffsetData: """Experimental movement data with default values.""" return LabwareMovementOffsetData( @@ -93,6 +110,49 @@ def default_experimental_movement_data() -> LabwareMovementOffsetData: ) +async def set_up_decoy_hardware_gripper( + decoy: Decoy, ot3_hardware_api: OT3API, state_store: StateStore +) -> None: + """Shared hardware gripper decoy setup.""" + decoy.when(state_store.config.use_virtual_gripper).then_return(False) + decoy.when(ot3_hardware_api.has_gripper()).then_return(True) + decoy.when(ot3_hardware_api.gripper_jaw_can_home()).then_return(True) + assert ot3_hardware_api.hardware_gripper + decoy.when( + await ot3_hardware_api.gantry_position(mount=OT3Mount.GRIPPER) + ).then_return(Point(x=777, y=888, z=999)) + + decoy.when( + await ot3_hardware_api.encoder_current_position_ot3(OT3Mount.GRIPPER) + ).then_return({Axis.G: 4.0}) + + decoy.when( + state_store.labware.get_dimensions(labware_id="my-teleporting-labware") + ).then_return(Dimensions(x=100, y=85, z=0)) + + decoy.when( + ot3_hardware_api.hardware_gripper.geometry.max_allowed_grip_error + ).then_return(6.0) + + decoy.when(ot3_hardware_api.hardware_gripper.jaw_width).then_return(89) + + decoy.when( + state_store.labware.get_grip_force("my-teleporting-labware") + ).then_return(100) + + decoy.when(state_store.labware.get_labware_offset("new-offset-id")).then_return( + LabwareOffset( + id="new-offset-id", + createdAt=datetime(year=2022, month=10, day=20), + definitionUri="my-labware", + location=LabwareOffsetLocation( + slotName=DeckSlotName.SLOT_5 + ), # this location doesn't matter for this test + vector=LabwareOffsetVector(x=0.5, y=0.6, z=0.7), + ) + ) + + @pytest.mark.ot3_only @pytest.fixture def subject( @@ -116,6 +176,83 @@ def subject( ) +@pytest.mark.ot3_only +async def test_check_labware_pickup( + decoy: Decoy, + state_store: StateStore, + thermocycler_plate_lifter: ThermocyclerPlateLifter, + ot3_hardware_api: OT3API, + subject: LabwareMovementHandler, + hardware_gripper_offset_data: Tuple[ + LabwareMovementOffsetData, LabwareMovementOffsetData + ], +) -> None: + """Test that the gripper position check is called at the right time.""" + # This function should only be called when after the gripper opens, + # and then closes again. This is when we expect the labware to be + # in the gripper jaws. + await set_up_decoy_hardware_gripper(decoy, ot3_hardware_api, state_store) + assert ot3_hardware_api.hardware_gripper + + user_offset_data, final_offset_data = hardware_gripper_offset_data + + starting_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_1) + to_location = DeckSlotLocation(slotName=DeckSlotName.SLOT_2) + + mock_tc_context_manager = decoy.mock() + decoy.when( + thermocycler_plate_lifter.lift_plate_for_labware_movement( + labware_location=starting_location + ) + ).then_return(mock_tc_context_manager) + + decoy.when( + state_store.geometry.get_final_labware_movement_offset_vectors( + from_location=starting_location, + to_location=to_location, + additional_offset_vector=user_offset_data, + ) + ).then_return(final_offset_data) + + decoy.when( + state_store.geometry.get_labware_grip_point( + labware_id="my-teleporting-labware", location=starting_location + ) + ).then_return(Point(101, 102, 119.5)) + + decoy.when( + state_store.geometry.get_labware_grip_point( + labware_id="my-teleporting-labware", location=to_location + ) + ).then_return(Point(201, 202, 219.5)) + + decoy.when(ot3_hardware_api.hardware_gripper.check_labware_pickup(85)).then_return() + + await subject.move_labware_with_gripper( + labware_id="my-teleporting-labware", + current_location=starting_location, + new_location=DeckSlotLocation(slotName=DeckSlotName.SLOT_2), + user_offset_data=user_offset_data, + post_drop_slide_offset=Point(x=1, y=1, z=1), + ) + + decoy.verify( + await ot3_hardware_api.home(axes=[Axis.Z_L, Axis.Z_R, Axis.Z_G]), + await mock_tc_context_manager.__aenter__(), + await ot3_hardware_api.grip(force_newtons=100), + await ot3_hardware_api.ungrip(), + await ot3_hardware_api.grip(force_newtons=100), + ot3_hardware_api.hardware_gripper.check_labware_pickup(labware_width=85), + await ot3_hardware_api.grip(force_newtons=100), + ot3_hardware_api.hardware_gripper.check_labware_pickup(labware_width=85), + await ot3_hardware_api.grip(force_newtons=100), + ot3_hardware_api.hardware_gripper.check_labware_pickup(labware_width=85), + await ot3_hardware_api.disengage_axes([Axis.Z_G]), + await ot3_hardware_api.ungrip(), + await ot3_hardware_api.ungrip(), + ) + + # TODO (spp, 2022-10-18): # 1. Should write an acceptance test w/ real labware on ot3 deck. # 2. This test will be split once waypoints generation is moved to motion planning. @@ -154,30 +291,17 @@ async def test_move_labware_with_gripper( from_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], to_location: Union[DeckSlotLocation, ModuleLocation, OnLabwareLocation], slide_offset: Optional[Point], + hardware_gripper_offset_data: Tuple[ + LabwareMovementOffsetData, LabwareMovementOffsetData + ], ) -> None: """It should perform a labware movement with gripper by delegating to OT3API.""" # TODO (spp, 2023-07-26): this test does NOT stub out movement waypoints in order to # keep this as the semi-smoke test that it previously was. We should add a proper # smoke test for gripper labware movement with actual labware and make this a unit test. + await set_up_decoy_hardware_gripper(decoy, ot3_hardware_api, state_store) - user_offset_data = LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=123, y=234, z=345), - dropOffset=LabwareOffsetVector(x=111, y=222, z=333), - ) - final_offset_data = LabwareMovementOffsetData( - pickUpOffset=LabwareOffsetVector(x=-1, y=-2, z=-3), - dropOffset=LabwareOffsetVector(x=1, y=2, z=3), - ) - - decoy.when(state_store.config.use_virtual_gripper).then_return(False) - decoy.when(ot3_hardware_api.has_gripper()).then_return(True) - decoy.when(ot3_hardware_api._gripper_handler.is_ready_for_jaw_home()).then_return( - True - ) - - decoy.when( - await ot3_hardware_api.gantry_position(mount=OT3Mount.GRIPPER) - ).then_return(Point(x=777, y=888, z=999)) + user_offset_data, final_offset_data = hardware_gripper_offset_data decoy.when( state_store.geometry.get_final_labware_movement_offset_vectors( @@ -192,15 +316,11 @@ async def test_move_labware_with_gripper( labware_id="my-teleporting-labware", location=from_location ) ).then_return(Point(101, 102, 119.5)) - decoy.when( state_store.geometry.get_labware_grip_point( labware_id="my-teleporting-labware", location=to_location ) ).then_return(Point(201, 202, 219.5)) - decoy.when( - state_store.labware.get_grip_force("my-teleporting-labware") - ).then_return(100) mock_tc_context_manager = decoy.mock() decoy.when( thermocycler_plate_lifter.lift_plate_for_labware_movement( @@ -208,18 +328,6 @@ async def test_move_labware_with_gripper( ) ).then_return(mock_tc_context_manager) - decoy.when(state_store.labware.get_labware_offset("new-offset-id")).then_return( - LabwareOffset( - id="new-offset-id", - createdAt=datetime(year=2022, month=10, day=20), - definitionUri="my-labware", - location=LabwareOffsetLocation( - slotName=DeckSlotName.SLOT_5 - ), # this location doesn't matter for this test - vector=LabwareOffsetVector(x=0.5, y=0.6, z=0.7), - ) - ) - expected_waypoints = [ Point(100, 100, 999), # move to above slot 1 Point(100, 100, 116.5), # move to labware on slot 1 diff --git a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py index 0ddf2d5dd2a..6a84810ff61 100644 --- a/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py +++ b/api/tests/opentrons/protocol_engine/execution/test_tip_handler.py @@ -8,6 +8,7 @@ from opentrons.types import Mount, MountType from opentrons.hardware_control import API as HardwareAPI from opentrons.hardware_control.types import TipStateType +from opentrons.hardware_control.protocols.types import OT2RobotType, FlexRobotType from opentrons.protocols.models import LabwareDefinition from opentrons.protocol_engine.state import StateView @@ -27,7 +28,9 @@ @pytest.fixture def mock_hardware_api(decoy: Decoy) -> HardwareAPI: """Get a mock in the shape of a HardwareAPI.""" - return decoy.mock(cls=HardwareAPI) + mock = decoy.mock(cls=HardwareAPI) + decoy.when(mock.get_robot_type()).then_return(OT2RobotType) + return mock @pytest.fixture @@ -365,6 +368,7 @@ async def test_get_tip_presence_on_ot3( from opentrons.hardware_control.ot3api import OT3API ot3_hardware_api = decoy.mock(cls=OT3API) + decoy.when(ot3_hardware_api.get_robot_type()).then_return(FlexRobotType) subject = HardwareTipHandler( state_view=mock_state_view, @@ -399,6 +403,7 @@ async def test_verify_tip_presence_on_ot3( from opentrons.hardware_control.ot3api import OT3API ot3_hardware_api = decoy.mock(cls=OT3API) + decoy.when(ot3_hardware_api.get_robot_type()).then_return(FlexRobotType) subject = HardwareTipHandler( state_view=mock_state_view, diff --git a/api/tests/opentrons/protocol_engine/resources/test_ot3_validation.py b/api/tests/opentrons/protocol_engine/resources/test_ot3_validation.py index d2382bf70a1..327daf82129 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_ot3_validation.py +++ b/api/tests/opentrons/protocol_engine/resources/test_ot3_validation.py @@ -6,6 +6,7 @@ from opentrons.protocol_engine.errors.exceptions import HardwareNotSupportedError from opentrons.hardware_control.api import API +from opentrons.hardware_control.protocols.types import FlexRobotType, OT2RobotType @pytest.mark.ot3_only @@ -16,6 +17,7 @@ def test_ensure_ot3_hardware(decoy: Decoy) -> None: from opentrons.hardware_control.ot3api import OT3API ot_3_hardware_api = decoy.mock(cls=OT3API) + decoy.when(ot_3_hardware_api.get_robot_type()).then_return(FlexRobotType) result = ensure_ot3_hardware( ot_3_hardware_api, ) @@ -28,6 +30,7 @@ def test_ensure_ot3_hardware(decoy: Decoy) -> None: def test_ensure_ot3_hardware_raises_error(decoy: Decoy) -> None: """Should raise a HardwareNotSupportedError exception.""" ot_2_hardware_api = decoy.mock(cls=API) + decoy.when(ot_2_hardware_api.get_robot_type()).then_return(OT2RobotType) with pytest.raises(HardwareNotSupportedError): ensure_ot3_hardware( ot_2_hardware_api, diff --git a/components/.npmignore b/components/.npmignore new file mode 100644 index 00000000000..b8cffed3c08 --- /dev/null +++ b/components/.npmignore @@ -0,0 +1,3 @@ +src +dist +*.tgz diff --git a/components/Makefile b/components/Makefile index a21d82b1d77..18f163e28f7 100644 --- a/components/Makefile +++ b/components/Makefile @@ -26,6 +26,11 @@ clean: dist: yarn --cwd .. build-storybook +.PHONY: lib +lib: export NODE_ENV := production +lib: + yarn webpack + # development ##################################################################### diff --git a/components/README.md b/components/README.md index 45d4fb5350c..9c4cc4cb0c0 100644 --- a/components/README.md +++ b/components/README.md @@ -68,20 +68,3 @@ Unit tests live in a `__tests__` directory in the same directory as the module u [jest-snapshots]: https://facebook.github.io/jest/docs/en/snapshot-testing.html [contributing]: ../CONTRIBUTING.md - -### Flow definitions - -While the components library is written in TypeScript, some dependents of the components library are not yet converted. To ease the conversion effort, `components/flow-types` contains automatically generated Flow type definitions from the TypeScript typings generated by `tsc`. - -To generate these definitions - -```shell -# ensure all TypeScript definitions are built -make build-ts - -# build flow definitions (this may take a while!) -make -C components flow-types - -# you can also build individual files if you're testing out small changes -make -C components flow-types/buttons/Button.js.flow -``` diff --git a/components/package.json b/components/package.json index 4570691b889..643bd433cd2 100644 --- a/components/package.json +++ b/components/package.json @@ -5,6 +5,8 @@ "source": "src/index.ts", "types": "lib/index.d.ts", "style": "src/index.css", + "main": "lib/opentrons-components.js", + "module": "src/index.ts", "repository": { "type": "git", "url": "git+https://github.com/Opentrons/opentrons.git" diff --git a/components/src/legacy-hardware-sim/Labware.css b/components/src/legacy-hardware-sim/Labware.css deleted file mode 100644 index a5a3c3db224..00000000000 --- a/components/src/legacy-hardware-sim/Labware.css +++ /dev/null @@ -1,23 +0,0 @@ -@import '..'; - -.fallback_plate_text { - height: 100%; - width: 100%; - font-size: var(--fs-body-1); - font-style: italic; - color: gray; - display: flex; - flex-direction: column; - justify-content: space-around; - align-items: center; -} - -.labware_outline { - fill: var(--c-white); - stroke: var(--c-black); -} - -.tiprack_outline { - fill: var(--c-plate-bg); - stroke: var(--c-charcoal); -} \ No newline at end of file diff --git a/components/src/legacy-hardware-sim/LabwareNameOverlay.tsx b/components/src/legacy-hardware-sim/LabwareNameOverlay.tsx index d199e118dcb..72ffa5ab43f 100644 --- a/components/src/legacy-hardware-sim/LabwareNameOverlay.tsx +++ b/components/src/legacy-hardware-sim/LabwareNameOverlay.tsx @@ -8,6 +8,7 @@ export interface LabwareNameOverlayProps { className?: string } +/** @deprecated use LabwareDisplayName or custom RobotCoordsForeignDiv */ export function LabwareNameOverlay( props: LabwareNameOverlayProps ): JSX.Element { diff --git a/components/src/legacy-hardware-sim/LegacyLabware.tsx b/components/src/legacy-hardware-sim/LegacyLabware.tsx deleted file mode 100644 index b7c253a6b80..00000000000 --- a/components/src/legacy-hardware-sim/LegacyLabware.tsx +++ /dev/null @@ -1,286 +0,0 @@ -// TODO(mc, 2020-02-19): still used but deprecated; remove when able -import * as React from 'react' -import map from 'lodash/map' -import assert from 'assert' -import cx from 'classnames' -import { - SLOT_RENDER_WIDTH, - SLOT_RENDER_HEIGHT, - getLabwareV1Def as getLabware, - wellIsRect, -} from '@opentrons/shared-data' -import type { LabwareDefinition1, WellDefinition } from '@opentrons/shared-data' -import { SELECTABLE_WELL_CLASS } from '../constants' - -import labwareStyles from './Labware.css' -import wellStyles from './Well.css' - -import { LabwareOutline, RobotCoordsForeignDiv } from '../hardware-sim' - -export interface LabwareProps { - /** labware type, to get legacy definition from shared-data */ - labwareType?: string - definition?: LabwareDefinition1 | null | undefined -} - -/** - * This is a legacy component that is only responsible - * for visualizing a labware schema v1 definition by def or loadName - * - * @deprecated Use {@link LabwareRender instead} - */ -export class LegacyLabware extends React.Component { - render(): JSX.Element { - const { labwareType, definition } = this.props - - const labwareDefinition = - definition || (labwareType ? getLabware(labwareType) : null) - - if (!labwareDefinition) { - return - } - - const tipVolume = - labwareDefinition.metadata && labwareDefinition.metadata.tipVolume - - const isTiprack = - labwareDefinition.metadata && labwareDefinition.metadata.isTiprack - - return ( - - - {map(labwareDefinition.wells, (wellDef, wellName) => { - assert( - wellDef, - `No well definition for labware ${ - labwareType || 'unknown labware' - }, well ${wellName}` - ) - // NOTE x + 1, y + 3 HACK offset from old getWellDefsForSVG has been purposefully - // left out here; it's intention was to make the well viz offset less "off" - return isTiprack ? ( - - ) : ( - - ) - })} - - ) - } -} - -// TODO: BC 2019-06-18 remove when old Labware component is no longer used anywhere -/** - * @deprecated No longer necessary, do not use - */ -export function FallbackLabware(): JSX.Element { - return ( - - - -

Custom Labware

-
-
- ) -} - -export interface TipProps { - wellDef: WellDefinition - tipVolume: number | null | undefined - empty?: boolean | null | undefined - highlighted?: boolean | null | undefined -} - -/** - * @deprecated No longer necessary, do not use - */ -export function Tip(props: TipProps): JSX.Element { - const { wellDef, empty, highlighted, tipVolume } = props - const circleProps = { - cx: wellDef.x, - cy: wellDef.y, - } - - // TODO: Ian 2018-08-13 refine tip sizes for different tip racks - let outerRadius = 3 - let innerRadius = 2 - - if (typeof tipVolume === 'number' && tipVolume > 20) { - outerRadius = 3.5 - innerRadius = 2.5 - } - - if (empty) { - return ( - - ) - } - - const outerCircleClassName = highlighted - ? wellStyles.highlighted - : wellStyles.tip_border - - return ( - - {/* Fill contents */} - - {/* Outer circle */} - - {/* Inner circle */} - - - ) -} - -export interface SingleWell { - wellName: string - highlighted?: boolean | null | undefined // highlighted is the same as hovered - selected?: boolean | null - error?: boolean | null - maxVolume?: number - fillColor?: string | null -} - -export interface WellProps extends SingleWell { - selectable?: boolean - wellDef: WellDefinition - onMouseOver?: React.MouseEventHandler - onMouseLeave?: React.MouseEventHandler - onMouseMove?: React.MouseEventHandler -} - -/** - * @deprecated No longer necessary, do not use - */ -export class Well extends React.Component { - shouldComponentUpdate(nextProps: WellProps): boolean { - return ( - this.props.highlighted !== nextProps.highlighted || - this.props.selected !== nextProps.selected || - this.props.fillColor !== nextProps.fillColor - ) - } - - render(): JSX.Element | null { - const { - wellName, - selectable, - highlighted, - selected, - error, - wellDef, - onMouseOver, - onMouseLeave, - onMouseMove, - } = this.props - - const fillColor = this.props.fillColor || 'transparent' - - const wellOverlayClassname = cx(wellStyles.well_border, { - [SELECTABLE_WELL_CLASS]: selectable, - [wellStyles.selected]: selected, - [wellStyles.selected_overlay]: selected, - [wellStyles.highlighted]: highlighted, - [wellStyles.error]: error, - }) - - const selectionProps = { - 'data-wellname': wellName, - onMouseOver, - onMouseLeave, - onMouseMove, - } - - const isRect = wellIsRect(wellDef) - const isCircle = !isRect - - if (isRect) { - const rectProps = { - x: wellDef.x, - y: wellDef.y, - width: wellDef.width, - height: wellDef.length, - } - - return ( - - {/* Fill contents */} - - {/* Border + overlay */} - - - ) - } - - if (isCircle) { - const circleProps = { - cx: wellDef.x, - cy: wellDef.y, - r: (wellDef.diameter || 0) / 2, - } - - return ( - - {/* Fill contents */} - - {/* Border + overlay */} - - - ) - } - - console.warn( - 'Invalid well: neither rectangle or circle: ' + JSON.stringify(wellDef) - ) - return null - } -} diff --git a/components/src/legacy-hardware-sim/ModuleViz.css b/components/src/legacy-hardware-sim/ModuleViz.css deleted file mode 100644 index 27d8941b018..00000000000 --- a/components/src/legacy-hardware-sim/ModuleViz.css +++ /dev/null @@ -1,19 +0,0 @@ -@import '..'; - -.module_viz { - rx: 6; - stroke: var(--c-plate-bg); - stroke-width: 1; - fill: var(--c-lightest-gray); -} - -.module_slot_area { - fill: transparent; - stroke: var(--c-plate-bg); - stroke-width: 2; - stroke-dasharray: 8 4; -} - -.module_slot_text { - fill: var(--c-plate-bg); -} diff --git a/components/src/legacy-hardware-sim/ModuleViz.tsx b/components/src/legacy-hardware-sim/ModuleViz.tsx deleted file mode 100644 index 5bf4f71d383..00000000000 --- a/components/src/legacy-hardware-sim/ModuleViz.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import * as React from 'react' -import styles from './ModuleViz.css' -import { getModuleVizDims, ModuleType } from '@opentrons/shared-data' - -interface Props { - x: number - y: number - orientation: 'left' | 'right' - moduleType: ModuleType -} - -export const ModuleViz = (props: Props): JSX.Element => { - const moduleType = props.moduleType - const { - xOffset, - yOffset, - xDimension, - yDimension, - childXOffset, - childYOffset, - childXDimension, - childYDimension, - } = getModuleVizDims(props.orientation, moduleType) - - return ( - - - - - ) -} diff --git a/components/src/legacy-hardware-sim/Well.css b/components/src/legacy-hardware-sim/Well.css deleted file mode 100644 index 9ab048b0a6a..00000000000 --- a/components/src/legacy-hardware-sim/Well.css +++ /dev/null @@ -1,48 +0,0 @@ -@import '@opentrons/components'; - -/* Well fill contents, the "bottom layer" */ - -.well_fill { - fill: currentColor; -} - -/* Styles for border + overlay, the "top layer" */ - -.well_border { - stroke-width: 0.4; - stroke: var(--c-black); - fill: transparent; -} - -.selected { - stroke-width: 1; - stroke: var(--c-highlight); - fill: color-mod(var(--c-highlight) alpha(20%)); -} - -.highlighted { - stroke-width: 1; - stroke: var(--c-highlight); - fill: transparent; -} - -.error { - stroke-width: 1; - stroke: var(--c-red); -} - -/* Tip-specific */ - -.tip_border { - stroke-width: 0.6; - stroke: var(--c-near-black); - fill: transparent; -} - -.empty_tip { - fill: color-mod(var(--c-charcoal) alpha(40%)); -} - -.tip_fill { - fill: var(--c-white); -} \ No newline at end of file diff --git a/components/src/legacy-hardware-sim/constants.ts b/components/src/legacy-hardware-sim/constants.ts deleted file mode 100644 index 0510e32e210..00000000000 --- a/components/src/legacy-hardware-sim/constants.ts +++ /dev/null @@ -1,15 +0,0 @@ -// @deprecated TODO: remove final usage of this in PD and replace with deck definition accessor -export const sortedSlotnames = [ - '1', - '2', - '3', - '4', - '5', - '6', - '7', - '8', - '9', - '10', - '11', - '12', -] diff --git a/components/src/legacy-hardware-sim/index.ts b/components/src/legacy-hardware-sim/index.ts index 61ff1079b09..0f915451971 100644 --- a/components/src/legacy-hardware-sim/index.ts +++ b/components/src/legacy-hardware-sim/index.ts @@ -1,5 +1,2 @@ -export * from './constants' export * from './LabwareNameOverlay' -export * from './ModuleViz' // legacy PD module rendering export * from './ModuleItem' // legacy App module rendering -export * from './LegacyLabware' diff --git a/components/webpack.config.js b/components/webpack.config.js new file mode 100644 index 00000000000..74bcc0c6ff5 --- /dev/null +++ b/components/webpack.config.js @@ -0,0 +1,19 @@ +'use strict' + +const path = require('path') +const webpackMerge = require('webpack-merge') +const { baseConfig } = require('@opentrons/webpack-config') + +const ENTRY_INDEX = path.join(__dirname, 'src/index.ts') +const OUTPUT_PATH = path.join(__dirname, 'lib') + +module.exports = async () => + webpackMerge(baseConfig, { + entry: { index: ENTRY_INDEX }, + output: { + path: OUTPUT_PATH, + filename: 'opentrons-components.js', + library: '@opentrons/components', + libraryTarget: 'umd', + }, + }) diff --git a/discovery-client/package.json b/discovery-client/package.json index dc351992afb..f52d56a4be9 100644 --- a/discovery-client/package.json +++ b/discovery-client/package.json @@ -3,7 +3,6 @@ "version": "0.0.0-dev", "description": "Node.js client for discovering Opentrons robots on the network", "main": "lib/index.js", - "flow:main": "flow-types/index.js.flow", "types": "lib/index.d.ts", "source": "src/index.ts", "bin": { diff --git a/hardware-testing/hardware_testing/drivers/asair_sensor.py b/hardware-testing/hardware_testing/drivers/asair_sensor.py index 4e30c743045..f1d694cf105 100644 --- a/hardware-testing/hardware_testing/drivers/asair_sensor.py +++ b/hardware-testing/hardware_testing/drivers/asair_sensor.py @@ -185,8 +185,9 @@ def get_reading(self) -> Reading: def get_serial(self) -> str: """Read the device ID register.""" - serial_addr = "0A" - data_packet = "{}0300000002{}".format(serial_addr, addrs[serial_addr]) + data_packet = "{}0300000002{}".format( + self._sensor_address, addrs[self._sensor_address] + ) log.debug(f"sending {data_packet}") command_bytes = codecs.decode(data_packet.encode(), "hex") try: @@ -197,6 +198,7 @@ def get_serial(self) -> str: length = self._th_sensor.inWaiting() res = self._th_sensor.read(length) + res = codecs.encode(res, "hex") log.debug(f"received {res}") dev_id = res[6:14] return dev_id.decode() diff --git a/protocol-designer/cypress/integration/migrations.spec.js b/protocol-designer/cypress/integration/migrations.spec.js index d820395fee2..f6b6e9c51f7 100644 --- a/protocol-designer/cypress/integration/migrations.spec.js +++ b/protocol-designer/cypress/integration/migrations.spec.js @@ -53,15 +53,22 @@ describe('Protocol fixtures migrate and match snapshots', () => { migrationModal: 'v8', unusedPipettes: false, }, - // TODO(jr, 11/30/23): write a test fixture here for v8 migrated to v8 with deck config when the ff is removed - // { - // title: 'doItAllV8 flex robot -> reimported', - // importFixture: '../../fixtures/protocol/8/doItAllV8.json', - // expectedExportFixture: - // '../../fixtures/protocol/8/doItAllV8.json', - // migrationModal: 'noBehaviorChange', - // unusedPipettes: false, - // }, + { + title: '96-channel full and column schema 8 -> reimported as schema 8', + importFixture: + '../../fixtures/protocol/8/ninetySixChannelFullAndColumn.json', + expectedExportFixture: + '../../fixtures/protocol/8/ninetySixChannelFullAndColumn.json', + migrationModal: null, + unusedPipettes: false, + }, + { + title: 'doItAllV8 flex robot -> reimported, should not migrate', + importFixture: '../../fixtures/protocol/8/doItAllV8.json', + expectedExportFixture: '../../fixtures/protocol/8/doItAllV8.json', + migrationModal: null, + unusedPipettes: false, + }, ] testCases.forEach( @@ -159,7 +166,7 @@ describe('Protocol fixtures migrate and match snapshots', () => { ).forEach(stepForm => { if (stepForm.stepType === 'moveLiquid') { stepForm.dropTip_location = 'trash drop tip location' - if (stepForm.blowout_location.includes('trashBin')) { + if (stepForm.blowout_location?.includes('trashBin')) { stepForm.blowout_location = 'trash blowout location' } } diff --git a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json index 299a9b4bec9..f03c0211145 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV3MigratedToV8.json @@ -115,7 +115,8 @@ "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", - "stepDetails": "" + "stepDetails": "", + "nozzles": null }, "54dc3200-75c7-11ea-b42f-4b64e50f43e5": { "pauseAction": "untilResume", @@ -167,7 +168,8 @@ "id": "a4cee9a0-75dc-11ea-b42f-4b64e50f43e5", "stepType": "mix", "stepName": "mix", - "stepDetails": "" + "stepDetails": "", + "nozzles": null } }, "orderedStepIds": [ diff --git a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json index cc131ed8ffc..34db8b9efee 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV4MigratedToV8.json @@ -149,7 +149,8 @@ "id": "3961e4c0-75c7-11ea-b42f-4b64e50f43e5", "stepType": "moveLiquid", "stepName": "transfer", - "stepDetails": "" + "stepDetails": "", + "nozzles": null }, "4f4057e0-75c7-11ea-b42f-4b64e50f43e5": { "moduleId": "0b419310-75c7-11ea-b42f-4b64e50f43e5:magneticModuleType", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json index 5c390164328..beaa9e2a71c 100644 --- a/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/doItAllV7MigratedToV8.json @@ -191,7 +191,8 @@ "id": "f9a294f1-f42b-4cae-893a-592405349d56", "stepType": "moveLiquid", "stepName": "transfer", - "stepDetails": "" + "stepDetails": "", + "nozzles": null }, "5fdb9a12-fab4-42fd-886f-40af107b15d6": { "times": "2", @@ -217,7 +218,8 @@ "id": "5fdb9a12-fab4-42fd-886f-40af107b15d6", "stepType": "mix", "stepName": "mix", - "stepDetails": "" + "stepDetails": "", + "nozzles": null }, "3901f6f9-cecd-4d2a-8d85-40d85f9f8b4f": { "labware": "fcba73e7-b88e-438e-963e-f8b9a5de0983:opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2", diff --git a/protocol-designer/fixtures/protocol/8/doItAllV8.json b/protocol-designer/fixtures/protocol/8/doItAllV8.json new file mode 100644 index 00000000000..c07bae330b3 --- /dev/null +++ b/protocol-designer/fixtures/protocol/8/doItAllV8.json @@ -0,0 +1,4060 @@ +{ + "$otSharedSchema": "#/protocol/schemas/8", + "schemaVersion": 8, + "metadata": { + "protocolName": "doItAllV8", + "author": "", + "description": "", + "created": 1701659107408, + "lastModified": 1701659583856, + "category": null, + "subcategory": null, + "tags": [] + }, + "designerApplication": { + "name": "opentrons/protocol-designer", + "version": "8.0.0", + "data": { + "_internalAppBuildDate": "Mon, 04 Dec 2023 03:04:04 GMT", + "defaultValues": { + "aspirate_mmFromBottom": 1, + "dispense_mmFromBottom": 0.5, + "touchTip_mmFromTop": -1, + "blowout_mmFromTop": 0 + }, + "pipetteTiprackAssignments": { + "9fcd50d9-92b2-45ac-acf1-e2cf773feffc": "opentrons/opentrons_flex_96_tiprack_1000ul/1" + }, + "dismissedWarnings": { "form": {}, "timeline": {} }, + "ingredients": { + "0": { + "name": "h20", + "displayColor": "#b925ff", + "description": null, + "serialize": false, + "liquidGroupId": "0" + }, + "1": { + "name": "sample", + "displayColor": "#ffd600", + "description": null, + "serialize": false, + "liquidGroupId": "1" + } + }, + "ingredLocations": { + "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1": { + "A1": { "0": { "volume": 10000 } } + }, + "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2": { + "A1": { "1": { "volume": 100 } }, + "B1": { "1": { "volume": 100 } }, + "C1": { "1": { "volume": 100 } }, + "D1": { "1": { "volume": 100 } }, + "E1": { "1": { "volume": 100 } }, + "F1": { "1": { "volume": 100 } }, + "G1": { "1": { "volume": 100 } }, + "H1": { "1": { "volume": 100 } } + } + }, + "savedStepForms": { + "__INITIAL_DECK_SETUP_STEP__": { + "stepType": "manualIntervention", + "id": "__INITIAL_DECK_SETUP_STEP__", + "labwareLocationUpdate": { + "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1": "C2", + "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1": "A4" + }, + "pipetteLocationUpdate": { + "9fcd50d9-92b2-45ac-acf1-e2cf773feffc": "left" + }, + "moduleLocationUpdate": { + "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType": "D1", + "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType": "B1" + } + }, + "dcec0c89-338b-453b-a79b-c081830ff138": { + "id": "dcec0c89-338b-453b-a79b-c081830ff138", + "stepType": "thermocycler", + "stepName": "thermocycler", + "stepDetails": "", + "thermocyclerFormType": "thermocyclerState", + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "blockIsActive": false, + "blockTargetTemp": null, + "lidIsActive": false, + "lidTargetTemp": null, + "lidOpen": true, + "profileVolume": null, + "profileTargetLidTemp": null, + "orderedProfileItems": [], + "profileItemsById": {}, + "blockIsActiveHold": false, + "blockTargetTempHold": null, + "lidIsActiveHold": false, + "lidTargetTempHold": null, + "lidOpenHold": null + }, + "e6904828-c44c-4c25-8144-1b7921b5f8dc": { + "id": "e6904828-c44c-4c25-8144-1b7921b5f8dc", + "stepType": "moveLabware", + "stepName": "move labware", + "stepDetails": "", + "labware": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "useGripper": true, + "newLocation": "C1" + }, + "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7": { + "id": "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", + "stepType": "moveLiquid", + "stepName": "transfer", + "stepDetails": "", + "pipette": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": "100", + "changeTip": "always", + "path": "single", + "aspirate_wells_grouped": false, + "aspirate_flowRate": null, + "aspirate_labware": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "aspirate_wells": ["A1"], + "aspirate_wellOrder_first": "t2b", + "aspirate_wellOrder_second": "l2r", + "aspirate_mix_checkbox": false, + "aspirate_mix_times": null, + "aspirate_mix_volume": null, + "aspirate_mmFromBottom": null, + "aspirate_touchTip_checkbox": false, + "aspirate_touchTip_mmFromBottom": null, + "dispense_flowRate": null, + "dispense_labware": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "dispense_wells": ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + "dispense_wellOrder_first": "t2b", + "dispense_wellOrder_second": "l2r", + "dispense_mix_checkbox": false, + "dispense_mix_times": null, + "dispense_mix_volume": null, + "dispense_mmFromBottom": null, + "dispense_touchTip_checkbox": false, + "dispense_touchTip_mmFromBottom": null, + "disposalVolume_checkbox": true, + "disposalVolume_volume": "5", + "blowout_checkbox": false, + "blowout_location": null, + "preWetTip": false, + "aspirate_airGap_checkbox": false, + "aspirate_airGap_volume": "5", + "aspirate_delay_checkbox": false, + "aspirate_delay_mmFromBottom": null, + "aspirate_delay_seconds": "1", + "dispense_airGap_checkbox": false, + "dispense_airGap_volume": "5", + "dispense_delay_checkbox": false, + "dispense_delay_seconds": "1", + "dispense_delay_mmFromBottom": null, + "dropTip_location": "9d61f642-8f9b-467d-b2f7-b67fb162fd26:wasteChute", + "nozzles": null + }, + "240a2c96-3db8-4679-bdac-049306b7b9c4": { + "id": "240a2c96-3db8-4679-bdac-049306b7b9c4", + "stepType": "thermocycler", + "stepName": "thermocycler", + "stepDetails": "", + "thermocyclerFormType": "thermocyclerState", + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "blockIsActive": true, + "blockTargetTemp": "40", + "lidIsActive": false, + "lidTargetTemp": null, + "lidOpen": false, + "profileVolume": null, + "profileTargetLidTemp": null, + "orderedProfileItems": [], + "profileItemsById": {}, + "blockIsActiveHold": false, + "blockTargetTempHold": null, + "lidIsActiveHold": false, + "lidTargetTempHold": null, + "lidOpenHold": null + }, + "bfa5b548-f9eb-4a80-95d5-26e41cc2c69e": { + "id": "bfa5b548-f9eb-4a80-95d5-26e41cc2c69e", + "stepType": "pause", + "stepName": "pause", + "stepDetails": "", + "pauseAction": "untilTime", + "pauseHour": null, + "pauseMinute": "1", + "pauseSecond": null, + "pauseMessage": "", + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "pauseTemperature": null + }, + "bc3245e5-b22e-492a-9937-03aed3a07710": { + "id": "bc3245e5-b22e-492a-9937-03aed3a07710", + "stepType": "thermocycler", + "stepName": "thermocycler", + "stepDetails": "", + "thermocyclerFormType": "thermocyclerState", + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "blockIsActive": false, + "blockTargetTemp": null, + "lidIsActive": false, + "lidTargetTemp": null, + "lidOpen": true, + "profileVolume": null, + "profileTargetLidTemp": null, + "orderedProfileItems": [], + "profileItemsById": {}, + "blockIsActiveHold": false, + "blockTargetTempHold": null, + "lidIsActiveHold": false, + "lidTargetTempHold": null, + "lidOpenHold": null + }, + "8782dcc1-8960-4d13-9b29-e8837228ba56": { + "id": "8782dcc1-8960-4d13-9b29-e8837228ba56", + "stepType": "moveLabware", + "stepName": "move labware", + "stepDetails": "", + "labware": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "useGripper": true, + "newLocation": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1" + }, + "b5dc46b1-52ac-4b61-9318-778fb437d1ef": { + "id": "b5dc46b1-52ac-4b61-9318-778fb437d1ef", + "stepType": "heaterShaker", + "stepName": "heater-shaker", + "stepDetails": "", + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "setHeaterShakerTemperature": null, + "targetHeaterShakerTemperature": null, + "targetSpeed": null, + "setShake": null, + "latchOpen": true, + "heaterShakerSetTimer": null, + "heaterShakerTimerMinutes": null, + "heaterShakerTimerSeconds": null + }, + "8622d8f8-acbc-48ff-86f8-0476d1de0e02": { + "id": "8622d8f8-acbc-48ff-86f8-0476d1de0e02", + "stepType": "heaterShaker", + "stepName": "heater-shaker", + "stepDetails": "", + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "setHeaterShakerTemperature": false, + "targetHeaterShakerTemperature": null, + "targetSpeed": "200", + "setShake": true, + "latchOpen": false, + "heaterShakerSetTimer": true, + "heaterShakerTimerMinutes": "1", + "heaterShakerTimerSeconds": null + }, + "07dd4472-3ea4-475c-8fd3-18819519b401": { + "id": "07dd4472-3ea4-475c-8fd3-18819519b401", + "stepType": "moveLabware", + "stepName": "move labware", + "stepDetails": "", + "labware": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "useGripper": true, + "newLocation": "B4" + }, + "2b8f84e2-b079-41e8-a66e-ff8d9c5dfe1d": { + "id": "2b8f84e2-b079-41e8-a66e-ff8d9c5dfe1d", + "stepType": "heaterShaker", + "stepName": "heater-shaker", + "stepDetails": "", + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "setHeaterShakerTemperature": null, + "targetHeaterShakerTemperature": null, + "targetSpeed": null, + "setShake": null, + "latchOpen": true, + "heaterShakerSetTimer": null, + "heaterShakerTimerMinutes": null, + "heaterShakerTimerSeconds": null + }, + "ed84f11e-db82-4039-9e04-e619b03af42f": { + "id": "ed84f11e-db82-4039-9e04-e619b03af42f", + "stepType": "moveLabware", + "stepName": "move labware", + "stepDetails": "", + "labware": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "useGripper": true, + "newLocation": "cutoutD3" + } + }, + "orderedStepIds": [ + "dcec0c89-338b-453b-a79b-c081830ff138", + "e6904828-c44c-4c25-8144-1b7921b5f8dc", + "d2f74144-a7bf-4ba2-aaab-30d70b2b62c7", + "240a2c96-3db8-4679-bdac-049306b7b9c4", + "bfa5b548-f9eb-4a80-95d5-26e41cc2c69e", + "bc3245e5-b22e-492a-9937-03aed3a07710", + "b5dc46b1-52ac-4b61-9318-778fb437d1ef", + "8782dcc1-8960-4d13-9b29-e8837228ba56", + "8622d8f8-acbc-48ff-86f8-0476d1de0e02", + "2b8f84e2-b079-41e8-a66e-ff8d9c5dfe1d", + "07dd4472-3ea4-475c-8fd3-18819519b401", + "ed84f11e-db82-4039-9e04-e619b03af42f" + ] + } + }, + "robot": { "model": "OT-3 Standard", "deckId": "ot3_standard" }, + "labwareDefinitionSchemaId": "opentronsLabwareSchemaV2", + "labwareDefinitions": { + "opentrons/opentrons_flex_96_tiprack_1000ul/1": { + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { "brand": "Opentrons", "brandId": [] }, + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.47, + "totalLiquidVolume": 1000, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 95.6, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_flex_96_tiprack_1000ul" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + } + }, + "opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2": { + "namespace": "opentrons", + "version": 2, + "schemaVersion": 2, + "parameters": { + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "format": "96Standard", + "isTiprack": false, + "isMagneticModuleCompatible": true + }, + "metadata": { + "displayName": "Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "brand": { + "brand": "Opentrons", + "brandId": ["991-00076"], + "links": [ + "https://shop.opentrons.com/tough-0.2-ml-96-well-pcr-plate-full-skirt/" + ] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.48, + "zDimension": 16 + }, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "stackingOffsetWithLabware": { + "opentrons_96_pcr_adapter": { "x": 0, "y": 0, "z": 10.95 }, + "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 11.91 } + }, + "stackingOffsetWithModule": { + "magneticBlockV1": { "x": 0, "y": 0, "z": 3.54 }, + "thermocyclerModuleV2": { "x": 0, "y": 0, "z": 10.7 } + }, + "gripForce": 9, + "gripHeightFromLabwareBottom": 10, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "wells": { + "A1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 74.24, + "z": 1.05 + }, + "B1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 65.24, + "z": 1.05 + }, + "C1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 56.24, + "z": 1.05 + }, + "D1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 47.24, + "z": 1.05 + }, + "E1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 38.24, + "z": 1.05 + }, + "F1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 29.24, + "z": 1.05 + }, + "G1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 20.24, + "z": 1.05 + }, + "H1": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 14.38, + "y": 11.24, + "z": 1.05 + }, + "A2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 74.24, + "z": 1.05 + }, + "B2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 65.24, + "z": 1.05 + }, + "C2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 56.24, + "z": 1.05 + }, + "D2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 47.24, + "z": 1.05 + }, + "E2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 38.24, + "z": 1.05 + }, + "F2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 29.24, + "z": 1.05 + }, + "G2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 20.24, + "z": 1.05 + }, + "H2": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 23.38, + "y": 11.24, + "z": 1.05 + }, + "A3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 74.24, + "z": 1.05 + }, + "B3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 65.24, + "z": 1.05 + }, + "C3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 56.24, + "z": 1.05 + }, + "D3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 47.24, + "z": 1.05 + }, + "E3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 38.24, + "z": 1.05 + }, + "F3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 29.24, + "z": 1.05 + }, + "G3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 20.24, + "z": 1.05 + }, + "H3": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 32.38, + "y": 11.24, + "z": 1.05 + }, + "A4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 74.24, + "z": 1.05 + }, + "B4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 65.24, + "z": 1.05 + }, + "C4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 56.24, + "z": 1.05 + }, + "D4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 47.24, + "z": 1.05 + }, + "E4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 38.24, + "z": 1.05 + }, + "F4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 29.24, + "z": 1.05 + }, + "G4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 20.24, + "z": 1.05 + }, + "H4": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 41.38, + "y": 11.24, + "z": 1.05 + }, + "A5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 74.24, + "z": 1.05 + }, + "B5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 65.24, + "z": 1.05 + }, + "C5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 56.24, + "z": 1.05 + }, + "D5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 47.24, + "z": 1.05 + }, + "E5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 38.24, + "z": 1.05 + }, + "F5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 29.24, + "z": 1.05 + }, + "G5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 20.24, + "z": 1.05 + }, + "H5": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 50.38, + "y": 11.24, + "z": 1.05 + }, + "A6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 74.24, + "z": 1.05 + }, + "B6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 65.24, + "z": 1.05 + }, + "C6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 56.24, + "z": 1.05 + }, + "D6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 47.24, + "z": 1.05 + }, + "E6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 38.24, + "z": 1.05 + }, + "F6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 29.24, + "z": 1.05 + }, + "G6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 20.24, + "z": 1.05 + }, + "H6": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 59.38, + "y": 11.24, + "z": 1.05 + }, + "A7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 74.24, + "z": 1.05 + }, + "B7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 65.24, + "z": 1.05 + }, + "C7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 56.24, + "z": 1.05 + }, + "D7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 47.24, + "z": 1.05 + }, + "E7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 38.24, + "z": 1.05 + }, + "F7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 29.24, + "z": 1.05 + }, + "G7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 20.24, + "z": 1.05 + }, + "H7": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 68.38, + "y": 11.24, + "z": 1.05 + }, + "A8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 74.24, + "z": 1.05 + }, + "B8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 65.24, + "z": 1.05 + }, + "C8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 56.24, + "z": 1.05 + }, + "D8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 47.24, + "z": 1.05 + }, + "E8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 38.24, + "z": 1.05 + }, + "F8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 29.24, + "z": 1.05 + }, + "G8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 20.24, + "z": 1.05 + }, + "H8": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 77.38, + "y": 11.24, + "z": 1.05 + }, + "A9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 74.24, + "z": 1.05 + }, + "B9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 65.24, + "z": 1.05 + }, + "C9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 56.24, + "z": 1.05 + }, + "D9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 47.24, + "z": 1.05 + }, + "E9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 38.24, + "z": 1.05 + }, + "F9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 29.24, + "z": 1.05 + }, + "G9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 20.24, + "z": 1.05 + }, + "H9": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 86.38, + "y": 11.24, + "z": 1.05 + }, + "A10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 74.24, + "z": 1.05 + }, + "B10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 65.24, + "z": 1.05 + }, + "C10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 56.24, + "z": 1.05 + }, + "D10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 47.24, + "z": 1.05 + }, + "E10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 38.24, + "z": 1.05 + }, + "F10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 29.24, + "z": 1.05 + }, + "G10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 20.24, + "z": 1.05 + }, + "H10": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 95.38, + "y": 11.24, + "z": 1.05 + }, + "A11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 74.24, + "z": 1.05 + }, + "B11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 65.24, + "z": 1.05 + }, + "C11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 56.24, + "z": 1.05 + }, + "D11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 47.24, + "z": 1.05 + }, + "E11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 38.24, + "z": 1.05 + }, + "F11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 29.24, + "z": 1.05 + }, + "G11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 20.24, + "z": 1.05 + }, + "H11": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 104.38, + "y": 11.24, + "z": 1.05 + }, + "A12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 74.24, + "z": 1.05 + }, + "B12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 65.24, + "z": 1.05 + }, + "C12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 56.24, + "z": 1.05 + }, + "D12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 47.24, + "z": 1.05 + }, + "E12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 38.24, + "z": 1.05 + }, + "F12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 29.24, + "z": 1.05 + }, + "G12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 20.24, + "z": 1.05 + }, + "H12": { + "depth": 14.95, + "totalLiquidVolume": 200, + "shape": "circular", + "diameter": 5.5, + "x": 113.38, + "y": 11.24, + "z": 1.05 + } + }, + "groups": [ + { + "metadata": { "wellBottomShape": "v" }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ] + }, + "opentrons/opentrons_96_pcr_adapter/1": { + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { "brand": "Opentrons", "brandId": [] }, + "metadata": { + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 111, + "yDimension": 75, + "zDimension": 13.85 + }, + "wells": { + "A1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 69, + "z": 1.85 + }, + "B1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 60, + "z": 1.85 + }, + "C1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 51, + "z": 1.85 + }, + "D1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 42, + "z": 1.85 + }, + "E1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 33, + "z": 1.85 + }, + "F1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 24, + "z": 1.85 + }, + "G1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 15, + "z": 1.85 + }, + "H1": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 6, + "y": 6, + "z": 1.85 + }, + "A2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 69, + "z": 1.85 + }, + "B2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 60, + "z": 1.85 + }, + "C2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 51, + "z": 1.85 + }, + "D2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 42, + "z": 1.85 + }, + "E2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 33, + "z": 1.85 + }, + "F2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 24, + "z": 1.85 + }, + "G2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 15, + "z": 1.85 + }, + "H2": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 15, + "y": 6, + "z": 1.85 + }, + "A3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 69, + "z": 1.85 + }, + "B3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 60, + "z": 1.85 + }, + "C3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 51, + "z": 1.85 + }, + "D3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 42, + "z": 1.85 + }, + "E3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 33, + "z": 1.85 + }, + "F3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 24, + "z": 1.85 + }, + "G3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 15, + "z": 1.85 + }, + "H3": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 24, + "y": 6, + "z": 1.85 + }, + "A4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 69, + "z": 1.85 + }, + "B4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 60, + "z": 1.85 + }, + "C4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 51, + "z": 1.85 + }, + "D4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 42, + "z": 1.85 + }, + "E4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 33, + "z": 1.85 + }, + "F4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 24, + "z": 1.85 + }, + "G4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 15, + "z": 1.85 + }, + "H4": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 33, + "y": 6, + "z": 1.85 + }, + "A5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 69, + "z": 1.85 + }, + "B5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 60, + "z": 1.85 + }, + "C5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 51, + "z": 1.85 + }, + "D5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 42, + "z": 1.85 + }, + "E5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 33, + "z": 1.85 + }, + "F5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 24, + "z": 1.85 + }, + "G5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 15, + "z": 1.85 + }, + "H5": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 42, + "y": 6, + "z": 1.85 + }, + "A6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 69, + "z": 1.85 + }, + "B6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 60, + "z": 1.85 + }, + "C6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 51, + "z": 1.85 + }, + "D6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 42, + "z": 1.85 + }, + "E6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 33, + "z": 1.85 + }, + "F6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 24, + "z": 1.85 + }, + "G6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 15, + "z": 1.85 + }, + "H6": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 51, + "y": 6, + "z": 1.85 + }, + "A7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 69, + "z": 1.85 + }, + "B7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 60, + "z": 1.85 + }, + "C7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 51, + "z": 1.85 + }, + "D7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 42, + "z": 1.85 + }, + "E7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 33, + "z": 1.85 + }, + "F7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 24, + "z": 1.85 + }, + "G7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 15, + "z": 1.85 + }, + "H7": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 60, + "y": 6, + "z": 1.85 + }, + "A8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 69, + "z": 1.85 + }, + "B8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 60, + "z": 1.85 + }, + "C8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 51, + "z": 1.85 + }, + "D8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 42, + "z": 1.85 + }, + "E8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 33, + "z": 1.85 + }, + "F8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 24, + "z": 1.85 + }, + "G8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 15, + "z": 1.85 + }, + "H8": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 69, + "y": 6, + "z": 1.85 + }, + "A9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 69, + "z": 1.85 + }, + "B9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 60, + "z": 1.85 + }, + "C9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 51, + "z": 1.85 + }, + "D9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 42, + "z": 1.85 + }, + "E9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 33, + "z": 1.85 + }, + "F9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 24, + "z": 1.85 + }, + "G9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 15, + "z": 1.85 + }, + "H9": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 78, + "y": 6, + "z": 1.85 + }, + "A10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 69, + "z": 1.85 + }, + "B10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 60, + "z": 1.85 + }, + "C10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 51, + "z": 1.85 + }, + "D10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 42, + "z": 1.85 + }, + "E10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 33, + "z": 1.85 + }, + "F10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 24, + "z": 1.85 + }, + "G10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 15, + "z": 1.85 + }, + "H10": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 87, + "y": 6, + "z": 1.85 + }, + "A11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 69, + "z": 1.85 + }, + "B11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 60, + "z": 1.85 + }, + "C11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 51, + "z": 1.85 + }, + "D11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 42, + "z": 1.85 + }, + "E11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 33, + "z": 1.85 + }, + "F11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 24, + "z": 1.85 + }, + "G11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 15, + "z": 1.85 + }, + "H11": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 96, + "y": 6, + "z": 1.85 + }, + "A12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 69, + "z": 1.85 + }, + "B12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 60, + "z": 1.85 + }, + "C12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 51, + "z": 1.85 + }, + "D12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 42, + "z": 1.85 + }, + "E12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 33, + "z": 1.85 + }, + "F12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 24, + "z": 1.85 + }, + "G12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 15, + "z": 1.85 + }, + "H12": { + "depth": 12, + "shape": "circular", + "diameter": 5.64, + "totalLiquidVolume": 0, + "x": 105, + "y": 6, + "z": 1.85 + } + }, + "groups": [ + { + "metadata": { "wellBottomShape": "v" }, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_96_pcr_adapter" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { "x": 8.5, "y": 5.5, "z": 0 }, + "gripperOffsets": { + "default": { + "pickUpOffset": { "x": 0, "y": 0, "z": 0 }, + "dropOffset": { "x": 0, "y": 0, "z": 1 } + } + } + }, + "opentrons/axygen_1_reservoir_90ml/1": { + "ordering": [["A1"]], + "brand": { + "brand": "Axygen", + "brandId": ["RES-SW1-LP"], + "links": [ + "https://ecatalog.corning.com/life-sciences/b2c/US/en/Genomics-%26-Molecular-Biology/Automation-Consumables/Automation-Reservoirs/Axygen%C2%AE-Reagent-Reservoirs/p/RES-SW1-LP?clear=true" + ] + }, + "metadata": { + "displayName": "Axygen 1 Well Reservoir 90 mL", + "displayCategory": "reservoir", + "displayVolumeUnits": "mL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.76, + "yDimension": 85.47, + "zDimension": 19.05 + }, + "wells": { + "A1": { + "depth": 12.42, + "shape": "rectangular", + "xDimension": 106.76, + "yDimension": 70.52, + "totalLiquidVolume": 90000, + "x": 63.88, + "y": 42.735, + "z": 6.63 + } + }, + "groups": [ + { "wells": ["A1"], "metadata": { "wellBottomShape": "flat" } } + ], + "parameters": { + "format": "trough", + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "axygen_1_reservoir_90ml", + "quirks": ["centerMultichannelOnWells", "touchTipDisabled"] + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 } + } + }, + "liquidSchemaId": "opentronsLiquidSchemaV1", + "liquids": { + "0": { "displayName": "h20", "description": "", "displayColor": "#b925ff" }, + "1": { + "displayName": "sample", + "description": "", + "displayColor": "#ffd600" + } + }, + "commandSchemaId": "opentronsCommandSchemaV8", + "commands": [ + { + "key": "218e9dfc-6036-4b3a-961a-6ae1b15a6a9e", + "commandType": "loadPipette", + "params": { + "pipetteName": "p1000_single_flex", + "mount": "left", + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" + } + }, + { + "key": "85f93f33-67e9-41f6-b749-3d10d20c3c46", + "commandType": "loadModule", + "params": { + "model": "heaterShakerModuleV1", + "location": { "slotName": "D1" }, + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "key": "3d216d80-8112-4c67-8bf7-c5901f490ac4", + "commandType": "loadModule", + "params": { + "model": "thermocyclerModuleV2", + "location": { "slotName": "B1" }, + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "key": "3dd786db-df11-4fc4-8277-8e65c8894d4a", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons 96 PCR Heater-Shaker Adapter", + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1", + "loadName": "opentrons_96_pcr_adapter", + "namespace": "opentrons", + "version": 1, + "location": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + } + }, + { + "key": "ff711b8d-7a74-442e-8972-e287581c59d1", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons Flex 96 Tip Rack 1000 µL", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "loadName": "opentrons_flex_96_tiprack_1000ul", + "namespace": "opentrons", + "version": 1, + "location": { "slotName": "C2" } + } + }, + { + "key": "9e62c4ba-dcbb-47b5-9d75-cc8afa7bcb59", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons Tough 96 Well Plate 200 µL PCR Full Skirt", + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "loadName": "opentrons_96_wellplate_200ul_pcr_full_skirt", + "namespace": "opentrons", + "version": 2, + "location": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + } + }, + { + "key": "7f5ab1f3-9bd0-475f-b352-280d8ffb1404", + "commandType": "loadLabware", + "params": { + "displayName": "Axygen 1 Well Reservoir 90 mL", + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "loadName": "axygen_1_reservoir_90ml", + "namespace": "opentrons", + "version": 1, + "location": { "addressableAreaName": "A4" } + } + }, + { + "commandType": "loadLiquid", + "key": "fab47ab6-bff7-4667-a3b3-d04d63733660", + "params": { + "liquidId": "1", + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "volumeByWell": { + "A1": 100, + "B1": 100, + "C1": 100, + "D1": 100, + "E1": 100, + "F1": 100, + "G1": 100, + "H1": 100 + } + } + }, + { + "commandType": "loadLiquid", + "key": "a704c87c-ccfc-46c5-a64b-ab1ccd46037c", + "params": { + "liquidId": "0", + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "volumeByWell": { "A1": 10000 } + } + }, + { + "commandType": "thermocycler/openLid", + "key": "c6789048-a5d9-44c1-a575-e00eeab7f133", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "commandType": "moveLabware", + "key": "3b30a344-59d2-4560-af7f-e522c4c20a1a", + "params": { + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "strategy": "usingGripper", + "newLocation": { "slotName": "C1" } + } + }, + { + "commandType": "pickUpTip", + "key": "976ffeb8-1715-422c-b51a-b6cc907af6f2", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "A1" + } + }, + { + "commandType": "aspirate", + "key": "d37a1d88-bf7f-4cc0-9830-e43d8e64f05c", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "3d2356d5-d7eb-43c9-b2be-bf4a16e600d5", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "d18252a3-48cd-4a17-b2ba-27c504564480", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "08fc3f60-be44-4bf7-a571-1248a40bf8cf", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "fa26bd0f-0719-4dfb-a905-bb3c01a2642a", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "B1" + } + }, + { + "commandType": "aspirate", + "key": "4d817f29-2049-4cb4-b8d7-230cd34c8ab8", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "eba62a93-d957-4d07-b24d-75833d332f54", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "B1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "e77ae2f7-62c1-4d4d-8953-007eabd265f7", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "3ecd1919-5454-4cd1-8d30-5546ee63dc34", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "1384b97f-7ef2-4c32-8190-5a5bf77bcba5", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "C1" + } + }, + { + "commandType": "aspirate", + "key": "341c9cc2-a8cf-4931-8eb8-e17611aed279", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "234cd6e1-b13e-44cf-ac6f-42d6d93431c2", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "C1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "752b7cd9-8e26-48dd-92ea-25b27d3f1148", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "bf749183-a4d7-4b50-88e5-0dbc707a776d", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "db84e3d5-cb2c-4405-9d29-de07bf2baadf", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "D1" + } + }, + { + "commandType": "aspirate", + "key": "ae2ed07d-1d95-4744-80d2-f8c93312b616", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "d02ffda4-bfed-49ac-81d9-5033cbd096f9", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "D1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "4e9250c8-60e5-419c-b5a2-2932f72e81a3", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "5c2756b1-7c96-4884-a76e-b7d5597b41af", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "24063ff4-bde0-43ea-af16-da70772ed83b", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "E1" + } + }, + { + "commandType": "aspirate", + "key": "92d4f69c-67c1-4a25-8e6d-9a73ac52f1ec", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "c01962df-98e3-4889-b243-bf1e00ed59cb", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "E1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "8cc3f36e-29b9-4347-ab7b-6916b11761c3", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "f9100143-c9de-436c-a1c4-9f241b48f28d", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "863d42f2-671f-437a-81d9-76b73bd0d403", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "F1" + } + }, + { + "commandType": "aspirate", + "key": "eb86e6c1-9f07-4809-a9b4-3424c65d4a0d", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "63963156-9749-469f-9fef-2dd101164b33", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "F1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "46453561-dc88-426b-9c19-d9b27a902a50", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "2ac1c8e2-f789-40cb-912b-bb5b77bcf0f6", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "8e79d987-6b4c-4de7-a34f-47022b814420", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "G1" + } + }, + { + "commandType": "aspirate", + "key": "24ecca70-7452-4721-a1a1-6be942e89cfb", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "d1264dba-ccac-492b-aaba-e575d35c2aec", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "G1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "108f9a39-3f10-474b-a86b-4dca765c3c61", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "0ded1139-533a-4590-b520-871ba41c5e8b", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "pickUpTip", + "key": "b4ca24ff-2448-481b-ba2c-052a98f47204", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "wellName": "H1" + } + }, + { + "commandType": "aspirate", + "key": "070b74b2-ef42-42a3-98c9-394f0f31903b", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "8bacda22-9e05-45e8-bef4-cc04414a204f:opentrons/axygen_1_reservoir_90ml/1", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 478 + } + }, + { + "commandType": "dispense", + "key": "0dcfa1ff-2feb-4deb-a1a0-aeda9d1479ba", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "volume": 100, + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "wellName": "H1", + "wellLocation": { "origin": "bottom", "offset": { "z": 0.5 } }, + "flowRate": 478 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "66e18af3-14c4-429a-9045-b38f7579d3dd", + "params": { + "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc", + "addressableAreaName": "1ChannelWasteChute", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "0a6aa502-9f2a-4a39-8e4c-a980e480a8f3", + "params": { "pipetteId": "9fcd50d9-92b2-45ac-acf1-e2cf773feffc" } + }, + { + "commandType": "thermocycler/closeLid", + "key": "e38a94f4-30f7-4528-8508-1c1be5ab9c36", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "commandType": "thermocycler/setTargetBlockTemperature", + "key": "1f08bd29-4750-4713-961b-9a2dbebcd001", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType", + "celsius": 40 + } + }, + { + "commandType": "thermocycler/waitForBlockTemperature", + "key": "b9c5d3b4-51a7-4cfe-99db-9c3b83c79230", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "commandType": "waitForDuration", + "key": "9a0ac176-868d-4f0f-8b23-6ae031f2b681", + "params": { "seconds": 60, "message": "" } + }, + { + "commandType": "thermocycler/openLid", + "key": "bee91ff3-8a69-4c9b-8ef5-9dbbdbb0411a", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "commandType": "thermocycler/deactivateBlock", + "key": "8c2743c1-6597-4522-946c-561f8d9b25ef", + "params": { + "moduleId": "fd6da9f1-d63b-414b-929e-c646b64790e9:thermocyclerModuleType" + } + }, + { + "commandType": "heaterShaker/deactivateHeater", + "key": "ecf793ae-ff88-4612-a634-e1313331f4df", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/openLabwareLatch", + "key": "1283880b-1d8e-4810-be5c-8ced5c0110b1", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "moveLabware", + "key": "0a818fa8-2db8-4d26-b02c-1e44963c3946", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "strategy": "usingGripper", + "newLocation": { + "labwareId": "7c4d59fa-0e50-442f-adce-9e4b0c7f0b88:opentrons/opentrons_96_pcr_adapter/1" + } + } + }, + { + "commandType": "heaterShaker/closeLabwareLatch", + "key": "aec0cc18-96d5-40cd-8e0b-8cd228f2e009", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/deactivateHeater", + "key": "a82263b7-2b77-46c6-844f-4bcf17af7dbe", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/setAndWaitForShakeSpeed", + "key": "d284c43a-acfe-4184-a7e4-3c936b770acf", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType", + "rpm": 200 + } + }, + { + "commandType": "waitForDuration", + "key": "017afd08-4027-4747-b280-0b0e8966ea6c", + "params": { "seconds": 60 } + }, + { + "commandType": "heaterShaker/deactivateShaker", + "key": "93bd7cc3-d33c-492d-9044-670ea4ab74aa", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/deactivateHeater", + "key": "3a2642df-3e88-46c8-ade5-191d3a44c07a", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/deactivateHeater", + "key": "f09129ff-e21d-4c8f-a07f-e037d67a5e07", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "heaterShaker/openLabwareLatch", + "key": "352c3f4d-6467-4f6a-80b5-4a424f4bce71", + "params": { + "moduleId": "23347241-80bb-4a7e-9c91-5d9727a9e483:heaterShakerModuleType" + } + }, + { + "commandType": "moveLabware", + "key": "841d359f-6b6a-4c0a-817c-c0dafc2b4c30", + "params": { + "labwareId": "54370838-4fca-4a14-b88a-7840e4903649:opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2", + "strategy": "usingGripper", + "newLocation": { "addressableAreaName": "B4" } + } + }, + { + "commandType": "moveLabware", + "key": "ed6a526a-4237-4f52-b3fd-b0506d25455f", + "params": { + "labwareId": "f2d371ea-5146-4c89-8200-9c056a7f321a:opentrons/opentrons_flex_96_tiprack_1000ul/1", + "strategy": "usingGripper", + "newLocation": { "addressableAreaName": "gripperWasteChute" } + } + } + ], + "commandAnnotationSchemaId": "opentronsCommandAnnotationSchemaV1", + "commandAnnotations": [] +} diff --git a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json index 35cce3b9123..7b8a5976d8d 100644 --- a/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json +++ b/protocol-designer/fixtures/protocol/8/example_1_1_0MigratedToV8.json @@ -125,7 +125,8 @@ "id": "e7d36200-92a5-11e9-ac62-1b173f839d9e", "stepType": "moveLiquid", "stepName": "transfer things", - "stepDetails": "yeah notes" + "stepDetails": "yeah notes", + "nozzles": null }, "18113c80-92a6-11e9-ac62-1b173f839d9e": { "times": 3, @@ -151,7 +152,8 @@ "id": "18113c80-92a6-11e9-ac62-1b173f839d9e", "stepType": "mix", "stepName": "mix", - "stepDetails": "" + "stepDetails": "", + "nozzles": null }, "2e622080-92a6-11e9-ac62-1b173f839d9e": { "pauseAction": "untilTime", @@ -3635,7 +3637,7 @@ "key": "c28746a8-655a-44c4-81ed-6b2f08f16dfb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -3835,7 +3837,7 @@ "key": "00d2c247-580b-4057-a7e8-8b6003bc0573", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -4035,7 +4037,7 @@ "key": "b95637fc-ea1e-4672-baad-0634cf051fea", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -4235,7 +4237,7 @@ "key": "f3e31686-b015-45fe-a3b2-07e1e843c191", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -4435,7 +4437,7 @@ "key": "9488a452-ed46-4c22-b547-15fd9ccaa8fb", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -4635,7 +4637,7 @@ "key": "8f2fe539-a9c9-466e-97be-99a6e356f112", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -4835,7 +4837,7 @@ "key": "1dc029a7-7df6-4d63-9961-5ce081b5d8d0", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -5035,7 +5037,7 @@ "key": "6622c244-4cd4-44de-ab39-77997b69b467", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { @@ -5235,7 +5237,7 @@ "key": "a2729301-e561-41a2-abd7-68b73c6e735d", "params": { "pipetteId": "c6f45030-92a5-11e9-ac62-1b173f839d9e", - "flowRate": 10 + "flowRate": 1000 } }, { diff --git a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json index 725c02e38aa..32bc795210c 100644 --- a/protocol-designer/fixtures/protocol/8/mix_8_0_0.json +++ b/protocol-designer/fixtures/protocol/8/mix_8_0_0.json @@ -76,7 +76,8 @@ "id": "fc4dc7c0-fc3a-11ea-8809-e959e7d61d96", "stepType": "mix", "stepName": "mix", - "stepDetails": "" + "stepDetails": "", + "nozzles": null } }, "orderedStepIds": [ diff --git a/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json new file mode 100644 index 00000000000..2fc11bb355c --- /dev/null +++ b/protocol-designer/fixtures/protocol/8/ninetySixChannelFullAndColumn.json @@ -0,0 +1,2423 @@ +{ + "$otSharedSchema": "#/protocol/schemas/8", + "schemaVersion": 8, + "metadata": { + "protocolName": "96ChannelFullAndColumn", + "author": "", + "description": "", + "created": 1701805621086, + "lastModified": 1701872458249, + "category": null, + "subcategory": null, + "tags": [] + }, + "designerApplication": { + "name": "opentrons/protocol-designer", + "version": "8.0.0", + "data": { + "_internalAppBuildDate": "Wed, 06 Dec 2023 14:20:10 GMT", + "defaultValues": { + "aspirate_mmFromBottom": 1, + "dispense_mmFromBottom": 0.5, + "touchTip_mmFromTop": -1, + "blowout_mmFromTop": 0 + }, + "pipetteTiprackAssignments": { + "de7da440-95ec-43e8-8723-851321fbd6f9": "opentrons/opentrons_flex_96_tiprack_50ul/1" + }, + "dismissedWarnings": { "form": {}, "timeline": {} }, + "ingredients": {}, + "ingredLocations": { + "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1": {} + }, + "savedStepForms": { + "__INITIAL_DECK_SETUP_STEP__": { + "labwareLocationUpdate": { + "ec850fd3-cf7c-44c5-b358-fba3a30315c9:opentrons/opentrons_flex_96_tiprack_adapter/1": "C2", + "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1": "ec850fd3-cf7c-44c5-b358-fba3a30315c9:opentrons/opentrons_flex_96_tiprack_adapter/1", + "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2": "B1", + "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1": "D1" + }, + "pipetteLocationUpdate": { + "de7da440-95ec-43e8-8723-851321fbd6f9": "left" + }, + "moduleLocationUpdate": {}, + "stepType": "manualIntervention", + "id": "__INITIAL_DECK_SETUP_STEP__" + }, + "83a095fa-b649-4105-99d4-177f1a3f363a": { + "pipette": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": "10", + "changeTip": "always", + "path": "single", + "aspirate_wells_grouped": false, + "aspirate_flowRate": null, + "aspirate_labware": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", + "aspirate_wells": ["A1"], + "aspirate_wellOrder_first": "t2b", + "aspirate_wellOrder_second": "l2r", + "aspirate_mix_checkbox": false, + "aspirate_mix_times": null, + "aspirate_mix_volume": null, + "aspirate_mmFromBottom": null, + "aspirate_touchTip_checkbox": false, + "aspirate_touchTip_mmFromBottom": null, + "dispense_flowRate": null, + "dispense_labware": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", + "dispense_wells": [], + "dispense_wellOrder_first": "t2b", + "dispense_wellOrder_second": "l2r", + "dispense_mix_checkbox": false, + "dispense_mix_times": null, + "dispense_mix_volume": null, + "dispense_mmFromBottom": null, + "dispense_touchTip_checkbox": false, + "dispense_touchTip_mmFromBottom": null, + "disposalVolume_checkbox": true, + "disposalVolume_volume": "5", + "blowout_checkbox": false, + "blowout_location": null, + "preWetTip": false, + "aspirate_airGap_checkbox": false, + "aspirate_airGap_volume": "5", + "aspirate_delay_checkbox": false, + "aspirate_delay_mmFromBottom": null, + "aspirate_delay_seconds": "1", + "dispense_airGap_checkbox": false, + "dispense_airGap_volume": "5", + "dispense_delay_checkbox": false, + "dispense_delay_seconds": "1", + "dispense_delay_mmFromBottom": null, + "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", + "nozzles": "ALL", + "id": "83a095fa-b649-4105-99d4-177f1a3f363a", + "stepType": "moveLiquid", + "stepName": "transfer", + "stepDetails": "" + }, + "f5ea3139-1585-4848-9d5f-832eb88c99ca": { + "pipette": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": "10", + "changeTip": "always", + "path": "single", + "aspirate_wells_grouped": false, + "aspirate_flowRate": null, + "aspirate_labware": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", + "aspirate_wells": ["A7"], + "aspirate_wellOrder_first": "t2b", + "aspirate_wellOrder_second": "l2r", + "aspirate_mix_checkbox": false, + "aspirate_mix_times": null, + "aspirate_mix_volume": null, + "aspirate_mmFromBottom": null, + "aspirate_touchTip_checkbox": false, + "aspirate_touchTip_mmFromBottom": null, + "dispense_flowRate": null, + "dispense_labware": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", + "dispense_wells": [], + "dispense_wellOrder_first": "t2b", + "dispense_wellOrder_second": "l2r", + "dispense_mix_checkbox": false, + "dispense_mix_times": null, + "dispense_mix_volume": null, + "dispense_mmFromBottom": null, + "dispense_touchTip_checkbox": false, + "dispense_touchTip_mmFromBottom": null, + "disposalVolume_checkbox": true, + "disposalVolume_volume": "5", + "blowout_checkbox": false, + "blowout_location": null, + "preWetTip": false, + "aspirate_airGap_checkbox": false, + "aspirate_airGap_volume": "5", + "aspirate_delay_checkbox": false, + "aspirate_delay_mmFromBottom": null, + "aspirate_delay_seconds": "1", + "dispense_airGap_checkbox": false, + "dispense_airGap_volume": "5", + "dispense_delay_checkbox": false, + "dispense_delay_seconds": "1", + "dispense_delay_mmFromBottom": null, + "dropTip_location": "1e553651-9e4d-44b1-a31b-92459642bfd7:trashBin", + "nozzles": "COLUMN", + "id": "f5ea3139-1585-4848-9d5f-832eb88c99ca", + "stepType": "moveLiquid", + "stepName": "transfer", + "stepDetails": "" + } + }, + "orderedStepIds": [ + "83a095fa-b649-4105-99d4-177f1a3f363a", + "f5ea3139-1585-4848-9d5f-832eb88c99ca" + ] + } + }, + "robot": { "model": "OT-3 Standard", "deckId": "ot3_standard" }, + "labwareDefinitionSchemaId": "opentronsLabwareSchemaV2", + "labwareDefinitions": { + "opentrons/opentrons_flex_96_tiprack_50ul/1": { + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "brand": { "brand": "Opentrons", "brandId": [] }, + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack 50 µL", + "displayCategory": "tipRack", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 127.75, + "yDimension": 85.75, + "zDimension": 99 + }, + "gripForce": 16, + "gripHeightFromLabwareBottom": 23.9, + "wells": { + "A1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 74.38, + "z": 1.5 + }, + "B1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 65.38, + "z": 1.5 + }, + "C1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 56.38, + "z": 1.5 + }, + "D1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 47.38, + "z": 1.5 + }, + "E1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 38.38, + "z": 1.5 + }, + "F1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 29.38, + "z": 1.5 + }, + "G1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 20.38, + "z": 1.5 + }, + "H1": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 14.38, + "y": 11.38, + "z": 1.5 + }, + "A2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 74.38, + "z": 1.5 + }, + "B2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 65.38, + "z": 1.5 + }, + "C2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 56.38, + "z": 1.5 + }, + "D2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 47.38, + "z": 1.5 + }, + "E2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 38.38, + "z": 1.5 + }, + "F2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 29.38, + "z": 1.5 + }, + "G2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 20.38, + "z": 1.5 + }, + "H2": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 23.38, + "y": 11.38, + "z": 1.5 + }, + "A3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 74.38, + "z": 1.5 + }, + "B3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 65.38, + "z": 1.5 + }, + "C3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 56.38, + "z": 1.5 + }, + "D3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 47.38, + "z": 1.5 + }, + "E3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 38.38, + "z": 1.5 + }, + "F3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 29.38, + "z": 1.5 + }, + "G3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 20.38, + "z": 1.5 + }, + "H3": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 32.38, + "y": 11.38, + "z": 1.5 + }, + "A4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 74.38, + "z": 1.5 + }, + "B4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 65.38, + "z": 1.5 + }, + "C4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 56.38, + "z": 1.5 + }, + "D4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 47.38, + "z": 1.5 + }, + "E4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 38.38, + "z": 1.5 + }, + "F4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 29.38, + "z": 1.5 + }, + "G4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 20.38, + "z": 1.5 + }, + "H4": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 41.38, + "y": 11.38, + "z": 1.5 + }, + "A5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 74.38, + "z": 1.5 + }, + "B5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 65.38, + "z": 1.5 + }, + "C5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 56.38, + "z": 1.5 + }, + "D5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 47.38, + "z": 1.5 + }, + "E5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 38.38, + "z": 1.5 + }, + "F5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 29.38, + "z": 1.5 + }, + "G5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 20.38, + "z": 1.5 + }, + "H5": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 50.38, + "y": 11.38, + "z": 1.5 + }, + "A6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 74.38, + "z": 1.5 + }, + "B6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 65.38, + "z": 1.5 + }, + "C6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 56.38, + "z": 1.5 + }, + "D6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 47.38, + "z": 1.5 + }, + "E6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 38.38, + "z": 1.5 + }, + "F6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 29.38, + "z": 1.5 + }, + "G6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 20.38, + "z": 1.5 + }, + "H6": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 59.38, + "y": 11.38, + "z": 1.5 + }, + "A7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 74.38, + "z": 1.5 + }, + "B7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 65.38, + "z": 1.5 + }, + "C7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 56.38, + "z": 1.5 + }, + "D7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 47.38, + "z": 1.5 + }, + "E7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 38.38, + "z": 1.5 + }, + "F7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 29.38, + "z": 1.5 + }, + "G7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 20.38, + "z": 1.5 + }, + "H7": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 68.38, + "y": 11.38, + "z": 1.5 + }, + "A8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 74.38, + "z": 1.5 + }, + "B8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 65.38, + "z": 1.5 + }, + "C8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 56.38, + "z": 1.5 + }, + "D8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 47.38, + "z": 1.5 + }, + "E8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 38.38, + "z": 1.5 + }, + "F8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 29.38, + "z": 1.5 + }, + "G8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 20.38, + "z": 1.5 + }, + "H8": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 77.38, + "y": 11.38, + "z": 1.5 + }, + "A9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 74.38, + "z": 1.5 + }, + "B9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 65.38, + "z": 1.5 + }, + "C9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 56.38, + "z": 1.5 + }, + "D9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 47.38, + "z": 1.5 + }, + "E9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 38.38, + "z": 1.5 + }, + "F9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 29.38, + "z": 1.5 + }, + "G9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 20.38, + "z": 1.5 + }, + "H9": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 86.38, + "y": 11.38, + "z": 1.5 + }, + "A10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 74.38, + "z": 1.5 + }, + "B10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 65.38, + "z": 1.5 + }, + "C10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 56.38, + "z": 1.5 + }, + "D10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 47.38, + "z": 1.5 + }, + "E10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 38.38, + "z": 1.5 + }, + "F10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 29.38, + "z": 1.5 + }, + "G10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 20.38, + "z": 1.5 + }, + "H10": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 95.38, + "y": 11.38, + "z": 1.5 + }, + "A11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 74.38, + "z": 1.5 + }, + "B11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 65.38, + "z": 1.5 + }, + "C11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 56.38, + "z": 1.5 + }, + "D11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 47.38, + "z": 1.5 + }, + "E11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 38.38, + "z": 1.5 + }, + "F11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 29.38, + "z": 1.5 + }, + "G11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 20.38, + "z": 1.5 + }, + "H11": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 104.38, + "y": 11.38, + "z": 1.5 + }, + "A12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 74.38, + "z": 1.5 + }, + "B12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 65.38, + "z": 1.5 + }, + "C12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 56.38, + "z": 1.5 + }, + "D12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 47.38, + "z": 1.5 + }, + "E12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 38.38, + "z": 1.5 + }, + "F12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 29.38, + "z": 1.5 + }, + "G12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 20.38, + "z": 1.5 + }, + "H12": { + "depth": 97.5, + "shape": "circular", + "diameter": 5.58, + "totalLiquidVolume": 50, + "x": 113.38, + "y": 11.38, + "z": 1.5 + } + }, + "groups": [ + { + "metadata": {}, + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ] + } + ], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": true, + "tipLength": 57.9, + "tipOverlap": 10.5, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_flex_96_tiprack_50ul" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "stackingOffsetWithLabware": { + "opentrons_flex_96_tiprack_adapter": { "x": 0, "y": 0, "z": 121 } + } + }, + "opentrons/opentrons_flex_96_tiprack_adapter/1": { + "ordering": [], + "brand": { "brand": "Opentrons", "brandId": [] }, + "metadata": { + "displayName": "Opentrons Flex 96 Tip Rack Adapter", + "displayCategory": "adapter", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "xDimension": 156.5, + "yDimension": 93, + "zDimension": 132 + }, + "wells": {}, + "groups": [{ "metadata": {}, "wells": [] }], + "parameters": { + "format": "96Standard", + "quirks": [], + "isTiprack": false, + "isMagneticModuleCompatible": false, + "loadName": "opentrons_flex_96_tiprack_adapter" + }, + "namespace": "opentrons", + "version": 1, + "schemaVersion": 2, + "allowedRoles": ["adapter"], + "cornerOffsetFromSlot": { "x": -14.25, "y": -3.5, "z": 0 } + }, + "opentrons/biorad_96_wellplate_200ul_pcr/2": { + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "schemaVersion": 2, + "version": 2, + "namespace": "opentrons", + "metadata": { + "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", + "displayCategory": "wellPlate", + "displayVolumeUnits": "µL", + "tags": [] + }, + "dimensions": { + "yDimension": 85.48, + "xDimension": 127.76, + "zDimension": 16.06 + }, + "parameters": { + "format": "96Standard", + "isTiprack": false, + "loadName": "biorad_96_wellplate_200ul_pcr", + "isMagneticModuleCompatible": true, + "magneticModuleEngageHeight": 18 + }, + "gripForce": 15, + "gripHeightFromLabwareBottom": 10.14, + "wells": { + "H1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 11.24, + "z": 1.25 + }, + "G1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 20.24, + "z": 1.25 + }, + "F1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 29.24, + "z": 1.25 + }, + "E1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 38.24, + "z": 1.25 + }, + "D1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 47.24, + "z": 1.25 + }, + "C1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 56.24, + "z": 1.25 + }, + "B1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 65.24, + "z": 1.25 + }, + "A1": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 14.38, + "y": 74.24, + "z": 1.25 + }, + "H2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 11.24, + "z": 1.25 + }, + "G2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 20.24, + "z": 1.25 + }, + "F2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 29.24, + "z": 1.25 + }, + "E2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 38.24, + "z": 1.25 + }, + "D2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 47.24, + "z": 1.25 + }, + "C2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 56.24, + "z": 1.25 + }, + "B2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 65.24, + "z": 1.25 + }, + "A2": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 23.38, + "y": 74.24, + "z": 1.25 + }, + "H3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 11.24, + "z": 1.25 + }, + "G3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 20.24, + "z": 1.25 + }, + "F3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 29.24, + "z": 1.25 + }, + "E3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 38.24, + "z": 1.25 + }, + "D3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 47.24, + "z": 1.25 + }, + "C3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 56.24, + "z": 1.25 + }, + "B3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 65.24, + "z": 1.25 + }, + "A3": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 32.38, + "y": 74.24, + "z": 1.25 + }, + "H4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 11.24, + "z": 1.25 + }, + "G4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 20.24, + "z": 1.25 + }, + "F4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 29.24, + "z": 1.25 + }, + "E4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 38.24, + "z": 1.25 + }, + "D4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 47.24, + "z": 1.25 + }, + "C4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 56.24, + "z": 1.25 + }, + "B4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 65.24, + "z": 1.25 + }, + "A4": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 41.38, + "y": 74.24, + "z": 1.25 + }, + "H5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 11.24, + "z": 1.25 + }, + "G5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 20.24, + "z": 1.25 + }, + "F5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 29.24, + "z": 1.25 + }, + "E5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 38.24, + "z": 1.25 + }, + "D5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 47.24, + "z": 1.25 + }, + "C5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 56.24, + "z": 1.25 + }, + "B5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 65.24, + "z": 1.25 + }, + "A5": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 50.38, + "y": 74.24, + "z": 1.25 + }, + "H6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 11.24, + "z": 1.25 + }, + "G6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 20.24, + "z": 1.25 + }, + "F6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 29.24, + "z": 1.25 + }, + "E6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 38.24, + "z": 1.25 + }, + "D6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 47.24, + "z": 1.25 + }, + "C6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 56.24, + "z": 1.25 + }, + "B6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 65.24, + "z": 1.25 + }, + "A6": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 59.38, + "y": 74.24, + "z": 1.25 + }, + "H7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 11.24, + "z": 1.25 + }, + "G7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 20.24, + "z": 1.25 + }, + "F7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 29.24, + "z": 1.25 + }, + "E7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 38.24, + "z": 1.25 + }, + "D7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 47.24, + "z": 1.25 + }, + "C7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 56.24, + "z": 1.25 + }, + "B7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 65.24, + "z": 1.25 + }, + "A7": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 68.38, + "y": 74.24, + "z": 1.25 + }, + "H8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 11.24, + "z": 1.25 + }, + "G8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 20.24, + "z": 1.25 + }, + "F8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 29.24, + "z": 1.25 + }, + "E8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 38.24, + "z": 1.25 + }, + "D8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 47.24, + "z": 1.25 + }, + "C8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 56.24, + "z": 1.25 + }, + "B8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 65.24, + "z": 1.25 + }, + "A8": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 77.38, + "y": 74.24, + "z": 1.25 + }, + "H9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 11.24, + "z": 1.25 + }, + "G9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 20.24, + "z": 1.25 + }, + "F9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 29.24, + "z": 1.25 + }, + "E9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 38.24, + "z": 1.25 + }, + "D9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 47.24, + "z": 1.25 + }, + "C9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 56.24, + "z": 1.25 + }, + "B9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 65.24, + "z": 1.25 + }, + "A9": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 86.38, + "y": 74.24, + "z": 1.25 + }, + "H10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 11.24, + "z": 1.25 + }, + "G10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 20.24, + "z": 1.25 + }, + "F10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 29.24, + "z": 1.25 + }, + "E10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 38.24, + "z": 1.25 + }, + "D10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 47.24, + "z": 1.25 + }, + "C10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 56.24, + "z": 1.25 + }, + "B10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 65.24, + "z": 1.25 + }, + "A10": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 95.38, + "y": 74.24, + "z": 1.25 + }, + "H11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 11.24, + "z": 1.25 + }, + "G11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 20.24, + "z": 1.25 + }, + "F11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 29.24, + "z": 1.25 + }, + "E11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 38.24, + "z": 1.25 + }, + "D11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 47.24, + "z": 1.25 + }, + "C11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 56.24, + "z": 1.25 + }, + "B11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 65.24, + "z": 1.25 + }, + "A11": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 104.38, + "y": 74.24, + "z": 1.25 + }, + "H12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 11.24, + "z": 1.25 + }, + "G12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 20.24, + "z": 1.25 + }, + "F12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 29.24, + "z": 1.25 + }, + "E12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 38.24, + "z": 1.25 + }, + "D12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 47.24, + "z": 1.25 + }, + "C12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 56.24, + "z": 1.25 + }, + "B12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 65.24, + "z": 1.25 + }, + "A12": { + "depth": 14.81, + "shape": "circular", + "diameter": 5.46, + "totalLiquidVolume": 200, + "x": 113.38, + "y": 74.24, + "z": 1.25 + } + }, + "brand": { + "brand": "Bio-Rad", + "brandId": ["hsp9601"], + "links": [ + "http://www.bio-rad.com/en-us/sku/hsp9601-hard-shell-96-well-pcr-plates-low-profile-thin-wall-skirted-white-clear?ID=hsp9601" + ] + }, + "groups": [ + { + "wells": [ + "A1", + "B1", + "C1", + "D1", + "E1", + "F1", + "G1", + "H1", + "A2", + "B2", + "C2", + "D2", + "E2", + "F2", + "G2", + "H2", + "A3", + "B3", + "C3", + "D3", + "E3", + "F3", + "G3", + "H3", + "A4", + "B4", + "C4", + "D4", + "E4", + "F4", + "G4", + "H4", + "A5", + "B5", + "C5", + "D5", + "E5", + "F5", + "G5", + "H5", + "A6", + "B6", + "C6", + "D6", + "E6", + "F6", + "G6", + "H6", + "A7", + "B7", + "C7", + "D7", + "E7", + "F7", + "G7", + "H7", + "A8", + "B8", + "C8", + "D8", + "E8", + "F8", + "G8", + "H8", + "A9", + "B9", + "C9", + "D9", + "E9", + "F9", + "G9", + "H9", + "A10", + "B10", + "C10", + "D10", + "E10", + "F10", + "G10", + "H10", + "A11", + "B11", + "C11", + "D11", + "E11", + "F11", + "G11", + "H11", + "A12", + "B12", + "C12", + "D12", + "E12", + "F12", + "G12", + "H12" + ], + "metadata": { "wellBottomShape": "v" } + } + ], + "cornerOffsetFromSlot": { "x": 0, "y": 0, "z": 0 }, + "stackingOffsetWithLabware": { + "opentrons_96_well_aluminum_block": { "x": 0, "y": 0, "z": 15.41 }, + "opentrons_96_pcr_adapter": { + "x": 0, + "y": 0, + "z": 10.16 + } + }, + "stackingOffsetWithModule": { + "thermocyclerModuleV2": { "x": 0, "y": 0, "z": 10.75 }, + "magneticBlockV1": { + "x": 0, + "y": 0, + "z": 3.87 + } + } + } + }, + "liquidSchemaId": "opentronsLiquidSchemaV1", + "liquids": {}, + "commandSchemaId": "opentronsCommandSchemaV8", + "commands": [ + { + "key": "a04d26cb-a689-4dcb-ac27-1cef05d53677", + "commandType": "loadPipette", + "params": { + "pipetteName": "p1000_96", + "mount": "left", + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" + } + }, + { + "key": "4f2796ef-1087-4adf-a5fe-005c30dcc6db", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons Flex 96 Tip Rack Adapter", + "labwareId": "ec850fd3-cf7c-44c5-b358-fba3a30315c9:opentrons/opentrons_flex_96_tiprack_adapter/1", + "loadName": "opentrons_flex_96_tiprack_adapter", + "namespace": "opentrons", + "version": 1, + "location": { "slotName": "C2" } + } + }, + { + "key": "2047ebfd-1af3-4e05-b50b-8ace628af278", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons Flex 96 Tip Rack 50 µL", + "labwareId": "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1", + "loadName": "opentrons_flex_96_tiprack_50ul", + "namespace": "opentrons", + "version": 1, + "location": { + "labwareId": "ec850fd3-cf7c-44c5-b358-fba3a30315c9:opentrons/opentrons_flex_96_tiprack_adapter/1" + } + } + }, + { + "key": "74ac5df9-371f-4f87-b649-e393c8c82c61", + "commandType": "loadLabware", + "params": { + "displayName": "Bio-Rad 96 Well Plate 200 µL PCR", + "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", + "loadName": "biorad_96_wellplate_200ul_pcr", + "namespace": "opentrons", + "version": 2, + "location": { "slotName": "B1" } + } + }, + { + "key": "85e1b54e-a32c-41eb-81a2-017f6ca4a143", + "commandType": "loadLabware", + "params": { + "displayName": "Opentrons Flex 96 Tip Rack 50 µL", + "labwareId": "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1", + "loadName": "opentrons_flex_96_tiprack_50ul", + "namespace": "opentrons", + "version": 1, + "location": { "slotName": "D1" } + } + }, + { + "commandType": "configureNozzleLayout", + "key": "b4ce373e-8b48-434a-b96e-ec8fba4fbe19", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "configurationParams": { "style": "ALL" } + } + }, + { + "commandType": "pickUpTip", + "key": "e5c62b8a-efe9-4ba9-bb7d-5973bff58b76", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "labwareId": "75aa666f-98d8-4af9-908e-963ced428580:opentrons/opentrons_flex_96_tiprack_50ul/1", + "wellName": "A1" + } + }, + { + "commandType": "aspirate", + "key": "868d7c2a-8009-46c6-b5d6-34ccc10f628a", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": 10, + "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", + "wellName": "A1", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 6 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "de22e6ff-5989-4820-a8ca-8f39785eb1c6", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "addressableAreaName": "movableTrashA3", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dispenseInPlace", + "key": "e1828ea8-1567-4787-9185-43895b1f50c9", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": 10, + "flowRate": 6 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "1a094b33-5bef-4371-8968-182f83838f77", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "addressableAreaName": "movableTrashA3", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "fafbc2d4-6675-48c7-aff0-04aa4a5f4dcf", + "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } + }, + { + "commandType": "configureNozzleLayout", + "key": "c0022ba6-ea8f-468e-b3db-3ab1137ac8e6", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "configurationParams": { "primaryNozzle": "A12", "style": "COLUMN" } + } + }, + { + "commandType": "pickUpTip", + "key": "a9fb93dd-d2bc-4829-a331-6e39b453c5d0", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "labwareId": "9bd16b50-4ae9-4cfd-8583-3378087e6a6c:opentrons/opentrons_flex_96_tiprack_50ul/1", + "wellName": "A1" + } + }, + { + "commandType": "aspirate", + "key": "f355e2fb-7045-43df-8dba-5485007eca92", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": 10, + "labwareId": "fe1942b1-1b75-4d3a-9c12-d23004958a12:opentrons/biorad_96_wellplate_200ul_pcr/2", + "wellName": "A7", + "wellLocation": { "origin": "bottom", "offset": { "z": 1 } }, + "flowRate": 6 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "edc0b963-b9fb-4ec8-b528-fb1e513e70bc", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "addressableAreaName": "movableTrashA3", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dispenseInPlace", + "key": "492bb9b6-6349-4f8e-91a7-cacb310732e0", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "volume": 10, + "flowRate": 6 + } + }, + { + "commandType": "moveToAddressableArea", + "key": "56906667-0837-4b36-b13f-08903b7a4d8c", + "params": { + "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9", + "addressableAreaName": "movableTrashA3", + "offset": { "x": 0, "y": 0, "z": 0 } + } + }, + { + "commandType": "dropTipInPlace", + "key": "2b6cf6b5-73e6-46db-a52d-be7c1e09f281", + "params": { "pipetteId": "de7da440-95ec-43e8-8723-851321fbd6f9" } + } + ], + "commandAnnotationSchemaId": "opentronsCommandAnnotationSchemaV1", + "commandAnnotations": [] +} diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index f826e695462..f834b436eb8 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -39,6 +39,7 @@ import { TRASH_BIN_ADAPTER_FIXTURE, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' +import { SPAN7_8_10_11_SLOT } from '../../constants' import { selectors as labwareDefSelectors } from '../../labware-defs' import { selectors as featureFlagSelectors } from '../../feature-flags' @@ -190,16 +191,12 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { <> {/* all modules */} {allModules.map(moduleOnDeck => { - // modules can be on the deck, including pseudo-slots (eg special 'spanning' slot for thermocycler position) - // const moduleParentSlots = [...deckSlots, ...values(PSEUDO_DECK_SLOTS)] - // const slot = moduleParentSlots.find( - // slot => slot.id === moduleOnDeck.slot - // ) - const slotPosition = getPositionFromSlotId(moduleOnDeck.slot, deckDef) + const slotId = + moduleOnDeck.slot === SPAN7_8_10_11_SLOT ? '7' : moduleOnDeck.slot + + const slotPosition = getPositionFromSlotId(slotId, deckDef) if (slotPosition == null) { - console.warn( - `no slot ${moduleOnDeck.slot} for module ${moduleOnDeck.id}` - ) + console.warn(`no slot ${slotId} for module ${moduleOnDeck.id}`) return null } const moduleDef = getModuleDef2(moduleOnDeck.model) @@ -257,7 +254,7 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { def={moduleDef} orientation={inferModuleOrientationFromXCoordinate(slotPosition[0])} innerProps={getModuleInnerProps(moduleOnDeck.moduleState)} - targetSlotId={moduleOnDeck.slot} + targetSlotId={slotId} targetDeckId={deckDef.otId} > {labwareLoadedOnModule != null && !shouldHideChildren ? ( @@ -550,7 +547,8 @@ export const DeckSetup = (): JSX.Element => { aE => STAGING_AREA_CUTOUTS.includes(aE.location as CutoutId) && aE.name === 'stagingArea' && - aE.location === WASTE_CHUTE_CUTOUT + aE.location === WASTE_CHUTE_CUTOUT && + wasteChuteFixtures.length > 0 ) const hasWasteChute = diff --git a/protocol-designer/src/components/DeckSetupManager.tsx b/protocol-designer/src/components/DeckSetupManager.tsx index 29d16ba14e9..8aabeba5090 100644 --- a/protocol-designer/src/components/DeckSetupManager.tsx +++ b/protocol-designer/src/components/DeckSetupManager.tsx @@ -1,6 +1,5 @@ import * as React from 'react' import { useSelector } from 'react-redux' -import { getEnableOffDeckVisAndMultiTip } from '../feature-flags/selectors' import { getBatchEditSelectedStepTypes, getHoveredItem, @@ -12,15 +11,12 @@ import { OffDeckLabwareButton } from './OffDeckLabwareButton' export const DeckSetupManager = (): JSX.Element => { const batchEditSelectedStepTypes = useSelector(getBatchEditSelectedStepTypes) const hoveredItem = useSelector(getHoveredItem) - const enableOffDeckVisAndMultiTipFF = useSelector( - getEnableOffDeckVisAndMultiTip - ) if (batchEditSelectedStepTypes.length === 0 || hoveredItem !== null) { // not batch edit mode, or batch edit while item is hovered: show the deck return ( <> - {enableOffDeckVisAndMultiTipFF ? : null} + ) diff --git a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx index 69a22b67424..f886399a9f7 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx @@ -207,12 +207,16 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { const getIsLabwareFiltered = React.useCallback( (labwareDef: LabwareDefinition2) => { - const smallXDimension = labwareDef.dimensions.xDimension < 127.75 - const smallYDimension = labwareDef.dimensions.yDimension < 85.48 - const irregularSize = smallXDimension && smallYDimension - const adapter = labwareDef.metadata.displayCategory === 'adapter' - const isAdapter96Channel = - labwareDef.parameters.loadName === ADAPTER_96_CHANNEL + const { dimensions, parameters } = labwareDef + const { xDimension, yDimension } = dimensions + + const isSmallXDimension = xDimension < 127.75 + const isSmallYDimension = yDimension < 85.48 + const isIrregularSize = isSmallXDimension && isSmallYDimension + + const isAdapter = labwareDef.allowedRoles?.includes('adapter') + const isAdapter96Channel = parameters.loadName === ADAPTER_96_CHANNEL + return ( (filterRecommended && !getLabwareIsRecommended(labwareDef, moduleModel)) || @@ -222,10 +226,11 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM )) || !getLabwareCompatible(labwareDef) || - (adapter && - irregularSize && + (isAdapter && + isIrregularSize && !slot?.includes(HEATERSHAKER_MODULE_TYPE)) || - (isAdapter96Channel && !has96Channel) + (isAdapter96Channel && !has96Channel) || + (slot === 'offDeck' && isAdapter) ) }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] diff --git a/protocol-designer/src/components/LiquidPlacementModal.tsx b/protocol-designer/src/components/LiquidPlacementModal.tsx index e3558a58ec4..993f320db38 100644 --- a/protocol-designer/src/components/LiquidPlacementModal.tsx +++ b/protocol-designer/src/components/LiquidPlacementModal.tsx @@ -84,6 +84,7 @@ class LiquidPlacementModalComponent extends React.Component { updateHighlightedWells={this.updateHighlightedWells} ingredNames={this.props.liquidNamesById} wellContents={this.props.wellContents} + nozzleType={null} /> )} diff --git a/protocol-designer/src/components/StepEditForm/fields/Configure96ChannelField.tsx b/protocol-designer/src/components/StepEditForm/fields/Configure96ChannelField.tsx new file mode 100644 index 00000000000..c1c74f9c062 --- /dev/null +++ b/protocol-designer/src/components/StepEditForm/fields/Configure96ChannelField.tsx @@ -0,0 +1,92 @@ +import * as React from 'react' +import { useSelector } from 'react-redux' +import { ALL, COLUMN } from '@opentrons/shared-data' +import { + FormGroup, + SelectField, + Tooltip, + TOOLTIP_FIXED, + useHoverTooltip, +} from '@opentrons/components' +import { i18n } from '../../../localization' +import { getInitialDeckSetup } from '../../../step-forms/selectors' +import { StepFormDropdown } from './StepFormDropdownField' +import styles from '../StepEditForm.css' + +export function Configure96ChannelField( + props: Omit, 'options'> +): JSX.Element { + const { value: dropdownItem, name, updateValue } = props + const deckSetup = useSelector(getInitialDeckSetup) + const tipracks = Object.values(deckSetup.labware).filter( + labware => labware.def.parameters.isTiprack + ) + const tipracksNotOnAdapter = tipracks.filter( + tiprack => deckSetup.labware[tiprack.slot] == null + ) + + const options = [ + { name: 'all', value: ALL }, + { + name: 'column', + value: COLUMN, + isDisabled: tipracksNotOnAdapter.length === 0, + }, + ] + + const [selectedValue, setSelectedValue] = React.useState( + dropdownItem || options[0].value + ) + React.useEffect(() => { + updateValue(selectedValue) + }, [selectedValue]) + + return ( + + { + updateValue(value) + setSelectedValue(value) + }} + formatOptionLabel={({ value, isDisabled }) => ( + + )} + /> + + ) +} + +interface OptionLabelProps { + value: string + disabled?: boolean +} + +const OptionLabel = (props: OptionLabelProps): JSX.Element => { + const { value, disabled } = props + const [targetProps, tooltipProps] = useHoverTooltip({ + placement: 'bottom-start', + strategy: TOOLTIP_FIXED, + }) + return ( + <> +
+ {i18n.t(`form.step_edit_form.field.nozzles.option.${value}`)} + {disabled ? ( + +
+ {i18n.t( + `form.step_edit_form.field.nozzles.option_tooltip.${value}` + )} +
+
+ ) : null} +
+ + ) +} diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx index 5e7c4513c5d..ec41ab13105 100644 --- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx @@ -1,20 +1,21 @@ import * as React from 'react' +import { Dispatch } from 'redux' import { connect } from 'react-redux' +import { COLUMN } from '@opentrons/shared-data' import { FormGroup, InputField } from '@opentrons/components' import { i18n } from '../../../../localization' -import { WellSelectionModal } from './WellSelectionModal' import { Portal } from '../../../portals/MainPageModalPortal' import { actions as stepsActions, getSelectedStepId, getWellSelectionLabwareKey, } from '../../../../ui/steps' +import { WellSelectionModal } from './WellSelectionModal' import styles from '../../StepEditForm.css' -import { Dispatch } from 'redux' -import { StepIdType } from '../../../../form-types' -import { BaseState } from '../../../../types' -import { FieldProps } from '../../types' +import type { StepIdType } from '../../../../form-types' +import type { BaseState, NozzleType } from '../../../../types' +import type { FieldProps } from '../../types' export interface SP { stepId?: StepIdType | null @@ -28,7 +29,7 @@ export interface DP { export type OP = FieldProps & { primaryWellCount?: number - is8Channel?: boolean | null + nozzleType?: NozzleType | null pipetteId?: string | null labwareId?: string | null } @@ -64,9 +65,10 @@ export class WellSelectionInputComponent extends React.Component { render(): JSX.Element { const modalKey = this.getModalKey() - const label = this.props.is8Channel - ? i18n.t('form.step_edit_form.wellSelectionLabel.columns') - : i18n.t('form.step_edit_form.wellSelectionLabel.wells') + const label = + this.props.nozzleType === '8-channel' || this.props.nozzleType === COLUMN + ? i18n.t('form.step_edit_form.wellSelectionLabel.columns') + : i18n.t('form.step_edit_form.wellSelectionLabel.wells') return ( { pipetteId={this.props.pipetteId} updateValue={this.props.updateValue} value={this.props.value} + nozzleType={this.props.nozzleType} /> diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx index 74ccc23e29d..0f2e4cb540d 100644 --- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionModal.tsx @@ -17,41 +17,44 @@ import { } from '@opentrons/shared-data' import { arrayToWellGroup } from '../../../../utils' -import { WellSelectionInstructions } from '../../../WellSelectionInstructions' -import { SelectableLabware, wellFillFromWellContents } from '../../../labware' - import * as wellContentsSelectors from '../../../../top-selectors/well-contents' import { selectors } from '../../../../labware-ingred/selectors' import { selectors as stepFormSelectors } from '../../../../step-forms' -import { ContentsByWell } from '../../../../labware-ingred/types' -import { WellIngredientNames } from '../../../../steplist/types' -import { StepFieldName } from '../../../../form-types' +import { WellSelectionInstructions } from '../../../WellSelectionInstructions' +import { SelectableLabware, wellFillFromWellContents } from '../../../labware' + +import type { ContentsByWell } from '../../../../labware-ingred/types' +import type { WellIngredientNames } from '../../../../steplist/types' +import type { StepFieldName } from '../../../../form-types' +import type { NozzleType } from '../../../../types' import styles from './WellSelectionModal.css' import modalStyles from '../../../modals/modal.css' interface WellSelectionModalProps { isOpen: boolean - labwareId?: string | null name: StepFieldName onCloseClick: (e?: React.MouseEvent) => unknown - pipetteId?: string | null value: unknown updateValue: (val: unknown | null | undefined) => void + nozzleType?: NozzleType | null + labwareId?: string | null + pipetteId?: string | null } interface WellSelectionModalComponentProps { deselectWells: (wellGroup: WellGroup) => unknown + nozzleType: NozzleType | null handleSave: () => unknown highlightedWells: WellGroup ingredNames: WellIngredientNames - labwareDef?: LabwareDefinition2 | null onCloseClick: (e?: React.MouseEvent) => unknown - pipetteSpec?: PipetteNameSpecs | null selectedPrimaryWells: WellGroup selectWells: (wellGroup: WellGroup) => unknown updateHighlightedWells: (wellGroup: WellGroup) => unknown wellContents: ContentsByWell + labwareDef?: LabwareDefinition2 | null + pipetteSpec?: PipetteNameSpecs | null } const WellSelectionModalComponent = ( @@ -69,6 +72,7 @@ const WellSelectionModalComponent = ( selectWells, wellContents, updateHighlightedWells, + nozzleType, } = props const liquidDisplayColors = useSelector(selectors.getLiquidDisplayColors) @@ -107,7 +111,7 @@ const WellSelectionModalComponent = ( selectWells={selectWells} deselectWells={deselectWells} updateHighlightedWells={updateHighlightedWells} - pipetteChannels={pipetteSpec ? pipetteSpec.channels : null} + nozzleType={nozzleType} ingredNames={ingredNames} wellContents={wellContents} /> @@ -121,9 +125,14 @@ const WellSelectionModalComponent = ( export const WellSelectionModal = ( props: WellSelectionModalProps ): JSX.Element | null => { - const { isOpen, labwareId, onCloseClick, pipetteId } = props + const { + isOpen, + labwareId, + onCloseClick, + pipetteId, + nozzleType = null, + } = props const wellFieldData = props.value - // selector data const allWellContentsForStep = useSelector( wellContentsSelectors.getAllWellContentsForActiveItem @@ -176,6 +185,7 @@ export const WellSelectionModal = ( ingredNames, labwareDef, onCloseClick, + nozzleType, pipetteSpec: pipette?.spec, selectWells, selectedPrimaryWells, diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts index 5bad7c89f74..2d7b9aa137a 100644 --- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts +++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts @@ -1,12 +1,13 @@ import { connect } from 'react-redux' +import { ALL, COLUMN } from '@opentrons/shared-data' +import { selectors as stepFormSelectors } from '../../../../step-forms' import { WellSelectionInput, Props as WellSelectionInputProps, DP, } from './WellSelectionInput' -import { selectors as stepFormSelectors } from '../../../../step-forms' -import { BaseState } from '../../../../types' -import { FieldProps } from '../../types' +import type { BaseState, NozzleType } from '../../../../types' +import type { FieldProps } from '../../types' type Props = Omit< JSX.LibraryManagedAttributes< @@ -16,25 +17,36 @@ type Props = Omit< keyof DP > type OP = FieldProps & { + nozzles: string | null labwareId?: string | null pipetteId?: string | null } interface SP { - is8Channel: Props['is8Channel'] + nozzleType: Props['nozzleType'] primaryWellCount: Props['primaryWellCount'] } const mapStateToProps = (state: BaseState, ownProps: OP): SP => { - const { pipetteId } = ownProps + const { pipetteId, nozzles } = ownProps const selectedWells = ownProps.value const pipette = pipetteId && stepFormSelectors.getPipetteEntities(state)[pipetteId] const is8Channel = pipette ? pipette.spec.channels === 8 : false + + let nozzleType: NozzleType | null = null + if (pipette !== null && is8Channel) { + nozzleType = '8-channel' + } else if (nozzles === COLUMN) { + nozzleType = COLUMN + } else if (nozzles === ALL) { + nozzleType = ALL + } + return { primaryWellCount: Array.isArray(selectedWells) ? selectedWells.length : undefined, - is8Channel, + nozzleType, } } @@ -53,7 +65,7 @@ function mergeProps(stateProps: SP, _dispatchProps: null, ownProps: OP): Props { return { disabled, errorToShow, - is8Channel: stateProps.is8Channel, + nozzleType: stateProps.nozzleType, labwareId, name, onFieldBlur, diff --git a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx index 55cae67eaac..2cb83da515d 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MixForm.tsx @@ -1,7 +1,9 @@ import * as React from 'react' import cx from 'classnames' +import { useSelector } from 'react-redux' import { FormGroup } from '@opentrons/components' import { i18n } from '../../../localization' +import { getPipetteEntities } from '../../../step-forms/selectors' import { BlowoutLocationField, ChangeTipField, @@ -20,17 +22,22 @@ import { getBlowoutLocationOptionsForForm, getLabwareFieldForPositioningField, } from '../utils' -import { AspDispSection } from './AspDispSection' +import { Configure96ChannelField } from '../fields/Configure96ChannelField' import { DropTipField } from '../fields/DropTipField' +import { AspDispSection } from './AspDispSection' -import { StepFormProps } from '../types' +import type { StepFormProps } from '../types' import styles from '../StepEditForm.css' export const MixForm = (props: StepFormProps): JSX.Element => { const [collapsed, setCollapsed] = React.useState(true) + const pipettes = useSelector(getPipetteEntities) const { propsForFields, formData } = props + const is96Channel = + propsForFields.pipette.value != null && + pipettes[String(propsForFields.pipette.value)].name === 'p1000_96' const toggleCollapsed = (): void => setCollapsed(prevCollapsed => !prevCollapsed) @@ -44,6 +51,9 @@ export const MixForm = (props: StepFormProps): JSX.Element => {
+ {is96Channel ? ( + + ) : null} { {...propsForFields.wells} labwareId={formData.labware} pipetteId={formData.pipette} + nozzles={ + propsForFields.nozzles?.value != null + ? String(propsForFields.nozzles.value) + : null + } />
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestHeaders.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestHeaders.tsx index d79a29cadce..e8cfb602cc4 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestHeaders.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/SourceDestHeaders.tsx @@ -54,6 +54,7 @@ export const SourceDestHeaders = (props: Props): JSX.Element => { {...propsForFields[addFieldNamePrefix('wells')]} labwareId={trashOrLabwareId} pipetteId={formData.pipette} + nozzles={String(propsForFields.nozzles.value) ?? null} /> )}
diff --git a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx index 8a3ff2aeef8..932eb975d8b 100644 --- a/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx +++ b/protocol-designer/src/components/StepEditForm/forms/MoveLiquidForm/index.tsx @@ -1,6 +1,8 @@ import * as React from 'react' import cx from 'classnames' +import { useSelector } from 'react-redux' import { i18n } from '../../../../localization' +import { getPipetteEntities } from '../../../../step-forms/selectors' import { VolumeField, PipetteField, @@ -8,6 +10,7 @@ import { DisposalVolumeField, PathField, } from '../../fields' +import { Configure96ChannelField } from '../../fields/Configure96ChannelField' import { DropTipField } from '../../fields/DropTipField' import styles from '../../StepEditForm.css' import { SourceDestFields } from './SourceDestFields' @@ -19,11 +22,15 @@ import type { StepFormProps } from '../../types' export const MoveLiquidForm = (props: StepFormProps): JSX.Element => { const [collapsed, _setCollapsed] = React.useState(true) + const pipettes = useSelector(getPipetteEntities) const toggleCollapsed = (): void => _setCollapsed(!collapsed) const { propsForFields, formData } = props const { stepType, path } = formData + const is96Channel = + propsForFields.pipette.value != null && + pipettes[String(propsForFields.pipette.value)].name === 'p1000_96' return (
@@ -34,6 +41,9 @@ export const MoveLiquidForm = (props: StepFormProps): JSX.Element => {
+ {is96Channel ? ( + + ) : null} unknown deselectWells: (wellGroup: WellGroup) => unknown updateHighlightedWells: (wellGroup: WellGroup) => unknown - pipetteChannels?: Channels | null + nozzleType: NozzleType | null ingredNames: WellIngredientNames wellContents: ContentsByWell } +type ChannelType = 8 | 96 + +const getChannelsFromNozleType = (nozzleType: NozzleType): ChannelType => { + if (nozzleType === '8-channel' || nozzleType === COLUMN) { + return 8 + } else { + return 96 + } +} + export class SelectableLabware extends React.Component { _getWellsFromRect: (rect: GenericRect) => WellGroup = rect => { const selectedWells = getCollidingWells(rect, SELECTABLE_WELL_CLASS) @@ -45,9 +56,10 @@ export class SelectableLabware extends React.Component { selectedWells: WellGroup ) => WellGroup = selectedWells => { const labwareDef = this.props.labwareProps.definition + // Returns PRIMARY WELLS from the selection. - if (this.props.pipetteChannels === 8 || this.props.pipetteChannels === 96) { - const channels = this.props.pipetteChannels + if (this.props.nozzleType != null) { + const channels = getChannelsFromNozleType(this.props.nozzleType) // for the wells that have been highlighted, // get all 8-well well sets and merge them const primaryWells: WellGroup = reduce( @@ -76,11 +88,8 @@ export class SelectableLabware extends React.Component { ) => { const labwareDef = this.props.labwareProps.definition if (!e.shiftKey) { - if ( - this.props.pipetteChannels === 8 || - this.props.pipetteChannels === 96 - ) { - const channels = this.props.pipetteChannels + if (this.props.nozzleType != null) { + const channels = getChannelsFromNozleType(this.props.nozzleType) const selectedWells = this._getWellsFromRect(rect) const allWellsForMulti: WellGroup = reduce( selectedWells, @@ -115,8 +124,8 @@ export class SelectableLabware extends React.Component { } handleMouseEnterWell: (args: WellMouseEvent) => void = args => { - if (this.props.pipetteChannels === 8 || this.props.pipetteChannels === 96) { - const channels = this.props.pipetteChannels + if (this.props.nozzleType != null) { + const channels = getChannelsFromNozleType(this.props.nozzleType) const labwareDef = this.props.labwareProps.definition const wellSet = getWellSetForMultichannel( labwareDef, @@ -140,19 +149,20 @@ export class SelectableLabware extends React.Component { labwareProps, ingredNames, wellContents, - pipetteChannels, + nozzleType, selectedPrimaryWells, } = this.props // For rendering, show all wells not just primary wells const allSelectedWells = - pipetteChannels === 8 || pipetteChannels === 96 + nozzleType != null ? reduce( selectedPrimaryWells, (acc, _, wellName): WellGroup => { + const channels = getChannelsFromNozleType(nozzleType) const wellSet = getWellSetForMultichannel( this.props.labwareProps.definition, wellName, - pipetteChannels + channels ) if (!wellSet) return acc return { ...acc, ...arrayToWellGroup(wellSet) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx index 51ae447f8fb..5616f2b55ec 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/ModulesAndOtherTile.tsx @@ -79,11 +79,8 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { featureFlagSelectors.getDisableModuleRestrictions ) const [targetProps, tooltipProps] = useHoverTooltip() - const enableDeckModification = useSelector( - featureFlagSelectors.getEnableDeckModification - ) const hasATrash = - robotType === FLEX_ROBOT_TYPE && enableDeckModification + robotType === FLEX_ROBOT_TYPE ? values.additionalEquipment.includes('wasteChute') || values.additionalEquipment.includes('trashBin') : true @@ -151,10 +148,7 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { onSetFieldTouched={setFieldTouched} /> ) : ( - + )} {robotType === OT2_ROBOT_TYPE && moduleRestrictionsDisabled !== true ? modCrashWarning @@ -168,26 +162,11 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { > { - if (!enableDeckModification || robotType === OT2_ROBOT_TYPE) { - if (values.pipettesByMount.left.pipetteName === 'p1000_96') { - goBack(4) - } else if ( - values.pipettesByMount.right.pipetteName === '' && - robotType === FLEX_ROBOT_TYPE - ) { - goBack(3) - } else if ( - values.pipettesByMount.right.pipetteName === '' && - robotType === OT2_ROBOT_TYPE - ) { - goBack(2) - } else if ( - values.pipettesByMount.right.pipetteName !== '' && - robotType === FLEX_ROBOT_TYPE - ) { + if (robotType === OT2_ROBOT_TYPE) { + if (values.pipettesByMount.right.pipetteName === '') { goBack(2) } else { - goBack() + goBack(1) } } else { goBack() @@ -211,11 +190,9 @@ export function ModulesAndOtherTile(props: WizardTileProps): JSX.Element { ) } -interface FlexModuleFieldsProps extends WizardTileProps { - enableDeckModification: boolean -} -function FlexModuleFields(props: FlexModuleFieldsProps): JSX.Element { - const { values, setFieldValue, enableDeckModification } = props + +function FlexModuleFields(props: WizardTileProps): JSX.Element { + const { values, setFieldValue } = props const isFlex = values.fields.robotType === FLEX_ROBOT_TYPE const trashBinDisabled = getTrashBinOptionDisabled(values) @@ -284,7 +261,7 @@ function FlexModuleFields(props: FlexModuleFieldsProps): JSX.Element { text="Gripper" showCheckbox /> - {enableDeckModification && isFlex ? ( + {isFlex ? ( <> handleSetEquipmentOption('wasteChute')} diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx index 7b9f1135f2d..c90a6006352 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTypeTile.tsx @@ -1,6 +1,5 @@ import * as React from 'react' import { css } from 'styled-components' -import { useSelector } from 'react-redux' import { FormikProps } from 'formik' import { DIRECTION_COLUMN, @@ -23,7 +22,6 @@ import { getAllPipetteNames, getPipetteNameSpecs, } from '@opentrons/shared-data' -import { getAllow96Channel } from '../../../feature-flags/selectors' import { i18n } from '../../../localization' import { GoBack } from './GoBack' @@ -39,13 +37,12 @@ export function FirstPipetteTypeTile( > ): JSX.Element { const mount = LEFT - const allow96Channel = useSelector(getAllow96Channel) return ( ) @@ -136,7 +133,6 @@ function PipetteField(props: OT2FieldProps): JSX.Element { display96Channel, } = props const robotType = values.fields.robotType - const allow96Channel = useSelector(getAllow96Channel) const pipetteOptions = React.useMemo(() => { const allPipetteOptions = getAllPipetteNames('maxVolume', 'channels') .filter(name => @@ -149,7 +145,7 @@ function PipetteField(props: OT2FieldProps): JSX.Element { name: getPipetteNameSpecs(name)?.displayName ?? '', })) const noneOption = allowNoPipette ? [{ name: 'None', value: '' }] : [] - return allow96Channel && display96Channel + return display96Channel ? [...allPipetteOptions, ...noneOption] : [ ...allPipetteOptions.filter(o => o.value !== 'p1000_96'), diff --git a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx index 423b0c93689..7b3c754e5e5 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/StagingAreaTile.tsx @@ -1,5 +1,4 @@ import * as React from 'react' -import { useSelector } from 'react-redux' import without from 'lodash/without' import { DIRECTION_COLUMN, @@ -18,7 +17,6 @@ import { STAGING_AREA_RIGHT_SLOT_FIXTURE, } from '@opentrons/shared-data' import { i18n } from '../../../localization' -import { getEnableDeckModification } from '../../../feature-flags/selectors' import { GoBack } from './GoBack' import { HandleEnter } from './HandleEnter' @@ -28,7 +26,6 @@ import type { WizardTileProps } from './types' export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { const { values, goBack, proceed, setFieldValue } = props const isOt2 = values.fields.robotType === OT2_ROBOT_TYPE - const deckConfigurationFF = useSelector(getEnableDeckModification) const stagingAreaItems = values.additionalEquipment.filter(equipment => // TODO(bc, 11/14/2023): refactor the additional items field to include a cutoutId // and a cutoutFixtureId so that we don't have to string parse here to generate them @@ -73,7 +70,7 @@ export function StagingAreaTile(props: WizardTileProps): JSX.Element | null { initialSlots ) - if (!deckConfigurationFF || isOt2) { + if (isOt2) { proceed() return null } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/CreateFileWizard.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/CreateFileWizard.test.tsx index 1e8adbaa1aa..2790175db47 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/CreateFileWizard.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/CreateFileWizard.test.tsx @@ -17,10 +17,7 @@ import { toggleIsGripperRequired, createDeckFixture, } from '../../../../step-forms/actions/additionalItems' -import { - getAllowAllTipracks, - getEnableDeckModification, -} from '../../../../feature-flags/selectors' +import { getAllowAllTipracks } from '../../../../feature-flags/selectors' import { getTiprackOptions } from '../../utils' import { CreateFileWizard } from '..' @@ -76,9 +73,6 @@ const mockCreateModule = createModule as jest.MockedFunction< const mockCreateDeckFixture = createDeckFixture as jest.MockedFunction< typeof createDeckFixture > -const mockGetEnableDeckModification = getEnableDeckModification as jest.MockedFunction< - typeof getEnableDeckModification -> const render = () => { return renderWithProviders()[0] } @@ -91,7 +85,6 @@ const ten = '10uL' describe('CreateFileWizard', () => { beforeEach(() => { - mockGetEnableDeckModification.mockReturnValue(false) mockGetNewProtocolModal.mockReturnValue(true) mockGetAllowAllTipracks.mockReturnValue(false) mockGetLabwareDefsByURI.mockReturnValue({ @@ -155,7 +148,6 @@ describe('CreateFileWizard', () => { expect(mockToggleNewProtocolModal).toHaveBeenCalled() }) it('renders the wizard for a Flex with custom tiprack', () => { - mockGetEnableDeckModification.mockReturnValue(true) const Custom = 'custom' mockGetCustomLabwareDefsByURI.mockReturnValue({ [Custom]: fixtureTipRack10ul, diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx index 22aa30f301f..4f169c8d689 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/ModulesAndOtherTile.test.tsx @@ -2,10 +2,7 @@ import * as React from 'react' import i18n from 'i18next' import { renderWithProviders } from '@opentrons/components' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' -import { - getDisableModuleRestrictions, - getEnableDeckModification, -} from '../../../../feature-flags/selectors' +import { getDisableModuleRestrictions } from '../../../../feature-flags/selectors' import { CrashInfoBox } from '../../../modules' import { ModuleFields } from '../../FilePipettesModal/ModuleFields' import { ModulesAndOtherTile } from '../ModulesAndOtherTile' @@ -13,10 +10,10 @@ import { EquipmentOption } from '../EquipmentOption' import type { FormPipettesByMount } from '../../../../step-forms' import type { FormState, WizardTileProps } from '../types' -jest.mock('../../../../feature-flags/selectors') jest.mock('../../../modules') jest.mock('../../FilePipettesModal/ModuleFields') jest.mock('../EquipmentOption') +jest.mock('../../../../feature-flags/selectors') jest.mock('../../FilePipettesModal') const mockEquipmentOption = EquipmentOption as jest.MockedFunction< @@ -31,9 +28,6 @@ const mockGetDisableModuleRestrictions = getDisableModuleRestrictions as jest.Mo const mockModuleFields = ModuleFields as jest.MockedFunction< typeof ModuleFields > -const mockGetEnableDeckModification = getEnableDeckModification as jest.MockedFunction< - typeof getEnableDeckModification -> const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -79,21 +73,8 @@ describe('ModulesAndOtherTile', () => { mockEquipmentOption.mockReturnValue(
mock EquipmentOption
) mockGetDisableModuleRestrictions.mockReturnValue(false) mockModuleFields.mockReturnValue(
mock ModuleFields
) - mockGetEnableDeckModification.mockReturnValue(false) - }) - - it('renders correct module + gripper length for flex', () => { - const { getByText, getAllByText, getByRole } = render(props) - getByText('Choose additional items') - expect(getAllByText('mock EquipmentOption')).toHaveLength(5) - getByText('Go back') - getByRole('button', { name: 'GoBack_button' }).click() - expect(props.goBack).toHaveBeenCalled() - getByText('Review file details').click() - expect(props.proceed).toHaveBeenCalled() }) it('renders correct module, gripper and trash length for flex with disabled button', () => { - mockGetEnableDeckModification.mockReturnValue(true) const { getByText, getAllByText, getByRole } = render(props) getByText('Choose additional items') expect(getAllByText('mock EquipmentOption')).toHaveLength(7) @@ -110,7 +91,6 @@ describe('ModulesAndOtherTile', () => { additionalEquipment: ['trashBin'], }, } as WizardTileProps - mockGetEnableDeckModification.mockReturnValue(true) const { getByText, getAllByText, getByRole } = render(props) getByText('Choose additional items') expect(getAllByText('mock EquipmentOption')).toHaveLength(7) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx index 74d5fe5a8f0..8537297c095 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/PipetteTypeTile.test.tsx @@ -6,19 +6,13 @@ import { PipetteTypeTile } from '../PipetteTypeTile' import { EquipmentOption } from '../EquipmentOption' import type { FormPipettesByMount } from '../../../../step-forms' import type { FormState, WizardTileProps } from '../types' -import { getAllow96Channel } from '../../../../feature-flags/selectors' jest.mock('../EquipmentOption') -jest.mock('../../../../feature-flags/selectors') const mockEquipmentOption = EquipmentOption as jest.MockedFunction< typeof EquipmentOption > -const mockGetAllow96Channel = getAllow96Channel as jest.MockedFunction< - typeof getAllow96Channel -> - const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18n, @@ -65,7 +59,6 @@ describe('PipetteTypeTile', () => { display96Channel: true, } mockEquipmentOption.mockReturnValue(
mock EquipmentOption
) - mockGetAllow96Channel.mockReturnValue(true) }) it('renders the correct pipettes for flex with no empty pip allowed and btn ctas work', () => { const { getByText, getAllByText, getByRole } = render(props) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/StagingAreaTile.test.tsx b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/StagingAreaTile.test.tsx index 87029c3450d..b8b665d9385 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/__tests__/StagingAreaTile.test.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/__tests__/StagingAreaTile.test.tsx @@ -2,17 +2,12 @@ import * as React from 'react' import i18n from 'i18next' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { DeckConfigurator, renderWithProviders } from '@opentrons/components' -import { getEnableDeckModification } from '../../../../feature-flags/selectors' import { StagingAreaTile } from '../StagingAreaTile' import type { FormState, WizardTileProps } from '../types' -jest.mock('../../../../feature-flags/selectors') jest.mock('@opentrons/components/src/hardware-sim/DeckConfigurator/index') -const mockGetEnableDeckModification = getEnableDeckModification as jest.MockedFunction< - typeof getEnableDeckModification -> const mockDeckConfigurator = DeckConfigurator as jest.MockedFunction< typeof DeckConfigurator > @@ -44,7 +39,6 @@ describe('StagingAreaTile', () => { ...props, ...mockWizardTileProps, } as WizardTileProps - mockGetEnableDeckModification.mockReturnValue(true) mockDeckConfigurator.mockReturnValue(
mock deck configurator
) }) it('renders null when robot type is ot-2', () => { diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index d00890051e7..845256cb336 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -45,7 +45,6 @@ import * as labwareDefSelectors from '../../../labware-defs/selectors' import * as labwareDefActions from '../../../labware-defs/actions' import * as labwareIngredActions from '../../../labware-ingred/actions' import { actions as steplistActions } from '../../../steplist' -import { getEnableDeckModification } from '../../../feature-flags/selectors' import { createDeckFixture, toggleIsGripperRequired, @@ -100,8 +99,6 @@ export function CreateFileWizard(): JSX.Element | null { const customLabware = useSelector( labwareDefSelectors.getCustomLabwareDefsByURI ) - const enableDeckModification = useSelector(getEnableDeckModification) - const [currentStepIndex, setCurrentStepIndex] = React.useState(0) const [wizardSteps, setWizardSteps] = React.useState( WIZARD_STEPS @@ -209,11 +206,7 @@ export function CreateFileWizard(): JSX.Element | null { ) // add trash - if ( - (enableDeckModification && - values.additionalEquipment.includes('trashBin')) || - !enableDeckModification - ) { + if (values.additionalEquipment.includes('trashBin')) { // defaulting trash to appropriate locations dispatch( createDeckFixture( @@ -226,17 +219,14 @@ export function CreateFileWizard(): JSX.Element | null { } // add waste chute - if ( - enableDeckModification && - values.additionalEquipment.includes('wasteChute') - ) { + if (values.additionalEquipment.includes('wasteChute')) { dispatch(createDeckFixture('wasteChute', WASTE_CHUTE_CUTOUT)) } // add staging areas const stagingAreas = values.additionalEquipment.filter(equipment => equipment.includes('stagingArea') ) - if (enableDeckModification && stagingAreas.length > 0) { + if (stagingAreas.length > 0) { stagingAreas.forEach(stagingArea => { const [, location] = stagingArea.split('_') dispatch(createDeckFixture('stagingArea', location)) diff --git a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx index c83b6301654..23645c6919a 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx @@ -14,17 +14,14 @@ import { OT2_PIPETTES, OT2_ROBOT_TYPE, OT3_PIPETTES, + RIGHT, RobotType, } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { createCustomTiprackDef } from '../../../labware-defs/actions' import { getLabwareDefsByURI } from '../../../labware-defs/selectors' import { FormPipettesByMount } from '../../../step-forms' -import { - getAllowAllTipracks, - getAllow96Channel, -} from '../../../feature-flags/selectors' -import { RIGHT } from '@opentrons/shared-data/js/constants' +import { getAllowAllTipracks } from '../../../feature-flags/selectors' import { getTiprackOptions } from '../utils' import { PipetteDiagram } from './PipetteDiagram' @@ -90,7 +87,6 @@ export function PipetteFields(props: Props): JSX.Element { } = props const allowAllTipracks = useSelector(getAllowAllTipracks) - const allow96Channel = useSelector(getAllow96Channel) const dispatch = useDispatch() const allLabware = useSelector(getLabwareDefsByURI) const initialTabIndex = props.initialTabIndex || 1 @@ -105,7 +101,7 @@ export function PipetteFields(props: Props): JSX.Element { const { tabIndex, mount } = props const pipetteName = values[mount].pipetteName - const filter96 = !allow96Channel || mount === RIGHT ? ['p1000_96'] : [] + const filter96 = mount === RIGHT ? ['p1000_96'] : [] return ( diff --git a/protocol-designer/src/components/modals/FileUploadMessageModal/modalContents.tsx b/protocol-designer/src/components/modals/FileUploadMessageModal/modalContents.tsx index 0d8cadfe0c7..f569bb768e1 100644 --- a/protocol-designer/src/components/modals/FileUploadMessageModal/modalContents.tsx +++ b/protocol-designer/src/components/modals/FileUploadMessageModal/modalContents.tsx @@ -86,6 +86,11 @@ export const toV8MigrationMessage: ModalContents = { next to them in the Protocol Timeline. To resolve the error, choose another location for aspirating or mixing.

+

+ Additionally, we have addressed a bug where blow out speeds were slower + than expected. Your protocol will automatically update the flow rates + unless they were specifically initialized. +

As always, please contact us with any questions or feedback.

), diff --git a/protocol-designer/src/components/modules/EditModulesCard.tsx b/protocol-designer/src/components/modules/EditModulesCard.tsx index ccec79d9174..3b8c1b3f7d9 100644 --- a/protocol-designer/src/components/modules/EditModulesCard.tsx +++ b/protocol-designer/src/components/modules/EditModulesCard.tsx @@ -17,7 +17,6 @@ import { } from '../../step-forms' import { selectors as featureFlagSelectors } from '../../feature-flags' import { SUPPORTED_MODULE_TYPES } from '../../modules' -import { getEnableDeckModification } from '../../feature-flags/selectors' import { getAdditionalEquipment } from '../../step-forms/selectors' import { deleteDeckFixture, @@ -39,7 +38,6 @@ export interface Props { export function EditModulesCard(props: Props): JSX.Element { const { modules, openEditModuleModal } = props - const enableDeckModification = useSelector(getEnableDeckModification) const pipettesByMount = useSelector( stepFormSelectors.getPipettesForEditPipetteForm ) @@ -153,7 +151,7 @@ export function EditModulesCard(props: Props): JSX.Element { ) } })} - {enableDeckModification && isFlex ? ( + {isFlex ? ( <> - + {hasConflictedSlot ? ( @@ -138,19 +139,12 @@ const StagingAreasModalComponent = ( ) : null} - - - - - + + -const mockGetEnableDeckModification = getEnableDeckModification as jest.MockedFunction< - typeof getEnableDeckModification -> const mockGetLabwareEntities = getLabwareEntities as jest.MockedFunction< typeof getLabwareEntities > @@ -107,7 +101,6 @@ describe('EditModulesCard', () => { tiprackDefURI: null, }, }) - mockGetEnableDeckModification.mockReturnValue(false) mockGetLabwareEntities.mockReturnValue({}) mockGetInitialDeckSetup.mockReturnValue({ labware: { @@ -274,10 +267,14 @@ describe('EditModulesCard', () => { it('displays gripper row with no gripper', () => { mockGetRobotType.mockReturnValue(FLEX_ROBOT_TYPE) const wrapper = render(props) - expect(wrapper.find(AdditionalItemsRow)).toHaveLength(1) - expect(wrapper.find(AdditionalItemsRow).props().isEquipmentAdded).toEqual( - false - ) + expect(wrapper.find(AdditionalItemsRow)).toHaveLength(3) + expect( + wrapper.find(AdditionalItemsRow).filter({ name: 'gripper' }).props() + ).toEqual({ + isEquipmentAdded: false, + name: 'gripper', + handleAttachment: expect.anything(), + }) }) it('displays gripper row with gripper attached', () => { const mockGripperId = 'gripeprId' @@ -286,16 +283,19 @@ describe('EditModulesCard', () => { [mockGripperId]: { name: 'gripper', id: mockGripperId }, }) const wrapper = render(props) - expect(wrapper.find(AdditionalItemsRow)).toHaveLength(1) - expect(wrapper.find(AdditionalItemsRow).props().isEquipmentAdded).toEqual( - true - ) + expect(wrapper.find(AdditionalItemsRow)).toHaveLength(3) + expect( + wrapper.find(AdditionalItemsRow).filter({ name: 'gripper' }).props() + ).toEqual({ + isEquipmentAdded: true, + name: 'gripper', + handleAttachment: expect.anything(), + }) }) it('displays gripper waste chute, staging area, and trash row with all are attached', () => { const mockGripperId = 'gripperId' const mockWasteChuteId = 'wasteChuteId' const mockStagingAreaId = 'stagingAreaId' - mockGetEnableDeckModification.mockReturnValue(true) mockGetRobotType.mockReturnValue(FLEX_ROBOT_TYPE) mockGetAdditionalEquipment.mockReturnValue({ mockGripperId: { name: 'gripper', id: mockGripperId }, @@ -309,12 +309,11 @@ describe('EditModulesCard', () => { id: mockStagingAreaId, location: 'B3', }, - }) - mockGetLabwareEntities.mockReturnValue({ - mockTrashId: { + mockTrash: { + name: 'trashBin', id: mockTrashId, - labwareDefURI: FLEX_TRASH_DEF_URI, - } as any, + location: 'cutoutA3', + }, }) props = { diff --git a/protocol-designer/src/constants.ts b/protocol-designer/src/constants.ts index 92b2ba2d5df..734d132948d 100644 --- a/protocol-designer/src/constants.ts +++ b/protocol-designer/src/constants.ts @@ -156,6 +156,3 @@ export const DND_TYPES = { // Values for TC fields export const THERMOCYCLER_STATE: 'thermocyclerState' = 'thermocyclerState' export const THERMOCYCLER_PROFILE: 'thermocyclerProfile' = 'thermocyclerProfile' - -export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' -export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 0396dc0a83c..d4492497624 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -21,11 +21,7 @@ const initialFlags: Flags = { process.env.OT_PD_DISABLE_MODULE_RESTRICTIONS === '1' || false, OT_PD_ALLOW_ALL_TIPRACKS: process.env.OT_PD_ALLOW_ALL_TIPRACKS === '1' || false, - OT_PD_ALLOW_96_CHANNEL: process.env.OT_PD_ALLOW_96_CHANNEL === '1' || false, - OT_PD_ENABLE_FLEX_DECK_MODIFICATION: - process.env.OT_PD_ENABLE_FLEX_DECK_MODIFICATION === '1' || false, - OT_PD_ENABLE_OFF_DECK_VIS_AND_MULTI_TIP: - process.env.OT_PD_ENABLE_OFF_DECK_VIS_AND_MULTI_TIP === '1' || false, + OT_PD_ENABLE_MULTI_TIP: process.env.OT_PD_ENABLE_MULTI_TIP === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index e5c91c86cdf..0a27f7fc108 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -19,15 +19,7 @@ export const getAllowAllTipracks: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ALLOW_ALL_TIPRACKS ?? false ) -export const getAllow96Channel: Selector = createSelector( +export const getEnableMultiTip: Selector = createSelector( getFeatureFlagData, - flags => flags.OT_PD_ALLOW_96_CHANNEL ?? false -) -export const getEnableDeckModification: Selector = createSelector( - getFeatureFlagData, - flags => flags.OT_PD_ENABLE_FLEX_DECK_MODIFICATION ?? false -) -export const getEnableOffDeckVisAndMultiTip: Selector = createSelector( - getFeatureFlagData, - flags => flags.OT_PD_ENABLE_OFF_DECK_VIS_AND_MULTI_TIP ?? false + flags => flags.OT_PD_ENABLE_MULTI_TIP ?? false ) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 15e305bab36..1e441bb9062 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -20,15 +20,15 @@ export const DEPRECATED_FLAGS = [ 'OT_PD_ENABLE_THERMOCYCLER_GEN_2', 'OT_PD_ENABLE_LIQUID_COLOR_ENHANCEMENTS', 'OT_PD_ENABLE_OT_3', + 'OT_PD_ALLOW_96_CHANNEL', + 'OT_PD_ENABLE_FLEX_DECK_MODIFICATION', ] // union of feature flag string constant IDs export type FlagTypes = | 'PRERELEASE_MODE' | 'OT_PD_DISABLE_MODULE_RESTRICTIONS' | 'OT_PD_ALLOW_ALL_TIPRACKS' - | 'OT_PD_ALLOW_96_CHANNEL' - | 'OT_PD_ENABLE_FLEX_DECK_MODIFICATION' - | 'OT_PD_ENABLE_OFF_DECK_VIS_AND_MULTI_TIP' + | 'OT_PD_ENABLE_MULTI_TIP' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', diff --git a/protocol-designer/src/file-data/__tests__/createFile.test.ts b/protocol-designer/src/file-data/__tests__/createFile.test.ts index d3677c91f42..ef94c800aea 100644 --- a/protocol-designer/src/file-data/__tests__/createFile.test.ts +++ b/protocol-designer/src/file-data/__tests__/createFile.test.ts @@ -84,8 +84,6 @@ describe('createFile selector', () => { pipetteEntities, labwareNicknamesById, labwareDefsByURI, - // TODO(jr, 10/3/23): add additionalEquipmentEntities when the schemaV8 supports - // loadAddressableArea {} ) expectResultToMatchSchema(result) diff --git a/protocol-designer/src/file-data/selectors/commands.ts b/protocol-designer/src/file-data/selectors/commands.ts index e30a8a2c015..924767297bd 100644 --- a/protocol-designer/src/file-data/selectors/commands.ts +++ b/protocol-designer/src/file-data/selectors/commands.ts @@ -55,18 +55,18 @@ export const getInitialRobotState: ( stepFormSelectors.getInvariantContext, getLabwareLiquidState, (initialDeckSetup, invariantContext, labwareLiquidState) => { - const labware: Record = mapValues( - initialDeckSetup.labware, - (l: LabwareOnDeck): LabwareTemporalProperties => ({ - slot: l.slot, - }) - ) const pipettes: Record = mapValues( initialDeckSetup.pipettes, (p: PipetteOnDeck): PipetteTemporalProperties => ({ mount: p.mount, }) ) + const labware: Record = mapValues( + initialDeckSetup.labware, + (l: LabwareOnDeck): LabwareTemporalProperties => ({ + slot: l.slot, + }) + ) const modules: Record = mapValues( initialDeckSetup.modules, (m: ModuleOnDeck): ModuleTemporalProperties => { diff --git a/protocol-designer/src/file-data/selectors/fileCreator.ts b/protocol-designer/src/file-data/selectors/fileCreator.ts index 4f265db32dd..f7b75d84ff5 100644 --- a/protocol-designer/src/file-data/selectors/fileCreator.ts +++ b/protocol-designer/src/file-data/selectors/fileCreator.ts @@ -27,7 +27,6 @@ import { getLoadLiquidCommands, } from '../../load-file/migration/utils/getLoadLiquidCommands' import { swatchColors } from '../../components/swatchColors' -import { getAdditionalEquipmentEntities } from '../../step-forms/selectors' import { DEFAULT_MM_FROM_BOTTOM_ASPIRATE, DEFAULT_MM_FROM_BOTTOM_DISPENSE, @@ -114,8 +113,6 @@ export const createFile: Selector = createSelector( stepFormSelectors.getPipetteEntities, uiLabwareSelectors.getLabwareNicknamesById, labwareDefSelectors.getLabwareDefsByURI, - getAdditionalEquipmentEntities, - ( fileMetadata, initialRobotState, @@ -130,8 +127,7 @@ export const createFile: Selector = createSelector( moduleEntities, pipetteEntities, labwareNicknamesById, - labwareDefsByURI, - additionalEquipmentEntities + labwareDefsByURI ) => { const { author, description, created } = fileMetadata const name = fileMetadata.protocolName || 'untitled' @@ -286,6 +282,8 @@ export const createFile: Selector = createSelector( location = { labwareId: labware.slot } } else if (isAddressableAreaName) { location = { addressableAreaName: labware.slot } + } else if (labware.slot === 'offDeck') { + location = 'offDeck' } const loadLabwareCommands = { diff --git a/protocol-designer/src/form-types.ts b/protocol-designer/src/form-types.ts index 1994f6336b9..8aeec17909a 100644 --- a/protocol-designer/src/form-types.ts +++ b/protocol-designer/src/form-types.ts @@ -3,9 +3,12 @@ import { PAUSE_UNTIL_TIME, PAUSE_UNTIL_TEMP, } from './constants' -import { IconName } from '@opentrons/components' -import { LabwareLocation } from '@opentrons/shared-data' -import { +import type { IconName } from '@opentrons/components' +import type { + LabwareLocation, + NozzleConfigurationStyle, +} from '@opentrons/shared-data' +import type { AdditionalEquipmentEntity, ChangeTipOptions, LabwareEntity, @@ -217,6 +220,7 @@ export interface HydratedMoveLiquidFormData { blowout_checkbox: boolean blowout_location: string | null | undefined // labwareId or 'SOURCE_WELL' or 'DEST_WELL' dropTip_location: string + nozzles: NozzleConfigurationStyle | null } } @@ -256,6 +260,7 @@ export interface HydratedMixFormDataLegacy { dispense_delay_checkbox: boolean dispense_delay_seconds: number | null | undefined dropTip_location: string + nozzles: NozzleConfigurationStyle | null } export type MagnetAction = 'engage' | 'disengage' export type HydratedMagnetFormData = AnnotationFields & { diff --git a/protocol-designer/src/images/deck_configuration.png b/protocol-designer/src/images/deck_configuration.png index 5a58b5f26e9..dc31c6eb10d 100644 Binary files a/protocol-designer/src/images/deck_configuration.png and b/protocol-designer/src/images/deck_configuration.png differ diff --git a/protocol-designer/src/labware-defs/__mocks__/utils.ts b/protocol-designer/src/labware-defs/__mocks__/utils.ts index caeafd8fab8..ee6add0af2f 100644 --- a/protocol-designer/src/labware-defs/__mocks__/utils.ts +++ b/protocol-designer/src/labware-defs/__mocks__/utils.ts @@ -1,6 +1,6 @@ // replace webpack-specific require.context with Node-based glob in tests import assert from 'assert' -import { getLabwareDefURI } from '@opentrons/shared-data' +import { LabwareDefinition1, getLabwareDefURI } from '@opentrons/shared-data' import { LabwareDefByDefURI } from '../types' import path from 'path' import glob from 'glob' @@ -24,3 +24,16 @@ export const getAllDefinitions = jest.fn(() => allLabware) export const _getSharedLabware = jest.fn(() => null) export const getOnlyLatestDefs = jest.fn(() => allLabware) + +const LEGACY_LABWARE_FIXTURE_PATTERN = path.join( + __dirname, + '../../../../shared-data/labware/fixtures/1/*.json' +) +// @ts-expect-error(sa, 2021-6-20): not sure why TS thinks d is void +const legacyLabwareDefs: LabwareDefinition1[] = glob + .sync(LEGACY_LABWARE_FIXTURE_PATTERN) + .map(require) + +export const getLegacyLabwareDef = jest.fn(() => { + return legacyLabwareDefs[0] +}) diff --git a/protocol-designer/src/labware-defs/utils.ts b/protocol-designer/src/labware-defs/utils.ts index 8963bb8a826..f299e9e5a0d 100644 --- a/protocol-designer/src/labware-defs/utils.ts +++ b/protocol-designer/src/labware-defs/utils.ts @@ -2,10 +2,36 @@ import groupBy from 'lodash/groupBy' import { getLabwareDefURI, PD_DO_NOT_LIST, + LabwareDefinition1, LabwareDefinition2, } from '@opentrons/shared-data' import { LabwareDefByDefURI } from './types' +// require all definitions in the labware/definitions/1 directory +// require.context is webpack-specific method +const labwareSchemaV1DefsContext = require.context( + '@opentrons/shared-data/labware/definitions/1', + true, // traverse subdirectories + /\.json$/, // import filter + 'sync' // load every definition into one synchronous chunk +) +let labwareSchemaV1Defs: Readonly | null = null +function getLegacyLabwareDefs(): Readonly { + if (!labwareSchemaV1Defs) { + labwareSchemaV1Defs = labwareSchemaV1DefsContext + .keys() + .map((name: string) => labwareSchemaV1DefsContext(name)) + } + + return labwareSchemaV1Defs as Readonly +} +export function getLegacyLabwareDef( + loadName: string | null | undefined +): LabwareDefinition1 | null { + const def = getLegacyLabwareDefs().find(d => d.metadata.name === loadName) + return def || null +} + // TODO: Ian 2019-04-11 getAllDefinitions also exists (differently) in labware-library, // should reconcile differences & make a general util fn imported from shared-data // require all definitions in the labware/definitions/2 directory diff --git a/protocol-designer/src/load-file/migration/1_1_0.ts b/protocol-designer/src/load-file/migration/1_1_0.ts index 13e28d4fd87..26e9abc0775 100644 --- a/protocol-designer/src/load-file/migration/1_1_0.ts +++ b/protocol-designer/src/load-file/migration/1_1_0.ts @@ -4,12 +4,13 @@ import mapValues from 'lodash/mapValues' import omit from 'lodash/omit' import omitBy from 'lodash/omitBy' import flow from 'lodash/flow' -import { getLabwareV1Def, getPipetteNameSpecs } from '@opentrons/shared-data' +import { getPipetteNameSpecs } from '@opentrons/shared-data' import { FileLabware, FilePipette, ProtocolFile, } from '@opentrons/shared-data/protocol/types/schemaV1' +import { getLegacyLabwareDef } from '../../labware-defs' import { FormPatch } from '../../steplist/actions' import { FormData } from '../../form-types' export interface PDMetadata { @@ -70,7 +71,7 @@ function getPipetteCapacityLegacy( } // @ts-expect-error unable to cast type string from manipulation above to type PipetteName const specs = getPipetteNameSpecs(pipetteName) - const tiprackDef = getLabwareV1Def(pipette.tiprackModel) + const tiprackDef = getLegacyLabwareDef(pipette.tiprackModel) if (specs && tiprackDef && tiprackDef.metadata.tipVolume) { return Math.min(specs.maxVolume, tiprackDef.metadata.tipVolume) diff --git a/protocol-designer/src/load-file/migration/8_0_0.ts b/protocol-designer/src/load-file/migration/8_0_0.ts index faee8495ee9..74de76358b7 100644 --- a/protocol-designer/src/load-file/migration/8_0_0.ts +++ b/protocol-designer/src/load-file/migration/8_0_0.ts @@ -90,6 +90,7 @@ export const migrateFile = ( if (stepForm.stepType === 'moveLiquid') { return { ...stepForm, + nozzles: null, aspirate_labware: stepForm.aspirate_labware === 'fixedTrash' ? null @@ -103,6 +104,7 @@ export const migrateFile = ( } else if (stepForm.stepType === 'mix') { return { ...stepForm, + nozzles: null, labware: stepForm.labware === 'fixedTrash' ? null : stepForm.labware, ...sharedParams, } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index e420024c65a..5bf9b3c8fc2 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -167,6 +167,14 @@ "GRIPPER_REQUIRED": { "title": "A gripper is required to complete this action", "body": "Attempting to move a labware without a gripper into the waste chute. Please add a gripper to this step." + }, + "REMOVE_96_CHANNEL_TIPRACK_ADAPTER": { + "title": "Do not use tip rack adapter for partial tip pickup", + "body": "Partial tip pickup requires a tip rack placed directly on the deck. Remove the adapter, or add a new tip rack without an adapter." + }, + "CANNOT_MOVE_WITH_GRIPPER": { + "title": "Cannot move with gripper", + "body": "The gripper cannot move aluminum blocks. Edit the step and deselect the 'Use Gripper' checkbox." } }, "warning": { diff --git a/protocol-designer/src/localization/en/feature_flags.json b/protocol-designer/src/localization/en/feature_flags.json index ff6e8008d64..e234a9c5b19 100644 --- a/protocol-designer/src/localization/en/feature_flags.json +++ b/protocol-designer/src/localization/en/feature_flags.json @@ -12,16 +12,8 @@ "title": "Allow all tip rack options", "description": "Enable selection of all tip racks for each pipette." }, - "OT_PD_ALLOW_96_CHANNEL": { - "title": "Enable 96-channel pipette", - "description": "Allow users to select 96-channel pipette" - }, - "OT_PD_ENABLE_FLEX_DECK_MODIFICATION": { - "title": "Enable Flex deck modification", - "description": "Allow users to select waste chute, Flex staging, and modify trash slot" - }, - "OT_PD_ENABLE_OFF_DECK_VIS_AND_MULTI_TIP": { - "title": "Enable off-deck visuals and multi tiprack support", - "description": "Allow users to see off-deck labware visualizations and multi tiprack support" + "OT_PD_ENABLE_MULTI_TIP": { + "title": "Enable multi tiprack support", + "description": "Allow users to select multiple tipracks per pipette" } } diff --git a/protocol-designer/src/localization/en/form.json b/protocol-designer/src/localization/en/form.json index 1e51c437356..36acb83462f 100644 --- a/protocol-designer/src/localization/en/form.json +++ b/protocol-designer/src/localization/en/form.json @@ -49,6 +49,13 @@ "wells": "wells" }, "field": { + "nozzles": { + "label": "nozzles", + "option": { "ALL": "All", "COLUMN": "Column" }, + "option_tooltip": { + "COLUMN": "To use column partial tip pickup, a tiprack without an adapter must be placed on the deck." + } + }, "airGap": { "label": "air gap" }, "blowout": { "label": "blowout" }, "location": { diff --git a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts index 42504c62ba9..ba0b8f93984 100644 --- a/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts +++ b/protocol-designer/src/step-forms/test/createPresavedStepForm.test.ts @@ -127,6 +127,7 @@ describe('createPresavedStepForm', () => { expect(createPresavedStepForm(args)).toEqual({ id: stepId, pipette: 'leftPipetteId', + nozzles: null, stepType: 'moveLiquid', // default fields dropTip_location: null, @@ -184,6 +185,7 @@ describe('createPresavedStepForm', () => { stepType: 'mix', // default fields labware: null, + nozzles: null, dropTip_location: null, wells: [], aspirate_delay_checkbox: false, diff --git a/protocol-designer/src/step-forms/types.ts b/protocol-designer/src/step-forms/types.ts index 2a7db89fa94..6e700fdaa72 100644 --- a/protocol-designer/src/step-forms/types.ts +++ b/protocol-designer/src/step-forms/types.ts @@ -7,6 +7,7 @@ import { THERMOCYCLER_MODULE_TYPE, HEATERSHAKER_MODULE_TYPE, MAGNETIC_BLOCK_TYPE, + NozzleConfigurationStyle, } from '@opentrons/shared-data' import { DeckSlot } from '../types' @@ -88,6 +89,8 @@ export interface LabwareTemporalProperties { } export interface PipetteTemporalProperties { mount: Mount + nozzles?: NozzleConfigurationStyle + prevNozzles?: NozzleConfigurationStyle } // =========== ON DECK ======== // The "on deck" types are entities with added properties (slot / mount) diff --git a/protocol-designer/src/step-forms/utils/index.ts b/protocol-designer/src/step-forms/utils/index.ts index 8bd93cf176f..7f1baae8fdc 100644 --- a/protocol-designer/src/step-forms/utils/index.ts +++ b/protocol-designer/src/step-forms/utils/index.ts @@ -32,6 +32,21 @@ import type { import type { FormData } from '../../form-types' export { createPresavedStepForm } from './createPresavedStepForm' +const slotToCutoutOt2Map: { [key: string]: string } = { + '1': 'cutout1', + '2': 'cutout2', + '3': 'cutout3', + '4': 'cutout4', + '5': 'cutout5', + '6': 'cutout6', + '7': 'cutout7', + '8': 'cutout8', + '9': 'cutout9', + '10': 'cutout10', + '11': 'cutout11', + '12': 'cutout12', +} + export function getIdsInRange( orderedIds: T[], startId: T, @@ -128,24 +143,29 @@ export const getSlotIsEmpty = ( return false } - const filteredAdditionalEquipmentOnDeck = includeStagingAreas - ? values( - initialDeckSetup.additionalEquipmentOnDeck - ).filter((additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location?.includes(slot) - ) - : values(initialDeckSetup.additionalEquipmentOnDeck).filter( - (additionalEquipment: AdditionalEquipmentOnDeck) => - additionalEquipment.location?.includes(slot) && - additionalEquipment.name !== 'stagingArea' - ) + const filteredAdditionalEquipmentOnDeck = values( + initialDeckSetup.additionalEquipmentOnDeck + ).filter((additionalEquipment: AdditionalEquipmentOnDeck) => { + const cutoutForSlotOt2 = slotToCutoutOt2Map[slot] + const includeStaging = includeStagingAreas + ? true + : additionalEquipment.name !== 'stagingArea' + if (cutoutForSlotOt2 != null) { + // for Ot-2 + return additionalEquipment.location === cutoutForSlotOt2 && includeStaging + } else { + // for Flex + return additionalEquipment.location?.includes(slot) && includeStaging + } + }) + return ( [ - ...values(initialDeckSetup.modules).filter((moduleOnDeck: ModuleOnDeck) => - slot.includes(moduleOnDeck.slot) + ...values(initialDeckSetup.modules).filter( + (moduleOnDeck: ModuleOnDeck) => moduleOnDeck.slot === slot ), - ...values(initialDeckSetup.labware).filter((labware: LabwareOnDeckType) => - slot.includes(labware.slot) + ...values(initialDeckSetup.labware).filter( + (labware: LabwareOnDeckType) => labware.slot === slot ), ...filteredAdditionalEquipmentOnDeck, ].length === 0 diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index 2258d842434..69acc572228 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -148,6 +148,7 @@ export const incompatibleLabware = ( ): FormError | null => { const { labware, pipette } = fields if (!labware || !pipette) return null + // trashBin and wasteChute cannot mix into a labware return !canPipetteUseLabware(pipette.spec, labware.def) ? INCOMPATIBLE_LABWARE : null @@ -170,6 +171,7 @@ export const incompatibleAspirateLabware = ( ): FormError | null => { const { aspirate_labware, pipette } = fields if (!aspirate_labware || !pipette) return null + // trashBin and wasteChute cannot aspirate into a labware return !canPipetteUseLabware(pipette.spec, aspirate_labware.def) ? INCOMPATIBLE_ASPIRATE_LABWARE : null diff --git a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts index 6a2b44cac5e..0f63b401350 100644 --- a/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts +++ b/protocol-designer/src/steplist/formLevel/getDefaultsForStepType.ts @@ -35,6 +35,7 @@ export function getDefaultsForStepType( mix_touchTip_checkbox: false, mix_touchTip_mmFromBottom: null, dropTip_location: null, + nozzles: null, } case 'moveLiquid': @@ -82,6 +83,7 @@ export function getDefaultsForStepType( dispense_delay_seconds: `${DEFAULT_DELAY_SECONDS}`, dispense_delay_mmFromBottom: null, dropTip_location: null, + nozzles: null, } case 'moveLabware': diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts index 4291f9b5a0a..c69616feec2 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts @@ -51,15 +51,38 @@ const updatePatchOnPipetteChannelChange = ( if (patch.pipette === undefined) return patch let update = {} const prevChannels = getChannels(rawForm.pipette, pipetteEntities) - const nextChannels = + const nChannels = typeof patch.pipette === 'string' ? getChannels(patch.pipette, pipetteEntities) : null const appliedPatch = { ...rawForm, ...patch } + let previousChannels = prevChannels + if ( + rawForm.stepType === 'moveLiquid' || + (rawForm.stepType === 'mix' && prevChannels === 96) + ) { + if (rawForm.nozzles === 'full') { + previousChannels = 96 + } else { + previousChannels = 8 + } + } + let nextChannels = nChannels + if ( + rawForm.stepType === 'moveLiquid' || + (rawForm.stepType === 'mix' && nChannels === 96) + ) { + if (rawForm.nozzles === 'full') { + nextChannels = 96 + } else { + nextChannels = 8 + } + } + const singleToMulti = - prevChannels === 1 && (nextChannels === 8 || nextChannels === 96) + previousChannels === 1 && (nextChannels === 8 || nextChannels === 96) const multiToSingle = - (prevChannels === 8 || prevChannels === 96) && nextChannels === 1 + (previousChannels === 8 || previousChannels === 96) && nextChannels === 1 if (patch.pipette === null || singleToMulti) { // reset all well selection @@ -74,8 +97,15 @@ const updatePatchOnPipetteChannelChange = ( } } else if (multiToSingle) { let channels: 8 | 96 = 8 - if (prevChannels === 96) { - channels = 96 + if ( + rawForm.stepType === 'moveLiquid' || + (rawForm.stepType === 'mix' && prevChannels === 96) + ) { + if (rawForm.nozzles === 'full') { + channels = 96 + } else { + channels = 8 + } } // multi-channel to single-channel: convert primary wells to all wells const labwareId = appliedPatch.labware diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts index 6e16999f298..1edccb7c59d 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/mixFormToArgs.ts @@ -15,7 +15,7 @@ type MixStepArgs = MixArgs export const mixFormToArgs = ( hydratedFormData: HydratedMixFormDataLegacy ): MixStepArgs => { - const { labware, pipette, dropTip_location } = hydratedFormData + const { labware, pipette, dropTip_location, nozzles } = hydratedFormData const unorderedWells = hydratedFormData.wells || [] const orderFirst = hydratedFormData.mix_wellOrder_first const orderSecond = hydratedFormData.mix_wellOrder_second @@ -55,7 +55,9 @@ export const mixFormToArgs = ( ? hydratedFormData.blowout_location : null // Blowout settings - const blowoutFlowRateUlSec = dispenseFlowRateUlSec + const blowoutFlowRateUlSec = + hydratedFormData.dispense_flowRate ?? + pipette.spec.defaultBlowOutFlowRate.value const blowoutOffsetFromTopMm = blowoutLocation ? DEFAULT_MM_BLOWOUT_OFFSET_FROM_TOP : 0 @@ -94,5 +96,6 @@ export const mixFormToArgs = ( aspirateDelaySeconds, dispenseDelaySeconds, dropTipLocation: dropTip_location, + nozzles, } } diff --git a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts index 1da71fb98c8..62fde81b5cb 100644 --- a/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts +++ b/protocol-designer/src/steplist/formLevel/stepFormToArgs/moveLiquidFormToArgs.ts @@ -77,6 +77,7 @@ export const moveLiquidFormToArgs = ( dispense_wells: destWellsUnordered, dropTip_location: dropTipLocation, path, + nozzles, } = fields let sourceWells = getOrderedWells( fields.aspirate_wells, @@ -185,7 +186,7 @@ export const moveLiquidFormToArgs = ( dispenseOffsetFromBottomMm: fields.dispense_mmFromBottom || DEFAULT_MM_FROM_BOTTOM_DISPENSE, blowoutFlowRateUlSec: - fields.dispense_flowRate || pipetteSpec.defaultDispenseFlowRate.value, + fields.dispense_flowRate || pipetteSpec.defaultBlowOutFlowRate.value, blowoutOffsetFromTopMm, changeTip: fields.changeTip, preWetTip: Boolean(fields.preWetTip), @@ -200,6 +201,7 @@ export const moveLiquidFormToArgs = ( description: hydratedFormData.description, name: hydratedFormData.stepName, dropTipLocation, + nozzles, } assert( sourceWellsUnordered.length > 0, diff --git a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts index edb40f000d6..50d6fc35b85 100644 --- a/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts +++ b/protocol-designer/src/steplist/formLevel/test/getDefaultsForStepType.test.ts @@ -15,6 +15,7 @@ describe('getDefaultsForStepType', () => { it('should get the correct defaults', () => { expect(getDefaultsForStepType('moveLiquid')).toEqual({ pipette: null, + nozzles: null, volume: null, changeTip: DEFAULT_CHANGE_TIP_OPTION, path: 'single', @@ -84,6 +85,7 @@ describe('getDefaultsForStepType', () => { mix_touchTip_mmFromBottom: null, mix_touchTip_checkbox: false, pipette: null, + nozzles: null, volume: undefined, times: null, wells: [], diff --git a/protocol-designer/src/steplist/generateSubstepItem.ts b/protocol-designer/src/steplist/generateSubstepItem.ts index 320ff917cfc..d0b0f192787 100644 --- a/protocol-designer/src/steplist/generateSubstepItem.ts +++ b/protocol-designer/src/steplist/generateSubstepItem.ts @@ -281,7 +281,8 @@ function transferLikeSubsteps(args: { substepCommandCreator, invariantContext, initialRobotState, - pipetteSpec.channels + pipetteSpec.channels, + stepArgs.nozzles ) const mergedMultiRows: StepItemSourceDestRow[][] = mergeSubstepRowsMultiChannel( { @@ -304,7 +305,8 @@ function transferLikeSubsteps(args: { substepCommandCreator, invariantContext, initialRobotState, - 1 + 1, + null ) const mergedRows: StepItemSourceDestRow[] = mergeSubstepRowsSingleChannel({ substepRows, diff --git a/protocol-designer/src/steplist/substepTimeline.ts b/protocol-designer/src/steplist/substepTimeline.ts index 577ec552da7..cfd487c738b 100644 --- a/protocol-designer/src/steplist/substepTimeline.ts +++ b/protocol-designer/src/steplist/substepTimeline.ts @@ -4,6 +4,15 @@ import { getWellsForTips, getNextRobotStateAndWarningsSingleCommand, } from '@opentrons/step-generation' +import { + AddressableAreaName, + FLEX_ROBOT_TYPE, + ALL, + COLUMN, + CreateCommand, + OT2_ROBOT_TYPE, + NozzleConfigurationStyle, +} from '@opentrons/shared-data' import { Channels } from '@opentrons/components' import { getCutoutIdByAddressableArea } from '../utils' import type { @@ -13,12 +22,6 @@ import type { InvariantContext, RobotState, } from '@opentrons/step-generation' -import { - AddressableAreaName, - CreateCommand, - FLEX_ROBOT_TYPE, - OT2_ROBOT_TYPE, -} from '@opentrons/shared-data' import type { SubstepTimelineFrame, SourceDestData, TipLocation } from './types' const wasteChuteddressableAreaNamesPipette = [ @@ -211,7 +214,8 @@ export const substepTimelineMultiChannel = ( commandCreator: CurriedCommandCreator, invariantContext: InvariantContext, initialRobotState: RobotState, - channels: Channels + channels: Channels, + nozzles: NozzleConfigurationStyle | null ): SubstepTimelineFrame[] => { const nextFrame = commandCreator(invariantContext, initialRobotState) // @ts-expect-error(sa, 2021-6-14): type narrow using in operator @@ -235,10 +239,20 @@ export const substepTimelineMultiChannel = ( ? invariantContext.labwareEntities[labwareId].def : null + let numChannels = channels + if (nozzles === ALL) { + numChannels = 96 + } else if (nozzles === COLUMN) { + numChannels = 8 + } else { + console.error( + 'we currently do not support other 96-channel configurations' + ) + } const wellsForTips = - channels && + numChannels && labwareDef && - getWellsForTips(channels, labwareDef, wellName).wellsForTips + getWellsForTips(numChannels, labwareDef, wellName).wellsForTips const wellInfo = { labwareId, @@ -362,7 +376,8 @@ export const substepTimeline = ( commandCreator: CurriedCommandCreator, invariantContext: InvariantContext, initialRobotState: RobotState, - channels: Channels + channels: Channels, + nozzles: NozzleConfigurationStyle | null ): SubstepTimelineFrame[] => { if (channels === 1) { return substepTimelineSingleChannel( @@ -375,7 +390,8 @@ export const substepTimeline = ( commandCreator, invariantContext, initialRobotState, - channels + channels, + nozzles ) } } diff --git a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts index e5dfe651fda..aaea246bb90 100644 --- a/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts +++ b/protocol-designer/src/timelineMiddleware/__tests__/generateRobotStateTimeline.test.ts @@ -45,6 +45,7 @@ describe('generateRobotStateTimeline', () => { destWells: ['A12', 'A12'], mixBeforeAspirate: null, description: null, + nozzles: null, }, }, b: { @@ -79,6 +80,7 @@ describe('generateRobotStateTimeline', () => { destWells: ['A12'], mixBeforeAspirate: null, description: null, + nozzles: null, }, }, c: { @@ -105,6 +107,7 @@ describe('generateRobotStateTimeline', () => { blowoutOffsetFromTopMm: 0, aspirateDelaySeconds: null, dispenseDelaySeconds: null, + nozzles: null, }, }, } diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index d2ddec12e42..9396bd121b8 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -224,8 +224,7 @@ export const getUnoccupiedLabwareLocationOptions: Selector< const isTrashSlot = robotType === FLEX_ROBOT_TYPE ? MOVABLE_TRASH_ADDRESSABLE_AREAS.includes(slotId) - : slotId === 'fixedTrash' - + : ['fixedTrash', '12'].includes(slotId) return ( !slotIdsOccupiedByModules.includes(slotId) && !Object.values(labware) diff --git a/protocol-designer/src/top-selectors/substep-highlight.ts b/protocol-designer/src/top-selectors/substep-highlight.ts index b4d3b57c4fc..f9ce4a32c41 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.ts +++ b/protocol-designer/src/top-selectors/substep-highlight.ts @@ -1,6 +1,11 @@ import { createSelector } from 'reselect' import mapValues from 'lodash/mapValues' -import { getWellNamePerMultiTip } from '@opentrons/shared-data' +import { + ALL, + COLUMN, + getWellNamePerMultiTip, + NozzleConfigurationStyle, +} from '@opentrons/shared-data' import { WellGroup } from '@opentrons/components' import * as StepGeneration from '@opentrons/step-generation' import { selectors as stepFormSelectors } from '../step-forms' @@ -15,11 +20,21 @@ import type { SubstepItemData } from '../steplist/types' function _wellsForPipette( pipetteEntity: PipetteEntity, labwareEntity: LabwareEntity, - wells: string[] + wells: string[], + nozzles: NozzleConfigurationStyle | null ): string[] { - const channels = pipetteEntity.spec.channels + const pipChannels = pipetteEntity.spec.channels + // `wells` is all the wells that pipette's channel 1 interacts with. - if (channels === 8 || channels === 96) { + if (pipChannels === 8 || pipChannels === 96) { + let channels: 8 | 96 = pipChannels + if (nozzles === ALL) { + channels = 96 + } else if (nozzles === COLUMN) { + channels = 8 + } else { + console.error(`we don't support other 96-channel configurations yet`) + } return wells.reduce((acc: string[], well: string) => { const setOfWellsForMulti = getWellNamePerMultiTip( labwareEntity.def, @@ -54,9 +69,10 @@ function _getSelectedWellsForStep( if (!pipetteEntity || !labwareEntity) { return [] } + const nozzles = 'nozzles' in stepArgs ? stepArgs.nozzles : null const getWells = (wells: string[]): string[] => - _wellsForPipette(pipetteEntity, labwareEntity, wells) + _wellsForPipette(pipetteEntity, labwareEntity, wells, nozzles) const wells = [] @@ -94,25 +110,38 @@ function _getSelectedWellsForStep( frame.commands.forEach((c: CreateCommand) => { if (c.commandType === 'pickUpTip' && c.params.labwareId === labwareId) { - const commandWellName = c.params.wellName const pipetteId = c.params.pipetteId const pipetteSpec = invariantContext.pipetteEntities[pipetteId]?.spec || {} + let channels = 1 + if ( + stepArgs.commandCreatorFnName === 'mix' || + stepArgs.commandCreatorFnName === 'transfer' + ) { + if (stepArgs.nozzles === ALL) { + channels = 96 + } else if (stepArgs.nozzles === COLUMN) { + channels = 8 + } else { + channels = pipetteSpec.channels + } + } + const commandWellName = c.params.wellName - if (pipetteSpec.channels === 1) { + if (channels === 1) { wells.push(commandWellName) - } else if (pipetteSpec.channels === 8 || pipetteSpec.channels === 96) { + } else if (channels === 8 || channels === 96) { const wellSet = getWellSetForMultichannel( invariantContext.labwareEntities[labwareId].def, commandWellName, - pipetteSpec.channels + channels ) || [] wells.push(...wellSet) } else { console.error( `Unexpected number of channels: ${ - pipetteSpec.channels || '?' + channels || '?' }. Could not get tip highlight state` ) } @@ -185,30 +214,47 @@ function _getSelectedWellsForSubstep( if (substeps && substeps.substepType === 'sourceDest') { let tipWellSet: string[] = [] - - if (substeps.multichannel) { - const { activeTips } = substeps.multiRows[substepIndex][0] - - // just use first multi row - if (activeTips && activeTips.labwareId === labwareId) { - const multiTipWellSet = getWellSetForMultichannel( - invariantContext.labwareEntities[labwareId].def, - activeTips.wellName, - 8 + if ('pipette' in stepArgs) { + if (substeps.multichannel) { + const { activeTips } = substeps.multiRows[substepIndex][0] + const pipChannels = + invariantContext.pipetteEntities[stepArgs.pipette].spec.channels + let channels = pipChannels + if ('nozzles' in stepArgs) { + if (stepArgs.nozzles === ALL) { + channels = 96 + } else if (stepArgs.nozzles === COLUMN) { + channels = 8 + } else { + console.error( + `we don't support other 96-channel configurations yet` + ) + } + } + // just use first multi row + if ( + activeTips && + activeTips.labwareId === labwareId && + channels !== 1 + ) { + const multiTipWellSet = getWellSetForMultichannel( + invariantContext.labwareEntities[labwareId].def, + activeTips.wellName, + channels + ) + if (multiTipWellSet) tipWellSet = multiTipWellSet + } + } else { + // single-channel + const { activeTips } = substeps.rows[substepIndex] + if ( + activeTips && + activeTips.labwareId === labwareId && + activeTips.wellName ) - if (multiTipWellSet) tipWellSet = multiTipWellSet + tipWellSet = [activeTips.wellName] } - } else { - // single-channel - const { activeTips } = substeps.rows[substepIndex] - if ( - activeTips && - activeTips.labwareId === labwareId && - activeTips.wellName - ) - tipWellSet = [activeTips.wellName] } - wells.push(...tipWellSet) } diff --git a/protocol-designer/src/types.ts b/protocol-designer/src/types.ts index 49066d78a2d..0feb5692270 100644 --- a/protocol-designer/src/types.ts +++ b/protocol-designer/src/types.ts @@ -1,4 +1,5 @@ import type { OutputSelector } from 'reselect' +import type { NozzleConfigurationStyle } from '@opentrons/shared-data' import type { RootState as Analytics } from './analytics' import type { RootState as Dismiss } from './dismiss' import type { RootState as FileData } from './file-data' @@ -43,3 +44,5 @@ export type WellVolumes = Record // or special PD-specific 'span7_8_10_11' slot (for thermocycler) // or a module ID. export type DeckSlot = string + +export type NozzleType = NozzleConfigurationStyle | '8-channel' diff --git a/protocol-designer/src/ui/labware/selectors.ts b/protocol-designer/src/ui/labware/selectors.ts index 25b1a222795..3919547b419 100644 --- a/protocol-designer/src/ui/labware/selectors.ts +++ b/protocol-designer/src/ui/labware/selectors.ts @@ -2,15 +2,12 @@ import { createSelector } from 'reselect' import mapValues from 'lodash/mapValues' import reduce from 'lodash/reduce' import { getIsTiprack, getLabwareDisplayName } from '@opentrons/shared-data' -import { - AdditionalEquipmentEntity, - COLUMN_4_SLOTS, -} from '@opentrons/step-generation' +import { AdditionalEquipmentEntity } from '@opentrons/step-generation' import { i18n } from '../../localization' import * as stepFormSelectors from '../../step-forms/selectors' import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { getModuleUnderLabware } from '../modules/utils' -import { getLabwareOffDeck } from './utils' +import { getLabwareOffDeck, getLabwareInColumn4 } from './utils' import type { LabwareEntity } from '@opentrons/step-generation' import type { DropdownOption, Options } from '@opentrons/components' @@ -82,27 +79,6 @@ export const getLabwareOptions: Selector = createSelector( savedStepForms ?? {}, labwareId ) - const isStartingInColumn4 = COLUMN_4_SLOTS.includes( - initialDeckSetup.labware[labwareId]?.slot - ) - - const isInColumn4 = - savedStepForms != null - ? Object.values(savedStepForms) - ?.reverse() - .some( - form => - form.stepType === 'moveLabware' && - form.labware === labwareId && - (COLUMN_4_SLOTS.includes(form.newLocation) || - (isStartingInColumn4 && - !COLUMN_4_SLOTS.includes(form.newLocation))) - ) - : false - - const isAdapterOrAluminumBlock = - isAdapter || - labwareEntity.def.metadata.displayCategory === 'aluminumBlock' const moduleOnDeck = getModuleUnderLabware( initialDeckSetup, @@ -116,12 +92,18 @@ export const getLabwareOptions: Selector = createSelector( ) : null + const isLabwareInColumn4 = getLabwareInColumn4( + initialDeckSetup, + savedStepForms ?? {}, + labwareId + ) + let nickName = nicknamesById[labwareId] if (module != null) { nickName = `${nicknamesById[labwareId]} in ${module}` } else if (isOffDeck) { - nickName = `Off-deck - ${nicknamesById[labwareId]}` - } else if (isInColumn4) { + nickName = `${nicknamesById[labwareId]} off-deck` + } else if (isLabwareInColumn4) { nickName = `${nicknamesById[labwareId]} in staging area slot` } @@ -140,9 +122,9 @@ export const getLabwareOptions: Selector = createSelector( }, ] } else { - // filter out moving trash, aluminum blocks, adapters and labware in + // filter out moving trash, adapters, and labware in // waste chute for moveLabware - return isAdapterOrAluminumBlock || isLabwareInWasteChute + return isAdapter || isLabwareInWasteChute ? acc : [ ...acc, diff --git a/protocol-designer/src/ui/labware/utils.ts b/protocol-designer/src/ui/labware/utils.ts index ea2cc98b8b7..06cb388dd51 100644 --- a/protocol-designer/src/ui/labware/utils.ts +++ b/protocol-designer/src/ui/labware/utils.ts @@ -1,3 +1,4 @@ +import { COLUMN_4_SLOTS } from '@opentrons/step-generation' import type { InitialDeckSetup, SavedStepFormState } from '../../step-forms' export function getLabwareOffDeck( @@ -24,3 +25,33 @@ export function getLabwareOffDeck( return true } else return false } + +export function getLabwareInColumn4( + initialDeckSetup: InitialDeckSetup, + savedStepForms: SavedStepFormState, + labwareId: string +): boolean { + const isStartingInColumn4 = COLUMN_4_SLOTS.includes( + initialDeckSetup.labware[labwareId]?.slot + ) + // latest moveLabware step related to labwareId + const moveLabwareStep = Object.values(savedStepForms) + .filter( + state => + state.stepType === 'moveLabware' && + labwareId != null && + labwareId === state.labware + ) + .reverse()[0] + + if ( + moveLabwareStep?.newLocation != null && + COLUMN_4_SLOTS.includes(moveLabwareStep.newLocation) + ) { + return true + } else if (moveLabwareStep == null && isStartingInColumn4) { + return true + } else { + return false + } +} diff --git a/protocol-designer/src/ui/modules/utils.ts b/protocol-designer/src/ui/modules/utils.ts index 84571c88a42..e3fdd56acf4 100644 --- a/protocol-designer/src/ui/modules/utils.ts +++ b/protocol-designer/src/ui/modules/utils.ts @@ -45,12 +45,21 @@ export function getModuleUnderLabware( .reverse()[0] const newLocation = moveLabwareStep?.newLocation - return values(initialDeckSetup.modules).find( - (moduleOnDeck: ModuleOnDeck) => - (newLocation != null - ? newLocation - : initialDeckSetup.labware[labwareId]?.slot) === moduleOnDeck.id - ) + return values(initialDeckSetup.modules).find((moduleOnDeck: ModuleOnDeck) => { + const labwareSlot = initialDeckSetup.labware[labwareId]?.slot + let location + if (newLocation != null) { + location = newLocation + } else if ( + labwareSlot != null && + initialDeckSetup.labware[labwareSlot] != null + ) { + location = initialDeckSetup.labware[labwareSlot].slot + } else { + location = labwareSlot + } + return location === moduleOnDeck.id + }) } export function getModuleLabwareOptions( initialDeckSetup: InitialDeckSetup, diff --git a/protocol-designer/src/ui/steps/test/selectors.test.ts b/protocol-designer/src/ui/steps/test/selectors.test.ts index 59dd7cceeb8..6e87638e8f9 100644 --- a/protocol-designer/src/ui/steps/test/selectors.test.ts +++ b/protocol-designer/src/ui/steps/test/selectors.test.ts @@ -575,6 +575,10 @@ describe('_getSavedMultiSelectFieldValues', () => { isIndeterminate: false, value: 'some_pipette_id', }, + nozzles: { + isIndeterminate: false, + value: undefined, + }, volume: { isIndeterminate: false, value: '30', @@ -628,6 +632,7 @@ describe('_getSavedMultiSelectFieldValues', () => { // same thing with dispense_touchTip_mmFromBottom blowout_checkbox: false, // same thing here with blowout location + nozzles: null, }, } }) @@ -782,6 +787,9 @@ describe('_getSavedMultiSelectFieldValues', () => { isIndeterminate: false, value: 'some_pipette_id', }, + nozzles: { + isIndeterminate: true, + }, volume: { isIndeterminate: false, value: '30', @@ -834,6 +842,7 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_delay_seconds: { value: '1', isIndeterminate: false }, mix_touchTip_checkbox: { value: false, isIndeterminate: false }, mix_touchTip_mmFromBottom: { value: null, isIndeterminate: false }, + nozzles: { value: undefined, isIndeterminate: false }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, @@ -869,6 +878,7 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_delay_seconds: '3', mix_touchTip_checkbox: true, mix_touchTip_mmFromBottom: '14', + nozzles: null, }, } @@ -901,6 +911,7 @@ describe('_getSavedMultiSelectFieldValues', () => { dispense_delay_seconds: { isIndeterminate: true }, mix_touchTip_checkbox: { isIndeterminate: true }, mix_touchTip_mmFromBottom: { isIndeterminate: true }, + nozzles: { isIndeterminate: true }, dropTip_location: { value: 'fixedTrash', isIndeterminate: false, diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index ea02c5d9685..0bb32a23e2b 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -98,6 +98,7 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< [PCR_ADAPTER_LOADNAME]: [ 'opentrons/nest_96_wellplate_100ul_pcr_full_skirt/2', 'opentrons/opentrons_96_wellplate_200ul_pcr_full_skirt/2', + 'opentrons/biorad_96_wellplate_200ul_pcr/2', ], [UNIVERSAL_FLAT_ADAPTER_LOADNAME]: [ 'opentrons/corning_384_wellplate_112ul_flat/2', @@ -174,6 +175,12 @@ export const getAdapterLabwareIsAMatch = ( 'opentrons_flex_96_filtertiprack_1000ul', ] + const pcrLabwares = [ + 'biorad_96_wellplate_200ul_pcr', + 'nest_96_wellplate_100ul_pcr_full_skirt', + 'opentrons_96_wellplate_200ul_pcr_full_skirt', + ] + const deepWellPair = loadName === DEEP_WELL_ADAPTER_LOADNAME && draggedLabwareLoadname === 'nest_96_wellplate_2ml_deep' @@ -182,8 +189,7 @@ export const getAdapterLabwareIsAMatch = ( draggedLabwareLoadname === 'nest_96_wellplate_200ul_flat' const pcrPair = loadName === PCR_ADAPTER_LOADNAME && - (draggedLabwareLoadname === 'nest_96_wellplate_100ul_pcr_full_skirt' || - draggedLabwareLoadname === 'opentrons_96_wellplate_200ul_pcr_full_skirt') + pcrLabwares.includes(draggedLabwareLoadname) const universalPair = loadName === UNIVERSAL_FLAT_ADAPTER_LOADNAME && (draggedLabwareLoadname === 'corning_384_wellplate_112ul_flat' || diff --git a/protocol-designer/typings/reselect.d.ts b/protocol-designer/typings/reselect.d.ts index b6828c1ba7d..c905b8139c4 100644 --- a/protocol-designer/typings/reselect.d.ts +++ b/protocol-designer/typings/reselect.d.ts @@ -1,6 +1,6 @@ import { OutputSelector, Selector } from 'reselect' declare module 'reselect' { - // declaring type for createSelector with 15 selectors because the reselect types only support up to 12 selectors + // declaring type for createSelector with 14 selectors because the reselect types only support up to 12 selectors export function createSelector< S, R1, @@ -17,7 +17,6 @@ declare module 'reselect' { R12, R13, R14, - R15, T >( selector1: Selector, @@ -34,7 +33,6 @@ declare module 'reselect' { selector12: Selector, selector13: Selector, selector14: Selector, - selector15: Selector, combiner: ( res1: R1, res2: R2, @@ -49,8 +47,7 @@ declare module 'reselect' { res11: R11, res12: R12, res13: R13, - res14: R14, - res15: R15 + res14: R14 ) => T ): OutputSelector< S, @@ -69,8 +66,7 @@ declare module 'reselect' { res11: R11, res12: R12, res13: R13, - res14: R14, - res15: R15 + res14: R14 ) => T > } diff --git a/protocol-library-kludge/src/URLDeck.tsx b/protocol-library-kludge/src/URLDeck.tsx index 2e72457655d..dbfffaf34a7 100644 --- a/protocol-library-kludge/src/URLDeck.tsx +++ b/protocol-library-kludge/src/URLDeck.tsx @@ -3,13 +3,12 @@ import styles from './URLDeck.css' import { RobotWorkSpace, - LegacyLabware, LabwareNameOverlay, LabwareRender, ModuleItem, RobotCoordsForeignDiv, } from '@opentrons/components' -import { getLatestLabwareDef, getLegacyLabwareDef } from './getLabware' +import { getLatestLabwareDef } from './getLabware' import { getDeckDefinitions } from '@opentrons/components/src/hardware-sim/Deck/getDeckDefinitions' import type { ModuleModel, DeckSlotId } from '@opentrons/shared-data' @@ -82,16 +81,9 @@ export class URLDeck extends React.Component<{}> { const labware = labwareBySlot && labwareBySlot[slotId] const labwareDefV2 = labware && getLatestLabwareDef(labware.labwareType) - const labwareDefV1 = - labwareDefV2 || !labware - ? null - : getLegacyLabwareDef(labware.labwareType) let labwareDisplayType: string | null = null if (labwareDefV2) { labwareDisplayType = labwareDefV2.metadata.displayName - } else if (labwareDefV1) { - labwareDisplayType = - labwareDefV1.metadata.displayName || labwareDefV1.metadata.name } else { labwareDisplayType = labware?.labwareType || null } @@ -115,14 +107,7 @@ export class URLDeck extends React.Component<{}> { > {labwareDefV2 ? ( - ) : ( - - )} + ) : null} )} {labware && ( diff --git a/robot-server/robot_server/instruments/router.py b/robot-server/robot_server/instruments/router.py index ebe2af7f6a0..326e1bb3caa 100644 --- a/robot-server/robot_server/instruments/router.py +++ b/robot-server/robot_server/instruments/router.py @@ -1,5 +1,5 @@ """Instruments routes.""" -from typing import Optional, Dict, List, TYPE_CHECKING, cast +from typing import Optional, Dict, List, cast from fastapi import APIRouter, status, Depends @@ -48,8 +48,7 @@ from robot_server.subsystems.models import SubSystem from robot_server.subsystems.router import status_route_for, update_route_for -if TYPE_CHECKING: - from opentrons.hardware_control.ot3api import OT3API +from opentrons.hardware_control import OT3HardwareControlAPI instruments_router = APIRouter() @@ -151,7 +150,7 @@ def _bad_pipette_response(subsystem: SubSystem) -> BadPipette: async def _get_gripper_instrument_data( - hardware: "OT3API", + hardware: OT3HardwareControlAPI, attached_gripper: Optional[GripperDict], ) -> Optional[AttachedItem]: subsys = HWSubSystem.of_mount(OT3Mount.GRIPPER) @@ -167,7 +166,7 @@ async def _get_gripper_instrument_data( async def _get_pipette_instrument_data( - hardware: "OT3API", + hardware: OT3HardwareControlAPI, attached_pipettes: Dict[Mount, PipetteDict], mount: Mount, ) -> Optional[AttachedItem]: @@ -193,7 +192,7 @@ async def _get_pipette_instrument_data( async def _get_instrument_data( - hardware: "OT3API", + hardware: OT3HardwareControlAPI, ) -> List[AttachedItem]: attached_pipettes = hardware.attached_pipettes attached_gripper = hardware.attached_gripper @@ -214,7 +213,7 @@ async def _get_instrument_data( async def _get_attached_instruments_ot3( - hardware: "OT3API", + hardware: OT3HardwareControlAPI, ) -> PydanticResponse[SimpleMultiBody[AttachedItem]]: # OT3 await hardware.cache_instruments() diff --git a/robot-server/tests/instruments/test_router.py b/robot-server/tests/instruments/test_router.py index 092af18b63f..b67f24a14cd 100644 --- a/robot-server/tests/instruments/test_router.py +++ b/robot-server/tests/instruments/test_router.py @@ -26,6 +26,7 @@ GripperModel, ) from opentrons_shared_data.pipette.dev_types import PipetteName, PipetteModel +from opentrons.hardware_control.protocols.types import FlexRobotType, OT2RobotType from robot_server.instruments.instrument_models import ( Gripper, @@ -50,7 +51,9 @@ @pytest.fixture def ot2_hardware_api(decoy: Decoy) -> HardwareControlAPI: """Get a mock hardware control API.""" - return decoy.mock(cls=API) + mock = decoy.mock(cls=API) + decoy.when(mock.get_robot_type()).then_return(OT2RobotType) + return mock def get_sample_pipette_dict( @@ -77,7 +80,9 @@ def ot3_hardware_api(decoy: Decoy) -> HardwareControlAPI: try: from opentrons.hardware_control.ot3api import OT3API - return decoy.mock(cls=OT3API) + mock = decoy.mock(cls=OT3API) + decoy.when(mock.get_robot_type()).then_return(FlexRobotType) + return mock except ImportError: return None # type: ignore[return-value] diff --git a/shared-data/.npmignore b/shared-data/.npmignore new file mode 100644 index 00000000000..f70a5b8e891 --- /dev/null +++ b/shared-data/.npmignore @@ -0,0 +1,4 @@ +dist +js +python +*.tgz diff --git a/shared-data/Makefile b/shared-data/Makefile index cbcf76d582b..7a648da9db6 100644 --- a/shared-data/Makefile +++ b/shared-data/Makefile @@ -3,9 +3,6 @@ # using bash instead of /bin/bash in SHELL prevents macOS optimizing away our PATH update SHELL := bash -# TODO(mc, 2018-10-25): use dist to match other projects -BUILD_DIR := build - # These variables can be overriden when make is invoked to customize the # behavior of jest tests ?= @@ -24,21 +21,16 @@ setup: setup-py setup-js dist: dist-js dist-py .PHONY: clean -clean: clean-js clean-py +clean: clean-py # JavaScript targets -.PHONY: setup-js -setup-js: dist-js +.PHONY: lib-js +lib-js: export NODE_ENV := production +lib-js: + yarn webpack -.PHONY: dist-js -dist-js: - @yarn shx mkdir -p $(BUILD_DIR) - node js/scripts/build.js $(BUILD_DIR) -.PHONY: clean-js -clean-js: - yarn shx rm -rf $(BUILD_DIR) # Python targets diff --git a/shared-data/command/types/setup.ts b/shared-data/command/types/setup.ts index fa6e3e564b9..eb409f1dbc4 100644 --- a/shared-data/command/types/setup.ts +++ b/shared-data/command/types/setup.ts @@ -175,11 +175,11 @@ interface LoadFixtureParams { fixtureId?: string } -const COLUMN = 'COLUMN' +export const COLUMN = 'COLUMN' const SINGLE = 'SINGLE' const ROW = 'ROW' const QUADRANT = 'QUADRANT' -const ALL = 'ALL' +export const ALL = 'ALL' export type NozzleConfigurationStyle = | typeof COLUMN @@ -189,7 +189,7 @@ export type NozzleConfigurationStyle = | typeof ALL interface NozzleConfigurationParams { - primaryNozzle: string + primaryNozzle?: string style: NozzleConfigurationStyle } diff --git a/shared-data/deck/types/schemaV4.ts b/shared-data/deck/types/schemaV4.ts index ed7f726be3c..398d9a006eb 100644 --- a/shared-data/deck/types/schemaV4.ts +++ b/shared-data/deck/types/schemaV4.ts @@ -40,6 +40,7 @@ export type OT2AddressableAreaName = | '9' | '10' | '11' + | '12' | 'fixedTrash' export type AddressableAreaName = diff --git a/shared-data/errors/definitions/1/errors.json b/shared-data/errors/definitions/1/errors.json index 1f31ac9af6e..7ed86dcff30 100644 --- a/shared-data/errors/definitions/1/errors.json +++ b/shared-data/errors/definitions/1/errors.json @@ -114,6 +114,10 @@ "detail": "Execution Cancelled", "category": "roboticsControlError" }, + "2015": { + "detail": "Gripper Pickup Failed", + "category": "roboticsControlError" + }, "3000": { "detail": "A robotics interaction error occurred.", "category": "roboticsInteractionError" diff --git a/shared-data/gripper/definitions/1/gripperV1.1.json b/shared-data/gripper/definitions/1/gripperV1.1.json index b5587c9e78a..ccc4b1d918d 100644 --- a/shared-data/gripper/definitions/1/gripperV1.1.json +++ b/shared-data/gripper/definitions/1/gripperV1.1.json @@ -23,6 +23,7 @@ "jawWidth": { "min": 60.0, "max": 92.0 - } + }, + "maxAllowedGripError": 6.0 } } diff --git a/shared-data/gripper/definitions/1/gripperV1.2.json b/shared-data/gripper/definitions/1/gripperV1.2.json index 5cb3c045bc6..d4b33ecd087 100644 --- a/shared-data/gripper/definitions/1/gripperV1.2.json +++ b/shared-data/gripper/definitions/1/gripperV1.2.json @@ -23,6 +23,7 @@ "jawWidth": { "min": 60.0, "max": 92.0 - } + }, + "maxAllowedGripError": 6.0 } } diff --git a/shared-data/gripper/definitions/1/gripperV1.json b/shared-data/gripper/definitions/1/gripperV1.json index 093ed42f745..a066a9de6d7 100644 --- a/shared-data/gripper/definitions/1/gripperV1.json +++ b/shared-data/gripper/definitions/1/gripperV1.json @@ -22,6 +22,7 @@ "jawWidth": { "min": 58.0, "max": 90.0 - } + }, + "maxAllowedGripError": 6.0 } } diff --git a/shared-data/gripper/schemas/1.json b/shared-data/gripper/schemas/1.json index 1ecc38087e9..4ec4d6ee0f8 100644 --- a/shared-data/gripper/schemas/1.json +++ b/shared-data/gripper/schemas/1.json @@ -109,6 +109,11 @@ "additionalProperties": { "type": "number" } + }, + "maxAllowedGripError": { + "title": "MaxAllowedGripError", + "minimum": 0.0, + "type": "number" } }, "required": [ diff --git a/shared-data/js/getLabware.ts b/shared-data/js/getLabware.ts index 2121123df5d..74678b91885 100644 --- a/shared-data/js/getLabware.ts +++ b/shared-data/js/getLabware.ts @@ -1,7 +1,4 @@ -import assert from 'assert' import mapValues from 'lodash/mapValues' -// TODO: Ian 2019-06-04 remove the shared-data build process for labware v1 -import definitions from '../build/labware.json' import { FIXED_TRASH_RENDER_HEIGHT, @@ -15,11 +12,6 @@ import type { WellDefinition, } from './types' -assert( - definitions && Object.keys(definitions).length > 0, - 'Expected v1 labware defs. Something went wrong with shared-data/build/labware.json' -) - // do not list in any "available labware" UI. // TODO(mc, 2019-12-3): how should this correspond to RETIRED_LABWARE? // see shared-data/js/helpers/index.js @@ -62,15 +54,6 @@ export const PD_DO_NOT_LIST = [ 'opentrons_96_aluminumblock_nest_wellplate_100ul', ] -export function getLabwareV1Def( - labwareName: string -): LabwareDefinition1 | null | undefined { - const labware: LabwareDefinition1 | null | undefined = - // @ts-expect-error(mc, 2021-04-27): make lookup more strict or remove v1 defs entirely - definitions[labwareName] - return labware -} - export function getIsLabwareV1Tiprack(def: LabwareDefinition1): boolean { return Boolean(def?.metadata?.isTiprack) } diff --git a/shared-data/js/scripts/build.js b/shared-data/js/scripts/build.js deleted file mode 100644 index be348d96816..00000000000 --- a/shared-data/js/scripts/build.js +++ /dev/null @@ -1,32 +0,0 @@ -// This build script is run by `make setup` - -// Merge all v1 labware files into a single JSON file, build/labware.json, -// with each filename as a key in the final JSON file. -const fs = require('fs') -const path = require('path') -const glob = require('glob') - -const buildDir = process.argv[2] - -if (!buildDir) { - throw new Error( - 'build.js requires a build directory given as an argument. eg `node js/scripts/build.js path/to/build/`' - ) -} - -const output = {} - -const files = glob.sync( - path.join(__dirname, '../../labware/definitions/1/*.json') -) - -files.forEach(filename => { - const contents = require(filename) - const labwareName = path.parse(filename).name - - output[labwareName] = contents -}) - -const jsonOutput = JSON.stringify(output) - -fs.writeFileSync(path.join(buildDir, 'labware.json'), jsonOutput) diff --git a/shared-data/js/types.ts b/shared-data/js/types.ts index 114b696cb4f..dd898f6f91a 100644 --- a/shared-data/js/types.ts +++ b/shared-data/js/types.ts @@ -538,6 +538,7 @@ export interface GripperDefinition { pinOneOffsetFromBase: [number, number, number] pinTwoOffsetFromBase: [number, number, number] jawWidth: { min: number; max: number } + maxAllowedGripError: number } } diff --git a/shared-data/labware/fixtures/1/fixture_tiprack.json b/shared-data/labware/fixtures/1/fixture_tiprack.json new file mode 100644 index 00000000000..ee0cfac33d1 --- /dev/null +++ b/shared-data/labware/fixtures/1/fixture_tiprack.json @@ -0,0 +1,986 @@ +{ + "metadata": { + "name": "fixture_tiprack", + "format": "96-standard", + "tipVolume": 10, + "displayCategory": "tiprack", + "isTiprack": true, + "isValidSource": false + }, + "ordering": [ + ["A1", "B1", "C1", "D1", "E1", "F1", "G1", "H1"], + ["A2", "B2", "C2", "D2", "E2", "F2", "G2", "H2"], + ["A3", "B3", "C3", "D3", "E3", "F3", "G3", "H3"], + ["A4", "B4", "C4", "D4", "E4", "F4", "G4", "H4"], + ["A5", "B5", "C5", "D5", "E5", "F5", "G5", "H5"], + ["A6", "B6", "C6", "D6", "E6", "F6", "G6", "H6"], + ["A7", "B7", "C7", "D7", "E7", "F7", "G7", "H7"], + ["A8", "B8", "C8", "D8", "E8", "F8", "G8", "H8"], + ["A9", "B9", "C9", "D9", "E9", "F9", "G9", "H9"], + ["A10", "B10", "C10", "D10", "E10", "F10", "G10", "H10"], + ["A11", "B11", "C11", "D11", "E11", "F11", "G11", "H11"], + ["A12", "B12", "C12", "D12", "E12", "F12", "G12", "H12"] + ], + "wells": { + "A1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 72.49, + "z": 0 + }, + "A10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 72.49, + "z": 0 + }, + "A11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 72.49, + "z": 0 + }, + "A12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 72.49, + "z": 0 + }, + "A2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 72.49, + "z": 0 + }, + "A3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 72.49, + "z": 0 + }, + "A4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 72.49, + "z": 0 + }, + "A5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 72.49, + "z": 0 + }, + "A6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 72.49, + "z": 0 + }, + "A7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 72.49, + "z": 0 + }, + "A8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 72.49, + "z": 0 + }, + "A9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 72.49, + "z": 0 + }, + "B1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 63.49, + "z": 0 + }, + "B10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 63.49, + "z": 0 + }, + "B11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 63.49, + "z": 0 + }, + "B12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 63.49, + "z": 0 + }, + "B2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 63.49, + "z": 0 + }, + "B3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 63.49, + "z": 0 + }, + "B4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 63.49, + "z": 0 + }, + "B5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 63.49, + "z": 0 + }, + "B6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 63.49, + "z": 0 + }, + "B7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 63.49, + "z": 0 + }, + "B8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 63.49, + "z": 0 + }, + "B9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 63.49, + "z": 0 + }, + "C1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 54.49, + "z": 0 + }, + "C10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 54.49, + "z": 0 + }, + "C11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 54.49, + "z": 0 + }, + "C12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 54.49, + "z": 0 + }, + "C2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 54.49, + "z": 0 + }, + "C3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 54.49, + "z": 0 + }, + "C4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 54.49, + "z": 0 + }, + "C5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 54.49, + "z": 0 + }, + "C6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 54.49, + "z": 0 + }, + "C7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 54.49, + "z": 0 + }, + "C8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 54.49, + "z": 0 + }, + "C9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 54.49, + "z": 0 + }, + "D1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 45.49, + "z": 0 + }, + "D10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 45.49, + "z": 0 + }, + "D11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 45.49, + "z": 0 + }, + "D12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 45.49, + "z": 0 + }, + "D2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 45.49, + "z": 0 + }, + "D3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 45.49, + "z": 0 + }, + "D4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 45.49, + "z": 0 + }, + "D5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 45.49, + "z": 0 + }, + "D6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 45.49, + "z": 0 + }, + "D7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 45.49, + "z": 0 + }, + "D8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 45.49, + "z": 0 + }, + "D9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 45.49, + "z": 0 + }, + "E1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 36.49, + "z": 0 + }, + "E10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 36.49, + "z": 0 + }, + "E11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 36.49, + "z": 0 + }, + "E12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 36.49, + "z": 0 + }, + "E2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 36.49, + "z": 0 + }, + "E3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 36.49, + "z": 0 + }, + "E4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 36.49, + "z": 0 + }, + "E5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 36.49, + "z": 0 + }, + "E6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 36.49, + "z": 0 + }, + "E7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 36.49, + "z": 0 + }, + "E8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 36.49, + "z": 0 + }, + "E9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 36.49, + "z": 0 + }, + "F1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 27.49, + "z": 0 + }, + "F10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 27.49, + "z": 0 + }, + "F11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 27.49, + "z": 0 + }, + "F12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 27.49, + "z": 0 + }, + "F2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 27.49, + "z": 0 + }, + "F3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 27.49, + "z": 0 + }, + "F4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 27.49, + "z": 0 + }, + "F5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 27.49, + "z": 0 + }, + "F6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 27.49, + "z": 0 + }, + "F7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 27.49, + "z": 0 + }, + "F8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 27.49, + "z": 0 + }, + "F9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 27.49, + "z": 0 + }, + "G1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 18.49, + "z": 0 + }, + "G10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 18.49, + "z": 0 + }, + "G11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 18.49, + "z": 0 + }, + "G12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 18.49, + "z": 0 + }, + "G2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 18.49, + "z": 0 + }, + "G3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 18.49, + "z": 0 + }, + "G4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 18.49, + "z": 0 + }, + "G5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 18.49, + "z": 0 + }, + "G6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 18.49, + "z": 0 + }, + "G7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 18.49, + "z": 0 + }, + "G8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 18.49, + "z": 0 + }, + "G9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 18.49, + "z": 0 + }, + "H1": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 12.59, + "y": 9.49, + "z": 0 + }, + "H10": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 93.59, + "y": 9.49, + "z": 0 + }, + "H11": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 102.59, + "y": 9.49, + "z": 0 + }, + "H12": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 111.59, + "y": 9.49, + "z": 0 + }, + "H2": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 21.59, + "y": 9.49, + "z": 0 + }, + "H3": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 30.59, + "y": 9.49, + "z": 0 + }, + "H4": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 39.59, + "y": 9.49, + "z": 0 + }, + "H5": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 48.59, + "y": 9.49, + "z": 0 + }, + "H6": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 57.59, + "y": 9.49, + "z": 0 + }, + "H7": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 66.59, + "y": 9.49, + "z": 0 + }, + "H8": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 75.59, + "y": 9.49, + "z": 0 + }, + "H9": { + "depth": 39.2, + "diameter": 3.5, + "height": 64.89, + "length": 3.5, + "width": 3.5, + "x": 84.59, + "y": 9.49, + "z": 0 + } + } +} diff --git a/shared-data/package.json b/shared-data/package.json index c43a05ad90c..7f61401f4ff 100755 --- a/shared-data/package.json +++ b/shared-data/package.json @@ -1,7 +1,7 @@ { "name": "@opentrons/shared-data", "version": "0.0.0-dev", - "description": "Default labware definitions for Opentrons robots", + "description": "Shared system definitions and utilities for Opentrons robots and software", "repository": { "type": "git", "url": "https://github.com/Opentrons/opentrons.git" @@ -10,7 +10,8 @@ "license": "Apache-2.0", "source": "js/index.ts", "types": "lib/js/index.d.ts", - "flow:main": "flow-types/js/index.js.flow", + "main": "lib/opentrons-shared-data.js", + "module": "js/index.ts", "dependencies": { "ajv": "^6.12.3", "html-react-parser": "^1.2.8", diff --git a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json index ad39d6a2a3d..92d9b6da4b4 100644 --- a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json +++ b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json @@ -13,6 +13,11 @@ "value": 10, "min": 0.001, "max": 50 + }, + "defaultBlowOutFlowRate": { + "value": 1000, + "min": 5, + "max": 1000 } }, "p10_multi": { @@ -29,6 +34,11 @@ "min": 0.001, "max": 50 }, + "defaultBlowOutFlowRate": { + "value": 1000, + "min": 5, + "max": 1000 + }, "channels": 8 }, "p50_single": { @@ -43,6 +53,11 @@ "min": 0.001, "max": 100 }, + "defaultBlowOutFlowRate": { + "value": 3.78, + "min": 0.08, + "max": 24 + }, "channels": 1, "minVolume": 5, "maxVolume": 50 @@ -59,6 +74,11 @@ "min": 0.001, "max": 100 }, + "defaultBlowOutFlowRate": { + "value": 1000, + "min": 5, + "max": 1000 + }, "channels": 8, "minVolume": 5, "maxVolume": 50 @@ -75,6 +95,11 @@ "min": 0.001, "max": 600 }, + "defaultBlowOutFlowRate": { + "value": 1000, + "min": 5, + "max": 1000 + }, "channels": 1, "minVolume": 30, "maxVolume": 300 @@ -91,6 +116,11 @@ "min": 0.001, "max": 600 }, + "defaultBlowOutFlowRate": { + "value": 94, + "min": 1, + "max": 275 + }, "channels": 8, "minVolume": 30, "maxVolume": 300 @@ -107,6 +137,11 @@ "min": 50, "max": 2000 }, + "defaultBlowOutFlowRate": { + "value": 1000, + "min": 5, + "max": 1000 + }, "channels": 1, "minVolume": 100, "maxVolume": 1000 @@ -123,6 +158,11 @@ "min": 3, "max": 812 }, + "defaultBlowOutFlowRate": { + "value": 80, + "min": 3, + "max": 812 + }, "channels": 96, "minVolume": 5, "maxVolume": 1000 diff --git a/shared-data/python/opentrons_shared_data/errors/codes.py b/shared-data/python/opentrons_shared_data/errors/codes.py index ab79e03b06e..5788b2fca93 100644 --- a/shared-data/python/opentrons_shared_data/errors/codes.py +++ b/shared-data/python/opentrons_shared_data/errors/codes.py @@ -58,6 +58,7 @@ class ErrorCodes(Enum): UNMATCHED_TIP_PRESENCE_STATES = _code_from_dict_entry("2012") POSITION_UNKNOWN = _code_from_dict_entry("2013") EXECUTION_CANCELLED = _code_from_dict_entry("2014") + FAILED_GRIPPER_PICKUP_ERROR = _code_from_dict_entry("2015") ROBOTICS_INTERACTION_ERROR = _code_from_dict_entry("3000") LABWARE_DROPPED = _code_from_dict_entry("3001") LABWARE_NOT_PICKED_UP = _code_from_dict_entry("3002") diff --git a/shared-data/python/opentrons_shared_data/errors/exceptions.py b/shared-data/python/opentrons_shared_data/errors/exceptions.py index 9483b404965..5bf2fc0e67c 100644 --- a/shared-data/python/opentrons_shared_data/errors/exceptions.py +++ b/shared-data/python/opentrons_shared_data/errors/exceptions.py @@ -447,6 +447,23 @@ def __init__( ) +class FailedGripperPickupError(RoboticsControlError): + """Raised when the gripper expects to be holding an object, but the jaw is closed farther than expected.""" + + def __init__( + self, + details: Optional[Dict[str, Any]] = None, + wrapping: Optional[Sequence[EnumeratedError]] = None, + ) -> None: + """Build a FailedGripperPickupError.""" + super().__init__( + ErrorCodes.FAILED_GRIPPER_PICKUP_ERROR, + "Expected to grip labware, but none found.", + details, + wrapping, + ) + + class EdgeNotFoundError(RoboticsControlError): """An error indicating that a calibration square edge was not able to be found.""" diff --git a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py index 70ca41f54ce..4c4c30c623b 100644 --- a/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py +++ b/shared-data/python/opentrons_shared_data/gripper/gripper_definition.py @@ -69,6 +69,7 @@ class Geometry(GripperBaseModel): pin_one_offset_from_base: Offset pin_two_offset_from_base: Offset jaw_width: Dict[str, float] + max_allowed_grip_error: _StrictNonNegativeFloat PolynomialTerm = Tuple[_StrictNonNegativeInt, float] diff --git a/shared-data/webpack.config.js b/shared-data/webpack.config.js new file mode 100644 index 00000000000..7f9dc870132 --- /dev/null +++ b/shared-data/webpack.config.js @@ -0,0 +1,20 @@ +'use strict' + +const path = require('path') +const webpackMerge = require('webpack-merge') +const { baseConfig } = require('@opentrons/webpack-config') + +const ENTRY_INDEX = path.join(__dirname, 'js/index.ts') +const OUTPUT_PATH = path.join(__dirname, 'lib') + +module.exports = async () => + webpackMerge(baseConfig, { + entry: { index: ENTRY_INDEX }, + output: { + ...baseConfig.output, + path: OUTPUT_PATH, + filename: 'opentrons-shared-data.js', + library: '@opentrons/shared-data', + libraryTarget: 'umd', + }, + }) diff --git a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap index da54c69f97f..e3bcf48ce6e 100644 --- a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap +++ b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap @@ -9099,6 +9099,11 @@ Object { "min": 3, "value": 7.85, }, + "defaultBlowOutFlowRate": Object { + "max": 812, + "min": 3, + "value": 80, + }, "defaultDispenseFlowRate": Object { "max": 812, "min": 3, @@ -10254,6 +10259,11 @@ Object { "min": 0.001, "value": 5, }, + "defaultBlowOutFlowRate": Object { + "max": 1000, + "min": 5, + "value": 1000, + }, "defaultDispenseFlowRate": Object { "max": 50, "min": 0.001, @@ -11403,6 +11413,11 @@ Object { "min": 0.001, "value": 5, }, + "defaultBlowOutFlowRate": Object { + "max": 1000, + "min": 5, + "value": 1000, + }, "defaultDispenseFlowRate": Object { "max": 50, "min": 0.001, @@ -12552,6 +12567,11 @@ Object { "min": 0.001, "value": 150, }, + "defaultBlowOutFlowRate": Object { + "max": 275, + "min": 1, + "value": 94, + }, "defaultDispenseFlowRate": Object { "max": 600, "min": 0.001, @@ -13695,6 +13715,11 @@ Object { "min": 0.001, "value": 150, }, + "defaultBlowOutFlowRate": Object { + "max": 1000, + "min": 5, + "value": 1000, + }, "defaultDispenseFlowRate": Object { "max": 600, "min": 0.001, diff --git a/step-generation/src/__tests__/configureNozzleLayout.test.ts b/step-generation/src/__tests__/configureNozzleLayout.test.ts new file mode 100644 index 00000000000..9d2609bc61a --- /dev/null +++ b/step-generation/src/__tests__/configureNozzleLayout.test.ts @@ -0,0 +1,50 @@ +import { ALL, COLUMN } from '@opentrons/shared-data' +import { getSuccessResult } from '../fixtures' +import { configureNozzleLayout } from '../commandCreators/atomic/configureNozzleLayout' + +const getRobotInitialState = (): any => { + return {} +} + +const invariantContext: any = {} +const robotInitialState = getRobotInitialState() +const mockPipette = 'mockPipette' + +describe('configureNozzleLayout', () => { + it('should call configureNozzleLayout with correct params for full tip', () => { + const result = configureNozzleLayout( + { nozzles: ALL, pipetteId: mockPipette }, + invariantContext, + robotInitialState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + { + commandType: 'configureNozzleLayout', + key: expect.any(String), + params: { + pipetteId: mockPipette, + configurationParams: { style: ALL }, + }, + }, + ]) + }) + it('should call configureNozzleLayout with correct params for column tip', () => { + const result = configureNozzleLayout( + { nozzles: COLUMN, pipetteId: mockPipette }, + invariantContext, + robotInitialState + ) + const res = getSuccessResult(result) + expect(res.commands).toEqual([ + { + commandType: 'configureNozzleLayout', + key: expect.any(String), + params: { + pipetteId: mockPipette, + configurationParams: { primaryNozzle: 'A12', style: COLUMN }, + }, + }, + ]) + }) +}) diff --git a/step-generation/src/__tests__/moveLabware.test.ts b/step-generation/src/__tests__/moveLabware.test.ts index 12ecf2e46a8..85c95645074 100644 --- a/step-generation/src/__tests__/moveLabware.test.ts +++ b/step-generation/src/__tests__/moveLabware.test.ts @@ -1,5 +1,6 @@ import { HEATERSHAKER_MODULE_TYPE, + LabwareDefinition2, WASTE_CHUTE_CUTOUT, } from '@opentrons/shared-data' import { @@ -126,6 +127,41 @@ describe('moveLabware', () => { type: 'LABWARE_OFF_DECK', }) }) + it('should return an error for trying to move an aluminum block with a gripper', () => { + const aluminumBlockDef = ({ + metadata: { displayCategory: 'aluminumBlock' }, + } as any) as LabwareDefinition2 + + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + mockGripperId: { + name: 'gripper', + id: mockGripperId, + }, + }, + labwareEntities: { + [SOURCE_LABWARE]: { + id: 'labwareid', + labwareDefURI: 'mockDefUri', + def: aluminumBlockDef, + }, + }, + } + + const params = { + commandCreatorFnName: 'moveLabware', + labware: SOURCE_LABWARE, + useGripper: true, + newLocation: { slotName: 'A1' }, + } as MoveLabwareArgs + + const result = moveLabware(params, invariantContext, robotState) + expect(getErrorResult(result).errors).toHaveLength(1) + expect(getErrorResult(result).errors[0]).toMatchObject({ + type: 'CANNOT_MOVE_WITH_GRIPPER', + }) + }) it('should return an error when trying to move labware to the thermocycler when lid is closed', () => { const temperatureModuleId = 'temperatureModuleId' const thermocyclerId = 'thermocyclerId' diff --git a/step-generation/src/__tests__/ninetySixChannelCollision.test.ts b/step-generation/src/__tests__/ninetySixChannelCollision.test.ts new file mode 100644 index 00000000000..18dac0c10b0 --- /dev/null +++ b/step-generation/src/__tests__/ninetySixChannelCollision.test.ts @@ -0,0 +1,139 @@ +import { getIsTallLabwareWestOf96Channel } from '../utils/ninetySixChannelCollision' +import type { LabwareDefinition2 } from '@opentrons/shared-data' +import type { RobotState, InvariantContext } from '../types' + +let invariantContext: InvariantContext +let robotState: RobotState + +const mockSourceId = 'sourceId' +const mockWestId = 'westId' +const mockPipetteId = 'pipetteId' +const mockSourceDef: LabwareDefinition2 = { + dimensions: { zDimension: 100 }, +} as any +const mockWestDef: LabwareDefinition2 = { + dimensions: { zDimension: 90 }, +} as any +const mockWestDefTall: LabwareDefinition2 = { + dimensions: { zDimension: 101 }, +} as any +const mockTiprackDefinition: LabwareDefinition2 = { + parameters: { tipLength: 10 }, +} as any +describe('getIsTallLabwareWestOf96Channel ', () => { + beforeEach(() => { + invariantContext = { + labwareEntities: { + [mockSourceId]: { + id: mockSourceId, + labwareDefURI: 'mockDefUri', + def: mockSourceDef, + }, + }, + additionalEquipmentEntities: {}, + moduleEntities: {}, + config: {} as any, + pipetteEntities: { + [mockPipetteId]: { + name: 'p1000_96', + id: mockPipetteId, + tiprackDefURI: 'mockUri', + tiprackLabwareDef: mockTiprackDefinition, + spec: {} as any, + }, + }, + } + robotState = { + labware: { [mockSourceId]: { slot: 'A1' } }, + pipettes: {}, + modules: {}, + tipState: { pipettes: { [mockPipetteId]: false } } as any, + liquidState: {} as any, + } + }) + it('should return false when the slot is in column is 1', () => { + expect( + getIsTallLabwareWestOf96Channel( + robotState, + invariantContext, + mockSourceId, + mockPipetteId + ) + ).toBe(false) + }) + it('should return false when source id is a waste chute', () => { + invariantContext = { + ...invariantContext, + additionalEquipmentEntities: { + [mockSourceId]: { + id: mockSourceId, + name: 'wasteChute', + location: 'D3', + }, + }, + } + expect( + getIsTallLabwareWestOf96Channel( + robotState, + invariantContext, + mockSourceId, + mockPipetteId + ) + ).toBe(false) + }) + it('should return false when there is no labware west of source labware', () => { + robotState.labware = { [mockSourceId]: { slot: 'A2' } } + expect( + getIsTallLabwareWestOf96Channel( + robotState, + invariantContext, + mockSourceId, + mockPipetteId + ) + ).toBe(false) + }) + it('should return false when the west labware height is not tall enough', () => { + invariantContext.labwareEntities = { + ...invariantContext.labwareEntities, + [mockWestId]: { + id: mockWestId, + labwareDefURI: 'mockDefUri', + def: mockWestDef, + }, + } + robotState.labware = { + [mockSourceId]: { slot: 'A2' }, + [mockWestId]: { slot: 'A1' }, + } + expect( + getIsTallLabwareWestOf96Channel( + robotState, + invariantContext, + mockSourceId, + mockPipetteId + ) + ).toBe(false) + }) + it('should return true when the west labware height is tall enough', () => { + invariantContext.labwareEntities = { + ...invariantContext.labwareEntities, + [mockWestId]: { + id: mockWestId, + labwareDefURI: 'mockDefUri', + def: mockWestDefTall, + }, + } + robotState.labware = { + [mockSourceId]: { slot: 'A2' }, + [mockWestId]: { slot: 'A1' }, + } + expect( + getIsTallLabwareWestOf96Channel( + robotState, + invariantContext, + mockSourceId, + mockPipetteId + ) + ).toBe(true) + }) +}) diff --git a/step-generation/src/__tests__/replaceTip.test.ts b/step-generation/src/__tests__/replaceTip.test.ts index 4bd3989ed5e..8a3e7b886bc 100644 --- a/step-generation/src/__tests__/replaceTip.test.ts +++ b/step-generation/src/__tests__/replaceTip.test.ts @@ -1,4 +1,5 @@ import merge from 'lodash/merge' +import { COLUMN } from '@opentrons/shared-data' import { getInitialRobotStateStandard, makeContext, @@ -251,9 +252,10 @@ describe('replaceTip', () => { }) }) describe('replaceTip: 96-channel', () => { - it('96-channel, dropping tips in waste chute', () => { + it('96-channel, dropping 1 column of tips in waste chute', () => { invariantContext = { ...invariantContext, + additionalEquipmentEntities: { wasteChuteId: { name: 'wasteChute', @@ -262,7 +264,9 @@ describe('replaceTip', () => { }, }, } - const initialTestRobotState = merge({}, initialRobotState, { + initialRobotState = { + ...initialRobotState, + pipettes: { p100096Id: { mount: 'left', nozzles: COLUMN } }, tipState: { tipracks: { [tiprack4Id]: getTiprackTipstate(false), @@ -272,14 +276,16 @@ describe('replaceTip', () => { p100096Id: true, }, }, - }) + } + const result = replaceTip( { pipette: p100096Id, dropTipLocation: 'wasteChuteId', + nozzles: COLUMN, }, invariantContext, - initialTestRobotState + initialRobotState ) const res = getSuccessResult(result) expect(res.commands).toEqual([ diff --git a/step-generation/src/__tests__/robotStateSelectors.test.ts b/step-generation/src/__tests__/robotStateSelectors.test.ts index 40e0d10a5e2..f5a2c3449e5 100644 --- a/step-generation/src/__tests__/robotStateSelectors.test.ts +++ b/step-generation/src/__tests__/robotStateSelectors.test.ts @@ -151,8 +151,8 @@ describe('getNextTiprack - single-channel', () => { const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('B1') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('B1') }) it('single tiprack, empty, should return null', () => { @@ -164,7 +164,7 @@ describe('getNextTiprack - single-channel', () => { }) const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) - expect(result).toEqual(null) + expect(result.nextTiprack).toEqual(null) }) it('multiple tipracks, all full, should return the filled tiprack in the lowest slot', () => { @@ -179,8 +179,8 @@ describe('getNextTiprack - single-channel', () => { }) const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('A1') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('A1') }) it('multiple tipracks, some partially full, should return the filled tiprack in the lowest slot', () => { @@ -198,8 +198,8 @@ describe('getNextTiprack - single-channel', () => { robotState.tipState.tipracks.tiprack2Id.A1 = false const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('B1') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('B1') }) it('multiple tipracks, all empty, should return null', () => { @@ -214,7 +214,7 @@ describe('getNextTiprack - single-channel', () => { }) const result = getNextTiprack(DEFAULT_PIPETTE, invariantContext, robotState) - expect(result).toBe(null) + expect(result.nextTiprack).toBe(null) }) }) @@ -231,8 +231,8 @@ describe('getNextTiprack - 8-channel', () => { const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('A1') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('A1') }) it('single tiprack, partially full', () => { @@ -252,8 +252,8 @@ describe('getNextTiprack - 8-channel', () => { } const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('A3') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('A3') }) it('single tiprack, empty, should return null', () => { @@ -267,7 +267,7 @@ describe('getNextTiprack - 8-channel', () => { }) const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result).toEqual(null) + expect(result.nextTiprack).toEqual(null) }) it('single tiprack, a well missing from each column, should return null', () => { @@ -297,7 +297,7 @@ describe('getNextTiprack - 8-channel', () => { const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result).toEqual(null) + expect(result.nextTiprack).toEqual(null) }) it('multiple tipracks, all full, should return the filled tiprack in the lowest slot', () => { @@ -313,8 +313,8 @@ describe('getNextTiprack - 8-channel', () => { }) const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack1Id') - expect(result && result.well).toEqual('A1') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack1Id') + expect(result && result.nextTiprack?.well).toEqual('A1') }) it('multiple tipracks, some partially full, should return the filled tiprack in the lowest slot', () => { @@ -368,8 +368,8 @@ describe('getNextTiprack - 8-channel', () => { const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result && result.tiprackId).toEqual('tiprack3Id') - expect(result && result.well).toEqual('A2') + expect(result && result.nextTiprack?.tiprackId).toEqual('tiprack3Id') + expect(result && result.nextTiprack?.well).toEqual('A2') }) it('multiple tipracks, all empty, should return null', () => { @@ -388,7 +388,7 @@ describe('getNextTiprack - 8-channel', () => { }, }) const result = getNextTiprack('p300MultiId', invariantContext, robotState) - expect(result).toEqual(null) + expect(result.nextTiprack).toEqual(null) }) }) diff --git a/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts b/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts new file mode 100644 index 00000000000..19a7e35db0a --- /dev/null +++ b/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts @@ -0,0 +1,33 @@ +import { COLUMN, NozzleConfigurationStyle } from '@opentrons/shared-data' +import { uuid } from '../../utils' +import type { CommandCreator } from '../../types' + +interface configureNozzleLayoutArgs { + pipetteId: string + nozzles: NozzleConfigurationStyle +} + +export const configureNozzleLayout: CommandCreator = ( + args, + invariantContext, + prevRobotState +) => { + const { pipetteId, nozzles } = args + + const commands = [ + { + commandType: 'configureNozzleLayout' as const, + key: uuid(), + params: { + pipetteId, + configurationParams: { + primaryNozzle: nozzles === COLUMN ? 'A12' : undefined, + style: nozzles, + }, + }, + }, + ] + return { + commands, + } +} diff --git a/step-generation/src/commandCreators/atomic/index.ts b/step-generation/src/commandCreators/atomic/index.ts index 28e2274d10a..5e1564a00d7 100644 --- a/step-generation/src/commandCreators/atomic/index.ts +++ b/step-generation/src/commandCreators/atomic/index.ts @@ -2,6 +2,8 @@ import { aspirate } from './aspirate' import { aspirateInPlace } from './aspirateInPlace' import { blowout } from './blowout' import { blowOutInPlace } from './blowOutInPlace' +import { configureForVolume } from './configureForVolume' +import { configureNozzleLayout } from './configureNozzleLayout' import { deactivateTemperature } from './deactivateTemperature' import { delay } from './delay' import { disengageMagnet } from './disengageMagnet' @@ -23,6 +25,8 @@ export { aspirateInPlace, blowout, blowOutInPlace, + configureForVolume, + configureNozzleLayout, deactivateTemperature, delay, disengageMagnet, diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 46962e1bb76..97068aee0fe 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -26,7 +26,7 @@ export const moveLabware: CommandCreator = ( prevRobotState ) => { const { labware, useGripper, newLocation } = args - const { additionalEquipmentEntities } = invariantContext + const { additionalEquipmentEntities, labwareEntities } = invariantContext const hasWasteChute = getHasWasteChute(additionalEquipmentEntities) const tiprackHasTip = prevRobotState.tipState != null @@ -36,7 +36,6 @@ export const moveLabware: CommandCreator = ( prevRobotState.liquidState != null ? getLabwareHasLiquid(prevRobotState.liquidState, labware) : false - const actionName = 'moveToLabware' const errors: CommandCreatorError[] = [] const warnings: CommandCreatorWarning[] = [] @@ -61,6 +60,13 @@ export const moveLabware: CommandCreator = ( errors.push(errorCreators.labwareOffDeck()) } + const isAluminumBlock = + labwareEntities[labware]?.def.metadata.displayCategory === 'aluminumBlock' + + if (useGripper && isAluminumBlock) { + errors.push(errorCreators.cannotMoveWithGripper()) + } + if ( (newLocationInWasteChute && hasGripper && !useGripper) || (!hasGripper && useGripper) diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index 658bb53fc59..d90165cebce 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -1,24 +1,26 @@ +import { ALL, COLUMN, NozzleConfigurationStyle } from '@opentrons/shared-data' import { getNextTiprack } from '../../robotStateSelectors' import * as errorCreators from '../../errorCreators' import { COLUMN_4_SLOTS } from '../../constants' -import { dropTip } from './dropTip' import { movableTrashCommandsUtil } from '../../utils/movableTrashCommandsUtil' import { curryCommandCreator, + getIsHeaterShakerEastWestMultiChannelPipette, + getIsHeaterShakerEastWestWithLatchOpen, + getIsTallLabwareWestOf96Channel, getLabwareSlot, - reduceCommandCreators, modulePipetteCollision, - uuid, pipetteAdjacentHeaterShakerWhileShaking, - getIsHeaterShakerEastWestWithLatchOpen, - getIsHeaterShakerEastWestMultiChannelPipette, + reduceCommandCreators, + uuid, wasteChuteCommandsUtil, getWasteChuteAddressableAreaNamePip, } from '../../utils' +import { dropTip } from './dropTip' import type { + CommandCreator, CommandCreatorError, CurriedCommandCreator, - CommandCreator, } from '../../types' interface PickUpTipArgs { pipette: string @@ -33,14 +35,6 @@ const _pickUpTip: CommandCreator = ( ) => { const errors: CommandCreatorError[] = [] const tiprackSlot = prevRobotState.labware[args.tiprack].slot - const pipetteName = invariantContext.pipetteEntities[args.pipette].name - const adapterId = - invariantContext.labwareEntities[tiprackSlot] != null - ? invariantContext.labwareEntities[tiprackSlot] - : null - if (adapterId == null && pipetteName === 'p1000_96') { - errors.push(errorCreators.missingAdapter()) - } if (COLUMN_4_SLOTS.includes(tiprackSlot)) { errors.push( errorCreators.pipettingIntoColumn4({ typeOfStep: 'pick up tip' }) @@ -68,6 +62,7 @@ const _pickUpTip: CommandCreator = ( interface ReplaceTipArgs { pipette: string dropTipLocation: string + nozzles?: NozzleConfigurationStyle } /** @@ -80,8 +75,31 @@ export const replaceTip: CommandCreator = ( invariantContext, prevRobotState ) => { - const { pipette, dropTipLocation } = args - const nextTiprack = getNextTiprack(pipette, invariantContext, prevRobotState) + const { pipette, dropTipLocation, nozzles } = args + const { nextTiprack, tipracks } = getNextTiprack( + pipette, + invariantContext, + prevRobotState, + nozzles + ) + const pipetteSpec = invariantContext.pipetteEntities[pipette]?.spec + const channels = pipetteSpec?.channels + const hasMoreTipracksOnDeck = + tipracks?.totalTipracks > tipracks?.filteredTipracks + + const is96ChannelTipracksAvailable = + nextTiprack == null && channels === 96 && hasMoreTipracksOnDeck + if (nozzles === ALL && is96ChannelTipracksAvailable) { + return { + errors: [errorCreators.missingAdapter()], + } + } + + if (nozzles === COLUMN && is96ChannelTipracksAvailable) { + return { + errors: [errorCreators.removeAdapter()], + } + } if (nextTiprack == null) { // no valid next tip / tiprack, bail out @@ -90,10 +108,8 @@ export const replaceTip: CommandCreator = ( } } - const pipetteSpec = invariantContext.pipetteEntities[pipette]?.spec const isFlexPipette = - (pipetteSpec?.displayCategory === 'FLEX' || pipetteSpec?.channels === 96) ?? - false + (pipetteSpec?.displayCategory === 'FLEX' || channels === 96) ?? false if (!pipetteSpec) return { @@ -133,6 +149,28 @@ export const replaceTip: CommandCreator = ( ) { return { errors: [errorCreators.dropTipLocationDoesNotExist()] } } + if ( + channels === 96 && + nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + nextTiprack.tiprackId, + pipette + ) + ) { + return { + errors: [ + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'tiprack', + labware: + invariantContext.labwareEntities[nextTiprack.tiprackId].def.metadata + .displayName, + }), + ], + } + } + if ( modulePipetteCollision({ pipette, @@ -177,7 +215,6 @@ export const replaceTip: CommandCreator = ( } } - const channels = pipetteSpec.channels const addressableAreaNameWasteChute = getWasteChuteAddressableAreaNamePip( channels ) diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index c61240fd42d..e43e482084b 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -1,6 +1,10 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' -import { getWellDepth, LOW_VOLUME_PIPETTES } from '@opentrons/shared-data' +import { + COLUMN, + getWellDepth, + LOW_VOLUME_PIPETTES, +} from '@opentrons/shared-data' import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' @@ -14,11 +18,13 @@ import { airGapHelper, dispenseLocationHelper, moveHelper, + getIsTallLabwareWestOf96Channel, getWasteChuteAddressableAreaNamePip, } from '../../utils' -import { configureForVolume } from '../atomic/configureForVolume' import { aspirate, + configureForVolume, + configureNozzleLayout, delay, dropTip, moveToWell, @@ -26,6 +32,7 @@ import { touchTip, } from '../atomic' import { mixUtil } from './mix' + import type { ConsolidateArgs, CommandCreator, @@ -51,6 +58,8 @@ export const consolidate: CommandCreator = ( */ const actionName = 'consolidate' const pipetteData = prevRobotState.pipettes[args.pipette] + const is96Channel = + invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 if (!pipetteData) { // bail out before doing anything else @@ -79,6 +88,50 @@ export const consolidate: CommandCreator = ( return { errors: [errorCreators.dropTipLocationDoesNotExist()] } } + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.sourceLabware, + args.pipette + ) + ) { + return { + errors: [ + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'aspirate', + labware: + invariantContext.labwareEntities[args.sourceLabware].def.metadata + .displayName, + }), + ], + } + } + + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.destLabware, + args.pipette + ) + ) { + return { + errors: [ + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'dispense', + labware: + invariantContext.labwareEntities[args.destLabware].def.metadata + .displayName, + }), + ], + } + } + // TODO: BC 2019-07-08 these argument names are a bit misleading, instead of being values bound // to the action of aspiration of dispensing in a given command, they are actually values bound // to a given labware associated with a command (e.g. Source, Destination). For this reason we @@ -211,6 +264,7 @@ export const consolidate: CommandCreator = ( }), ] : [] + return [ curryCommandCreator(aspirate, { pipette: args.pipette, @@ -411,11 +465,24 @@ export const consolidate: CommandCreator = ( const dropTipAfterDispenseAirGap = airGapAfterDispenseCommands.length > 0 ? dropTipCommand : [] + const stateNozzles = prevRobotState.pipettes[args.pipette].nozzles + const configureNozzleLayoutCommand: CurriedCommandCreator[] = + // only emit the command if previous nozzle state is different + is96Channel && args.nozzles != null && args.nozzles !== stateNozzles + ? [ + curryCommandCreator(configureNozzleLayout, { + nozzles: args.nozzles, + pipetteId: args.pipette, + }), + ] + : [] + return [ + ...configureNozzleLayoutCommand, ...tipCommands, + ...configureForVolumeCommand, ...mixBeforeCommands, ...preWetTipCommands, // NOTE when you both mix-before and pre-wet tip, it's kinda redundant. Prewet is like mixing once. - ...configureForVolumeCommand, ...aspirateCommands, ...dispenseCommands, ...delayAfterDispenseCommands, diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index ddffe574083..12c93ce9a3e 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -1,7 +1,11 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' import last from 'lodash/last' -import { getWellDepth, LOW_VOLUME_PIPETTES } from '@opentrons/shared-data' +import { + COLUMN, + getWellDepth, + LOW_VOLUME_PIPETTES, +} from '@opentrons/shared-data' import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' @@ -12,10 +16,13 @@ import { blowoutUtil, wasteChuteCommandsUtil, getDispenseAirGapLocation, + getIsTallLabwareWestOf96Channel, getWasteChuteAddressableAreaNamePip, } from '../../utils' import { aspirate, + configureForVolume, + configureNozzleLayout, delay, dispense, dropTip, @@ -23,7 +30,6 @@ import { replaceTip, touchTip, } from '../atomic' -import { configureForVolume } from '../atomic/configureForVolume' import { mixUtil } from './mix' import type { DistributeArgs, @@ -50,6 +56,8 @@ export const distribute: CommandCreator = ( // TODO Ian 2018-05-03 next ~20 lines match consolidate.js const actionName = 'distribute' const errors: CommandCreatorError[] = [] + const is96Channel = + invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 // TODO: Ian 2019-04-19 revisit these pipetteDoesNotExist errors, how to do it DRY? if ( @@ -80,6 +88,46 @@ export const distribute: CommandCreator = ( errors.push(errorCreators.dropTipLocationDoesNotExist()) } + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.sourceLabware, + args.pipette + ) + ) { + errors.push( + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'aspirate', + labware: + invariantContext.labwareEntities[args.sourceLabware].def.metadata + .displayName, + }) + ) + } + + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.destLabware, + args.pipette + ) + ) { + errors.push( + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'dispense', + labware: + invariantContext.labwareEntities[args.destLabware].def.metadata + .displayName, + }) + ) + } + if (errors.length > 0) return { errors, @@ -399,10 +447,23 @@ export const distribute: CommandCreator = ( ] : [] + const stateNozzles = prevRobotState.pipettes[args.pipette].nozzles + const configureNozzleLayoutCommand: CurriedCommandCreator[] = + // only emit the command if previous nozzle state is different + is96Channel && args.nozzles != null && args.nozzles !== stateNozzles + ? [ + curryCommandCreator(configureNozzleLayout, { + nozzles: args.nozzles, + pipetteId: args.pipette, + }), + ] + : [] + return [ + ...configureNozzleLayoutCommand, ...tipCommands, - ...mixBeforeAspirateCommands, ...configureForVolumeCommand, + ...mixBeforeAspirateCommands, curryCommandCreator(aspirate, { pipette, volume: args.volume * destWellChunk.length + disposalVolume, diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index 96a0df982cb..01d870eeb4b 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -1,14 +1,23 @@ import flatMap from 'lodash/flatMap' -import { LOW_VOLUME_PIPETTES } from '@opentrons/shared-data' +import { LOW_VOLUME_PIPETTES, COLUMN } from '@opentrons/shared-data' import { repeatArray, blowoutUtil, curryCommandCreator, reduceCommandCreators, + getIsTallLabwareWestOf96Channel, } from '../../utils' import * as errorCreators from '../../errorCreators' -import { configureForVolume } from '../atomic/configureForVolume' -import { aspirate, dispense, delay, replaceTip, touchTip } from '../atomic' +import { + aspirate, + configureForVolume, + configureNozzleLayout, + delay, + dispense, + replaceTip, + touchTip, +} from '../atomic' + import type { MixArgs, CommandCreator, @@ -112,6 +121,9 @@ export const mix: CommandCreator = ( dropTipLocation, } = data + const is96Channel = + invariantContext.pipetteEntities[pipette]?.spec.channels === 96 + // Errors if ( !prevRobotState.pipettes[pipette] || @@ -146,6 +158,38 @@ export const mix: CommandCreator = ( return { errors: [errorCreators.dropTipLocationDoesNotExist()] } } + if ( + is96Channel && + data.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + labware, + pipette + ) + ) { + return { + errors: [ + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'mix', + labware: + invariantContext.labwareEntities[labware].def.metadata.displayName, + }), + ], + } + } + const stateNozzles = prevRobotState.pipettes[pipette].nozzles + const configureNozzleLayoutCommand: CurriedCommandCreator[] = + // only emit the command if previous nozzle state is different + is96Channel && data.nozzles != null && data.nozzles !== stateNozzles + ? [ + curryCommandCreator(configureNozzleLayout, { + nozzles: data.nozzles, + pipetteId: pipette, + }), + ] + : [] + const configureForVolumeCommand: CurriedCommandCreator[] = LOW_VOLUME_PIPETTES.includes( invariantContext.pipetteEntities[pipette].name ) @@ -156,7 +200,6 @@ export const mix: CommandCreator = ( }), ] : [] - // Command generation const commandCreators = flatMap( wells, @@ -168,6 +211,7 @@ export const mix: CommandCreator = ( curryCommandCreator(replaceTip, { pipette, dropTipLocation, + nozzles: data.nozzles ?? undefined, }), ] } @@ -208,6 +252,7 @@ export const mix: CommandCreator = ( dispenseDelaySeconds, }) return [ + ...configureNozzleLayoutCommand, ...tipCommands, ...configureForVolumeCommand, ...mixCommands, diff --git a/step-generation/src/commandCreators/compound/transfer.ts b/step-generation/src/commandCreators/compound/transfer.ts index dbe505a7e75..76c8067d04d 100644 --- a/step-generation/src/commandCreators/compound/transfer.ts +++ b/step-generation/src/commandCreators/compound/transfer.ts @@ -1,6 +1,10 @@ import assert from 'assert' import zip from 'lodash/zip' -import { getWellDepth, LOW_VOLUME_PIPETTES } from '@opentrons/shared-data' +import { + getWellDepth, + COLUMN, + LOW_VOLUME_PIPETTES, +} from '@opentrons/shared-data' import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' @@ -14,19 +18,22 @@ import { getTrashOrLabware, dispenseLocationHelper, moveHelper, + getIsTallLabwareWestOf96Channel, getWasteChuteAddressableAreaNamePip, } from '../../utils' import { aspirate, + configureForVolume, + configureNozzleLayout, delay, dispense, dropTip, + moveToWell, replaceTip, touchTip, - moveToWell, } from '../atomic' -import { configureForVolume } from '../atomic/configureForVolume' import { mixUtil } from './mix' + import type { TransferArgs, CurriedCommandCreator, @@ -81,6 +88,8 @@ export const transfer: CommandCreator = ( // TODO Ian 2018-04-02 following ~10 lines are identical to first lines of consolidate.js... const actionName = 'transfer' const errors: CommandCreatorError[] = [] + const is96Channel = + invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 if ( !prevRobotState.pipettes[args.pipette] || @@ -117,6 +126,47 @@ export const transfer: CommandCreator = ( ) { errors.push(errorCreators.dropTipLocationDoesNotExist()) } + + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.sourceLabware, + args.pipette + ) + ) { + errors.push( + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'aspirate', + labware: + invariantContext.labwareEntities[args.sourceLabware].def.metadata + .displayName, + }) + ) + } + + if ( + is96Channel && + args.nozzles === COLUMN && + getIsTallLabwareWestOf96Channel( + prevRobotState, + invariantContext, + args.destLabware, + args.pipette + ) + ) { + errors.push( + errorCreators.tallLabwareWestOf96ChannelPipetteLabware({ + source: 'dispense', + labware: + invariantContext.labwareEntities[args.destLabware].def.metadata + .displayName, + }) + ) + } + if (errors.length > 0) return { errors, @@ -224,6 +274,17 @@ export const transfer: CommandCreator = ( changeTipNow = isInitialSubtransfer || destinationWell !== prevDestWell } + const stateNozzles = prevRobotState.pipettes[args.pipette].nozzles + const configureNozzleLayoutCommand: CurriedCommandCreator[] = + // only emit the command if previous nozzle state is different + is96Channel && args.nozzles != null && args.nozzles !== stateNozzles + ? [ + curryCommandCreator(configureNozzleLayout, { + nozzles: args.nozzles, + pipetteId: args.pipette, + }), + ] + : [] const configureForVolumeCommand: CurriedCommandCreator[] = LOW_VOLUME_PIPETTES.includes( invariantContext.pipetteEntities[args.pipette].name @@ -240,6 +301,7 @@ export const transfer: CommandCreator = ( ? [ curryCommandCreator(replaceTip, { pipette: args.pipette, + nozzles: args.nozzles ?? undefined, dropTipLocation: args.dropTipLocation, }), ] @@ -514,10 +576,11 @@ export const transfer: CommandCreator = ( : [] const nextCommands = [ + ...configureNozzleLayoutCommand, ...tipCommands, ...preWetTipCommands, - ...mixBeforeAspirateCommands, ...configureForVolumeCommand, + ...mixBeforeAspirateCommands, ...aspirateCommand, ...delayAfterAspirateCommands, ...touchTipAfterAspirateCommands, diff --git a/step-generation/src/constants.ts b/step-generation/src/constants.ts index 63e0f0d4018..3e77143cbb8 100644 --- a/step-generation/src/constants.ts +++ b/step-generation/src/constants.ts @@ -18,7 +18,4 @@ export const MODULES_WITH_COLLISION_ISSUES: ModuleModel[] = [ ] export const FIXED_TRASH_ID: 'fixedTrash' = 'fixedTrash' -export const OT_2_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_1100ml_fixed/1' -export const FLEX_TRASH_DEF_URI = 'opentrons/opentrons_1_trash_3200ml_fixed/1' - export const COLUMN_4_SLOTS = ['A4', 'B4', 'C4', 'D4'] diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index 461cf2b3d37..a03582d1309 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -19,6 +19,13 @@ export function missingAdapter(): CommandCreatorError { } } +export function removeAdapter(): CommandCreatorError { + return { + type: 'REMOVE_96_CHANNEL_TIPRACK_ADAPTER', + message: 'A 96-channel cannot pick up tips partially with an adapter', + } +} + export function noTipOnPipette(args: { actionName: string pipette: string @@ -161,6 +168,16 @@ export const tallLabwareEastWestOfHeaterShaker = ( } } +export const tallLabwareWestOf96ChannelPipetteLabware = (args: { + source: string + labware: string +}): CommandCreatorError => { + return { + type: 'TALL_LABWARE_WEST_OF_96_CHANNEL_LABWARE', + message: `Labware to the left of the ${args.source} ${args.labware} is too tall and will collide with the 96-channel.`, + } +} + export const heaterShakerEastWestWithLatchOpen = (): CommandCreatorError => { return { type: 'HEATER_SHAKER_EAST_WEST_LATCH_OPEN', @@ -225,3 +242,10 @@ export const pipettingIntoColumn4 = (args: { message: `Cannot ${args.typeOfStep} into a column 4 slot.`, } } + +export const cannotMoveWithGripper = (): CommandCreatorError => { + return { + type: 'CANNOT_MOVE_WITH_GRIPPER', + message: 'The gripper cannot move aluminum blocks', + } +} diff --git a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts index 6ac258eb88e..62cd0348ded 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts @@ -2,6 +2,7 @@ import assert from 'assert' import range from 'lodash/range' import isEmpty from 'lodash/isEmpty' import uniq from 'lodash/uniq' +import { COLUMN } from '@opentrons/shared-data' import { AIR, mergeLiquid, @@ -20,13 +21,17 @@ export function forAspirate( const { pipetteId, volume, labwareId } = params const { robotState, warnings } = robotStateAndWarnings const { liquidState } = robotState + const nozzles = robotState.pipettes[pipetteId].nozzles const pipetteSpec = invariantContext.pipetteEntities[pipetteId].spec const labwareDef = invariantContext.labwareEntities[labwareId].def + const channels = nozzles === COLUMN ? 8 : pipetteSpec.channels + const { allWellsShared, wellsForTips } = getWellsForTips( - pipetteSpec.channels, + channels, labwareDef, params.wellName ) + assert( // @ts-expect-error (sa, 2021-05-03): this assert is unnecessary uniq(wellsForTips).length === allWellsShared ? 1 : wellsForTips.length, @@ -35,12 +40,12 @@ export function forAspirate( )}` ) - if (pipetteSpec.channels > 1 && allWellsShared) { + if (channels > 1 && allWellsShared) { // special case: trough-like "shared" well with multi-channel pipette const commonWell = wellsForTips[0] const sourceLiquidState = liquidState.labware[labwareId][commonWell] const isOveraspirate = - volume * pipetteSpec.channels > getLocationTotalVolume(sourceLiquidState) + volume * channels > getLocationTotalVolume(sourceLiquidState) if (isEmpty(sourceLiquidState)) { warnings.push(warningCreators.aspirateFromPristineWell()) @@ -49,11 +54,11 @@ export function forAspirate( } const volumePerTip = isOveraspirate - ? getLocationTotalVolume(sourceLiquidState) / pipetteSpec.channels + ? getLocationTotalVolume(sourceLiquidState) / channels : volume // all tips get the same amount of the same liquid added to them, from the source well const newLiquidFromWell = splitLiquid(volumePerTip, sourceLiquidState).dest - range(pipetteSpec.channels).forEach((tipIndex): void => { + range(channels).forEach((tipIndex): void => { const pipette = liquidState.pipettes[pipetteId] const indexToString = tipIndex.toString() const tipLiquidState = pipette[indexToString] @@ -71,21 +76,20 @@ export function forAspirate( }) // Remove liquid from source well liquidState.labware[labwareId][commonWell] = splitLiquid( - volume * pipetteSpec.channels, + volume * channels, liquidState.labware[labwareId][commonWell] ).source return } // general case (no common well shared across all tips) - range(pipetteSpec.channels).forEach(tipIndex => { + range(channels).forEach(tipIndex => { const indexToString = tipIndex.toString() const pipette = liquidState.pipettes[pipetteId] const tipLiquidState = pipette[indexToString] const sourceLiquidState = liquidState.labware[labwareId][wellsForTips[tipIndex]] const newLiquidFromWell = splitLiquid(volume, sourceLiquidState).dest - if (isEmpty(sourceLiquidState)) { warnings.push(warningCreators.aspirateFromPristineWell()) } else if (volume > getLocationTotalVolume(sourceLiquidState)) { diff --git a/step-generation/src/getNextRobotStateAndWarnings/forConfigureNozzleLayout.ts b/step-generation/src/getNextRobotStateAndWarnings/forConfigureNozzleLayout.ts new file mode 100644 index 00000000000..dbfcb5f50e7 --- /dev/null +++ b/step-generation/src/getNextRobotStateAndWarnings/forConfigureNozzleLayout.ts @@ -0,0 +1,21 @@ +import { NozzleConfigurationStyle } from '@opentrons/shared-data' +import type { InvariantContext, RobotStateAndWarnings } from '../types' + +interface ConfigureNozzleLayoutParams { + pipetteId: string + configurationParams: { + style: NozzleConfigurationStyle + primaryNozzle?: string + } +} + +export function forConfigureNozzleLayout( + params: ConfigureNozzleLayoutParams, + invariantContext: InvariantContext, + robotStateAndWarnings: RobotStateAndWarnings +): void { + const { pipetteId, configurationParams } = params + const { robotState } = robotStateAndWarnings + + robotState.pipettes[pipetteId].nozzles = configurationParams.style +} diff --git a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts index 17990124187..a0a1c649b6b 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { getIsTiprack } from '@opentrons/shared-data' +import { ALL, COLUMN, getIsTiprack } from '@opentrons/shared-data' import type { PickUpTipParams } from '@opentrons/shared-data/protocol/types/schemaV6/command/pipetting' import type { InvariantContext, RobotStateAndWarnings } from '../types' export function forPickUpTip( @@ -15,13 +15,14 @@ export function forPickUpTip( `forPickUpTip expected ${labwareId} to be a tiprack` ) const tipState = robotStateAndWarnings.robotState.tipState + const nozzles = robotStateAndWarnings.robotState.pipettes[pipetteId].nozzles // pipette now has tip(s) tipState.pipettes[pipetteId] = true // remove tips from tiprack if (pipetteSpec.channels === 1) { tipState.tipracks[labwareId][wellName] = false - } else if (pipetteSpec.channels === 8) { + } else if (pipetteSpec.channels === 8 || nozzles === COLUMN) { const allWells = tiprackDef.ordering.find(col => col[0] === wellName) if (!allWells) { @@ -32,7 +33,7 @@ export function forPickUpTip( allWells.forEach(function (wellName) { tipState.tipracks[labwareId][wellName] = false }) - } else if (pipetteSpec.channels === 96) { + } else if (pipetteSpec.channels === 96 && nozzles === ALL) { const allTips: string[] = tiprackDef.ordering.reduce( (acc, wells) => acc.concat(wells), [] diff --git a/step-generation/src/getNextRobotStateAndWarnings/index.ts b/step-generation/src/getNextRobotStateAndWarnings/index.ts index 81383f4576a..384e6bd0abe 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/index.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/index.ts @@ -46,6 +46,7 @@ import type { RobotState, RobotStateAndWarnings, } from '../types' +import { forConfigureNozzleLayout } from './forConfigureNozzleLayout' // WARNING this will mutate the prevRobotState function _getNextRobotStateAndWarningsSingleCommand( @@ -131,6 +132,14 @@ function _getNextRobotStateAndWarningsSingleCommand( ) break + case 'configureNozzleLayout': + forConfigureNozzleLayout( + command.params, + invariantContext, + robotStateAndWarnings + ) + break + case 'touchTip': case 'waitForDuration': case 'waitForResume': diff --git a/step-generation/src/robotStateSelectors.ts b/step-generation/src/robotStateSelectors.ts index ebb75ee9dc6..3b13aecf86d 100644 --- a/step-generation/src/robotStateSelectors.ts +++ b/step-generation/src/robotStateSelectors.ts @@ -6,6 +6,9 @@ import { getTiprackVolume, THERMOCYCLER_MODULE_TYPE, orderWells, + NozzleConfigurationStyle, + COLUMN, + ALL, } from '@opentrons/shared-data' import type { InvariantContext, @@ -25,9 +28,10 @@ export function _getNextTip(args: { tiprackId: string invariantContext: InvariantContext robotState: RobotState + nozzles?: NozzleConfigurationStyle }): string | null { // return the well name of the next available tip for a pipette (or null) - const { pipetteId, tiprackId, invariantContext, robotState } = args + const { pipetteId, tiprackId, invariantContext, robotState, nozzles } = args const pipetteChannels = invariantContext.pipetteEntities[pipetteId]?.spec?.channels const tiprackWellsState = robotState.tipState.tipracks[tiprackId] @@ -41,13 +45,13 @@ export function _getNextTip(args: { return well || null } - if (pipetteChannels === 8) { + if (pipetteChannels === 8 || (pipetteChannels === 96 && nozzles === COLUMN)) { // return first well in the column (for 96-well format, the 'A' row) const tiprackColumns = tiprackDef.ordering const fullColumn = tiprackColumns.find(col => col.every(hasTip)) return fullColumn != null ? fullColumn[0] : null } - if (pipetteChannels === 96) { + if (pipetteChannels === 96 && nozzles === ALL) { const allWellsHaveTip = orderedWells.every(hasTip) return allWellsHaveTip ? orderedWells[0] : null } @@ -55,15 +59,19 @@ export function _getNextTip(args: { assert(false, `Pipette ${pipetteId} has no channels/spec, cannot _getNextTip`) return null } -type NextTiprack = { - tiprackId: string - well: string -} | null +interface NextTiprackInfo { + nextTiprack: { + tiprackId: string + well: string + } | null + tipracks: { totalTipracks: number; filteredTipracks: number } +} export function getNextTiprack( pipetteId: string, invariantContext: InvariantContext, - robotState: RobotState -): NextTiprack { + robotState: RobotState, + nozzles?: NozzleConfigurationStyle +): NextTiprackInfo { /** Returns the next tiprack that has tips. Tipracks are any labwareIds that exist in tipState.tipracks. For 8-channel pipette, tipracks need a full column of tips. @@ -92,34 +100,64 @@ export function getNextTiprack( ) } ) - const firstAvailableTiprack = sortedTipracksIds.find(tiprackId => + const is96Channel = pipetteEntity.spec.channels === 96 + const filteredSortedTipRackIdsFor96Channel = sortedTipracksIds.filter( + tiprackId => { + const tipRackLocation = robotState.labware[tiprackId].slot + const adapterEntity = invariantContext.labwareEntities[tipRackLocation] + const has96TiprackAdapterId = + adapterEntity?.def.parameters.loadName === + 'opentrons_flex_96_tiprack_adapter' && + adapterEntity?.def.namespace === 'opentrons' + + return nozzles === ALL ? has96TiprackAdapterId : !has96TiprackAdapterId + } + ) + const firstAvailableTiprack = (is96Channel + ? filteredSortedTipRackIdsFor96Channel + : sortedTipracksIds + ).find(tiprackId => _getNextTip({ pipetteId, tiprackId, + nozzles, invariantContext, robotState, }) ) - // TODO Ian 2018-02-12: avoid calling _getNextTip twice const nextTip = firstAvailableTiprack && _getNextTip({ pipetteId, tiprackId: firstAvailableTiprack, + nozzles, invariantContext, robotState, }) if (firstAvailableTiprack && nextTip) { return { - tiprackId: firstAvailableTiprack, - well: nextTip, + nextTiprack: { + tiprackId: firstAvailableTiprack, + well: nextTip, + }, + tipracks: { + totalTipracks: sortedTipracksIds.length, + filteredTipracks: filteredSortedTipRackIdsFor96Channel.length, + }, } } - // No available tipracks (for given pipette channels) - return null + // No available tipracks (for given pipette channels) but keep track of tiprack numbers + // for 96-channel and tiprack adapters + return { + nextTiprack: null, + tipracks: { + totalTipracks: sortedTipracksIds.length, + filteredTipracks: filteredSortedTipRackIdsFor96Channel.length, + }, + } } export function getPipetteWithTipMaxVol( pipetteId: string, diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index ded3bbab515..2d805b11177 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -14,6 +14,7 @@ import type { ModuleModel, PipetteNameSpecs, PipetteName, + NozzleConfigurationStyle, } from '@opentrons/shared-data' import type { AtomicProfileStep, @@ -40,6 +41,7 @@ export interface LabwareTemporalProperties { export interface PipetteTemporalProperties { mount: Mount + nozzles?: NozzleConfigurationStyle } export interface MagneticModuleState { @@ -165,7 +167,7 @@ interface CommonArgs { export type SharedTransferLikeArgs = CommonArgs & { pipette: string // PipetteId - + nozzles: NozzleConfigurationStyle | null // setting for 96-channel sourceLabware: string destLabware: string /** volume is interpreted differently by different Step types */ @@ -261,6 +263,7 @@ export type MixArgs = CommonArgs & { commandCreatorFnName: 'mix' labware: string pipette: string + nozzles: NozzleConfigurationStyle | null // setting for 96-channel wells: string[] /** Mix volume (should not exceed pipette max) */ volume: number @@ -494,6 +497,7 @@ export interface RobotState { } export type ErrorType = + | 'CANNOT_MOVE_WITH_GRIPPER' | 'DROP_TIP_LOCATION_DOES_NOT_EXIST' | 'EQUIPMENT_DOES_NOT_EXIST' | 'GRIPPER_REQUIRED' @@ -517,7 +521,9 @@ export type ErrorType = | 'PIPETTE_DOES_NOT_EXIST' | 'PIPETTE_VOLUME_EXCEEDED' | 'PIPETTING_INTO_COLUMN_4' + | 'REMOVE_96_CHANNEL_TIPRACK_ADAPTER' | 'TALL_LABWARE_EAST_WEST_OF_HEATER_SHAKER' + | 'TALL_LABWARE_WEST_OF_96_CHANNEL_LABWARE' | 'THERMOCYCLER_LID_CLOSED' | 'TIP_VOLUME_EXCEEDED' diff --git a/step-generation/src/utils/index.ts b/step-generation/src/utils/index.ts index 70d4a55346a..9a16aad6fc1 100644 --- a/step-generation/src/utils/index.ts +++ b/step-generation/src/utils/index.ts @@ -20,5 +20,6 @@ export * from './commandCreatorArgsGetters' export * from './heaterShakerCollision' export * from './misc' export * from './movableTrashCommandsUtil' +export * from './ninetySixChannelCollision' export * from './wasteChuteCommandsUtil' export const uuid: () => string = uuidv4 diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index 3128a2f177b..f2a4f952258 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -13,30 +13,31 @@ import { EIGHT_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, NINETY_SIX_CHANNEL_WASTE_CHUTE_ADDRESSABLE_AREA, } from '@opentrons/shared-data' +import { reduceCommandCreators, wasteChuteCommandsUtil } from './index' +import { + aspirate, + dispense, + moveToAddressableArea, + moveToWell, +} from '../commandCreators/atomic' import { blowout } from '../commandCreators/atomic/blowout' import { curryCommandCreator } from './curryCommandCreator' +import { movableTrashCommandsUtil } from './movableTrashCommandsUtil' import type { LabwareDefinition2 } from '@opentrons/shared-data' import type { BlowoutParams } from '@opentrons/shared-data/protocol/types/schemaV4' import type { + AdditionalEquipmentEntities, + AdditionalEquipmentEntity, + CommandCreator, CurriedCommandCreator, InvariantContext, + LabwareEntities, LabwareEntity, LocationLiquidState, PipetteEntity, RobotState, SourceAndDest, } from '../types' -import { - AdditionalEquipmentEntities, - AdditionalEquipmentEntity, - CommandCreator, - dispense, - LabwareEntities, - aspirate, -} from '..' -import { reduceCommandCreators, wasteChuteCommandsUtil } from './index' -import { moveToAddressableArea, moveToWell } from '../commandCreators/atomic' -import { movableTrashCommandsUtil } from './movableTrashCommandsUtil' export const AIR: '__air__' = '__air__' export const SOURCE_WELL_BLOWOUT_DESTINATION: 'source_well' = 'source_well' export const DEST_WELL_BLOWOUT_DESTINATION: 'dest_well' = 'dest_well' diff --git a/step-generation/src/utils/ninetySixChannelCollision.ts b/step-generation/src/utils/ninetySixChannelCollision.ts new file mode 100644 index 00000000000..60dc39dc15f --- /dev/null +++ b/step-generation/src/utils/ninetySixChannelCollision.ts @@ -0,0 +1,81 @@ +import toNumber from 'lodash/toNumber' +import { getModuleDef2 } from '@opentrons/shared-data' +import type { RobotState, InvariantContext } from '../types' + +const SAFETY_MARGIN = 10 +const targetNumbers = ['2', '3', '4'] + +export const getIsTallLabwareWestOf96Channel = ( + robotState: RobotState, + invariantContext: InvariantContext, + sourceLabwareId: string, + pipetteId: string +): boolean => { + const { + labwareEntities, + additionalEquipmentEntities, + pipetteEntities, + } = invariantContext + const { labware: labwareState, tipState } = robotState + const pipetteHasTip = tipState.pipettes[pipetteId] + const tipLength = pipetteHasTip + ? pipetteEntities[pipetteId].tiprackLabwareDef.parameters.tipLength ?? 0 + : 0 + // early exit if source labware is the waste chute or trash bin + if (additionalEquipmentEntities[sourceLabwareId] != null) { + return false + } + + const labwareSlot = labwareState[sourceLabwareId].slot + const letter = labwareSlot.charAt(0) + const number = labwareSlot.charAt(1) + + if (targetNumbers.includes(number)) { + const westNumber = toNumber(number) - 1 + const westSlot = letter + westNumber + + const westLabwareState = Object.entries(labwareState).find( + ([id, labware]) => labware.slot === westSlot + ) + if (westLabwareState != null) { + const westLabwareId = westLabwareState[0] + if (labwareEntities[westLabwareId] == null) { + console.error( + `expected to find labware west of source labware but could not, with labware id ${westLabwareId}` + ) + } + if (labwareEntities[westLabwareId] != null) { + const westLabwareHeight = + labwareEntities[westLabwareId].def.dimensions.zDimension + const westLabwareSlot = robotState.labware[westLabwareId].slot + let adapterHeight: number = 0 + let moduleHeight: number = 0 + // if labware is on an adapter + or on an adapter + module + if (robotState.labware[westLabwareSlot] != null) { + const adapterSlot = robotState.labware[westLabwareSlot]?.slot + adapterHeight = + invariantContext.labwareEntities[westLabwareSlot]?.def.dimensions + .zDimension + const moduleModel = + invariantContext.moduleEntities[adapterSlot]?.model + const moduleDimensions = + moduleModel != null ? getModuleDef2(moduleModel)?.dimensions : null + moduleHeight = + moduleDimensions != null ? moduleDimensions.bareOverallHeight : 0 + // if labware is on a module + } else if (invariantContext.moduleEntities[westLabwareSlot] != null) { + const moduleModel = + invariantContext.moduleEntities[westLabwareSlot].model + moduleHeight = getModuleDef2(moduleModel).dimensions.bareOverallHeight + } + const totalHighestZ = westLabwareHeight + adapterHeight + moduleHeight + const sourceLabwareHeight = + labwareEntities[sourceLabwareId].def.dimensions.zDimension + + return totalHighestZ + SAFETY_MARGIN > sourceLabwareHeight + tipLength + } + } + } + + return false +}