diff --git a/components/src/molecules/DropdownMenu/index.tsx b/components/src/molecules/DropdownMenu/index.tsx index b1dd875e573..037ab8f8677 100644 --- a/components/src/molecules/DropdownMenu/index.tsx +++ b/components/src/molecules/DropdownMenu/index.tsx @@ -38,7 +38,7 @@ export interface DropdownOption { /** subtext below the name */ subtext?: string disabled?: boolean - tooltipText?: string + tooltipText?: string | null } export type DropdownBorder = 'rounded' | 'neutral' diff --git a/protocol-designer/src/assets/localization/en/feature_flags.json b/protocol-designer/src/assets/localization/en/feature_flags.json index 92a074088ba..585cb19aa9b 100644 --- a/protocol-designer/src/assets/localization/en/feature_flags.json +++ b/protocol-designer/src/assets/localization/en/feature_flags.json @@ -35,5 +35,9 @@ "OT_PD_ENABLE_LIQUID_CLASSES": { "title": "Enable liquid classes", "description": "Enable liquid classes support" + }, + "OT_PD_ENABLE_PARTIAL_TIP_SUPPORT": { + "title": "Enable partial tip support", + "description": "Partial tip configurations that are not released yet" } } diff --git a/protocol-designer/src/assets/localization/en/form.json b/protocol-designer/src/assets/localization/en/form.json index 98a1e328809..3f80e462b6b 100644 --- a/protocol-designer/src/assets/localization/en/form.json +++ b/protocol-designer/src/assets/localization/en/form.json @@ -127,7 +127,7 @@ "COLUMN": "Column" }, "option_tooltip": { - "COLUMN": "To use column partial tip pickup, a tiprack without an adapter must be placed on the deck." + "partial": "To use partial tip pickup, a tiprack without an adapter must be placed on the deck." } }, "path": { diff --git a/protocol-designer/src/assets/localization/en/protocol_steps.json b/protocol-designer/src/assets/localization/en/protocol_steps.json index 12ebc493447..f7a9457a35f 100644 --- a/protocol-designer/src/assets/localization/en/protocol_steps.json +++ b/protocol-designer/src/assets/localization/en/protocol_steps.json @@ -103,6 +103,7 @@ "select_volume": "Select a volume", "shake": "Shake", "single": "Single path", + "single_nozzle": "Single", "speed": "Speed", "starting_deck": "Starting deck", "step_substeps": "{{stepType}} details", diff --git a/protocol-designer/src/feature-flags/reducers.ts b/protocol-designer/src/feature-flags/reducers.ts index 336014c6691..8d65dacd619 100644 --- a/protocol-designer/src/feature-flags/reducers.ts +++ b/protocol-designer/src/feature-flags/reducers.ts @@ -32,6 +32,8 @@ const initialFlags: Flags = { OT_PD_ENABLE_REACT_SCAN: process.env.OT_PD_ENABLE_REACT_SCAN === '1' || false, OT_PD_ENABLE_LIQUID_CLASSES: process.env.OT_PD_ENABLE_LIQUID_CLASSES === '1' || false, + OT_PD_ENABLE_PARTIAL_TIP_SUPPORT: + process.env.OT_PD_ENABLE_PARTIAL_TIP_SUPPORT === '1' || false, } // @ts-expect-error(sa, 2021-6-10): cannot use string literals as action type // TODO IMMEDIATELY: refactor this to the old fashioned way if we cannot have type safety: https://github.com/redux-utilities/redux-actions/issues/282#issuecomment-595163081 diff --git a/protocol-designer/src/feature-flags/selectors.ts b/protocol-designer/src/feature-flags/selectors.ts index 6b8a70f8b30..9239314739a 100644 --- a/protocol-designer/src/feature-flags/selectors.ts +++ b/protocol-designer/src/feature-flags/selectors.ts @@ -49,3 +49,7 @@ export const getEnableLiquidClasses: Selector = createSelector( getFeatureFlagData, flags => flags.OT_PD_ENABLE_LIQUID_CLASSES ?? false ) +export const getEnablePartialTipSupport: Selector = createSelector( + getFeatureFlagData, + flags => flags.OT_PD_ENABLE_PARTIAL_TIP_SUPPORT ?? false +) diff --git a/protocol-designer/src/feature-flags/types.ts b/protocol-designer/src/feature-flags/types.ts index 6840786d149..d28d8bef855 100644 --- a/protocol-designer/src/feature-flags/types.ts +++ b/protocol-designer/src/feature-flags/types.ts @@ -37,6 +37,7 @@ export type FlagTypes = | 'OT_PD_ENABLE_HOT_KEYS_DISPLAY' | 'OT_PD_ENABLE_REACT_SCAN' | 'OT_PD_ENABLE_LIQUID_CLASSES' + | 'OT_PD_ENABLE_PARTIAL_TIP_SUPPORT' // flags that are not in this list only show in prerelease mode export const userFacingFlags: FlagTypes[] = [ 'OT_PD_DISABLE_MODULE_RESTRICTIONS', @@ -51,5 +52,6 @@ export const allFlags: FlagTypes[] = [ 'OT_PD_ENABLE_RETURN_TIP', 'OT_PD_ENABLE_REACT_SCAN', 'OT_PD_ENABLE_LIQUID_CLASSES', + 'OT_PD_ENABLE_PARTIAL_TIP_SUPPORT', ] export type Flags = Partial> diff --git a/protocol-designer/src/organisms/Labware/SelectableLabware.tsx b/protocol-designer/src/organisms/Labware/SelectableLabware.tsx index 27c77f7e7cb..1e6eccc4fd2 100644 --- a/protocol-designer/src/organisms/Labware/SelectableLabware.tsx +++ b/protocol-designer/src/organisms/Labware/SelectableLabware.tsx @@ -1,6 +1,6 @@ import reduce from 'lodash/reduce' -import { COLUMN } from '@opentrons/shared-data' +import { COLUMN, SINGLE } from '@opentrons/shared-data' import { COLORS } from '@opentrons/components' import { @@ -39,7 +39,7 @@ export interface SelectableLabwareProps { type ChannelType = 8 | 96 -const getChannelsFromNozleType = (nozzleType: NozzleType): ChannelType => { +const getChannelsFromNozzleType = (nozzleType: NozzleType): ChannelType => { if (nozzleType === '8-channel' || nozzleType === COLUMN) { return 8 } else { @@ -67,8 +67,8 @@ export const SelectableLabware = ( selectedWells: WellGroup ) => WellGroup = selectedWells => { // Returns PRIMARY WELLS from the selection. - if (nozzleType != null) { - const channels = getChannelsFromNozleType(nozzleType) + if (nozzleType !== null && nozzleType !== SINGLE) { + const channels = getChannelsFromNozzleType(nozzleType) // for the wells that have been highlighted, // get all 8-well well sets and merge them const primaryWells: WellGroup = reduce( @@ -101,8 +101,8 @@ export const SelectableLabware = ( rect ) => { if (!e.shiftKey) { - if (nozzleType != null) { - const channels = getChannelsFromNozleType(nozzleType) + if (nozzleType !== null && nozzleType !== SINGLE) { + const channels = getChannelsFromNozzleType(nozzleType) const selectedWells = _getWellsFromRect(rect) const allWellsForMulti: WellGroup = reduce( selectedWells, @@ -142,8 +142,8 @@ export const SelectableLabware = ( } const handleMouseEnterWell: (args: WellMouseEvent) => void = args => { - if (nozzleType != null) { - const channels = getChannelsFromNozleType(nozzleType) + if (nozzleType !== null && nozzleType !== SINGLE) { + const channels = getChannelsFromNozzleType(nozzleType) const wellSet = getWellSetForMultichannel({ labwareDef, wellName: args.wellName, @@ -158,11 +158,11 @@ export const SelectableLabware = ( // For rendering, show all wells not just primary wells const allSelectedWells = - nozzleType != null + nozzleType !== null && nozzleType !== SINGLE ? reduce( selectedPrimaryWells, (acc, _, wellName): WellGroup => { - const channels = getChannelsFromNozleType(nozzleType) + const channels = getChannelsFromNozzleType(nozzleType) const wellSet = getWellSetForMultichannel({ labwareDef, wellName, diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx index 1410bbfda40..2598617619d 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/PipetteFields/PartialTipField.tsx @@ -1,50 +1,74 @@ -import { useEffect, useState } from 'react' +import { useState } from 'react' import { useTranslation } from 'react-i18next' import { useSelector } from 'react-redux' -import { ALL, COLUMN } from '@opentrons/shared-data' +import { ALL, COLUMN, SINGLE } from '@opentrons/shared-data' import { Flex, DropdownMenu, SPACING } from '@opentrons/components' +import { getEnablePartialTipSupport } from '../../../../../feature-flags/selectors' import { getInitialDeckSetup } from '../../../../../step-forms/selectors' +import type { PipetteV2Specs } from '@opentrons/shared-data' +import type { DropdownOption } from '@opentrons/components' import type { FieldProps } from '../types' -export function PartialTipField(props: FieldProps): JSX.Element { +interface PartialTipFieldProps extends FieldProps { + pipetteSpecs: PipetteV2Specs +} +export function PartialTipField(props: PartialTipFieldProps): JSX.Element { const { value: dropdownItem, updateValue, errorToShow, padding = `0 ${SPACING.spacing16}`, tooltipContent, + pipetteSpecs, } = props const { t } = useTranslation('protocol_steps') const deckSetup = useSelector(getInitialDeckSetup) + const enablePartialTip = useSelector(getEnablePartialTipSupport) + const is96Channel = pipetteSpecs.channels === 96 + const tipracks = Object.values(deckSetup.labware).filter( labware => labware.def.parameters.isTiprack ) const tipracksNotOnAdapter = tipracks.filter( tiprack => deckSetup.labware[tiprack.slot] == null ) + const noTipracksOnAdapter = tipracksNotOnAdapter.length === 0 - const options = [ + const options: DropdownOption[] = [ { name: t('all'), value: ALL, }, - { + ] + if (is96Channel) { + options.push({ name: t('column'), value: COLUMN, - disabled: tipracksNotOnAdapter.length === 0, - tooltipText: - tipracksNotOnAdapter.length === 0 - ? t('form:step_edit_form.field.nozzles.option_tooltip.COLUMN') - : undefined, - }, - ] + disabled: noTipracksOnAdapter, + tooltipText: noTipracksOnAdapter + ? t('form:step_edit_form.field.nozzles.option_tooltip.partial') + : null, + }) + if (enablePartialTip) { + options.push({ + name: t('single_nozzle'), + value: SINGLE, + disabled: noTipracksOnAdapter, + tooltipText: noTipracksOnAdapter + ? t('form:step_edit_form.field.nozzles.option_tooltip.partial') + : null, + }) + } + } else { + options.push({ + name: t('single_nozzle'), + value: SINGLE, + }) + } const [selectedValue, setSelectedValue] = useState( dropdownItem || options[0].value ) - useEffect(() => { - updateValue(selectedValue) - }, [selectedValue]) return ( diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx index 981ff980660..4973bafce0e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/StepTools/MixTools/index.tsx @@ -16,7 +16,10 @@ import { getLabwareEntities, getPipetteEntities, } from '../../../../../../step-forms/selectors' -import { getEnableReturnTip } from '../../../../../../feature-flags/selectors' +import { + getEnablePartialTipSupport, + getEnableReturnTip, +} from '../../../../../../feature-flags/selectors' import { BlowoutLocationField, BlowoutOffsetField, @@ -53,6 +56,7 @@ export function MixTools(props: StepFormProps): JSX.Element { } = props const pipettes = useSelector(getPipetteEntities) const enableReturnTip = useSelector(getEnableReturnTip) + const enablePartialTip = useSelector(getEnablePartialTipSupport) const labwares = useSelector(getLabwareEntities) const { t, i18n } = useTranslation(['application', 'form']) const aspirateTab = { @@ -73,7 +77,10 @@ export function MixTools(props: StepFormProps): JSX.Element { const is96Channel = propsForFields.pipette.value != null && - pipettes[String(propsForFields.pipette.value)].name === 'p1000_96' + pipettes[String(propsForFields.pipette.value)].spec.channels === 96 + const is8Channel = + propsForFields.pipette.value != null && + pipettes[String(propsForFields.pipette.value)].spec.channels === 8 const userSelectedPickUpTipLocation = labwares[String(propsForFields.pickUpTip_location.value)] != null const userSelectedDropTipLocation = @@ -88,7 +95,13 @@ export function MixTools(props: StepFormProps): JSX.Element { paddingY={SPACING.spacing16} > - {is96Channel ? : null} + {propsForFields.pipette.value != null && + (is96Channel || (is8Channel && enablePartialTip)) ? ( + + ) : null} - {is96Channel ? ( + {propsForFields.pipette.value != null && + (is96Channel || (is8Channel && enablePartialTip)) ? ( <> - + ) : null} diff --git a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts index 753746ecd87..e9a98ab584e 100644 --- a/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts +++ b/protocol-designer/src/pages/Designer/ProtocolSteps/StepForm/utils.ts @@ -5,7 +5,7 @@ import { SOURCE_WELL_BLOWOUT_DESTINATION, DEST_WELL_BLOWOUT_DESTINATION, } from '@opentrons/step-generation' -import { ALL, COLUMN } from '@opentrons/shared-data' +import { SINGLE } from '@opentrons/shared-data' import { getFieldErrors } from '../../../../steplist/fieldLevel' import { getDisabledFields, @@ -227,12 +227,10 @@ export const getNozzleType = ( nozzles: string | null ): NozzleType | null => { const is8Channel = pipette != null && pipette.spec.channels === 8 - if (is8Channel) { + if (is8Channel && nozzles !== SINGLE) { return '8-channel' - } else if (nozzles === COLUMN) { - return COLUMN - } else if (nozzles === ALL) { - return ALL + } else if (nozzles != null) { + return nozzles as NozzleType } else { return null } diff --git a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts index 137e0afc60d..949f3c6ab00 100644 --- a/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts +++ b/protocol-designer/src/step-forms/utils/createPresavedStepForm.ts @@ -1,6 +1,7 @@ import last from 'lodash/last' import { ABSORBANCE_READER_TYPE, + ALL, HEATERSHAKER_MODULE_TYPE, MAGNETIC_MODULE_TYPE, TEMPERATURE_MODULE_TYPE, @@ -86,6 +87,34 @@ const _patchDefaultPipette = (args: { return null } +const _patchDefaultNozzle = (args: { + labwareEntities: LabwareEntities + pipetteEntities: PipetteEntities +}): FormUpdater => formData => { + const { labwareEntities, pipetteEntities } = args + const hasPartialTipSupportedChannel = Object.values(pipetteEntities).find( + pip => pip.spec.channels === 96 + // || pip.spec.channels === 8 + // TODO: add this in once we remove enablePartialTip feature flag + ) + + const formHasNozzlesField = formData && 'nozzles' in formData + + if (formHasNozzlesField && hasPartialTipSupportedChannel) { + const updatedFields = handleFormChange( + { + nozzles: ALL, + }, + formData, + pipetteEntities, + labwareEntities + ) + return updatedFields + } + + return null +} + const _patchDefaultDropTipLocation = (args: { additionalEquipmentEntities: AdditionalEquipmentEntities labwareEntities: LabwareEntities @@ -363,6 +392,11 @@ export const createPresavedStepForm = ({ stepType, }) + const updateDefaultNozzles = _patchDefaultNozzle({ + labwareEntities, + pipetteEntities, + }) + const updateDefaultDropTip = _patchDefaultDropTipLocation({ labwareEntities, pipetteEntities, @@ -428,6 +462,7 @@ export const createPresavedStepForm = ({ updateMagneticModuleId, updateAbsorbanceReaderModuleId, updateDefaultLabwareLocations, + updateDefaultNozzles, ].reduce( (acc, updater: FormUpdater) => { const updates = updater(acc) diff --git a/protocol-designer/src/steplist/substepTimeline.ts b/protocol-designer/src/steplist/substepTimeline.ts index 0788acbde6f..b1c8ca54bb7 100644 --- a/protocol-designer/src/steplist/substepTimeline.ts +++ b/protocol-designer/src/steplist/substepTimeline.ts @@ -9,6 +9,7 @@ import { ALL, COLUMN, OT2_ROBOT_TYPE, + SINGLE, } from '@opentrons/shared-data' import { getCutoutIdByAddressableArea } from '../utils' import type { Channels } from '@opentrons/components' @@ -254,10 +255,12 @@ export const substepTimelineMultiChannel = ( : null let numChannels = channels - if (nozzles === ALL) { + if (nozzles === ALL && channels !== 8) { numChannels = 96 } else if (nozzles === COLUMN) { numChannels = 8 + } else if (nozzles === SINGLE) { + numChannels = 1 } const wellsForTips = numChannels && diff --git a/protocol-designer/src/top-selectors/substep-highlight.ts b/protocol-designer/src/top-selectors/substep-highlight.ts index 8724f86a909..2507dec2ce5 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.ts +++ b/protocol-designer/src/top-selectors/substep-highlight.ts @@ -1,6 +1,11 @@ import { createSelector } from 'reselect' import mapValues from 'lodash/mapValues' -import { ALL, COLUMN, getWellNamePerMultiTip } from '@opentrons/shared-data' +import { + ALL, + COLUMN, + SINGLE, + getWellNamePerMultiTip, +} from '@opentrons/shared-data' import * as StepGeneration from '@opentrons/step-generation' import { selectors as stepFormSelectors } from '../step-forms' import { selectors as fileDataSelectors } from '../file-data' @@ -28,14 +33,17 @@ function _wellsForPipette( const pipChannels = pipetteEntity.spec.channels // `wells` is all the wells that pipette's channel 1 interacts with. - if (pipChannels === 8 || pipChannels === 96) { - let channels: 8 | 96 = pipChannels - if (nozzles === ALL) { + if ((pipChannels === 8 || pipChannels === 96) && nozzles !== SINGLE) { + let channels = pipChannels + if (nozzles === ALL && pipChannels === 8) { channels = 96 - } else if (nozzles === COLUMN || pipChannels === 8) { + } else if ( + (nozzles === COLUMN && pipChannels === 96) || + pipChannels === 8 + ) { channels = 8 } else { - console.error(`we don't support other 96-channel configurations yet`) + console.error(`we don't support other partial tip configurations yet`) } return wells.reduce((acc: string[], well: string) => { const setOfWellsForMulti = getWellNamePerMultiTip( @@ -120,12 +128,12 @@ function _getSelectedWellsForStep( stepArgs.commandCreatorFnName === 'mix' || stepArgs.commandCreatorFnName === 'transfer' ) { - if (stepArgs.nozzles === ALL) { + if (stepArgs.nozzles === ALL && pipetteSpec.channels === 96) { channels = 96 } else if (stepArgs.nozzles === COLUMN) { channels = 8 - } else { - channels = pipetteSpec.channels + } else if (nozzles !== SINGLE && pipetteSpec.channels === 8) { + channels = 8 } } const commandWellName = c.params.wellName @@ -224,33 +232,45 @@ function _getSelectedWellsForSubstep( let tipWellSet: string[] = [] if ('pipette' in stepArgs) { if (substeps.multichannel) { - const { activeTips } = substeps.multiRows[substepIndex][0] - const pipChannels = - invariantContext.pipetteEntities[stepArgs.pipette].spec.channels - let channels = pipChannels - if ('nozzles' in stepArgs) { - if (stepArgs.nozzles === ALL) { + if ('nozzles' in stepArgs && stepArgs.nozzles !== SINGLE) { + const { activeTips } = substeps.multiRows[substepIndex][0] + const pipChannels = + invariantContext.pipetteEntities[stepArgs.pipette].spec.channels + let channels = pipChannels + if (stepArgs.nozzles === ALL && pipChannels === 96) { channels = 96 - } else if (stepArgs.nozzles === COLUMN) { + } else if ( + (stepArgs.nozzles === COLUMN && pipChannels === 96) || + pipChannels === 8 + ) { channels = 8 } else { console.error( - `we don't support other 96-channel configurations yet` + `we don't support other partial tip configurations yet` ) } - } - // just use first multi row - if ( - activeTips && - activeTips.labwareId === labwareId && - channels !== 1 - ) { - const multiTipWellSet = getWellSetForMultichannel({ - labwareDef: invariantContext.labwareEntities[labwareId].def, - wellName: activeTips.wellName, - channels, - }) - if (multiTipWellSet) tipWellSet = multiTipWellSet + // just use first multi row + if ( + activeTips && + activeTips.labwareId === labwareId && + channels !== 1 + ) { + const multiTipWellSet = getWellSetForMultichannel({ + labwareDef: invariantContext.labwareEntities[labwareId].def, + wellName: activeTips.wellName, + channels, + }) + if (multiTipWellSet) tipWellSet = multiTipWellSet + } + } else { + // single-nozzle pick up + const { activeTips } = substeps.multiRows[substepIndex][0] + if ( + activeTips && + activeTips.labwareId === labwareId && + activeTips.wellName + ) + tipWellSet = [activeTips.wellName] } } else { // single-channel diff --git a/step-generation/src/__tests__/configureNozzleLayout.test.ts b/step-generation/src/__tests__/configureNozzleLayout.test.ts index 8474b5d2d07..2ad12657bb7 100644 --- a/step-generation/src/__tests__/configureNozzleLayout.test.ts +++ b/step-generation/src/__tests__/configureNozzleLayout.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from 'vitest' -import { ALL, COLUMN } from '@opentrons/shared-data' +import { ALL, COLUMN, fixtureP100096V2Specs } from '@opentrons/shared-data' import { getSuccessResult } from '../fixtures' import { configureNozzleLayout } from '../commandCreators/atomic/configureNozzleLayout' @@ -7,7 +7,9 @@ const getRobotInitialState = (): any => { return {} } -const invariantContext: any = {} +const invariantContext: any = { + pipetteEntities: { mockPipette: { spec: fixtureP100096V2Specs } }, +} const robotInitialState = getRobotInitialState() const mockPipette = 'mockPipette' diff --git a/step-generation/src/__tests__/getIsSafePipetteMovement.test.ts b/step-generation/src/__tests__/getIsSafePipetteMovement.test.ts index 1319917a66b..56f38d49bf6 100644 --- a/step-generation/src/__tests__/getIsSafePipetteMovement.test.ts +++ b/step-generation/src/__tests__/getIsSafePipetteMovement.test.ts @@ -1,6 +1,7 @@ import { expect, describe, it, beforeEach } from 'vitest' import { getIsSafePipetteMovement } from '../utils' import { + COLUMN, TEMPERATURE_MODULE_TYPE, TEMPERATURE_MODULE_V2, fixture96Plate, @@ -76,6 +77,7 @@ describe('getIsSafePipetteMovement', () => { it('returns true when the labware id is a trash bin', () => { const result = getIsSafePipetteMovement( + COLUMN, { labware: {}, pipettes: {}, @@ -101,6 +103,7 @@ describe('getIsSafePipetteMovement', () => { }) it('returns false when within pipette extents is false', () => { const result = getIsSafePipetteMovement( + COLUMN, mockRobotState, mockInvariantProperties, mockPipId, @@ -123,6 +126,7 @@ describe('getIsSafePipetteMovement', () => { }, } const result = getIsSafePipetteMovement( + COLUMN, mockRobotState, mockInvariantProperties, mockPipId, @@ -140,6 +144,7 @@ describe('getIsSafePipetteMovement', () => { [mockAdapter]: { slot: 'D1' }, } const result = getIsSafePipetteMovement( + COLUMN, mockRobotState, mockInvariantProperties, mockPipId, @@ -171,6 +176,7 @@ describe('getIsSafePipetteMovement', () => { }, } const result = getIsSafePipetteMovement( + COLUMN, mockRobotState, mockInvariantProperties, mockPipId, diff --git a/step-generation/src/commandCreators/atomic/aspirate.ts b/step-generation/src/commandCreators/atomic/aspirate.ts index 3d6726b8be2..79627280447 100644 --- a/step-generation/src/commandCreators/atomic/aspirate.ts +++ b/step-generation/src/commandCreators/atomic/aspirate.ts @@ -1,4 +1,9 @@ -import { COLUMN, FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { + ALL, + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + SINGLE, +} from '@opentrons/shared-data' import * as errorCreators from '../../errorCreators' import { getPipetteWithTipMaxVol } from '../../robotStateSelectors' import { @@ -112,14 +117,15 @@ export const aspirate: CommandCreator = ( }) ) } - - const is96Channel = - invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 + const pipChannels = + invariantContext.pipetteEntities[args.pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 if ( - is96Channel && - nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + nozzles, prevRobotState, invariantContext, args.pipette, diff --git a/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts b/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts index bff6a097eda..8607fff56cc 100644 --- a/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts +++ b/step-generation/src/commandCreators/atomic/configureNozzleLayout.ts @@ -1,4 +1,4 @@ -import { COLUMN } from '@opentrons/shared-data' +import { COLUMN, SINGLE } from '@opentrons/shared-data' import { uuid } from '../../utils' import type { CommandCreator } from '../../types' import type { NozzleConfigurationStyle } from '@opentrons/shared-data' @@ -14,6 +14,17 @@ export const configureNozzleLayout: CommandCreator = prevRobotState ) => { const { pipetteId, nozzles } = args + const { pipetteEntities } = invariantContext + const channels = pipetteEntities[pipetteId]?.spec.channels + + let primaryNozzle + if (nozzles === COLUMN) { + primaryNozzle = 'A12' + } else if (nozzles === SINGLE && channels === 96) { + primaryNozzle = 'H12' + } else if (nozzles === SINGLE && channels === 8) { + primaryNozzle = 'H1' + } const commands = [ { @@ -22,7 +33,7 @@ export const configureNozzleLayout: CommandCreator = params: { pipetteId, configurationParams: { - primaryNozzle: nozzles === COLUMN ? 'A12' : undefined, + primaryNozzle, style: nozzles, }, }, diff --git a/step-generation/src/commandCreators/atomic/dispense.ts b/step-generation/src/commandCreators/atomic/dispense.ts index c009ce34403..0aaedb70118 100644 --- a/step-generation/src/commandCreators/atomic/dispense.ts +++ b/step-generation/src/commandCreators/atomic/dispense.ts @@ -1,4 +1,9 @@ -import { COLUMN, FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE } from '@opentrons/shared-data' +import { + ALL, + FLEX_ROBOT_TYPE, + OT2_ROBOT_TYPE, + SINGLE, +} from '@opentrons/shared-data' import * as errorCreators from '../../errorCreators' import { modulePipetteCollision, @@ -109,14 +114,15 @@ export const dispense: CommandCreator = ( ) } } - - const is96Channel = - invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 + const pipChannels = + invariantContext.pipetteEntities[args.pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 if ( - is96Channel && - nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + nozzles, prevRobotState, invariantContext, args.pipette, diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index c516a1a4012..f70cee88567 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -3,6 +3,7 @@ import { COLUMN, FLEX_ROBOT_TYPE, OT2_ROBOT_TYPE, + SINGLE, } from '@opentrons/shared-data' import { getNextTiprack } from '../../robotStateSelectors' import * as errorCreators from '../../errorCreators' @@ -45,13 +46,16 @@ const _pickUpTip: CommandCreator = ( ) => { const errors: CommandCreatorError[] = [] - const is96Channel = - invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 + const pipChannels = + invariantContext.pipetteEntities[args.pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 if ( - is96Channel && - args.nozzles === COLUMN && + ((is96Channel && args.nozzles !== ALL) || + (is8Channel && args.nozzles === SINGLE)) && !getIsSafePipetteMovement( + args.nozzles ?? null, prevRobotState, invariantContext, args.pipette, @@ -251,7 +255,9 @@ export const replaceTip: CommandCreator = ( const configureNozzleLayoutCommand: CurriedCommandCreator[] = // only emit the command if previous nozzle state is different - channels === 96 && args.nozzles != null && args.nozzles !== stateNozzles + (channels === 96 || channels === 8) && + args.nozzles != null && + args.nozzles !== stateNozzles ? [ curryCommandCreator(configureNozzleLayout, { nozzles: args.nozzles, diff --git a/step-generation/src/commandCreators/compound/consolidate.ts b/step-generation/src/commandCreators/compound/consolidate.ts index 94d20e255f0..01828fb6f4a 100644 --- a/step-generation/src/commandCreators/compound/consolidate.ts +++ b/step-generation/src/commandCreators/compound/consolidate.ts @@ -1,10 +1,11 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' import { - COLUMN, getWellDepth, LOW_VOLUME_PIPETTES, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + ALL, + SINGLE, } from '@opentrons/shared-data' import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' @@ -86,8 +87,10 @@ export const consolidate: CommandCreator = ( } = args const pipetteData = prevRobotState.pipettes[args.pipette] - const is96Channel = - invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 + const pipChannels = + invariantContext.pipetteEntities[args.pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 if (!pipetteData) { // bail out before doing anything else @@ -130,9 +133,9 @@ export const consolidate: CommandCreator = ( } if ( - is96Channel && - args.nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + nozzles, prevRobotState, invariantContext, args.pipette, @@ -147,9 +150,9 @@ export const consolidate: CommandCreator = ( } if ( - is96Channel && - args.nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + nozzles, prevRobotState, invariantContext, args.pipette, diff --git a/step-generation/src/commandCreators/compound/distribute.ts b/step-generation/src/commandCreators/compound/distribute.ts index ceba8f3e89b..7dbe6598634 100644 --- a/step-generation/src/commandCreators/compound/distribute.ts +++ b/step-generation/src/commandCreators/compound/distribute.ts @@ -2,10 +2,11 @@ import chunk from 'lodash/chunk' import flatMap from 'lodash/flatMap' import last from 'lodash/last' import { - COLUMN, getWellDepth, LOW_VOLUME_PIPETTES, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + ALL, + SINGLE, } from '@opentrons/shared-data' import { AIR_GAP_OFFSET_FROM_TOP } from '../../constants' import * as errorCreators from '../../errorCreators' @@ -78,8 +79,10 @@ export const distribute: CommandCreator = ( // TODO Ian 2018-05-03 next ~20 lines match consolidate.js const actionName = 'distribute' const errors: CommandCreatorError[] = [] - const is96Channel = - invariantContext.pipetteEntities[args.pipette]?.spec.channels === 96 + const pipChannels = + invariantContext.pipetteEntities[args.pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 // TODO: Ian 2019-04-19 revisit these pipetteDoesNotExist errors, how to do it DRY? if ( @@ -125,9 +128,9 @@ export const distribute: CommandCreator = ( } if ( - is96Channel && - args.nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + args.nozzles, prevRobotState, invariantContext, args.pipette, @@ -140,9 +143,9 @@ export const distribute: CommandCreator = ( } if ( - is96Channel && - args.nozzles === COLUMN && + ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) && !getIsSafePipetteMovement( + args.nozzles, prevRobotState, invariantContext, args.pipette, diff --git a/step-generation/src/commandCreators/compound/mix.ts b/step-generation/src/commandCreators/compound/mix.ts index 15ceb73221c..6f5929810cb 100644 --- a/step-generation/src/commandCreators/compound/mix.ts +++ b/step-generation/src/commandCreators/compound/mix.ts @@ -1,8 +1,9 @@ import flatMap from 'lodash/flatMap' import { LOW_VOLUME_PIPETTES, - COLUMN, GRIPPER_WASTE_CHUTE_ADDRESSABLE_AREA, + ALL, + SINGLE, } from '@opentrons/shared-data' import { repeatArray, @@ -152,8 +153,9 @@ export const mix: CommandCreator = ( nozzles, } = data - const is96Channel = - invariantContext.pipetteEntities[pipette]?.spec.channels === 96 + const pipChannels = invariantContext.pipetteEntities[pipette]?.spec.channels + const is96Channel = pipChannels === 96 + const is8Channel = pipChannels === 8 // Errors if ( @@ -200,8 +202,9 @@ export const mix: CommandCreator = ( return { errors: [errorCreators.dropTipLocationDoesNotExist()] } } - if (is96Channel && data.nozzles === COLUMN) { + if ((is96Channel && nozzles !== ALL) || (is8Channel && nozzles === SINGLE)) { const isAspirateSafePipetteMovement = getIsSafePipetteMovement( + data.nozzles, prevRobotState, invariantContext, pipette, @@ -210,6 +213,7 @@ export const mix: CommandCreator = ( { x: aspirateXOffset, y: aspirateYOffset } ) const isDispenseSafePipetteMovement = getIsSafePipetteMovement( + data.nozzles, prevRobotState, invariantContext, pipette, diff --git a/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts b/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts index 84d26e219c1..12d0db9285e 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts @@ -1,6 +1,6 @@ import mapValues from 'lodash/mapValues' import reduce from 'lodash/reduce' -import { COLUMN } from '@opentrons/shared-data' +import { COLUMN, SINGLE } from '@opentrons/shared-data' import { splitLiquid, mergeLiquid, @@ -44,7 +44,12 @@ export function dispenseUpdateLiquidState( } = args const pipetteSpec = invariantContext.pipetteEntities[pipetteId].spec const nozzles = robotStateAndWarnings.robotState.pipettes[pipetteId].nozzles - const channels = nozzles === COLUMN ? 8 : pipetteSpec.channels + let channels = pipetteSpec.channels + if (nozzles === COLUMN) { + channels = 8 + } else if (nozzles === SINGLE) { + channels = 1 + } const trashId = Object.values( invariantContext.additionalEquipmentEntities ).find(aE => aE.name === 'wasteChute' || aE.name === 'trashBin')?.id diff --git a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts index fdf5ae1d4b8..aac892ad140 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts @@ -1,7 +1,7 @@ import range from 'lodash/range' import isEmpty from 'lodash/isEmpty' import uniq from 'lodash/uniq' -import { COLUMN } from '@opentrons/shared-data' +import { COLUMN, SINGLE } from '@opentrons/shared-data' import { AIR, mergeLiquid, @@ -24,8 +24,13 @@ export function forAspirate( const pipetteSpec = invariantContext.pipetteEntities[pipetteId].spec const labwareDef = invariantContext.labwareEntities[labwareId].def const isReservoir = labwareDef.metadata.displayCategory === 'reservoir' - const channels = nozzles === COLUMN ? 8 : pipetteSpec.channels - + let channels = pipetteSpec.channels + if (nozzles === COLUMN) { + channels = 8 + } else if (nozzles === SINGLE) { + channels = 1 + } + console.log('for aspirate nozzles', nozzles) const { allWellsShared, wellsForTips } = getWellsForTips( channels, labwareDef, diff --git a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts index c32f347b793..4b8a913ad12 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts @@ -1,5 +1,5 @@ import assert from 'assert' -import { ALL, COLUMN, getIsTiprack } from '@opentrons/shared-data' +import { ALL, COLUMN, SINGLE, getIsTiprack } from '@opentrons/shared-data' import type { PickUpTipParams } from '@opentrons/shared-data' import type { InvariantContext, RobotStateAndWarnings } from '../types' export function forPickUpTip( @@ -19,7 +19,7 @@ export function forPickUpTip( // pipette now has tip(s) tipState.pipettes[pipetteId] = true // remove tips from tiprack - if (pipetteSpec.channels === 1) { + if (pipetteSpec.channels === 1 || nozzles === SINGLE) { tipState.tipracks[labwareId][wellName] = false } else if (pipetteSpec.channels === 8 || nozzles === COLUMN) { const allWells = tiprackDef.ordering.find(col => col[0] === wellName) diff --git a/step-generation/src/robotStateSelectors.ts b/step-generation/src/robotStateSelectors.ts index 94a3d1e7821..102656ae8cb 100644 --- a/step-generation/src/robotStateSelectors.ts +++ b/step-generation/src/robotStateSelectors.ts @@ -8,6 +8,7 @@ import { getTiprackVolume, orderWells, THERMOCYCLER_MODULE_TYPE, + SINGLE, } from '@opentrons/shared-data' import { COLUMN_4_SLOTS } from './constants' import type { @@ -62,7 +63,7 @@ export function _getNextTip(args: { const hasTip = (wellName: string): boolean => tiprackWellsState[wellName] const orderedWells = orderWells(tiprackDef.ordering, 't2b', 'l2r') - if (pipetteChannels === 1) { + if (pipetteChannels === 1 || nozzles === SINGLE) { const well = orderedWells.find(hasTip) return well || null } diff --git a/step-generation/src/utils/safePipetteMovements.ts b/step-generation/src/utils/safePipetteMovements.ts index 3b306db3687..ef55e230e2e 100644 --- a/step-generation/src/utils/safePipetteMovements.ts +++ b/step-generation/src/utils/safePipetteMovements.ts @@ -1,5 +1,6 @@ import { FLEX_ROBOT_TYPE, + SINGLE, THERMOCYCLER_MODULE_TYPE, getAddressableAreaFromSlotId, getDeckDefFromRobotType, @@ -22,8 +23,6 @@ import type { const A12_column_front_left_bound = { x: -11.03, y: 2 } const A12_column_back_right_bound = { x: 526.77, y: 506.2 } -const PRIMARY_NOZZLE = 'A12' -const NOZZLE_CONFIGURATION = 'COLUMN' const FLEX_TC_LID_COLLISION_ZONE = { back_left: { x: -43.25, y: 454.9, z: 211.91 }, front_right: { x: 128.75, y: 402, z: 211.91 }, @@ -78,7 +77,7 @@ const getPipetteBoundsAtSpecifiedMoveToPosition = ( pipetteEntity: PipetteEntity, tipLength: number, wellTargetPoint: Point, - primaryNozzle: string = 'A12' // hardcoding A12 becasue only column pick up supported currently + primaryNozzle: string ): Point[] => { const { nozzleMap, @@ -293,6 +292,7 @@ const getWellPosition = ( // util to use in step-generation for if the pipette movement is safe export const getIsSafePipetteMovement = ( + nozzleConfiguation: NozzleConfigurationStyle | null, robotState: RobotState, invariantContext: InvariantContext, pipetteId: string, @@ -310,8 +310,12 @@ export const getIsSafePipetteMovement = ( } = invariantContext const { labware: labwareState, tipState } = robotState - // early exit if labwareId is a trashBin or wasteChute - if (labwareEntities[labwareId] == null || wellTargetName == null) { + // early exit if labwareId is a trashBin or wasteChute or if no nozzle is provided + if ( + labwareEntities[labwareId] == null || + wellTargetName == null || + nozzleConfiguation == null + ) { return true } @@ -341,13 +345,20 @@ export const getIsSafePipetteMovement = ( addressableAreaOffset, pipetteHasTip ) + let primaryNozzle = 'A12' + if (nozzleConfiguation === SINGLE && pipetteEntity.spec.channels === 96) { + primaryNozzle = 'H12' + } else if ( + nozzleConfiguation === SINGLE && + pipetteEntity.spec.channels === 8 + ) { + primaryNozzle = 'H1' + } const isWithinPipetteExtents = getIsWithinPipetteExtents( wellTargetPoint, - // TODO(jr, 4/22/24): PD only supports A12 as a primary nozzle for now - // and only for 96-channel column pick up - NOZZLE_CONFIGURATION, - PRIMARY_NOZZLE + nozzleConfiguation, + primaryNozzle ) if (!isWithinPipetteExtents) { return false @@ -355,7 +366,8 @@ export const getIsSafePipetteMovement = ( const pipetteBoundsAtWellLocation = getPipetteBoundsAtSpecifiedMoveToPosition( pipetteEntity, tipLength, - wellTargetPoint + wellTargetPoint, + primaryNozzle ) const surroundingSlots = getFlexSurroundingSlots( labwareSlot,