From 129c9dbb0ba67b43a7bf064027f9ee002351543f Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 22 Sep 2023 11:41:54 -0400 Subject: [PATCH 01/19] address RAUT-679 96 channel tiprack adapter support --- .../DeckSetup/LabwareOverlays/EditLabware.tsx | 9 ++- .../LabwareOverlays/LabwareControls.tsx | 12 ++- .../LabwareOverlays/SlotControls.tsx | 13 ++-- .../__tests__/SlotControls.test.tsx | 1 + .../src/components/DeckSetup/index.tsx | 12 +++ .../LabwareSelectionModal.tsx | 69 +++++++++-------- .../__tests__/LabwareSelectionModal.test.tsx | 27 ++++++- .../components/LabwareSelectionModal/index.ts | 18 +++++ .../modals/CreateFileWizard/index.tsx | 6 ++ .../src/labware-ingred/actions/actions.ts | 6 +- .../src/labware-ingred/actions/thunks.ts | 75 ++++++++++++++++--- .../top-selectors/labware-locations/index.ts | 19 +++-- .../src/utils/labwareModuleCompatibility.ts | 39 ++++++++-- robot-server/simulators/test.json | 18 ++++- .../src/commandCreators/atomic/moveLabware.ts | 24 +++--- 15 files changed, 270 insertions(+), 78 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx index 7bdf7570efb..ffd09d85a1c 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx @@ -31,6 +31,7 @@ interface OP { setHoveredLabware: (val?: LabwareOnDeck | null) => unknown setDraggedLabware: (val?: LabwareOnDeck | null) => unknown swapBlocked: boolean + adapterId?: string } interface SP { isYetUnnamed: boolean @@ -209,7 +210,13 @@ const mapDispatchToProps = ( ): DP => ({ editLiquids: () => dispatch(openIngredientSelector(ownProps.labwareOnDeck.id)), - duplicateLabware: () => dispatch(duplicateLabware(ownProps.labwareOnDeck.id)), + duplicateLabware: () => + dispatch( + duplicateLabware({ + templateLabwareId: ownProps.labwareOnDeck.id, + templateAdapterId: ownProps.adapterId, + }) + ), deleteLabware: () => { window.confirm( `Are you sure you want to permanently delete this ${getLabwareDisplayName( diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx index e9d71aba1dd..83622a529d3 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx @@ -13,12 +13,14 @@ import { LabwareHighlight } from './LabwareHighlight' import styles from './LabwareOverlays.css' interface LabwareControlsProps { + allLabware: LabwareOnDeck[] + has96Channel: boolean labwareOnDeck: LabwareOnDeck - selectedTerminalItemId?: TerminalItemId | null slot: DeckSlot setHoveredLabware: (labware?: LabwareOnDeck | null) => unknown setDraggedLabware: (labware?: LabwareOnDeck | null) => unknown swapBlocked: boolean + selectedTerminalItemId?: TerminalItemId | null } export const LabwareControls = (props: LabwareControlsProps): JSX.Element => { @@ -29,11 +31,18 @@ export const LabwareControls = (props: LabwareControlsProps): JSX.Element => { setHoveredLabware, setDraggedLabware, swapBlocked, + allLabware, + has96Channel, } = props + const canEdit = selectedTerminalItemId === START_TERMINAL_ITEM_ID const [x, y] = slot.position const width = labwareOnDeck.def.dimensions.xDimension const height = labwareOnDeck.def.dimensions.yDimension + const isTiprack = labwareOnDeck.def.parameters.isTiprack + const adapterId = Object.values(allLabware).find( + adapter => adapter.id === labwareOnDeck.slot + )?.id return ( <> { {canEdit ? ( // @ts-expect-error(sa, 2021-6-21): react dnd type mismatch unknown } @@ -68,6 +69,7 @@ export const SlotControlsComponent = ( draggedItem, itemType, customLabwareDefs, + has96Channel, } = props if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || @@ -82,11 +84,12 @@ export const SlotControlsComponent = ( let slotBlocked: string | null = null if ( - isOver && - moduleType != null && - draggedDef != null && - !getLabwareIsCompatible(draggedDef, moduleType) && - !isCustomLabware + (isOver && + moduleType != null && + draggedDef != null && + !getLabwareIsCompatible(draggedDef, moduleType) && + !isCustomLabware) || + (has96Channel && draggedDef?.parameters.isTiprack) ) { slotBlocked = 'Labware incompatible with this module' } diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx index b6ae460ab10..c7f1901cabc 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx @@ -50,6 +50,7 @@ describe('SlotControlsComponent', () => { }, itemType: DND_TYPES.LABWARE, customLabwareDefs: {}, + has96Channel: false, } getLabwareIsCompatibleSpy = jest.spyOn( diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 7e80df35cff..4455ea75299 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -110,6 +110,10 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { robotType, trashSlot, } = props + const pipettes = activeDeckSetup.pipettes + const has96Channel = Object.values(pipettes).some( + pip => pip.name === 'p1000_96' + ) // NOTE: handling module<>labware compat when moving labware to empty module // is handled by SlotControls. // But when swapping labware when at least one is on a module, we need to be aware @@ -277,6 +281,8 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { /> ) : ( { !isAdapter ? ( // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier { return ( // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier { ) : ( { /> tiprack assignment) */ permittedTipracks: string[] isNextToHeaterShaker: boolean + has96Channel: boolean adapterLoadName?: string } @@ -121,6 +123,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { selectLabware, isNextToHeaterShaker, adapterLoadName, + has96Channel, } = props const defs = getOnlyLatestDefs() const [selectedCategory, setSelectedCategory] = React.useState( @@ -190,7 +193,8 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { const smallYDimension = labwareDef.dimensions.yDimension < 85.48 const irregularSize = smallXDimension && smallYDimension const adapter = labwareDef.metadata.displayCategory === 'adapter' - + const adapter96Channel = + labwareDef.parameters.loadName === ADAPTER_96_CHANNEL return ( (filterRecommended && !getLabwareIsRecommended(labwareDef, moduleType)) || @@ -200,7 +204,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM )) || !getLabwareCompatible(labwareDef) || - (adapter && irregularSize && !slot?.includes(HEATERSHAKER_MODULE_TYPE)) + (adapter && + irregularSize && + !slot?.includes(HEATERSHAKER_MODULE_TYPE)) || + (adapter96Channel && !has96Channel) ) }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] @@ -239,13 +246,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { defs, (acc, def: typeof defs[keyof typeof defs]) => { const category: string = def.metadata.displayCategory - // filter out non-permitted tipracks + - // temporarily filtering out 96-channel adapter until we support - // 96-channel + // filter out non-permitted tipracks if ( - (category === 'tipRack' && - !permittedTipracks.includes(getLabwareDefURI(def))) || - def.parameters.loadName === 'opentrons_flex_96_tiprack_adapter' + category === 'tipRack' && + !permittedTipracks.includes(getLabwareDefURI(def)) ) { return acc } @@ -420,6 +424,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { }) ) : ( { onClick={makeToggleCategory(adapterCompatibleLabware)} inert={false} > - {getLabwareCompatibleWithAdapter(adapterLoadName).map( - (adapterDefUri, index) => { - const latestDefs = getOnlyLatestDefs() - const Uris = Object.keys(latestDefs) - const labwareDefUri = Uris.find( - defUri => defUri === adapterDefUri - ) - const labwareDef = labwareDefUri - ? latestDefs[labwareDefUri] - : null + {getLabwareCompatibleWithAdapter( + has96Channel ? permittedTipracks : [], + adapterLoadName + ).map((adapterDefUri, index) => { + const latestDefs = getOnlyLatestDefs() + + const URIs = Object.keys(latestDefs) + const labwareDefUri = URIs.find( + defUri => defUri === adapterDefUri + ) + const labwareDef = labwareDefUri + ? latestDefs[labwareDefUri] + : null - return labwareDef != null ? ( - setPreviewedLabware(labwareDef)} - // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) - onMouseLeave={() => setPreviewedLabware()} - /> - ) : null - } - )} + return labwareDef != null ? ( + setPreviewedLabware(labwareDef)} + // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) + onMouseLeave={() => setPreviewedLabware()} + /> + ) : null + })} )} diff --git a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx index 6eed36b3d7f..fd286a9d327 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx @@ -1,12 +1,16 @@ import * as React from 'react' import i18next from 'i18next' -import { renderWithProviders } from '@opentrons/components' +import { renderWithProviders, nestedTextMatcher } from '@opentrons/components' import { getIsLabwareAboveHeight, MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, } from '@opentrons/shared-data' +import { getLabwareCompatibleWithAdapter } from '../../../utils/labwareModuleCompatibility' +import { Portal } from '../../portals/TopPortal' import { LabwareSelectionModal } from '../LabwareSelectionModal' +jest.mock('../../../utils/labwareModuleCompatibility') +jest.mock('../../portals/TopPortal') jest.mock('../../Hints/useBlockingHint') jest.mock('@opentrons/shared-data', () => { const actualSharedData = jest.requireActual('@opentrons/shared-data') @@ -19,7 +23,10 @@ jest.mock('@opentrons/shared-data', () => { const mockGetIsLabwareAboveHeight = getIsLabwareAboveHeight as jest.MockedFunction< typeof getIsLabwareAboveHeight > - +const mockPortal = Portal as jest.MockedFunction +const mockGetLabwareCompatibleWithAdapter = getLabwareCompatibleWithAdapter as jest.MockedFunction< + typeof getLabwareCompatibleWithAdapter +> const render = (props: React.ComponentProps) => { return renderWithProviders(, { i18nInstance: i18next, @@ -36,7 +43,10 @@ describe('LabwareSelectionModal', () => { customLabwareDefs: {}, permittedTipracks: [], isNextToHeaterShaker: false, + has96Channel: false, } + mockPortal.mockReturnValue(
mock portal
) + mockGetLabwareCompatibleWithAdapter.mockReturnValue([]) }) it('should NOT filter out labware above 57 mm when the slot is NOT next to a heater shaker', () => { props.isNextToHeaterShaker = false @@ -51,4 +61,17 @@ describe('LabwareSelectionModal', () => { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM ) }) + it('should display only permitted tipracks if has', () => { + const mockPermittedTipracks = ['mockPermittedTip', 'mockPermittedTip2'] + props.slot = 'A2' + props.has96Channel = true + props.adapterLoadName = 'mockLoadName' + props.permittedTipracks = mockPermittedTipracks + const { getByText } = render(props) + getByText(nestedTextMatcher('adapter compatible labware')).click() + expect(mockGetLabwareCompatibleWithAdapter).toHaveBeenCalledWith( + mockPermittedTipracks, + props.adapterLoadName + ) + }) }) diff --git a/protocol-designer/src/components/LabwareSelectionModal/index.ts b/protocol-designer/src/components/LabwareSelectionModal/index.ts index 7e227953f55..52b750be470 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/index.ts +++ b/protocol-designer/src/components/LabwareSelectionModal/index.ts @@ -18,6 +18,8 @@ import { } from '../../labware-defs' import { selectors as stepFormSelectors, ModuleOnDeck } from '../../step-forms' import { BaseState, ThunkDispatch } from '../../types' +import { getPipetteEntities } from '../../step-forms/selectors' +import { adapter96ChannelDefUri } from '../modals/CreateFileWizard' interface SP { customLabwareDefs: LabwareSelectionModalProps['customLabwareDefs'] slot: LabwareSelectionModalProps['slot'] @@ -25,11 +27,18 @@ interface SP { moduleType: LabwareSelectionModalProps['moduleType'] permittedTipracks: LabwareSelectionModalProps['permittedTipracks'] isNextToHeaterShaker: boolean + has96Channel: boolean + adapterDefUri?: string adapterLoadName?: string } function mapStateToProps(state: BaseState): SP { const slot = labwareIngredSelectors.selectedAddLabwareSlot(state) || null + const pipettes = getPipetteEntities(state) + const has96Channel = Object.values(pipettes).some( + pip => pip.name === 'p1000_96' + ) + // TODO: Ian 2019-10-29 needs revisit to support multiple manualIntervention steps const modulesById = stepFormSelectors.getInitialDeckSetup(state).modules const initialModules: ModuleOnDeck[] = Object.keys(modulesById).map( @@ -57,6 +66,8 @@ function mapStateToProps(state: BaseState): SP { parentSlot, moduleType, isNextToHeaterShaker, + has96Channel, + adapterDefUri: has96Channel ? adapter96ChannelDefUri : undefined, permittedTipracks: stepFormSelectors.getPermittedTipracks(state), adapterLoadName: adapterLoadNameOnDeck, } @@ -77,11 +88,18 @@ function mergeProps( onUploadLabware: fileChangeEvent => dispatch(labwareDefActions.createCustomLabwareDef(fileChangeEvent)), selectLabware: labwareDefURI => { + console.log(labwareDefURI) + console.log(stateProps.permittedTipracks) if (stateProps.slot) { dispatch( createContainer({ slot: stateProps.slot, labwareDefURI, + adapterUnderLabwareDefURI: stateProps.permittedTipracks.includes( + labwareDefURI + ) + ? stateProps.adapterDefUri + : undefined, }) ) } diff --git a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx index 87e3c9fff6c..5bb0029d221 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/index.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/index.tsx @@ -94,6 +94,8 @@ const WIZARD_STEPS_OT2: WizardStep[] = [ 'second_pipette_tips', 'modulesAndOther', ] +export const adapter96ChannelDefUri = + 'opentrons/opentrons_flex_96_tiprack_adapter/1' export function CreateFileWizard(): JSX.Element | null { const { t } = useTranslation() @@ -265,6 +267,10 @@ export function CreateFileWizard(): JSX.Element | null { dispatch( labwareIngredActions.createContainer({ labwareDefURI: tiprackDefURI, + adapterUnderLabwareDefURI: + values.pipettesByMount.left.pipetteName === 'p1000_96' + ? adapter96ChannelDefUri + : undefined, }) ) }) diff --git a/protocol-designer/src/labware-ingred/actions/actions.ts b/protocol-designer/src/labware-ingred/actions/actions.ts index de3b2003ebd..7d39ec3ac76 100644 --- a/protocol-designer/src/labware-ingred/actions/actions.ts +++ b/protocol-designer/src/labware-ingred/actions/actions.ts @@ -54,9 +54,11 @@ export const drillUpFromLabware: () => DrillUpFromLabwareAction = createAction( ) // ==== Create/delete/modify labware ===== export interface CreateContainerArgs { - slot?: DeckSlot - // NOTE: if slot is omitted, next available slot will be used. labwareDefURI: string + // NOTE: adapterUnderLabwareDefURI is only for rendering an adapter under the labware/tiprack + adapterUnderLabwareDefURI?: string + // NOTE: if slot is omitted, next available slot will be used. + slot?: DeckSlot } export interface CreateContainerAction { type: 'CREATE_CONTAINER' diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index 1de7622b53e..d5f881b605c 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -63,11 +63,35 @@ export const createContainer: ( if (slot) { const id = `${uuid()}:${args.labwareDefURI}` - dispatch({ - type: 'CREATE_CONTAINER', - payload: { ...args, id, slot }, - }) + const adapterId = + args.adapterUnderLabwareDefURI != null + ? `${uuid()}:${args.adapterUnderLabwareDefURI}` + : null + if (adapterId != null && args.adapterUnderLabwareDefURI != null) { + dispatch({ + type: 'CREATE_CONTAINER', + payload: { + ...args, + labwareDefURI: args.adapterUnderLabwareDefURI, + id: adapterId, + slot, + }, + }) + dispatch({ + type: 'CREATE_CONTAINER', + payload: { + ...args, + id, + slot: adapterId, + }, + }) + } else { + dispatch({ + type: 'CREATE_CONTAINER', + payload: { ...args, id, slot }, + }) + } if (isTiprack) { // Tipracks cannot be named, but should auto-increment. // We can't rely on reducers to do that themselves bc they don't have access @@ -80,21 +104,33 @@ export const createContainer: ( console.warn('no slots available, cannot create labware') } } -export const duplicateLabware: ( + +interface DuplicateArgs { templateLabwareId: string -) => ThunkAction = templateLabwareId => ( - dispatch, - getState -) => { + templateAdapterId?: string +} +export const duplicateLabware: ( + args: DuplicateArgs +) => ThunkAction = args => (dispatch, getState) => { + const { templateLabwareId, templateAdapterId } = args const state = getState() const robotType = state.fileData.robotType const templateLabwareDefURI = stepFormSelectors.getLabwareEntities(state)[ templateLabwareId ].labwareDefURI + const tempAdapterEntity = + templateAdapterId != null + ? stepFormSelectors.getLabwareEntities(state)[templateAdapterId] + : null + const templateAdapterDefURI = tempAdapterEntity?.labwareDefURI ?? null + const templateAdapterDisplayName = + tempAdapterEntity?.def.metadata.displayName ?? null + assert( templateLabwareDefURI, `no labwareDefURI for labware ${templateLabwareId}, cannot run duplicateLabware thunk` ) + const initialDeckSetup = stepFormSelectors.getInitialDeckSetup(state) const templateLabwareIdIsOffDeck = initialDeckSetup.labware[templateLabwareId].slot === 'offDeck' @@ -109,6 +145,27 @@ export const duplicateLabware: ( ) if (templateLabwareDefURI && duplicateSlot) { + if (templateAdapterDefURI != null && templateAdapterId != null) { + dispatch({ + type: 'DUPLICATE_LABWARE', + payload: { + // you can't set a nick name for adapters + duplicateLabwareNickname: templateAdapterDisplayName ?? '', + templateLabwareId: templateAdapterId, + duplicateLabwareId: uuid() + ':' + templateAdapterDefURI, + slot: duplicateSlot, + }, + }) + dispatch({ + type: 'DUPLICATE_LABWARE', + payload: { + duplicateLabwareNickname, + templateLabwareId, + duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, + slot: templateAdapterId, + }, + }) + } dispatch({ type: 'DUPLICATE_LABWARE', payload: { diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index bc351e2e684..2167acdb8ac 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -132,6 +132,8 @@ export const getUnocuppiedLabwareLocationOptions: Selector< const labwareOnAdapter = Object.values(labware).find( temporalProperties => temporalProperties.slot === labwareId ) + + const adapterSlot = labwareOnDeck.slot const modIdWithAdapter = Object.keys(modules).find( modId => modId === labwareOnDeck.slot ) @@ -145,13 +147,18 @@ export const getUnocuppiedLabwareLocationOptions: Selector< ? [ ...acc, { - name: `${adapterDisplayName} on top of ${ + name: modIdWithAdapter != null - ? getModuleDisplayName( - moduleEntities[modIdWithAdapter].model - ) - : 'unknown module' - } in slot ${modSlot ?? 'unknown slot'}`, + ? `${adapterDisplayName} on top of ${ + modIdWithAdapter != null + ? getModuleDisplayName( + moduleEntities[modIdWithAdapter].model + ) + : 'unknown module' + } in slot ${modSlot ?? 'unknown slot'}` + : `${adapterDisplayName} on slot ${ + adapterSlot ?? 'unknown' + }`, value: labwareId, }, ] diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index eb4dd843ff8..4eb8cab6618 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -84,6 +84,7 @@ const PCR_ADAPTER_LOADNAME = 'opentrons_96_pcr_adapter' const UNIVERSAL_FLAT_ADAPTER_LOADNAME = 'opentrons_universal_flat_adapter' const ALUMINUM_BLOCK_96_LOADNAME = 'opentrons_96_well_aluminum_block' const ALUMINUM_FLAT_BOTTOM_PLATE = 'opentrons_aluminum_flat_bottom_plate' +export const ADAPTER_96_CHANNEL = 'opentrons_flex_96_tiprack_adapter' export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< string, @@ -118,15 +119,28 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< 'opentrons/corning_6_wellplate_16.8ml_flat/2', 'opentrons/nest_96_wellplate_200ul_flat/2', ], + [ADAPTER_96_CHANNEL]: [ + 'opentrons/opentrons_flex_96_tiprack_50ul/1', + 'opentrons/opentrons_flex_96_tiprack_200ul/1', + 'opentrons/opentrons_flex_96_tiprack_1000ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_50ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_200ul/1', + 'opentrons/opentrons_flex_96_filtertiprack_1000ul/1', + ], } export const getLabwareCompatibleWithAdapter = ( + permittedTipracks: string[], adapterLoadName?: string -): string[] => - adapterLoadName != null - ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] - : [] - +): string[] => { + if (permittedTipracks.length > 0) { + return permittedTipracks + } else { + return adapterLoadName != null + ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] + : [] + } +} export const getLabwareIsCustom = ( customLabwares: LabwareDefByDefURI, labwareOnDeck: LabwareOnDeck @@ -152,6 +166,15 @@ export const getAdapterLabwareIsAMatch = ( 'nest_96_wellplate_200ul_flat', ] + const adapter96Tipracks = [ + 'opentrons_flex_96_tiprack_50ul', + 'opentrons_flex_96_tiprack_200ul', + 'opentrons_flex_96_tiprack_1000ul', + 'opentrons_flex_96_filtertiprack_50ul', + 'opentrons_flex_96_filtertiprack_200ul', + 'opentrons_flex_96_filtertiprack_1000ul', + ] + const deepWellPair = loadName === DEEP_WELL_ADAPTER_LOADNAME && draggedLabwareLoadname === 'nest_96_wellplate_2ml_deep' @@ -172,6 +195,9 @@ export const getAdapterLabwareIsAMatch = ( const aluminumFlatBottomPlatePairs = loadName === ALUMINUM_FLAT_BOTTOM_PLATE && flatBottomLabwares.includes(draggedLabwareLoadname) + const adapter96ChannelPairs = + loadName === ADAPTER_96_CHANNEL && + adapter96Tipracks.includes(draggedLabwareLoadname) if ( deepWellPair || @@ -179,7 +205,8 @@ export const getAdapterLabwareIsAMatch = ( pcrPair || universalPair || aluminumBlock96Pairs || - aluminumFlatBottomPlatePairs + aluminumFlatBottomPlatePairs || + adapter96ChannelPairs ) { return true } else { diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index 0e23a2e8351..9594846b20f 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -1,12 +1,22 @@ { + "machine": "OT-3 Standard", + "strict_attached_instruments": false, "attached_instruments": { "right": { - "model": "p300_single_v1", - "id": "321" + "model": "p1000_single_v3.4", + "id": "321", + "max_volume": 1000, + "name": "p1000_single", + "tip_length": 0, + "channels": 1 }, "left": { - "model": "p10_single_v1", - "id": "123" + "model": "p50_single_v3.4", + "id": "123", + "max_volume": 50, + "name": "p50_single", + "tip_length": 0, + "channels": 1 } }, "attached_modules": { diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 76df6423d3a..467a216db9d 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -113,17 +113,19 @@ export const moveLabware: CommandCreator = ( if (destinationModuleId != null) { const destModuleState = prevRobotState.modules[destinationModuleId].moduleState - if ( - destModuleState.type === THERMOCYCLER_MODULE_TYPE && - destModuleState.lidOpen !== true - ) { - errors.push(errorCreators.thermocyclerLidClosed()) - } else if (destModuleState.type === HEATERSHAKER_MODULE_TYPE) { - if (destModuleState.latchOpen !== true) { - errors.push(errorCreators.heaterShakerLatchClosed()) - } - if (destModuleState.targetSpeed !== null) { - errors.push(errorCreators.heaterShakerIsShaking()) + if (destModuleState != null) { + if ( + destModuleState.type === THERMOCYCLER_MODULE_TYPE && + destModuleState.lidOpen !== true + ) { + errors.push(errorCreators.thermocyclerLidClosed()) + } else if (destModuleState.type === HEATERSHAKER_MODULE_TYPE) { + if (destModuleState.latchOpen !== true) { + errors.push(errorCreators.heaterShakerLatchClosed()) + } + if (destModuleState.targetSpeed !== null) { + errors.push(errorCreators.heaterShakerIsShaking()) + } } } } From 2c21e3995f7bdf444667b96689928b4740eaea29 Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 22 Sep 2023 17:33:35 -0400 Subject: [PATCH 02/19] support selectin 96 channel wells --- .../determineMultiChannelSupport.test.ts | 6 +- .../utils/determineMultiChannelSupport.ts | 2 +- .../components/labware/SelectableLabware.tsx | 35 ++- protocol-designer/src/labware-defs/actions.ts | 5 +- .../src/labware-ingred/actions/thunks.ts | 24 +- .../formLevel/handleFormChange/utils.ts | 2 +- .../src/steplist/substepTimeline.ts | 1 - .../src/steplist/utils/orderWells.ts | 3 +- .../src/top-selectors/substep-highlight.ts | 20 +- .../__tests__/getWellNamePerMultiTip.test.ts | 28 ++- .../js/helpers/__tests__/wellSets.test.ts | 234 +++++++++++++++++- .../js/helpers/getWellNamePerMultiTip.ts | 53 +++- shared-data/js/helpers/index.ts | 1 + .../js/helpers}/orderWells.ts | 3 +- shared-data/js/helpers/wellSets.ts | 46 ++-- step-generation/src/__tests__/utils.test.ts | 2 +- .../dispenseUpdateLiquidState.ts | 1 - .../forAspirate.ts | 1 - .../forPickUpTip.ts | 8 + step-generation/src/robotStateSelectors.ts | 8 +- step-generation/src/utils/index.ts | 2 - step-generation/src/utils/misc.ts | 4 +- 22 files changed, 401 insertions(+), 88 deletions(-) rename {step-generation/src/utils => shared-data/js/helpers}/orderWells.ts (97%) diff --git a/labware-library/src/labware-creator/__tests__/utils/determineMultiChannelSupport.test.ts b/labware-library/src/labware-creator/__tests__/utils/determineMultiChannelSupport.test.ts index f969cb9df87..94ccbeb0f74 100644 --- a/labware-library/src/labware-creator/__tests__/utils/determineMultiChannelSupport.test.ts +++ b/labware-library/src/labware-creator/__tests__/utils/determineMultiChannelSupport.test.ts @@ -26,7 +26,7 @@ describe('determineMultiChannelSupport', () => { it('should allow multi channel when getWellNamePerMultiTip returns 8 wells', () => { const def: any = 'fakeDef' when(getWellNamePerMultiTipMock) - .calledWith(def, 'A1') + .calledWith(def, 'A1', 8) .mockReturnValue(['A1', 'B1', 'C1', 'D1', 'E1', 'F1', 'G1', 'H1']) const result = determineMultiChannelSupport(def) expect(result).toEqual({ @@ -37,7 +37,9 @@ describe('determineMultiChannelSupport', () => { it('should NOT allow multi channel when getWellNamePerMultiTip does not return 8 wells', () => { const def: any = 'fakeDef' - when(getWellNamePerMultiTipMock).calledWith(def, 'A1').mockReturnValue(null) + when(getWellNamePerMultiTipMock) + .calledWith(def, 'A1', 8) + .mockReturnValue(null) const result = determineMultiChannelSupport(def) expect(result).toEqual({ disablePipetteField: false, diff --git a/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts b/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts index 17fea2c9470..7de8b96f4a2 100644 --- a/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts +++ b/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts @@ -16,7 +16,7 @@ export const determineMultiChannelSupport = ( // allow multichannel pipette options only if // all 8 channels fit into the first column correctly const multiChannelTipsFirstColumn = - def !== null ? getWellNamePerMultiTip(def, 'A1') : null + def !== null ? getWellNamePerMultiTip(def, 'A1', 8) : null const allowMultiChannel = multiChannelTipsFirstColumn !== null && diff --git a/protocol-designer/src/components/labware/SelectableLabware.tsx b/protocol-designer/src/components/labware/SelectableLabware.tsx index b9e69074856..acc82b91679 100644 --- a/protocol-designer/src/components/labware/SelectableLabware.tsx +++ b/protocol-designer/src/components/labware/SelectableLabware.tsx @@ -46,13 +46,18 @@ export class SelectableLabware extends React.Component { ) => WellGroup = selectedWells => { const labwareDef = this.props.labwareProps.definition // Returns PRIMARY WELLS from the selection. - if (this.props.pipetteChannels === 8) { + if (this.props.pipetteChannels === 8 || this.props.pipetteChannels === 96) { + const channels = this.props.pipetteChannels // for the wells that have been highlighted, // get all 8-well well sets and merge them const primaryWells: WellGroup = reduce( selectedWells, (acc: WellGroup, _, wellName: string): WellGroup => { - const wellSet = getWellSetForMultichannel(labwareDef, wellName) + const wellSet = getWellSetForMultichannel( + labwareDef, + wellName, + channels + ) if (!wellSet) return acc return { ...acc, [wellSet[0]]: null } }, @@ -71,13 +76,17 @@ export class SelectableLabware extends React.Component { ) => { const labwareDef = this.props.labwareProps.definition if (!e.shiftKey) { - if (this.props.pipetteChannels === 8) { + if ( + this.props.pipetteChannels === 8 || + this.props.pipetteChannels === 96 + ) { + const channels = this.props.pipetteChannels const selectedWells = this._getWellsFromRect(rect) const allWellsForMulti: WellGroup = reduce( selectedWells, (acc: WellGroup, _, wellName: string): WellGroup => { const wellSetForMulti = - getWellSetForMultichannel(labwareDef, wellName) || [] + getWellSetForMultichannel(labwareDef, wellName, channels) || [] const channelWells = arrayToWellGroup(wellSetForMulti) return { ...acc, @@ -106,9 +115,14 @@ export class SelectableLabware extends React.Component { } handleMouseEnterWell: (args: WellMouseEvent) => void = args => { - if (this.props.pipetteChannels === 8) { + if (this.props.pipetteChannels === 8 || this.props.pipetteChannels === 96) { + const channels = this.props.pipetteChannels const labwareDef = this.props.labwareProps.definition - const wellSet = getWellSetForMultichannel(labwareDef, args.wellName) + const wellSet = getWellSetForMultichannel( + labwareDef, + args.wellName, + channels + ) const nextHighlightedWells = arrayToWellGroup(wellSet || []) nextHighlightedWells && this.props.updateHighlightedWells(nextHighlightedWells) @@ -129,18 +143,19 @@ export class SelectableLabware extends React.Component { pipetteChannels, selectedPrimaryWells, } = this.props - // For rendering, show all wells not just primary wells - const allSelectedWells = - pipetteChannels === 8 + let allSelectedWells = + pipetteChannels === 8 || pipetteChannels === 96 ? reduce( selectedPrimaryWells, (acc, _, wellName): WellGroup => { const wellSet = getWellSetForMultichannel( this.props.labwareProps.definition, - wellName + wellName, + pipetteChannels ) if (!wellSet) return acc + console.log('wellSet', wellSet) return { ...acc, ...arrayToWellGroup(wellSet) } }, {} diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index 3f9614131d9..6326b30170c 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -81,8 +81,8 @@ const getIsOverwriteMismatched = ( const matchedMultiUse = matchedWellOrdering && isEqual( - getAllWellSetsForLabware(newDef), - getAllWellSetsForLabware(overwrittenDef) + getAllWellSetsForLabware(newDef, 8), + getAllWellSetsForLabware(overwrittenDef, 8) ) return !(matchedWellOrdering && matchedMultiUse) } @@ -98,6 +98,7 @@ const _createCustomLabwareDef: ( const customLabwareDefs: LabwareDefinition2[] = values( labwareDefSelectors.getCustomLabwareDefsByURI(getState()) ) + // @ts-expect-error(sa, 2021-6-20): null check const file = event.currentTarget.files[0] const reader = new FileReader() diff --git a/protocol-designer/src/labware-ingred/actions/thunks.ts b/protocol-designer/src/labware-ingred/actions/thunks.ts index d5f881b605c..42765454c50 100644 --- a/protocol-designer/src/labware-ingred/actions/thunks.ts +++ b/protocol-designer/src/labware-ingred/actions/thunks.ts @@ -146,13 +146,14 @@ export const duplicateLabware: ( if (templateLabwareDefURI && duplicateSlot) { if (templateAdapterDefURI != null && templateAdapterId != null) { + const adapterDuplicateId = uuid() + ':' + templateAdapterDefURI dispatch({ type: 'DUPLICATE_LABWARE', payload: { // you can't set a nick name for adapters duplicateLabwareNickname: templateAdapterDisplayName ?? '', templateLabwareId: templateAdapterId, - duplicateLabwareId: uuid() + ':' + templateAdapterDefURI, + duplicateLabwareId: adapterDuplicateId, slot: duplicateSlot, }, }) @@ -162,18 +163,19 @@ export const duplicateLabware: ( duplicateLabwareNickname, templateLabwareId, duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, - slot: templateAdapterId, + slot: adapterDuplicateId, + }, + }) + } else { + dispatch({ + type: 'DUPLICATE_LABWARE', + payload: { + duplicateLabwareNickname, + templateLabwareId, + duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, + slot: templateLabwareIdIsOffDeck ? 'offDeck' : duplicateSlot, }, }) } - dispatch({ - type: 'DUPLICATE_LABWARE', - payload: { - duplicateLabwareNickname, - templateLabwareId, - duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, - slot: templateLabwareIdIsOffDeck ? 'offDeck' : duplicateSlot, - }, - }) } } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts index 7efaf545117..bb3d8c61e52 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts @@ -22,7 +22,7 @@ export function getAllWellsFromPrimaryWells( labwareDef: LabwareDefinition2 ): string[] { const allWells = primaryWells.reduce((acc: string[], well: string) => { - const nextWellSet = getWellSetForMultichannel(labwareDef, well) + const nextWellSet = getWellSetForMultichannel(labwareDef, well, 8) // filter out any nulls (but you shouldn't get any) if (!nextWellSet) { diff --git a/protocol-designer/src/steplist/substepTimeline.ts b/protocol-designer/src/steplist/substepTimeline.ts index 8c802b04e51..2ef909db37d 100644 --- a/protocol-designer/src/steplist/substepTimeline.ts +++ b/protocol-designer/src/steplist/substepTimeline.ts @@ -147,7 +147,6 @@ export const substepTimelineMultiChannel = ( const wellsForTips = channels && labwareDef && - // @ts-expect-error 96 channels not yet supported getWellsForTips(channels, labwareDef, wellName).wellsForTips const wellInfo = { labwareId, diff --git a/protocol-designer/src/steplist/utils/orderWells.ts b/protocol-designer/src/steplist/utils/orderWells.ts index c9d81ec795b..ab4db8e4faf 100644 --- a/protocol-designer/src/steplist/utils/orderWells.ts +++ b/protocol-designer/src/steplist/utils/orderWells.ts @@ -1,6 +1,5 @@ import intersection from 'lodash/intersection' -import { orderWells } from '@opentrons/step-generation' -import { LabwareDefinition2 } from '@opentrons/shared-data' +import { LabwareDefinition2, orderWells } from '@opentrons/shared-data' import { WellOrderOption } from '../../form-types' export function getOrderedWells( unorderedWells: string[], diff --git a/protocol-designer/src/top-selectors/substep-highlight.ts b/protocol-designer/src/top-selectors/substep-highlight.ts index 0ce14c6e344..4169f31f97f 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.ts +++ b/protocol-designer/src/top-selectors/substep-highlight.ts @@ -12,15 +12,23 @@ import { PipetteEntity, LabwareEntity } from '@opentrons/step-generation' import { Selector } from '../types' import { SubstepItemData } from '../steplist/types' +type MultiChannels = 8 | 96 + function _wellsForPipette( pipetteEntity: PipetteEntity, labwareEntity: LabwareEntity, wells: string[] ): string[] { // `wells` is all the wells that pipette's channel 1 interacts with. - if (pipetteEntity.spec.channels === 8) { + if (pipetteEntity.spec.channels === 8 || pipetteEntity.spec.channels === 96) { return wells.reduce((acc: string[], well: string) => { - const setOfWellsForMulti = getWellNamePerMultiTip(labwareEntity.def, well) + const channels = pipetteEntity.spec.channels as MultiChannels + const setOfWellsForMulti = getWellNamePerMultiTip( + labwareEntity.def, + well, + channels + ) + return setOfWellsForMulti ? [...acc, ...setOfWellsForMulti] : acc // setOfWellsForMulti is null }, []) } @@ -95,11 +103,12 @@ function _getSelectedWellsForStep( if (pipetteSpec.channels === 1) { wells.push(commandWellName) - } else if (pipetteSpec.channels === 8) { + } else if (pipetteSpec.channels === 8 || pipetteSpec.channels === 96) { const wellSet = getWellSetForMultichannel( invariantContext.labwareEntities[labwareId].def, - commandWellName + commandWellName, + pipetteSpec.channels ) || [] wells.push(...wellSet) } else { @@ -186,7 +195,8 @@ function _getSelectedWellsForSubstep( if (activeTips && activeTips.labwareId === labwareId) { const multiTipWellSet = getWellSetForMultichannel( invariantContext.labwareEntities[labwareId].def, - activeTips.wellName + activeTips.wellName, + 8 ) if (multiTipWellSet) tipWellSet = multiTipWellSet } diff --git a/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts b/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts index fa6a7717ddf..7062e0aa03e 100644 --- a/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts +++ b/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts @@ -13,12 +13,14 @@ const fixture96Plate = fixture_96_plate as LabwareDefinition2 const fixture384Plate = fixture_384_plate as LabwareDefinition2 const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture24Tuberack = fixture_24_tuberack as LabwareDefinition2 +const EIGHT_CHANNEL = 8 +const NINETY_SIX_CHANNEL = 96 describe('96 plate', () => { const labware = fixture96Plate it('A1 => column 1', () => { - expect(getWellNamePerMultiTip(labware, 'A1')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'B1', 'C1', @@ -31,7 +33,7 @@ describe('96 plate', () => { }) it('A2 => column 2', () => { - expect(getWellNamePerMultiTip(labware, 'A2')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A2', EIGHT_CHANNEL)).toEqual([ 'A2', 'B2', 'C2', @@ -44,7 +46,7 @@ describe('96 plate', () => { }) it('B1 => null (cannot access with 8-channel)', () => { - expect(getWellNamePerMultiTip(labware, 'B1')).toEqual(null) + expect(getWellNamePerMultiTip(labware, 'B1', EIGHT_CHANNEL)).toEqual(null) }) }) @@ -52,7 +54,7 @@ describe('384 plate', () => { const labware = fixture384Plate it('A1 => column 1 ACEGIKMO', () => { - expect(getWellNamePerMultiTip(labware, 'A1')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'C1', 'E1', @@ -65,7 +67,7 @@ describe('384 plate', () => { }) it('A2 => column 2 ACEGIKMO', () => { - expect(getWellNamePerMultiTip(labware, 'A2')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A2', EIGHT_CHANNEL)).toEqual([ 'A2', 'C2', 'E2', @@ -78,7 +80,7 @@ describe('384 plate', () => { }) it('B1 => column 1 BDFHJLNP', () => { - expect(getWellNamePerMultiTip(labware, 'B1')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'B1', EIGHT_CHANNEL)).toEqual([ 'B1', 'D1', 'F1', @@ -91,7 +93,7 @@ describe('384 plate', () => { }) it('C1 => null (cannot access with 8-channel)', () => { - expect(getWellNamePerMultiTip(labware, 'C1')).toEqual(null) + expect(getWellNamePerMultiTip(labware, 'C1', EIGHT_CHANNEL)).toEqual(null) }) }) @@ -99,7 +101,7 @@ describe('Fixed trash', () => { const labware = fixtureTrash it('A1 => all tips in A1', () => { - expect(getWellNamePerMultiTip(labware, 'A1')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'A1', 'A1', @@ -112,7 +114,7 @@ describe('Fixed trash', () => { }) it('A2 => null (well does not exist)', () => { - expect(getWellNamePerMultiTip(labware, 'A2')).toEqual(null) + expect(getWellNamePerMultiTip(labware, 'A2', EIGHT_CHANNEL)).toEqual(null) }) }) @@ -121,7 +123,7 @@ describe('tube rack 2mL', () => { it('tube rack 2mL not accessible by 8-channel (return null)', () => { ;['A1', 'A2', 'B1', 'B2'].forEach(well => { - expect(getWellNamePerMultiTip(labware, well)).toEqual(null) + expect(getWellNamePerMultiTip(labware, well, EIGHT_CHANNEL)).toEqual(null) }) }) }) @@ -130,7 +132,7 @@ describe('12 channel trough', () => { const labware = fixture12Trough it('A1 => all tips in A1', () => { - expect(getWellNamePerMultiTip(labware, 'A1')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'A1', 'A1', @@ -143,7 +145,7 @@ describe('12 channel trough', () => { }) it('A2 => all tips in A2', () => { - expect(getWellNamePerMultiTip(labware, 'A2')).toEqual([ + expect(getWellNamePerMultiTip(labware, 'A2', EIGHT_CHANNEL)).toEqual([ 'A2', 'A2', 'A2', @@ -156,6 +158,6 @@ describe('12 channel trough', () => { }) it('B1 => null (well does not exist)', () => { - expect(getWellNamePerMultiTip(labware, 'B1')).toEqual(null) + expect(getWellNamePerMultiTip(labware, 'B1', EIGHT_CHANNEL)).toEqual(null) }) }) diff --git a/shared-data/js/helpers/__tests__/wellSets.test.ts b/shared-data/js/helpers/__tests__/wellSets.test.ts index 68ea47ec823..179eb382c23 100644 --- a/shared-data/js/helpers/__tests__/wellSets.test.ts +++ b/shared-data/js/helpers/__tests__/wellSets.test.ts @@ -16,6 +16,205 @@ const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture96Plate = fixture_96_plate as LabwareDefinition2 const fixture384Plate = fixture_384_plate as LabwareDefinition2 const fixtureOverlappyWellplate = fixture_overlappy_wellplate as LabwareDefinition2 +const EIGHT_CHANNEL = 8 +const NINETY_SIX_CHANNEL = 96 +const wellsForReservoir = [ + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A1', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A2', + 'A3', + 'A3', + 'A3', + 'A3', + 'A3', + 'A3', + 'A3', + 'A3', + 'A4', + 'A4', + 'A4', + 'A4', + 'A4', + 'A4', + 'A4', + 'A4', + 'A5', + 'A5', + 'A5', + 'A5', + 'A5', + 'A5', + 'A5', + 'A5', + 'A6', + 'A6', + 'A6', + 'A6', + 'A6', + 'A6', + 'A6', + 'A6', + 'A7', + 'A7', + 'A7', + 'A7', + 'A7', + 'A7', + 'A7', + 'A7', + 'A8', + 'A8', + 'A8', + 'A8', + 'A8', + 'A8', + 'A8', + 'A8', + 'A9', + 'A9', + 'A9', + 'A9', + 'A9', + 'A9', + 'A9', + 'A9', + 'A10', + 'A10', + 'A10', + 'A10', + 'A10', + 'A10', + 'A10', + 'A10', + 'A11', + 'A11', + 'A11', + 'A11', + 'A11', + 'A11', + 'A11', + 'A11', + 'A12', + 'A12', + 'A12', + 'A12', + 'A12', + 'A12', + 'A12', + 'A12', +] + +const wellsFor96WellPlate = [ + 'A1', + 'B1', + 'C1', + 'D1', + 'E1', + 'F1', + 'G1', + 'H1', + 'A2', + 'B2', + 'C2', + 'D2', + 'E2', + 'F2', + 'G2', + 'H2', + 'A3', + 'B3', + 'C3', + 'D3', + 'E3', + 'F3', + 'G3', + 'H3', + 'A4', + 'B4', + 'C4', + 'D4', + 'E4', + 'F4', + 'G4', + 'H4', + 'A5', + 'B5', + 'C5', + 'D5', + 'E5', + 'F5', + 'G5', + 'H5', + 'A6', + 'B6', + 'C6', + 'D6', + 'E6', + 'F6', + 'G6', + 'H6', + 'A7', + 'B7', + 'C7', + 'D7', + 'E7', + 'F7', + 'G7', + 'H7', + 'A8', + 'B8', + 'C8', + 'D8', + 'E8', + 'F8', + 'G8', + 'H8', + 'A9', + 'B9', + 'C9', + 'D9', + 'E9', + 'F9', + 'G9', + 'H9', + 'A10', + 'B10', + 'C10', + 'D10', + 'E10', + 'F10', + 'G10', + 'H10', + 'A11', + 'B11', + 'C11', + 'D11', + 'E11', + 'F11', + 'G11', + 'H11', + 'A12', + 'B12', + 'C12', + 'D12', + 'E12', + 'F12', + 'G12', + 'H12', +] describe('findWellAt', () => { it('should determine if given (x, y) is within a rectangular well', () => { @@ -106,7 +305,7 @@ describe('getWellSetForMultichannel (integration test)', () => { it('96-flat', () => { const labwareDef = fixture96Plate - expect(getWellSetForMultichannel(labwareDef, 'A1')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'B1', 'C1', @@ -117,7 +316,7 @@ describe('getWellSetForMultichannel (integration test)', () => { 'H1', ]) - expect(getWellSetForMultichannel(labwareDef, 'B1')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'B1', EIGHT_CHANNEL)).toEqual([ 'A1', 'B1', 'C1', @@ -128,7 +327,7 @@ describe('getWellSetForMultichannel (integration test)', () => { 'H1', ]) - expect(getWellSetForMultichannel(labwareDef, 'H1')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'H1', EIGHT_CHANNEL)).toEqual([ 'A1', 'B1', 'C1', @@ -139,7 +338,7 @@ describe('getWellSetForMultichannel (integration test)', () => { 'H1', ]) - expect(getWellSetForMultichannel(labwareDef, 'A2')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'A2', EIGHT_CHANNEL)).toEqual([ 'A2', 'B2', 'C2', @@ -149,18 +348,25 @@ describe('getWellSetForMultichannel (integration test)', () => { 'G2', 'H2', ]) + + // 96-channel + expect( + getWellSetForMultichannel(labwareDef, 'A1', NINETY_SIX_CHANNEL) + ).toEqual(wellsFor96WellPlate) }) it('invalid well', () => { const labwareDef = fixture96Plate - expect(getWellSetForMultichannel(labwareDef, 'A13')).toBeFalsy() + expect( + getWellSetForMultichannel(labwareDef, 'A13', EIGHT_CHANNEL) + ).toBeFalsy() }) it('trough-12row', () => { const labwareDef = fixture12Trough - expect(getWellSetForMultichannel(labwareDef, 'A1')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'A1', EIGHT_CHANNEL)).toEqual([ 'A1', 'A1', 'A1', @@ -171,7 +377,7 @@ describe('getWellSetForMultichannel (integration test)', () => { 'A1', ]) - expect(getWellSetForMultichannel(labwareDef, 'A2')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'A2', EIGHT_CHANNEL)).toEqual([ 'A2', 'A2', 'A2', @@ -181,12 +387,17 @@ describe('getWellSetForMultichannel (integration test)', () => { 'A2', 'A2', ]) + + // 96-channel + expect( + getWellSetForMultichannel(labwareDef, 'A1', NINETY_SIX_CHANNEL) + ).toEqual(wellsForReservoir) }) it('384-plate', () => { const labwareDef = fixture384Plate - expect(getWellSetForMultichannel(labwareDef, 'C1')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'C1', EIGHT_CHANNEL)).toEqual([ 'A1', 'C1', 'E1', @@ -197,7 +408,7 @@ describe('getWellSetForMultichannel (integration test)', () => { 'O1', ]) - expect(getWellSetForMultichannel(labwareDef, 'F2')).toEqual([ + expect(getWellSetForMultichannel(labwareDef, 'F2', EIGHT_CHANNEL)).toEqual([ 'B2', 'D2', 'F2', @@ -207,5 +418,10 @@ describe('getWellSetForMultichannel (integration test)', () => { 'N2', 'P2', ]) + + // 96-channel + // expect( + // getWellSetForMultichannel(labwareDef, 'A1', NINETY_SIX_CHANNEL) + // ).toEqual([]) }) }) diff --git a/shared-data/js/helpers/getWellNamePerMultiTip.ts b/shared-data/js/helpers/getWellNamePerMultiTip.ts index f7b0192a332..5464069b1f5 100644 --- a/shared-data/js/helpers/getWellNamePerMultiTip.ts +++ b/shared-data/js/helpers/getWellNamePerMultiTip.ts @@ -1,5 +1,5 @@ import range from 'lodash/range' -import { getLabwareHasQuirk, sortWells } from '.' +import { getLabwareHasQuirk, orderWells, sortWells } from './index' import type { LabwareDefinition2 } from '../types' // TODO Ian 2018-03-13 pull pipette offsets/positions from some pipette definitions data @@ -36,7 +36,8 @@ export function findWellAt( // "topWellName" means well at the "top" of the column we're accessing: usually A row, or B row for 384-format export function getWellNamePerMultiTip( labwareDef: LabwareDefinition2, - topWellName: string + topWellName: string, + channels: 8 | 96 ): string[] | null { const topWell = labwareDef.wells[topWellName] @@ -51,6 +52,7 @@ export function getWellNamePerMultiTip( let offsetYTipPositions: number[] = range(0, 8).map( tipNo => y - tipNo * OFFSET_8_CHANNEL ) + const orderedWells = orderWells(labwareDef.ordering, 't2b', 'l2r') if (getLabwareHasQuirk(labwareDef, 'centerMultichannelOnWells')) { // move multichannel up in Y by half the pipette's tip span to center it in the well @@ -58,19 +60,58 @@ export function getWellNamePerMultiTip( tipPosY => tipPosY + MULTICHANNEL_TIP_SPAN / 2 ) } - + console.log('offsetYTipPositions', offsetYTipPositions) // Return null for containers with any undefined wells const wellsAccessed = offsetYTipPositions.reduce( (acc: string[] | null, tipPosY) => { const wellForTip = findWellAt(labwareDef, x, tipPosY) - if (acc === null || !wellForTip) { return null } - return acc.concat(wellForTip) }, [] ) - return wellsAccessed + // let ninetySixChannelWells = orderedWells + // special casing 384 well plates since its the only labware + // where the full 96-channel tip rack can't aspirate from + // every well + // if (orderedWells.length === 384) { + // const selectedWells: string[] = [] + + // const maxX = 12 // Number of wells in the X-axis + // const maxY = 8 // Number of wells in the Y-axis + // const maxCount = maxX * maxY + + // // Split the starting well into row and column parts + // const startingRow = topWellName.charAt(0) + // const startingCol = parseInt(topWellName.substring(1), 10) + + // let currentRow = startingRow + // let currentCol = startingCol + + // for (let y = 0; y < maxY; y++) { + // for (let x = 0; x < maxX; x++) { + // // Ensure the currentRow and currentCol are within the bounds of your array + // if (currentRow.charCodeAt(0) <= 'P'.charCodeAt(0) && currentCol <= 24) { + // // Get the current well + // const well = currentRow + currentCol.toString() + // selectedWells.push(well) + + // // Move to the next well in the X-axis + // currentCol += 2 + // } + // } + + // // Move to the next row in the Y-axis and reset the X-axis + // currentRow = String.fromCharCode(currentRow.charCodeAt(0) + 1) + // currentCol = startingCol + // } + + // ninetySixChannelWells = selectedWells + // } + + // console.log('wellsAccessed', wellsAccessed) + // console.log('orderedWells', orderedWells) + return channels === 8 ? wellsAccessed : orderedWells } diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 2bfb8f4b0d2..24c4dcf9961 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -16,6 +16,7 @@ import type { export { getWellNamePerMultiTip } from './getWellNamePerMultiTip' export { getWellTotalVolume } from './getWellTotalVolume' export { wellIsRect } from './wellIsRect' +export { orderWells } from './orderWells' export * from './parseProtocolData' export * from './volume' diff --git a/step-generation/src/utils/orderWells.ts b/shared-data/js/helpers/orderWells.ts similarity index 97% rename from step-generation/src/utils/orderWells.ts rename to shared-data/js/helpers/orderWells.ts index 4a9243fa4f9..a4b2e7400e0 100644 --- a/step-generation/src/utils/orderWells.ts +++ b/shared-data/js/helpers/orderWells.ts @@ -2,7 +2,8 @@ import zipWith from 'lodash/zipWith' import uniq from 'lodash/uniq' import compact from 'lodash/compact' import flatten from 'lodash/flatten' -import type { WellOrderOption } from '../types' + +type WellOrderOption = 'l2r' | 'r2l' | 't2b' | 'b2t' // labware definitions in shared-data have an ordering // attribute which is an Array of Arrays of wells. Each inner diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index 8460640ade2..22c0c0bd767 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -21,14 +21,23 @@ type WellSetByPrimaryWell = string[][] // Compute all well sets for a labware def (non-memoized) function _getAllWellSetsForLabware( - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + channels: 8 | 96 ): WellSetByPrimaryWell { const allWells: string[] = Object.keys(labwareDef.wells) return allWells.reduce( (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { - const wellSet = getWellNamePerMultiTip(labwareDef, well) - return wellSet === null ? acc : [...acc, wellSet] + const wellSet = getWellNamePerMultiTip(labwareDef, well, channels) + console.log(wellSet, 'wellSet') + + if (wellSet === null) { + return acc + } else if (channels === 8) { + return [...acc, wellSet] + } else { + return [wellSet] + } }, [] ) @@ -37,12 +46,14 @@ function _getAllWellSetsForLabware( // creates memoized getAllWellSetsForLabware + getWellSetForMultichannel fns. export interface WellSetHelpers { getAllWellSetsForLabware: ( - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + channels: 8 | 96 ) => WellSetByPrimaryWell getWellSetForMultichannel: ( labwareDef: LabwareDefinition2, - well: string + well: string, + channels: 8 | 96 ) => string[] | null | undefined canPipetteUseLabware: ( @@ -60,7 +71,8 @@ export const makeWellSetHelpers = (): WellSetHelpers => { }> = {} const getAllWellSetsForLabware = ( - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + channels: 8 | 96 ): WellSetByPrimaryWell => { const labwareDefURI = getLabwareDefURI(labwareDef) const c = cache[labwareDefURI] @@ -71,7 +83,7 @@ export const makeWellSetHelpers = (): WellSetHelpers => { return c.wellSetByPrimaryWell } - const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef) + const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef, channels) cache[labwareDefURI] = { labwareDef, @@ -82,16 +94,23 @@ export const makeWellSetHelpers = (): WellSetHelpers => { const getWellSetForMultichannel = ( labwareDef: LabwareDefinition2, - well: string + well: string, + channels: 8 | 96 ): string[] | null | undefined => { /** Given a well for a labware, returns the well set it belongs to (or null) * for 8-channel access. * Ie: C2 for 96-flat => ['A2', 'B2', 'C2', ... 'H2'] * Or A1 for trough => ['A1', 'A1', 'A1', ...] **/ - const allWellSets = getAllWellSetsForLabware(labwareDef) - - return allWellSets.find((wellSet: string[]) => wellSet.includes(well)) + const allWellSets = getAllWellSetsForLabware(labwareDef, channels) + const allWells: string[] = allWellSets.reduce( + (acc, wells) => acc.concat(wells), + [] + ) + + return channels === 8 + ? allWellSets.find((wellSet: string[]) => wellSet.includes(well)) + : allWells } const canPipetteUseLabware = ( @@ -103,19 +122,18 @@ export const makeWellSetHelpers = (): WellSetHelpers => { return true } - const allWellSets = getAllWellSetsForLabware(labwareDef) + const allWellSets = getAllWellSetsForLabware(labwareDef, 8) return allWellSets.some(wellSet => { const uniqueWells = uniq(wellSet) // if all wells are non-null, and there are either 1 (reservoir-like) // or 8 (well plate-like) unique wells in the set, - // then assume multi-channel will work + // then assume both 8 and 96 channel pipettes will work return ( uniqueWells.every(well => well != null) && [1, 8].includes(uniqueWells.length) ) }) } - return { getAllWellSetsForLabware, getWellSetForMultichannel, diff --git a/step-generation/src/__tests__/utils.test.ts b/step-generation/src/__tests__/utils.test.ts index 75fbd22fa78..1807b1c1b8b 100644 --- a/step-generation/src/__tests__/utils.test.ts +++ b/step-generation/src/__tests__/utils.test.ts @@ -9,6 +9,7 @@ import { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, HEATERSHAKER_MODULE_TYPE, PipetteNameSpecs, + orderWells, } from '@opentrons/shared-data' import { fixtureP10Single, @@ -37,7 +38,6 @@ import { getIsHeaterShakerEastWestWithLatchOpen, getIsHeaterShakerEastWestMultiChannelPipette, getIsTallLabwareEastWestOfHeaterShaker, - orderWells, pipetteAdjacentHeaterShakerWhileShaking, thermocyclerPipetteCollision, } from '../utils' diff --git a/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts b/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts index a5c1edc829a..c05b430cb4e 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/dispenseUpdateLiquidState.ts @@ -49,7 +49,6 @@ export function dispenseUpdateLiquidState( 'in dispenseUpdateLiquidState, either volume or useFullVolume are required' ) const { wellsForTips, allWellsShared } = getWellsForTips( - // @ts-expect-error 96 channels not yet supported pipetteSpec.channels, labwareDef, wellName diff --git a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts index 89bfcb0e64f..6ac258eb88e 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forAspirate.ts @@ -23,7 +23,6 @@ export function forAspirate( const pipetteSpec = invariantContext.pipetteEntities[pipetteId].spec const labwareDef = invariantContext.labwareEntities[labwareId].def const { allWellsShared, wellsForTips } = getWellsForTips( - // @ts-expect-error 96 channels not yet supported pipetteSpec.channels, labwareDef, params.wellName diff --git a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts index 8ad6c3455d8..17990124187 100644 --- a/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts +++ b/step-generation/src/getNextRobotStateAndWarnings/forPickUpTip.ts @@ -32,5 +32,13 @@ export function forPickUpTip( allWells.forEach(function (wellName) { tipState.tipracks[labwareId][wellName] = false }) + } else if (pipetteSpec.channels === 96) { + const allTips: string[] = tiprackDef.ordering.reduce( + (acc, wells) => acc.concat(wells), + [] + ) + allTips.forEach(function (wellName) { + tipState.tipracks[labwareId][wellName] = false + }) } } diff --git a/step-generation/src/robotStateSelectors.ts b/step-generation/src/robotStateSelectors.ts index db7b8a5d8f8..ebb75ee9dc6 100644 --- a/step-generation/src/robotStateSelectors.ts +++ b/step-generation/src/robotStateSelectors.ts @@ -1,11 +1,11 @@ import assert from 'assert' // TODO: Ian 2019-04-18 move orderWells somewhere more general -- shared-data util? -import { orderWells } from './utils/orderWells' import min from 'lodash/min' import sortBy from 'lodash/sortBy' import { getTiprackVolume, THERMOCYCLER_MODULE_TYPE, + orderWells, } from '@opentrons/shared-data' import type { InvariantContext, @@ -36,19 +36,21 @@ export function _getNextTip(args: { const hasTip = (wellName: string): boolean => tiprackWellsState[wellName] const orderedWells = orderWells(tiprackDef.ordering, 't2b', 'l2r') - if (pipetteChannels === 1) { const well = orderedWells.find(hasTip) return well || null } if (pipetteChannels === 8) { - // Otherwise, pipetteChannels === 8. // return first well in the column (for 96-well format, the 'A' row) const tiprackColumns = tiprackDef.ordering const fullColumn = tiprackColumns.find(col => col.every(hasTip)) return fullColumn != null ? fullColumn[0] : null } + if (pipetteChannels === 96) { + const allWellsHaveTip = orderedWells.every(hasTip) + return allWellsHaveTip ? orderedWells[0] : null + } assert(false, `Pipette ${pipetteId} has no channels/spec, cannot _getNextTip`) return null diff --git a/step-generation/src/utils/index.ts b/step-generation/src/utils/index.ts index 1534e33d114..fd0df1a2940 100644 --- a/step-generation/src/utils/index.ts +++ b/step-generation/src/utils/index.ts @@ -4,13 +4,11 @@ import { curryCommandCreator } from './curryCommandCreator' import { reduceCommandCreators } from './reduceCommandCreators' import { modulePipetteCollision } from './modulePipetteCollision' import { thermocyclerPipetteCollision } from './thermocyclerPipetteCollision' -import { orderWells } from './orderWells' import { isValidSlot } from './isValidSlot' import { getLabwareSlot } from './getLabwareSlot' export { commandCreatorsTimeline, curryCommandCreator, - orderWells, reduceCommandCreators, modulePipetteCollision, thermocyclerPipetteCollision, diff --git a/step-generation/src/utils/misc.ts b/step-generation/src/utils/misc.ts index ff6c7b1d08c..8dd2df669ff 100644 --- a/step-generation/src/utils/misc.ts +++ b/step-generation/src/utils/misc.ts @@ -142,7 +142,7 @@ export function mergeLiquid( } // TODO: Ian 2019-04-19 move to shared-data helpers? export function getWellsForTips( - channels: 1 | 8, + channels: 1 | 8 | 96, labwareDef: LabwareDefinition2, well: string ): { @@ -151,7 +151,7 @@ export function getWellsForTips( } { // Array of wells corresponding to the tip at each position. const wellsForTips = - channels === 1 ? [well] : getWellNamePerMultiTip(labwareDef, well) + channels === 1 ? [well] : getWellNamePerMultiTip(labwareDef, well, channels) if (!wellsForTips) { console.warn( From 9fc187bd3a0e2a41c48a7f68e915aecdc108d2b1 Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 22 Sep 2023 18:01:11 -0400 Subject: [PATCH 03/19] clean up a bit --- .../components/labware/SelectableLabware.tsx | 3 +-- robot-server/simulators/test.json | 18 ++++-------------- .../js/helpers/getWellNamePerMultiTip.ts | 3 ++- shared-data/js/helpers/wellSets.ts | 1 - 4 files changed, 7 insertions(+), 18 deletions(-) diff --git a/protocol-designer/src/components/labware/SelectableLabware.tsx b/protocol-designer/src/components/labware/SelectableLabware.tsx index acc82b91679..ab0f686be80 100644 --- a/protocol-designer/src/components/labware/SelectableLabware.tsx +++ b/protocol-designer/src/components/labware/SelectableLabware.tsx @@ -144,7 +144,7 @@ export class SelectableLabware extends React.Component { selectedPrimaryWells, } = this.props // For rendering, show all wells not just primary wells - let allSelectedWells = + const allSelectedWells = pipetteChannels === 8 || pipetteChannels === 96 ? reduce( selectedPrimaryWells, @@ -155,7 +155,6 @@ export class SelectableLabware extends React.Component { pipetteChannels ) if (!wellSet) return acc - console.log('wellSet', wellSet) return { ...acc, ...arrayToWellGroup(wellSet) } }, {} diff --git a/robot-server/simulators/test.json b/robot-server/simulators/test.json index 9594846b20f..0e23a2e8351 100644 --- a/robot-server/simulators/test.json +++ b/robot-server/simulators/test.json @@ -1,22 +1,12 @@ { - "machine": "OT-3 Standard", - "strict_attached_instruments": false, "attached_instruments": { "right": { - "model": "p1000_single_v3.4", - "id": "321", - "max_volume": 1000, - "name": "p1000_single", - "tip_length": 0, - "channels": 1 + "model": "p300_single_v1", + "id": "321" }, "left": { - "model": "p50_single_v3.4", - "id": "123", - "max_volume": 50, - "name": "p50_single", - "tip_length": 0, - "channels": 1 + "model": "p10_single_v1", + "id": "123" } }, "attached_modules": { diff --git a/shared-data/js/helpers/getWellNamePerMultiTip.ts b/shared-data/js/helpers/getWellNamePerMultiTip.ts index 5464069b1f5..53879814cd1 100644 --- a/shared-data/js/helpers/getWellNamePerMultiTip.ts +++ b/shared-data/js/helpers/getWellNamePerMultiTip.ts @@ -49,7 +49,7 @@ export function getWellNamePerMultiTip( } const { x, y } = topWell - let offsetYTipPositions: number[] = range(0, 8).map( + let offsetYTipPositions: number[] = range(0, channels).map( tipNo => y - tipNo * OFFSET_8_CHANNEL ) const orderedWells = orderWells(labwareDef.ordering, 't2b', 'l2r') @@ -72,6 +72,7 @@ export function getWellNamePerMultiTip( }, [] ) + console.log(' wellsAccessed', wellsAccessed) // let ninetySixChannelWells = orderedWells // special casing 384 well plates since its the only labware // where the full 96-channel tip rack can't aspirate from diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index 22c0c0bd767..3c0adb30ae1 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -29,7 +29,6 @@ function _getAllWellSetsForLabware( return allWells.reduce( (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { const wellSet = getWellNamePerMultiTip(labwareDef, well, channels) - console.log(wellSet, 'wellSet') if (wellSet === null) { return acc From 9fd92547970542c433ff2b3b847662c9e3f24f11 Mon Sep 17 00:00:00 2001 From: Jethary Date: Sat, 23 Sep 2023 13:03:08 -0400 Subject: [PATCH 04/19] add support for 384 well plate --- .../__tests__/getWellNamePerMultiTip.test.ts | 1 - .../js/helpers/__tests__/orderWells.test.ts | 87 +++++++++++++++ .../js/helpers/__tests__/wellSets.test.ts | 103 +++--------------- .../helpers/get96Channel384WellPlateWells.ts | 42 +++++++ .../js/helpers/getWellNamePerMultiTip.ts | 56 ++-------- shared-data/js/helpers/index.ts | 1 + shared-data/js/helpers/orderWells.ts | 2 +- shared-data/js/helpers/wellSets.ts | 33 ++++-- .../name/pipetteNameSpecFixtures.json | 44 ++++++++ step-generation/src/__tests__/utils.test.ts | 87 --------------- step-generation/src/types.ts | 3 - 11 files changed, 226 insertions(+), 233 deletions(-) create mode 100644 shared-data/js/helpers/__tests__/orderWells.test.ts create mode 100644 shared-data/js/helpers/get96Channel384WellPlateWells.ts diff --git a/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts b/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts index 7062e0aa03e..33b712c6f63 100644 --- a/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts +++ b/shared-data/js/__tests__/getWellNamePerMultiTip.test.ts @@ -14,7 +14,6 @@ const fixture384Plate = fixture_384_plate as LabwareDefinition2 const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture24Tuberack = fixture_24_tuberack as LabwareDefinition2 const EIGHT_CHANNEL = 8 -const NINETY_SIX_CHANNEL = 96 describe('96 plate', () => { const labware = fixture96Plate diff --git a/shared-data/js/helpers/__tests__/orderWells.test.ts b/shared-data/js/helpers/__tests__/orderWells.test.ts new file mode 100644 index 00000000000..462f487e7a7 --- /dev/null +++ b/shared-data/js/helpers/__tests__/orderWells.test.ts @@ -0,0 +1,87 @@ +import { orderWells } from '../orderWells' +import type { WellOrderOption } from '../orderWells' + +describe('orderWells', () => { + const orderTuples: Array<[WellOrderOption, WellOrderOption]> = [ + ['t2b', 'l2r'], + ['t2b', 'r2l'], + ['b2t', 'l2r'], + ['b2t', 'r2l'], + ['l2r', 't2b'], + ['l2r', 'b2t'], + ['r2l', 't2b'], + ['r2l', 'b2t'], + ] + + describe('regular labware', () => { + const regularOrdering = [ + ['A1', 'B1'], + ['A2', 'B2'], + ] + const regularAnswerMap: Record< + WellOrderOption, + Partial> + > = { + t2b: { + l2r: ['A1', 'B1', 'A2', 'B2'], + r2l: ['A2', 'B2', 'A1', 'B1'], + }, + b2t: { + l2r: ['B1', 'A1', 'B2', 'A2'], + r2l: ['B2', 'A2', 'B1', 'A1'], + }, + l2r: { + t2b: ['A1', 'A2', 'B1', 'B2'], + b2t: ['B1', 'B2', 'A1', 'A2'], + }, + r2l: { + t2b: ['A2', 'A1', 'B2', 'B1'], + b2t: ['B2', 'B1', 'A2', 'A1'], + }, + } + orderTuples.forEach(tuple => { + it(`first ${tuple[0]} then ${tuple[1]}`, () => { + expect(orderWells(regularOrdering, ...tuple)).toEqual( + regularAnswerMap[tuple[0]][tuple[1]] + ) + }) + }) + describe('irregular labware', () => { + const irregularOrdering = [ + ['A1', 'B1'], + ['A2', 'B2', 'C2'], + ['A3'], + ['A4', 'B4', 'C4', 'D4'], + ] + const irregularAnswerMap: Record< + WellOrderOption, + Partial> + > = { + t2b: { + l2r: ['A1', 'B1', 'A2', 'B2', 'C2', 'A3', 'A4', 'B4', 'C4', 'D4'], + r2l: ['A4', 'B4', 'C4', 'D4', 'A3', 'A2', 'B2', 'C2', 'A1', 'B1'], + }, + b2t: { + l2r: ['B1', 'A1', 'C2', 'B2', 'A2', 'A3', 'D4', 'C4', 'B4', 'A4'], + r2l: ['D4', 'C4', 'B4', 'A4', 'A3', 'C2', 'B2', 'A2', 'B1', 'A1'], + }, + l2r: { + t2b: ['A1', 'A2', 'A3', 'A4', 'B1', 'B2', 'B4', 'C2', 'C4', 'D4'], + b2t: ['D4', 'C2', 'C4', 'B1', 'B2', 'B4', 'A1', 'A2', 'A3', 'A4'], + }, + r2l: { + t2b: ['A4', 'A3', 'A2', 'A1', 'B4', 'B2', 'B1', 'C4', 'C2', 'D4'], + b2t: ['D4', 'C4', 'C2', 'B4', 'B2', 'B1', 'A4', 'A3', 'A2', 'A1'], + }, + } + + orderTuples.forEach(tuple => { + it(`first ${tuple[0]} then ${tuple[1]}`, () => { + expect(orderWells(irregularOrdering, ...tuple)).toEqual( + irregularAnswerMap[tuple[0]][tuple[1]] + ) + }) + }) + }) + }) +}) diff --git a/shared-data/js/helpers/__tests__/wellSets.test.ts b/shared-data/js/helpers/__tests__/wellSets.test.ts index 179eb382c23..a0be4f4edde 100644 --- a/shared-data/js/helpers/__tests__/wellSets.test.ts +++ b/shared-data/js/helpers/__tests__/wellSets.test.ts @@ -3,15 +3,16 @@ import fixture_12_trough from '../../../labware/fixtures/2/fixture_12_trough.jso import fixture_96_plate from '../../../labware/fixtures/2/fixture_96_plate.json' import fixture_384_plate from '../../../labware/fixtures/2/fixture_384_plate.json' import fixture_overlappy_wellplate from '../../../labware/fixtures/2/fixture_overlappy_wellplate.json' - import { makeWellSetHelpers } from '../wellSets' import { findWellAt } from '../getWellNamePerMultiTip' +import { get96Channel384WellPlateWells, orderWells } from '..' import type { LabwareDefinition2, PipetteNameSpecs } from '../../types' import type { WellSetHelpers } from '../wellSets' const fixtureP10Single = pipetteNameSpecsFixtures.p10_single as PipetteNameSpecs const fixtureP10Multi = pipetteNameSpecsFixtures.p10_multi as PipetteNameSpecs +const fixtureP100096 = (pipetteNameSpecsFixtures.p1000_96 as any) as PipetteNameSpecs const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture96Plate = fixture_96_plate as LabwareDefinition2 const fixture384Plate = fixture_384_plate as LabwareDefinition2 @@ -20,100 +21,16 @@ const EIGHT_CHANNEL = 8 const NINETY_SIX_CHANNEL = 96 const wellsForReservoir = [ 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A1', - 'A2', - 'A2', - 'A2', 'A2', - 'A2', - 'A2', - 'A2', - 'A2', - 'A3', - 'A3', - 'A3', - 'A3', - 'A3', - 'A3', - 'A3', 'A3', 'A4', - 'A4', - 'A4', - 'A4', - 'A4', - 'A4', - 'A4', - 'A4', - 'A5', - 'A5', - 'A5', - 'A5', 'A5', - 'A5', - 'A5', - 'A5', - 'A6', - 'A6', - 'A6', - 'A6', - 'A6', - 'A6', - 'A6', 'A6', 'A7', - 'A7', - 'A7', - 'A7', - 'A7', - 'A7', - 'A7', - 'A7', - 'A8', - 'A8', - 'A8', - 'A8', 'A8', - 'A8', - 'A8', - 'A8', - 'A9', - 'A9', - 'A9', 'A9', - 'A9', - 'A9', - 'A9', - 'A9', - 'A10', 'A10', - 'A10', - 'A10', - 'A10', - 'A10', - 'A10', - 'A10', - 'A11', - 'A11', - 'A11', - 'A11', - 'A11', 'A11', - 'A11', - 'A11', - 'A12', - 'A12', - 'A12', - 'A12', - 'A12', - 'A12', - 'A12', 'A12', ] @@ -279,11 +196,13 @@ describe('canPipetteUseLabware', () => { canPipetteUseLabware = helpers.canPipetteUseLabware }) - it('returns false when wells are too close together for multi channel pipette', () => { + it('returns false when wells are too close together for multi channel pipettes', () => { const labwareDef = fixtureOverlappyWellplate const pipette = fixtureP10Multi + const pipette96 = fixtureP100096 expect(canPipetteUseLabware(pipette, labwareDef)).toBe(false) + expect(canPipetteUseLabware(pipette96, labwareDef)).toBe(false) }) it('returns true when pipette is single channel', () => { @@ -396,6 +315,12 @@ describe('getWellSetForMultichannel (integration test)', () => { it('384-plate', () => { const labwareDef = fixture384Plate + const well96Channel = 'A1' + const all384Wells = orderWells(labwareDef.ordering, 't2b', 'l2r') + const ninetySixChannelWells = get96Channel384WellPlateWells( + all384Wells, + well96Channel + ) expect(getWellSetForMultichannel(labwareDef, 'C1', EIGHT_CHANNEL)).toEqual([ 'A1', @@ -420,8 +345,8 @@ describe('getWellSetForMultichannel (integration test)', () => { ]) // 96-channel - // expect( - // getWellSetForMultichannel(labwareDef, 'A1', NINETY_SIX_CHANNEL) - // ).toEqual([]) + expect( + getWellSetForMultichannel(labwareDef, well96Channel, NINETY_SIX_CHANNEL) + ).toEqual(ninetySixChannelWells) }) }) diff --git a/shared-data/js/helpers/get96Channel384WellPlateWells.ts b/shared-data/js/helpers/get96Channel384WellPlateWells.ts new file mode 100644 index 00000000000..96b4b4a1b88 --- /dev/null +++ b/shared-data/js/helpers/get96Channel384WellPlateWells.ts @@ -0,0 +1,42 @@ +export function get96Channel384WellPlateWells( + all384Wells: string[], + well: string +): string[] { + const totalWells = 96 * 2 // multiplying 2 because we will remove either odd or even numbers + + const filterWellsFor96Channel = (start: number, isOdd: boolean): string[] => { + const filteredWells: string[] = [] + let count = start + for (let i = 0; i < totalWells; i++) { + if (count < all384Wells.length) { + const well = all384Wells[count] + const numberFromWell = well.match(/\d+/) + if (numberFromWell) { + const number = parseInt(numberFromWell[0]) + if ((number % 2 === 1 && isOdd) || (number % 2 === 0 && !isOdd)) { + filteredWells.push(well) + } + } + count += 2 + } else { + break + } + } + return filteredWells + } + + const allSetsOfWells = [ + filterWellsFor96Channel(0, true), + filterWellsFor96Channel(1, true), + filterWellsFor96Channel(0, false), + filterWellsFor96Channel(1, false), + ] + + for (const wells of allSetsOfWells) { + if (wells.includes(well)) { + return wells + } + } + + return [] +} diff --git a/shared-data/js/helpers/getWellNamePerMultiTip.ts b/shared-data/js/helpers/getWellNamePerMultiTip.ts index 53879814cd1..37026138214 100644 --- a/shared-data/js/helpers/getWellNamePerMultiTip.ts +++ b/shared-data/js/helpers/getWellNamePerMultiTip.ts @@ -1,4 +1,5 @@ import range from 'lodash/range' +import { get96Channel384WellPlateWells } from './get96Channel384WellPlateWells' import { getLabwareHasQuirk, orderWells, sortWells } from './index' import type { LabwareDefinition2 } from '../types' @@ -40,7 +41,6 @@ export function getWellNamePerMultiTip( channels: 8 | 96 ): string[] | null { const topWell = labwareDef.wells[topWellName] - if (!topWell) { console.warn( `well "${topWellName}" does not exist in labware ${labwareDef?.namespace}/${labwareDef?.parameters?.loadName}, cannot getWellNamePerMultiTip` @@ -49,7 +49,7 @@ export function getWellNamePerMultiTip( } const { x, y } = topWell - let offsetYTipPositions: number[] = range(0, channels).map( + let offsetYTipPositions: number[] = range(0, 8).map( tipNo => y - tipNo * OFFSET_8_CHANNEL ) const orderedWells = orderWells(labwareDef.ordering, 't2b', 'l2r') @@ -60,7 +60,6 @@ export function getWellNamePerMultiTip( tipPosY => tipPosY + MULTICHANNEL_TIP_SPAN / 2 ) } - console.log('offsetYTipPositions', offsetYTipPositions) // Return null for containers with any undefined wells const wellsAccessed = offsetYTipPositions.reduce( (acc: string[] | null, tipPosY) => { @@ -72,47 +71,16 @@ export function getWellNamePerMultiTip( }, [] ) - console.log(' wellsAccessed', wellsAccessed) - // let ninetySixChannelWells = orderedWells - // special casing 384 well plates since its the only labware - // where the full 96-channel tip rack can't aspirate from - // every well - // if (orderedWells.length === 384) { - // const selectedWells: string[] = [] - - // const maxX = 12 // Number of wells in the X-axis - // const maxY = 8 // Number of wells in the Y-axis - // const maxCount = maxX * maxY - - // // Split the starting well into row and column parts - // const startingRow = topWellName.charAt(0) - // const startingCol = parseInt(topWellName.substring(1), 10) - - // let currentRow = startingRow - // let currentCol = startingCol - - // for (let y = 0; y < maxY; y++) { - // for (let x = 0; x < maxX; x++) { - // // Ensure the currentRow and currentCol are within the bounds of your array - // if (currentRow.charCodeAt(0) <= 'P'.charCodeAt(0) && currentCol <= 24) { - // // Get the current well - // const well = currentRow + currentCol.toString() - // selectedWells.push(well) - // // Move to the next well in the X-axis - // currentCol += 2 - // } - // } - - // // Move to the next row in the Y-axis and reset the X-axis - // currentRow = String.fromCharCode(currentRow.charCodeAt(0) + 1) - // currentCol = startingCol - // } - - // ninetySixChannelWells = selectedWells - // } + let ninetySixChannelWells = orderedWells + // special casing 384 well plates to be every other well + // both on the x and y ases. + if (orderedWells.length === 384) { + ninetySixChannelWells = get96Channel384WellPlateWells( + orderedWells, + topWellName + ) + } - // console.log('wellsAccessed', wellsAccessed) - // console.log('orderedWells', orderedWells) - return channels === 8 ? wellsAccessed : orderedWells + return channels === 8 ? wellsAccessed : ninetySixChannelWells } diff --git a/shared-data/js/helpers/index.ts b/shared-data/js/helpers/index.ts index 24c4dcf9961..7c6cebc3847 100644 --- a/shared-data/js/helpers/index.ts +++ b/shared-data/js/helpers/index.ts @@ -17,6 +17,7 @@ export { getWellNamePerMultiTip } from './getWellNamePerMultiTip' export { getWellTotalVolume } from './getWellTotalVolume' export { wellIsRect } from './wellIsRect' export { orderWells } from './orderWells' +export { get96Channel384WellPlateWells } from './get96Channel384WellPlateWells' export * from './parseProtocolData' export * from './volume' diff --git a/shared-data/js/helpers/orderWells.ts b/shared-data/js/helpers/orderWells.ts index a4b2e7400e0..70877b266fa 100644 --- a/shared-data/js/helpers/orderWells.ts +++ b/shared-data/js/helpers/orderWells.ts @@ -3,7 +3,7 @@ import uniq from 'lodash/uniq' import compact from 'lodash/compact' import flatten from 'lodash/flatten' -type WellOrderOption = 'l2r' | 'r2l' | 't2b' | 'b2t' +export type WellOrderOption = 'l2r' | 'r2l' | 't2b' | 'b2t' // labware definitions in shared-data have an ordering // attribute which is an Array of Arrays of wells. Each inner diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index 3c0adb30ae1..f01b104ebc4 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -11,10 +11,10 @@ // A 384 plate has 48 well sets, 2 for each column b/c it has staggered columns. // // If a labware has no possible well sets, then it is not compatible with multi-channel pipettes. -import { getLabwareDefURI } from '.' import uniq from 'lodash/uniq' import { getWellNamePerMultiTip } from './getWellNamePerMultiTip' +import { get96Channel384WellPlateWells, getLabwareDefURI, orderWells } from '.' import type { LabwareDefinition2, PipetteNameSpecs } from '../types' type WellSetByPrimaryWell = string[][] @@ -29,7 +29,6 @@ function _getAllWellSetsForLabware( return allWells.reduce( (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { const wellSet = getWellNamePerMultiTip(labwareDef, well, channels) - if (wellSet === null) { return acc } else if (channels === 8) { @@ -101,15 +100,33 @@ export const makeWellSetHelpers = (): WellSetHelpers => { * Ie: C2 for 96-flat => ['A2', 'B2', 'C2', ... 'H2'] * Or A1 for trough => ['A1', 'A1', 'A1', ...] **/ - const allWellSets = getAllWellSetsForLabware(labwareDef, channels) - const allWells: string[] = allWellSets.reduce( - (acc, wells) => acc.concat(wells), - [] + const allWellSetsFor8Channel = getAllWellSetsForLabware( + labwareDef, + channels + ) + /** getting all wells from the plate and turning into 1D array for 96-channel + */ + const orderedWellsFor96Channel = orderWells( + labwareDef.ordering, + 't2b', + 'l2r' ) + let ninetySixChannelWells = orderedWellsFor96Channel + /** special casing 384 well plates to be every other well + * both on the x and y ases. + */ + if (orderedWellsFor96Channel.length === 384) { + ninetySixChannelWells = get96Channel384WellPlateWells( + orderedWellsFor96Channel, + well + ) + } return channels === 8 - ? allWellSets.find((wellSet: string[]) => wellSet.includes(well)) - : allWells + ? allWellSetsFor8Channel.find((wellSet: string[]) => + wellSet.includes(well) + ) + : ninetySixChannelWells } const canPipetteUseLabware = ( diff --git a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json index 591b290121d..6c1f8cc0689 100644 --- a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json +++ b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json @@ -110,5 +110,49 @@ "channels": 1, "minVolume": 100, "maxVolume": 1000 + }, + "p1000_96": { + "displayName": "Flex 96-Channel 1000 μL", + "displayCategory": "GEN1", + "defaultAspirateFlowRate": { + "value": 7.85, + "min": 3, + "max": 812, + "valuesByApiLevel": { + "2.0": 7.85 + } + }, + "defaultDispenseFlowRate": { + "value": 7.85, + "min": 3, + "max": 812, + "valuesByApiLevel": { + "2.0": 7.85 + } + }, + "defaultBlowOutFlowRate": { + "value": 7.85, + "min": 3, + "max": 812, + "valuesByApiLevel": { + "2.0": 7.85 + } + }, + "channels": 96, + "minVolume": 5, + "maxVolume": 1000, + "smoothieConfigs": { + "stepsPerMM": 2133.33, + "homePosition": 230.15, + "travelDistance": 80 + }, + "defaultTipracks": [ + "opentrons/opentrons_flex_96_tiprack_1000ul/1", + "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", + "opentrons/opentrons_flex_96_tiprack_200ul/1", + "opentrons/opentrons_flex_96_filtertiprack_200ul/1", + "opentrons/opentrons_flex_96_tiprack_50ul/1", + "opentrons/opentrons_flex_96_filtertiprack_50ul/1" + ] } } diff --git a/step-generation/src/__tests__/utils.test.ts b/step-generation/src/__tests__/utils.test.ts index 1807b1c1b8b..34453d25c3d 100644 --- a/step-generation/src/__tests__/utils.test.ts +++ b/step-generation/src/__tests__/utils.test.ts @@ -9,7 +9,6 @@ import { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM, HEATERSHAKER_MODULE_TYPE, PipetteNameSpecs, - orderWells, } from '@opentrons/shared-data' import { fixtureP10Single, @@ -47,7 +46,6 @@ import type { LabwareEntity, ThermocyclerModuleState, ThermocyclerStateStepArgs, - WellOrderOption, } from '../types' import { getIsHeaterShakerNorthSouthOfNonTiprackWithMultiChannelPipette } from '../utils/heaterShakerCollision' @@ -793,91 +791,6 @@ describe('getLocationTotalVolume', () => { }) }) -describe('orderWells', () => { - const orderTuples: Array<[WellOrderOption, WellOrderOption]> = [ - ['t2b', 'l2r'], - ['t2b', 'r2l'], - ['b2t', 'l2r'], - ['b2t', 'r2l'], - ['l2r', 't2b'], - ['l2r', 'b2t'], - ['r2l', 't2b'], - ['r2l', 'b2t'], - ] - - describe('regular labware', () => { - const regularOrdering = [ - ['A1', 'B1'], - ['A2', 'B2'], - ] - const regularAnswerMap: Record< - WellOrderOption, - Partial> - > = { - t2b: { - l2r: ['A1', 'B1', 'A2', 'B2'], - r2l: ['A2', 'B2', 'A1', 'B1'], - }, - b2t: { - l2r: ['B1', 'A1', 'B2', 'A2'], - r2l: ['B2', 'A2', 'B1', 'A1'], - }, - l2r: { - t2b: ['A1', 'A2', 'B1', 'B2'], - b2t: ['B1', 'B2', 'A1', 'A2'], - }, - r2l: { - t2b: ['A2', 'A1', 'B2', 'B1'], - b2t: ['B2', 'B1', 'A2', 'A1'], - }, - } - orderTuples.forEach(tuple => { - it(`first ${tuple[0]} then ${tuple[1]}`, () => { - expect(orderWells(regularOrdering, ...tuple)).toEqual( - regularAnswerMap[tuple[0]][tuple[1]] - ) - }) - }) - }) - - describe('irregular labware', () => { - const irregularOrdering = [ - ['A1', 'B1'], - ['A2', 'B2', 'C2'], - ['A3'], - ['A4', 'B4', 'C4', 'D4'], - ] - const irregularAnswerMap: Record< - WellOrderOption, - Partial> - > = { - t2b: { - l2r: ['A1', 'B1', 'A2', 'B2', 'C2', 'A3', 'A4', 'B4', 'C4', 'D4'], - r2l: ['A4', 'B4', 'C4', 'D4', 'A3', 'A2', 'B2', 'C2', 'A1', 'B1'], - }, - b2t: { - l2r: ['B1', 'A1', 'C2', 'B2', 'A2', 'A3', 'D4', 'C4', 'B4', 'A4'], - r2l: ['D4', 'C4', 'B4', 'A4', 'A3', 'C2', 'B2', 'A2', 'B1', 'A1'], - }, - l2r: { - t2b: ['A1', 'A2', 'A3', 'A4', 'B1', 'B2', 'B4', 'C2', 'C4', 'D4'], - b2t: ['D4', 'C2', 'C4', 'B1', 'B2', 'B4', 'A1', 'A2', 'A3', 'A4'], - }, - r2l: { - t2b: ['A4', 'A3', 'A2', 'A1', 'B4', 'B2', 'B1', 'C4', 'C2', 'D4'], - b2t: ['D4', 'C4', 'C2', 'B4', 'B2', 'B1', 'A4', 'A3', 'A2', 'A1'], - }, - } - - orderTuples.forEach(tuple => { - it(`first ${tuple[0]} then ${tuple[1]}`, () => { - expect(orderWells(irregularOrdering, ...tuple)).toEqual( - irregularAnswerMap[tuple[0]][tuple[1]] - ) - }) - }) - }) -}) describe('getIsTallLabwareEastWestOfHeaterShaker', () => { const fakeLabwareDef: any = {} let labwareEntities: LabwareEntities diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 62577f26388..43b6b8e4833 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -566,6 +566,3 @@ export interface RobotStateAndWarnings { robotState: RobotState warnings: CommandCreatorWarning[] } - -// Copied from PD -export type WellOrderOption = 'l2r' | 'r2l' | 't2b' | 'b2t' From 634a2af011686df540c251383f55d8957f0e0079 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 25 Sep 2023 10:22:28 -0400 Subject: [PATCH 05/19] extend logic for custom labware --- .../src/components/DeckSetup/index.tsx | 5 +-- .../components/LabwareSelectionModal/index.ts | 16 ++++--- .../CreateFileWizard/PipetteTipsTile.tsx | 11 ++++- .../FilePipettesModal/PipetteFields.tsx | 9 +++- protocol-designer/src/labware-defs/actions.ts | 43 ++++++++++++------- .../dependentFieldsUpdateMix.ts | 25 ++++++++--- .../dependentFieldsUpdateMoveLiquid.ts | 16 +++++-- .../formLevel/handleFormChange/utils.ts | 5 ++- protocol-designer/src/utils/index.ts | 5 +++ shared-data/pipette/fixtures/name/index.ts | 2 +- .../name/pipetteNameSpecFixtures.json | 34 ++------------- 11 files changed, 99 insertions(+), 72 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index 4455ea75299..8573c3b1f37 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -60,6 +60,7 @@ import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { TerminalItemId } from '../../steplist' import { getSelectedTerminalItemId } from '../../ui/steps' +import { getHas96Channel } from '../../utils' import { getRobotType } from '../../file-data/selectors' import { BrowseLabwareModal } from '../labware' import { SlotWarning } from './SlotWarning' @@ -111,9 +112,7 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { trashSlot, } = props const pipettes = activeDeckSetup.pipettes - const has96Channel = Object.values(pipettes).some( - pip => pip.name === 'p1000_96' - ) + const has96Channel = getHas96Channel(pipettes) // NOTE: handling module<>labware compat when moving labware to empty module // is handled by SlotControls. // But when swapping labware when at least one is on a module, we need to be aware diff --git a/protocol-designer/src/components/LabwareSelectionModal/index.ts b/protocol-designer/src/components/LabwareSelectionModal/index.ts index 52b750be470..3f591de60b9 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/index.ts +++ b/protocol-designer/src/components/LabwareSelectionModal/index.ts @@ -17,9 +17,10 @@ import { selectors as labwareDefSelectors, } from '../../labware-defs' import { selectors as stepFormSelectors, ModuleOnDeck } from '../../step-forms' -import { BaseState, ThunkDispatch } from '../../types' +import { getHas96Channel } from '../../utils' import { getPipetteEntities } from '../../step-forms/selectors' import { adapter96ChannelDefUri } from '../modals/CreateFileWizard' +import type { BaseState, ThunkDispatch } from '../../types' interface SP { customLabwareDefs: LabwareSelectionModalProps['customLabwareDefs'] slot: LabwareSelectionModalProps['slot'] @@ -35,9 +36,7 @@ interface SP { function mapStateToProps(state: BaseState): SP { const slot = labwareIngredSelectors.selectedAddLabwareSlot(state) || null const pipettes = getPipetteEntities(state) - const has96Channel = Object.values(pipettes).some( - pip => pip.name === 'p1000_96' - ) + const has96Channel = getHas96Channel(pipettes) // TODO: Ian 2019-10-29 needs revisit to support multiple manualIntervention steps const modulesById = stepFormSelectors.getInitialDeckSetup(state).modules @@ -86,10 +85,13 @@ function mergeProps( dispatch(closeLabwareSelector()) }, onUploadLabware: fileChangeEvent => - dispatch(labwareDefActions.createCustomLabwareDef(fileChangeEvent)), + dispatch( + labwareDefActions.createCustomLabwareDef( + fileChangeEvent, + stateProps.has96Channel + ) + ), selectLabware: labwareDefURI => { - console.log(labwareDefURI) - console.log(stateProps.permittedTipracks) if (stateProps.slot) { dispatch( createContainer({ diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx index 5e49aa18b3b..ae119d476b6 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx @@ -23,7 +23,7 @@ import { Btn, JUSTIFY_END, } from '@opentrons/components' -import { getPipetteNameSpecs } from '@opentrons/shared-data' +import { getPipetteNameSpecs, LEFT } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { getLabwareDefsByURI } from '../../../labware-defs/selectors' import { createCustomTiprackDef } from '../../../labware-defs/actions' @@ -237,7 +237,14 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null { dispatch(createCustomTiprackDef(e))} + onChange={e => + dispatch( + createCustomTiprackDef( + e, + values.pipettesByMount[LEFT].pipetteName === 'p1000_96' + ) + ) + } /> diff --git a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx index 917253fb288..166675dcd9c 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx @@ -232,7 +232,14 @@ export function PipetteFields(props: Props): JSX.Element { {i18n.t('button.upload_custom_tip_rack')} dispatch(createCustomTiprackDef(e))} + onChange={e => + dispatch( + createCustomTiprackDef( + e, + values.left.pipetteName === 'p1000_96' + ) + ) + } /> diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index 6326b30170c..9df5e129dc7 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -13,8 +13,8 @@ import { } from '@opentrons/shared-data' import * as labwareDefSelectors from './selectors' import { getAllWellSetsForLabware } from '../utils' -import { ThunkAction } from '../types' -import { LabwareUploadMessage } from './types' +import type { ThunkAction } from '../types' +import type { LabwareUploadMessage } from './types' export interface LabwareUploadMessageAction { type: 'LABWARE_UPLOAD_MESSAGE' payload: LabwareUploadMessage @@ -75,30 +75,32 @@ const _labwareDefsMatchingDisplayName = ( const getIsOverwriteMismatched = ( newDef: LabwareDefinition2, - overwrittenDef: LabwareDefinition2 + overwrittenDef: LabwareDefinition2, + channel: 8 | 96 ): boolean => { const matchedWellOrdering = isEqual(newDef.ordering, overwrittenDef.ordering) const matchedMultiUse = matchedWellOrdering && isEqual( - getAllWellSetsForLabware(newDef, 8), - getAllWellSetsForLabware(overwrittenDef, 8) + getAllWellSetsForLabware(newDef, channel), + getAllWellSetsForLabware(overwrittenDef, channel) ) return !(matchedWellOrdering && matchedMultiUse) } const _createCustomLabwareDef: ( - onlyTiprack: boolean -) => ( - event: React.SyntheticEvent -) => ThunkAction = onlyTiprack => event => (dispatch, getState) => { + onlyTiprack: boolean, + has96Channel: boolean +) => (event: React.SyntheticEvent) => ThunkAction = ( + onlyTiprack, + has96Channel +) => event => (dispatch, getState) => { const allLabwareDefs: LabwareDefinition2[] = values( labwareDefSelectors.getLabwareDefsByURI(getState()) ) const customLabwareDefs: LabwareDefinition2[] = values( labwareDefSelectors.getCustomLabwareDefsByURI(getState()) ) - // @ts-expect-error(sa, 2021-6-20): null check const file = event.currentTarget.files[0] const reader = new FileReader() @@ -199,7 +201,8 @@ const _createCustomLabwareDef: ( isOverwriteMismatched: getIsOverwriteMismatched( // @ts-expect-error(sa, 2021-6-20): parsedLabwareDef might be nullsy parsedLabwareDef, - matchingDefs[0] + matchingDefs[0], + has96Channel ? 96 : 8 ), }) ) @@ -242,11 +245,21 @@ const _createCustomLabwareDef: ( } export const createCustomLabwareDef: ( - event: React.SyntheticEvent -) => ThunkAction = _createCustomLabwareDef(false) + event: React.SyntheticEvent, + has96Channel: boolean +) => (event: React.SyntheticEvent) => ThunkAction = ( + event, + has96Channel +) => _createCustomLabwareDef(false, has96Channel) + export const createCustomTiprackDef: ( - event: React.SyntheticEvent -) => ThunkAction = _createCustomLabwareDef(true) + event: React.SyntheticEvent, + has96Channel: boolean +) => (event: React.SyntheticEvent) => ThunkAction = ( + event, + has96Channel +) => _createCustomLabwareDef(true, has96Channel) + interface DismissLabwareUploadMessage { type: 'DISMISS_LABWARE_UPLOAD_MESSAGE' } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts index 1bb83b47b93..4291f9b5a0a 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMix.ts @@ -7,9 +7,12 @@ import { getAllWellsFromPrimaryWells, } from './utils' import { getDefaultsForStepType } from '../getDefaultsForStepType' -import { LabwareEntities, PipetteEntities } from '@opentrons/step-generation' -import { FormData, StepFieldName } from '../../../form-types' -import { FormPatch } from '../../actions/types' +import type { + LabwareEntities, + PipetteEntities, +} from '@opentrons/step-generation' +import type { FormData, StepFieldName } from '../../../form-types' +import type { FormPatch } from '../../actions/types' // TODO: Ian 2019-02-21 import this from a more central place - see #2926 const getDefaultFields = (...fields: StepFieldName[]): FormPatch => @@ -53,8 +56,10 @@ const updatePatchOnPipetteChannelChange = ( ? getChannels(patch.pipette, pipetteEntities) : null const appliedPatch = { ...rawForm, ...patch } - const singleToMulti = prevChannels === 1 && nextChannels === 8 - const multiToSingle = prevChannels === 8 && nextChannels === 1 + const singleToMulti = + prevChannels === 1 && (nextChannels === 8 || nextChannels === 96) + const multiToSingle = + (prevChannels === 8 || prevChannels === 96) && nextChannels === 1 if (patch.pipette === null || singleToMulti) { // reset all well selection @@ -68,11 +73,19 @@ const updatePatchOnPipetteChannelChange = ( }), } } else if (multiToSingle) { + let channels: 8 | 96 = 8 + if (prevChannels === 96) { + channels = 96 + } // multi-channel to single-channel: convert primary wells to all wells const labwareId = appliedPatch.labware const labwareDef = labwareEntities[labwareId].def update = { - wells: getAllWellsFromPrimaryWells(appliedPatch.wells, labwareDef), + wells: getAllWellsFromPrimaryWells( + appliedPatch.wells, + labwareDef, + channels + ), } } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts index bad8da93da3..29730634925 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/dependentFieldsUpdateMoveLiquid.ts @@ -451,8 +451,10 @@ const updatePatchOnPipetteChannelChange = ( : null const { id, stepType, ...stepData } = rawForm const appliedPatch = { ...(stepData as FormPatch), ...patch, id, stepType } - const singleToMulti = prevChannels === 1 && nextChannels === 8 - const multiToSingle = prevChannels === 8 && nextChannels === 1 + const singleToMulti = + prevChannels === 1 && (nextChannels === 8 || nextChannels === 96) + const multiToSingle = + (prevChannels === 8 || prevChannels === 96) && nextChannels === 1 if (patch.pipette === null || singleToMulti) { // reset all well selection @@ -475,6 +477,10 @@ const updatePatchOnPipetteChannelChange = ( }), } } else if (multiToSingle) { + let channels = 8 + if (prevChannels === 96) { + channels = 96 + } // multi-channel to single-channel: convert primary wells to all wells // @ts-expect-error(sa, 2021-06-14): appliedPatch.aspirate_labware is type ?unknown. Address in #3161 const sourceLabwareId: string = appliedPatch.aspirate_labware @@ -488,12 +494,14 @@ const updatePatchOnPipetteChannelChange = ( aspirate_wells: getAllWellsFromPrimaryWells( // @ts-expect-error(sa, 2021-06-14): appliedPatch.aspirate_wells is type ?unknown. Address in #3161 appliedPatch.aspirate_wells, // @ts-expect-error(sa, 2021-06-14): sourceLabwareDef is not typed properly. Address in #3161 - sourceLabwareDef + sourceLabwareDef, + channels ), dispense_wells: getAllWellsFromPrimaryWells( // @ts-expect-error(sa, 2021-06-14): appliedPatch.dispense_wells is type ?unknown. Address in #3161 appliedPatch.dispense_wells, // @ts-expect-error(sa, 2021-06-14): destLabwareDef is not typed properly. Address in #3161 - destLabwareDef + destLabwareDef, + channels ), } } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts index bb3d8c61e52..be4c393775a 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts @@ -19,10 +19,11 @@ export function chainPatchUpdaters( // included in that set. Used to convert multi to single. export function getAllWellsFromPrimaryWells( primaryWells: string[], - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + channels: 8 | 96 ): string[] { const allWells = primaryWells.reduce((acc: string[], well: string) => { - const nextWellSet = getWellSetForMultichannel(labwareDef, well, 8) + const nextWellSet = getWellSetForMultichannel(labwareDef, well, channels) // filter out any nulls (but you shouldn't get any) if (!nextWellSet) { diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 09cef6a9143..9e50c5e2fec 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -6,6 +6,7 @@ import { BoundingRect, GenericRect } from '../collision-types' import type { AdditionalEquipmentEntity, LabwareEntities, + PipetteEntities, } from '@opentrons/step-generation' export const registerSelectors: (arg0: any) => void = @@ -127,3 +128,7 @@ export const getStagingAreaSlots = ( // we can assume that the location is always a string return stagingAreas.map(area => area.location as string) } + +export const getHas96Channel = (pipettes: PipetteEntities): boolean => { + return Object.values(pipettes).some(pip => pip.name === 'p1000_96') +} diff --git a/shared-data/pipette/fixtures/name/index.ts b/shared-data/pipette/fixtures/name/index.ts index 19272e7adb0..1625636981e 100644 --- a/shared-data/pipette/fixtures/name/index.ts +++ b/shared-data/pipette/fixtures/name/index.ts @@ -1,5 +1,5 @@ import _pipetteNameSpecFixtures from './pipetteNameSpecFixtures.json' -import type { PipetteName, PipetteNameSpecs } from '@opentrons/shared-data' +import type { PipetteName, PipetteNameSpecs } from '../../../js' const pipetteNameSpecFixtures = _pipetteNameSpecFixtures as Record< PipetteName, diff --git a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json index 6c1f8cc0689..ad39d6a2a3d 100644 --- a/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json +++ b/shared-data/pipette/fixtures/name/pipetteNameSpecFixtures.json @@ -113,46 +113,18 @@ }, "p1000_96": { "displayName": "Flex 96-Channel 1000 μL", - "displayCategory": "GEN1", "defaultAspirateFlowRate": { "value": 7.85, "min": 3, - "max": 812, - "valuesByApiLevel": { - "2.0": 7.85 - } + "max": 812 }, "defaultDispenseFlowRate": { "value": 7.85, "min": 3, - "max": 812, - "valuesByApiLevel": { - "2.0": 7.85 - } - }, - "defaultBlowOutFlowRate": { - "value": 7.85, - "min": 3, - "max": 812, - "valuesByApiLevel": { - "2.0": 7.85 - } + "max": 812 }, "channels": 96, "minVolume": 5, - "maxVolume": 1000, - "smoothieConfigs": { - "stepsPerMM": 2133.33, - "homePosition": 230.15, - "travelDistance": 80 - }, - "defaultTipracks": [ - "opentrons/opentrons_flex_96_tiprack_1000ul/1", - "opentrons/opentrons_flex_96_filtertiprack_1000ul/1", - "opentrons/opentrons_flex_96_tiprack_200ul/1", - "opentrons/opentrons_flex_96_filtertiprack_200ul/1", - "opentrons/opentrons_flex_96_tiprack_50ul/1", - "opentrons/opentrons_flex_96_filtertiprack_50ul/1" - ] + "maxVolume": 1000 } } From edc8800ee2578638042d2e7395d3c578426baa34 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 25 Sep 2023 16:51:32 -0400 Subject: [PATCH 06/19] change text when selecting wells --- .../labware-creator/utils/determineMultiChannelSupport.ts | 2 ++ .../fields/WellSelectionField/WellSelectionInput.tsx | 4 ++-- .../StepEditForm/fields/WellSelectionField/index.ts | 8 ++++---- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts b/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts index 7de8b96f4a2..46b7800494c 100644 --- a/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts +++ b/labware-library/src/labware-creator/utils/determineMultiChannelSupport.ts @@ -15,6 +15,8 @@ export const determineMultiChannelSupport = ( // allow multichannel pipette options only if // all 8 channels fit into the first column correctly + // TODO(Jr, 9/25/23): support 96-channel in labware creator then plug in + // channels below in getWellNamePerMultiTip const multiChannelTipsFirstColumn = def !== null ? getWellNamePerMultiTip(def, 'A1', 8) : null diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx index 6d2bc74c57f..5e7c4513c5d 100644 --- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx +++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/WellSelectionInput.tsx @@ -28,7 +28,7 @@ export interface DP { export type OP = FieldProps & { primaryWellCount?: number - isMulti?: boolean | null + is8Channel?: boolean | null pipetteId?: string | null labwareId?: string | null } @@ -64,7 +64,7 @@ export class WellSelectionInputComponent extends React.Component { render(): JSX.Element { const modalKey = this.getModalKey() - const label = this.props.isMulti + const label = this.props.is8Channel ? i18n.t('form.step_edit_form.wellSelectionLabel.columns') : i18n.t('form.step_edit_form.wellSelectionLabel.wells') return ( diff --git a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts index 134b30aae81..5bad7c89f74 100644 --- a/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts +++ b/protocol-designer/src/components/StepEditForm/fields/WellSelectionField/index.ts @@ -20,7 +20,7 @@ type OP = FieldProps & { pipetteId?: string | null } interface SP { - isMulti: Props['isMulti'] + is8Channel: Props['is8Channel'] primaryWellCount: Props['primaryWellCount'] } @@ -29,12 +29,12 @@ const mapStateToProps = (state: BaseState, ownProps: OP): SP => { const selectedWells = ownProps.value const pipette = pipetteId && stepFormSelectors.getPipetteEntities(state)[pipetteId] - const isMulti = pipette ? pipette.spec.channels > 1 : false + const is8Channel = pipette ? pipette.spec.channels === 8 : false return { primaryWellCount: Array.isArray(selectedWells) ? selectedWells.length : undefined, - isMulti, + is8Channel, } } @@ -53,7 +53,7 @@ function mergeProps(stateProps: SP, _dispatchProps: null, ownProps: OP): Props { return { disabled, errorToShow, - isMulti: stateProps.isMulti, + is8Channel: stateProps.is8Channel, labwareId, name, onFieldBlur, From 33e4110fd4fae9f7d61cf19814e033fe612c8ea4 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 25 Sep 2023 17:08:27 -0400 Subject: [PATCH 07/19] fix lint --- .../DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx index 3828b542161..41b7d363e5b 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx @@ -129,7 +129,10 @@ const mapDispatchToProps = ( ): DP => ({ editLiquids: () => dispatch(openIngredientSelector(ownProps.labwareEntity.id)), - duplicateLabware: () => dispatch(duplicateLabware(ownProps.labwareEntity.id)), + duplicateLabware: () => + dispatch( + duplicateLabware({ templateLabwareId: ownProps.labwareEntity.id }) + ), deleteLabware: () => { window.confirm( `Are you sure you want to permanently delete this ${getLabwareDisplayName( From 5106e19dc9a4a1420b73f1ab1918c900af9b1c30 Mon Sep 17 00:00:00 2001 From: Jethary Date: Fri, 29 Sep 2023 13:21:46 -0400 Subject: [PATCH 08/19] add some logic for pipetting into 384 well plate with 50uL tip --- .../src/steplist/formLevel/errors.ts | 18 ++++++++++-- .../handleFormChange/test/mix.test.ts | 6 ++++ .../formLevel/handleFormChange/utils.ts | 5 +++- .../js/helpers/__tests__/wellSets.test.ts | 28 +++++++++++++++++-- shared-data/js/helpers/wellSets.ts | 28 ++++++++++++++++--- 5 files changed, 74 insertions(+), 11 deletions(-) diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index 1a017910a04..ef04b76ad1d 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -141,7 +141,11 @@ export const incompatibleLabware = ( ): FormError | null => { const { labware, pipette } = fields if (!labware || !pipette) return null - return !canPipetteUseLabware(pipette.spec, labware.def) + return !canPipetteUseLabware( + pipette.spec, + labware.def, + pipette.tiprackLabwareDef + ) ? INCOMPATIBLE_LABWARE : null } @@ -150,7 +154,11 @@ export const incompatibleDispenseLabware = ( ): FormError | null => { const { dispense_labware, pipette } = fields if (!dispense_labware || !pipette) return null - return !canPipetteUseLabware(pipette.spec, dispense_labware.def) + return !canPipetteUseLabware( + pipette.spec, + dispense_labware.def, + pipette.tiprackLabwareDef + ) ? INCOMPATIBLE_DISPENSE_LABWARE : null } @@ -159,7 +167,11 @@ export const incompatibleAspirateLabware = ( ): FormError | null => { const { aspirate_labware, pipette } = fields if (!aspirate_labware || !pipette) return null - return !canPipetteUseLabware(pipette.spec, aspirate_labware.def) + return !canPipetteUseLabware( + pipette.spec, + aspirate_labware.def, + pipette.tiprackLabwareDef + ) ? INCOMPATIBLE_ASPIRATE_LABWARE : null } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/test/mix.test.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/test/mix.test.ts index 0f3d5f268a0..c2c0f9b2d54 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/test/mix.test.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/test/mix.test.ts @@ -1,6 +1,8 @@ import { LabwareDefinition2 } from '@opentrons/shared-data' import _fixture_96_plate from '@opentrons/shared-data/labware/fixtures/2/fixture_96_plate.json' import _fixture_trash from '@opentrons/shared-data/labware/fixtures/2/fixture_trash.json' +import fixture_tiprack_10_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_10_ul.json' +import fixture_tiprack_300_ul from '@opentrons/shared-data/labware/fixtures/2/fixture_tiprack_300_ul.json' import { LabwareEntities, PipetteEntities } from '@opentrons/step-generation' import { DEFAULT_MM_FROM_BOTTOM_DISPENSE } from '../../../../constants' import { FormData } from '../../../../form-types' @@ -8,6 +10,8 @@ import { dependentFieldsUpdateMix } from '../dependentFieldsUpdateMix' const fixture96Plate = _fixture_96_plate as LabwareDefinition2 const fixtureTrash = _fixture_trash as LabwareDefinition2 +const fixtureTipRack10ul = fixture_tiprack_10_ul as LabwareDefinition2 +const fixtureTipRack300ul = fixture_tiprack_300_ul as LabwareDefinition2 let pipetteEntities: PipetteEntities let labwareEntities: LabwareEntities @@ -20,6 +24,7 @@ beforeEach(() => { spec: { channels: 1, }, + tiprackLabwareDef: fixtureTipRack300ul, }, pipetteMultiId: { name: 'p10_multi', @@ -27,6 +32,7 @@ beforeEach(() => { spec: { channels: 8, }, + tiprackLabwareDef: fixtureTipRack10ul, }, } as any labwareEntities = { diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts index be4c393775a..f531ad136b0 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts @@ -7,6 +7,7 @@ import { LabwareDefinition2, PipetteChannels } from '@opentrons/shared-data' import { LabwareEntities, PipetteEntities } from '@opentrons/step-generation' import { FormPatch } from '../../actions/types' import { FormData, PathOption, StepFieldName } from '../../../form-types' + export function chainPatchUpdaters( initialPatch: FormPatch, fns: Array<(arg0: FormPatch) => FormPatch> @@ -145,9 +146,11 @@ export function getDefaultWells(args: GetDefaultWellsArgs): string[] { ) return [] const labwareDef = labwareEntities[labwareId].def + const pipetteCanUseLabware = canPipetteUseLabware( pipetteEntities[pipetteId].spec, - labwareDef + labwareDef, + pipetteEntities[pipetteId].tiprackLabwareDef ) if (!pipetteCanUseLabware) return [] const isSingleWellLabware = diff --git a/shared-data/js/helpers/__tests__/wellSets.test.ts b/shared-data/js/helpers/__tests__/wellSets.test.ts index a0be4f4edde..475d6bf545f 100644 --- a/shared-data/js/helpers/__tests__/wellSets.test.ts +++ b/shared-data/js/helpers/__tests__/wellSets.test.ts @@ -2,6 +2,8 @@ import pipetteNameSpecsFixtures from '../../../pipette/fixtures/name/pipetteName import fixture_12_trough from '../../../labware/fixtures/2/fixture_12_trough.json' import fixture_96_plate from '../../../labware/fixtures/2/fixture_96_plate.json' import fixture_384_plate from '../../../labware/fixtures/2/fixture_384_plate.json' +import fixture_tiprack_10_ul from '../../../labware/fixtures/2/fixture_tiprack_10_ul.json' +import fixture_tiprack_300_ul from '../../../labware/fixtures/2/fixture_tiprack_300_ul.json' import fixture_overlappy_wellplate from '../../../labware/fixtures/2/fixture_overlappy_wellplate.json' import { makeWellSetHelpers } from '../wellSets' import { findWellAt } from '../getWellNamePerMultiTip' @@ -17,6 +19,8 @@ const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture96Plate = fixture_96_plate as LabwareDefinition2 const fixture384Plate = fixture_384_plate as LabwareDefinition2 const fixtureOverlappyWellplate = fixture_overlappy_wellplate as LabwareDefinition2 +const fixtureTipRack10ul = fixture_tiprack_10_ul as LabwareDefinition2 +const fixtureTipRack300ul = fixture_tiprack_300_ul as LabwareDefinition2 const EIGHT_CHANNEL = 8 const NINETY_SIX_CHANNEL = 96 const wellsForReservoir = [ @@ -201,15 +205,33 @@ describe('canPipetteUseLabware', () => { const pipette = fixtureP10Multi const pipette96 = fixtureP100096 - expect(canPipetteUseLabware(pipette, labwareDef)).toBe(false) - expect(canPipetteUseLabware(pipette96, labwareDef)).toBe(false) + expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack10ul)).toBe( + false + ) + expect( + canPipetteUseLabware(pipette96, labwareDef, fixtureTipRack10ul) + ).toBe(false) }) it('returns true when pipette is single channel', () => { const labwareDef = fixtureOverlappyWellplate const pipette = fixtureP10Single - expect(canPipetteUseLabware(pipette, labwareDef)).toBe(true) + expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack10ul)).toBe( + true + ) + }) + it('returns false when the tip volume is too high with the 384 well plate', () => { + const labwareDef = fixture384Plate + const pipette = fixtureP10Multi + const pipette96 = fixtureP100096 + + expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack300ul)).toBe( + false + ) + expect( + canPipetteUseLabware(pipette96, labwareDef, fixtureTipRack300ul) + ).toBe(false) }) }) diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index f01b104ebc4..0af496b75ed 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -14,7 +14,12 @@ import uniq from 'lodash/uniq' import { getWellNamePerMultiTip } from './getWellNamePerMultiTip' -import { get96Channel384WellPlateWells, getLabwareDefURI, orderWells } from '.' +import { + get96Channel384WellPlateWells, + getLabwareDefURI, + getTiprackVolume, + orderWells, +} from '.' import type { LabwareDefinition2, PipetteNameSpecs } from '../types' type WellSetByPrimaryWell = string[][] @@ -56,7 +61,8 @@ export interface WellSetHelpers { canPipetteUseLabware: ( pipetteSpec: PipetteNameSpecs, - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + tiprackDef: LabwareDefinition2 ) => boolean } @@ -131,10 +137,24 @@ export const makeWellSetHelpers = (): WellSetHelpers => { const canPipetteUseLabware = ( pipetteSpec: PipetteNameSpecs, - labwareDef: LabwareDefinition2 + labwareDef: LabwareDefinition2, + tiprackDef: LabwareDefinition2 ): boolean => { + // 384 well plates are only compatible with p50s or p20/p10s + const tiprackVolume = getTiprackVolume(tiprackDef) + const is384WellPlate = Object.keys(labwareDef.wells).length === 384 + if ( + (tiprackVolume === 200 || + tiprackVolume === 300 || + tiprackVolume === 1000) && + is384WellPlate + ) { + return false + } + if (pipetteSpec.channels === 1) { - // assume all labware can be used by single-channel + // assume all labware except for the 384 well plate with certain volumes + // can be used by single-channel return true } From e9bc5f5e6f7b35cd48fa9222026fd032c029b105 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 16 Oct 2023 10:23:42 -0400 Subject: [PATCH 09/19] add extra null protection for moveLabware onto a module --- step-generation/src/commandCreators/atomic/moveLabware.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step-generation/src/commandCreators/atomic/moveLabware.ts b/step-generation/src/commandCreators/atomic/moveLabware.ts index 467a216db9d..3c7c414d0e8 100644 --- a/step-generation/src/commandCreators/atomic/moveLabware.ts +++ b/step-generation/src/commandCreators/atomic/moveLabware.ts @@ -112,7 +112,7 @@ export const moveLabware: CommandCreator = ( if (destinationModuleId != null) { const destModuleState = - prevRobotState.modules[destinationModuleId].moduleState + prevRobotState.modules[destinationModuleId]?.moduleState ?? null if (destModuleState != null) { if ( destModuleState.type === THERMOCYCLER_MODULE_TYPE && From 402c459141994c626b898cc3939250bb8bea6241 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 16 Oct 2023 13:59:49 -0400 Subject: [PATCH 10/19] remove pipette and384 well restrictions, clean up adapter rendering logic --- .../DeckSetup/LabwareOverlays/EditLabware.tsx | 9 +-- .../LabwareOverlays/EditLabwareOffDeck.tsx | 5 +- .../LabwareOverlays/LabwareControls.tsx | 9 --- .../src/components/DeckSetup/index.tsx | 6 -- .../components/LabwareSelectionModal/index.ts | 5 -- .../src/labware-ingred/actions/thunks.ts | 62 +++++-------------- .../src/localization/en/alert.json | 4 ++ .../src/steplist/formLevel/errors.ts | 18 +----- .../formLevel/handleFormChange/utils.ts | 5 +- .../src/utils/labwareModuleCompatibility.ts | 2 +- .../js/helpers/__tests__/wellSets.test.ts | 24 ++----- shared-data/js/helpers/wellSets.ts | 28 ++------- .../src/commandCreators/atomic/replaceTip.ts | 4 ++ step-generation/src/errorCreators.ts | 7 +++ step-generation/src/types.ts | 1 + 15 files changed, 46 insertions(+), 143 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx index ffd09d85a1c..7bdf7570efb 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabware.tsx @@ -31,7 +31,6 @@ interface OP { setHoveredLabware: (val?: LabwareOnDeck | null) => unknown setDraggedLabware: (val?: LabwareOnDeck | null) => unknown swapBlocked: boolean - adapterId?: string } interface SP { isYetUnnamed: boolean @@ -210,13 +209,7 @@ const mapDispatchToProps = ( ): DP => ({ editLiquids: () => dispatch(openIngredientSelector(ownProps.labwareOnDeck.id)), - duplicateLabware: () => - dispatch( - duplicateLabware({ - templateLabwareId: ownProps.labwareOnDeck.id, - templateAdapterId: ownProps.adapterId, - }) - ), + duplicateLabware: () => dispatch(duplicateLabware(ownProps.labwareOnDeck.id)), deleteLabware: () => { window.confirm( `Are you sure you want to permanently delete this ${getLabwareDisplayName( diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx index 41b7d363e5b..3828b542161 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/EditLabwareOffDeck.tsx @@ -129,10 +129,7 @@ const mapDispatchToProps = ( ): DP => ({ editLiquids: () => dispatch(openIngredientSelector(ownProps.labwareEntity.id)), - duplicateLabware: () => - dispatch( - duplicateLabware({ templateLabwareId: ownProps.labwareEntity.id }) - ), + duplicateLabware: () => dispatch(duplicateLabware(ownProps.labwareEntity.id)), deleteLabware: () => { window.confirm( `Are you sure you want to permanently delete this ${getLabwareDisplayName( diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx index 83622a529d3..05c5a585b8f 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/LabwareControls.tsx @@ -13,8 +13,6 @@ import { LabwareHighlight } from './LabwareHighlight' import styles from './LabwareOverlays.css' interface LabwareControlsProps { - allLabware: LabwareOnDeck[] - has96Channel: boolean labwareOnDeck: LabwareOnDeck slot: DeckSlot setHoveredLabware: (labware?: LabwareOnDeck | null) => unknown @@ -31,18 +29,12 @@ export const LabwareControls = (props: LabwareControlsProps): JSX.Element => { setHoveredLabware, setDraggedLabware, swapBlocked, - allLabware, - has96Channel, } = props const canEdit = selectedTerminalItemId === START_TERMINAL_ITEM_ID const [x, y] = slot.position const width = labwareOnDeck.def.dimensions.xDimension const height = labwareOnDeck.def.dimensions.yDimension - const isTiprack = labwareOnDeck.def.parameters.isTiprack - const adapterId = Object.values(allLabware).find( - adapter => adapter.id === labwareOnDeck.slot - )?.id return ( <> { {canEdit ? ( // @ts-expect-error(sa, 2021-6-21): react dnd type mismatch { /> ) : ( { ) : ( { /> ThunkAction = args => (dispatch, getState) => { - const { templateLabwareId, templateAdapterId } = args + templateLabwareId: string +) => ThunkAction = templateLabwareId => ( + dispatch, + getState +) => { const state = getState() const robotType = state.fileData.robotType const templateLabwareDefURI = stepFormSelectors.getLabwareEntities(state)[ templateLabwareId ].labwareDefURI - const tempAdapterEntity = - templateAdapterId != null - ? stepFormSelectors.getLabwareEntities(state)[templateAdapterId] - : null - const templateAdapterDefURI = tempAdapterEntity?.labwareDefURI ?? null - const templateAdapterDisplayName = - tempAdapterEntity?.def.metadata.displayName ?? null - assert( templateLabwareDefURI, `no labwareDefURI for labware ${templateLabwareId}, cannot run duplicateLabware thunk` ) - const initialDeckSetup = stepFormSelectors.getInitialDeckSetup(state) const templateLabwareIdIsOffDeck = initialDeckSetup.labware[templateLabwareId].slot === 'offDeck' @@ -145,37 +134,14 @@ export const duplicateLabware: ( ) if (templateLabwareDefURI && duplicateSlot) { - if (templateAdapterDefURI != null && templateAdapterId != null) { - const adapterDuplicateId = uuid() + ':' + templateAdapterDefURI - dispatch({ - type: 'DUPLICATE_LABWARE', - payload: { - // you can't set a nick name for adapters - duplicateLabwareNickname: templateAdapterDisplayName ?? '', - templateLabwareId: templateAdapterId, - duplicateLabwareId: adapterDuplicateId, - slot: duplicateSlot, - }, - }) - dispatch({ - type: 'DUPLICATE_LABWARE', - payload: { - duplicateLabwareNickname, - templateLabwareId, - duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, - slot: adapterDuplicateId, - }, - }) - } else { - dispatch({ - type: 'DUPLICATE_LABWARE', - payload: { - duplicateLabwareNickname, - templateLabwareId, - duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, - slot: templateLabwareIdIsOffDeck ? 'offDeck' : duplicateSlot, - }, - }) - } + dispatch({ + type: 'DUPLICATE_LABWARE', + payload: { + duplicateLabwareNickname, + templateLabwareId, + duplicateLabwareId: uuid() + ':' + templateLabwareDefURI, + slot: templateLabwareIdIsOffDeck ? 'offDeck' : duplicateSlot, + }, + }) } } diff --git a/protocol-designer/src/localization/en/alert.json b/protocol-designer/src/localization/en/alert.json index 3de66e80e2d..95ec5fb2f81 100644 --- a/protocol-designer/src/localization/en/alert.json +++ b/protocol-designer/src/localization/en/alert.json @@ -173,6 +173,10 @@ "TIPRACK_IN_WASTE_CHUTE_HAS_TIPS": { "title": "Moving tiprack into waste chute", "body": "This tiprack has remaining tips, be advised that once you dispose of it, there is no way to get it back later in the protocol. " + }, + "MISSING_96_CHANNEL_TIPRACK_ADAPTER": { + "title": "Missing tiprack adapter for 96-channel", + "body": "A 96-channel cannot pick up a full tiprack without an adapter underneath" } } }, diff --git a/protocol-designer/src/steplist/formLevel/errors.ts b/protocol-designer/src/steplist/formLevel/errors.ts index ef04b76ad1d..1a017910a04 100644 --- a/protocol-designer/src/steplist/formLevel/errors.ts +++ b/protocol-designer/src/steplist/formLevel/errors.ts @@ -141,11 +141,7 @@ export const incompatibleLabware = ( ): FormError | null => { const { labware, pipette } = fields if (!labware || !pipette) return null - return !canPipetteUseLabware( - pipette.spec, - labware.def, - pipette.tiprackLabwareDef - ) + return !canPipetteUseLabware(pipette.spec, labware.def) ? INCOMPATIBLE_LABWARE : null } @@ -154,11 +150,7 @@ export const incompatibleDispenseLabware = ( ): FormError | null => { const { dispense_labware, pipette } = fields if (!dispense_labware || !pipette) return null - return !canPipetteUseLabware( - pipette.spec, - dispense_labware.def, - pipette.tiprackLabwareDef - ) + return !canPipetteUseLabware(pipette.spec, dispense_labware.def) ? INCOMPATIBLE_DISPENSE_LABWARE : null } @@ -167,11 +159,7 @@ export const incompatibleAspirateLabware = ( ): FormError | null => { const { aspirate_labware, pipette } = fields if (!aspirate_labware || !pipette) return null - return !canPipetteUseLabware( - pipette.spec, - aspirate_labware.def, - pipette.tiprackLabwareDef - ) + return !canPipetteUseLabware(pipette.spec, aspirate_labware.def) ? INCOMPATIBLE_ASPIRATE_LABWARE : null } diff --git a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts index f531ad136b0..be4c393775a 100644 --- a/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts +++ b/protocol-designer/src/steplist/formLevel/handleFormChange/utils.ts @@ -7,7 +7,6 @@ import { LabwareDefinition2, PipetteChannels } from '@opentrons/shared-data' import { LabwareEntities, PipetteEntities } from '@opentrons/step-generation' import { FormPatch } from '../../actions/types' import { FormData, PathOption, StepFieldName } from '../../../form-types' - export function chainPatchUpdaters( initialPatch: FormPatch, fns: Array<(arg0: FormPatch) => FormPatch> @@ -146,11 +145,9 @@ export function getDefaultWells(args: GetDefaultWellsArgs): string[] { ) return [] const labwareDef = labwareEntities[labwareId].def - const pipetteCanUseLabware = canPipetteUseLabware( pipetteEntities[pipetteId].spec, - labwareDef, - pipetteEntities[pipetteId].tiprackLabwareDef + labwareDef ) if (!pipetteCanUseLabware) return [] const isSingleWellLabware = diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 4eb8cab6618..05986f9f9b5 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -133,7 +133,7 @@ export const getLabwareCompatibleWithAdapter = ( permittedTipracks: string[], adapterLoadName?: string ): string[] => { - if (permittedTipracks.length > 0) { + if (permittedTipracks.length > 0 && adapterLoadName === ADAPTER_96_CHANNEL) { return permittedTipracks } else { return adapterLoadName != null diff --git a/shared-data/js/helpers/__tests__/wellSets.test.ts b/shared-data/js/helpers/__tests__/wellSets.test.ts index 475d6bf545f..d96d5c405f0 100644 --- a/shared-data/js/helpers/__tests__/wellSets.test.ts +++ b/shared-data/js/helpers/__tests__/wellSets.test.ts @@ -2,8 +2,6 @@ import pipetteNameSpecsFixtures from '../../../pipette/fixtures/name/pipetteName import fixture_12_trough from '../../../labware/fixtures/2/fixture_12_trough.json' import fixture_96_plate from '../../../labware/fixtures/2/fixture_96_plate.json' import fixture_384_plate from '../../../labware/fixtures/2/fixture_384_plate.json' -import fixture_tiprack_10_ul from '../../../labware/fixtures/2/fixture_tiprack_10_ul.json' -import fixture_tiprack_300_ul from '../../../labware/fixtures/2/fixture_tiprack_300_ul.json' import fixture_overlappy_wellplate from '../../../labware/fixtures/2/fixture_overlappy_wellplate.json' import { makeWellSetHelpers } from '../wellSets' import { findWellAt } from '../getWellNamePerMultiTip' @@ -19,8 +17,6 @@ const fixture12Trough = fixture_12_trough as LabwareDefinition2 const fixture96Plate = fixture_96_plate as LabwareDefinition2 const fixture384Plate = fixture_384_plate as LabwareDefinition2 const fixtureOverlappyWellplate = fixture_overlappy_wellplate as LabwareDefinition2 -const fixtureTipRack10ul = fixture_tiprack_10_ul as LabwareDefinition2 -const fixtureTipRack300ul = fixture_tiprack_300_ul as LabwareDefinition2 const EIGHT_CHANNEL = 8 const NINETY_SIX_CHANNEL = 96 const wellsForReservoir = [ @@ -205,33 +201,23 @@ describe('canPipetteUseLabware', () => { const pipette = fixtureP10Multi const pipette96 = fixtureP100096 - expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack10ul)).toBe( - false - ) - expect( - canPipetteUseLabware(pipette96, labwareDef, fixtureTipRack10ul) - ).toBe(false) + expect(canPipetteUseLabware(pipette, labwareDef)).toBe(false) + expect(canPipetteUseLabware(pipette96, labwareDef)).toBe(false) }) it('returns true when pipette is single channel', () => { const labwareDef = fixtureOverlappyWellplate const pipette = fixtureP10Single - expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack10ul)).toBe( - true - ) + expect(canPipetteUseLabware(pipette, labwareDef)).toBe(true) }) it('returns false when the tip volume is too high with the 384 well plate', () => { const labwareDef = fixture384Plate const pipette = fixtureP10Multi const pipette96 = fixtureP100096 - expect(canPipetteUseLabware(pipette, labwareDef, fixtureTipRack300ul)).toBe( - false - ) - expect( - canPipetteUseLabware(pipette96, labwareDef, fixtureTipRack300ul) - ).toBe(false) + expect(canPipetteUseLabware(pipette, labwareDef)).toBe(false) + expect(canPipetteUseLabware(pipette96, labwareDef)).toBe(false) }) }) diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index 0af496b75ed..f01b104ebc4 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -14,12 +14,7 @@ import uniq from 'lodash/uniq' import { getWellNamePerMultiTip } from './getWellNamePerMultiTip' -import { - get96Channel384WellPlateWells, - getLabwareDefURI, - getTiprackVolume, - orderWells, -} from '.' +import { get96Channel384WellPlateWells, getLabwareDefURI, orderWells } from '.' import type { LabwareDefinition2, PipetteNameSpecs } from '../types' type WellSetByPrimaryWell = string[][] @@ -61,8 +56,7 @@ export interface WellSetHelpers { canPipetteUseLabware: ( pipetteSpec: PipetteNameSpecs, - labwareDef: LabwareDefinition2, - tiprackDef: LabwareDefinition2 + labwareDef: LabwareDefinition2 ) => boolean } @@ -137,24 +131,10 @@ export const makeWellSetHelpers = (): WellSetHelpers => { const canPipetteUseLabware = ( pipetteSpec: PipetteNameSpecs, - labwareDef: LabwareDefinition2, - tiprackDef: LabwareDefinition2 + labwareDef: LabwareDefinition2 ): boolean => { - // 384 well plates are only compatible with p50s or p20/p10s - const tiprackVolume = getTiprackVolume(tiprackDef) - const is384WellPlate = Object.keys(labwareDef.wells).length === 384 - if ( - (tiprackVolume === 200 || - tiprackVolume === 300 || - tiprackVolume === 1000) && - is384WellPlate - ) { - return false - } - if (pipetteSpec.channels === 1) { - // assume all labware except for the 384 well plate with certain volumes - // can be used by single-channel + // assume all labware can be used by single-channel return true } diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index 21674b44ee3..2d157f9c5b2 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -55,12 +55,16 @@ export const replaceTip: CommandCreator = ( ) => { const { pipette, dropTipLocation } = args const nextTiprack = getNextTiprack(pipette, invariantContext, prevRobotState) + + // TODO(jr, 10/16/23): plug in missingAdapter() error creator + if (nextTiprack == null) { // no valid next tip / tiprack, bail out return { errors: [errorCreators.insufficientTips()], } } + const pipetteSpec = invariantContext.pipetteEntities[pipette]?.spec const isFlexPipette = (pipetteSpec?.displayCategory === 'FLEX' || pipetteSpec?.channels === 96) ?? diff --git a/step-generation/src/errorCreators.ts b/step-generation/src/errorCreators.ts index bc2212a93e1..ed78da0fa47 100644 --- a/step-generation/src/errorCreators.ts +++ b/step-generation/src/errorCreators.ts @@ -12,6 +12,13 @@ export function insufficientTips(): CommandCreatorError { } } +export function missingAdapter(): CommandCreatorError { + return { + type: 'MISSING_96_CHANNEL_TIPRACK_ADAPTER', + message: 'A 96-channel cannot pick up tips fully without an adapter', + } +} + export function noTipOnPipette(args: { actionName: string pipette: string diff --git a/step-generation/src/types.ts b/step-generation/src/types.ts index 43b6b8e4833..02bb947ca0c 100644 --- a/step-generation/src/types.ts +++ b/step-generation/src/types.ts @@ -512,6 +512,7 @@ export type ErrorType = | 'HEATER_SHAKER_LATCH_CLOSED' | 'LABWARE_OFF_DECK' | 'DROP_TIP_LOCATION_DOES_NOT_EXIST' + | 'MISSING_96_CHANNEL_TIPRACK_ADAPTER' export interface CommandCreatorError { message: string From aad03f5599cd801d5e014b9352932d7978fef464 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 16 Oct 2023 14:09:25 -0400 Subject: [PATCH 11/19] add a TODO --- step-generation/src/commandCreators/atomic/replaceTip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/step-generation/src/commandCreators/atomic/replaceTip.ts b/step-generation/src/commandCreators/atomic/replaceTip.ts index 2d157f9c5b2..fb8f0a281ec 100644 --- a/step-generation/src/commandCreators/atomic/replaceTip.ts +++ b/step-generation/src/commandCreators/atomic/replaceTip.ts @@ -56,7 +56,7 @@ export const replaceTip: CommandCreator = ( const { pipette, dropTipLocation } = args const nextTiprack = getNextTiprack(pipette, invariantContext, prevRobotState) - // TODO(jr, 10/16/23): plug in missingAdapter() error creator + // TODO(jr, 10/16/23): plug in missingAdapter() error creator, need to get current tiprackId if (nextTiprack == null) { // no valid next tip / tiprack, bail out From 4f2be80b3eb4dca782935fd5dba35ac8ee5bcd10 Mon Sep 17 00:00:00 2001 From: Jethary Date: Mon, 16 Oct 2023 16:51:19 -0400 Subject: [PATCH 12/19] fix test --- shared-data/js/helpers/__tests__/wellSets.test.ts | 8 -------- 1 file changed, 8 deletions(-) diff --git a/shared-data/js/helpers/__tests__/wellSets.test.ts b/shared-data/js/helpers/__tests__/wellSets.test.ts index d96d5c405f0..a0be4f4edde 100644 --- a/shared-data/js/helpers/__tests__/wellSets.test.ts +++ b/shared-data/js/helpers/__tests__/wellSets.test.ts @@ -211,14 +211,6 @@ describe('canPipetteUseLabware', () => { expect(canPipetteUseLabware(pipette, labwareDef)).toBe(true) }) - it('returns false when the tip volume is too high with the 384 well plate', () => { - const labwareDef = fixture384Plate - const pipette = fixtureP10Multi - const pipette96 = fixtureP100096 - - expect(canPipetteUseLabware(pipette, labwareDef)).toBe(false) - expect(canPipetteUseLabware(pipette96, labwareDef)).toBe(false) - }) }) describe('getWellSetForMultichannel (integration test)', () => { From d82963bfe596e3b6ec79054ecc88a1ad73acf2b2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 18 Oct 2023 12:23:35 -0400 Subject: [PATCH 13/19] clean up LabwareSelectModal, address feeedback --- .../LabwareOverlays/SlotControls.tsx | 27 ++++----- .../__tests__/SlotControls.test.tsx | 1 - .../src/components/DeckSetup/index.tsx | 5 -- .../LabwareSelectionModal.tsx | 55 ++++++++++--------- .../__tests__/LabwareSelectionModal.test.tsx | 9 +-- .../src/utils/labwareModuleCompatibility.ts | 11 +--- 6 files changed, 46 insertions(+), 62 deletions(-) diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx index a8d5c23fc83..ca2fb3a9fdd 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/SlotControls.tsx @@ -1,17 +1,16 @@ import assert from 'assert' import * as React from 'react' -import { Icon, RobotCoordsForeignDiv } from '@opentrons/components' -import { DropTarget, DropTargetConnector, DropTargetMonitor } from 'react-dnd' -import cx from 'classnames' import { connect } from 'react-redux' import noop from 'lodash/noop' +import { DropTarget, DropTargetConnector, DropTargetMonitor } from 'react-dnd' +import cx from 'classnames' +import { Icon, RobotCoordsForeignDiv } from '@opentrons/components' import { i18n } from '../../../localization' import { DND_TYPES } from '../../../constants' import { getLabwareIsCompatible, getLabwareIsCustom, } from '../../../utils/labwareModuleCompatibility' -import { BlockedSlot } from './BlockedSlot' import { moveDeckItem, openAddLabwareModal, @@ -21,13 +20,14 @@ import { selectors as labwareDefSelectors, } from '../../../labware-defs' import { START_TERMINAL_ITEM_ID, TerminalItemId } from '../../../steplist' +import { BlockedSlot } from './BlockedSlot' -import { BaseState, DeckSlot, ThunkDispatch } from '../../../types' -import { LabwareOnDeck } from '../../../step-forms' -import { +import type { DeckSlot as DeckSlotDefinition, ModuleType, } from '@opentrons/shared-data' +import type { BaseState, DeckSlot, ThunkDispatch } from '../../../types' +import type { LabwareOnDeck } from '../../../step-forms' import styles from './LabwareOverlays.css' interface DNDP { @@ -40,7 +40,6 @@ interface DNDP { interface OP { slot: DeckSlotDefinition & { id: DeckSlot } // NOTE: Ian 2019-10-22 make slot `id` more restrictive when used in PD moduleType: ModuleType | null - has96Channel: boolean selectedTerminalItemId?: TerminalItemId | null handleDragHover?: () => unknown } @@ -69,7 +68,6 @@ export const SlotControlsComponent = ( draggedItem, itemType, customLabwareDefs, - has96Channel, } = props if ( selectedTerminalItemId !== START_TERMINAL_ITEM_ID || @@ -84,12 +82,11 @@ export const SlotControlsComponent = ( let slotBlocked: string | null = null if ( - (isOver && - moduleType != null && - draggedDef != null && - !getLabwareIsCompatible(draggedDef, moduleType) && - !isCustomLabware) || - (has96Channel && draggedDef?.parameters.isTiprack) + isOver && + moduleType != null && + draggedDef != null && + !getLabwareIsCompatible(draggedDef, moduleType) && + !isCustomLabware ) { slotBlocked = 'Labware incompatible with this module' } diff --git a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx index c7f1901cabc..b6ae460ab10 100644 --- a/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx +++ b/protocol-designer/src/components/DeckSetup/LabwareOverlays/__tests__/SlotControls.test.tsx @@ -50,7 +50,6 @@ describe('SlotControlsComponent', () => { }, itemType: DND_TYPES.LABWARE, customLabwareDefs: {}, - has96Channel: false, } getLabwareIsCompatibleSpy = jest.spyOn( diff --git a/protocol-designer/src/components/DeckSetup/index.tsx b/protocol-designer/src/components/DeckSetup/index.tsx index d0f031912b6..7e80df35cff 100644 --- a/protocol-designer/src/components/DeckSetup/index.tsx +++ b/protocol-designer/src/components/DeckSetup/index.tsx @@ -60,7 +60,6 @@ import { getDeckSetupForActiveItem } from '../../top-selectors/labware-locations import { selectors as labwareIngredSelectors } from '../../labware-ingred/selectors' import { TerminalItemId } from '../../steplist' import { getSelectedTerminalItemId } from '../../ui/steps' -import { getHas96Channel } from '../../utils' import { getRobotType } from '../../file-data/selectors' import { BrowseLabwareModal } from '../labware' import { SlotWarning } from './SlotWarning' @@ -111,8 +110,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { robotType, trashSlot, } = props - const pipettes = activeDeckSetup.pipettes - const has96Channel = getHas96Channel(pipettes) // NOTE: handling module<>labware compat when moving labware to empty module // is handled by SlotControls. // But when swapping labware when at least one is on a module, we need to be aware @@ -300,7 +297,6 @@ export const DeckSetupContents = (props: ContentsProps): JSX.Element => { !isAdapter ? ( // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier { return ( // @ts-expect-error (ce, 2021-06-21) once we upgrade to the react-dnd hooks api, and use react-redux hooks, typing this will be easier { const smallYDimension = labwareDef.dimensions.yDimension < 85.48 const irregularSize = smallXDimension && smallYDimension const adapter = labwareDef.metadata.displayCategory === 'adapter' - const adapter96Channel = + const isAdapter96Channel = labwareDef.parameters.loadName === ADAPTER_96_CHANNEL return ( (filterRecommended && @@ -207,7 +207,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { (adapter && irregularSize && !slot?.includes(HEATERSHAKER_MODULE_TYPE)) || - (adapter96Channel && !has96Channel) + (isAdapter96Channel && !has96Channel) ) }, [filterRecommended, filterHeight, getLabwareCompatible, moduleType, slot] @@ -424,7 +424,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { }) ) : ( { onClick={makeToggleCategory(adapterCompatibleLabware)} inert={false} > - {getLabwareCompatibleWithAdapter( - has96Channel ? permittedTipracks : [], - adapterLoadName - ).map((adapterDefUri, index) => { - const latestDefs = getOnlyLatestDefs() + {has96Channel + ? permittedTipracks + : getLabwareCompatibleWithAdapter(adapterLoadName).map( + (adapterDefUri, index) => { + const latestDefs = getOnlyLatestDefs() - const URIs = Object.keys(latestDefs) - const labwareDefUri = URIs.find( - defUri => defUri === adapterDefUri - ) - const labwareDef = labwareDefUri - ? latestDefs[labwareDefUri] - : null + const URIs = Object.keys(latestDefs) + const labwareDefUri = URIs.find( + defUri => defUri === adapterDefUri + ) + const labwareDef = labwareDefUri + ? latestDefs[labwareDefUri] + : null - return labwareDef != null ? ( - setPreviewedLabware(labwareDef)} - // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) - onMouseLeave={() => setPreviewedLabware()} - /> - ) : null - })} + return labwareDef != null ? ( + setPreviewedLabware(labwareDef)} + // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) + onMouseLeave={() => setPreviewedLabware()} + /> + ) : null + } + )} )} diff --git a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx index fd286a9d327..2a1232f6d06 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/__tests__/LabwareSelectionModal.test.tsx @@ -61,17 +61,14 @@ describe('LabwareSelectionModal', () => { MAX_LABWARE_HEIGHT_EAST_WEST_HEATER_SHAKER_MM ) }) - it('should display only permitted tipracks if has', () => { + it('should display only permitted tipracks if the 96-channel is attached', () => { const mockPermittedTipracks = ['mockPermittedTip', 'mockPermittedTip2'] props.slot = 'A2' props.has96Channel = true props.adapterLoadName = 'mockLoadName' props.permittedTipracks = mockPermittedTipracks - const { getByText } = render(props) + const { getByText, getAllByRole } = render(props) getByText(nestedTextMatcher('adapter compatible labware')).click() - expect(mockGetLabwareCompatibleWithAdapter).toHaveBeenCalledWith( - mockPermittedTipracks, - props.adapterLoadName - ) + expect(getAllByRole('list', { name: '' })).toHaveLength(2) }) }) diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index 05986f9f9b5..cdabe67c7a7 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -130,16 +130,11 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< } export const getLabwareCompatibleWithAdapter = ( - permittedTipracks: string[], adapterLoadName?: string ): string[] => { - if (permittedTipracks.length > 0 && adapterLoadName === ADAPTER_96_CHANNEL) { - return permittedTipracks - } else { - return adapterLoadName != null - ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] - : [] - } + return adapterLoadName != null + ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] + : [] } export const getLabwareIsCustom = ( customLabwares: LabwareDefByDefURI, From 6ed4076dadffdf2f048bae466fd9c820f08f1205 Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 18 Oct 2023 12:41:01 -0400 Subject: [PATCH 14/19] clean up LabwareSelectionModal logic --- .../LabwareSelectionModal.tsx | 45 ++++++++++--------- 1 file changed, 25 insertions(+), 20 deletions(-) diff --git a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx index 75f68d35bdf..1787d1d22a9 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx @@ -126,6 +126,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { has96Channel, } = props const defs = getOnlyLatestDefs() + const URIs = Object.keys(defs) const [selectedCategory, setSelectedCategory] = React.useState( null ) @@ -233,6 +234,21 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { return `Slot ${slot} Labware` } + const getLabwareAdapterItem = (index: number, labwareDefUri?: string) => { + const labwareDef = labwareDefUri != null ? defs[labwareDefUri] : null + return labwareDef != null ? ( + setPreviewedLabware(labwareDef)} + // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) + onMouseLeave={() => setPreviewedLabware()} + /> + ) : null + } + const customLabwareURIs: string[] = React.useMemo( () => Object.keys(customLabwareDefs), [customLabwareDefs] @@ -347,6 +363,7 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { moduleCompatibility = 'notCompatible' } } + return ( <> @@ -432,31 +449,19 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { onClick={makeToggleCategory(adapterCompatibleLabware)} inert={false} > - {has96Channel - ? permittedTipracks + {has96Channel && adapterLoadName === ADAPTER_96_CHANNEL + ? permittedTipracks.map((tiprackDefUri, index) => { + const labwareDefUri = URIs.find( + defUri => defUri === tiprackDefUri + ) + return getLabwareAdapterItem(index, labwareDefUri) + }) : getLabwareCompatibleWithAdapter(adapterLoadName).map( (adapterDefUri, index) => { - const latestDefs = getOnlyLatestDefs() - - const URIs = Object.keys(latestDefs) const labwareDefUri = URIs.find( defUri => defUri === adapterDefUri ) - const labwareDef = labwareDefUri - ? latestDefs[labwareDefUri] - : null - - return labwareDef != null ? ( - setPreviewedLabware(labwareDef)} - // @ts-expect-error(sa, 2021-6-22): setPreviewedLabware expects an argument (even if nullsy) - onMouseLeave={() => setPreviewedLabware()} - /> - ) : null + return getLabwareAdapterItem(index, labwareDefUri) } )} From 9450a525c6ce9b2b525c93997fdb620874c9a7da Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 18 Oct 2023 13:21:11 -0400 Subject: [PATCH 15/19] fix test --- .../LabwareSelectionModal.tsx | 5 ++++- .../__tests__/LabwareSelectionModal.test.tsx | 14 +++++++++----- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx index 1787d1d22a9..abf692ecda6 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx +++ b/protocol-designer/src/components/LabwareSelectionModal/LabwareSelectionModal.tsx @@ -234,7 +234,10 @@ export const LabwareSelectionModal = (props: Props): JSX.Element | null => { return `Slot ${slot} Labware` } - const getLabwareAdapterItem = (index: number, labwareDefUri?: string) => { + const getLabwareAdapterItem = ( + index: number, + labwareDefUri?: string + ): JSX.Element | null => { const labwareDef = labwareDefUri != null ? defs[labwareDefUri] : null return labwareDef != null ? ( { ) }) it('should display only permitted tipracks if the 96-channel is attached', () => { - const mockPermittedTipracks = ['mockPermittedTip', 'mockPermittedTip2'] + const mockTipUri = 'fixture/fixture_tiprack_1000_ul/1' + const mockPermittedTipracks = [mockTipUri] props.slot = 'A2' props.has96Channel = true - props.adapterLoadName = 'mockLoadName' + props.adapterLoadName = ADAPTER_96_CHANNEL props.permittedTipracks = mockPermittedTipracks - const { getByText, getAllByRole } = render(props) + const { getByText } = render(props) getByText(nestedTextMatcher('adapter compatible labware')).click() - expect(getAllByRole('list', { name: '' })).toHaveLength(2) + getByText('Opentrons GEB 1000uL Tiprack') }) }) From 66aaf272cec3c8766abe491fea72e7dbcc09d1c2 Mon Sep 17 00:00:00 2001 From: Jethary Date: Thu, 19 Oct 2023 15:31:03 -0400 Subject: [PATCH 16/19] address various comments and clean up --- .../components/LabwareSelectionModal/index.ts | 7 +---- .../CreateFileWizard/PipetteTipsTile.tsx | 9 +----- .../FilePipettesModal/PipetteFields.tsx | 9 +----- protocol-designer/src/labware-defs/actions.ts | 31 +++++++------------ .../src/top-selectors/substep-highlight.ts | 6 ++-- protocol-designer/src/utils/index.ts | 2 +- .../src/utils/labwareModuleCompatibility.ts | 6 ++-- .../helpers/get96Channel384WellPlateWells.ts | 2 ++ shared-data/js/helpers/wellSets.ts | 6 ++-- 9 files changed, 25 insertions(+), 53 deletions(-) diff --git a/protocol-designer/src/components/LabwareSelectionModal/index.ts b/protocol-designer/src/components/LabwareSelectionModal/index.ts index efb47e7f66e..1c6adf48159 100644 --- a/protocol-designer/src/components/LabwareSelectionModal/index.ts +++ b/protocol-designer/src/components/LabwareSelectionModal/index.ts @@ -85,12 +85,7 @@ function mergeProps( dispatch(closeLabwareSelector()) }, onUploadLabware: fileChangeEvent => - dispatch( - labwareDefActions.createCustomLabwareDef( - fileChangeEvent, - stateProps.has96Channel - ) - ), + dispatch(labwareDefActions.createCustomLabwareDef(fileChangeEvent)), selectLabware: labwareDefURI => { if (stateProps.slot) { dispatch( diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx index ae119d476b6..8c2d30ce8bd 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx @@ -237,14 +237,7 @@ function PipetteTipsField(props: PipetteTipsFieldProps): JSX.Element | null { - dispatch( - createCustomTiprackDef( - e, - values.pipettesByMount[LEFT].pipetteName === 'p1000_96' - ) - ) - } + onChange={e => dispatch(createCustomTiprackDef(e))} /> diff --git a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx index 166675dcd9c..917253fb288 100644 --- a/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx +++ b/protocol-designer/src/components/modals/FilePipettesModal/PipetteFields.tsx @@ -232,14 +232,7 @@ export function PipetteFields(props: Props): JSX.Element { {i18n.t('button.upload_custom_tip_rack')} - dispatch( - createCustomTiprackDef( - e, - values.left.pipetteName === 'p1000_96' - ) - ) - } + onChange={e => dispatch(createCustomTiprackDef(e))} /> diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index 9df5e129dc7..9a33e7c8452 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -12,9 +12,10 @@ import { LabwareDefinition2, } from '@opentrons/shared-data' import * as labwareDefSelectors from './selectors' -import { getAllWellSetsForLabware } from '../utils' +import { getAllWellSetsForLabware, getHas96Channel } from '../utils' import type { ThunkAction } from '../types' import type { LabwareUploadMessage } from './types' +import { getPipetteEntities } from '../step-forms/selectors' export interface LabwareUploadMessageAction { type: 'LABWARE_UPLOAD_MESSAGE' payload: LabwareUploadMessage @@ -89,15 +90,15 @@ const getIsOverwriteMismatched = ( } const _createCustomLabwareDef: ( - onlyTiprack: boolean, - has96Channel: boolean -) => (event: React.SyntheticEvent) => ThunkAction = ( - onlyTiprack, - has96Channel -) => event => (dispatch, getState) => { + onlyTiprack: boolean +) => ( + event: React.SyntheticEvent +) => ThunkAction = onlyTiprack => event => (dispatch, getState) => { const allLabwareDefs: LabwareDefinition2[] = values( labwareDefSelectors.getLabwareDefsByURI(getState()) ) + const pipetteEntities = getPipetteEntities(getState()) + const has96Channel = getHas96Channel(pipetteEntities) const customLabwareDefs: LabwareDefinition2[] = values( labwareDefSelectors.getCustomLabwareDefsByURI(getState()) ) @@ -245,20 +246,12 @@ const _createCustomLabwareDef: ( } export const createCustomLabwareDef: ( - event: React.SyntheticEvent, - has96Channel: boolean -) => (event: React.SyntheticEvent) => ThunkAction = ( - event, - has96Channel -) => _createCustomLabwareDef(false, has96Channel) + event: React.SyntheticEvent +) => ThunkAction = _createCustomLabwareDef(false) export const createCustomTiprackDef: ( - event: React.SyntheticEvent, - has96Channel: boolean -) => (event: React.SyntheticEvent) => ThunkAction = ( - event, - has96Channel -) => _createCustomLabwareDef(true, has96Channel) + event: React.SyntheticEvent +) => ThunkAction = _createCustomLabwareDef(true) interface DismissLabwareUploadMessage { type: 'DISMISS_LABWARE_UPLOAD_MESSAGE' diff --git a/protocol-designer/src/top-selectors/substep-highlight.ts b/protocol-designer/src/top-selectors/substep-highlight.ts index 4169f31f97f..339c3a23326 100644 --- a/protocol-designer/src/top-selectors/substep-highlight.ts +++ b/protocol-designer/src/top-selectors/substep-highlight.ts @@ -12,17 +12,15 @@ import { PipetteEntity, LabwareEntity } from '@opentrons/step-generation' import { Selector } from '../types' import { SubstepItemData } from '../steplist/types' -type MultiChannels = 8 | 96 - function _wellsForPipette( pipetteEntity: PipetteEntity, labwareEntity: LabwareEntity, wells: string[] ): string[] { + const channels = pipetteEntity.spec.channels // `wells` is all the wells that pipette's channel 1 interacts with. - if (pipetteEntity.spec.channels === 8 || pipetteEntity.spec.channels === 96) { + if (channels === 8 || channels === 96) { return wells.reduce((acc: string[], well: string) => { - const channels = pipetteEntity.spec.channels as MultiChannels const setOfWellsForMulti = getWellNamePerMultiTip( labwareEntity.def, well, diff --git a/protocol-designer/src/utils/index.ts b/protocol-designer/src/utils/index.ts index 9e50c5e2fec..210016ffa45 100644 --- a/protocol-designer/src/utils/index.ts +++ b/protocol-designer/src/utils/index.ts @@ -130,5 +130,5 @@ export const getStagingAreaSlots = ( } export const getHas96Channel = (pipettes: PipetteEntities): boolean => { - return Object.values(pipettes).some(pip => pip.name === 'p1000_96') + return Object.values(pipettes).some(pip => pip.spec.channels === 96) } diff --git a/protocol-designer/src/utils/labwareModuleCompatibility.ts b/protocol-designer/src/utils/labwareModuleCompatibility.ts index cdabe67c7a7..159b285ba87 100644 --- a/protocol-designer/src/utils/labwareModuleCompatibility.ts +++ b/protocol-designer/src/utils/labwareModuleCompatibility.ts @@ -131,11 +131,11 @@ export const COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER: Record< export const getLabwareCompatibleWithAdapter = ( adapterLoadName?: string -): string[] => { - return adapterLoadName != null +): string[] => + adapterLoadName != null ? COMPATIBLE_LABWARE_ALLOWLIST_FOR_ADAPTER[adapterLoadName] : [] -} + export const getLabwareIsCustom = ( customLabwares: LabwareDefByDefURI, labwareOnDeck: LabwareOnDeck diff --git a/shared-data/js/helpers/get96Channel384WellPlateWells.ts b/shared-data/js/helpers/get96Channel384WellPlateWells.ts index 96b4b4a1b88..c66ae6c1301 100644 --- a/shared-data/js/helpers/get96Channel384WellPlateWells.ts +++ b/shared-data/js/helpers/get96Channel384WellPlateWells.ts @@ -1,3 +1,5 @@ +// TODO(jr, 10/19/23): should extend getWellNamePerMultiTip to use similar math for 96-channel and 384 well plates +// instead of special casing using this util export function get96Channel384WellPlateWells( all384Wells: string[], well: string diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index f01b104ebc4..ff62e626fe9 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -28,13 +28,11 @@ function _getAllWellSetsForLabware( return allWells.reduce( (acc: WellSetByPrimaryWell, well: string): WellSetByPrimaryWell => { - const wellSet = getWellNamePerMultiTip(labwareDef, well, channels) + const wellSet = getWellNamePerMultiTip(labwareDef, well, 8) if (wellSet === null) { return acc - } else if (channels === 8) { - return [...acc, wellSet] } else { - return [wellSet] + return [...acc, wellSet] } }, [] From e0af8383b4c0bb51057fec32fcd53213cb7a8a33 Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 24 Oct 2023 22:00:15 -0400 Subject: [PATCH 17/19] address some more comments --- .../modals/CreateFileWizard/PipetteTipsTile.tsx | 2 +- protocol-designer/src/labware-defs/actions.ts | 16 ++++++++-------- shared-data/js/helpers/wellSets.ts | 15 +++++---------- 3 files changed, 14 insertions(+), 19 deletions(-) diff --git a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx index 8c2d30ce8bd..5e49aa18b3b 100644 --- a/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx +++ b/protocol-designer/src/components/modals/CreateFileWizard/PipetteTipsTile.tsx @@ -23,7 +23,7 @@ import { Btn, JUSTIFY_END, } from '@opentrons/components' -import { getPipetteNameSpecs, LEFT } from '@opentrons/shared-data' +import { getPipetteNameSpecs } from '@opentrons/shared-data' import { i18n } from '../../../localization' import { getLabwareDefsByURI } from '../../../labware-defs/selectors' import { createCustomTiprackDef } from '../../../labware-defs/actions' diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index 9a33e7c8452..cd240bbbbe5 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -11,11 +11,11 @@ import { OPENTRONS_LABWARE_NAMESPACE, LabwareDefinition2, } from '@opentrons/shared-data' +import { getPipetteEntities } from '../step-forms/selectors' +import { getAllWellSetsForLabware } from '../utils' import * as labwareDefSelectors from './selectors' -import { getAllWellSetsForLabware, getHas96Channel } from '../utils' import type { ThunkAction } from '../types' import type { LabwareUploadMessage } from './types' -import { getPipetteEntities } from '../step-forms/selectors' export interface LabwareUploadMessageAction { type: 'LABWARE_UPLOAD_MESSAGE' payload: LabwareUploadMessage @@ -94,14 +94,14 @@ const _createCustomLabwareDef: ( ) => ( event: React.SyntheticEvent ) => ThunkAction = onlyTiprack => event => (dispatch, getState) => { - const allLabwareDefs: LabwareDefinition2[] = values( - labwareDefSelectors.getLabwareDefsByURI(getState()) - ) - const pipetteEntities = getPipetteEntities(getState()) - const has96Channel = getHas96Channel(pipetteEntities) - const customLabwareDefs: LabwareDefinition2[] = values( + const customLabwareDefs = values( labwareDefSelectors.getCustomLabwareDefsByURI(getState()) ) + const allLabwareDefs = values( + labwareDefSelectors.getLabwareDefsByURI(getState()) + ) + const pipetteEntities = values(getPipetteEntities(getState())) + const has96Channel = pipetteEntities.some(pip => pip.spec.channels === 96) // @ts-expect-error(sa, 2021-6-20): null check const file = event.currentTarget.files[0] const reader = new FileReader() diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index ff62e626fe9..d05f486c334 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -21,8 +21,7 @@ type WellSetByPrimaryWell = string[][] // Compute all well sets for a labware def (non-memoized) function _getAllWellSetsForLabware( - labwareDef: LabwareDefinition2, - channels: 8 | 96 + labwareDef: LabwareDefinition2 ): WellSetByPrimaryWell { const allWells: string[] = Object.keys(labwareDef.wells) @@ -67,8 +66,7 @@ export const makeWellSetHelpers = (): WellSetHelpers => { }> = {} const getAllWellSetsForLabware = ( - labwareDef: LabwareDefinition2, - channels: 8 | 96 + labwareDef: LabwareDefinition2 ): WellSetByPrimaryWell => { const labwareDefURI = getLabwareDefURI(labwareDef) const c = cache[labwareDefURI] @@ -79,7 +77,7 @@ export const makeWellSetHelpers = (): WellSetHelpers => { return c.wellSetByPrimaryWell } - const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef, channels) + const wellSetByPrimaryWell = _getAllWellSetsForLabware(labwareDef) cache[labwareDefURI] = { labwareDef, @@ -98,10 +96,7 @@ export const makeWellSetHelpers = (): WellSetHelpers => { * Ie: C2 for 96-flat => ['A2', 'B2', 'C2', ... 'H2'] * Or A1 for trough => ['A1', 'A1', 'A1', ...] **/ - const allWellSetsFor8Channel = getAllWellSetsForLabware( - labwareDef, - channels - ) + const allWellSetsFor8Channel = getAllWellSetsForLabware(labwareDef) /** getting all wells from the plate and turning into 1D array for 96-channel */ const orderedWellsFor96Channel = orderWells( @@ -136,7 +131,7 @@ export const makeWellSetHelpers = (): WellSetHelpers => { return true } - const allWellSets = getAllWellSetsForLabware(labwareDef, 8) + const allWellSets = getAllWellSetsForLabware(labwareDef) return allWellSets.some(wellSet => { const uniqueWells = uniq(wellSet) // if all wells are non-null, and there are either 1 (reservoir-like) From 5a01be974d3611b3fca28a297dc48dcb6a39100a Mon Sep 17 00:00:00 2001 From: Jethary Date: Tue, 24 Oct 2023 22:02:44 -0400 Subject: [PATCH 18/19] clean up nested ternery --- .../top-selectors/labware-locations/index.ts | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/protocol-designer/src/top-selectors/labware-locations/index.ts b/protocol-designer/src/top-selectors/labware-locations/index.ts index 2167acdb8ac..5db5f8f7509 100644 --- a/protocol-designer/src/top-selectors/labware-locations/index.ts +++ b/protocol-designer/src/top-selectors/labware-locations/index.ts @@ -142,6 +142,12 @@ export const getUnocuppiedLabwareLocationOptions: Selector< const modSlot = modIdWithAdapter != null ? modules[modIdWithAdapter].slot : null const isAdapter = getIsAdapter(labwareId, labwareEntities) + const moduleUnderAdapter = + modIdWithAdapter != null + ? getModuleDisplayName(moduleEntities[modIdWithAdapter].model) + : 'unknown module' + const moduleSlotInfo = modSlot ?? 'unknown slot' + const adapterSlotInfo = adapterSlot ?? 'unknown adapter' return labwareOnAdapter == null && isAdapter ? [ @@ -149,16 +155,8 @@ export const getUnocuppiedLabwareLocationOptions: Selector< { name: modIdWithAdapter != null - ? `${adapterDisplayName} on top of ${ - modIdWithAdapter != null - ? getModuleDisplayName( - moduleEntities[modIdWithAdapter].model - ) - : 'unknown module' - } in slot ${modSlot ?? 'unknown slot'}` - : `${adapterDisplayName} on slot ${ - adapterSlot ?? 'unknown' - }`, + ? `${adapterDisplayName} on top of ${moduleUnderAdapter} in slot ${moduleSlotInfo}` + : `${adapterDisplayName} on slot ${adapterSlotInfo}`, value: labwareId, }, ] From c737bb225843a11ce348bc6c28e6bee004d522dc Mon Sep 17 00:00:00 2001 From: Jethary Date: Wed, 25 Oct 2023 08:28:04 -0400 Subject: [PATCH 19/19] clean up props in createCustomLabwareDef --- protocol-designer/src/labware-defs/actions.ts | 13 ++++--------- shared-data/js/helpers/wellSets.ts | 3 +-- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/protocol-designer/src/labware-defs/actions.ts b/protocol-designer/src/labware-defs/actions.ts index cd240bbbbe5..b782a9795b7 100644 --- a/protocol-designer/src/labware-defs/actions.ts +++ b/protocol-designer/src/labware-defs/actions.ts @@ -11,7 +11,6 @@ import { OPENTRONS_LABWARE_NAMESPACE, LabwareDefinition2, } from '@opentrons/shared-data' -import { getPipetteEntities } from '../step-forms/selectors' import { getAllWellSetsForLabware } from '../utils' import * as labwareDefSelectors from './selectors' import type { ThunkAction } from '../types' @@ -76,15 +75,14 @@ const _labwareDefsMatchingDisplayName = ( const getIsOverwriteMismatched = ( newDef: LabwareDefinition2, - overwrittenDef: LabwareDefinition2, - channel: 8 | 96 + overwrittenDef: LabwareDefinition2 ): boolean => { const matchedWellOrdering = isEqual(newDef.ordering, overwrittenDef.ordering) const matchedMultiUse = matchedWellOrdering && isEqual( - getAllWellSetsForLabware(newDef, channel), - getAllWellSetsForLabware(overwrittenDef, channel) + getAllWellSetsForLabware(newDef), + getAllWellSetsForLabware(overwrittenDef) ) return !(matchedWellOrdering && matchedMultiUse) } @@ -100,8 +98,6 @@ const _createCustomLabwareDef: ( const allLabwareDefs = values( labwareDefSelectors.getLabwareDefsByURI(getState()) ) - const pipetteEntities = values(getPipetteEntities(getState())) - const has96Channel = pipetteEntities.some(pip => pip.spec.channels === 96) // @ts-expect-error(sa, 2021-6-20): null check const file = event.currentTarget.files[0] const reader = new FileReader() @@ -202,8 +198,7 @@ const _createCustomLabwareDef: ( isOverwriteMismatched: getIsOverwriteMismatched( // @ts-expect-error(sa, 2021-6-20): parsedLabwareDef might be nullsy parsedLabwareDef, - matchingDefs[0], - has96Channel ? 96 : 8 + matchingDefs[0] ), }) ) diff --git a/shared-data/js/helpers/wellSets.ts b/shared-data/js/helpers/wellSets.ts index d05f486c334..904e5cc019e 100644 --- a/shared-data/js/helpers/wellSets.ts +++ b/shared-data/js/helpers/wellSets.ts @@ -41,8 +41,7 @@ function _getAllWellSetsForLabware( // creates memoized getAllWellSetsForLabware + getWellSetForMultichannel fns. export interface WellSetHelpers { getAllWellSetsForLabware: ( - labwareDef: LabwareDefinition2, - channels: 8 | 96 + labwareDef: LabwareDefinition2 ) => WellSetByPrimaryWell getWellSetForMultichannel: (