From 577ae43d464176d21c4834b783d7416524fa027e Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Thu, 23 Jan 2025 15:58:07 -0500 Subject: [PATCH 1/4] feat(protocol-designer): handle (no) gripper with absorbance reader (#17336) This PR updates logic for adding absorbance reader module given presence or lack thereof of a gripper. During onboarding, if no gripper is added, the selector button for adding an absorbance reader module is disabled with tooltip. However, in deck setup, we no longer prevent the user from adding an absorbance reader module without the presence of a gripper. We will be permissive in setup, and present timeline errors in protocol timeline. Closes AUTH-1345 --- .../localization/en/create_new_protocol.json | 1 + .../CreateNewProtocolWizard/SelectModules.tsx | 54 ++++++++++++++++--- .../__tests__/SelectModules.test.tsx | 11 +++- .../Designer/DeckSetup/DeckSetupTools.tsx | 15 +----- .../__tests__/DeckSetupTools.test.tsx | 13 ----- 5 files changed, 57 insertions(+), 37 deletions(-) diff --git a/protocol-designer/src/assets/localization/en/create_new_protocol.json b/protocol-designer/src/assets/localization/en/create_new_protocol.json index d5d447c2d09..adde4d4c959 100644 --- a/protocol-designer/src/assets/localization/en/create_new_protocol.json +++ b/protocol-designer/src/assets/localization/en/create_new_protocol.json @@ -2,6 +2,7 @@ "add_custom_tips": "Add custom pipette tips", "add_fixtures": "Add your fixtures", "add_gripper": "Add a gripper", + "add_gripper_for_absorbance_reader": "Add a gripper to enable adding Absorbance Plate Reader", "add_modules": "Add your modules", "add_pipette": "Add a pipette", "add_pipette_to_continue": "Add a pipette to continue", diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx index 568859d205d..f2ef2c15cf5 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/SelectModules.tsx @@ -10,7 +10,9 @@ import { ListItem, SPACING, StyledText, + Tooltip, TYPOGRAPHY, + useHoverTooltip, WRAP, } from '@opentrons/components' import { @@ -73,6 +75,7 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { MAGNETIC_BLOCK_TYPE, ABSORBANCE_READER_TYPE, ] + const hasGripper = additionalEquipment.some(aE => aE === 'gripper') const handleAddModule = ( moduleModel: ModuleModel, @@ -189,15 +192,12 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { moduleModel ) return ( - { - handleAddModule(moduleModel, numSlotsAvailable === 0) - }} + moduleModel={moduleModel} + areSlotsAvailable={numSlotsAvailable > 0} + hasGripper={hasGripper} + handleAddModule={handleAddModule} /> ) })} @@ -306,3 +306,41 @@ export function SelectModules(props: WizardTileProps): JSX.Element | null { ) } + +interface AddModuleEmptySelectorButtonProps { + moduleModel: ModuleModel + areSlotsAvailable: boolean + hasGripper: boolean + handleAddModule: (arg0: ModuleModel, arg1: boolean) => void +} + +function AddModuleEmptySelectorButton( + props: AddModuleEmptySelectorButtonProps +): JSX.Element { + const { moduleModel, areSlotsAvailable, hasGripper, handleAddModule } = props + const [targetProps, tooltipProps] = useHoverTooltip() + const { t } = useTranslation('create_new_protocol') + const disableGripperRequired = + !hasGripper && moduleModel === ABSORBANCE_READER_V1 + + return ( + <> + + { + handleAddModule(moduleModel, !areSlotsAvailable) + }} + /> + + {disableGripperRequired ? ( + + {t('add_gripper_for_absorbance_reader')} + + ) : null} + + ) +} diff --git a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx index 30dcf7fae43..c8c0a07cf7b 100644 --- a/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx +++ b/protocol-designer/src/pages/CreateNewProtocolWizard/__tests__/SelectModules.test.tsx @@ -1,4 +1,4 @@ -import { describe, it, expect, vi, beforeEach } from 'vitest' +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' import '@testing-library/jest-dom/vitest' import { FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' import { fireEvent, screen } from '@testing-library/react' @@ -46,7 +46,9 @@ describe('SelectModules', () => { } as WizardTileProps vi.mocked(getEnableAbsorbanceReader).mockReturnValue(true) }) - + afterEach(() => { + vi.restoreAllMocks() + }) it('renders the flex options and overall text', () => { render(props) screen.getByText('Step 4') @@ -95,4 +97,9 @@ describe('SelectModules', () => { fireEvent.click(screen.getByRole('button', { name: 'Go back' })) expect(props.goBack).toHaveBeenCalled() }) + it('disables absorbance reader if no gripper', () => { + render(props) + fireEvent.click(screen.getByText('Absorbance Plate Reader Module GEN1')) + expect(props.setValue).not.toHaveBeenCalled() + }) }) diff --git a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx index d95abdfe870..b86d36896cf 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/DeckSetupTools.tsx @@ -18,7 +18,6 @@ import { TYPOGRAPHY, } from '@opentrons/components' import { - ABSORBANCE_READER_TYPE, ABSORBANCE_READER_V1, FLEX_ROBOT_TYPE, FLEX_STAGING_AREA_SLOT_ADDRESSABLE_AREAS, @@ -36,10 +35,7 @@ import { createDeckFixture, deleteDeckFixture, } from '../../../step-forms/actions/additionalItems' -import { - getAdditionalEquipment, - getSavedStepForms, -} from '../../../step-forms/selectors' +import { getSavedStepForms } from '../../../step-forms/selectors' import { deleteModule } from '../../../step-forms/actions' import { getDeckSetupForActiveItem } from '../../../top-selectors/labware-locations' import { @@ -126,10 +122,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { const [changeModuleWarningInfo, displayModuleWarning] = useState( false ) - const additionalEquipment = useSelector(getAdditionalEquipment) - const isGripperAttached = Object.values(additionalEquipment).some( - equipment => equipment?.name === 'gripper' - ) const [selectedHardware, setSelectedHardware] = useState< ModuleModel | Fixture | null >(null) @@ -345,11 +337,6 @@ export function DeckSetupTools(props: DeckSetupToolsProps): JSX.Element | null { if (selectedModuleModel != null) { // create module const moduleType = getModuleType(selectedModuleModel) - // enforce gripper present in order to add plate reader - if (moduleType === ABSORBANCE_READER_TYPE && !isGripperAttached) { - makeSnackbar(t('gripper_required_for_plate_reader') as string) - return - } const moduleSteps = Object.values(savedSteps).filter(step => { return ( diff --git a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx index 339d0f81695..6a7a4099402 100644 --- a/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx +++ b/protocol-designer/src/pages/Designer/DeckSetup/__tests__/DeckSetupTools.test.tsx @@ -196,17 +196,4 @@ describe('DeckSetupTools', () => { fireEvent.click(screen.getByText('Done')) expect(props.onCloseClick).toHaveBeenCalled() }) - it('should prevent saving plate reader and make toast if gripper not configured', () => { - vi.mocked(selectors.getZoomedInSlotInfo).mockReturnValue({ - selectedLabwareDefUri: null, - selectedNestedLabwareDefUri: null, - selectedFixture: null, - selectedModuleModel: ABSORBANCE_READER_V1, - selectedSlot: { slot: 'D3', cutout: 'cutoutD3' }, - }) - render(props) - fireEvent.click(screen.getByText('Done')) - expect(props.onCloseClick).not.toHaveBeenCalled() - expect(mockMakeSnackbar).toHaveBeenCalled() - }) }) From 1a8766579104f55ba3762c28fa0c468856ba6a3c Mon Sep 17 00:00:00 2001 From: Seth Foster Date: Thu, 23 Jan 2025 16:31:34 -0500 Subject: [PATCH 2/4] refactor(api): split up protocol engine types (#17338) This was getting _quite_ unwieldy. Let's split it into multiple files. We can change what the names of the files are later if we want because nothing is poking through and importing them individually (except each other). ## review seem like a good idea? ## test automated tests should handle this --- api/src/opentrons/protocol_engine/types.py | 1328 ----------------- .../protocol_engine/types/__init__.py | 252 ++++ .../types/automatic_tip_selection.py | 39 + .../types/command_annotations.py | 53 + .../types/deck_configuration.py | 72 + .../protocol_engine/types/execution.py | 96 ++ .../types/hardware_passthrough.py | 25 + .../protocol_engine/types/instrument.py | 47 + .../types/instrument_sensors.py | 47 + .../protocol_engine/types/labware.py | 80 + .../protocol_engine/types/labware_movement.py | 22 + .../types/labware_offset_location.py | 59 + .../types/labware_offset_vector.py | 33 + .../opentrons/protocol_engine/types/liquid.py | 40 + .../protocol_engine/types/liquid_class.py | 59 + .../protocol_engine/types/liquid_handling.py | 13 + .../types/liquid_level_detection.py | 48 + .../protocol_engine/types/location.py | 110 ++ .../opentrons/protocol_engine/types/module.py | 260 ++++ .../types/partial_tip_configuration.py | 76 + .../types/run_time_parameters.py | 133 ++ .../opentrons/protocol_engine/types/tip.py | 18 + .../opentrons/protocol_engine/types/util.py | 21 + .../protocol_engine/types/well_position.py | 107 ++ 24 files changed, 1710 insertions(+), 1328 deletions(-) delete mode 100644 api/src/opentrons/protocol_engine/types.py create mode 100644 api/src/opentrons/protocol_engine/types/__init__.py create mode 100644 api/src/opentrons/protocol_engine/types/automatic_tip_selection.py create mode 100644 api/src/opentrons/protocol_engine/types/command_annotations.py create mode 100644 api/src/opentrons/protocol_engine/types/deck_configuration.py create mode 100644 api/src/opentrons/protocol_engine/types/execution.py create mode 100644 api/src/opentrons/protocol_engine/types/hardware_passthrough.py create mode 100644 api/src/opentrons/protocol_engine/types/instrument.py create mode 100644 api/src/opentrons/protocol_engine/types/instrument_sensors.py create mode 100644 api/src/opentrons/protocol_engine/types/labware.py create mode 100644 api/src/opentrons/protocol_engine/types/labware_movement.py create mode 100644 api/src/opentrons/protocol_engine/types/labware_offset_location.py create mode 100644 api/src/opentrons/protocol_engine/types/labware_offset_vector.py create mode 100644 api/src/opentrons/protocol_engine/types/liquid.py create mode 100644 api/src/opentrons/protocol_engine/types/liquid_class.py create mode 100644 api/src/opentrons/protocol_engine/types/liquid_handling.py create mode 100644 api/src/opentrons/protocol_engine/types/liquid_level_detection.py create mode 100644 api/src/opentrons/protocol_engine/types/location.py create mode 100644 api/src/opentrons/protocol_engine/types/module.py create mode 100644 api/src/opentrons/protocol_engine/types/partial_tip_configuration.py create mode 100644 api/src/opentrons/protocol_engine/types/run_time_parameters.py create mode 100644 api/src/opentrons/protocol_engine/types/tip.py create mode 100644 api/src/opentrons/protocol_engine/types/util.py create mode 100644 api/src/opentrons/protocol_engine/types/well_position.py diff --git a/api/src/opentrons/protocol_engine/types.py b/api/src/opentrons/protocol_engine/types.py deleted file mode 100644 index ea070e4374e..00000000000 --- a/api/src/opentrons/protocol_engine/types.py +++ /dev/null @@ -1,1328 +0,0 @@ -"""Public protocol engine value types and models.""" -from __future__ import annotations -from datetime import datetime -from enum import Enum -from dataclasses import dataclass -from pathlib import Path -from typing import ( - Any, - Dict, - FrozenSet, - List, - Mapping, - NamedTuple, - Optional, - Tuple, - Union, -) - -from pydantic import ( - ConfigDict, - BaseModel, - Field, - RootModel, - StrictBool, - StrictFloat, - StrictInt, - StrictStr, -) -from typing_extensions import Literal, TypeGuard - -from opentrons_shared_data.pipette.types import PipetteNameType -from opentrons.types import MountType, DeckSlotName, StagingSlotName -from opentrons.hardware_control.types import ( - TipStateType as HwTipStateType, - InstrumentProbeType, -) -from opentrons.hardware_control.modules import ( - ModuleType as ModuleType, -) -from opentrons_shared_data.liquid_classes.liquid_class_definition import ( - ByTipTypeSetting, -) -from opentrons_shared_data.pipette.types import ( # noqa: F401 - # convenience re-export of LabwareUri type - LabwareUri as LabwareUri, -) -from opentrons_shared_data.module.types import ModuleType as SharedDataModuleType - - -# todo(mm, 2024-06-24): This monolithic status field is getting to be a bit much. -# We should consider splitting this up into multiple fields. -class EngineStatus(str, Enum): - """Current execution status of a ProtocolEngine. - - This is a high-level summary of what the robot is doing and what interactions are - appropriate. - """ - - # Statuses for an ongoing run: - - IDLE = "idle" - """The protocol has not been started yet. - - The robot may truly be idle, or it may be executing commands with `intent: "setup"`. - """ - - RUNNING = "running" - """The engine is actively running the protocol.""" - - PAUSED = "paused" - """A pause has been requested. Activity is paused, or will pause soon. - - (There is currently no way to tell which.) - """ - - BLOCKED_BY_OPEN_DOOR = "blocked-by-open-door" - """The robot's door is open. Activity is paused, or will pause soon.""" - - STOP_REQUESTED = "stop-requested" - """A stop has been requested. Activity will stop soon.""" - - FINISHING = "finishing" - """The robot is doing post-run cleanup, like homing and dropping tips.""" - - # Statuses for error recovery mode: - - AWAITING_RECOVERY = "awaiting-recovery" - """The engine is waiting for external input to recover from a nonfatal error. - - New commands with `intent: "fixit"` may be enqueued, which will run immediately. - The run can't be paused in this state, but it can be canceled, or resumed from the - next protocol command if recovery is complete. - """ - - AWAITING_RECOVERY_PAUSED = "awaiting-recovery-paused" - """The engine is paused while in error recovery mode. Activity is paused, or will pause soon. - - This state is not possible to enter manually. It happens when an open door - gets closed during error recovery. - """ - - AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = "awaiting-recovery-blocked-by-open-door" - """The robot's door is open while in recovery mode. Activity is paused, or will pause soon.""" - - # Terminal statuses: - - STOPPED = "stopped" - """All activity is over; it was stopped by an explicit external request.""" - - FAILED = "failed" - """All activity is over; there was a fatal error.""" - - SUCCEEDED = "succeeded" - """All activity is over; things completed without any fatal error.""" - - -class DeckSlotLocation(BaseModel): - """The location of something placed in a single deck slot.""" - - slotName: DeckSlotName = Field( - ..., - description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. - "A slot on the robot's deck." - "\n\n" - 'The plain numbers like `"5"` are for the OT-2,' - ' and the coordinates like `"C2"` are for the Flex.' - "\n\n" - "When you provide one of these values, you can use either style." - " It will automatically be converted to match the robot." - "\n\n" - "When one of these values is returned, it will always match the robot." - ), - ) - - -class StagingSlotLocation(BaseModel): - """The location of something placed in a single staging slot.""" - - slotName: StagingSlotName = Field( - ..., - description=( - # This description should be kept in sync with LabwareOffsetLocation.slotName. - "A slot on the robot's staging area." - "\n\n" - "These apply only to the Flex. The OT-2 has no staging slots." - ), - ) - - -class AddressableAreaLocation(BaseModel): - """The location of something place in an addressable area. This is a superset of deck slots.""" - - addressableAreaName: str = Field( - ..., - description=( - "The name of the addressable area that you want to use." - " Valid values are the `id`s of `addressableArea`s in the" - " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." - ), - ) - - -class ModuleLocation(BaseModel): - """The location of something placed atop a hardware module.""" - - moduleId: str = Field( - ..., - description="The ID of a loaded module from a prior `loadModule` command.", - ) - - -class OnLabwareLocation(BaseModel): - """The location of something placed atop another labware.""" - - labwareId: str = Field( - ..., - description="The ID of a loaded Labware from a prior `loadLabware` command.", - ) - - -_OffDeckLocationType = Literal["offDeck"] -_SystemLocationType = Literal["systemLocation"] -OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" -SYSTEM_LOCATION: _SystemLocationType = "systemLocation" - -LabwareLocation = Union[ - DeckSlotLocation, - ModuleLocation, - OnLabwareLocation, - _OffDeckLocationType, - _SystemLocationType, - AddressableAreaLocation, -] -"""Union of all locations where it's legal to keep a labware.""" - -OnDeckLabwareLocation = Union[ - DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation -] - -NonStackedLocation = Union[ - DeckSlotLocation, - AddressableAreaLocation, - ModuleLocation, - _OffDeckLocationType, - _SystemLocationType, -] -"""Union of all locations where it's legal to keep a labware that can't be stacked on another labware""" - - -class WellOrigin(str, Enum): - """Origin of WellLocation offset. - - Props: - TOP: the top-center of the well - BOTTOM: the bottom-center of the well - CENTER: the middle-center of the well - MENISCUS: the meniscus-center of the well - """ - - TOP = "top" - BOTTOM = "bottom" - CENTER = "center" - MENISCUS = "meniscus" - - -class PickUpTipWellOrigin(str, Enum): - """The origin of a PickUpTipWellLocation offset. - - Props: - TOP: the top-center of the well - BOTTOM: the bottom-center of the well - CENTER: the middle-center of the well - """ - - TOP = "top" - BOTTOM = "bottom" - CENTER = "center" - - -class DropTipWellOrigin(str, Enum): - """The origin of a DropTipWellLocation offset. - - Props: - TOP: the top-center of the well - BOTTOM: the bottom-center of the well - CENTER: the middle-center of the well - DEFAULT: the default drop-tip location of the well, - based on pipette configuration and length of the tip. - """ - - TOP = "top" - BOTTOM = "bottom" - CENTER = "center" - DEFAULT = "default" - - -# This is deliberately a separate type from Vec3f to let components default to 0. -class WellOffset(BaseModel): - """An offset vector in (x, y, z).""" - - x: float = 0 - y: float = 0 - z: float = 0 - - -class WellLocation(BaseModel): - """A relative location in reference to a well's location.""" - - origin: WellOrigin = WellOrigin.TOP - offset: WellOffset = Field(default_factory=WellOffset) - volumeOffset: float = Field( - default=0.0, - description="""A volume of liquid, in µL, to offset the z-axis offset.""", - ) - - -class LiquidHandlingWellLocation(BaseModel): - """A relative location in reference to a well's location. - - To be used with commands that handle liquids. - """ - - origin: WellOrigin = WellOrigin.TOP - offset: WellOffset = Field(default_factory=WellOffset) - volumeOffset: Union[float, Literal["operationVolume"]] = Field( - default=0.0, - description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""", - ) - - -class PickUpTipWellLocation(BaseModel): - """A relative location in reference to a well's location. - - To be used for picking up tips. - """ - - origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP - offset: WellOffset = Field(default_factory=WellOffset) - - -class DropTipWellLocation(BaseModel): - """Like WellLocation, but for dropping tips. - - Unlike a typical WellLocation, the location for a drop tip - defaults to location based on the tip length rather than the well's top. - """ - - origin: DropTipWellOrigin = DropTipWellOrigin.DEFAULT - offset: WellOffset = Field(default_factory=WellOffset) - - -@dataclass(frozen=True) -class Dimensions: - """Dimensions of an object in deck-space.""" - - x: float - y: float - z: float - - -# TODO(mm, 2022-11-07): Deduplicate with Vec3f. -class DeckPoint(BaseModel): - """Coordinates of a point in deck space.""" - - x: float - y: float - z: float - - -# TODO(mm, 2023-05-10): Deduplicate with constants in -# opentrons.protocols.api_support.deck_type -# and consider moving to shared-data. -class DeckType(str, Enum): - """Types of deck available.""" - - OT2_STANDARD = "ot2_standard" - OT2_SHORT_TRASH = "ot2_short_trash" - OT3_STANDARD = "ot3_standard" - - -class LoadedPipette(BaseModel): - """A pipette that has been loaded.""" - - id: str - pipetteName: PipetteNameType - mount: MountType - - -@dataclass -class FlowRates: - """Default and current flow rates for a pipette.""" - - default_blow_out: Dict[str, float] - default_aspirate: Dict[str, float] - default_dispense: Dict[str, float] - - -@dataclass(frozen=True) -class CurrentWell: - """The latest well that the robot has accessed.""" - - pipette_id: str - labware_id: str - well_name: str - - -class LoadedVolumeInfo(BaseModel): - """A well's liquid volume, initialized by a LoadLiquid, updated by Aspirate and Dispense.""" - - volume: Optional[float] = None - last_loaded: datetime - operations_since_load: int - - -class ProbedHeightInfo(BaseModel): - """A well's liquid height, initialized by a LiquidProbe, cleared by Aspirate and Dispense.""" - - height: Optional[float] = None - last_probed: datetime - - -class ProbedVolumeInfo(BaseModel): - """A well's liquid volume, initialized by a LiquidProbe, updated by Aspirate and Dispense.""" - - volume: Optional[float] = None - last_probed: datetime - operations_since_probe: int - - -class WellInfoSummary(BaseModel): - """Payload for a well's liquid info in StateSummary.""" - - labware_id: str - well_name: str - loaded_volume: Optional[float] = None - probed_height: Optional[float] = None - probed_volume: Optional[float] = None - - -@dataclass -class WellLiquidInfo: - """Tracked and sensed information about liquid in a well.""" - - probed_height: Optional[ProbedHeightInfo] - loaded_volume: Optional[LoadedVolumeInfo] - probed_volume: Optional[ProbedVolumeInfo] - - -@dataclass(frozen=True) -class CurrentAddressableArea: - """The latest addressable area the robot has accessed.""" - - pipette_id: str - addressable_area_name: str - - -CurrentPipetteLocation = Union[CurrentWell, CurrentAddressableArea] - - -@dataclass(frozen=True) -class TipGeometry: - """Tip geometry data. - - Props: - length: The effective length (total length minus overlap) of a tip in mm. - diameter: Tip diameter in mm. - volume: Maximum volume in µL. - """ - - length: float - diameter: float - volume: float - - -class FluidKind(str, Enum): - """A kind of fluid that can be inside a pipette.""" - - LIQUID = "LIQUID" - AIR = "AIR" - - -@dataclass(frozen=True) -class AspiratedFluid: - """Fluid inside a pipette.""" - - kind: FluidKind - volume: float - - -class MovementAxis(str, Enum): - """Axis on which to issue a relative movement.""" - - X = "x" - Y = "y" - Z = "z" - - -class MotorAxis(str, Enum): - """Motor axis on which to issue a home command.""" - - X = "x" - Y = "y" - LEFT_Z = "leftZ" - RIGHT_Z = "rightZ" - LEFT_PLUNGER = "leftPlunger" - RIGHT_PLUNGER = "rightPlunger" - EXTENSION_Z = "extensionZ" - EXTENSION_JAW = "extensionJaw" - AXIS_96_CHANNEL_CAM = "axis96ChannelCam" - - -# TODO(mc, 2022-01-18): use opentrons_shared_data.module.types.ModuleModel -class ModuleModel(str, Enum): - """All available modules' models.""" - - TEMPERATURE_MODULE_V1 = "temperatureModuleV1" - TEMPERATURE_MODULE_V2 = "temperatureModuleV2" - MAGNETIC_MODULE_V1 = "magneticModuleV1" - MAGNETIC_MODULE_V2 = "magneticModuleV2" - THERMOCYCLER_MODULE_V1 = "thermocyclerModuleV1" - THERMOCYCLER_MODULE_V2 = "thermocyclerModuleV2" - HEATER_SHAKER_MODULE_V1 = "heaterShakerModuleV1" - MAGNETIC_BLOCK_V1 = "magneticBlockV1" - ABSORBANCE_READER_V1 = "absorbanceReaderV1" - FLEX_STACKER_MODULE_V1 = "flexStackerModuleV1" - - def as_type(self) -> ModuleType: - """Get the ModuleType of this model.""" - if ModuleModel.is_temperature_module_model(self): - return ModuleType.TEMPERATURE - elif ModuleModel.is_magnetic_module_model(self): - return ModuleType.MAGNETIC - elif ModuleModel.is_thermocycler_module_model(self): - return ModuleType.THERMOCYCLER - elif ModuleModel.is_heater_shaker_module_model(self): - return ModuleType.HEATER_SHAKER - elif ModuleModel.is_magnetic_block(self): - return ModuleType.MAGNETIC_BLOCK - elif ModuleModel.is_absorbance_reader(self): - return ModuleType.ABSORBANCE_READER - elif ModuleModel.is_flex_stacker(self): - return ModuleType.FLEX_STACKER - - assert False, f"Invalid ModuleModel {self}" - - @classmethod - def is_temperature_module_model( - cls, model: ModuleModel - ) -> TypeGuard[TemperatureModuleModel]: - """Whether a given model is a Temperature Module.""" - return model in [cls.TEMPERATURE_MODULE_V1, cls.TEMPERATURE_MODULE_V2] - - @classmethod - def is_magnetic_module_model( - cls, model: ModuleModel - ) -> TypeGuard[MagneticModuleModel]: - """Whether a given model is a Magnetic Module.""" - return model in [cls.MAGNETIC_MODULE_V1, cls.MAGNETIC_MODULE_V2] - - @classmethod - def is_thermocycler_module_model( - cls, model: ModuleModel - ) -> TypeGuard[ThermocyclerModuleModel]: - """Whether a given model is a Thermocycler Module.""" - return model in [cls.THERMOCYCLER_MODULE_V1, cls.THERMOCYCLER_MODULE_V2] - - @classmethod - def is_heater_shaker_module_model( - cls, model: ModuleModel - ) -> TypeGuard[HeaterShakerModuleModel]: - """Whether a given model is a Heater-Shaker Module.""" - return model == cls.HEATER_SHAKER_MODULE_V1 - - @classmethod - def is_magnetic_block(cls, model: ModuleModel) -> TypeGuard[MagneticBlockModel]: - """Whether a given model is a Magnetic block.""" - return model == cls.MAGNETIC_BLOCK_V1 - - @classmethod - def is_absorbance_reader( - cls, model: ModuleModel - ) -> TypeGuard[AbsorbanceReaderModel]: - """Whether a given model is an Absorbance Plate Reader.""" - return model == cls.ABSORBANCE_READER_V1 - - @classmethod - def is_flex_stacker(cls, model: ModuleModel) -> TypeGuard[FlexStackerModuleModel]: - """Whether a given model is a Flex Stacker..""" - return model == cls.FLEX_STACKER_MODULE_V1 - - -TemperatureModuleModel = Literal[ - ModuleModel.TEMPERATURE_MODULE_V1, ModuleModel.TEMPERATURE_MODULE_V2 -] -MagneticModuleModel = Literal[ - ModuleModel.MAGNETIC_MODULE_V1, ModuleModel.MAGNETIC_MODULE_V2 -] -ThermocyclerModuleModel = Literal[ - ModuleModel.THERMOCYCLER_MODULE_V1, ModuleModel.THERMOCYCLER_MODULE_V2 -] -HeaterShakerModuleModel = Literal[ModuleModel.HEATER_SHAKER_MODULE_V1] -MagneticBlockModel = Literal[ModuleModel.MAGNETIC_BLOCK_V1] -AbsorbanceReaderModel = Literal[ModuleModel.ABSORBANCE_READER_V1] -FlexStackerModuleModel = Literal[ModuleModel.FLEX_STACKER_MODULE_V1] - - -class ModuleDimensions(BaseModel): - """Dimension type for modules.""" - - bareOverallHeight: float - overLabwareHeight: float - lidHeight: Optional[float] = None - - -class Vec3f(BaseModel): - """A 3D vector of floats.""" - - x: float - y: float - z: float - - -# TODO(mm, 2022-11-07): Deduplicate with Vec3f. -class ModuleCalibrationPoint(BaseModel): - """Calibration Point type for module definition.""" - - x: float - y: float - z: float - - -# TODO(mm, 2022-11-07): Deduplicate with Vec3f. -class LabwareOffsetVector(BaseModel): - """Offset, in deck coordinates from nominal to actual position.""" - - x: float - y: float - z: float - - def __add__(self, other: Any) -> LabwareOffsetVector: - """Adds two vectors together.""" - if not isinstance(other, LabwareOffsetVector): - return NotImplemented - return LabwareOffsetVector( - x=self.x + other.x, y=self.y + other.y, z=self.z + other.z - ) - - def __sub__(self, other: Any) -> LabwareOffsetVector: - """Subtracts two vectors.""" - if not isinstance(other, LabwareOffsetVector): - return NotImplemented - return LabwareOffsetVector( - x=self.x - other.x, y=self.y - other.y, z=self.z - other.z - ) - - -# TODO(mm, 2022-11-07): Deduplicate with Vec3f. -class InstrumentOffsetVector(BaseModel): - """Instrument Offset from home position to robot deck.""" - - x: float - y: float - z: float - - -# TODO(mm, 2022-11-07): Deduplicate with Vec3f. -class ModuleOffsetVector(BaseModel): - """Offset, in deck coordinates, from nominal to actual position of labware on a module.""" - - x: float - y: float - z: float - - -@dataclass -class ModuleOffsetData: - """Module calibration offset data.""" - - moduleOffsetVector: ModuleOffsetVector - location: DeckSlotLocation - - -class OverlapOffset(Vec3f): - """Offset representing overlap space of one labware on top of another labware or module.""" - - -class AddressableOffsetVector(Vec3f): - """Offset, in deck coordinates, from nominal to actual position of an addressable area.""" - - -class LabwareMovementOffsetData(BaseModel): - """Offsets to be used during labware movement.""" - - pickUpOffset: LabwareOffsetVector - dropOffset: LabwareOffsetVector - - -# TODO(mm, 2023-04-13): Move to shared-data, so this binding can be maintained alongside the JSON -# schema that it's sourced from. We already do that for labware definitions and JSON protocols. -class ModuleDefinition(BaseModel): - """A module definition conforming to module definition schema v3.""" - - # Note: This field is misleading. - # - # This class only models v3 definitions ("module/schemas/3"), not v2 ("module/schemas/2"). - # labwareOffset is required to have a z-component, for example. - # - # When parsing from a schema v3 JSON definition into this model, - # the definition's `"$otSharedSchema": "module/schemas/3"` field will be thrown away - # because it has a dollar sign, which doesn't match this field. - # Then, this field will default to "module/schemas/2", because no value was provided. - # - # We should fix this field once Jira RSS-221 is resolved. RSS-221 makes it difficult to fix - # because robot-server has been storing and loading these bad fields in its database. - otSharedSchema: str = Field("module/schemas/2", description="The current schema.") - - moduleType: ModuleType = Field( - ..., - description="Module type (Temperature/Magnetic/Thermocycler)", - ) - - model: ModuleModel = Field(..., description="Model name of the module") - - labwareOffset: LabwareOffsetVector = Field( - ..., - description="Labware offset in x, y, z.", - ) - - dimensions: ModuleDimensions = Field(..., description="Module dimension") - - calibrationPoint: ModuleCalibrationPoint = Field( - ..., - description="Calibration point of module.", - ) - - displayName: str = Field(..., description="Display name.") - - quirks: List[str] = Field(..., description="Module quirks") - - # In releases prior to https://github.com/Opentrons/opentrons/pull/11873 (v6.3.0), - # the matrices in slotTransforms were 3x3. - # After, they are 4x4, even though there was no schema version bump. - # - # Because old objects of this class, with the 3x3 matrices, were stored in robot-server's - # database, this field needs to stay typed loosely enough to support both sizes. - # We can fix this once Jira RSS-221 is resolved. - slotTransforms: Dict[str, Any] = Field( - ..., - description="Dictionary of transforms for each slot.", - ) - - compatibleWith: List[ModuleModel] = Field( - ..., - description="List of module models this model is compatible with.", - ) - gripperOffsets: Optional[Dict[str, LabwareMovementOffsetData]] = Field( - default_factory=dict, - description="Offsets to use for labware movement using gripper", - ) - - -class LoadedModule(BaseModel): - """A module that has been loaded.""" - - id: str - model: ModuleModel - location: Optional[DeckSlotLocation] = None - serialNumber: Optional[str] = None - - -class LabwareOffsetLocation(BaseModel): - """Parameters describing when a given offset may apply to a given labware load.""" - - slotName: DeckSlotName = Field( - ..., - description=( - "The deck slot where the protocol will load the labware." - " Or, if the protocol will load the labware on a module," - " the deck slot where the protocol will load that module." - "\n\n" - # This description should be kept in sync with DeckSlotLocation.slotName. - 'The plain numbers like `"5"` are for the OT-2,' - ' and the coordinates like `"C2"` are for the Flex.' - "\n\n" - "When you provide one of these values, you can use either style." - " It will automatically be converted to match the robot." - "\n\n" - "When one of these values is returned, it will always match the robot." - ), - ) - moduleModel: Optional[ModuleModel] = Field( - None, - description=( - "The model of the module that the labware will be loaded onto," - " if applicable." - "\n\n" - "Because of module compatibility, the model that the protocol requests" - " may not be exactly the same" - " as what it will find physically connected during execution." - " For this labware offset to apply," - " this field must be the *requested* model, not the connected one." - " You can retrieve this from a `loadModule` command's `params.model`" - " in the protocol's analysis." - ), - ) - definitionUri: Optional[str] = Field( - None, - description=( - "The definition URI of a labware that a labware can be loaded onto," - " if applicable." - "\n\n" - "This can be combined with moduleModel if the labware is loaded on top of" - " an adapter that is loaded on a module." - ), - ) - - -class LabwareOffset(BaseModel): - """An offset that the robot adds to a pipette's position when it moves to a labware. - - During the run, if a labware is loaded whose definition URI and location - both match what's found here, the given offset will be added to all - pipette movements that use that labware as a reference point. - """ - - id: str = Field(..., description="Unique labware offset record identifier.") - createdAt: datetime = Field(..., description="When this labware offset was added.") - definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( - ..., - description="Where the labware is located on the robot.", - ) - vector: LabwareOffsetVector = Field( - ..., - description="The offset applied to matching labware.", - ) - - -class LabwareOffsetCreate(BaseModel): - """Create request data for a labware offset.""" - - definitionUri: str = Field(..., description="The URI for the labware's definition.") - location: LabwareOffsetLocation = Field( - ..., - description="Where the labware is located on the robot.", - ) - vector: LabwareOffsetVector = Field( - ..., - description="The offset applied to matching labware.", - ) - - -class LoadedLabware(BaseModel): - """A labware that has been loaded.""" - - id: str - loadName: str - definitionUri: str - location: LabwareLocation = Field( - ..., description="The labware's current location." - ) - lid_id: Optional[str] = Field( - None, - description=("Labware ID of a Lid currently loaded on top of the labware."), - ) - offsetId: Optional[str] = Field( - None, - description=( - "An ID referencing the labware offset" - " that applies to this labware placement." - " Null or undefined means no offset was provided for this load," - " so the default of (0, 0, 0) will be used." - ), - ) - displayName: Optional[str] = Field( - None, - description="A user-specified display name for this labware, if provided.", - ) - - -class HexColor(RootModel[str]): - """Hex color representation.""" - - root: str = Field(pattern=r"^#(?:[0-9a-fA-F]{3,4}){1,2}$") - - -EmptyLiquidId = Literal["EMPTY"] -LiquidId = str | EmptyLiquidId - - -class Liquid(BaseModel): - """Payload required to create a liquid.""" - - id: str - displayName: str - description: str - displayColor: Optional[HexColor] = None - - -class LiquidClassRecord(ByTipTypeSetting, frozen=True): - """LiquidClassRecord is our internal representation of an (immutable) liquid class. - - Conceptually, a liquid class record is the tuple (name, pipette, tip, transfer properties). - We consider two liquid classes to be the same if every entry in that tuple is the same; and liquid - classes are different if any entry in the tuple is different. - - This class defines the tuple via inheritance so that we can reuse the definitions from shared_data. - """ - - liquidClassName: str = Field( - ..., - description="Identifier for the liquid of this liquid class, e.g. glycerol50.", - ) - pipetteModel: str = Field( - ..., - description="Identifier for the pipette of this liquid class.", - ) - # The other fields like tiprack ID, aspirate properties, etc. are pulled in from ByTipTypeSetting. - - def __hash__(self) -> int: - """Hash function for LiquidClassRecord.""" - # Within the Protocol Engine, LiquidClassRecords are immutable, and we'd like to be able to - # look up LiquidClassRecords by value, which involves hashing. However, Pydantic does not - # generate a usable hash function if any of the subfields (like Coordinate) are not frozen. - # So we have to implement the hash function ourselves. - # Our strategy is to recursively convert this object into a list of (key, value) tuples. - def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: - return tuple( - ( - field_name, - dict_to_tuple(value) - if isinstance(value, dict) - else tuple(value) - if isinstance(value, list) - else value, - ) - for field_name, value in d.items() - ) - - return hash(dict_to_tuple(self.model_dump())) - - -class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): - """A LiquidClassRecord with its ID, for use in summary lists.""" - - liquidClassId: str = Field( - ..., - description="Unique identifier for this liquid class.", - ) - - -class SpeedRange(NamedTuple): - """Minimum and maximum allowed speeds for a shaking module.""" - - min: int - max: int - - -class TemperatureRange(NamedTuple): - """Minimum and maximum allowed temperatures for a heating module.""" - - min: float - max: float - - -class HeaterShakerLatchStatus(Enum): - """Heater-Shaker latch status for determining pipette and labware movement errors.""" - - CLOSED = "closed" - OPEN = "open" - UNKNOWN = "unknown" - - -@dataclass(frozen=True) -class HeaterShakerMovementRestrictors: - """Shaking status, latch status and slot location for determining movement restrictions.""" - - plate_shaking: bool - latch_status: HeaterShakerLatchStatus - deck_slot: int - - -class LabwareMovementStrategy(str, Enum): - """Strategy to use for labware movement.""" - - USING_GRIPPER = "usingGripper" - MANUAL_MOVE_WITH_PAUSE = "manualMoveWithPause" - MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause" - - -@dataclass(frozen=True) -class PotentialCutoutFixture: - """Cutout and cutout fixture id associated with a potential cutout fixture that can be on the deck.""" - - cutout_id: str - cutout_fixture_id: str - provided_addressable_areas: FrozenSet[str] - - -class AreaType(Enum): - """The type of addressable area.""" - - SLOT = "slot" - STAGING_SLOT = "stagingSlot" - MOVABLE_TRASH = "movableTrash" - FIXED_TRASH = "fixedTrash" - WASTE_CHUTE = "wasteChute" - THERMOCYCLER = "thermocycler" - HEATER_SHAKER = "heaterShaker" - TEMPERATURE = "temperatureModule" - MAGNETICBLOCK = "magneticBlock" - ABSORBANCE_READER = "absorbanceReader" - FLEX_STACKER = "flexStacker" - LID_DOCK = "lidDock" - - -@dataclass(frozen=True) -class AddressableArea: - """Addressable area that has been loaded.""" - - area_name: str - area_type: AreaType - base_slot: DeckSlotName - display_name: str - bounding_box: Dimensions - position: AddressableOffsetVector - compatible_module_types: List[SharedDataModuleType] - - -class PostRunHardwareState(Enum): - """State of robot gantry & motors after a stop is performed and the hardware API is reset. - - HOME_AND_STAY_ENGAGED: home the gantry and keep all motors engaged. This allows the - robot to continue performing movement actions without re-homing - HOME_THEN_DISENGAGE: home the gantry and then disengage motors. - Reduces current consumption of the motors and prevents coil heating. - Re-homing is required to re-engage the motors and resume robot movement. - STAY_ENGAGED_IN_PLACE: do not home after the stop and keep the motors engaged. - Keeps gantry in the same position as prior to `stop()` execution - and allows the robot to execute movement commands without requiring to re-home first. - DISENGAGE_IN_PLACE: disengage motors and do not home the robot - Probable states for pipette: - - for 1- or 8-channel: - - HOME_AND_STAY_ENGAGED after protocol runs - - STAY_ENGAGED_IN_PLACE after maintenance runs - - for 96-channel: - - HOME_THEN_DISENGAGE after protocol runs - - DISENGAGE_IN_PLACE after maintenance runs - """ - - HOME_AND_STAY_ENGAGED = "homeAndStayEngaged" - HOME_THEN_DISENGAGE = "homeThenDisengage" - STAY_ENGAGED_IN_PLACE = "stayEngagedInPlace" - DISENGAGE_IN_PLACE = "disengageInPlace" - - -NOZZLE_NAME_REGEX = r"[A-Z]\d{1,2}" -PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"] - - -class AllNozzleLayoutConfiguration(BaseModel): - """All basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default.""" - - style: Literal["ALL"] = "ALL" - - -class SingleNozzleLayoutConfiguration(BaseModel): - """Minimum information required for a new nozzle configuration.""" - - style: Literal["SINGLE"] = "SINGLE" - primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( - ..., - description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - ) - - -class RowNozzleLayoutConfiguration(BaseModel): - """Minimum information required for a new nozzle configuration.""" - - style: Literal["ROW"] = "ROW" - primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( - ..., - description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - ) - - -class ColumnNozzleLayoutConfiguration(BaseModel): - """Information required for nozzle configurations of type ROW and COLUMN.""" - - style: Literal["COLUMN"] = "COLUMN" - primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( - ..., - description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - ) - - -class QuadrantNozzleLayoutConfiguration(BaseModel): - """Information required for nozzle configurations of type QUADRANT.""" - - style: Literal["QUADRANT"] = "QUADRANT" - primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( - ..., - description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", - ) - frontRightNozzle: str = Field( - ..., - pattern=NOZZLE_NAME_REGEX, - description="The front right nozzle in your configuration.", - ) - backLeftNozzle: str = Field( - ..., - pattern=NOZZLE_NAME_REGEX, - description="The back left nozzle in your configuration.", - ) - - -NozzleLayoutConfigurationType = Union[ - AllNozzleLayoutConfiguration, - SingleNozzleLayoutConfiguration, - ColumnNozzleLayoutConfiguration, - RowNozzleLayoutConfiguration, - QuadrantNozzleLayoutConfiguration, -] - -# TODO make the below some sort of better type -# TODO This should instead contain a proper cutout fixture type -DeckConfigurationType = List[ - Tuple[str, str, Optional[str]] -] # cutout_id, cutout_fixture_id, opentrons_module_serial_number - - -class InstrumentSensorId(str, Enum): - """Primary and secondary sensor ids.""" - - PRIMARY = "primary" - SECONDARY = "secondary" - BOTH = "both" - - def to_instrument_probe_type(self) -> InstrumentProbeType: - """Convert to InstrumentProbeType.""" - return { - InstrumentSensorId.PRIMARY: InstrumentProbeType.PRIMARY, - InstrumentSensorId.SECONDARY: InstrumentProbeType.SECONDARY, - InstrumentSensorId.BOTH: InstrumentProbeType.BOTH, - }[self] - - -class TipPresenceStatus(str, Enum): - """Tip presence status reported by a pipette.""" - - PRESENT = "present" - ABSENT = "absent" - UNKNOWN = "unknown" - - def to_hw_state(self) -> HwTipStateType: - """Convert to hardware tip state.""" - assert self != TipPresenceStatus.UNKNOWN - return { - TipPresenceStatus.PRESENT: HwTipStateType.PRESENT, - TipPresenceStatus.ABSENT: HwTipStateType.ABSENT, - }[self] - - @classmethod - def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": - """Convert from hardware tip state.""" - return { - HwTipStateType.PRESENT: TipPresenceStatus.PRESENT, - HwTipStateType.ABSENT: TipPresenceStatus.ABSENT, - }[state] - - -class NextTipInfo(BaseModel): - """Next available tip labware and well name data.""" - - labwareId: str = Field( - ..., - description="The labware ID of the tip rack where the next available tip(s) are located.", - ) - tipStartingWell: str = Field( - ..., description="The (starting) well name of the next available tip(s)." - ) - - -class NoTipReason(Enum): - """The cause of no tip being available for a pipette and tip rack(s).""" - - NO_AVAILABLE_TIPS = "noAvailableTips" - STARTING_TIP_WITH_PARTIAL = "startingTipWithPartial" - INCOMPATIBLE_CONFIGURATION = "incompatibleConfiguration" - - -class NoTipAvailable(BaseModel): - """No available next tip data.""" - - noTipReason: NoTipReason = Field( - ..., description="The reason why no next available tip could be provided." - ) - message: Optional[str] = Field( - None, description="Optional message explaining why a tip wasn't available." - ) - - -class BaseCommandAnnotation(BaseModel): - """Optional annotations for protocol engine commands.""" - - commandKeys: List[str] = Field( - ..., description="Command keys to which this annotation applies" - ) - annotationType: str = Field( - ..., description="The type of annotation (for machine parsing)" - ) - - -class SecondOrderCommandAnnotation(BaseCommandAnnotation): - """Annotates a group of atomic commands which were the direct result of a second order command. - - Examples of second order commands would be transfer, consolidate, mix, etc. - """ - - annotationType: Literal["secondOrderCommand"] = "secondOrderCommand" - params: Dict[str, Any] = Field( - ..., - description="Key value pairs of the parameters passed to the second order command that this annotates.", - ) - machineReadableName: str = Field( - ..., - description="The name of the second order command in the form that the generating software refers to it", - ) - userSpecifiedName: Optional[str] = Field( - None, description="The optional user-specified name of the second order command" - ) - userSpecifiedDescription: Optional[str] = Field( - None, - description="The optional user-specified description of the second order command", - ) - - -class CustomCommandAnnotation(BaseCommandAnnotation): - """Annotates a group of atomic commands in some manner that Opentrons software does not anticipate or originate.""" - - annotationType: Literal["custom"] = "custom" - model_config = ConfigDict(extra="allow") - - -CommandAnnotation = Union[SecondOrderCommandAnnotation, CustomCommandAnnotation] - - -# TODO (spp, 2024-04-02): move all RTP types to runner -class RTPBase(BaseModel): - """Parameters defined in a protocol.""" - - displayName: StrictStr = Field(..., description="Display string for the parameter.") - variableName: StrictStr = Field( - ..., description="Python variable name of the parameter." - ) - description: Optional[StrictStr] = Field( - None, description="Detailed description of the parameter." - ) - suffix: Optional[StrictStr] = Field( - None, - description="Units (like mL, mm/sec, etc) or a custom suffix for the parameter.", - ) - - -class NumberParameter(RTPBase): - """An integer parameter defined in a protocol.""" - - type: Literal["int", "float"] = Field( - ..., description="String specifying whether the number is an int or float type." - ) - min: Union[StrictInt, StrictFloat] = Field( - ..., description="Minimum value that the number param is allowed to have." - ) - max: Union[StrictInt, StrictFloat] = Field( - ..., description="Maximum value that the number param is allowed to have." - ) - value: Union[StrictInt, StrictFloat] = Field( - ..., - description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", - ) - default: Union[StrictInt, StrictFloat] = Field( - ..., - description="Default value of the parameter, to be used when there is no client-specified value.", - ) - - -class BooleanParameter(RTPBase): - """A boolean parameter defined in a protocol.""" - - type: Literal["bool"] = Field( - default="bool", description="String specifying the type of this parameter" - ) - value: StrictBool = Field( - ..., - description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", - ) - default: StrictBool = Field( - ..., - description="Default value of the parameter, to be used when there is no client-specified value.", - ) - - -class EnumChoice(BaseModel): - """Components of choices used in RTP Enum Parameters.""" - - displayName: StrictStr = Field( - ..., description="Display string for the param's choice." - ) - value: Union[StrictInt, StrictFloat, StrictStr] = Field( - ..., description="Enum value of the param's choice." - ) - - -class EnumParameter(RTPBase): - """A string enum defined in a protocol.""" - - type: Literal["int", "float", "str"] = Field( - ..., - description="String specifying whether the parameter is an int or float or string type.", - ) - choices: List[EnumChoice] = Field( - ..., description="List of valid choices for this parameter." - ) - value: Union[StrictInt, StrictFloat, StrictStr] = Field( - ..., - description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", - ) - default: Union[StrictInt, StrictFloat, StrictStr] = Field( - ..., - description="Default value of the parameter, to be used when there is no client-specified value.", - ) - - -class FileInfo(BaseModel): - """A file UUID descriptor.""" - - id: str = Field( - ..., - description="The UUID identifier of the file stored on the robot.", - ) - name: str = Field(..., description="Name of the file, including the extension.") - - -class CSVParameter(RTPBase): - """A CSV file parameter defined in a protocol.""" - - type: Literal["csv_file"] = Field( - default="csv_file", description="String specifying the type of this parameter" - ) - file: Optional[FileInfo] = Field( - default=None, - description="ID of the CSV file stored on the robot; to be used for fetching the CSV file." - " For local analysis this will most likely be empty.", - ) - - -RunTimeParameter = Union[NumberParameter, EnumParameter, BooleanParameter, CSVParameter] - -PrimitiveRunTimeParamValuesType = Mapping[ - StrictStr, Union[StrictInt, StrictFloat, StrictBool, StrictStr] -] # update value types as more RTP types are added - -CSVRunTimeParamFilesType = Mapping[StrictStr, StrictStr] -CSVRuntimeParamPaths = Dict[str, Path] - - -ABSMeasureMode = Literal["single", "multi"] diff --git a/api/src/opentrons/protocol_engine/types/__init__.py b/api/src/opentrons/protocol_engine/types/__init__.py new file mode 100644 index 00000000000..9bf512a7a29 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/__init__.py @@ -0,0 +1,252 @@ +"""Public protocol engine value types and models.""" + +from __future__ import annotations + + +from opentrons_shared_data.pipette.types import LabwareUri +from opentrons.hardware_control.modules import ModuleType + + +from .run_time_parameters import ( + NumberParameter, + BooleanParameter, + EnumParameter, + CSVParameter, + RunTimeParameter, + PrimitiveRunTimeParamValuesType, + CSVRunTimeParamFilesType, + CSVRuntimeParamPaths, + FileInfo, + EnumChoice, +) + +from .command_annotations import ( + SecondOrderCommandAnnotation, + CustomCommandAnnotation, + CommandAnnotation, +) +from .partial_tip_configuration import ( + AllNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, + NozzleLayoutConfigurationType, + PRIMARY_NOZZLE_LITERAL, +) +from .automatic_tip_selection import NextTipInfo, NoTipReason, NoTipAvailable +from .instrument_sensors import InstrumentSensorId, TipPresenceStatus +from .deck_configuration import ( + AddressableOffsetVector, + PotentialCutoutFixture, + AreaType, + AddressableArea, + DeckConfigurationType, + DeckType, +) +from .liquid_class import LiquidClassRecord, LiquidClassRecordWithId +from .module import ( + ModuleModel, + TemperatureModuleModel, + MagneticModuleModel, + ThermocyclerModuleModel, + HeaterShakerModuleModel, + MagneticBlockModel, + AbsorbanceReaderModel, + FlexStackerModuleModel, + ModuleDimensions, + ModuleCalibrationPoint, + ModuleDefinition, + LoadedModule, + SpeedRange, + TemperatureRange, + HeaterShakerLatchStatus, + HeaterShakerMovementRestrictors, + ABSMeasureMode, + ModuleOffsetVector, + ModuleOffsetData, +) +from .location import ( + DeckSlotLocation, + StagingSlotLocation, + AddressableAreaLocation, + ModuleLocation, + OnLabwareLocation, + OFF_DECK_LOCATION, + SYSTEM_LOCATION, + LabwareLocation, + OnDeckLabwareLocation, + NonStackedLocation, + DeckPoint, +) +from .labware import ( + OverlapOffset, + LabwareOffset, + LabwareOffsetCreate, + LoadedLabware, +) +from .liquid import HexColor, EmptyLiquidId, LiquidId, Liquid, FluidKind, AspiratedFluid +from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_vector import LabwareOffsetVector +from .well_position import ( + WellOrigin, + PickUpTipWellOrigin, + DropTipWellOrigin, + WellOffset, + WellLocation, + LiquidHandlingWellLocation, + PickUpTipWellLocation, + DropTipWellLocation, +) +from .instrument import ( + LoadedPipette, + CurrentAddressableArea, + CurrentWell, + CurrentPipetteLocation, + InstrumentOffsetVector, +) +from .execution import EngineStatus, PostRunHardwareState +from .liquid_level_detection import ( + LoadedVolumeInfo, + ProbedHeightInfo, + ProbedVolumeInfo, + WellInfoSummary, + WellLiquidInfo, +) +from .liquid_handling import FlowRates +from .labware_movement import LabwareMovementStrategy, LabwareMovementOffsetData +from .tip import TipGeometry +from .hardware_passthrough import MovementAxis, MotorAxis +from .util import Vec3f, Dimensions + +__all__ = [ + # Runtime parameters + "NumberParameter", + "BooleanParameter", + "EnumParameter", + "EnumChoice", + "CSVParameter", + "PrimitiveRunTimeParamValuesType", + "CSVRunTimeParamFilesType", + "CSVRuntimeParamPaths", + "FileInfo", + "RunTimeParameter", + # Command annotations + "SecondOrderCommandAnnotation", + "CustomCommandAnnotation", + "CommandAnnotation", + # Partial tip handling + "AllNozzleLayoutConfiguration", + "SingleNozzleLayoutConfiguration", + "RowNozzleLayoutConfiguration", + "ColumnNozzleLayoutConfiguration", + "QuadrantNozzleLayoutConfiguration", + "NozzleLayoutConfigurationType", + "PRIMARY_NOZZLE_LITERAL", + # Automatic tip selection + "NextTipInfo", + "NoTipReason", + "NoTipAvailable", + # Instrument sensors + "InstrumentSensorId", + "TipPresenceStatus", + # Deck configuration + "AddressableOffsetVector", + "PotentialCutoutFixture", + "AreaType", + "AddressableArea", + "DeckConfigurationType", + "DeckType", + # Liquid classes + "LiquidClassRecord", + "LiquidClassRecordWithId", + # Modules + "ModuleModel", + "ModuleType", + "TemperatureModuleModel", + "MagneticModuleModel", + "ThermocyclerModuleModel", + "HeaterShakerModuleModel", + "MagneticBlockModel", + "AbsorbanceReaderModel", + "FlexStackerModuleModel", + "ModuleDimensions", + "ModuleCalibrationPoint", + "ModuleDefinition", + "LoadedModule", + "SpeedRange", + "TemperatureRange", + "HeaterShakerLatchStatus", + "HeaterShakerMovementRestrictors", + "ABSMeasureMode", + "ModuleOffsetVector", + "ModuleOffsetData", + # Locations of things on deck + "DeckSlotLocation", + "StagingSlotLocation", + "AddressableAreaLocation", + "ModuleLocation", + "OnLabwareLocation", + "OFF_DECK_LOCATION", + "SYSTEM_LOCATION", + "LabwareLocation", + "OnDeckLabwareLocation", + "NonStackedLocation", + "DeckPoint", + # Labware offset location + "LabwareOffsetLocation", + # Labware offset vector + "LabwareOffsetVector", + # Labware + "OverlapOffset", + "LabwareOffset", + "LabwareOffsetCreate", + "LoadedLabware", + "LabwareOffsetVector", + # Liquids + "HexColor", + "EmptyLiquidId", + "LiquidId", + "Liquid", + "FluidKind", + "AspiratedFluid", + # Well locations + "WellOrigin", + "PickUpTipWellOrigin", + "DropTipWellOrigin", + "WellOffset", + "WellLocation", + "LiquidHandlingWellLocation", + "PickUpTipWellLocation", + "DropTipWellLocation", + # Execution + "EngineStatus", + "PostRunHardwareState", + # Instruments + "LoadedPipette", + "CurrentAddressableArea", + "CurrentWell", + "CurrentPipetteLocation", + "InstrumentOffsetVector", + # Liquid level detection types + "LoadedVolumeInfo", + "ProbedHeightInfo", + "ProbedVolumeInfo", + "WellInfoSummary", + "WellLiquidInfo", + # Liquid handling + "FlowRates", + # Labware movement + "LabwareMovementStrategy", + "LabwareMovementOffsetData", + # Tips + "TipGeometry", + # Hardware passthrough + "MovementAxis", + "MotorAxis", + # Utility types + "Vec3f", + "Dimensions", + # Convenience re-export + "LabwareUri", +] diff --git a/api/src/opentrons/protocol_engine/types/automatic_tip_selection.py b/api/src/opentrons/protocol_engine/types/automatic_tip_selection.py new file mode 100644 index 00000000000..982c40c884d --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/automatic_tip_selection.py @@ -0,0 +1,39 @@ +"""Protocol engine types dealing with automatic tip selection.""" +from enum import Enum +from typing import Optional + +from pydantic import ( + BaseModel, + Field, +) + + +class NextTipInfo(BaseModel): + """Next available tip labware and well name data.""" + + labwareId: str = Field( + ..., + description="The labware ID of the tip rack where the next available tip(s) are located.", + ) + tipStartingWell: str = Field( + ..., description="The (starting) well name of the next available tip(s)." + ) + + +class NoTipReason(Enum): + """The cause of no tip being available for a pipette and tip rack(s).""" + + NO_AVAILABLE_TIPS = "noAvailableTips" + STARTING_TIP_WITH_PARTIAL = "startingTipWithPartial" + INCOMPATIBLE_CONFIGURATION = "incompatibleConfiguration" + + +class NoTipAvailable(BaseModel): + """No available next tip data.""" + + noTipReason: NoTipReason = Field( + ..., description="The reason why no next available tip could be provided." + ) + message: Optional[str] = Field( + None, description="Optional message explaining why a tip wasn't available." + ) diff --git a/api/src/opentrons/protocol_engine/types/command_annotations.py b/api/src/opentrons/protocol_engine/types/command_annotations.py new file mode 100644 index 00000000000..f1174387bf0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/command_annotations.py @@ -0,0 +1,53 @@ +"""Protocol Engine types dealing with command annotations.""" + +from typing import List, Literal, Dict, Any, Optional, Union +from pydantic import ( + ConfigDict, + BaseModel, + Field, +) + + +class BaseCommandAnnotation(BaseModel): + """Optional annotations for protocol engine commands.""" + + commandKeys: List[str] = Field( + ..., description="Command keys to which this annotation applies" + ) + annotationType: str = Field( + ..., description="The type of annotation (for machine parsing)" + ) + + +class SecondOrderCommandAnnotation(BaseCommandAnnotation): + """Annotates a group of atomic commands which were the direct result of a second order command. + + Examples of second order commands would be transfer, consolidate, mix, etc. + """ + + annotationType: Literal["secondOrderCommand"] = "secondOrderCommand" + params: Dict[str, Any] = Field( + ..., + description="Key value pairs of the parameters passed to the second order command that this annotates.", + ) + machineReadableName: str = Field( + ..., + description="The name of the second order command in the form that the generating software refers to it", + ) + userSpecifiedName: Optional[str] = Field( + None, description="The optional user-specified name of the second order command" + ) + userSpecifiedDescription: Optional[str] = Field( + None, + description="The optional user-specified description of the second order command", + ) + + +class CustomCommandAnnotation(BaseCommandAnnotation): + """Annotates a group of atomic commands in some manner that Opentrons software does not anticipate or originate.""" + + annotationType: Literal["custom"] = "custom" + model_config = ConfigDict(extra="allow") + + +CommandAnnotation = Union[SecondOrderCommandAnnotation, CustomCommandAnnotation] diff --git a/api/src/opentrons/protocol_engine/types/deck_configuration.py b/api/src/opentrons/protocol_engine/types/deck_configuration.py new file mode 100644 index 00000000000..f9bdccc402c --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/deck_configuration.py @@ -0,0 +1,72 @@ +"""Protocol engine types to do with deck configuration.""" + +from dataclasses import dataclass +from typing import FrozenSet, List, Tuple, Optional +from enum import Enum + +from opentrons.types import DeckSlotName + +from opentrons_shared_data.module.types import ModuleType as SharedDataModuleType + +from .util import Vec3f, Dimensions + + +class AddressableOffsetVector(Vec3f): + """Offset, in deck coordinates, from nominal to actual position of an addressable area.""" + + +@dataclass(frozen=True) +class PotentialCutoutFixture: + """Cutout and cutout fixture id associated with a potential cutout fixture that can be on the deck.""" + + cutout_id: str + cutout_fixture_id: str + provided_addressable_areas: FrozenSet[str] + + +class AreaType(Enum): + """The type of addressable area.""" + + SLOT = "slot" + STAGING_SLOT = "stagingSlot" + MOVABLE_TRASH = "movableTrash" + FIXED_TRASH = "fixedTrash" + WASTE_CHUTE = "wasteChute" + THERMOCYCLER = "thermocycler" + HEATER_SHAKER = "heaterShaker" + TEMPERATURE = "temperatureModule" + MAGNETICBLOCK = "magneticBlock" + ABSORBANCE_READER = "absorbanceReader" + FLEX_STACKER = "flexStacker" + LID_DOCK = "lidDock" + + +@dataclass(frozen=True) +class AddressableArea: + """Addressable area that has been loaded.""" + + area_name: str + area_type: AreaType + base_slot: DeckSlotName + display_name: str + bounding_box: Dimensions + position: AddressableOffsetVector + compatible_module_types: List[SharedDataModuleType] + + +# TODO make the below some sort of better type +# TODO This should instead contain a proper cutout fixture type +DeckConfigurationType = List[ + Tuple[str, str, Optional[str]] +] # cutout_id, cutout_fixture_id, opentrons_module_serial_number + + +# TODO(mm, 2023-05-10): Deduplicate with constants in +# opentrons.protocols.api_support.deck_type +# and consider moving to shared-data. +class DeckType(str, Enum): + """Types of deck available.""" + + OT2_STANDARD = "ot2_standard" + OT2_SHORT_TRASH = "ot2_short_trash" + OT3_STANDARD = "ot3_standard" diff --git a/api/src/opentrons/protocol_engine/types/execution.py b/api/src/opentrons/protocol_engine/types/execution.py new file mode 100644 index 00000000000..1f96391254c --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/execution.py @@ -0,0 +1,96 @@ +"""Protocol engine types to do with engine execution.""" +from enum import Enum + + +# todo(mm, 2024-06-24): This monolithic status field is getting to be a bit much. +# We should consider splitting this up into multiple fields. +class EngineStatus(str, Enum): + """Current execution status of a ProtocolEngine. + + This is a high-level summary of what the robot is doing and what interactions are + appropriate. + """ + + # Statuses for an ongoing run: + + IDLE = "idle" + """The protocol has not been started yet. + + The robot may truly be idle, or it may be executing commands with `intent: "setup"`. + """ + + RUNNING = "running" + """The engine is actively running the protocol.""" + + PAUSED = "paused" + """A pause has been requested. Activity is paused, or will pause soon. + + (There is currently no way to tell which.) + """ + + BLOCKED_BY_OPEN_DOOR = "blocked-by-open-door" + """The robot's door is open. Activity is paused, or will pause soon.""" + + STOP_REQUESTED = "stop-requested" + """A stop has been requested. Activity will stop soon.""" + + FINISHING = "finishing" + """The robot is doing post-run cleanup, like homing and dropping tips.""" + + # Statuses for error recovery mode: + + AWAITING_RECOVERY = "awaiting-recovery" + """The engine is waiting for external input to recover from a nonfatal error. + + New commands with `intent: "fixit"` may be enqueued, which will run immediately. + The run can't be paused in this state, but it can be canceled, or resumed from the + next protocol command if recovery is complete. + """ + + AWAITING_RECOVERY_PAUSED = "awaiting-recovery-paused" + """The engine is paused while in error recovery mode. Activity is paused, or will pause soon. + + This state is not possible to enter manually. It happens when an open door + gets closed during error recovery. + """ + + AWAITING_RECOVERY_BLOCKED_BY_OPEN_DOOR = "awaiting-recovery-blocked-by-open-door" + """The robot's door is open while in recovery mode. Activity is paused, or will pause soon.""" + + # Terminal statuses: + + STOPPED = "stopped" + """All activity is over; it was stopped by an explicit external request.""" + + FAILED = "failed" + """All activity is over; there was a fatal error.""" + + SUCCEEDED = "succeeded" + """All activity is over; things completed without any fatal error.""" + + +class PostRunHardwareState(Enum): + """State of robot gantry & motors after a stop is performed and the hardware API is reset. + + HOME_AND_STAY_ENGAGED: home the gantry and keep all motors engaged. This allows the + robot to continue performing movement actions without re-homing + HOME_THEN_DISENGAGE: home the gantry and then disengage motors. + Reduces current consumption of the motors and prevents coil heating. + Re-homing is required to re-engage the motors and resume robot movement. + STAY_ENGAGED_IN_PLACE: do not home after the stop and keep the motors engaged. + Keeps gantry in the same position as prior to `stop()` execution + and allows the robot to execute movement commands without requiring to re-home first. + DISENGAGE_IN_PLACE: disengage motors and do not home the robot + Probable states for pipette: + - for 1- or 8-channel: + - HOME_AND_STAY_ENGAGED after protocol runs + - STAY_ENGAGED_IN_PLACE after maintenance runs + - for 96-channel: + - HOME_THEN_DISENGAGE after protocol runs + - DISENGAGE_IN_PLACE after maintenance runs + """ + + HOME_AND_STAY_ENGAGED = "homeAndStayEngaged" + HOME_THEN_DISENGAGE = "homeThenDisengage" + STAY_ENGAGED_IN_PLACE = "stayEngagedInPlace" + DISENGAGE_IN_PLACE = "disengageInPlace" diff --git a/api/src/opentrons/protocol_engine/types/hardware_passthrough.py b/api/src/opentrons/protocol_engine/types/hardware_passthrough.py new file mode 100644 index 00000000000..0d2b3b59f50 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/hardware_passthrough.py @@ -0,0 +1,25 @@ +"""Protocol Engine types for hardware passthrough.""" + +from enum import Enum + + +class MovementAxis(str, Enum): + """Axis on which to issue a relative movement.""" + + X = "x" + Y = "y" + Z = "z" + + +class MotorAxis(str, Enum): + """Motor axis on which to issue a home command.""" + + X = "x" + Y = "y" + LEFT_Z = "leftZ" + RIGHT_Z = "rightZ" + LEFT_PLUNGER = "leftPlunger" + RIGHT_PLUNGER = "rightPlunger" + EXTENSION_Z = "extensionZ" + EXTENSION_JAW = "extensionJaw" + AXIS_96_CHANNEL_CAM = "axis96ChannelCam" diff --git a/api/src/opentrons/protocol_engine/types/instrument.py b/api/src/opentrons/protocol_engine/types/instrument.py new file mode 100644 index 00000000000..2fb371d460b --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/instrument.py @@ -0,0 +1,47 @@ +"""Protocol Engine types to do with instruments.""" + +from typing import Union + +from dataclasses import dataclass + +from pydantic import BaseModel + +from opentrons.types import MountType +from opentrons_shared_data.pipette.types import PipetteNameType + + +class LoadedPipette(BaseModel): + """A pipette that has been loaded.""" + + id: str + pipetteName: PipetteNameType + mount: MountType + + +@dataclass(frozen=True) +class CurrentAddressableArea: + """The latest addressable area the robot has accessed.""" + + pipette_id: str + addressable_area_name: str + + +@dataclass(frozen=True) +class CurrentWell: + """The latest well that the robot has accessed.""" + + pipette_id: str + labware_id: str + well_name: str + + +CurrentPipetteLocation = Union[CurrentWell, CurrentAddressableArea] + + +# TODO(mm, 2022-11-07): Deduplicate with Vec3f. +class InstrumentOffsetVector(BaseModel): + """Instrument Offset from home position to robot deck.""" + + x: float + y: float + z: float diff --git a/api/src/opentrons/protocol_engine/types/instrument_sensors.py b/api/src/opentrons/protocol_engine/types/instrument_sensors.py new file mode 100644 index 00000000000..f385059b5eb --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/instrument_sensors.py @@ -0,0 +1,47 @@ +"""Protocol engine types involving instrument sensors.""" +from enum import Enum + +from opentrons.hardware_control.types import ( + TipStateType as HwTipStateType, + InstrumentProbeType, +) + + +class InstrumentSensorId(str, Enum): + """Primary and secondary sensor ids.""" + + PRIMARY = "primary" + SECONDARY = "secondary" + BOTH = "both" + + def to_instrument_probe_type(self) -> InstrumentProbeType: + """Convert to InstrumentProbeType.""" + return { + InstrumentSensorId.PRIMARY: InstrumentProbeType.PRIMARY, + InstrumentSensorId.SECONDARY: InstrumentProbeType.SECONDARY, + InstrumentSensorId.BOTH: InstrumentProbeType.BOTH, + }[self] + + +class TipPresenceStatus(str, Enum): + """Tip presence status reported by a pipette.""" + + PRESENT = "present" + ABSENT = "absent" + UNKNOWN = "unknown" + + def to_hw_state(self) -> HwTipStateType: + """Convert to hardware tip state.""" + assert self != TipPresenceStatus.UNKNOWN + return { + TipPresenceStatus.PRESENT: HwTipStateType.PRESENT, + TipPresenceStatus.ABSENT: HwTipStateType.ABSENT, + }[self] + + @classmethod + def from_hw_state(cls, state: HwTipStateType) -> "TipPresenceStatus": + """Convert from hardware tip state.""" + return { + HwTipStateType.PRESENT: TipPresenceStatus.PRESENT, + HwTipStateType.ABSENT: TipPresenceStatus.ABSENT, + }[state] diff --git a/api/src/opentrons/protocol_engine/types/labware.py b/api/src/opentrons/protocol_engine/types/labware.py new file mode 100644 index 00000000000..b0dd5d52d31 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/labware.py @@ -0,0 +1,80 @@ +"""Protocol engine types to do with labware.""" + +from __future__ import annotations + +from typing import Optional +from datetime import datetime + +from pydantic import BaseModel, Field + +from .location import LabwareLocation +from .labware_offset_location import LabwareOffsetLocation +from .labware_offset_vector import LabwareOffsetVector +from .util import Vec3f + + +class OverlapOffset(Vec3f): + """Offset representing overlap space of one labware on top of another labware or module.""" + + +class LabwareOffset(BaseModel): + """An offset that the robot adds to a pipette's position when it moves to a labware. + + During the run, if a labware is loaded whose definition URI and location + both match what's found here, the given offset will be added to all + pipette movements that use that labware as a reference point. + """ + + id: str = Field(..., description="Unique labware offset record identifier.") + createdAt: datetime = Field(..., description="When this labware offset was added.") + definitionUri: str = Field(..., description="The URI for the labware's definition.") + location: LabwareOffsetLocation = Field( + ..., + description="Where the labware is located on the robot.", + ) + vector: LabwareOffsetVector = Field( + ..., + description="The offset applied to matching labware.", + ) + + +class LabwareOffsetCreate(BaseModel): + """Create request data for a labware offset.""" + + definitionUri: str = Field(..., description="The URI for the labware's definition.") + location: LabwareOffsetLocation = Field( + ..., + description="Where the labware is located on the robot.", + ) + vector: LabwareOffsetVector = Field( + ..., + description="The offset applied to matching labware.", + ) + + +class LoadedLabware(BaseModel): + """A labware that has been loaded.""" + + id: str + loadName: str + definitionUri: str + location: LabwareLocation = Field( + ..., description="The labware's current location." + ) + lid_id: Optional[str] = Field( + None, + description=("Labware ID of a Lid currently loaded on top of the labware."), + ) + offsetId: Optional[str] = Field( + None, + description=( + "An ID referencing the labware offset" + " that applies to this labware placement." + " Null or undefined means no offset was provided for this load," + " so the default of (0, 0, 0) will be used." + ), + ) + displayName: Optional[str] = Field( + None, + description="A user-specified display name for this labware, if provided.", + ) diff --git a/api/src/opentrons/protocol_engine/types/labware_movement.py b/api/src/opentrons/protocol_engine/types/labware_movement.py new file mode 100644 index 00000000000..36bf735b2ed --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/labware_movement.py @@ -0,0 +1,22 @@ +"""Protocol Engine types to do with moving labware.""" + +from enum import Enum + +from pydantic import BaseModel + +from .labware_offset_vector import LabwareOffsetVector + + +class LabwareMovementStrategy(str, Enum): + """Strategy to use for labware movement.""" + + USING_GRIPPER = "usingGripper" + MANUAL_MOVE_WITH_PAUSE = "manualMoveWithPause" + MANUAL_MOVE_WITHOUT_PAUSE = "manualMoveWithoutPause" + + +class LabwareMovementOffsetData(BaseModel): + """Offsets to be used during labware movement.""" + + pickUpOffset: LabwareOffsetVector + dropOffset: LabwareOffsetVector diff --git a/api/src/opentrons/protocol_engine/types/labware_offset_location.py b/api/src/opentrons/protocol_engine/types/labware_offset_location.py new file mode 100644 index 00000000000..cf7496be2e0 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/labware_offset_location.py @@ -0,0 +1,59 @@ +"""Protocol engine types for legacy labware offset locations. + +This is its own module to fix circular imports. +""" + +from typing import Optional + +from pydantic import BaseModel, Field + +from opentrons.types import DeckSlotName + +from .module import ModuleModel + + +class LabwareOffsetLocation(BaseModel): + """Parameters describing when a given offset may apply to a given labware load.""" + + slotName: DeckSlotName = Field( + ..., + description=( + "The deck slot where the protocol will load the labware." + " Or, if the protocol will load the labware on a module," + " the deck slot where the protocol will load that module." + "\n\n" + # This description should be kept in sync with DeckSlotLocation.slotName. + 'The plain numbers like `"5"` are for the OT-2,' + ' and the coordinates like `"C2"` are for the Flex.' + "\n\n" + "When you provide one of these values, you can use either style." + " It will automatically be converted to match the robot." + "\n\n" + "When one of these values is returned, it will always match the robot." + ), + ) + moduleModel: Optional[ModuleModel] = Field( + None, + description=( + "The model of the module that the labware will be loaded onto," + " if applicable." + "\n\n" + "Because of module compatibility, the model that the protocol requests" + " may not be exactly the same" + " as what it will find physically connected during execution." + " For this labware offset to apply," + " this field must be the *requested* model, not the connected one." + " You can retrieve this from a `loadModule` command's `params.model`" + " in the protocol's analysis." + ), + ) + definitionUri: Optional[str] = Field( + None, + description=( + "The definition URI of a labware that a labware can be loaded onto," + " if applicable." + "\n\n" + "This can be combined with moduleModel if the labware is loaded on top of" + " an adapter that is loaded on a module." + ), + ) diff --git a/api/src/opentrons/protocol_engine/types/labware_offset_vector.py b/api/src/opentrons/protocol_engine/types/labware_offset_vector.py new file mode 100644 index 00000000000..2a23d39dc75 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/labware_offset_vector.py @@ -0,0 +1,33 @@ +"""Protocol engine types for labware offset vectors. + +This is a separate module to avoid circular imports. +""" +from __future__ import annotations +from typing import Any + +from pydantic import BaseModel + + +# TODO(mm, 2022-11-07): Deduplicate with Vec3f. +class LabwareOffsetVector(BaseModel): + """Offset, in deck coordinates from nominal to actual position.""" + + x: float + y: float + z: float + + def __add__(self, other: Any) -> LabwareOffsetVector: + """Adds two vectors together.""" + if not isinstance(other, LabwareOffsetVector): + return NotImplemented + return LabwareOffsetVector( + x=self.x + other.x, y=self.y + other.y, z=self.z + other.z + ) + + def __sub__(self, other: Any) -> LabwareOffsetVector: + """Subtracts two vectors.""" + if not isinstance(other, LabwareOffsetVector): + return NotImplemented + return LabwareOffsetVector( + x=self.x - other.x, y=self.y - other.y, z=self.z - other.z + ) diff --git a/api/src/opentrons/protocol_engine/types/liquid.py b/api/src/opentrons/protocol_engine/types/liquid.py new file mode 100644 index 00000000000..62f6aaf5a2c --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/liquid.py @@ -0,0 +1,40 @@ +"""Protocol engine types to do with liquids.""" +from dataclasses import dataclass +from enum import Enum +from typing import Literal, Optional + +from pydantic import RootModel, BaseModel, Field + + +class HexColor(RootModel[str]): + """Hex color representation.""" + + root: str = Field(pattern=r"^#(?:[0-9a-fA-F]{3,4}){1,2}$") + + +EmptyLiquidId = Literal["EMPTY"] +LiquidId = str | EmptyLiquidId + + +class Liquid(BaseModel): + """Payload required to create a liquid.""" + + id: str + displayName: str + description: str + displayColor: Optional[HexColor] = None + + +class FluidKind(str, Enum): + """A kind of fluid that can be inside a pipette.""" + + LIQUID = "LIQUID" + AIR = "AIR" + + +@dataclass(frozen=True) +class AspiratedFluid: + """Fluid inside a pipette.""" + + kind: FluidKind + volume: float diff --git a/api/src/opentrons/protocol_engine/types/liquid_class.py b/api/src/opentrons/protocol_engine/types/liquid_class.py new file mode 100644 index 00000000000..6c6da968d2a --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/liquid_class.py @@ -0,0 +1,59 @@ +"""Protocol engine types to do with liquid classes.""" +from typing import Any +from pydantic import Field + +from opentrons_shared_data.liquid_classes.liquid_class_definition import ( + ByTipTypeSetting, +) + + +class LiquidClassRecord(ByTipTypeSetting, frozen=True): + """LiquidClassRecord is our internal representation of an (immutable) liquid class. + + Conceptually, a liquid class record is the tuple (name, pipette, tip, transfer properties). + We consider two liquid classes to be the same if every entry in that tuple is the same; and liquid + classes are different if any entry in the tuple is different. + + This class defines the tuple via inheritance so that we can reuse the definitions from shared_data. + """ + + liquidClassName: str = Field( + ..., + description="Identifier for the liquid of this liquid class, e.g. glycerol50.", + ) + pipetteModel: str = Field( + ..., + description="Identifier for the pipette of this liquid class.", + ) + # The other fields like tiprack ID, aspirate properties, etc. are pulled in from ByTipTypeSetting. + + def __hash__(self) -> int: + """Hash function for LiquidClassRecord.""" + # Within the Protocol Engine, LiquidClassRecords are immutable, and we'd like to be able to + # look up LiquidClassRecords by value, which involves hashing. However, Pydantic does not + # generate a usable hash function if any of the subfields (like Coordinate) are not frozen. + # So we have to implement the hash function ourselves. + # Our strategy is to recursively convert this object into a list of (key, value) tuples. + def dict_to_tuple(d: dict[str, Any]) -> tuple[tuple[str, Any], ...]: + return tuple( + ( + field_name, + dict_to_tuple(value) + if isinstance(value, dict) + else tuple(value) + if isinstance(value, list) + else value, + ) + for field_name, value in d.items() + ) + + return hash(dict_to_tuple(self.model_dump())) + + +class LiquidClassRecordWithId(LiquidClassRecord, frozen=True): + """A LiquidClassRecord with its ID, for use in summary lists.""" + + liquidClassId: str = Field( + ..., + description="Unique identifier for this liquid class.", + ) diff --git a/api/src/opentrons/protocol_engine/types/liquid_handling.py b/api/src/opentrons/protocol_engine/types/liquid_handling.py new file mode 100644 index 00000000000..5bbb8a27da5 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/liquid_handling.py @@ -0,0 +1,13 @@ +"""Protocol engine types to do with liquid handling.""" + +from dataclasses import dataclass +from typing import Dict + + +@dataclass +class FlowRates: + """Default and current flow rates for a pipette.""" + + default_blow_out: Dict[str, float] + default_aspirate: Dict[str, float] + default_dispense: Dict[str, float] diff --git a/api/src/opentrons/protocol_engine/types/liquid_level_detection.py b/api/src/opentrons/protocol_engine/types/liquid_level_detection.py new file mode 100644 index 00000000000..37b70d13980 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/liquid_level_detection.py @@ -0,0 +1,48 @@ +"""Protocol Engine types to do with liquid level detection.""" +from dataclasses import dataclass +from datetime import datetime +from typing import Optional + +from pydantic import BaseModel + + +class LoadedVolumeInfo(BaseModel): + """A well's liquid volume, initialized by a LoadLiquid, updated by Aspirate and Dispense.""" + + volume: Optional[float] = None + last_loaded: datetime + operations_since_load: int + + +class ProbedHeightInfo(BaseModel): + """A well's liquid height, initialized by a LiquidProbe, cleared by Aspirate and Dispense.""" + + height: Optional[float] = None + last_probed: datetime + + +class ProbedVolumeInfo(BaseModel): + """A well's liquid volume, initialized by a LiquidProbe, updated by Aspirate and Dispense.""" + + volume: Optional[float] = None + last_probed: datetime + operations_since_probe: int + + +class WellInfoSummary(BaseModel): + """Payload for a well's liquid info in StateSummary.""" + + labware_id: str + well_name: str + loaded_volume: Optional[float] = None + probed_height: Optional[float] = None + probed_volume: Optional[float] = None + + +@dataclass +class WellLiquidInfo: + """Tracked and sensed information about liquid in a well.""" + + probed_height: Optional[ProbedHeightInfo] + loaded_volume: Optional[LoadedVolumeInfo] + probed_volume: Optional[ProbedVolumeInfo] diff --git a/api/src/opentrons/protocol_engine/types/location.py b/api/src/opentrons/protocol_engine/types/location.py new file mode 100644 index 00000000000..5397b17cfeb --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/location.py @@ -0,0 +1,110 @@ +"""Protocol engine types to deal with locating things on the deck.""" + +from typing import Literal, Union + +from pydantic import BaseModel, Field + +from opentrons.types import DeckSlotName, StagingSlotName + + +class DeckSlotLocation(BaseModel): + """The location of something placed in a single deck slot.""" + + slotName: DeckSlotName = Field( + ..., + description=( + # This description should be kept in sync with LabwareOffsetLocation.slotName. + "A slot on the robot's deck." + "\n\n" + 'The plain numbers like `"5"` are for the OT-2,' + ' and the coordinates like `"C2"` are for the Flex.' + "\n\n" + "When you provide one of these values, you can use either style." + " It will automatically be converted to match the robot." + "\n\n" + "When one of these values is returned, it will always match the robot." + ), + ) + + +class StagingSlotLocation(BaseModel): + """The location of something placed in a single staging slot.""" + + slotName: StagingSlotName = Field( + ..., + description=( + # This description should be kept in sync with LabwareOffsetLocation.slotName. + "A slot on the robot's staging area." + "\n\n" + "These apply only to the Flex. The OT-2 has no staging slots." + ), + ) + + +class AddressableAreaLocation(BaseModel): + """The location of something place in an addressable area. This is a superset of deck slots.""" + + addressableAreaName: str = Field( + ..., + description=( + "The name of the addressable area that you want to use." + " Valid values are the `id`s of `addressableArea`s in the" + " [deck definition](https://github.com/Opentrons/opentrons/tree/edge/shared-data/deck)." + ), + ) + + +class ModuleLocation(BaseModel): + """The location of something placed atop a hardware module.""" + + moduleId: str = Field( + ..., + description="The ID of a loaded module from a prior `loadModule` command.", + ) + + +class OnLabwareLocation(BaseModel): + """The location of something placed atop another labware.""" + + labwareId: str = Field( + ..., + description="The ID of a loaded Labware from a prior `loadLabware` command.", + ) + + +_OffDeckLocationType = Literal["offDeck"] +_SystemLocationType = Literal["systemLocation"] +OFF_DECK_LOCATION: _OffDeckLocationType = "offDeck" +SYSTEM_LOCATION: _SystemLocationType = "systemLocation" + +LabwareLocation = Union[ + DeckSlotLocation, + ModuleLocation, + OnLabwareLocation, + _OffDeckLocationType, + _SystemLocationType, + AddressableAreaLocation, +] +"""Union of all locations where it's legal to keep a labware.""" + +OnDeckLabwareLocation = Union[ + DeckSlotLocation, ModuleLocation, OnLabwareLocation, AddressableAreaLocation +] + +NonStackedLocation = Union[ + DeckSlotLocation, + AddressableAreaLocation, + ModuleLocation, + _OffDeckLocationType, + _SystemLocationType, +] +"""Union of all locations where it's legal to keep a labware that can't be stacked on another labware""" + + +# TODO(mm, 2022-11-07): Deduplicate with Vec3f. +class DeckPoint(BaseModel): + """Coordinates of a point in deck space.""" + + x: float + y: float + z: float diff --git a/api/src/opentrons/protocol_engine/types/module.py b/api/src/opentrons/protocol_engine/types/module.py new file mode 100644 index 00000000000..da6df63e683 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/module.py @@ -0,0 +1,260 @@ +"""Protocol engine types to do with modules.""" + +from __future__ import annotations +from dataclasses import dataclass +from enum import Enum +from typing import ( + TypeGuard, + Literal, + Optional, + List, + Dict, + Any, + NamedTuple, +) + +from pydantic import BaseModel, Field + +from opentrons.hardware_control.modules import ( + ModuleType as ModuleType, +) + +from .location import DeckSlotLocation +from .labware_offset_vector import LabwareOffsetVector +from .labware_movement import LabwareMovementOffsetData + + +# TODO(mc, 2022-01-18): use opentrons_shared_data.module.types.ModuleModel +class ModuleModel(str, Enum): + """All available modules' models.""" + + TEMPERATURE_MODULE_V1 = "temperatureModuleV1" + TEMPERATURE_MODULE_V2 = "temperatureModuleV2" + MAGNETIC_MODULE_V1 = "magneticModuleV1" + MAGNETIC_MODULE_V2 = "magneticModuleV2" + THERMOCYCLER_MODULE_V1 = "thermocyclerModuleV1" + THERMOCYCLER_MODULE_V2 = "thermocyclerModuleV2" + HEATER_SHAKER_MODULE_V1 = "heaterShakerModuleV1" + MAGNETIC_BLOCK_V1 = "magneticBlockV1" + ABSORBANCE_READER_V1 = "absorbanceReaderV1" + FLEX_STACKER_MODULE_V1 = "flexStackerModuleV1" + + def as_type(self) -> ModuleType: + """Get the ModuleType of this model.""" + if ModuleModel.is_temperature_module_model(self): + return ModuleType.TEMPERATURE + elif ModuleModel.is_magnetic_module_model(self): + return ModuleType.MAGNETIC + elif ModuleModel.is_thermocycler_module_model(self): + return ModuleType.THERMOCYCLER + elif ModuleModel.is_heater_shaker_module_model(self): + return ModuleType.HEATER_SHAKER + elif ModuleModel.is_magnetic_block(self): + return ModuleType.MAGNETIC_BLOCK + elif ModuleModel.is_absorbance_reader(self): + return ModuleType.ABSORBANCE_READER + elif ModuleModel.is_flex_stacker(self): + return ModuleType.FLEX_STACKER + + assert False, f"Invalid ModuleModel {self}" + + @classmethod + def is_temperature_module_model( + cls, model: ModuleModel + ) -> TypeGuard[TemperatureModuleModel]: + """Whether a given model is a Temperature Module.""" + return model in [cls.TEMPERATURE_MODULE_V1, cls.TEMPERATURE_MODULE_V2] + + @classmethod + def is_magnetic_module_model( + cls, model: ModuleModel + ) -> TypeGuard[MagneticModuleModel]: + """Whether a given model is a Magnetic Module.""" + return model in [cls.MAGNETIC_MODULE_V1, cls.MAGNETIC_MODULE_V2] + + @classmethod + def is_thermocycler_module_model( + cls, model: ModuleModel + ) -> TypeGuard[ThermocyclerModuleModel]: + """Whether a given model is a Thermocycler Module.""" + return model in [cls.THERMOCYCLER_MODULE_V1, cls.THERMOCYCLER_MODULE_V2] + + @classmethod + def is_heater_shaker_module_model( + cls, model: ModuleModel + ) -> TypeGuard[HeaterShakerModuleModel]: + """Whether a given model is a Heater-Shaker Module.""" + return model == cls.HEATER_SHAKER_MODULE_V1 + + @classmethod + def is_magnetic_block(cls, model: ModuleModel) -> TypeGuard[MagneticBlockModel]: + """Whether a given model is a Magnetic block.""" + return model == cls.MAGNETIC_BLOCK_V1 + + @classmethod + def is_absorbance_reader( + cls, model: ModuleModel + ) -> TypeGuard[AbsorbanceReaderModel]: + """Whether a given model is an Absorbance Plate Reader.""" + return model == cls.ABSORBANCE_READER_V1 + + @classmethod + def is_flex_stacker(cls, model: ModuleModel) -> TypeGuard[FlexStackerModuleModel]: + """Whether a given model is a Flex Stacker..""" + return model == cls.FLEX_STACKER_MODULE_V1 + + +TemperatureModuleModel = Literal[ + ModuleModel.TEMPERATURE_MODULE_V1, ModuleModel.TEMPERATURE_MODULE_V2 +] +MagneticModuleModel = Literal[ + ModuleModel.MAGNETIC_MODULE_V1, ModuleModel.MAGNETIC_MODULE_V2 +] +ThermocyclerModuleModel = Literal[ + ModuleModel.THERMOCYCLER_MODULE_V1, ModuleModel.THERMOCYCLER_MODULE_V2 +] +HeaterShakerModuleModel = Literal[ModuleModel.HEATER_SHAKER_MODULE_V1] +MagneticBlockModel = Literal[ModuleModel.MAGNETIC_BLOCK_V1] +AbsorbanceReaderModel = Literal[ModuleModel.ABSORBANCE_READER_V1] +FlexStackerModuleModel = Literal[ModuleModel.FLEX_STACKER_MODULE_V1] + + +class ModuleDimensions(BaseModel): + """Dimension type for modules.""" + + bareOverallHeight: float + overLabwareHeight: float + lidHeight: Optional[float] = None + + +# TODO(mm, 2022-11-07): Deduplicate with Vec3f. +class ModuleCalibrationPoint(BaseModel): + """Calibration Point type for module definition.""" + + x: float + y: float + z: float + + +# TODO(mm, 2023-04-13): Move to shared-data, so this binding can be maintained alongside the JSON +# schema that it's sourced from. We already do that for labware definitions and JSON protocols. +class ModuleDefinition(BaseModel): + """A module definition conforming to module definition schema v3.""" + + # Note: This field is misleading. + # + # This class only models v3 definitions ("module/schemas/3"), not v2 ("module/schemas/2"). + # labwareOffset is required to have a z-component, for example. + # + # When parsing from a schema v3 JSON definition into this model, + # the definition's `"$otSharedSchema": "module/schemas/3"` field will be thrown away + # because it has a dollar sign, which doesn't match this field. + # Then, this field will default to "module/schemas/2", because no value was provided. + # + # We should fix this field once Jira RSS-221 is resolved. RSS-221 makes it difficult to fix + # because robot-server has been storing and loading these bad fields in its database. + otSharedSchema: str = Field("module/schemas/2", description="The current schema.") + + moduleType: ModuleType = Field( + ..., + description="Module type (Temperature/Magnetic/Thermocycler)", + ) + + model: ModuleModel = Field(..., description="Model name of the module") + + labwareOffset: LabwareOffsetVector = Field( + ..., + description="Labware offset in x, y, z.", + ) + + dimensions: ModuleDimensions = Field(..., description="Module dimension") + + calibrationPoint: ModuleCalibrationPoint = Field( + ..., + description="Calibration point of module.", + ) + + displayName: str = Field(..., description="Display name.") + + quirks: List[str] = Field(..., description="Module quirks") + + # In releases prior to https://github.com/Opentrons/opentrons/pull/11873 (v6.3.0), + # the matrices in slotTransforms were 3x3. + # After, they are 4x4, even though there was no schema version bump. + # + # Because old objects of this class, with the 3x3 matrices, were stored in robot-server's + # database, this field needs to stay typed loosely enough to support both sizes. + # We can fix this once Jira RSS-221 is resolved. + slotTransforms: Dict[str, Any] = Field( + ..., + description="Dictionary of transforms for each slot.", + ) + + compatibleWith: List[ModuleModel] = Field( + ..., + description="List of module models this model is compatible with.", + ) + gripperOffsets: Optional[Dict[str, LabwareMovementOffsetData]] = Field( + default_factory=dict, + description="Offsets to use for labware movement using gripper", + ) + + +class LoadedModule(BaseModel): + """A module that has been loaded.""" + + id: str + model: ModuleModel + location: Optional[DeckSlotLocation] = None + serialNumber: Optional[str] = None + + +class SpeedRange(NamedTuple): + """Minimum and maximum allowed speeds for a shaking module.""" + + min: int + max: int + + +class TemperatureRange(NamedTuple): + """Minimum and maximum allowed temperatures for a heating module.""" + + min: float + max: float + + +class HeaterShakerLatchStatus(Enum): + """Heater-Shaker latch status for determining pipette and labware movement errors.""" + + CLOSED = "closed" + OPEN = "open" + UNKNOWN = "unknown" + + +@dataclass(frozen=True) +class HeaterShakerMovementRestrictors: + """Shaking status, latch status and slot location for determining movement restrictions.""" + + plate_shaking: bool + latch_status: HeaterShakerLatchStatus + deck_slot: int + + +ABSMeasureMode = Literal["single", "multi"] + + +# TODO(mm, 2022-11-07): Deduplicate with Vec3f. +class ModuleOffsetVector(BaseModel): + """Offset, in deck coordinates, from nominal to actual position of labware on a module.""" + + x: float + y: float + z: float + + +@dataclass +class ModuleOffsetData: + """Module calibration offset data.""" + + moduleOffsetVector: ModuleOffsetVector + location: DeckSlotLocation diff --git a/api/src/opentrons/protocol_engine/types/partial_tip_configuration.py b/api/src/opentrons/protocol_engine/types/partial_tip_configuration.py new file mode 100644 index 00000000000..784a35d6c52 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/partial_tip_configuration.py @@ -0,0 +1,76 @@ +"""Protocol engine types to do with partial tip configurations.""" + +from typing import Literal, Union + +from pydantic import ( + BaseModel, + Field, +) + +NOZZLE_NAME_REGEX = r"[A-Z]\d{1,2}" +PRIMARY_NOZZLE_LITERAL = Literal["A1", "H1", "A12", "H12"] + + +class AllNozzleLayoutConfiguration(BaseModel): + """All basemodel to represent a reset to the nozzle configuration. Sending no parameters resets to default.""" + + style: Literal["ALL"] = "ALL" + + +class SingleNozzleLayoutConfiguration(BaseModel): + """Minimum information required for a new nozzle configuration.""" + + style: Literal["SINGLE"] = "SINGLE" + primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class RowNozzleLayoutConfiguration(BaseModel): + """Minimum information required for a new nozzle configuration.""" + + style: Literal["ROW"] = "ROW" + primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class ColumnNozzleLayoutConfiguration(BaseModel): + """Information required for nozzle configurations of type ROW and COLUMN.""" + + style: Literal["COLUMN"] = "COLUMN" + primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + + +class QuadrantNozzleLayoutConfiguration(BaseModel): + """Information required for nozzle configurations of type QUADRANT.""" + + style: Literal["QUADRANT"] = "QUADRANT" + primaryNozzle: PRIMARY_NOZZLE_LITERAL = Field( + ..., + description="The primary nozzle to use in the layout configuration. This nozzle will update the critical point of the current pipette. For now, this is also the back left corner of your rectangle.", + ) + frontRightNozzle: str = Field( + ..., + pattern=NOZZLE_NAME_REGEX, + description="The front right nozzle in your configuration.", + ) + backLeftNozzle: str = Field( + ..., + pattern=NOZZLE_NAME_REGEX, + description="The back left nozzle in your configuration.", + ) + + +NozzleLayoutConfigurationType = Union[ + AllNozzleLayoutConfiguration, + SingleNozzleLayoutConfiguration, + ColumnNozzleLayoutConfiguration, + RowNozzleLayoutConfiguration, + QuadrantNozzleLayoutConfiguration, +] diff --git a/api/src/opentrons/protocol_engine/types/run_time_parameters.py b/api/src/opentrons/protocol_engine/types/run_time_parameters.py new file mode 100644 index 00000000000..9055ad35e3a --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/run_time_parameters.py @@ -0,0 +1,133 @@ +#!/usr/bin/env python3 + +"""Protocol Engine types that have to do with runtime parameters.""" + +from pathlib import Path +from typing import Optional, Mapping, Union, Literal, Dict, List +from pydantic import ( + BaseModel, + Field, + StrictBool, + StrictFloat, + StrictInt, + StrictStr, +) + + +# TODO (spp, 2024-04-02): move all RTP types to runner +class RTPBase(BaseModel): + """Parameters defined in a protocol.""" + + displayName: StrictStr = Field(..., description="Display string for the parameter.") + variableName: StrictStr = Field( + ..., description="Python variable name of the parameter." + ) + description: Optional[StrictStr] = Field( + None, description="Detailed description of the parameter." + ) + suffix: Optional[StrictStr] = Field( + None, + description="Units (like mL, mm/sec, etc) or a custom suffix for the parameter.", + ) + + +class NumberParameter(RTPBase): + """An integer parameter defined in a protocol.""" + + type: Literal["int", "float"] = Field( + ..., description="String specifying whether the number is an int or float type." + ) + min: Union[StrictInt, StrictFloat] = Field( + ..., description="Minimum value that the number param is allowed to have." + ) + max: Union[StrictInt, StrictFloat] = Field( + ..., description="Maximum value that the number param is allowed to have." + ) + value: Union[StrictInt, StrictFloat] = Field( + ..., + description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", + ) + default: Union[StrictInt, StrictFloat] = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +class BooleanParameter(RTPBase): + """A boolean parameter defined in a protocol.""" + + type: Literal["bool"] = Field( + default="bool", description="String specifying the type of this parameter" + ) + value: StrictBool = Field( + ..., + description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", + ) + default: StrictBool = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +class EnumChoice(BaseModel): + """Components of choices used in RTP Enum Parameters.""" + + displayName: StrictStr = Field( + ..., description="Display string for the param's choice." + ) + value: Union[StrictInt, StrictFloat, StrictStr] = Field( + ..., description="Enum value of the param's choice." + ) + + +class EnumParameter(RTPBase): + """A string enum defined in a protocol.""" + + type: Literal["int", "float", "str"] = Field( + ..., + description="String specifying whether the parameter is an int or float or string type.", + ) + choices: List[EnumChoice] = Field( + ..., description="List of valid choices for this parameter." + ) + value: Union[StrictInt, StrictFloat, StrictStr] = Field( + ..., + description="The value assigned to the parameter; if not supplied by the client, will be assigned the default value.", + ) + default: Union[StrictInt, StrictFloat, StrictStr] = Field( + ..., + description="Default value of the parameter, to be used when there is no client-specified value.", + ) + + +class FileInfo(BaseModel): + """A file UUID descriptor.""" + + id: str = Field( + ..., + description="The UUID identifier of the file stored on the robot.", + ) + name: str = Field(..., description="Name of the file, including the extension.") + + +class CSVParameter(RTPBase): + """A CSV file parameter defined in a protocol.""" + + type: Literal["csv_file"] = Field( + default="csv_file", description="String specifying the type of this parameter" + ) + file: Optional[FileInfo] = Field( + default=None, + description="ID of the CSV file stored on the robot; to be used for fetching the CSV file." + " For local analysis this will most likely be empty.", + ) + + +RunTimeParameter = Union[NumberParameter, EnumParameter, BooleanParameter, CSVParameter] + +PrimitiveRunTimeParamValuesType = Mapping[ + StrictStr, Union[StrictInt, StrictFloat, StrictBool, StrictStr] +] # update value types as more RTP types are added + +CSVRunTimeParamFilesType = Mapping[StrictStr, StrictStr] +CSVRuntimeParamPaths = Dict[str, Path] diff --git a/api/src/opentrons/protocol_engine/types/tip.py b/api/src/opentrons/protocol_engine/types/tip.py new file mode 100644 index 00000000000..227196426de --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/tip.py @@ -0,0 +1,18 @@ +"""Protocol Engine types to deal with tips.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class TipGeometry: + """Tip geometry data. + + Props: + length: The effective length (total length minus overlap) of a tip in mm. + diameter: Tip diameter in mm. + volume: Maximum volume in µL. + """ + + length: float + diameter: float + volume: float diff --git a/api/src/opentrons/protocol_engine/types/util.py b/api/src/opentrons/protocol_engine/types/util.py new file mode 100644 index 00000000000..3cc96d70043 --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/util.py @@ -0,0 +1,21 @@ +"""Protocol engine utility types for model components.""" +from dataclasses import dataclass + +from pydantic import BaseModel + + +@dataclass(frozen=True) +class Dimensions: + """Dimensions of an object in deck-space.""" + + x: float + y: float + z: float + + +class Vec3f(BaseModel): + """A 3D vector of floats.""" + + x: float + y: float + z: float diff --git a/api/src/opentrons/protocol_engine/types/well_position.py b/api/src/opentrons/protocol_engine/types/well_position.py new file mode 100644 index 00000000000..d63d8daefbb --- /dev/null +++ b/api/src/opentrons/protocol_engine/types/well_position.py @@ -0,0 +1,107 @@ +"""Protocol engine types to do with positions inside wells.""" +from enum import Enum +from typing import Union, Literal + +from pydantic import BaseModel, Field + + +class WellOrigin(str, Enum): + """Origin of WellLocation offset. + + Props: + TOP: the top-center of the well + BOTTOM: the bottom-center of the well + CENTER: the middle-center of the well + MENISCUS: the meniscus-center of the well + """ + + TOP = "top" + BOTTOM = "bottom" + CENTER = "center" + MENISCUS = "meniscus" + + +class PickUpTipWellOrigin(str, Enum): + """The origin of a PickUpTipWellLocation offset. + + Props: + TOP: the top-center of the well + BOTTOM: the bottom-center of the well + CENTER: the middle-center of the well + """ + + TOP = "top" + BOTTOM = "bottom" + CENTER = "center" + + +class DropTipWellOrigin(str, Enum): + """The origin of a DropTipWellLocation offset. + + Props: + TOP: the top-center of the well + BOTTOM: the bottom-center of the well + CENTER: the middle-center of the well + DEFAULT: the default drop-tip location of the well, + based on pipette configuration and length of the tip. + """ + + TOP = "top" + BOTTOM = "bottom" + CENTER = "center" + DEFAULT = "default" + + +# This is deliberately a separate type from Vec3f to let components default to 0. +class WellOffset(BaseModel): + """An offset vector in (x, y, z).""" + + x: float = 0 + y: float = 0 + z: float = 0 + + +class WellLocation(BaseModel): + """A relative location in reference to a well's location.""" + + origin: WellOrigin = WellOrigin.TOP + offset: WellOffset = Field(default_factory=WellOffset) + volumeOffset: float = Field( + default=0.0, + description="""A volume of liquid, in µL, to offset the z-axis offset.""", + ) + + +class LiquidHandlingWellLocation(BaseModel): + """A relative location in reference to a well's location. + + To be used with commands that handle liquids. + """ + + origin: WellOrigin = WellOrigin.TOP + offset: WellOffset = Field(default_factory=WellOffset) + volumeOffset: Union[float, Literal["operationVolume"]] = Field( + default=0.0, + description="""A volume of liquid, in µL, to offset the z-axis offset. When "operationVolume" is specified, this volume is pulled from the command volume parameter.""", + ) + + +class PickUpTipWellLocation(BaseModel): + """A relative location in reference to a well's location. + + To be used for picking up tips. + """ + + origin: PickUpTipWellOrigin = PickUpTipWellOrigin.TOP + offset: WellOffset = Field(default_factory=WellOffset) + + +class DropTipWellLocation(BaseModel): + """Like WellLocation, but for dropping tips. + + Unlike a typical WellLocation, the location for a drop tip + defaults to location based on the tip length rather than the well's top. + """ + + origin: DropTipWellOrigin = DropTipWellOrigin.DEFAULT + offset: WellOffset = Field(default_factory=WellOffset) From 558eca04fb7d21d6f0159553539acd683add8bba Mon Sep 17 00:00:00 2001 From: David Chau <46395074+ddcc4@users.noreply.github.com> Date: Thu, 23 Jan 2025 17:30:13 -0500 Subject: [PATCH 3/4] =?UTF-8?q?fix(everything):=20replace=20Greek=20'?= =?UTF-8?q?=CE=BC'=20with=20micro=20'=C2=B5'=20(#17331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # Overview There are two different characters that look like the "mu" symbol: - `µ` (U+00B5 `MICRO SIGN`) - `μ` (U+03BC `GREEK SMALL LETTER MU`) In some fonts they look different; but in many fonts they look the same. The problem is that in the Public Sans font that we use for Protocol Designer, `U+03BC GREEK SMALL LETTER MU` is not available at all, because the font only supports Latin characters plus some scientific symbols. So whenever we use `U+03BC GREEK SMALL LETTER MU` in text, the browser has to render it with a fallback font, which can look ugly. For consistency, we should just use `U+00B5 MICRO SIGN` whenever we have a `µ` that means 1/1,000,000, and only use `U+03BC GREEK SMALL LETTER MU` when we're writing Greek text (like in a Greek-language user manual). ## Test Plan and Hands on Testing I did a global search and replace on all the text in our codebase. I'm relying on the CI tests to catch any issues. ## Risk assessment This could potentially break external dependencies that expect the Greek-text mu. --- api/docs/v1/pipettes.rst | 106 +++++++++--------- api/docs/v2/complex_commands/parameters.rst | 2 +- api/docs/v2/modules/temperature_module.rst | 6 +- api/docs/v2/tutorial.rst | 2 +- .../resources/test_pipette_data_provider.py | 4 +- .../instruments/__tests__/hooks.test.ts | 4 +- .../ProtocolInstrumentMountItem.test.tsx | 10 +- .../__tests__/AttachProbe.test.tsx | 4 +- .../__tests__/BeforeBeginning.test.tsx | 4 +- .../__tests__/ChoosePipette.test.tsx | 4 +- .../__tests__/DetachPipette.test.tsx | 6 +- .../__tests__/Results.test.tsx | 20 ++-- .../__tests__/hooks.test.tsx | 2 +- .../__tests__/InstrumentsDashboard.test.tsx | 4 +- app/src/redux/pipettes/__fixtures__/index.ts | 10 +- ...tachedPipettesFromInstrumentsQuery.test.ts | 4 +- components/src/forms/InputField.stories.tsx | 2 +- .../components/sections/Volume.tsx | 2 +- .../__tests__/InstrumentsSection.test.tsx | 8 +- .../__tests__/CreateProtocol.test.tsx | 4 +- .../utils/createProtocolTestUtils.tsx | 4 +- .../api/data/python_api_219_docs.md | 10 +- .../api/domain/fake_responses.py | 2 +- opentrons-ai-server/api/domain/utils.py | 4 +- .../storage/docs/serial_dilution_examples.md | 2 +- .../cypress/support/createNew.ts | 2 +- .../src/assets/localization/en/alert.json | 2 +- .../assets/localization/en/application.json | 4 +- .../__tests__/InstrumentsInfo.test.tsx | 4 +- shared-data/js/__tests__/pipettes.test.ts | 2 +- shared-data/js/titleCase.ts | 2 +- .../definitions/1/pipetteNameSpecs.json | 14 +-- .../2/general/eight_channel/p1000/3_0.json | 2 +- .../2/general/eight_channel/p1000/3_3.json | 2 +- .../2/general/eight_channel/p1000/3_4.json | 2 +- .../2/general/eight_channel/p1000/3_5.json | 2 +- .../2/general/eight_channel/p50/3_0.json | 2 +- .../2/general/eight_channel/p50/3_3.json | 2 +- .../2/general/eight_channel/p50/3_4.json | 2 +- .../2/general/eight_channel/p50/3_5.json | 2 +- .../2/general/eight_channel_em/p1000/1_0.json | 2 +- .../2/general/eight_channel_em/p1000/3_0.json | 2 +- .../general/ninety_six_channel/p1000/1_0.json | 2 +- .../general/ninety_six_channel/p1000/3_0.json | 2 +- .../general/ninety_six_channel/p1000/3_3.json | 2 +- .../general/ninety_six_channel/p1000/3_4.json | 2 +- .../general/ninety_six_channel/p1000/3_5.json | 2 +- .../general/ninety_six_channel/p1000/3_6.json | 2 +- .../general/ninety_six_channel/p200/1_0.json | 2 +- .../general/ninety_six_channel/p200/3_0.json | 2 +- .../2/general/single_channel/p1000/3_0.json | 2 +- .../2/general/single_channel/p1000/3_3.json | 2 +- .../2/general/single_channel/p1000/3_4.json | 2 +- .../2/general/single_channel/p1000/3_5.json | 2 +- .../2/general/single_channel/p1000/3_6.json | 2 +- .../2/general/single_channel/p1000/3_7.json | 2 +- .../2/general/single_channel/p50/3_0.json | 2 +- .../2/general/single_channel/p50/3_3.json | 2 +- .../2/general/single_channel/p50/3_4.json | 2 +- .../2/general/single_channel/p50/3_5.json | 2 +- .../2/general/single_channel/p50/3_6.json | 2 +- .../name/pipetteNameSpecFixtures.json | 2 +- .../fixtureGeneration.test.ts.snap | 2 +- step-generation/src/errorCreators.ts | 4 +- 64 files changed, 163 insertions(+), 163 deletions(-) diff --git a/api/docs/v1/pipettes.rst b/api/docs/v1/pipettes.rst index 72dc73d349f..713615d184f 100644 --- a/api/docs/v1/pipettes.rst +++ b/api/docs/v1/pipettes.rst @@ -55,12 +55,12 @@ same behavior as before. The P20 Single GEN2 is back-compatible with the P10 Single in this regard. If your protocol specifies an ``instruments.P10_Single`` and your robot has an ``instruments.P20_Single_GEN2`` attached, you can run your protocol, and the robot will act as if the maximum volume of the P20 -Single GEN2 is 10 μl. +Single GEN2 is 10 µl. If you have a P50 Single specified in your protocol, there is no automatic backwards compatibility. If you want to use a Gen2 Pipette, you must change your protocol to load either a P300 Single GEN2 -(if you are using volumes between 20 and 50 μl) or a P20 Single GEN2 (if you are using volumes -below 20 μl). +(if you are using volumes between 20 and 50 µl) or a P20 Single GEN2 (if you are using volumes +below 20 µl). Plunger Flow Rates @@ -99,92 +99,92 @@ The given defaults for every pipette model is the following: P10_Single ---------- -- Aspirate Default: 5 μl/s -- Dispense Default: 10 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 1 μl -- Maximum Volume: 10 μl +- Aspirate Default: 5 µl/s +- Dispense Default: 10 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 1 µl +- Maximum Volume: 10 µl P10_Multi --------- -- Aspirate Default: 5 μl/s -- Dispense Default: 10 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 1 μl -- Maximum Volume: 10 μl +- Aspirate Default: 5 µl/s +- Dispense Default: 10 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 1 µl +- Maximum Volume: 10 µl P50_Single ---------- -- Aspirate Default: 25 μl/s -- Dispense Default: 50 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 5 μl -- Maximum Volume: 50 μl +- Aspirate Default: 25 µl/s +- Dispense Default: 50 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 5 µl +- Maximum Volume: 50 µl P50_Multi --------- -- Aspirate Default: 25 μl/s -- Dispense Default: 50 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 5 μl -- Maximum Volume: 50 μl +- Aspirate Default: 25 µl/s +- Dispense Default: 50 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 5 µl +- Maximum Volume: 50 µl P300_Single ----------- -- Aspirate Default: 150 μl/s -- Dispense Default: 300 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 30 μl -- Maximum Volume: 300 μl +- Aspirate Default: 150 µl/s +- Dispense Default: 300 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 30 µl +- Maximum Volume: 300 µl P300_Multi ---------- -- Aspirate Default: 150 μl/s -- Dispense Default: 300 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 30 μl -- Maximum Volume: 300 μl +- Aspirate Default: 150 µl/s +- Dispense Default: 300 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 30 µl +- Maximum Volume: 300 µl P1000_Single ------------ -- Aspirate Default: 500 μl/s -- Dispense Default: 1000 μl/s -- Blow Out Default: 1000 μl/s -- Minimum Volume: 100 μl -- Maximum Volume: 1000 μl +- Aspirate Default: 500 µl/s +- Dispense Default: 1000 µl/s +- Blow Out Default: 1000 µl/s +- Minimum Volume: 100 µl +- Maximum Volume: 1000 µl P20_Single_GEN2 --------------- -- Aspirate Default: 3.78 μl/s -- Dispense Default: 3.78 μl/s -- Blow Out Default: 3.78 μl/s -- Minimum Volume: 1 μl -- Maximum Volume: 20 μl +- Aspirate Default: 3.78 µl/s +- Dispense Default: 3.78 µl/s +- Blow Out Default: 3.78 µl/s +- Minimum Volume: 1 µl +- Maximum Volume: 20 µl P300_Single_GEN2 ---------------- -- Aspirate Default: 46.43 μl/s -- Dispense Default: 46.43 μl/s -- Blow Out Default: 46.43 μl/s -- Minimum Volume: 20 μl -- Maximum Volume: 300 μl +- Aspirate Default: 46.43 µl/s +- Dispense Default: 46.43 µl/s +- Blow Out Default: 46.43 µl/s +- Minimum Volume: 20 µl +- Maximum Volume: 300 µl P1000_Single_GEN2 ----------------- -- Aspirate Default: 137.35 μl/s -- Dispense Default: 137.35 μl/s -- Blow Out Default: 137.35 μl/s -- Minimum Volume: 100 μl -- Maximum Volume: 1000 μl +- Aspirate Default: 137.35 µl/s +- Dispense Default: 137.35 µl/s +- Blow Out Default: 137.35 µl/s +- Minimum Volume: 100 µl +- Maximum Volume: 1000 µl Old Pipette Constructor ======================= diff --git a/api/docs/v2/complex_commands/parameters.rst b/api/docs/v2/complex_commands/parameters.rst index 14658509c97..2c044a73e6e 100644 --- a/api/docs/v2/complex_commands/parameters.rst +++ b/api/docs/v2/complex_commands/parameters.rst @@ -62,7 +62,7 @@ One reason to set ``new_tip="always"`` is to avoid cross-contamination between w :py:meth:`~.InstrumentContext.transfer` will pick up a new tip before *every* aspirate when ``new_tip="always"``. This includes when :ref:`tip refilling ` requires multiple aspirations from a single source well. -:py:meth:`~.InstrumentContext.distribute` and :py:meth:`~.InstrumentContext.consolidate` only pick up one tip, even when ``new_tip="always"``. For example, this distribute command returns to the source well a second time, because the amount to be distributed (400 µL total plus disposal volume) exceeds the pipette capacity (300 μL):: +:py:meth:`~.InstrumentContext.distribute` and :py:meth:`~.InstrumentContext.consolidate` only pick up one tip, even when ``new_tip="always"``. For example, this distribute command returns to the source well a second time, because the amount to be distributed (400 µL total plus disposal volume) exceeds the pipette capacity (300 µL):: pipette.distribute( volume=200, diff --git a/api/docs/v2/modules/temperature_module.rst b/api/docs/v2/modules/temperature_module.rst index 845f6c69931..0d13652658e 100644 --- a/api/docs/v2/modules/temperature_module.rst +++ b/api/docs/v2/modules/temperature_module.rst @@ -98,11 +98,11 @@ The Temperature Module supports these 96-well block and labware combinations for * - 96-well block contents - API Load Name - * - Bio-Rad well plate 200 μL + * - Bio-Rad well plate 200 µL - ``opentrons_96_aluminumblock_biorad_wellplate_200uL`` - * - Generic PCR strip 200 μL + * - Generic PCR strip 200 µL - ``opentrons_96_aluminumblock_generic_pcr_strip_200uL`` - * - NEST well plate 100 μL + * - NEST well plate 100 µL - ``opentrons_96_aluminumblock_nest_wellplate_100uL`` This command loads the same physical adapter and labware as the example in the Standalone Adapters section above, but it is also compatible with earlier API versions:: diff --git a/api/docs/v2/tutorial.rst b/api/docs/v2/tutorial.rst index b36becf8131..a2a0e945c9b 100644 --- a/api/docs/v2/tutorial.rst +++ b/api/docs/v2/tutorial.rst @@ -30,7 +30,7 @@ Hardware and Labware Before running a protocol, you’ll want to have the right kind of hardware and labware ready for your Flex or OT-2. -- **Flex users** should review Chapter 2: Installation and Relocation in the `instruction manual `_. Specifically, see the pipette information in the "Instrument Installation and Calibration" section. You can use either a 1-channel or 8-channel pipette for this tutorial. Most Flex code examples will use a `Flex 1-Channel 1000 μL pipette `_. +- **Flex users** should review Chapter 2: Installation and Relocation in the `instruction manual `_. Specifically, see the pipette information in the "Instrument Installation and Calibration" section. You can use either a 1-channel or 8-channel pipette for this tutorial. Most Flex code examples will use a `Flex 1-Channel 1000 µL pipette `_. - **OT-2 users** should review the robot setup and pipette information on the `Get Started page `_. Specifically, see `attaching pipettes `_ and `initial calibration `_. You can use either a single-channel or 8-channel pipette for this tutorial. Most OT-2 code examples will use a `P300 Single-Channel GEN2 `_ pipette. diff --git a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py index ae3d78d2230..b65d3eb0d8f 100644 --- a/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py +++ b/api/tests/opentrons/protocol_engine/resources/test_pipette_data_provider.py @@ -94,7 +94,7 @@ def test_configure_virtual_pipette_for_volume( ) assert result1 == LoadedStaticPipetteData( model="p50_single_v3.6", - display_name="Flex 1-Channel 50 μL", + display_name="Flex 1-Channel 50 µL", min_volume=5, max_volume=50.0, channels=1, @@ -128,7 +128,7 @@ def test_configure_virtual_pipette_for_volume( ) assert result2 == LoadedStaticPipetteData( model="p50_single_v3.6", - display_name="Flex 1-Channel 50 μL", + display_name="Flex 1-Channel 50 µL", min_volume=1, max_volume=30, channels=1, diff --git a/app/src/local-resources/instruments/__tests__/hooks.test.ts b/app/src/local-resources/instruments/__tests__/hooks.test.ts index 468c2da5e0d..45c800044dc 100644 --- a/app/src/local-resources/instruments/__tests__/hooks.test.ts +++ b/app/src/local-resources/instruments/__tests__/hooks.test.ts @@ -13,8 +13,8 @@ import type { PipetteV2Specs } from '@opentrons/shared-data' vi.mock('/app/resources/robot-settings/hooks') -const BRANDED_P1000_FLEX_DISPLAY_NAME = 'Flex 1-Channel 1000 μL' -const ANONYMOUS_P1000_FLEX_DISPLAY_NAME = '1-Channel 1000 μL' +const BRANDED_P1000_FLEX_DISPLAY_NAME = 'Flex 1-Channel 1000 µL' +const ANONYMOUS_P1000_FLEX_DISPLAY_NAME = '1-Channel 1000 µL' const mockP1000V2Specs = { $otSharedSchema: '#/pipette/schemas/2/pipetteGeometrySchema.json', diff --git a/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx b/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx index 52c62382241..687bb1d9832 100644 --- a/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx +++ b/app/src/organisms/ODD/InstrumentMountItem/__tests__/ProtocolInstrumentMountItem.test.tsx @@ -78,7 +78,7 @@ describe('ProtocolInstrumentMountItem', () => { render(props) screen.getByText('Left Mount') screen.getByText('No data') - screen.getByText('Flex 8-Channel 1000 μL') + screen.getByText('Flex 8-Channel 1000 µL') screen.getByText('Attach') fireEvent.click(screen.getByRole('button')) screen.getByText('pipette wizard flow') @@ -91,7 +91,7 @@ describe('ProtocolInstrumentMountItem', () => { render(props) screen.getByText('Left + Right Mount') screen.getByText('No data') - screen.getByText('Flex 96-Channel 1000 μL') + screen.getByText('Flex 96-Channel 1000 µL') screen.getByText('Attach') }) it('renders the correct information when there is a pipette attached with cal data', () => { @@ -103,7 +103,7 @@ describe('ProtocolInstrumentMountItem', () => { render(props) screen.getByText('Left Mount') screen.getByText('Calibrated') - screen.getByText('Flex 8-Channel 1000 μL') + screen.getByText('Flex 8-Channel 1000 µL') }) it('renders the pipette with no cal data and the calibration button and clicking on it launches the correct flow', () => { props = { @@ -119,7 +119,7 @@ describe('ProtocolInstrumentMountItem', () => { render(props) screen.getByText('Left Mount') screen.getByText('No data') - screen.getByText('Flex 8-Channel 1000 μL') + screen.getByText('Flex 8-Channel 1000 µL') const button = screen.getByText('Calibrate') fireEvent.click(button) screen.getByText('pipette wizard flow') @@ -132,7 +132,7 @@ describe('ProtocolInstrumentMountItem', () => { render(props) screen.getByText('Left Mount') screen.getByText('No data') - screen.getByText('Flex 8-Channel 1000 μL') + screen.getByText('Flex 8-Channel 1000 µL') const button = screen.getByText('Attach') fireEvent.click(button) screen.getByText('pipette wizard flow') diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx index 0bbab02ebc5..43f2357ec6d 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/AttachProbe.test.tsx @@ -133,7 +133,7 @@ describe('AttachProbe', () => { isRobotMoving: true, } render(props) - screen.getByText('Stand back, Flex 1-Channel 1000 μL is calibrating') + screen.getByText('Stand back, Flex 1-Channel 1000 µL is calibrating') screen.getByText( 'The calibration probe will touch the sides of the calibration square in slot C2 to determine its exact position.' ) @@ -152,7 +152,7 @@ describe('AttachProbe', () => { isRobotMoving: true, } render(props) - screen.getByText('Stand back, Flex 96-Channel 1000 μL is calibrating') + screen.getByText('Stand back, Flex 96-Channel 1000 µL is calibrating') screen.getByText( 'The calibration probe will touch the sides of the calibration square in slot C2 to determine its exact position.' ) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx index db8b03816c2..3a801f7a597 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/BeforeBeginning.test.tsx @@ -191,7 +191,7 @@ describe('BeforeBeginning', () => { screen.getByText( 'The calibration probe is included with the robot and should be stored on the front pillar of the robot.' ) - screen.getByAltText('Flex 1-Channel 1000 μL') + screen.getByAltText('Flex 1-Channel 1000 µL') screen.getByText('You will need:') screen.getByAltText('Calibration Probe') screen.getByAltText('2.5 mm Hex Screwdriver') @@ -449,7 +449,7 @@ describe('BeforeBeginning', () => { ) screen.getByAltText('2.5 mm Hex Screwdriver') screen.getByAltText('Calibration Probe') - screen.getByAltText('Flex 96-Channel 1000 μL') + screen.getByAltText('Flex 96-Channel 1000 µL') screen.getByAltText('96-Channel Mounting Plate') screen.getByText( 'Provided with the robot. Using another size can strip the instruments’s screws.' diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx index ab14c846013..6c2430d6dd3 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/ChoosePipette.test.tsx @@ -151,7 +151,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 µL and Attach 96-Channel pipette' ) }) @@ -164,7 +164,7 @@ describe('ChoosePipette', () => { props = { ...props, selectedPipette: NINETY_SIX_CHANNEL } render(props) screen.getByText( - 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel pipette' + 'Detach Flex 1-Channel 1000 µL and Attach 96-Channel pipette' ) }) }) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx index a8f85ef3d73..630b8772b1a 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/DetachPipette.test.tsx @@ -52,7 +52,7 @@ describe('DetachPipette', () => { }) it('returns the correct information, buttons work as expected for single mount pipettes', () => { render(props) - screen.getByText('Loosen screws and detach Flex 1-Channel 1000 μL') + screen.getByText('Loosen screws and detach Flex 1-Channel 1000 µL') screen.getByText( 'Hold the pipette in place and loosen the pipette screws. (The screws are captive and will not come apart from the pipette.) Then carefully remove the pipette.' ) @@ -83,7 +83,7 @@ describe('DetachPipette', () => { }, } render(props) - screen.getByText('Loosen screws and detach Flex 96-Channel 1000 μL') + screen.getByText('Loosen screws and detach Flex 96-Channel 1000 µL') screen.getByText( 'Hold the pipette in place and loosen the pipette screws. (The screws are captive and will not come apart from the pipette.) Then carefully remove the pipette.' ) @@ -113,7 +113,7 @@ describe('DetachPipette', () => { selectedPipette: NINETY_SIX_CHANNEL, } render(props) - screen.getByText('Loosen screws and detach Flex 1-Channel 1000 μL') + screen.getByText('Loosen screws and detach Flex 1-Channel 1000 µL') screen.getByText( 'Hold the pipette in place and loosen the pipette screws. (The screws are captive and will not come apart from the pipette.) Then carefully remove the pipette.' ) diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx index df53d95cdb7..21f53c295aa 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/Results.test.tsx @@ -69,7 +69,7 @@ describe('Results', () => { hasCalData: true, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully recalibrated') + screen.getByText('Flex 1-Channel 1000 µL successfully recalibrated') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -87,7 +87,7 @@ describe('Results', () => { flowType: FLOWS.ATTACH, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully attached') + screen.getByText('Flex 1-Channel 1000 µL successfully attached') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -187,7 +187,7 @@ describe('Results', () => { flowType: FLOWS.DETACH, } render(props) - screen.getByText('Flex 1-Channel 1000 μL still attached') + screen.getByText('Flex 1-Channel 1000 µL still attached') expect(screen.getByLabelText('ot-alert')).toHaveStyle( `color: ${String(COLORS.red50)}` ) @@ -222,7 +222,7 @@ describe('Results', () => { selectedPipette: NINETY_SIX_CHANNEL, } render(props) - screen.getByText('Flex 1-Channel 1000 μL still attached') + screen.getByText('Flex 1-Channel 1000 µL still attached') expect(screen.getByLabelText('ot-alert')).toHaveStyle( `color: ${String(COLORS.red50)}` ) @@ -254,7 +254,7 @@ describe('Results', () => { flowType: FLOWS.CALIBRATE, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully calibrated') + screen.getByText('Flex 1-Channel 1000 µL successfully calibrated') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -271,7 +271,7 @@ describe('Results', () => { totalStepCount: 9, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully calibrated') + screen.getByText('Flex 1-Channel 1000 µL successfully calibrated') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -288,7 +288,7 @@ describe('Results', () => { totalStepCount: 5, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully calibrated') + screen.getByText('Flex 1-Channel 1000 µL successfully calibrated') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -304,7 +304,7 @@ describe('Results', () => { hasCalData: true, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully recalibrated') + screen.getByText('Flex 1-Channel 1000 µL successfully recalibrated') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -340,7 +340,7 @@ describe('Results', () => { }, } render(props) - screen.getByText('Flex 1-Channel 1000 μL successfully attached') + screen.getByText('Flex 1-Channel 1000 µL successfully attached') const image = screen.getByRole('img', { name: 'Success Icon' }) expect(image.getAttribute('src')).toEqual( '/app/src/assets/images/icon_success.png' @@ -359,7 +359,7 @@ describe('Results', () => { } render(props) screen.getByText('Wrong instrument installed') - screen.getByText('Install Flex 8-Channel 50 μL instead') + screen.getByText('Install Flex 8-Channel 50 µL instead') fireEvent.click(screen.getByRole('button', { name: 'Detach and retry' })) await act(() => pipettePromise) expect(mockRefetchInstruments).toHaveBeenCalled() diff --git a/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx b/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx index 996ec520af2..4a3f1d8e7ba 100644 --- a/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx +++ b/app/src/organisms/PipetteWizardFlows/__tests__/hooks.test.tsx @@ -200,7 +200,7 @@ describe('usePipetteFlowWizardHeaderText', () => { } ) expect(result.current).toEqual( - 'Detach Flex 1-Channel 1000 μL and Attach 96-Channel Pipette' + 'Detach Flex 1-Channel 1000 µL and Attach 96-Channel Pipette' ) }) it('should return correct title for detaching single mount', () => { diff --git a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx index 5e78f28b4c9..c1d81beb97b 100644 --- a/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx +++ b/app/src/pages/ODD/InstrumentsDashboard/__tests__/InstrumentsDashboard.test.tsx @@ -118,9 +118,9 @@ describe('InstrumentsDashboard', () => { it('should render mount info for all attached mounts', () => { render('/instruments') screen.getByText('left Mount') - screen.getByText('Flex 1-Channel 1000 μL') + screen.getByText('Flex 1-Channel 1000 µL') screen.getByText('right Mount') - screen.getByText('Flex 1-Channel 50 μL') + screen.getByText('Flex 1-Channel 50 µL') screen.getByText('extension Mount') screen.getByText('Flex Gripper') }) diff --git a/app/src/redux/pipettes/__fixtures__/index.ts b/app/src/redux/pipettes/__fixtures__/index.ts index 8018bdcc5de..7582e12e24e 100644 --- a/app/src/redux/pipettes/__fixtures__/index.ts +++ b/app/src/redux/pipettes/__fixtures__/index.ts @@ -51,14 +51,14 @@ export const mockAttachedFlexPipette: Omit = { } export const mockFlexP1000PipetteSpecs: any = { - displayName: 'Flex 1-Channel 1000 μL', + displayName: 'Flex 1-Channel 1000 µL', name: 'p1000_single_flex', backCompatNames: ['p1000_single'], channels: 1, } export const mockFlexP1000Pipette8ChannelSpecs: any = { - displayName: 'Flex 8-Channel 1000 μL', + displayName: 'Flex 8-Channel 1000 µL', name: 'p1000_multi_flex', channels: 8, backCompatNames: ['p1000_multi'], @@ -355,7 +355,7 @@ export const mockPipetteData1Channel: PipetteData = { } export const mockAttachedPipetteInformation: PipetteInformation = { ...mockPipetteData1Channel, - displayName: 'Flex 1-Channel 1000 μL', + displayName: 'Flex 1-Channel 1000 µL', } export const mockPipetteData8Channel: PipetteData = { @@ -383,7 +383,7 @@ export const mockPipetteData8Channel: PipetteData = { } export const mock8ChannelAttachedPipetteInformation: PipetteInformation = { ...mockPipetteData8Channel, - displayName: 'Flex 8-Channel 1000 μL', + displayName: 'Flex 8-Channel 1000 µL', } export const mockPipetteData96Channel: PipetteData = { @@ -410,5 +410,5 @@ export const mockPipetteData96Channel: PipetteData = { } export const mock96ChannelAttachedPipetteInformation: PipetteInformation = { ...mockPipetteData96Channel, - displayName: 'Flex 96-Channel 1000 μL', + displayName: 'Flex 96-Channel 1000 µL', } diff --git a/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts b/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts index cc2bdbdc081..52485cf4d5a 100644 --- a/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts +++ b/app/src/resources/instruments/__tests__/useAttachedPipettesFromInstrumentsQuery.test.ts @@ -38,11 +38,11 @@ describe('useAttachedPipettesFromInstrumentsQuery hook', () => { expect(result.current).toEqual({ left: { ...instrumentsResponseLeftPipetteFixture, - displayName: 'Flex 1-Channel 1000 μL', + displayName: 'Flex 1-Channel 1000 µL', }, right: { ...instrumentsResponseRightPipetteFixture, - displayName: 'Flex 1-Channel 1000 μL', + displayName: 'Flex 1-Channel 1000 µL', }, }) }) diff --git a/components/src/forms/InputField.stories.tsx b/components/src/forms/InputField.stories.tsx index 2ca998acd2f..21ee50dfb97 100644 --- a/components/src/forms/InputField.stories.tsx +++ b/components/src/forms/InputField.stories.tsx @@ -35,7 +35,7 @@ export const InputField = Template.bind({}) InputField.args = { label: 'Input field', placeholder: 'Placeholder Text', - units: 'μL', + units: 'µL', caption: 'caption here', isIndeterminate: false, } diff --git a/labware-library/src/labware-creator/components/sections/Volume.tsx b/labware-library/src/labware-creator/components/sections/Volume.tsx index 536c86b23e4..50035914ee7 100644 --- a/labware-library/src/labware-creator/components/sections/Volume.tsx +++ b/labware-library/src/labware-creator/components/sections/Volume.tsx @@ -23,7 +23,7 @@ const Content = (props: ContentProps): JSX.Element => {
- +
) diff --git a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx b/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx index 0712ede4757..93a0ed161e4 100644 --- a/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx +++ b/opentrons-ai-client/src/organisms/InstrumentsSection/__tests__/InstrumentsSection.test.tsx @@ -86,14 +86,14 @@ describe('ApplicationSection', () => { const leftMount = screen.getAllByText('Choose pipette')[0] fireEvent.click(leftMount) - fireEvent.click(screen.getByText('Flex 1-Channel 50 μL')) + fireEvent.click(screen.getByText('Flex 1-Channel 50 µL')) const rightMount = screen.getByText('Choose pipette') fireEvent.click(rightMount) fireEvent.click(screen.getByText('None')) await waitFor(() => { - expect(screen.getByText('Flex 1-Channel 50 μL')).toBeInTheDocument() + expect(screen.getByText('Flex 1-Channel 50 µL')).toBeInTheDocument() }) expect(screen.getByText('None')).toBeInTheDocument() @@ -129,11 +129,11 @@ describe('ApplicationSection', () => { const leftMount = screen.getAllByText('Choose pipette')[0] fireEvent.click(leftMount) - fireEvent.click(screen.getByText('Flex 1-Channel 50 μL')) + fireEvent.click(screen.getByText('Flex 1-Channel 50 µL')) const rightMount = screen.getByText('Choose pipette') fireEvent.click(rightMount) - fireEvent.click(screen.getByText('Flex 8-Channel 50 μL')) + fireEvent.click(screen.getByText('Flex 8-Channel 50 µL')) await waitFor(() => { expect(screen.getByText('form is valid')).toBeInTheDocument() diff --git a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx index 919e5f735e8..1b4cc0df92e 100644 --- a/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx +++ b/opentrons-ai-client/src/pages/CreateProtocol/__tests__/CreateProtocol.test.tsx @@ -119,8 +119,8 @@ describe('CreateProtocol', () => { expect(previewItems[0]).toHaveTextContent('Basic aliquoting') expect(previewItems[1]).toHaveTextContent('Test description') expect(previewItems[2]).toHaveTextContent('Opentrons Flex') - expect(previewItems[3]).toHaveTextContent('Flex 1-Channel 50 μL') - expect(previewItems[4]).toHaveTextContent('Flex 8-Channel 50 μL') + expect(previewItems[3]).toHaveTextContent('Flex 1-Channel 50 µL') + expect(previewItems[4]).toHaveTextContent('Flex 8-Channel 50 µL') }) it('should open the Modules section when the Instruments section is completed', async () => { diff --git a/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx index 55f13465f0c..bd19e1dfa3d 100644 --- a/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx +++ b/opentrons-ai-client/src/resources/utils/createProtocolTestUtils.tsx @@ -21,11 +21,11 @@ export async function fillApplicationSectionAndClickConfirm(): Promise { export async function fillInstrumentsSectionAndClickConfirm(): Promise { const leftMount = screen.getAllByText('Choose pipette')[0] fireEvent.click(leftMount) - fireEvent.click(screen.getByText('Flex 1-Channel 50 μL')) + fireEvent.click(screen.getByText('Flex 1-Channel 50 µL')) const rightMount = screen.getAllByText('Choose pipette')[0] fireEvent.click(rightMount) - fireEvent.click(screen.getByText('Flex 8-Channel 50 μL')) + fireEvent.click(screen.getByText('Flex 8-Channel 50 µL')) const confirmButton = screen.getByText('Confirm') await waitFor(() => { diff --git a/opentrons-ai-server/api/data/python_api_219_docs.md b/opentrons-ai-server/api/data/python_api_219_docs.md index 31d3ce47404..5652ffadbce 100644 --- a/opentrons-ai-server/api/data/python_api_219_docs.md +++ b/opentrons-ai-server/api/data/python_api_219_docs.md @@ -53,7 +53,7 @@ To simulate your code, you’ll need [Python 3\.10](https://www.python.org/downl Before running a protocol, you’ll want to have the right kind of hardware and labware ready for your Flex or OT\-2\. -- **Flex users** should review Chapter 2: Installation and Relocation in the [instruction manual](https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Manual.pdf). Specifically, see the pipette information in the “Instrument Installation and Calibration” section. You can use either a 1\-channel or 8\-channel pipette for this tutorial. Most Flex code examples will use a [Flex 1\-Channel 1000 μL pipette](https://shop.opentrons.com/opentrons-flex-1-channel-pipette/). +- **Flex users** should review Chapter 2: Installation and Relocation in the [instruction manual](https://insights.opentrons.com/hubfs/Products/Flex/Opentrons%20Flex%20Manual.pdf). Specifically, see the pipette information in the “Instrument Installation and Calibration” section. You can use either a 1\-channel or 8\-channel pipette for this tutorial. Most Flex code examples will use a [Flex 1\-Channel 1000 µL pipette](https://shop.opentrons.com/opentrons-flex-1-channel-pipette/). - **OT\-2 users** should review the robot setup and pipette information on the [Get Started page](https://support.opentrons.com/s/ot2-get-started). Specifically, see [attaching pipettes](https://support.opentrons.com/s/article/Get-started-Attach-pipettes) and [initial calibration](https://support.opentrons.com/s/article/Get-started-Calibrate-the-deck). You can use either a single\-channel or 8\-channel pipette for this tutorial. Most OT\-2 code examples will use a [P300 Single\-Channel GEN2](https://shop.opentrons.com/single-channel-electronic-pipette-p20/) pipette. The Flex and OT\-2 use similar labware for serial dilution. The tutorial code will use the labware listed in the table below, but as long as you have labware of each type you can modify the code to run with your labware. @@ -1643,9 +1643,9 @@ The Temperature Module supports these 96\-well block and labware combinations fo | 96\-well block contents | API Load Name | | -------------------------- | ---------------------------------------------------- | -| Bio\-Rad well plate 200 μL | `opentrons_96_aluminumblock_biorad_wellplate_200uL` | -| Generic PCR strip 200 μL | `opentrons_96_aluminumblock_generic_pcr_strip_200uL` | -| NEST well plate 100 μL | `opentrons_96_aluminumblock_nest_wellplate_100uL` | +| Bio\-Rad well plate 200 µL | `opentrons_96_aluminumblock_biorad_wellplate_200uL` | +| Generic PCR strip 200 µL | `opentrons_96_aluminumblock_generic_pcr_strip_200uL` | +| NEST well plate 100 µL | `opentrons_96_aluminumblock_nest_wellplate_100uL` | This command loads the same physical adapter and labware as the example in the Standalone Adapters section above, but it is also compatible with earlier API versions: @@ -3756,7 +3756,7 @@ One reason to set `new_tip="always"` is to avoid cross\-contamination between we [`transfer()`](index.html#opentrons.protocol_api.InstrumentContext.transfer 'opentrons.protocol_api.InstrumentContext.transfer') will pick up a new tip before _every_ aspirate when `new_tip="always"`. This includes when [tip refilling](index.html#complex-tip-refilling) requires multiple aspirations from a single source well. -[`distribute()`](index.html#opentrons.protocol_api.InstrumentContext.distribute 'opentrons.protocol_api.InstrumentContext.distribute') and [`consolidate()`](index.html#opentrons.protocol_api.InstrumentContext.consolidate 'opentrons.protocol_api.InstrumentContext.consolidate') only pick up one tip, even when `new_tip="always"`. For example, this distribute command returns to the source well a second time, because the amount to be distributed (400 µL total plus disposal volume) exceeds the pipette capacity (300 μL): +[`distribute()`](index.html#opentrons.protocol_api.InstrumentContext.distribute 'opentrons.protocol_api.InstrumentContext.distribute') and [`consolidate()`](index.html#opentrons.protocol_api.InstrumentContext.consolidate 'opentrons.protocol_api.InstrumentContext.consolidate') only pick up one tip, even when `new_tip="always"`. For example, this distribute command returns to the source well a second time, because the amount to be distributed (400 µL total plus disposal volume) exceeds the pipette capacity (300 µL): ``` pipette.distribute( diff --git a/opentrons-ai-server/api/domain/fake_responses.py b/opentrons-ai-server/api/domain/fake_responses.py index 21eedb9bf0e..b60dddcf022 100644 --- a/opentrons-ai-server/api/domain/fake_responses.py +++ b/opentrons-ai-server/api/domain/fake_responses.py @@ -32,7 +32,7 @@ class FakeResponse(BaseModel): fake=True, ) -no_markdown: ChatResponse = ChatResponse(reply="👀 at me I am a response without markdown! 😊 99.99μl", fake=True) +no_markdown: ChatResponse = ChatResponse(reply="👀 at me I am a response without markdown! 😊 99.99µl", fake=True) empty_reply: ChatResponse = ChatResponse(reply="", fake=True) diff --git a/opentrons-ai-server/api/domain/utils.py b/opentrons-ai-server/api/domain/utils.py index 6b54bb0c477..abaa50972f4 100644 --- a/opentrons-ai-server/api/domain/utils.py +++ b/opentrons-ai-server/api/domain/utils.py @@ -13,7 +13,7 @@ def refine_characters(prompt: str) -> str: and certain special characters like backticks replaced with single quotes. Example: - >>> refine_characters("Transfer `10μ`") + >>> refine_characters("Transfer `10µ`") 'Transfer '10m'' """ @@ -29,7 +29,7 @@ def refine_characters(prompt: str) -> str: "ι": "i", "κ": "k", "λ": "l", - "μ": "m", + "µ": "m", "ν": "n", "ξ": "x", "ο": "o", diff --git a/opentrons-ai-server/api/storage/docs/serial_dilution_examples.md b/opentrons-ai-server/api/storage/docs/serial_dilution_examples.md index 0fa0ed870e2..cce4ceb9056 100644 --- a/opentrons-ai-server/api/storage/docs/serial_dilution_examples.md +++ b/opentrons-ai-server/api/storage/docs/serial_dilution_examples.md @@ -12,7 +12,7 @@ The lab task that you'll automate in this tutorial is serial dilution: taking a Before running a protocol, you'll want to have the right kind of hardware and labware ready for your Flex or OT-2. -- **Flex users** Most Flex code examples will use a Flex 1-Channel 1000 μL pipette. +- **Flex users** Most Flex code examples will use a Flex 1-Channel 1000 µL pipette. - **OT-2 users** You can use either a single-channel or 8-channel pipette for this tutorial. Most OT-2 code examples will use a P300 Single-Channel GEN2 pipette. The Flex and OT-2 use similar labware for serial dilution. The tutorial code will use the labware listed in the table below, but as long as you have labware of each type you can modify the code to run with your labware. diff --git a/protocol-designer/cypress/support/createNew.ts b/protocol-designer/cypress/support/createNew.ts index 02430507405..129edc636c8 100644 --- a/protocol-designer/cypress/support/createNew.ts +++ b/protocol-designer/cypress/support/createNew.ts @@ -91,7 +91,7 @@ export enum Content { TipRack = 'Filter Tip Rack 50 µL', PipetteType = 'Pipette type', PipetteVolume = 'Pipette volume', - FullP50SingleName = 'Flex 1-Channel 50 μL', + FullP50SingleName = 'Flex 1-Channel 50 µL', FullP50TiprackName = 'Opentrons Flex 96 Filter Tip Rack 50 µL', GoBack = 'Go back', Confirm = 'Confirm', diff --git a/protocol-designer/src/assets/localization/en/alert.json b/protocol-designer/src/assets/localization/en/alert.json index 3c3d5a560a4..72996105972 100644 --- a/protocol-designer/src/assets/localization/en/alert.json +++ b/protocol-designer/src/assets/localization/en/alert.json @@ -267,7 +267,7 @@ "unused_pipette_content": { "heading": "Protocol has unused pipette", "body1": "The {{pipette}} on {{mount}} mount is currently not used in any step. You won't be able to run this protocol unless this pipette is attached to your robot.", - "body1_96ch": "The Flex 8-Channel 1000 μL is currently not used in any step. You won't be able to run this protocol unless this pipette is attached to your robot.", + "body1_96ch": "The Flex 8-Channel 1000 µL is currently not used in any step. You won't be able to run this protocol unless this pipette is attached to your robot.", "body2": "If you don’t intend to use the pipette, remove it from your protocol." }, "unused_staging_area_content": { diff --git a/protocol-designer/src/assets/localization/en/application.json b/protocol-designer/src/assets/localization/en/application.json index 0ef5a366e5c..b6d0505a0d1 100644 --- a/protocol-designer/src/assets/localization/en/application.json +++ b/protocol-designer/src/assets/localization/en/application.json @@ -54,8 +54,8 @@ "cycles": "cycles", "degrees": "°C", "hours": "h", - "microliter": "μL", - "microliterPerSec": "μL/s", + "microliter": "µL", + "microliterPerSec": "µL/s", "millimeter": "mm", "nanometer": "nm", "minutes": "m", diff --git a/protocol-designer/src/pages/ProtocolOverview/__tests__/InstrumentsInfo.test.tsx b/protocol-designer/src/pages/ProtocolOverview/__tests__/InstrumentsInfo.test.tsx index 89d12f63f59..4165fa2b51e 100644 --- a/protocol-designer/src/pages/ProtocolOverview/__tests__/InstrumentsInfo.test.tsx +++ b/protocol-designer/src/pages/ProtocolOverview/__tests__/InstrumentsInfo.test.tsx @@ -110,9 +110,9 @@ describe('InstrumentsInfo', () => { } render(props) - screen.getByText('Flex 1-Channel 50 μL') + screen.getByText('Flex 1-Channel 50 µL') screen.getByText('Opentrons Flex 96 Tip Rack 50 µL') - screen.getByText('Flex 8-Channel 50 μL') + screen.getByText('Flex 8-Channel 50 µL') screen.getByText('Opentrons Flex 96 Filter Tip Rack 50 µL') screen.getByText('Opentrons Flex Gripper') }) diff --git a/shared-data/js/__tests__/pipettes.test.ts b/shared-data/js/__tests__/pipettes.test.ts index 99895132440..9613539262b 100644 --- a/shared-data/js/__tests__/pipettes.test.ts +++ b/shared-data/js/__tests__/pipettes.test.ts @@ -76,7 +76,7 @@ describe('pipette data accessors', () => { backlashDistance: 0.1, channels: 1, displayCategory: 'FLEX', - displayName: 'Flex 1-Channel 1000 μL', + displayName: 'Flex 1-Channel 1000 µL', dropTipConfigurations: { plungerEject: { current: 1, speed: 15 } }, liquids: { default: { diff --git a/shared-data/js/titleCase.ts b/shared-data/js/titleCase.ts index a03effcbc2b..6277151824b 100644 --- a/shared-data/js/titleCase.ts +++ b/shared-data/js/titleCase.ts @@ -1,4 +1,4 @@ -const SMALL_WORDS = /\b(?:an?d?|a[st]|because|but|by|en|for|i[fn]|neither|nor|o[fnr]|only|over|per|μL|so|some|tha[tn]|the|to|up|upon|vs?\.?|versus|via|when|with|without|yet)\b/i +const SMALL_WORDS = /\b(?:an?d?|a[st]|because|but|by|en|for|i[fn]|neither|nor|o[fnr]|only|over|per|µL|so|some|tha[tn]|the|to|up|upon|vs?\.?|versus|via|when|with|without|yet)\b/i const TOKENS = /[^\s:–—-]+|./g const WHITESPACE = /\s/ const IS_MANUAL_CASE = /.(?=[A-Z]|\..)/ diff --git a/shared-data/pipette/definitions/1/pipetteNameSpecs.json b/shared-data/pipette/definitions/1/pipetteNameSpecs.json index 37d44414ea1..103b4a44123 100644 --- a/shared-data/pipette/definitions/1/pipetteNameSpecs.json +++ b/shared-data/pipette/definitions/1/pipetteNameSpecs.json @@ -457,7 +457,7 @@ ] }, "p50_single_flex": { - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 35, @@ -502,7 +502,7 @@ ] }, "p1000_single_flex": { - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 478, @@ -552,7 +552,7 @@ ] }, "p1000_multi_flex": { - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 478, @@ -600,7 +600,7 @@ ] }, "p1000_multi_em_flex": { - "displayName": "Flex 8-Channel EM 1000 μL", + "displayName": "Flex 8-Channel EM 1000 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 478, @@ -648,7 +648,7 @@ ] }, "p50_multi_flex": { - "displayName": "Flex 8-Channel 50 μL", + "displayName": "Flex 8-Channel 50 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 35, @@ -693,7 +693,7 @@ ] }, "p1000_96": { - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "displayCategory": "GEN1", "defaultAspirateFlowRate": { "value": 6, @@ -740,7 +740,7 @@ ] }, "p200_96": { - "displayName": "Flex 96-Channel 200 μL", + "displayName": "Flex 96-Channel 200 µL", "displayCategory": "FLEX", "defaultAspirateFlowRate": { "value": 7.85, diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json index 62033d0444c..f44f1561199 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json index f7a517120de..8c6b13d059a 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_3.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json index 33eca65b2fc..f6b7f855cfd 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_4.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json index 33eca65b2fc..f6b7f855cfd 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p1000/3_5.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 1000 μL", + "displayName": "Flex 8-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json index 514677f27f1..828f021e54b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 50 μL", + "displayName": "Flex 8-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json index d5ef4b281ce..a5d9916d1c9 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_3.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 50 μL", + "displayName": "Flex 8-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json index d62d02e1b06..2442190e41b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_4.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 50 μL", + "displayName": "Flex 8-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json index d62d02e1b06..2442190e41b 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/eight_channel/p50/3_5.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 8-Channel 50 μL", + "displayName": "Flex 8-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/1_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/1_0.json index c267504b404..df1d00565dd 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/1_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "FLEX 8-Channel EM 1000 μL", + "displayName": "FLEX 8-Channel EM 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json index c267504b404..df1d00565dd 100644 --- a/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/eight_channel_em/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "FLEX 8-Channel EM 1000 μL", + "displayName": "FLEX 8-Channel EM 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json index 4e4ad89ca3f..715c6daf712 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/1_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json index 2b0faca936d..a25361875a6 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json index 853da180a1b..ba27935f317 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_3.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json index a47bbfc76da..eed4c0529d9 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_4.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json index 9c6df88b575..16031e8136a 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_5.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json index 7bcfb04e4f0..902eca9abe8 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p1000/3_6.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/1_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/1_0.json index 5719bb3437c..4ce934023ce 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/1_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/1_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 200 μL", + "displayName": "Flex 96-Channel 200 µL", "model": "p200", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json index 5719bb3437c..4ce934023ce 100644 --- a/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json +++ b/shared-data/pipette/definitions/2/general/ninety_six_channel/p200/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 96-Channel 200 μL", + "displayName": "Flex 96-Channel 200 µL", "model": "p200", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json index 6877ce9e12e..51309658e0d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json index 6877ce9e12e..51309658e0d 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_3.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json index f0ebd0e00a3..d2e6eab6467 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_4.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json index f0ebd0e00a3..d2e6eab6467 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_5.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_6.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_6.json index f0ebd0e00a3..d2e6eab6467 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_6.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_6.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_7.json b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_7.json index b1d202e897f..3eb596feec9 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p1000/3_7.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p1000/3_7.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 1000 μL", + "displayName": "Flex 1-Channel 1000 µL", "model": "p1000", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json index a2912a2a628..76b0aa4f439 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_0.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json index a2912a2a628..76b0aa4f439 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_3.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json index 3f8ba3c4a75..e928b494320 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_4.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json index 3f8ba3c4a75..e928b494320 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_5.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/definitions/2/general/single_channel/p50/3_6.json b/shared-data/pipette/definitions/2/general/single_channel/p50/3_6.json index 3f8ba3c4a75..e928b494320 100644 --- a/shared-data/pipette/definitions/2/general/single_channel/p50/3_6.json +++ b/shared-data/pipette/definitions/2/general/single_channel/p50/3_6.json @@ -1,6 +1,6 @@ { "$otSharedSchema": "#/pipette/schemas/2/pipettePropertiesSchema.json", - "displayName": "Flex 1-Channel 50 μL", + "displayName": "Flex 1-Channel 50 µL", "model": "p50", "displayCategory": "FLEX", "validNozzleMaps": { diff --git a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json index 92d9b6da4b4..0a5f1cd4a97 100644 --- a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json +++ b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json @@ -147,7 +147,7 @@ "maxVolume": 1000 }, "p1000_96": { - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "defaultAspirateFlowRate": { "value": 7.85, "min": 3, diff --git a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap index 991fdb2edff..82e4b097eef 100644 --- a/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap +++ b/step-generation/src/__tests__/__snapshots__/fixtureGeneration.test.ts.snap @@ -9114,7 +9114,7 @@ exports[`snapshot tests > makeContext 1`] = ` "backlashDistance": 0.3, "channels": 96, "displayCategory": "FLEX", - "displayName": "Flex 96-Channel 1000 μL", + "displayName": "Flex 96-Channel 1000 µL", "dropTipConfigurations": { "camAction": { "current": 1.5, diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index 12a9f2e59e2..a871112a180 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -103,7 +103,7 @@ export function tipVolumeExceeded(args: { }): CommandCreatorError { const { volume, maxVolume, actionName } = args return { - message: `This step tries to ${actionName} ${volume}μL, but the tip can only hold ${maxVolume}μL.`, + message: `This step tries to ${actionName} ${volume}µL, but the tip can only hold ${maxVolume}µL.`, type: 'TIP_VOLUME_EXCEEDED', } } @@ -118,7 +118,7 @@ export function pipetteVolumeExceeded(args: { const message = disposalVolume != null ? `Attemped to ${actionName} volume + disposal volume greater than pipette max volume (${volume} + ${disposalVolume} > ${maxVolume})` - : `This step tries to ${actionName} ${volume}μL, but the tip can only hold ${maxVolume}μL.` + : `This step tries to ${actionName} ${volume}µL, but the tip can only hold ${maxVolume}µL.` return { message, type: 'PIPETTE_VOLUME_EXCEEDED', From 78b89ed33a585694d9df62c908d10afdfadbd9e8 Mon Sep 17 00:00:00 2001 From: Nick Diehl <47604184+ncdiehl11@users.noreply.github.com> Date: Fri, 24 Jan 2025 10:29:50 -0500 Subject: [PATCH 4/4] fix(components, protocol-designer): implement ListButton in step forms (#17340) ListItems used for toggle/toggleExpand form fields in PD went unnoticed-- according to designs, these fields should actually return ListButtons. This PR swaps those components out where necessary and adds event stopPropogation functions to children input fields and dropdown menus click handlers. --- .../src/molecules/DropdownMenu/index.tsx | 5 +-- .../molecules/InputStepFormField/index.tsx | 3 ++ .../ToggleExpandStepFormField/index.tsx | 22 ++++++------- .../molecules/ToggleStepFormField/index.tsx | 21 ++++++------- .../AbsorbanceReaderTools/Initialization.tsx | 31 +++++++++++-------- .../pages/Designer/ProtocolSteps/index.tsx | 7 ++++- 6 files changed, 49 insertions(+), 40 deletions(-) diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index b1dd875e573..276be51511a 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -243,7 +243,7 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { { - e.preventDefault() + e.stopPropagation() toggleSetShowDropdownMenu() }} onFocus={onFocus} @@ -300,9 +300,10 @@ export function DropdownMenu(props: DropdownMenuProps): JSX.Element { disabled={option.disabled} zIndex={3} key={`${option.name}-${index}`} - onClick={() => { + onClick={e => { onClick(option.value) setShowDropdownMenu(false) + e.stopPropagation() }} border="none" onMouseEnter={() => onEnter?.(option.value)} diff --git a/protocol-designer/src/molecules/InputStepFormField/index.tsx b/protocol-designer/src/molecules/InputStepFormField/index.tsx index b3a30953890..8fc6ec64dee 100644 --- a/protocol-designer/src/molecules/InputStepFormField/index.tsx +++ b/protocol-designer/src/molecules/InputStepFormField/index.tsx @@ -52,6 +52,9 @@ export function InputStepFormField( name={name} error={formLevelError ?? errorToShow} onBlur={onFieldBlur} + onClick={e => { + e.stopPropagation() + }} onFocus={onFieldFocus} onChange={e => { updateValue(e.currentTarget.value) diff --git a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx index a99f6547f7a..075b9966e16 100644 --- a/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx +++ b/protocol-designer/src/molecules/ToggleExpandStepFormField/index.tsx @@ -1,12 +1,11 @@ import { ALIGN_CENTER, - Btn, COLORS, Check, DIRECTION_COLUMN, Flex, JUSTIFY_SPACE_BETWEEN, - ListItem, + ListButton, SPACING, StyledText, } from '@opentrons/components' @@ -73,12 +72,12 @@ export function ToggleExpandStepFormField( const label = isSelected ? onLabel : offLabel ?? null return ( - - + + {title} @@ -92,14 +91,11 @@ export function ToggleExpandStepFormField( ) : null} {toggleElement === 'toggle' ? ( ) : ( - - - + )} @@ -121,6 +117,6 @@ export function ToggleExpandStepFormField( ) : null} - + ) } diff --git a/protocol-designer/src/molecules/ToggleStepFormField/index.tsx b/protocol-designer/src/molecules/ToggleStepFormField/index.tsx index eaa7c371310..bf730e2523c 100644 --- a/protocol-designer/src/molecules/ToggleStepFormField/index.tsx +++ b/protocol-designer/src/molecules/ToggleStepFormField/index.tsx @@ -4,7 +4,7 @@ import { DIRECTION_COLUMN, Flex, JUSTIFY_SPACE_BETWEEN, - ListItem, + ListButton, SPACING, StyledText, TOOLTIP_BOTTOM, @@ -43,12 +43,14 @@ export function ToggleStepFormField( return ( <> - - + { + toggleUpdateValue(!toggleValue) + }} + > + { - toggleUpdateValue(!toggleValue) - }} label={isSelected ? onLabel : offLabel} toggledOn={isSelected} /> @@ -78,7 +77,7 @@ export function ToggleStepFormField( - + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx index 3bf0a32df2e..1b388e8f4ea 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/AbsorbanceReaderTools/Initialization.tsx @@ -14,6 +14,7 @@ import { Icon, InputField, JUSTIFY_SPACE_BETWEEN, + ListButton, ListItem, RadioButton, SPACING, @@ -383,28 +384,29 @@ function ReferenceWavelength(props: ReferenceWavelengthProps): JSX.Element { {t('step_edit_form.absorbanceReader.reference_wavelength.tooltip')} - { + propsForFields.referenceWavelengthActive.updateValue(!isExpanded) + }} > - + {t( 'step_edit_form.field.absorbanceReader.referenceWavelengthActive' )} - { - propsForFields.referenceWavelengthActive.updateValue(!isExpanded) - }} - > - - + {isExpanded ? ( <> @@ -446,6 +448,9 @@ function ReferenceWavelength(props: ReferenceWavelengthProps): JSX.Element { maskToInteger(e.target.value) ) }} + onClick={e => { + e.stopPropagation() + }} onBlur={() => { setIsFocused(false) }} @@ -457,7 +462,7 @@ function ReferenceWavelength(props: ReferenceWavelengthProps): JSX.Element { ) : null} ) : null} - + ) } diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx index 38f4979b0cf..77557abc0f7 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/index.tsx @@ -104,7 +104,12 @@ export function ProtocolSteps(): JSX.Element { maxWidth={CONTENT_MAX_WIDTH} > {showTimelineAlerts ? ( - + ) : null} {currentStep != null && hoveredTerminalItem == null ? (